/*
Project Name: SPIKE Prime Web Interface
File name: ServiceDock_SPIKE.js
Author: Jeremy Jung
Last update: 11/5/2020
Description: HTML Element definition for <service-spike> to be used in ServiceDocks
Credits/inspirations:
History:
Created by Jeremy on 7/16/20
Fixed baudRate by Teddy on 10/11/20
LICENSE: MIT
(C) Tufts Center for Engineering Education and Outreach (CEEO)
TODO:
include bluetooth_button and main_button in PrimeHub() SPIKE APP functions
Remove all instances of getPortsInfo in example codes
implement get_color
*/
// import { Service_SPIKE } from "./Service_SPIKE.js";
class servicespike extends HTMLElement {
constructor () {
super();
var active = false; // whether the service was activated
this.service = new Service_SPIKE(); // instantiate a service object ( one object per button )
this.service.executeAfterDisconnect(function () {
active = false;
status.style.backgroundColor = "red";
})
// Create a shadow root
var shadow = this.attachShadow({ mode: 'open' });
/* wrapper definition and CSS */
var wrapper = document.createElement('div');
wrapper.setAttribute('class', 'wrapper');
wrapper.setAttribute("style", "width: 50px; height: 50px; position: relative; margin-top: 10px;")
/* ServiceDock button definition and CSS */
var button = document.createElement("button");
button.setAttribute("id", "sl_button");
button.setAttribute("class", "SD_button");
//var imageRelPath = "./modules/views/SPIKE_button.png" // relative to the document in which a servicespike is created ( NOT this file )
var length = 50; // for width and height of button
var buttonBackgroundColor = "#A2E1EF" // background color of the button
var buttonStyle = "width:" + length + "px; height:" + length + "px; background:" + "url('data:image/png;base64,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')" + " no-repeat; background-size: 50px 50px; background-color:" + buttonBackgroundColor
+ "; border: none; background-position: center; cursor: pointer; border-radius: 10px; position: relative; margin: 4px 0px; "
button.setAttribute("style", buttonStyle);
/* status circle definition and CSS */
var status = document.createElement("div");
status.setAttribute("class", "status");
var length = 20; // for width and height of circle
var statusBackgroundColor = "red" // default background color of service (inactive color)
var posLeft = 30;
var posTop = 20;
var statusStyle = "border-radius: 50%; height:" + length + "px; width:" + length + "px; background-color:" + statusBackgroundColor +
"; position: relative; left:" + posLeft + "px; top:" + posTop + "px;";
status.setAttribute("style", statusStyle);
/* event listeners */
button.addEventListener("mouseleave", function (event) {
button.style.backgroundColor = "#A2E1EF";
button.style.color = "#000000";
});
button.addEventListener("mouseenter", function (event) {
button.style.backgroundColor = "#FFFFFF";
button.style.color = "#000000";
})
// when ServiceDock button is double clicked
this.addEventListener("click", async function () {
// check active flag so once activated, the service doesnt reinit
if (!active) {
if ('serial' in navigator) {
console.log("%cTuftsCEEO ", "color: #3ba336;", "Activating SPIKE Service");
var initSuccessful = await this.service.init();
if (initSuccessful) {
active = true;
status.style.backgroundColor = "green";
}
}
else {
var bodyTags = document.getElementsByTagName("body");
if (bodyTags != undefined) {
var bodyTag = document.getElementsByTagName("body")[0];
bodyTag.innerHTML = `
<div>
<h1>
To use the ServiceDock's LEGO SPIKE Prime Service, you must enable the <em><b>WebSerial API</b></em> in your
browser. To do so, please
make sure:
</h1>
<h3>
<ol style = "font-size: 20px">
<li>You are using the
<a id = "googlechromelink" href="https://www.google.com/chrome/" target="_blank">
Google Chrome browser</a>.</li>
<br/>
<li>The following chrome flags are enabled on chrome://flags.</li>
</ol>
<ul>
<li>Mac OSX user? #enable-experimental-web-platform-features</li>
<li>Windows user? #enable-experimental-web-platform-features AND #new-usb-backend</li>
</ul>
</h3>
<h2>
To enable these flags:
</h2>
<h3>
<ol style="font-size: 20px;">
<li>In your Browser URL, visit
<em>chrome://flags</em></li>
<br/>
<li> Set the your required flags to "Enabled" via dropdown</li>
<br/>
<li> Relaunch the browser to have changes take effect </li>
<br/>
<li> Revisit your Coding Rooms classroom (this website) </li>
<br/>
</ol>
</h3>
`;
}
else {
alert("Error: Please make sure you are using GOOGLE CHROME with the #enable-experimental-web-platform-features flag ENABLED")
}
}
} else {
this.service.rebootHub();
}
});
shadow.appendChild(wrapper);
button.appendChild(status);
wrapper.appendChild(button);
}
/* get the Service_SPIKE object */
getService() {
return this.service;
}
/* get whether the ServiceDock button was clicked */
getClicked() {
return this.active;
}
}
// when defining custom element, the name must have at least one - dash
window.customElements.define('service-spike', servicespike);
/*
Project Name: SPIKE Prime Web Interface
File name: Service_SPIKE.js
Author: Jeremy Jung
Last update: 7/22/20
Description: SPIKE Service Library (OOP)
Credits/inspirations:
Based on code wrriten by Ethan Danahy, Chris Rogers
History:
Created by Jeremy on 7/15/20
LICENSE: MIT
(C) Tufts Center for Engineering Education and Outreach (CEEO)
*/
/**
* @class Service_SPIKE
* @classdesc
* ServiceDock library for interfacing with LEGO® SPIKE™ Prime
* @example
* // assuming you declared <service-spike> with the id, "service_spike"
* var serviceSPIKE = document.getElemenyById("service_spike").getService();
* serviceSPIKE.executeAfterInit(async function() {
* // write code here
* })
*
* serviceSPIKE.init();
*/
function Service_SPIKE() {
//////////////////////////////////////////
// //
// Global Variables //
// //
//////////////////////////////////////////
/* private members */
const VENDOR_ID = 0x0694; // LEGO SPIKE Prime Hub
// common characters to send (for REPL/uPython on the Hub)
const CONTROL_C = '\x03'; // CTRL-C character (ETX character)
const CONTROL_D = '\x04'; // CTRL-D character (EOT character)
const RETURN = '\x0D'; // RETURN key (enter, new line)
/* using this filter in webserial setup will only take serial ports*/
const filter = {
usbVendorId: VENDOR_ID
};
// define for communication
let port;
let reader;
let writer;
let value;
let done;
let writableStreamClosed;
//define for json concatenation
let jsonline = "";
// contains latest full json object from SPIKE readings
let lastUJSONRPC;
// object containing real-time info on devices connected to each port of SPIKE Prime
let ports =
{
"A": { "device": "none", "data": {} },
"B": { "device": "none", "data": {} },
"C": { "device": "none", "data": {} },
"D": { "device": "none", "data": {} },
"E": { "device": "none", "data": {} },
"F": { "device": "none", "data": {} }
};
// object containing real-time info on hub sensor values
/*
!say the usb wire is the nose of the spike prime
( looks at which side of the hub is facing up)
gyro[0] - up/down detector ( down: 1000, up: -1000, neutral: 0)
gyro[1] - rightside/leftside detector ( leftside : 1000 , rightside: -1000, neutal: 0 )
gyro[2] - front/back detector ( front: 1000, back: -1000, neutral: 0 )
( assume the usb wire port is the nose of the spike prime )
accel[0] - roll acceleration (roll to right: -, roll to left: +)
accel[1] - pitch acceleration (up: +, down: -)
accel[2] - yaw acceleration (counterclockwise: +. clockwise: -)
()
pos[0] - yaw angle
pos[1] - pitch angle
pos[2] - roll angle
*/
let hub =
{
"gyro": [0, 0, 0],
"accel": [0, 0, 0],
"pos": [0, 0, 0]
}
let batteryAmount = 0; // battery [0-100]
// string containing real-time info on hub events
let hubFrontEvent;
/*
up: hub is upright/standing, with the display looking horizontally
down: hub is upsidedown with the display, with the display looking horizontally
front: hub's display facing towards the sky
back: hub's display facing towards the earth
leftside: hub rotated so that the side to the left of the display is facing the earth
rightside: hub rotated so that the side to the right of the display is facing the earth
*/
let lastHubOrientation; //PrimeHub orientation read from caught UJSONRPC
/*
shake
freefall
*/
let hubGesture;
//
let hubMainButton = { "pressed": false, "duration": 0 };
let hubBluetoothButton = { "pressed": false, "duration": 0 };
let hubLeftButton = { "pressed": false, "duration": 0 };
let hubRightButton = { "pressed": false, "duration": 0 };
/* PrimeHub data storage arrays for was_***() functions */
let hubGestures = []; // array of hubGestures run since program started or since was_gesture() ran
let hubButtonPresses = [];
let hubName = undefined;
let lastDetectedColor = undefined;
/* SPIKE Prime Projects */
let hubProjects = {
"0": "None",
"1": "None",
"2": "None",
"3": "None",
"4": "None",
"5": "None",
"6": "None",
"7": "None",
"8": "None",
"9": "None",
"10": "None",
"11": "None",
"12": "None",
"13": "None",
"14": "None",
"15": "None",
"16": "None",
"17": "None",
"18": "None",
"19": "None"
};
var colorDictionary = {
0: "BLACK",
1: "VIOLET",
3: "BLUE",
4: "AZURE",
5: "GREEN",
7: "YELLOW",
9: "RED",
1: "WHITE",
};
// true after Force Sensor is pressed, turned to false after reading it for the first time that it is released
let ForceSensorWasPressed = false;
var micropython_interpreter = false; // whether micropython was reached or not
let serviceActive = false; //serviceActive flag
var waitForNewOriFirst = true; //whether the wait_for_new_orientation method would be the first time called
/* stored callback functions from wait_until functions and etc. */
var funcAtInit = undefined; // function to call after init of SPIKE Service
var funcAfterNewGesture = undefined;
var funcAfterNewOrientation = undefined;
var funcAfterLeftButtonPress = undefined;
var funcAfterLeftButtonRelease = undefined;
var funcAfterRightButtonPress = undefined;
var funcAfterRightButtonRelease = undefined;
var funcAfterNewColor = undefined;
var waitUntilColorCallback = undefined; // [colorToDetect, function to execute]
var waitForDistanceFartherThanCallback = undefined; // [distance, function to execute]
var waitForDistanceCloserThanCallback = undefined; // [distance, function to execute]
var funcAfterForceSensorPress = undefined;
var funcAfterForceSensorRelease = undefined;
/* array that holds the pointers to callback functions to be executed after a UJSONRPC response */
var responseCallbacks = [];
// array of information needed for writing program
var startWriteProgramCallback = undefined; // [message_id, function to execute ]
var writePackageInformation = undefined; // [ message_id, remaining_data, transfer_id, blocksize]
var writeProgramCallback = undefined; // callback function to run after a program was successfully written
var writeProgramSetTimeout = undefined; // setTimeout object for looking for response to start_write_program
/* callback functions added for Coding Rooms */
var getFirmwareInfoCallback = undefined;
var funcAfterPrint = undefined; // function to call for SPIKE python program print statements or errors
var funcAfterError = undefined; // function to call for errors in ServiceDock
var funcAfterDisconnect = undefined; // function to call after SPIKE Prime is disconnected
var funcWithStream = undefined; // function to call after every parsed UJSONRPC package
var triggerCurrentStateCallback = undefined;
//////////////////////////////////////////
// //
// Public Functions //
// //
//////////////////////////////////////////
/** initialize SPIKE_service
* <p> Makes prompt in Google Chrome ( Google Chrome Browser needs "Experimental Web Interface" enabled) </p>
* <p> Starts streaming UJSONRPC </p>
* <p> <em> this function needs to be executed after executeAfterInit but before all other public functions </em> </p>
* @public
* @returns {boolean} True if service was successsfully initialized, false otherwise
*/
async function init() {
// reinit variables in the case of hardware disconnection and Service reactivation
reader = undefined;
writer = undefined;
// initialize web serial connection
var webSerialConnected = await initWebSerial();
if (webSerialConnected) {
// start streaming UJSONRPC
streamUJSONRPC();
await sleep(1000);
triggerCurrentState();
getFirmwareInfo( function (version) {
console.log("%cTuftsCEEO ", "color: #3ba336;", "This SPIKE Prime is using Hub OS ", version);
});
serviceActive = true;
await sleep(2000); // wait for service to init
// call funcAtInit if defined
if (funcAtInit !== undefined) {
funcAtInit();
}
return true;
}
else {
return false;
}
}
/** Get the callback function to execute after service is initialized.
* <p> <em> This function needs to be executed before calling init() </em> </p>
* @public
* @param {function} callback Function to execute after initialization ( during init() )
* @example
* serviceSPIKE.executeAfterInit( function () {
* var motor = serviceSPIKE.Motor("A");
* var speed = motor.get_speed();
* // do something with speed
* })
*/
function executeAfterInit(callback) {
// Assigns global variable funcAtInit a pointer to callback function
funcAtInit = callback;
}
/** Get the callback function to execute after a print or error from SPIKE python program
* @ignore
* @param {function} callback
*/
function executeAfterPrint(callback) {
funcAfterPrint = callback;
}
/** Get the callback function to execute after Service Dock encounters an error
* @ignore
* @param {any} callback
*/
function executeAfterError(callback) {
funcAfterError = callback;
}
/** Execute a stack of functions continuously with SPIKE sensor feed
*
* @public
* @param {any} callback
* @example
* var motor = new serviceSPIKE.Motor('A')
* serviceSPIKE.executeWithStream( async function() {
* var speed = await motor.get_speed();
* // do something with motor speed
* })
*/
function executeWithStream(callback) {
funcWithStream = callback;
}
/** Get the callback function to execute after service is disconnected
* @ignore
* @param {any} callback
*/
function executeAfterDisconnect(callback) {
funcAfterDisconnect = callback;
}
/** Send command to the SPIKE Prime (UJSON RPC or Micropy depending on current interpreter)
* <p> May make the SPIKE Prime do something </p>
* @ignore
* @param {string} command Command to send (or sequence of commands, separated by new lines)
*/
async function sendDATA(command) {
// look up the command to send
var commands = command.split("\n"); // split on new line
// ignore console logging trigger_current_state (to avoid it spamming)
if (command.indexOf("trigger_current_state") == -1)
console.log("%cTuftsCEEO ", "color: #3ba336;", "sendDATA: " + commands);
// make sure ready to write to device
setupWriter();
// send it in micropy if micropy reached
if (micropython_interpreter) {
for (var i = 0; i < commands.length; i++) {
// console.log("%cTuftsCEEO ", "color: #3ba336;", "commands.length", commands.length)
// trim trailing, leading whitespaces
var current = commands[i].trim();
writer.write(current);
writer.write(RETURN); // extra return at the end
}
}
// expect json scripts if micropy not reached
else {
// go through each line of the command
// trim it, send it, and send a return...
for (var i = 0; i < commands.length; i++) {
//console.log("%cTuftsCEEO ", "color: #3ba336;", "commands.length", commands.length)
current = commands[i].trim();
//console.log("%cTuftsCEEO ", "color: #3ba336;", "current", current);
// turn string into JSON
//string_current = (JSON.stringify(current));
//myobj = JSON.parse(string_current);
var myobj = await JSON.parse(current);
// turn JSON back into string and write it out
writer.write(JSON.stringify(myobj));
writer.write(RETURN); // extra return at the end
}
}
}
/** Send character sequences to reboot SPIKE Prime
* <p> <em> Run this function to exit micropython interpreter </em> </p>
* @public
* @example
* serviceSPIKE.rebootHub();
*/
function rebootHub() {
console.log("%cTuftsCEEO ", "color: #3ba336;", "rebooting")
// make sure ready to write to device
setupWriter();
writer.write(CONTROL_C);
writer.write(CONTROL_D);
//toggle micropython_interpreter flag if its was active
if (micropython_interpreter) {
micropython_interpreter = false;
}
}
/** Get the information of all the ports and devices connected to them
* @ignore
* @returns {object} <p> An object with keys as port letters and values as objects of device type and info </p>
* @example
* // USAGE
*
* var portsInfo = await serviceSPIKE.getPortsInfo();
* // ports.{yourPortLetter}.device --returns--> device type (ex. "smallMotor" or "ultrasonic") </p>
* // ports.{yourPortLetter}.data --returns--> device info (ex. {"speed": 0, "angle":0, "uAngle": 0, "power":0} ) </p>
*
* // Motor on port A
* var motorSpeed = portsInfo["A"]["speed"]; // motor speed
* var motorDegreesCounted = portsInfo["A"]["angle"]; // motor angle
* var motorPosition = portsInfo["A"]["uAngle"]; // motor angle in unit circle ( -180 ~ 180 )
* var motorPower = portsInfo["A"]["power"]; // motor power
*
* // Ultrasonic Sensor on port A
* var distance = portsInfo["A"]["distance"] // distance value from ultrasonic sensor
*
* // Color Sensor on port A
* var reflectedLight = portsInfo["A"]["reflected"]; // reflected light
* var color = portsInfo["A"]["color"]; // name of detected color
* var RGB = portsInfo["A"]["RGB"]; // [R, G, B]
*
* // Force Sensor on port A
* var forceNewtons = portsInfo["A"]["force"]; // Force in Newtons ( 1 ~ 10 )
* var pressedBool = portsInfo["A"]["pressed"] // whether pressed or not ( true or false )
* var forceSensitive = portsInfo["A"]["forceSensitive"] // More sensitive force output( 0 ~ 900 )
*/
function getPortsInfo() {
return ports;
}
/** get the info of a single port
* @ignore
* @param {string} letter Port on the SPIKE hub
* @returns {object} Keys as device and info as value
*/
function getPortInfo(letter) {
return ports[letter];
}
/** Get battery status
* @ignore
* @returns {integer} battery percentage
*/
function getBatteryStatus() {
return batteryAmount;
}
/** Get info of the hub
* @ignore
* @returns {object} Info of the hub
* @example
* var hubInfo = await serviceSPIKE.getHubInfo();
*
* var upDownDetector = hubInfo["gyro"][0];
* var rightSideLeftSideDetector = hubInfo["gyro"][1];
* var frontBackDetector = hubInfo["gyro"][2];
*
* var rollAcceleration = hubInfo["pos"][0];
* var pitchAcceleration = hubInfo["pos"][1];
* var yawAcceleration = hubInfo["pos"][2];
*
* var yawAngle = hubInfo["pos"][0];
* var pitchAngle = hubInfo["pos"][1];
* var rollAngle = hubInfo["pos"][2];
*
*
*/
function getHubInfo() {
return hub;
}
/** Get the name of the hub
*
* @public
* @returns name of hub
*/
function getHubName() {
return hubName;
}
/**
* @ignore
* @param {any} callback
*/
async function getFirmwareInfo(callback) {
UJSONRPC.getFirmwareInfo(callback);
}
/** get projects in all the slots of SPIKE Prime hub
*
* @ignore
* @returns {object}
*/
async function getProjects() {
UJSONRPC.getStorageStatus();
await sleep(2000);
return hubProjects
}
/** Reach the micropython interpreter beneath UJSON RPC
* <p> Note: Stops UJSON RPC stream </p>
* <p> hub needs to be rebooted to return to UJSONRPC stream</p>
* @ignore
* @example
* serviceSPIKE.reachMicroPy();
* serviceSPIKE.sendDATA("from spike import PrimeHub");
* serviceSPIKE.sendDATA("hub = PrimeHub()");
* serviceSPIKE.sendDATA("hub.light_matrix.show_image('HAPPY')");
*/
function reachMicroPy() {
console.log("%cTuftsCEEO ", "color: #3ba336;", "starting micropy interpreter");
setupWriter();
writer.write(CONTROL_C);
micropython_interpreter = true;
}
/** Get the latest complete line of UJSON RPC from stream
* @ignore
* @returns {string} Represents a JSON object from UJSON RPC
*/
async function getLatestUJSON() {
try {
var parsedUJSON = await JSON.parse(lastUJSONRPC)
}
catch (error) {
//console.log("%cTuftsCEEO ", "color: #3ba336;", '[retrieveData] ERROR', error);
}
return lastUJSONRPC
}
/** Get whether the Service was initialized or not
* @public
* @returns {boolean} True if service initialized, false otherwise
* @example
* if (serviceSPIKE.isActive()) {
* // do something
* }
*/
function isActive() {
return serviceActive;
}
/** Get the most recently detected event on the display of the hub
* @public
* @returns {string} ['tapped','doubletapped']
* var event = await serviceSPIKE.getHubEvent();
* if (event == "tapped" ) {
* console.log("SPIKE is tapped");
* }
*/
function getHubEvent() {
return hubFrontEvent;
}
/** Get the most recently detected gesture of the hub ( Gesture names differ from SPIKE app )
* @public
* @returns {string} ['shaken', 'freefall', 'tapped', 'doubletapped']
* @example
* var gesture = await serviceSPIKE.getHubGesture();
* if (gesture == "shaken") {
* console.log("SPIKE is being shaked");
* }
*/
function getHubGesture() {
return hubGesture;
}
/** Get the most recently detected orientation of the hub
* @public
* @returns {string} ['up','down','front','back','leftside','rightside']
* @example
* var orientation = await serviceSPIKE.getHubOrientation();
* if (orientation == "front") {
* console.log("SPIKE is facing up");
* }
*/
function getHubOrientation() {
return lastHubOrientation;
}
/** Get the latest press event information on the "connect" button
* @ignore
* @returns {object} { "pressed": BOOLEAN, "duration": NUMBER }
* @example
* var bluetoothButtonInfo = await serviceSPIKE.getBluetoothButton();
* var pressedBool = bluetoothButtonInfo["pressed"];
* var pressedDuration = bluetoothButtonInfo["duration"]; // duration is miliseconds the button was pressed until release
*/
function getBluetoothButton() {
return hubBluetoothButton;
}
/** Get the latest press event information on the "center" button
* @ignore
* @returns {object} { "pressed": BOOLEAN, "duration": NUMBER }
* @example
* var mainButtonInfo = await serviceSPIKE.getMainButton();
* var pressedBool = mainButtonInfo["pressed"];
* var pressedDuration = mainButtonInfo["duration"]; // duration is miliseconds the button was pressed until release
*
*/
function getMainButton() {
return hubMainButton;
}
/** Get the latest press event information on the "left" button
* @ignore
* @returns {object} { "pressed": BOOLEAN, "duration": NUMBER }
* @example
* var leftButtonInfo = await serviceSPIKE.getLeftButton();
* var pressedBool = leftButtonInfo["pressed"];
* var pressedDuration = leftButtonInfo["duration"]; // duration is miliseconds the button was pressed until release
*
*/
function getLeftButton() {
return hubLeftButton;
}
/** Get the latest press event information on the "right" button
* @ignore
* @returns {object} { "pressed": BOOLEAN, "duration": NUMBER }
* @example
* var rightButtonInfo = await serviceSPIKE.getRightButton();
* var pressedBool = rightButtonInfo["pressed"];
* var pressedDuration = rightButtonInfo["duration"]; // duration is miliseconds the button was pressed until release
*/
function getRightButton() {
return hubRightButton;
}
/** Get the letters of ports connected to any kind of Motors
* @public
* @returns {(string|Array)} Ports that are connected to Motors
* @example
* var motorPorts = serviceSPIKE.getMotorPorts();
*
* // get the alphabetically earliest port connected to a motor
* var randomPort = motorPorts[0];
*
* // get Motor object connected to the port
* var mySensor = new Motor(randomPort);
*/
function getMotorPorts() {
var portsInfo = ports;
var motorPorts = [];
for (var key in portsInfo) {
if (portsInfo[key].device == "smallMotor" || portsInfo[key].device == "bigMotor") {
motorPorts.push(key);
}
}
return motorPorts;
}
/** Get the letters of ports connected to Small Motors
* @public
* @returns {(string|Array)} Ports that are connected to Small Motors
* @example
* var smallMotorPorts = serviceSPIKE.getSmallMotorPorts();
*
* // get the alphabetically earliest port connected to a small motor
* var randomPort = smallMotorPorts[0];
*
* // get Motor object connected to the port
* var mySensor = new Motor(randomPort);
*/
function getSmallMotorPorts() {
var portsInfo = ports;
var motorPorts = [];
for (var key in portsInfo) {
if (portsInfo[key].device == "smallMotor") {
motorPorts.push(key);
}
}
return motorPorts;
}
/** Get the letters of ports connected to Big Motors
* @public
* @returns {(string|Array)} Ports that are connected to Big Motors
* @example
* var bigMotorPorts = serviceSPIKE.getBigMotorPorts();
*
* // get the alphabetically earliest port connected to a big motor
* var randomPort = bigMotorPorts[0];
*
* // get Motor object connected to the port
* var mySensor = new Motor(randomPort);
*/
function getBigMotorPorts() {
var portsInfo = ports;
var motorPorts = [];
for (var key in portsInfo) {
if (portsInfo[key].device == "bigMotor") {
motorPorts.push(key);
}
}
return motorPorts;
}
/** Get the letters of ports connected to Distance Sensors
* @public
* @returns {(string|Array)} Ports that are connected to Distance Sensors
* @example
* var distanceSensorPorts = serviceSPIKE.getDistancePorts();
*
* // get the alphabetically earliest port connected to a DistanceSensor
* var randomPort = distanceSensorPorts[0];
*
* // get DistanceSensor object connected to the port
* var mySensor = new DistanceSensor(randomPort);
*/
function getUltrasonicPorts() {
var portsInfo = this.getPortsInfo();
var ultrasonicPorts = [];
for (var key in portsInfo) {
if (portsInfo[key].device == "ultrasonic") {
ultrasonicPorts.push(key);
}
}
return ultrasonicPorts;
}
/** Get the letters of ports connected to Color Sensors
* @public
* @returns {(string|Array)} Ports that are connected to Color Sensors
* @example
* var colorSensorPorts = serviceSPIKE.getColorPorts();
*
* // get the alphabetically earliest port connected to a ColorSensor
* var randomPort = colorSensorPorts[0];
*
* // get ColorSensor object connected to the port
* var mySensor = new ColorSensor(randomPort);
*/
function getColorPorts() {
var portsInfo = this.getPortsInfo();
var colorPorts = [];
for (var key in portsInfo) {
if (portsInfo[key].device == "color") {
colorPorts.push(key);
}
}
return colorPorts;
}
/** Get the letters of ports connected to Force Sensors
* @public
* @returns {(string|Array)} Ports that are connected to Force Sensors
* @example
* var forceSensorPorts = serviceSPIKE.getForcePorts();
*
* // get the alphabetically earliest port connected to a ForceSensor
* var randomPort = forceSensorPorts[0];
*
* // get ForceSensor object connected to the port
* var mySensor = new ForceSensor(randomPort);
*/
function getForcePorts() {
var portsInfo = this.getPortsInfo();
var forcePorts = [];
for (var key in portsInfo) {
if (portsInfo[key].device == "force") {
forcePorts.push(key);
}
}
return forcePorts;
}
/** Get all motor objects currently connected to SPIKE
*
* @public
* @returns {array} All connected Motor objects
* @example
* var motors = serviceSPIKE.getMotors();
*
* if (motors.length > 0) {
*
* var myMotor = motors[0]; // get motor connected to most alphabetically early port
*
* // run motor for 10 seconds at 100 speed
* myMotor.run_for_seconds(10,100);
* }
*/
function getMotors() {
var portsInfo = ports;
var motors = [];
for (var key in portsInfo) {
if (portsInfo[key].device == "smallMotor" || portsInfo[key].device == "bigMotor") {
motors.push(new Motor(key));
}
}
return motors;
}
/** Get all distance sensor objects currently connected to SPIKE
*
* @public
* @returns {array} All connected DistanceSensor objects
* @example
* var distanceSensors = serviceSPIKE.getDistanceSensors();
*
* if (distanceSensors.length > 0) {
*
* var myDistanceSensor = distanceSensors[0]; // get DistanceSensor connected to most alphabetically early port
*
* // get distance in centimeters
* var distance = myDistanceSensor.get_distance_cm();
* console.log("distance in CM: ", distance);
* }
*/
function getDistanceSensors() {
var portsInfo = ports;
var distanceSensors = [];
for (var key in portsInfo) {
if (portsInfo[key].device == "ultrasonic") {
distanceSensors.push(new DistanceSensor(key));
}
}
return distanceSensors;
}
/** Get all color sensor objects currently connected to SPIKE
*
* @public
* @returns {object} All connected ColorSensor objects
* @example
* var colorSensors = serviceSPIKE.getColorSensors();
*
* if (colorSensors.length > 0) {
*
* var color_sensor = colorSensors[0]; // get ColorSensor connected to most alphabetically early port
*
* // get detected color
* var color = color_sensor.get_color();
* console.log("detected color: ", color);
* }
*/
function getColorSensors() {
var portsInfo = ports;
var colorSensors = [];
for (var key in portsInfo) {
if (portsInfo[key].device == "color") {
colorSensors.push( new ColorSensor(key));
}
}
return colorSensors;
}
/** Get all force sensor objects currently connected to SPIKE
*
* @public
* @returns {object} All connected ForceSensor objects
* @example
* var forceSensors = serviceSPIKE.getForceSensors();
*
* if (forceSensors.length > 0) {
*
* var force_sensor = forceSensors[0]; // get ForceSensor connected to most alphabetically early port
*
* // when ForceSensor is pressed, indicate button state on console
* force_sensor.wait_until_pressed( function() {
* console.log("ForceSensor at port A was pressed");
* })
* }
*
*/
function getForceSensors() {
var portsInfo = ports;
var forceSensors = [];
for (var key in portsInfo) {
if (portsInfo[key].device == "force") {
forceSensors.push( new ForceSensor(key));
}
}
return forceSensors;
}
/** Terminate currently running micropy program
*/
function stopCurrentProgram() {
UJSONRPC.programTerminate();
}
/** Push micropython code that retrieves all JS global variables and local variables at the scope in which
* this function was called
* @public
* @param {integer} slotid
* @param {string} program program to write must be in TEMPLATE LITERAL
* @ignore
* @example
* serviceSPIKE.micropython(10, `
*from spike import PrimeHub, LightMatrix, Motor, MotorPair
*from spike.control import wait_for_seconds, wait_until, Timer
*
*hub = PrimeHub()
*
*hub.light_matrix.write(run_for_seconds(2))
*
*run_for_seconds(3)
* `)
*/
function micropython(slotid, program) {
// initialize microPyUtils
micropyUtils.init();
/* add local variables of the caller of this function */
// get the function definition of caller
/* parse and add all local variable declarations to micropyUtils.storedVariables
var aString = "hi" or var aString = 'hi' > {aString: "hi"}
*/
var thisFunction = arguments.callee.caller.toString();
// split function scope by newlines
var newLineRule = /\n/g
var arrayLines = thisFunction.split(newLineRule);
// filter lines that dont contain var, or contains function
var arrayVarLines = [];
for (var index in arrayLines) {
if (arrayLines[index].indexOf("var") > -1) {
// filter out functions and objects
if (arrayLines[index].indexOf("function") == -1 && arrayLines[index].indexOf("{") == -1 && arrayLines[index].indexOf("}") == -1) {
arrayVarLines.push(arrayLines[index]);
}
}
}
var parseRule = /[[ ]/g
for (var index in arrayVarLines) {
// process line
var processedLine = micropyUtils.processString(arrayVarLines[index]);
// get [datatype] object = value format
var listParsedLine = processedLine.split(parseRule);
//listParsedLine = listParsedLine.split(/[=]/g)
var keyValue = micropyUtils.checkString(listParsedLine);
// insert into variables
for (var name in keyValue) {
micropyUtils.storedVariables[name] = keyValue[name];
}
}
/* generate lines of micropy variable declarations */
var lines = [];
for (var name in micropyUtils.storedVariables) {
var variableName = name;
if (typeof micropyUtils.storedVariables[name] !== "function" && typeof micropyUtils.storedVariables[name] !== "object") {
var variableValue = micropyUtils.convertToString(micropyUtils.storedVariables[name]);
lines.push("" + variableName + " = " + variableValue);
}
}
// do add new lines to every line
var linesChunk = ""
for (var index in lines ) {
var linePiece = lines[index];
linesChunk = linesChunk + linePiece + "\n";
}
var programToWrite = linesChunk + program;
writeProgram("micropython", programToWrite, slotid, function() {
console.log("%cTuftsCEEO ", "color: #3ba336;", "micropy program write complete");
})
}
/** Write a micropy program into a slot of the SPIKE Prime
*
* @param {string} projectName name of the program
* @param {string} data the micropython source code (expecting an input tag's value). All characters must be ASCII
* @param {integer} slotid slot number to assign the program
* @param {function} callback function to run after program is written
*/
async function writeProgram(projectName, data, slotid, callback) {
// check for non-ascii characters
let ascii = /[^\x00-\x7F]/;
if (ascii.test(data)) {
throw new Error(
"non-ASCII characters detected in micropy program. Only ASCII characters are supported. Please check your micropy input."
)
}
else {
// reinit witeProgramTimeout
if (writeProgramSetTimeout != undefined) {
clearTimeout(writeProgramSetTimeout);
writeProgramSetTimeout = undefined;
}
// template of python file that needs to be concatenated
var firstPart = "from runtime import VirtualMachine\n\n# Stack for execution:\nasync def stack_1(vm, stack):\n"
var secondPart = "# Setup for execution:\ndef setup(rpc, system, stop):\n\n # Initialize VM:\n vm = VirtualMachine(rpc, system, stop, \"Target__1\")\n\n # Register stack on VM:\n vm.register_on_start(\"stack_1\", stack_1)\n\n return vm"
// stringify data and strip trailing and leading quotation marks
var stringifiedData = JSON.stringify(data);
stringifiedData = stringifiedData.substring(1, stringifiedData.length - 1);
var result = ""; // string to which the final code will be appended
var splitData = stringifiedData.split(/\\n/); // split the code by every newline
// add a tab before every newline (this is syntactically needed for concatenating with the template)
for (var index in splitData) {
var addedTab = " " + splitData[index] + "\n";
result = result + addedTab;
}
// replace tab characters
result = result.replace(/\\t/g, " ");
stringifiedData = firstPart + result + secondPart;
writeProgramCallback = callback;
// begin the write program process
UJSONRPC.startWriteProgram(projectName, "python", stringifiedData, slotid);
}
}
/** Execute a program in SPIKE Prime
*
* @param {integer} slotid slot of which program to execute
* @example
* // execute program in slot 1 of SPIKE Prime hub
* serviceSPIKE.executeProgram(1);
*/
function executeProgram(slotid) {
UJSONRPC.programExecute(slotid)
}
//////////////////////////////////////////
// //
// SPIKE APP Functions //
// //
//////////////////////////////////////////
/** The PrimeHub object includes controllable interfaces ("constants") for your SPIKE Prime, such as left_button, right_button, motion_sensor, and light_matrix.
* @namespace
* @memberof Service_SPIKE
* @example
* // Initialize the Hub
* var hub = new serviceSPIKE.PrimeHub()
*/
PrimeHub = function () {
var newOrigin = 0;
/** The left button on the hub
* @namespace
* @memberof! PrimeHub
* @returns {functions} - functions from PrimeHub.left_button
* @example
* var hub = new serviceSPIKE.PrimeHub();
* var left_button = hub.left_button;
* // do something with left_button
*/
var left_button = {};
/** execute callback after this button is pressed
* @param {function} callback function to run when button is pressed
* @example
* var hub = new serviceSPIKE.PrimeHub();
* var left_button = hub.left_button;
* left_button.wait_until_pressed ( function () {
* console.log("left_button was pressed");
* })
*
*/
left_button.wait_until_pressed = function wait_until_pressed(callback) {
funcAfterLeftButtonPress = callback;
}
/** execute callback after this button is released
*
* @param {function} callback function to run when button is released
* @example
* var hub = new serviceSPIKE.PrimeHub();
* var left_button = hub.left_button;
* left_button.wait_until_released ( function () {
* console.log("left_button was released");
* })
*/
left_button.wait_until_released = function wait_until_released(callback) {
funcAfterLeftButtonRelease = callback;
}
/** Tests to see whether the button has been pressed since the last time this method called.
*
* @returns {boolean} - True if was pressed, false otherwise
* @example
* if (left_button.was_pressed()) {
* console.log("left_button was pressed")
* }
*/
left_button.was_pressed = function was_pressed() {
if (hubLeftButton.duration > 0) {
hubLeftButton.duration = 0;
return true;
} else {
return false;
}
}
/** Tests to see whether the button is pressed
*
* @returns {boolean} True if pressed, false otherwise
* @example
* if (left_button.is_pressed()) {
* console.log("left_button is pressed")
* }
*/
left_button.is_pressed = function is_pressed() {
if (hubLeftButton.pressed) {
return true;
}
else {
return false;
}
}
/** The right button on the hub
* @namespace
* @memberof! PrimeHub
* @returns {functions} functions from PrimeHub.right_button
* @example
* var hub = serviceSPIKE.PrimeHub();
* var right_button = hub.right_button;
* // do something with right_button
*/
var right_button = {};
/** execute callback after this button is pressed
*
* @param {function} callback function to run when button is pressed
* @example
* var hub = new serviceSPIKE.PrimeHub();
* var right_button = hub.right_button;
* right_button.wait_until_pressed ( function () {
* console.log("right_button was pressed");
* })
*/
right_button.wait_until_pressed = function wait_until_pressed(callback) {
funcAfterRightButtonPress = callback;
}
/** execute callback after this button is released
*
* @param {function} callback function to run when button is released
* @example
* var hub = new serviceSPIKE.PrimeHub();
* var right_button = hub.right_button;
* right_button.wait_until_released ( function () {
* console.log("right_button was released");
* })
*/
right_button.wait_until_released = function wait_until_released(callback) {
funcAfterRightButtonRelease = callback;
}
/** Tests to see whether the button has been pressed since the last time this method called.
*
* @returns {boolean} - True if was pressed, false otherwise
* @example
* var hub = new serviceSPIKE.PrimeHub();
* if ( hub.right_button.was_pressed() ) {
* console.log("right_button was pressed");
* }
*/
right_button.was_pressed = function was_pressed() {
if (hubRightButton.duration > 0) {
hubRightButton.duration = 0;
return true;
} else {
return false;
}
}
/** Tests to see whether the button is pressed
*
* @returns {boolean} True if pressed, false otherwise
* @example
* if (right_button.is_pressed()) {
* console.log("right_button is pressed")
* }
*/
right_button.is_pressed = function is_pressed() {
if (hubRightButton.pressed) {
return true;
}
else {
return false;
}
}
/** Following are all of the functions that are linked to the Hub’s programmable Brick Status Light.
* @namespace
* @memberof! PrimeHub
* @returns {functions} - functions from PrimeHub.light_matrix
* @example
* var hub = serviceSPIKE.PrimeHub();
* var status_light = hub.status_light;
* // do something with status_light
*/
var status_light = {};
/** Sets the color of the light.
* @param {string} color ["azure","black","blue","cyan","green","orange","pink","red","violet","yellow","white"]
* @example
* var hub = new Primehub()
* hub.status_light.on("blue")
*
*/
status_light.on = function on (color) {
let dictColor = {
"azure": 4,
"black": 12,
"blue": 3,
"cyan": 5,
"green": 6,
"orange": 8,
"pink": 1,
"red": 9,
"violet": 2,
"yellow": 7,
"white": 10
}
let intColor = dictColor[color];
UJSONRPC.centerButtonLightUp(intColor);
}
/** Turns off the light.
* @example
* var hub = new Primehub()
* hub.status_light.off()
*/
status_light.off = function off () {
UJSONRPC.centerButtonLightUp(0);
}
/** Hub's light matrix
* @namespace
* @memberof! PrimeHub
* @returns {functions} - functions from PrimeHub.light_matrix
* @example
* var hub = serviceSPIKE.PrimeHub();
* var light_matrix = hub.light_matrix;
* // do something with light_matrix
*/
var light_matrix = {};
/**
* @todo Implement this function
* @ignore
* @param {string}
*/
light_matrix.show_image = function show_image(image) {
}
/** Sets the brightness of one pixel (one of the 25 LED) on the Light Matrix.
*
* @param {integer} x [0 to 4]
* @param {integer} y [0 to 4]
* @param {integer} brightness [0 to 100]
*/
light_matrix.set_pixel = function set_pixel(x, y, brightness = 100) {
UJSONRPC.displaySetPixel(x, y, brightness);
}
/** Writes text on the Light Matrix, one letter at a time, scrolling from right to left.
*
* @param {string} message
*/
light_matrix.write = function write(message) {
UJSONRPC.displayText(message);
}
/** Turns off all the pixels on the Light Matrix.
*
*/
light_matrix.off = function off() {
UJSONRPC.displayClear();
}
/** Hub's speaker
* @namespace
* @memberof! PrimeHub
* @returns {functions} functions from Primehub.speaker
* @example
* var hub = serviceSPIKE.PrimeHub();
* var speaker = hub.speaker;
* // do something with speaker
*/
var speaker = {};
speaker.volume = 100;
/** Plays a beep on the Hub.
*
* @param {integer} note The MIDI note number [44 to 123 (60 is middle C note)]
* @param {number} seconds The duration of the beep in seconds
*/
speaker.beep = function beep(note, seconds) {
UJSONRPC.soundBeep(speaker.volume, note);
setTimeout(function () { UJSONRPC.soundStop() }, seconds * 1000);
}
/** Starts playing a beep.
*
* @param {integer} note The MIDI note number [44 to 123 (60 is middle C note)]
*/
speaker.start_beep = function start_beep(note) {
UJSONRPC.soundBeep(speaker.volume, note)
}
/** Stops any sound that is playing.
*
*/
speaker.stop = function stop() {
UJSONRPC.soundStop();
}
/** Retrieves the value of the speaker volume.
* @returns {number} The current volume [0 to 100]
*/
speaker.get_volume = function get_volume() {
return speaker.volume;
}
/** Sets the speaker volume.
*
* @param {integer} newVolume
*/
speaker.set_volume = function set_volume(newVolume) {
speaker.volume = newVolume
}
/** Hub's motion sensor
* @namespace
* @memberof! PrimeHub
* @returns {functions} functions from PrimeHub.motion_sensor
* @example
* var hub = serviceSPIKE.PrimeHub();
* var motion_sensor = hub.motion_sensor;
* // do something with motion_sensor
*/
var motion_sensor = {};
/** Sees whether a gesture has occurred since the last time was_gesture()
* was used or since the beginning of the program (for the first use).
*
* @param {string} gesture
* @returns {boolean} true if the gesture was made, false otherwise
*/
motion_sensor.was_gesture = function was_gesture(gesture) {
var gestureWasMade = false;
// iterate over the hubGestures array
for (index in hubGestures) {
// pick a gesture from the array
var oneGesture = hubGestures[index];
// switch the flag that gesture existed
if (oneGesture == gesture) {
gestureWasMade = true;
break;
}
}
// reinitialize hubGestures so it only holds gestures that occurred after this was_gesture() execution
hubGestures = [];
return gestureWasMade;
}
/** Executes callback when a new gesture happens
*
* @param {function(string)} callback - A callback of which argument is name of the gesture
* @example
* motion_sensor.wait_for_new_gesture( function ( newGesture ) {
* if ( newGesture == 'tapped') {
* console.log("SPIKE was tapped")
* }
* else if ( newGesture == 'doubletapped') {
* console.log("SPIKE was doubletapped")
* }
* else if ( newGesture == 'shaken') {
* console.log("SPIKE was shaken")
* }
* else if ( newGesture == 'freefall') {
* console.log("SPIKE was freefall")
* }
* })
*/
motion_sensor.wait_for_new_gesture = function wait_for_new_gesture(callback) {
funcAfterNewGesture = callback;
}
/** Executes callback when the orientation of the Hub changes or when function was first called
*
* @param {function(string)} callback - A callback whose signature is name of the orientation
* @example
* motion_sensor.wait_for_new_orientation( function ( newOrientation ) {
* if (newOrientation == "up") {
* console.log("orientation is up");
* }
* else if (newOrientation == "down") {
* console.log("orientation is down");
* }
* else if (newOrientation == "front") {
* console.log("orientation is front");
* }
* else if (newOrientation == "back") {
* console.log("orientation is back");
* }
* else if (newOrientation == "leftSide") {
* console.log("orientation is leftSide");
* }
* else if (newOrientation == "rightSide") {
* console.log("orientation is rightSide");
* }
* })
*/
motion_sensor.wait_for_new_orientation = function wait_for_new_orientation(callback) {
// immediately return current orientation if the method was called for the first time
if (waitForNewOriFirst) {
waitForNewOriFirst = false;
callback(lastHubOrientation);
}
// for future executions, wait until new orientation
else {
funcAfterNewOrientation = callback;
}
}
/** “Yaw” is the rotation around the front-back (vertical) axis.
*
* @returns {integer} yaw angle
*/
motion_sensor.get_yaw_angle = function get_yaw_angle() {
var currPos = hub.pos[0];
return currPos;
}
/** “Pitch” the is rotation around the left-right (transverse) axis.
*
* @returns {integer} pitch angle
*/
motion_sensor.get_pitch_angle = function get_pitch_angle() {
return hub.pos[1];
}
/** “Roll” the is rotation around the front-back (longitudinal) axis.
*
* @returns {integer} roll angle
*/
motion_sensor.get_roll_angle = function get_roll_angle() {
return hub.pos[2];
}
/** Gets the acceleration of the SPIKE's yaw axis
*
* @returns {integer} acceleration
*/
motion_sensor.get_yaw_acceleration = function get_yaw_acceleration() {
return hub.pos[2];
}
/** Gets the acceleration of the SPIKE's pitch axis
*
* @returns {integer} acceleration
*/
motion_sensor.get_pitch_acceleration = function get_pitch_acceleration() {
return hub.pos[1];
}
/** Gets the acceleration of the SPIKE's roll axis
*
* @returns {integer} acceleration
*/
motion_sensor.get_roll_acceleration = function get_roll_acceleration() {
return hub.pos[0];
}
/** Retrieves the most recently detected gesture.
*
* @returns {string} the name of gesture
*/
motion_sensor.get_gesture = function get_gesture() {
return hubGesture;
}
/** Retrieves the most recently detected orientation
* Note: Hub does not detect orientation of when it was connected
*
* @returns {string} the name of orientation
*/
motion_sensor.get_orientation = function get_orientation() {
return lastHubOrientation;
}
return {
motion_sensor: motion_sensor,
light_matrix: light_matrix,
left_button: left_button,
right_button: right_button,
speaker: speaker
}
}
/** Motor
* @namespace
* @memberof! Service_SPIKE
* @param {string} Port
* @returns {functions}
* @example
* // Initialize the Motor
* var motor = new serviceSPIKE.Motor("A")
*/
Motor = function (port) {
var motor = ports[port]; // get the motor info by port
// default settings
var defaultSpeed = 100;
var stopMethod = 1; // stop method doesnt seem to work in this current ujsonrpc config
var stallSetting = true;
var direction = {
COUNTERCLOCKWISE: 'counterClockwise',
CLOCKWISE: 'clockwise'
}
// check if device is a motor
if (motor.device != "smallMotor" && motor.device != "bigMotor") {
throw new Error("No motor detected at port " + port);
}
/** Get current speed of the motor
*
* @returns {number} speed of motor [-100 to 100]
*/
function get_speed() {
var motor = ports[port]; // get the motor info by port
var motorInfo = motor.data;
return motorInfo.speed;
}
/** Get current position of the motor. The position may differ by a little margin from
* the position to which a motor ran with run_to_position()
* @returns {number} position of motor [0 to 359]
*/
function get_position() {
var motor = ports[port]; // get the motor info by port
var motorInfo = motor.data;
let position = motorInfo.uAngle;
if (position < 0)
position = 360 + position;
return position;
}
/** Get current degrees counted of the motor
*
* @returns {number} counted degrees of the motor [any number]
*/
function get_degrees_counted() {
var motor = ports[port]; // get the motor info by port
var motorInfo = motor.data;
return motorInfo.angle;
}
/** Get the power of the motor
*
* @returns {number} motor power
*/
function get_power() {
var motor = ports[port]; // get the motor info by port
var motorInfo = motor.data;
return motorInfo.power;
}
/** Get the default speed of this motor
*
* @returns {number} motor default speed [-100 to 100]
*/
function get_default_speed() {
return defaultSpeed;
}
/** Set the default speed for this motor
*
* @param {number} speed [-100 to 100]
*/
function set_default_speed(speed) {
if (typeof speed == "number") {
defaultSpeed = speed;
}
}
/** Turns stall detection on or off.
* Stall detection senses when a motor has been blocked and can’t move.
* If stall detection has been enabled and a motor is blocked, the motor will be powered off
* after two seconds and the current motor command will be interrupted. If stall detection has been
* disabled, the motor will keep trying to run and programs will “get stuck” until the motor is no
* longer blocked.
* @param {boolean} boolean - true if to detect stall, false otherwise
*/
function set_stall_detection(boolean) {
if (typeof boolean == "boolean") {
stallSetting = boolean;
}
}
/** Runs the motor to an absolute position.
* The sign of the speed will be ignored (i.e., absolute value), and the motor will always travel in the direction that’s been specified by the "direction" parameter.
* If the speed is greater than "100," it will be limited to "100."
* @param {integer} degrees [0 to 359]
* @param {string} direction "Clockwise" or "Counterclockwise"
* @param {integer} speed [-100 to 100]
* @param {function} callback Params: "stalled" or "done"
* @ignore
* @example
* motor.run_to_position(180, 100, function() {
* console.log("motor finished moving");
* })
*/
function run_to_position(degrees, direction, speed, callback = undefined) {
if (speed !== undefined && typeof speed == "number")
UJSONRPC.motorGoRelPos(port, degrees, speed, stallSetting, stopMethod, callback);
else
UJSONRPC.motorGoRelPos(port, degrees, defaultSpeed, stallSetting, stopMethod, callback);
}
/** Runs the motor until the number of degrees counted is equal to the value that has been specified by the "degrees" parameter.
*
* @param {integer} degrees any number
* @param {integer} speed [0 to 100]
* @param {any} [callback] (optional callback) callback param: "stalled" or "done"
*/
function run_to_degrees_counted(degrees, speed, callback = undefined) {
if (speed !== undefined && typeof speed == "number")
UJSONRPC.motorGoRelPos(port, degrees, speed, stallSetting, stopMethod, callback);
else
UJSONRPC.motorGoRelPos(port, degrees, defaultSpeed, stallSetting, stopMethod, callback);
}
/** Start the motor at some power
*
* @param {integer} power [-100 to 100]
*/
function start_at_power(power) {
UJSONRPC.motorPwm(port, power, stallSetting);
}
/** Start the motor at some speed
*
* @param {integer} speed [-100 to 100]
*/
function start(speed = defaultSpeed) {
// if (speed !== undefined && typeof speed == "number") {
// UJSONRPC.motorStart (port, speed, stallSetting);
// }
// else {
// UJSONRPC.motorStart(port, defaultSpeed, stallSetting);
// }
UJSONRPC.motorStart(port, speed, stallSetting);
}
/** Run the motor for some seconds
*
* @param {integer} seconds
* @param {integer} speed [-100 to 100]
* @param {function} [callback==undefined] Parameters:"stalled" or "done"
* @example
* motor.run_for_seconds(10, 100, function() {
* console.log("motor just ran for 10 seconds");
* })
*/
function run_for_seconds(seconds, speed, callback = undefined) {
if (speed !== undefined && typeof speed == "number") {
UJSONRPC.motorRunTimed(port, seconds, speed, stallSetting, stopMethod, callback)
}
else {
UJSONRPC.motorRunTimed(port, seconds, defaultSpeed, stallSetting, stopMethod, callback)
}
}
/** Run the motor for some degrees
*
* @param {integer} degrees
* @param {integer} speed [-100 to 100]
* @param {function} [callback==undefined] Parameters:"stalled" or "done"
* motor.run_for_degrees(720, 100, function () {
* console.log("motor just ran for 720 degrees");
* })
*/
function run_for_degrees(degrees, speed, callback = undefined) {
if (speed !== undefined && typeof speed == "number") {
UJSONRPC.motorRunDegrees(port, degrees, speed, stallSetting, stopMethod, callback);
}
else {
UJSONRPC.motorRunDegrees(port, degrees, defaultSpeed, stallSetting, stopMethod, callback);
}
}
/** Stop the motor
*
*/
function stop() {
UJSONRPC.motorPwm(port, 0, stallSetting);
}
return {
run_to_position: run_to_position,
run_to_degrees_counted: run_to_degrees_counted,
start_at_power: start_at_power,
start: start,
stop: stop,
run_for_degrees: run_for_degrees,
run_for_seconds: run_for_seconds,
set_default_speed: set_default_speed,
set_stall_detection: set_stall_detection,
get_power: get_power,
get_degrees_counted: get_degrees_counted,
get_position: get_position,
get_speed: get_speed,
get_default_speed: get_default_speed
}
}
/** ColorSensor
* @namespace
* @param {string} Port
* @memberof Service_SPIKE
* @example
* // Initialize the Color Sensor
* var color = new serviceSPIKE.ColorSensor("E")
*/
ColorSensor = function (port) {
var waitForNewColorFirst = false;
var colorsensor = ports[port]; // get the color sensor info by port
var colorsensorData = colorsensor.data;
// check if device is a color sensor
if (colorsensor.device != "color") {
throw new Error("No Color Sensor detected at port " + port);
}
/** Get the name of the detected color
* @returns {string} 'black','violet','blue','cyan','green','yellow','red','white'
*/
function get_color() {
var colorsensor = ports[port]; // get the color sensor info by port
var colorsensorData = colorsensor.data;
var color = colorsensorData.color;
return color;
}
/** Retrieves the intensity of the ambient light.
* @ignore
* @returns {number} The ambient light intensity. [0 to 100]
*/
function get_ambient_light() {
var colorsensor = ports[port]; // get the color sensor info by port
var colorsensorData = colorsensor.data;
return colorsensorData.Cambient;
}
/** Retrieves the intensity of the reflected light.
*
* @returns {number} The reflected light intensity. [0 to 100]
*/
function get_reflected_light() {
var colorsensor = ports[port]; // get the color sensor info by port
var colorsensorData = colorsensor.data;
return colorsensorData.Creflected;
}
/** Retrieves the red, green, blue, and overall color intensity.
* @todo Implement overall intensity
* @ignore
* @returns {(number|Array)} Red, green, blue, and overall intensity (0-1024)
*/
function get_rgb_intensity() {
var colorsensor = ports[port]; // get the color sensor info by port
var colorsensorData = colorsensor.data;
var toReturn = [];
toReturn.push(colorsensorData.Cr);
toReturn.push(colorsensorData.Cg);
toReturn.push(colorsensorData.Cb)
toReturn.push("TODO: unimplemented");;
}
/** Retrieves the red color intensity.
*
* @returns {number} [0 to 1024]
*/
function get_red() {
var colorsensor = ports[port]; // get the color sensor info by port
var colorsensorData = colorsensor.data;
return colorsensorData.RGB[0];
}
/** Retrieves the green color intensity.
*
* @returns {number} [0 to 1024]
*/
function get_green() {
var colorsensor = ports[port]; // get the color sensor info by port
var colorsensorData = colorsensor.data;
return colorsensorData.RGB[1];
}
/** Retrieves the blue color intensity.
*
* @returns {number} [0 to 1024]
*/
function get_blue() {
var colorsensor = ports[port]; // get the color sensor info by port
var colorsensorData = colorsensor.data;
return colorsensorData.RGB[2];
}
/** Waits until the Color Sensor detects the specified color.
*
* @param {string} colorInput 'black','violet','blue','cyan','green','yellow','red','white'
* @param {function} callback callback function
*/
function wait_until_color(colorInput, callback) {
waitUntilColorCallback = [colorInput, callback];
}
/** Execute callback when Color Sensor detects a new color.
* The first time this method is called, it returns immediately the detected color.
* After that, it waits until the Color Sensor detects a color that is different from the color that
* was detected the last time this method was used.
* @param {function(string)} callback params: detected new color
*/
function wait_for_new_color(callback) {
// check if this method has been executed after start of program
if (waitForNewColorFirst) {
waitForNewColorFirst = false;
var currentColor = get_color();
callback(currentColor)
}
funcAfterNewColor = callback;
}
return {
get_color: get_color,
wait_until_color: wait_until_color,
wait_for_new_color: wait_for_new_color,
get_ambient_light: get_ambient_light,
get_reflected_light: get_reflected_light,
get_rgb_intensity: get_rgb_intensity,
get_red: get_red,
get_green: get_green,
get_blue: get_blue
}
}
/** DistanceSensor
* @namespace
* @param {string} Port
* @memberof Service_SPIKE
* @example
* // Initialize the DistanceSensor
* var distance_sensor = new serviceSPIKE.DistanceSensor("A");
*/
var DistanceSensor = function (port) {
var distanceSensor = ports[port]; // get the distance sensor info by port
// check if device is a distance sensor
if (distanceSensor.device != "ultrasonic") {
console.error("Ports Info: ", ports);
throw new Error("No DistanceSensor detected at port " + port);
}
/** Retrieves the measured distance in centimeters.
* @returns {number} [0 to 200]
* @todo find the short_range handling ujsonrpc script
* @example
* var distance_cm = distance_sensor.get_distance_cm();
*/
function get_distance_cm() {
var distanceSensor = ports[port] // get the distance sensor info by port
var distanceSensorData = distanceSensor.data;
return distanceSensorData.distance;
}
/** Retrieves the measured distance in inches.
*
* @returns {number} [0 to 79]
* @todo find the short_range handling ujsonrpc script
* @example
* var distance_inches = distance_sensor.get_distance_inches();
*/
function get_distance_inches() {
var distanceSensor = ports[port] // get the distance sensor info by port
var distanceSensorData = distanceSensor.data;
var inches = distanceSensorData.distance * 0.393701; // convert to inches
if (inches % 1 < 0.5)
inches = Math.floor(inches);
else
inches = Math.ceil(inches);
return inches;
}
/** Retrieves the measured distance in percent.
*
* @returns {number/string} [0 to 100] or 'none' if no distance is read
* var distance_percentage = distance_sensor.get_distance_percentage();
*/
function get_distance_percentage() {
var distanceSensor = ports[port] // get the distance sensor info by port
var distanceSensorData = distanceSensor.data;
if (distanceSensorData.distance == null) {
return "none"
}
var percentage = distanceSensorData.distance / 200;
return percentage;
}
/** Waits until the measured distance is greater than distance.
* @param {integer} threshold
* @param {string} unit 'cm','in','%'
* @param {function} callback function to execute when distance is farther than threshold
* @example
* distance_sensor.wait_for_distance_farther_than(10, 'cm', function () {
* console.log("distance is farther than 10 CM");
* })
*/
function wait_for_distance_farther_than(threshold, unit, callback) {
// set callbacks to be executed in updateHubPortsInfo()
if (unit == 'cm') {
waitForDistanceFartherThanCallback = [threshold, callback];
}
else if (unit == 'in') {
waitForDistanceFartherThanCallback = [threshold / 0.393701, callback];
}
else if (unit == '%') {
waitForDistanceFartherThanCallback = [(threshold * 0.01) * 200, callback];
}
else {
throw new Error("The 'unit' argument in wait_for_distance_farther_than(threshold, unit, callback) must be either 'cm', 'in', or '%'.")
}
}
/** Waits until the measured distance is less than distance.
* @param {integer} threshold
* @param {string} unit 'cm','in','%'
* @param {function} callback function to execute when distance is closer than threshold
* @example
* distance_sensor.wait_for_distance_closer_than(10, 'cm', function () {
* console.log("distance is closer than 10 CM");
* })
*/
function wait_for_distance_closer_than(threshold, unit, callback) {
// set callbacks to be executed in updateHubPortsInfo()
if (unit == 'cm') {
waitForDistanceCloserThanCallback = [threshold, callback];
}
else if (unit == 'in') {
waitForDistanceCloserThanCallback = [threshold / 0.393701, callback];
}
else if (unit == '%') {
/* floor or ceil thresholds larger or smaller than what's possible */
if (threshold > 100) {
threshold = 100;
}
else if (threshold < 0) {
threshold = 0;
}
waitForDistanceCloserThanCallback = [(threshold * 0.01) * 200, callback];
}
else {
throw new Error("The 'unit' argument in wait_for_distance_closer_than(threshold, unit, callback) must be either 'cm', 'in', or '%'.")
}
}
/** Sets the brightness of the individual lights on the Distance Sensor.
*
* @param {integer} right_top Brightness [1-100]
* @param {integer} left_top Brightness [1-100]
* @param {integer} right_bottom Brightness [1-100]
* @param {integer} left_bottom Brightness [1-100]
* @example
* distance_sensor.light_up(100,100,100,100);
*/
function light_up (right_top, left_top, right_bottom, left_bottom) {
let lightArray = [0,0,0,0];
lightArray[0] = right_top;
lightArray[1] = left_top;
lightArray[2] = right_bottom;
lightArray[3] = left_bottom;
UJSONRPC.ultrasonicLightUp(port, lightArray);
}
/** Lights up all of the lights on the Distance Sensor at the specified brightness.
*
* @param {number} [brightness=100] The specified brightness of all of the lights
* @example
* distance_sensor.light_up_all(50)
*/
function light_up_all(brightness = 100) {
let lightArray = [brightness, brightness, brightness, brightness];
UJSONRPC.ultrasonicLightUp(port, lightArray);
}
return {
get_distance_cm: get_distance_cm,
get_distance_inches: get_distance_inches,
get_distance_percentage: get_distance_percentage,
light_up: light_up,
light_up_all: light_up_all,
wait_for_distance_closer_than: wait_for_distance_closer_than,
wait_for_distance_farther_than:wait_for_distance_farther_than
}
}
/** ForceSensor
* @namespace
* @param {string} Port
* @memberof Service_SPIKE
* @example
* // Initialize the ForceSensor
* var force_sensor = new serviceSPIKE.ForceSensor("E")
*/
ForceSensor = function (port) {
var sensor = ports[port]; // get the force sensor info by port
if (sensor.device != "force") {
throw new Error("No Force Sensor detected at port " + port);
}
/** Tests whether the button on the sensor is pressed.
*
* @returns {boolean} true if force sensor is pressed, false otherwise
* @example
* if (force_sensor.is_pressed() === true) {
* console.log("force sensor is pressed");
* }
*/
function is_pressed() {
var sensor = ports[port]; // get the force sensor info by port
var ForceSensorData = sensor.data;
return ForceSensorData.pressed;
}
/** Retrieves the measured force, in newtons.
*
* @returns {number} Force in newtons [0 to 10]
* @example
* var newtons = force_sensor.get_force_newtons();
*/
function get_force_newton() {
var sensor = ports[port]; // get the force sensor info by port
var ForceSensorData = sensor.data;
return ForceSensorData.force;
}
/** Retrieves the measured force as a percentage of the maximum force.
*
* @returns {number} percentage [0 to 100]
* var percentage = force_sensor.get_force_percentage();
*/
function get_force_percentage() {
var sensor = ports[port]; // get the force sensor info by port
var ForceSensorData = sensor.data;
var denominator = 704 - 384 // highest detected - lowest detected forceSensitive values
var numerator = ForceSensorData.forceSensitive - 384 // 384 is the forceSensitive value when not pressed
var percentage = Math.round((numerator / denominator) * 100);
return percentage;
}
/** Executes callback when Force Sensor is pressed
* The function is executed in updateHubPortsInfo()'s Force Sensor part
* @param {function} callback
* @example
* force_sensor.wait_until_pressed( function () {
* console.log("force sensor is pressed!");
* })
*/
function wait_until_pressed(callback) {
funcAfterForceSensorPress = callback;
}
/** Executes callback when Force Sensor is released
* The function is executed in updateHubPortsInfo()'s Force Sensor part
* @param {function} callback
* @example
* force_sensor.wait_until_released ( function () {
* console.log("force sensor is released!");
* })
*/
function wait_until_released(callback) {
funcAfterForceSensorRelease = callback;
}
return {
is_pressed: is_pressed,
get_force_newton: get_force_newton,
get_force_percentage: get_force_percentage,
wait_until_pressed: wait_until_pressed,
wait_until_released: wait_until_released
}
}
/** MotorPair
* @namespace
* @param {string} leftPort
* @param {string} rightPort
* @memberof Service_SPIKE
* @example
* var pair = new serviceSPIKE.MotorPair("A", "B")
*/
MotorPair = function (leftPort, rightPort) {
// settings
var defaultSpeed = 100;
var leftMotor = ports[leftPort];
var rightMotor = ports[rightPort];
var DistanceTravelToRevolutionRatio = 17.6;
// check if device is a motor
if (leftMotor.device != "smallMotor" && leftMotor.device != "bigMotor") {
throw new Error("No motor detected at port " + port);
}
if (rightMotor.device != "smallMotor" && rightMotor.device != "bigMotor") {
throw new Error("No motor detected at port " + port);
}
/** Sets the ratio of one motor rotation to the distance traveled.
*
* If there are no gears used between the motors and the wheels of the Driving Base,
* then amount is the circumference of one wheel.
*
* Calling this method does not affect the Driving Base if it is already currently running.
* It will only have an effect the next time one of the move or start methods is used.
*
* @param {number} amount
* @param {string} unit 'cm','in'
*/
function set_motor_rotation(amount, unit) {
// assume unit is 'cm' when undefined
if (unit == "cm" || unit !== undefined) {
DistanceTravelToRevolutionRatio = amount;
}
else if (unit == "in") {
// convert to cm
DistanceTravelToRevolutionRatio = amount * 2.54;
}
}
/** Starts moving the Driving Base
*
* @param {integer} left_speed [-100 to 100]
* @param {integer} right_speed [-100 to 100]
* @example
* pair.start_tank(100,100);
*/
function start_tank(left_speed, right_speed) {
UJSONRPC.moveTankSpeeds(left_speed, right_speed, leftPort, rightPort);
}
/** Starts moving the Driving Base
*
* @param {integer} leftPower [-100 to 100]
* @param {integer} rightPower [-100 to 100]
* @example
* pair.start_tank_at_power(10, 10);
*/
function start_tank_at_power(leftPower, rightPower) {
UJSONRPC.moveTankPowers(leftPower, rightPower, leftPort, rightPort);
}
/** Stops the 2 motors simultaneously, which will stop a Driving Base.
* @example
* pair.stop();
*/
function stop() {
UJSONRPC.moveTankPowers(0, 0, leftPort, rightPort);
}
return {
stop: stop,
set_motor_rotation: set_motor_rotation,
start_tank: start_tank,
start_tank_at_power: start_tank_at_power
}
}
//////////////////////////////////////////
// //
// UJSONRPC Functions //
// //
//////////////////////////////////////////
/** Low Level UJSONRPC Commands
* @ignore
* @namespace UJSONRPC
*/
var UJSONRPC = {};
/**
*
* @memberof! UJSONRPC
* @param {string} text
*/
UJSONRPC.displayText = async function displayText(text) {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' + ', "m": "scratch.display_text", "p": {"text":' + '"' + text + '"' + '} }'
sendDATA(command);
}
/**
* @memberof! UJSONRPC
* @param {integer} x [0 to 4]
* @param {integer} y [0 to 4]
* @param {integer} brightness [1 to 100]
*/
UJSONRPC.displaySetPixel = async function displaySetPixel(x, y, brightness) {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' + ', "m": "scratch.display_set_pixel", "p": {"x":' + x +
', "y":' + y + ', "brightness":' + brightness + '} }';
sendDATA(command);
}
/**
* @memberof! UJSONRPC
*/
UJSONRPC.displayClear = async function displayClear() {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' + ', "m": "scratch.display_clear" }';
sendDATA(command);
}
/**
* @memberof! UJSONRPC
* @param {string} port
* @param {array} array [1-100,1-100,1-100,1-100] array of size 4
*/
UJSONRPC.ultrasonicLightUp = async function ultrasonicLightUp(port, array) {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' + ', "m": "scratch.ultrasonic_light_up", "p": {' +
'"port": ' + '"' + port + '"' +
', "lights": ' + '[' + array + ']' +
'} }';
sendDATA(command);
}
/**
* @memberof! UJSONRPC
* @param {string} port
* @param {integer} speed
* @param {integer} stall
*/
UJSONRPC.motorStart = async function motorStart(port, speed, stall) {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' + ', "m": "scratch.motor_start", "p": {"port":'
+ '"' + port + '"' +
', "speed":' + speed +
', "stall":' + stall +
'} }';
sendDATA(command);
}
/** moves motor to a position
*
* @memberof! UJSONRPC
* @param {string} port
* @param {integer} position
* @param {integer} speed
* @param {boolean} stall
* @param {boolean} stop
* @param {function} callback
*/
UJSONRPC.motorGoRelPos = async function motorGoRelPos(port, position, speed, stall, stop, callback) {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' +
', "m": "scratch.motor_go_to_relative_position"' +
', "p": {' +
'"port":' + '"' + port + '"' +
', "position":' + position +
', "speed":' + speed +
', "stall":' + stall +
', "stop":' + stop +
'} }';
if (callback != undefined) {
pushResponseCallback(randomId, callback);
}
sendDATA(command);
}
UJSONRPC.motorGoDirToPosition = async function motorGoDirToPosition(port, position, direction, speed, stall, stop, callback) {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' +
', "m": "scratch.motor_go_direction_to_position"' +
', "p": {' +
'"port":' + '"' + port + '"' +
', "position":' + position +
', "direction":' + direction +
', "speed":' + speed +
', "stall":' + stall +
', "stop":' + stop +
'} }';
if (callback != undefined) {
pushResponseCallback(randomId, callback);
}
sendDATA(command);
}
/**
*
* @memberof! UJSONRPC
* @param {string} port
* @param {integer} time
* @param {integer} speed
* @param {integer} stall
* @param {boolean} stop
* @param {function} callback
*/
UJSONRPC.motorRunTimed = async function motorRunTimed(port, time, speed, stall, stop, callback) {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' +
', "m": "scratch.motor_run_timed"' +
', "p": {' +
'"port":' + '"' + port + '"' +
', "time":' + time +
', "speed":' + speed +
', "stall":' + stall +
', "stop":' + stop +
'} }';
if (callback != undefined) {
pushResponseCallback(randomId, callback);
}
sendDATA(command);
}
/**
*
* @memberof! UJSONRPC
* @param {string} port
* @param {integer} degrees
* @param {integer} speed
* @param {integer} stall
* @param {boolean} stop
* @param {function} callback
*/
UJSONRPC.motorRunDegrees = async function motorRunDegrees(port, degrees, speed, stall, stop, callback) {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' +
', "m": "scratch.motor_run_for_degrees"' +
', "p": {' +
'"port":' + '"' + port + '"' +
', "degrees":' + degrees +
', "speed":' + speed +
', "stall":' + stall +
', "stop":' + stop +
'} }';
if ( callback != undefined ) {
pushResponseCallback(randomId, callback);
}
sendDATA(command);
}
/**
* @memberof! UJSONRPC
* @param {integer} time
* @param {integer} lspeed
* @param {integer} rspeed
* @param {string} lmotor
* @param {string} rmotor
* @param {boolean} stop
* @param {function} callback
*/
UJSONRPC.moveTankTime = async function moveTankTime(time, lspeed, rspeed, lmotor, rmotor, stop, callback) {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' +
', "m": "scratch.move_tank_time"' +
', "p": {' +
'"time":' + time +
', "lspeed":' + lspeed +
', "rspeed":' + rspeed +
', "lmotor":' + '"' + lmotor + '"' +
', "rmotor":' + '"' + rmotor + '"' +
', "stop":' + stop +
'} }';
if (callback != undefined) {
pushResponseCallback(randomId, callback);
}
sendDATA(command);
}
/**
*
* @memberof! UJSONRPC
* @param {integer} degrees
* @param {integer} lspeed
* @param {integer} rspeed
* @param {string} lmotor
* @param {string} rmotor
* @param {boolean} stop
* @param {function} callback
*/
UJSONRPC.moveTankDegrees = async function moveTankDegrees(degrees, lspeed, rspeed, lmotor, rmotor, stop, callback) {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' +
', "m": "scratch.move_tank_degrees"' +
', "p": {' +
'"degrees":' + degrees +
', "lspeed":' + lspeed +
', "rspeed":' + rspeed +
', "lmotor":' + '"' + lmotor + '"' +
', "rmotor":' + '"' + rmotor + '"' +
', "stop":' + stop +
'} }';
if (callback != undefined) {
pushResponseCallback(randomId, callback);
}
sendDATA(command);
}
/**
*
* @memberof! UJSONRPC
* @param {integer} lspeed
* @param {integer} rspeed
* @param {string} lmotor
* @param {string} rmotor
* @param {function} callback
*/
UJSONRPC.moveTankSpeeds = async function moveTankSpeeds(lspeed, rspeed, lmotor, rmotor, callback) {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' +
', "m": "scratch.move_start_speeds"' +
', "p": {' +
'"lspeed":' + lspeed +
', "rspeed":' + rspeed +
', "lmotor":' + '"' + lmotor + '"' +
', "rmotor":' + '"' + rmotor + '"' +
'} }';
if (callback != undefined) {
pushResponseCallback(randomId, callback);
}
sendDATA(command);
}
/**
*
* @memberof! UJSONRPC
* @param {integer} lpower
* @param {integer} rpower
* @param {string} lmotor
* @param {string} rmotor
* @param {function} callback
*/
UJSONRPC.moveTankPowers = async function moveTankPowers(lpower, rpower, lmotor, rmotor, callback) {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' +
', "m": "scratch.move_start_powers"' +
', "p": {' +
'"lpower":' + lpower +
', "rpower":' + rpower +
', "lmotor":' + '"' + lmotor + '"' +
', "rmotor":' + '"' + rmotor + '"' +
'} }';
if (callback != undefined) {
pushResponseCallback(randomId, callback);
}
sendDATA(command);
}
/**
*
* @memberof! UJSONRPC
* @param {integer} volume
* @param {integer} note
*/
UJSONRPC.soundBeep = async function soundBeep(volume, note) {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' +
', "m": "scratch.sound_beep"' +
', "p": {' +
'"volume":' + volume +
', "note":' + note +
'} }';
sendDATA(command);
}
/**
* @memberof! UJSONRPC
*/
UJSONRPC.soundStop = async function soundStop() {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' +
', "m": "scratch.sound_off"' +
'}';
sendDATA(command);
}
/**
*
* @memberof! UJSONRPC
* @param {string} port
* @param {integer} power
* @param {integer} stall
*/
UJSONRPC.motorPwm = async function motorPwm(port, power, stall) {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' + ', "m": "scratch.motor_pwm", "p": {"port":' + '"' + port + '"' +
', "power":' + power + ', "stall":' + stall + '} }';
sendDATA(command);
}
/**
*
* @memberof! UJSONRPC
* @param {function} callback
*/
UJSONRPC.getFirmwareInfo = async function getFirmwareInfo(callback) {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' + ', "m": "get_hub_info" ' + '}';
sendDATA(command);
if (callback != undefined) {
getFirmwareInfoCallback = [randomId, callback];
}
}
/**
* @memberof! UJSONRPC
* @param {function} callback
*/
UJSONRPC.triggerCurrentState = async function triggerCurrentState(callback) {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' + ', "m": "trigger_current_state" ' + '}';
sendDATA(command);
if (callback != undefined) {
triggerCurrentStateCallback = callback;
}
}
/**
*
* @memberof! UJSONRPC
* @param {integer} slotid
*/
UJSONRPC.programExecute = async function programExecute(slotid) {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' + ', "m": "program_execute", "p": {"slotid":' + slotid + '} }';
sendDATA(command);
}
/**
* @memberof! UJSONRPC
*/
UJSONRPC.programTerminate = function programTerminate() {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' +
', "m": "program_terminate"' +
'}';
sendDATA(command);
}
/**
* @memberof! UJSONRPC
* @param {string} projectName name of the project
* @param {integer} type type of data (micropy or scratch)
* @param {string} data entire data to send in ASCII
* @param {integer} slotid slot to which to assign the program
*/
UJSONRPC.startWriteProgram = async function startWriteProgram (projectName, type, data, slotid) {
console.log("%cTuftsCEEO ", "color: #3ba336;", "in startWriteProgram...");
console.log("%cTuftsCEEO ", "color: #3ba336;", "constructing start_write_program script...");
if (type == "python") {
var typeInt = 0;
}
// construct the UJSONRPC packet to start writing program
var dataSize = (new TextEncoder().encode(data)).length;
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' +
', "m": "start_write_program", "p": {' +
'"meta": {' +
'"created": ' + parseInt(Date.now()) +
', "modified": ' + parseInt(Date.now()) +
', "name": ' + '"' + btoa(projectName) + '"' +
', "type": ' + typeInt +
', "project_id":' + Math.floor(Math.random() * 1000) +
'}' +
', "fname": ' + '"' + projectName + '"' +
', "size": ' + dataSize +
', "slotid": ' + slotid +
'} }';
console.log("%cTuftsCEEO ", "color: #3ba336;", "constructed start_write_program script...");
// assign function to start sending packets after confirming blocksize and transferid
startWriteProgramCallback = [randomId, writePackageFunc];
console.log("%cTuftsCEEO ", "color: #3ba336;", "sending start_write_program script");
sendDATA(command);
// check if start_write_program received a response after 5 seconds
writeProgramSetTimeout = setTimeout(function () {
if (startWriteProgramCallback != undefined) {
if (funcAfterError != undefined) {
funcAfterError("5 seconds have passed without response... Please reboot the hub and try again.")
}
console.error("%cTuftsCEEO ", "color: #3ba336;", "5 seconds have passed without response... Please reboot the hub and try again.");
}
}, 5000)
// function to write the first packet of data
function writePackageFunc(blocksize, transferid) {
console.log("%cTuftsCEEO ", "color: #3ba336;", "in writePackageFunc...");
console.log("%cTuftsCEEO ", "color: #3ba336;", "stringified the entire data to send: ", data);
// when data's length is less than the blocksize limit of sending data
if (data.length <= blocksize) {
console.log("%cTuftsCEEO ", "color: #3ba336;", "data's length is less than the blocksize of ", blocksize);
// if the data's length is not zero (not empty)
if (data.length != 0) {
var dataToSend = data.substring(0, data.length); // get the entirety of data
console.log("%cTuftsCEEO ", "color: #3ba336;", "data's length is not zero, sending the entire data: ", dataToSend);
var base64data = btoa(dataToSend); // encode the packet to base64
UJSONRPC.writePackage(base64data, transferid); // send the packet
// writeProgram's callback defined by the user
if (writeProgramCallback != undefined) {
writeProgramCallback();
}
}
// the package to send is empty, so throw error
else {
throw new Error("package to send is initially empty");
}
}
// if the length of data to send is larger than the blocksize, send only a blocksize amount
// and save the remaining data to send packet by packet
else if (data.length > blocksize) {
console.log("%cTuftsCEEO ", "color: #3ba336;", "data's length is more than the blocksize of ", blocksize);
var dataToSend = data.substring(0, blocksize); // get the first block of packet
console.log("%cTuftsCEEO ", "color: #3ba336;", "sending the blocksize amount of data: ", dataToSend);
var base64data = btoa(dataToSend); // encode the packet to base64
var msgID = UJSONRPC.writePackage(base64data, transferid); // send the packet
var remainingData = data.substring(blocksize, data.length); // remove the portion just sent from data
console.log("%cTuftsCEEO ", "color: #3ba336;", "reassigning writePackageInformation with message ID: ", msgID);
console.log("%cTuftsCEEO ", "color: #3ba336;", "reassigning writePackageInformation with remainingData: ", remainingData);
// update package information to be used for sending remaining packets
writePackageInformation = [msgID, remainingData, transferid, blocksize];
}
}
}
/**
*
* @memberof! UJSONRPC
* @param {string} base64data base64 encoded data to send
* @param {string} transferid transferid of this program write process
* @returns {string} the randomly generated message id used to send this UJSONRPC script
*/
UJSONRPC.writePackage = function writePackage(base64data, transferid) {
var randomId = generateId();
var writePackageCommand = '{"i":' + '"' + randomId + '"' +
', "m": "write_package", "p": {' +
'"data": ' + '"' + base64data + '"' +
', "transferid": ' + '"' + transferid + '"' +
'} }';
sendDATA(writePackageCommand);
return randomId;
}
/**
* @memberof! UJSONRPC
*/
UJSONRPC.getStorageStatus = function getStorageStatus() {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' +
', "m": "get_storage_status"' +
'}';
sendDATA(command);
}
/**
* @memberof! UJSONRPC
* @param {string} slotid
*/
UJSONRPC.removeProject = function removeProject(slotid) {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' +
', "m": "remove_project", "p": {' +
'"slotid": ' + slotid +
'} }';
sendDATA(command);
}
/**
*
* @memberof! UJSONRPC
* @param {string} oldslotid
* @param {string} newslotid
*/
UJSONRPC.moveProject = function moveProject(oldslotid, newslotid) {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' +
', "m": "move_project", "p": {' +
'"old_slotid": ' + oldslotid +
', "new_slotid: ' + newslotid +
'} }';
sendDATA(command);
}
UJSONRPC.centerButtonLightUp = function centerButtonLightUp(color) {
var randomId = generateId();
var command = '{"i":' + '"' + randomId + '"' +
', "m": "scratch.center_button_lights", "p": {' +
'"color": ' + color +
'} }';
sendDATA(command);
}
//////////////////////////////////////////
// //
// Private Functions //
// //
//////////////////////////////////////////
/**
* @private
* @param {function} callback
*/
async function triggerCurrentState(callback) {
UJSONRPC.triggerCurrentState(callback);
}
/**
*
* @private
* @param {string} id
* @param {string} funcName
*/
function pushResponseCallback(id, funcName) {
var toPush = []; // [ ujson string id, function pointer ]
toPush.push(id);
toPush.push(funcName);
// responseCallbacks has elements in it
if (responseCallbacks.length > 0) {
var emptyFound = false; // empty index was found flag
// insert the pointer to the function where index is empty
for (var index in responseCallbacks) {
if (responseCallbacks[index] == undefined) {
responseCallbacks[index] = toPush;
emptyFound = true;
}
}
// if all indices were full, push to the back
if (!emptyFound) {
responseCallbacks.push(toPush);
}
}
// responseCallbacks current has no elements in it
else {
responseCallbacks.push(toPush);
}
}
/** Sleep function
* @private
* @param {number} ms Miliseconds to sleep
* @returns {Promise}
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/** generate random id for UJSONRPC messages
* @private
* @returns {string}
*/
function generateId() {
var generatedID = ""
var characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < 4; i++) {
var randomIndex = Math.floor(Math.random() * characters.length);
generatedID = generatedID + characters[randomIndex];
}
return generatedID;
}
/** Prompt user to select web serial port and make connection to SPIKE Prime
* <p> Effect Makes prompt in Google Chrome ( Google Chrome Browser needs "Experimental Web Interface" enabled) </p>
* <p> Note: </p>
* <p> This function is to be executed before reading in JSON RPC streams from the hub </p>
* <p> This function needs to be called when system is handling a user gesture (like button click) </p>
* @private
* @returns {boolean} True if web serial initialization is successful, false otherwise
*/
async function initWebSerial() {
try {
var success = false;
port = await navigator.serial.getPorts();
console.log("%cTuftsCEEO ", "color: #3ba336;", "ports:", port);
// select device
port = await navigator.serial.requestPort({
// filters:[filter]
});
// wait for the port to open.
try {
await port.open({ baudRate: 115200 });
}
catch (er) {
console.error("%cTuftsCEEO ", "color: #3ba336;", er);
// check if system requires baudRate syntax
if (er.message.indexOf("baudrate") > -1) {
console.log("%cTuftsCEEO ", "color: #3ba336;", "baudRate needs to be baudrate");
await port.open({ baudrate: 115200 });
}
// check if error is due to unsuccessful closing of previous port
else if (er.message.indexOf("close") > -1) {
if (funcAfterError != undefined) {
funcAfterError(er + "\nPlease try again. If error persists, refresh this environment.");
}
console.error("%cTuftsCEEO ", "color: #3ba336;", "Please check if you have any other window or app currently connected to your SPIKE Prime.");
await port.close();
}
// check if error in port.open was because it was already open
/* "failed to open serial port" */
else if (er.message.indexOf("open") > -1) {
try {
await port.close();
}
catch (err) {
console.error("%cTuftsCEEO ", "color: #3ba336;", err);
console.error("%cTuftsCEEO ", "color: #3ba336;", "Please check if you have any other window or app currently connected to your SPIKE Prime.");
}
}
else {
if (funcAfterError != undefined) {
funcAfterError(er + "\nPlease try again. If error persists, refresh this environment.");
}
console.error("%cTuftsCEEO ", "color: #3ba336;", "If error persists, refresh this environment");
}
await port.close();
}
if (port.readable) {
success = true;
}
else {
success = false;
}
return success;
} catch (e) {
if (e.message.indexOf("close") > -1) {
await port.close;
}
else {
console.log("%cTuftsCEEO ", "color: #3ba336;", "Cannot read port:", e);
if (funcAfterError != undefined) {
funcAfterError(e);
}
return false;
}
}
}
/** Initialize writer object before sending commands
* @private
*
*/
function setupWriter() {
// if writer not yet defined:
if (typeof writer === 'undefined') {
// set up writer for the first time
const encoder = new TextEncoderStream();
writableStreamClosed = encoder.readable.pipeTo(port.writable);
writer = encoder.writable.getWriter();
}
}
/** clean the json_string for concatenation into jsonline
* @private
*
* @param {any} json_string
* @returns {string}
*/
function cleanJsonString(json_string) {
var cleanedJsonString = "";
json_string = json_string.trim();
let findEscapedQuotes = /\\"/g;
cleanedJsonString = json_string.replace(findEscapedQuotes, '"');
cleanedJsonString = cleanedJsonString.substring(1, cleanedJsonString.length - 1);
// cleanedJsonString = cleanedJsonString.replace(findNewLines,'');
return cleanedJsonString;
}
/** Process the UJSON RPC script
*
* @private
* @param {any} lastUJSONRPC
* @param {string} [json_string="undefined"]
* @param {boolean} [testing=false]
* @param {any} callback
*/
async function processFullUJSONRPC(lastUJSONRPC, cleanedJsonString = "undefined", json_string = "undefined", testing = false, callback) {
try {
var parseTest = await JSON.parse(lastUJSONRPC)
if (testing) {
console.log("%cTuftsCEEO ", "color: #3ba336;", "processing FullUJSONRPC line: ", lastUJSONRPC);
}
// update hub information using lastUJSONRPC
if (parseTest["m"] == 0) {
await updateHubPortsInfo();
}
PrimeHubEventHandler();
if (funcWithStream) {
await funcWithStream();
}
}
catch (e) {
// don't throw error when failure of processing UJSONRPC is due to micropython
if (lastUJSONRPC.indexOf("Traceback") == -1 && lastUJSONRPC.indexOf(">>>") == -1 && json_string.indexOf("Traceback") == -1 && json_string.indexOf(">>>") == -1) {
if (funcAfterError != undefined) {
funcAfterError("Fatal Error: Please close any other window or program that is connected to your SPIKE Prime");
}
}
console.log(e);
console.log("%cTuftsCEEO ", "color: #3ba336;", "error parsing lastUJSONRPC: ", lastUJSONRPC);
console.log("%cTuftsCEEO ", "color: #3ba336;", "current jsonline: ", jsonline);
console.log("%cTuftsCEEO ", "color: #3ba336;", "current cleaned json_string: ", cleanedJsonString)
console.log("%cTuftsCEEO ", "color: #3ba336;", "current json_string: ", json_string);
console.log("%cTuftsCEEO ", "color: #3ba336;", "current value: ", value);
if (callback != undefined) {
callback();
}
}
}
/** Process a packet in UJSONRPC
* @private
*
*/
async function parsePacket(value, testing = false, callback) {
// console.log("%cTuftsCEEO ", "color: #3ba336;", value);
// stringify the packet to look for carriage return
var json_string = await JSON.stringify(value);
// remove quotation marks from json_string
var cleanedJsonString = cleanJsonString(json_string);
// cleanedJsonString = cleanedJsonString.replace(findNewLines,'');
// console.log(cleanedJsonString);
jsonline = jsonline + cleanedJsonString; // concatenate packet to data
jsonline = jsonline.trim();
// regex search for carriage return
let pattern = /\\r/g;
var carriageReIndex = jsonline.search(pattern);
// there is at least one carriage return in this packet
if (carriageReIndex > -1) {
//////////////////////////////// NEW parsePacket implementation ongoing since (29/12/20)
let jsonlineSplitByCR = jsonline.split(/\\r/); // array of jsonline split by \r
jsonline = ""; //reset jsonline
/*
each element in this array will be assessed for processing,
and the last element, if unable to be processed, will be concatenated to jsonline
*/
for (let i = 0; i < jsonlineSplitByCR.length; i++) {
// set lastUJSONRPC to an element in split array
lastUJSONRPC = jsonlineSplitByCR[i];
// remove any newline character in the beginning of lastUJSONRPC
if (lastUJSONRPC.search(/\\n/g) == 0)
lastUJSONRPC = lastUJSONRPC.substring(2, lastUJSONRPC.length);
/* Case 1: lastUJSONRPC is a valid, complete, and standard UJSONRPC packet */
if (lastUJSONRPC[0] == "{" && lastUJSONRPC[lastUJSONRPC.length - 1] == "}") {
let arrayLeftCurly = lastUJSONRPC.match(/{/g);
let arrayRightCurly = lastUJSONRPC.match(/}/g);
if (arrayLeftCurly.length === arrayRightCurly.length) {
/* Case 1A: complete packet*/
await processFullUJSONRPC(lastUJSONRPC, cleanedJsonString, json_string, testing, callback);
}
else {
/* Case 1B: {"i": 1234, "r": {} */
jsonline = lastUJSONRPC;
}
}
/* Case 3: lastUJSONRPC is a micropy print result */
else if (lastUJSONRPC != "" && lastUJSONRPC.indexOf('"p":') == -1 && lastUJSONRPC.indexOf('],') == -1 && lastUJSONRPC.indexOf('"m":') == -1 &&
lastUJSONRPC.indexOf('}') == -1 && lastUJSONRPC.indexOf('{"i":') == -1 && lastUJSONRPC.indexOf('{') == -1 ) {
/* filter reboot message */
var rebootMessage =
'Traceback (most recent call last): File "main.py", line 8, in <module> File "hub_runtime.py", line 1, in start File "event_loop/event_loop.py", line 1, in run_forever File "event_loop/event_loop.py", line 1, in step KeyboardInterrupt: MicroPython v1.12-1033-g97d7f7dd4 on 2020-09-18; LEGO Technic Large Hub with STM32F413xx Type "help()" for more in formation. >>> HUB: sync filesystems HUB: soft reboot'
let rebootMessageRemovedWS = rebootMessage.replace(/[' ']/g, "");
let lastUJSONRPCRemovedWS = lastUJSONRPC.replace(/[' ']/g, "");
if (rebootMessageRemovedWS.indexOf(lastUJSONRPCRemovedWS) == -1) {
console.log("%cTuftsCEEO ", "color: #3ba336;", "micropy print: ", lastUJSONRPC);
if (funcAfterPrint != undefined)
funcAfterPrint(lastUJSONRPC);
}
}
/* Case 3: lastUJSONRPC is only a portion of a standard UJSONRPC packet
Then lastUJSONRPC must be EITHER THE FIRST OR THE LAST ELEMENT in jsonlineSplitByCR
because
an incomplete UJSONRPC can either be
Case 3A: the beginning portion of a UJSONRPC packet with no \r in the end (LAST)
Case 3B: the last portion of a UJSONRPC packet with \r in the end (FIRST)
*/
else {
/* Case 3A: */
if (lastUJSONRPC[0] == "{") {
jsonline = lastUJSONRPC;
// console.log("TEST (last elemnt in split array): ", i == jsonlineSplitByCR.length-1);
// console.log("%cTuftsCEEO ", "color: #3ba336;", "jsonline was reset to:" + jsonline);
}
/* Case 3B: */
else {
/* the last portion of UJSONRPC cannot be concatenated to form a full packet
-> need to purge lastUJSONRPC
*/
}
}
}
}
}
/** Continuously take UJSON RPC input from SPIKE Prime
* @private
*/
async function streamUJSONRPC() {
try {
// COMMENTED BY JEREMY JUNG (DECEMBER/10/2020)
// var triggerCurrentStateInterval = setInterval(function() {
// UJSONRPC.triggerCurrentState();
// }, 500);
var firstReading = true;
// read when port is set up
while (port.readable) {
// initialize readers
const decoder = new TextDecoderStream();
const readableStreamClosed = port.readable.pipeTo(decoder.writable);
reader = decoder.readable.getReader();
// continuously get
while (true) {
try {
if (firstReading) {
console.log("%cTuftsCEEO ", "color: #3ba336;", "##### READING FIRST UJSONRPC LINE ##### CHECKING VARIABLES");
console.log("%cTuftsCEEO ", "color: #3ba336;", "jsonline: ", jsonline);
console.log("%cTuftsCEEO ", "color: #3ba336;", "lastUJSONRPC: ", lastUJSONRPC);
firstReading = false;
}
// read UJSON RPC stream ( actual data in {value} )
({ value, done } = await reader.read());
// log value
if (micropython_interpreter) {
console.log("%cTuftsCEEO ", "color: #3ba336;", value);
}
// console.log("%cTuftsCEEO ", "color: #3ba336;", value);
//concatenate UJSONRPC packets into complete JSON objects
if (value) {
await parsePacket(value);
}
if (done) {
serviceActive = false;
// reader has been canceled.
console.log("%cTuftsCEEO ", "color: #3ba336;", "[readLoop] DONE", done);
}
}
// error handler
catch (error) {
console.log("%cTuftsCEEO ", "color: #3ba336;", '[readLoop] ERROR', error);
serviceActive = false;
if (funcAfterDisconnect != undefined) {
funcAfterDisconnect();
}
if (funcAfterError != undefined) {
funcAfterError("SPIKE Prime hub has been disconnected");
}
writer.close();
//await writer.releaseLock();
await writableStreamClosed;
reader.cancel();
//await reader.releaseLock();
await readableStreamClosed.catch(reason => { });
await port.close();
writer = undefined;
reader = undefined;
jsonline = "";
lastUJSONRPC = undefined;
json_string = undefined;
cleanedJsonString = undefined;
break; // stop trying to read
}
} // end of: while (true) [reader loop]
// release the lock
reader.releaseLock();
} // end of: while (port.readable) [checking if readable loop]
console.log("%cTuftsCEEO ", "color: #3ba336;", "- port.readable is FALSE")
} // end of: trying to open port
catch (e) {
serviceActive = false;
// Permission to access a device was denied implicitly or explicitly by the user.
console.log("%cTuftsCEEO ", "color: #3ba336;", 'ERROR trying to open:', e);
}
}
/** Get the devices that are connected to each port on the SPIKE Prime
* <p> Effect: </p>
* <p> Modifies {ports} global variable </p>
* <p> Modifies {hub} global variable </p>
* @private
*/
async function updateHubPortsInfo() {
// if a complete ujson rpc line was read
if (lastUJSONRPC) {
var data_stream; //UJSON RPC info to be parsed
//get a line from the latest JSON RPC stream and parse to devices info
try {
data_stream = await JSON.parse(lastUJSONRPC);
data_stream = data_stream.p;
}
catch (e) {
console.log("%cTuftsCEEO ", "color: #3ba336;", "error parsing lastUJSONRPC at updateHubPortsInfo", lastUJSONRPC);
console.log("%cTuftsCEEO ", "color: #3ba336;", typeof lastUJSONRPC);
console.log("%cTuftsCEEO ", "color: #3ba336;", lastUJSONRPC.p);
if (funcAfterError != undefined) {
funcAfterError("Fatal Error: Please reboot the Hub and refresh this environment");
}
}
var index_to_port = ["A", "B", "C", "D", "E", "F"]
// iterate through each port and assign a device_type to {ports}
for (var key = 0; key < 6; key++) {
let device_value = { "device": "none", "data": {} }; // value to go in ports associated with the port letter keys
try {
var letter = index_to_port[key];
// get SMALL MOTOR information
if (data_stream[key][0] == 48) {
// parse motor information
var Mspeed = await data_stream[key][1][0];
var Mangle = await data_stream[key][1][1];
var Muangle = await data_stream[key][1][2];
var Mpower = await data_stream[key][1][3];
// populate value object
device_value.device = "smallMotor";
device_value.data = { "speed": Mspeed, "angle": Mangle, "uAngle": Muangle, "power": Mpower };
ports[letter] = device_value;
}
// get BIG MOTOR information
else if (data_stream[key][0] == 49) {
// parse motor information
var Mspeed = await data_stream[key][1][0];
var Mangle = await data_stream[key][1][1];
var Muangle = await data_stream[key][1][2];
var Mpower = await data_stream[key][1][3];
// populate value object
device_value.device = "bigMotor";
device_value.data = { "speed": Mspeed, "angle": Mangle, "uAngle": Muangle, "power": Mpower };
ports[letter] = device_value;
}
// get ULTRASONIC sensor information
else if (data_stream[key][0] == 62) {
// parse ultrasonic sensor information
var Udist = await data_stream[key][1][0];
// populate value object
device_value.device = "ultrasonic";
device_value.data = { "distance": Udist };
ports[letter] = device_value;
/* check if callback from wait_for_distance_farther_than() can be executed */
if (waitForDistanceFartherThanCallback != undefined) {
let thresholdDistance = waitForDistanceFartherThanCallback[0];
if (Udist > thresholdDistance) {
// current distance is farther than threshold, so execute callback
waitForDistanceFartherThanCallback[1]();
waitForDistanceFartherThanCallback = undefined; // reset callback
}
}
/* check if callback from wait_for_distance_closer_than() can be executed */
if (waitForDistanceCloserThanCallback != undefined) {
let thresholdDistance = waitForDistanceCloserThanCallback[0];
if (Udist < thresholdDistance) {
// current distance is closer than threshold, so execute callback
waitForDistanceCloserThanCallback[1]();
waitForDistanceCloserThanCallback = undefined; // reset callback
}
}
}
// get FORCE sensor information
else if (data_stream[key][0] == 63) {
// parse force sensor information
var Famount = await data_stream[key][1][0];
var Fbinary = await data_stream[key][1][1];
var Fbigamount = await data_stream[key][1][2];
// convert the binary output to boolean for "pressed" key
if (Fbinary == 1) {
var Fboolean = true;
} else {
var Fboolean = false;
}
// execute callback from ForceSensor.wait_until_pressed()
if (Fboolean) {
// execute call back from wait_until_pressed() if it is defined
funcAfterForceSensorPress !== undefined && funcAfterForceSensorPress();
// destruct callback function
funcAfterForceSensorPress = undefined;
// indicate that the ForceSensor was pressed
ForceSensorWasPressed = true;
}
// execute callback from ForceSensor.wait_until_released()
else {
// check if the Force Sensor was just released
if (ForceSensorWasPressed) {
ForceSensorWasPressed = false;
funcAfterForceSensorRelease !== undefined && funcAfterForceSensorRelease();
funcAfterForceSensorRelease = undefined;
}
}
// populate value object
device_value.device = "force";
device_value.data = { "force": Famount, "pressed": Fboolean, "forceSensitive": Fbigamount }
ports[letter] = device_value;
}
// get COLOR sensor information
else if (data_stream[key][0] == 61) {
// parse color sensor information
var Creflected = await data_stream[key][1][0];
var CcolorID = await data_stream[key][1][1];
var Ccolor = colorDictionary[CcolorID];
var Cr = await data_stream[key][1][2];
var Cg = await data_stream[key][1][3];
var Cb = await data_stream[key][1][4];
var rgb_array = [Cr, Cg, Cb];
// populate value object
device_value.device = "color";
// convert Ccolor to lower case because in the SPIKE APP the color is lower case
if (Ccolor !== undefined)
Ccolor = Ccolor.toLowerCase();
else
Ccolor = "null";
device_value.data = { "reflected": Creflected, "color": Ccolor, "RGB": rgb_array };
// execute wait_until_color callback when color matches its argument
if (waitUntilColorCallback != undefined)
if (Ccolor == waitUntilColorCallback[0]) {
waitUntilColorCallback[1]();
waitUntilColorCallback = undefined;
}
if (lastDetectedColor != Ccolor) {
if (funcAfterNewColor != undefined) {
funcAfterNewColor(Ccolor);
funcAfterNewColor = undefined;
}
lastDetectedColor = Ccolor;
}
ports[letter] = device_value;
}
/// NOTHING is connected
else if (data_stream[key][0] == 0) {
// populate value object
device_value.device = "none";
device_value.data = {};
ports[letter] = device_value;
}
ports.time = Date.now();
//parse hub information
var gyro_x = data_stream[6][0];
var gyro_y = data_stream[6][1];
var gyro_z = data_stream[6][2];
var gyro = [gyro_x, gyro_y, gyro_z];
hub["gyro"] = gyro;
var newOri = setHubOrientation(gyro);
// see if currently detected orientation is different from the last detected orientation
if (newOri !== lastHubOrientation) {
lastHubOrientation = newOri;
typeof funcAfterNewOrientation == "function" && funcAfterNewOrientation(newOri);
funcAfterNewOrientation = undefined;
}
var accel_x = data_stream[7][0];
var accel_y = data_stream[7][1];
var accel_z = data_stream[7][2];
var accel = [accel_x, accel_y, accel_z];
hub["accel"] = accel;
var posi_x = data_stream[8][0];
var posi_y = data_stream[8][1];
var posi_z = data_stream[8][2];
var pos = [posi_x, posi_y, posi_z];
hub["pos"] = pos;
} catch (e) {
console.log(e);
} //ignore errors
}
}
}
/** Catch hub events in UJSONRPC
* <p> Effect: </p>
* <p> Logs in the console when some particular messages are caught </p>
* <p> Assigns the hub events global variables </p>
* @private
*/
async function PrimeHubEventHandler() {
var parsedUJSON = await JSON.parse(lastUJSONRPC);
var messageType = parsedUJSON["m"];
//catch runtime_error made at ujsonrpc level
if (messageType == "runtime_error") {
var decodedResponse = atob(parsedUJSON["p"][3]);
decodedResponse = JSON.stringify(decodedResponse);
console.log("%cTuftsCEEO ", "color: #3ba336;", decodedResponse);
var splitData = decodedResponse.split(/\\n/); // split the code by every newline
// execute function after print if defined (only print the last line of error message)
if (funcAfterError != undefined) {
var errorType = splitData[splitData.length - 2];
// error is a syntax error
if (errorType.indexOf("SyntaxError") > -1) {
/* get the error line number*/
var lineNumberLine = splitData[splitData.length - 3];
console.log("%cTuftsCEEO ", "color: #3ba336;", "lineNumberLine: ", lineNumberLine);
var indexLine = lineNumberLine.indexOf("line");
var lineNumberSubstring = lineNumberLine.substring(indexLine, lineNumberLine.length);
var numberPattern = /\d+/g;
var lineNumber = lineNumberSubstring.match(numberPattern)[0];
console.log("%cTuftsCEEO ", "color: #3ba336;", lineNumberSubstring.match(numberPattern));
console.log("%cTuftsCEEO ", "color: #3ba336;", "lineNumber:", lineNumber);
console.log("%cTuftsCEEO ", "color: #3ba336;", "typeof lineNumber:", typeof lineNumber);
var lineNumberInNumber = parseInt(lineNumber) - 5;
console.log("%cTuftsCEEO ", "color: #3ba336;", "typeof lineNumberInNumber:", typeof lineNumberInNumber);
funcAfterError("line " + lineNumberInNumber + ": " + errorType);
}
else {
funcAfterError(errorType);
}
}
}
else if (messageType == 0) {
/*
DEV NOTE (26/12/2020):
messageType = 0 is regular UJSONRPC stream.
Pixel matrix SOMETIMES shows in this message, but exactly when is not clear.
*/
// console.log("%cTuftsCEEO ", "color: #3ba336;", lastUJSONRPC);
}
// storage information
else if (messageType == 1) {
var storageInfo = parsedUJSON["p"]["slots"]; // get info of all the slots
for (var slotid in storageInfo) {
hubProjects[slotid] = storageInfo[slotid]; // reassign hubProjects global variable
}
}
// battery status
else if (messageType == 2) {
batteryAmount = parsedUJSON["p"][1];
}
// give center button click, left, right (?)
else if (messageType == 3) {
console.log("%cTuftsCEEO ", "color: #3ba336;", lastUJSONRPC);
if (parsedUJSON.p[0] == "center") {
hubMainButton.pressed = true;
if (parsedUJSON.p[1] > 0) {
hubMainButton.pressed = false;
hubMainButton.duration = parsedUJSON.p[1];
}
}
else if (parsedUJSON.p[0] == "connect") {
hubBluetoothButton.pressed = true;
if (parsedUJSON.p[1] > 0) {
hubBluetoothButton.pressed = false;
hubBluetoothButton.duration = parsedUJSON.p[1];
}
}
else if (parsedUJSON.p[0] == "left") {
hubLeftButton.pressed = true;
// execute callback for wait_until_pressed() if defined
if (funcAfterLeftButtonPress != undefined ) {
funcAfterLeftButtonPress();
}
funcAfterLeftButtonPress = undefined;
if (parsedUJSON.p[1] > 0) {
hubLeftButton.pressed = false;
hubLeftButton.duration = parsedUJSON.p[1];
// execute callback for wait_until_released() if defined
if (funcAfterLeftButtonRelease != undefined ) {
funcAfterLeftButtonRelease();
}
funcAfterLeftButtonRelease = undefined;
}
}
else if (parsedUJSON.p[0] == "right") {
hubRightButton.pressed = true;
// execute callback for wait_until_pressed() if defined
if (funcAfterRightButtonPress != undefined) {
funcAfterRightButtonPress();
}
funcAfterRightButtonPress = undefined;
if (parsedUJSON.p[1] > 0) {
hubRightButton.pressed = false;
hubRightButton.duration = parsedUJSON.p[1];
// execute callback for wait_until_released() if defined
if (funcAfterRightButtonRelease != undefined) {
funcAfterRightButtonRelease();
}
funcAfterRightButtonRelease = undefined;
}
}
}
// gives orientation of the hub (leftside, up,..)
else if (messageType == 14) {
/* this data stream is about hub orientation */
var newOrientation = parsedUJSON.p;
// console.log(newOrientation);
if (newOrientation == "1") {
lastHubOrientation = "up";
}
else if (newOrientation == "4") {
lastHubOrientation = "down";
}
else if (newOrientation == "0") {
lastHubOrientation = "front";
}
else if (newOrientation == "3") {
lastHubOrientation = "back";
}
else if (newOrientation == "2") {
lastHubOrientation = "rightside";
}
else if (newOrientation == "5") {
lastHubOrientation = "leftside";
}
console.log("%cTuftsCEEO ", "color: #3ba336;", lastUJSONRPC);
}
else if (messageType == 7) {
if (funcAfterPrint != undefined) {
funcAfterPrint(">>> Program started!");
}
}
else if (messageType == 8) {
if (funcAfterPrint != undefined) {
funcAfterPrint(">>> Program finished!");
}
}
else if (messageType == 9) {
var encodedName = parsedUJSON["p"];
var decodedName = atob(encodedName);
hubName = decodedName;
if (triggerCurrentStateCallback != undefined) {
triggerCurrentStateCallback();
}
}
else if (messageType == 11) {
console.log("%cTuftsCEEO ", "color: #3ba336;", lastUJSONRPC);
}
else if (messageType == 12) {
// this is usually the response from trigger_current_state, don't console log to avoid spam
}
else if (messageType == 4) {
var newGesture = parsedUJSON.p;
if (newGesture == "3") {
hubGesture = "freefall";
hubGestures.push(hubGesture);
}
else if (newGesture == "2") {
hubGesture = "shaken";
hubGestures.push("shaken"); // the string is different at higher level
}
else if (newGesture == "1") {
hubFrontEvent = "doubletapped";
hubGesture = "doubletapped";
hubGestures.push(hubGesture);
}
else if (newGesture == "0") {
hubFrontEvent = "tapped";
hubGesture = "tapped";
hubGestures.push(hubGesture);
}
// execute funcAfterNewGesture callback that was taken at wait_for_new_gesture()
if (typeof funcAfterNewGesture === "function") {
funcAfterNewGesture(hubGesture);
funcAfterNewGesture = undefined;
}
console.log("%cTuftsCEEO ", "color: #3ba336;", lastUJSONRPC);
}
else {
// general parameters check
if (parsedUJSON["r"]) {
if (parsedUJSON["r"]["slots"]) {
var storageInfo = parsedUJSON["r"]["slots"]; // get info of all the slots
for (var slotid in storageInfo) {
hubProjects[slotid] = storageInfo[slotid]; // reassign hubProjects global variable
}
}
}
// getFirmwareInfo callback check
if (getFirmwareInfoCallback != undefined) {
if (getFirmwareInfoCallback[0] == parsedUJSON["i"]) {
var version = parsedUJSON["r"]["runtime"]["version"];
var stringVersion = ""
for (var index in version) {
if (index < version.length - 1) {
stringVersion = stringVersion + version[index] + ".";
}
else {
stringVersion = stringVersion + version[index];
}
}
// console.log("%cTuftsCEEO ", "color: #3ba336;", "firmware version: ", stringVersion);
getFirmwareInfoCallback[1](stringVersion);
}
}
// COMMENTED BY JEREMY JUNG ON DECEMBER 10TH AFTER REMOVING TRIGGER_CURRENT_STATE INTERVAL
// if (parsedUJSON.r !== undefined && parsedUJSON.r !== null) {
// if (Object.keys(parsedUJSON.r).length !== 0 && parsedUJSON.r.constructor === Object) {
// console.log("%cTuftsCEEO ", "color: #3ba336;", "received response: ", lastUJSONRPC);
// }
// }
// else {
// console.log("%cTuftsCEEO ", "color: #3ba336;", "received response: ", lastUJSONRPC);
// }
console.log("%cTuftsCEEO ", "color: #3ba336;", "received response: ", lastUJSONRPC);
/* See if any of the stored responseCallbacks need to be executed due to this UJSONRPC response */
for (var index = 0; index < responseCallbacks.length; index++) {
var currCallbackInfo = responseCallbacks[index];
if (currCallbackInfo != undefined) {
if (currCallbackInfo[0] == parsedUJSON["i"]) {
/* the message id of UJSONRPC corresponds to that of a response callback */
var response = "null";
// parse motor stoppage reason responses
if (parsedUJSON["r"] == 0) {
response = "done";
}
else if (parsedUJSON["r"] == 2) {
response = "stalled";
}
// execute callback with the response
currCallbackInfo[1](response);
// empty the index of which callback that was just executed
responseCallbacks[index] = undefined;
}
}
}
// execute the callback function after sending start_write_program UJSONRPC
if (startWriteProgramCallback != undefined) {
console.log("%cTuftsCEEO ", "color: #3ba336;", "startWriteProgramCallback is defined. Looking for matching mesasage id: ", startWriteProgramCallback[0]);
// check if the message id of UJSONRPC corresponds to that of a response callback
if (startWriteProgramCallback[0] == parsedUJSON["i"]) {
console.log("%cTuftsCEEO ", "color: #3ba336;", "matching message id detected with startWriteProgramCallback[0]: ", startWriteProgramCallback[0])
// get the information for the packet sending
var blocksize = parsedUJSON["r"]["blocksize"]; // maximum size of each packet to be sent in bytes
var transferid = parsedUJSON["r"]["transferid"]; // id to use for transferring this program
console.log("%cTuftsCEEO ", "color: #3ba336;", "executing writePackageFunc expecting transferID of ", transferid);
// execute callback
await startWriteProgramCallback[1](blocksize, transferid);
console.log("%cTuftsCEEO ", "color: #3ba336;", "deallocating startWriteProgramCallback");
// deallocate callback
startWriteProgramCallback = undefined;
}
}
// check if the program should write packages for a program
if (writePackageInformation != undefined) {
console.log("%cTuftsCEEO ", "color: #3ba336;", "writePackageInformation is defined. Looking for matching mesasage id: ", writePackageInformation[0]);
// check if the message id of UJSONRPC corresponds to that of the first write_package script that was sent
if (writePackageInformation[0] == parsedUJSON["i"]) {
console.log("%cTuftsCEEO ", "color: #3ba336;", "matching message id detected with writePackageInformation[0]: ", writePackageInformation[0]);
// get the information for the package sending process
var remainingData = writePackageInformation[1];
var transferID = writePackageInformation[2];
var blocksize = writePackageInformation[3];
// the size of the remaining data to send is less than or equal to blocksize
if (remainingData.length <= blocksize) {
console.log("%cTuftsCEEO ", "color: #3ba336;", "remaining data's length is less than or equal to blocksize");
// the size of remaining data is not zero
if (remainingData.length != 0) {
var dataToSend = remainingData.substring(0, remainingData.length);
console.log("%cTuftsCEEO ", "color: #3ba336;", "reminaing data's length is not zero, sending entire remaining data: ", dataToSend);
var base64data = btoa(dataToSend);
UJSONRPC.writePackage(base64data, transferID);
console.log("%cTuftsCEEO ", "color: #3ba336;", "deallocating writePackageInforamtion")
if (writeProgramCallback != undefined) {
writeProgramCallback();
}
writePackageInformation = undefined;
}
}
// the size of remaining data is more than the blocksize
else if (remainingData.length > blocksize) {
console.log("%cTuftsCEEO ", "color: #3ba336;", "remaining data's length is more than blocksize");
var dataToSend = remainingData.substring(0, blocksize);
console.log("%cTuftsCEEO ", "color: #3ba336;", "sending blocksize amount of data: ", dataToSend)
var base64data = btoa(dataToSend);
var messageid = UJSONRPC.writePackage(base64data, transferID);
console.log("%cTuftsCEEO ", "color: #3ba336;", "expected response with message id of ", messageid);
var remainingData = remainingData.substring(blocksize, remainingData.length);
writePackageInformation = [messageid, remainingData, transferID, blocksize];
}
}
}
}
}
/** Get the orientation of the hub based on gyroscope values
*
* @private
* @param {(number|Array)} gyro
*/
function setHubOrientation(gyro) {
var newOrientation;
if (gyro[0] < 500 && gyro[0] > -500) {
if (gyro[1] < 500 && gyro[1] > -500) {
if (gyro[2] > 500) {
newOrientation = "front";
}
else if (gyro[2] < -500) {
newOrientation = "back";
}
}
else if (gyro[1] > 500) {
newOrientation = "up";
}
else if (gyro[1] < -500) {
newOrientation = "down";
}
} else if (gyro[0] > 500) {
newOrientation = "rightside";
}
else if (gyro[0] < -500) {
newOrientation = "leftside";
}
return newOrientation;
}
/**
*
* @private
* @param {any} rawContent
* @returns {string}
*/
async function parseWaitForSeconds(rawContent) {
let index_waitForSeconds = await rawContent.indexOf("wait_for_seconds(");
if (index_waitForSeconds > -1) {
//find the index of rawContent at which the waitForSeconds function ends
let index_lastparen = await rawContent.indexOf(")", index_waitForSeconds);
//divide the rawContent into parts before the waitForSeconds and after
let first_rawContent_part = await rawContent.substring(0, index_waitForSeconds);
let second_rawContent_part = await rawContent.substring(index_lastparen + 1, rawContent.length);
//find the argument of the waitForSeconds
let waitForSeconds_string = await rawContent.substring(index_waitForSeconds, index_lastparen + 1);
let index_first_paren = await waitForSeconds_string.indexOf("(");
let index_last_paren = await waitForSeconds_string.indexOf(")");
let tagName = await waitForSeconds_string.substring(index_first_paren + 1, index_last_paren);
// get the tag's value from the cloud
var yieldCommand = "yield(" + tagName + "000)";
//send the final UJSONRPC script to the hub.
let final_RPC_command;
final_RPC_command = await first_rawContent_part + yieldCommand + second_rawContent_part;
return parseWaitForSeconds(final_RPC_command);
}
else {
return rawContent;
}
}
// public members
return {
init: init,
sendDATA: sendDATA,
rebootHub: rebootHub,
reachMicroPy: reachMicroPy,
executeAfterInit: executeAfterInit,
executeAfterPrint: executeAfterPrint,
executeAfterError: executeAfterError,
executeAfterDisconnect: executeAfterDisconnect,
executeWithStream: executeWithStream,
getPortsInfo: getPortsInfo,
getPortInfo: getPortInfo,
getBatteryStatus: getBatteryStatus,
getFirmwareInfo: getFirmwareInfo,
getHubInfo: getHubInfo,
getHubName: getHubName,
getProjects: getProjects,
isActive: isActive,
getBigMotorPorts: getBigMotorPorts,
getSmallMotorPorts: getSmallMotorPorts,
getUltrasonicPorts: getUltrasonicPorts,
getColorPorts: getColorPorts,
getForcePorts: getForcePorts,
getMotorPorts: getMotorPorts,
getMotors: getMotors,
getDistanceSensors: getDistanceSensors,
getColorSensors: getColorSensors,
getForceSensors: getForceSensors,
getLatestUJSON: getLatestUJSON,
getBluetoothButton: getBluetoothButton,
getMainButton: getMainButton,
getLeftButton: getLeftButton,
getRightButton: getRightButton,
getHubGesture: getHubGesture,
getHubEvent: getHubEvent,
getHubOrientation: getHubOrientation,
Motor: Motor,
PrimeHub: PrimeHub,
ForceSensor: ForceSensor,
DistanceSensor: DistanceSensor,
ColorSensor: ColorSensor,
MotorPair: MotorPair,
writeProgram: writeProgram,
stopCurrentProgram: stopCurrentProgram,
executeProgram: executeProgram,
micropython: micropython // for final projects
};
}
/*
Project Name: SPIKE Prime Web Interface
File name: micropyUtils.js
Author: Jeremy Jung
Last update: 10/22/20
Description: utility class to convert javascript variables to python variablse
for EN1 Simple Robotics final projects
Credits/inspirations:
History:
Created by Jeremy on 10/18/20
(C) Tufts Center for Engineering Education and Outreach (CEEO)
NOTE:
strings need to be in single quotes
*/
var micropyUtils = {};
micropyUtils.storedVariables = {}; // all variables declared in window
micropyUtils.beginVariables = {}; // all variables declared in window before code
// automatically initialize microPyUtils to exclude predeclared variables when window loads
// this initializes after global variable declarations but before hoisted functions in <script> are executed
window.onload = function () {
console.log("onload")
//micropyUtils.init();
}
// this initializes after global variable declarations but before hoisted functions in <script> are executed
// this runs earlier than onload
document.addEventListener("DOMContentLoaded", function () {
console.log("DOMCONtent")
//micropyUtils.init();
})
//////////////////////////////////////////
// //
// Public Functions //
// //
//////////////////////////////////////////
// remember global variables declared BEFORE user code
micropyUtils.remember = function () {
for (var name in window) {
micropyUtils.beginVariables[name] = window[name];
}
console.log("remembered predeclared variables ", micropyUtils.beginVariables)
}
/* parse and add all local variable declarations to micropyUtils.storedVariables
var aString = "hi" or var aString = 'hi' > {aString: "hi"}
*/
// micropyUtils.addLocalVariables = function() {
// // get the function definition of caller
// var thisFunction = arguments.callee.caller.toString();
// console.log(thisFunction);
// // split function scope by newlines
// var newLineRule = /\n/g
// var arrayLines = thisFunction.split(newLineRule);
// // filter lines that dont contain var, or contains function
// var arrayVarLines = [];
// for ( var index in arrayLines ) {
// if ( arrayLines[index].indexOf("var") > -1 ) {
// // filter out functions and objects
// if (arrayLines[index].indexOf("function") == -1 && arrayLines[index].indexOf("{") == -1 && arrayLines[index].indexOf("}") == -1) {
// arrayVarLines.push(arrayLines[index]);
// }
// }
// }
// var parseRule = /[[ ]/g
// for ( var index in arrayVarLines ) {
// // process line
// var processedLine = micropyUtils.processString(arrayVarLines[index]);
// // get [datatype] object = value format
// var listParsedLine = processedLine.split(parseRule);
// //listParsedLine = listParsedLine.split(/[=]/g)
// var keyValue = micropyUtils.checkString(listParsedLine);
// // insert into variables
// for ( var name in keyValue ) {
// micropyUtils.storedVariables[name] = keyValue[name];
// }
// }
// }
// initialize utility object (find window variables to exclude from conversion)
micropyUtils.init = function () {
var excludeVariables = {};
// get variables to exclude
for (var compare in micropyUtils.beginVariables) {
// if variables found on remember() are defined, these are not user-generated variables, so flag them predeclared
if (typeof micropyUtils.beginVariables[compare] !== "undefined") {
excludeVariables[compare] = "predeclared"
}
}
// append window variables to micropyUtils.storedVariables, but exclude those predeclared
for (var name in window) {
if (excludeVariables[name] != "predeclared") {
micropyUtils.storedVariables[name] = window[name];
}
}
console.log("stored Variabls in init: ", micropyUtils.storedVariables);
}
micropyUtils.makeMicroPyDeclarations = function () {
// initialize microPyUtils
micropyUtils.init();
/* add local variables of the caller of this function */
// get the function definition of caller
/* parse and add all local variable declarations to micropyUtils.storedVariables
var aString = "hi" or var aString = 'hi' > {aString: "hi"}
*/
var thisFunction = arguments.callee.caller.toString();
console.log(thisFunction);
// split function scope by newlines
var newLineRule = /\n/g
var arrayLines = thisFunction.split(newLineRule);
// filter lines that dont contain var, or contains function
var arrayVarLines = [];
for (var index in arrayLines) {
if (arrayLines[index].indexOf("var") > -1) {
// filter out functions and objects
if (arrayLines[index].indexOf("function") == -1 && arrayLines[index].indexOf("{") == -1 && arrayLines[index].indexOf("}") == -1) {
arrayVarLines.push(arrayLines[index]);
}
}
}
var parseRule = /[[ ]/g
for (var index in arrayVarLines) {
// process line
var processedLine = micropyUtils.processString(arrayVarLines[index]);
// get [datatype] object = value format
var listParsedLine = processedLine.split(parseRule);
//listParsedLine = listParsedLine.split(/[=]/g)
var keyValue = micropyUtils.checkString(listParsedLine);
// insert into variables
for (var name in keyValue) {
micropyUtils.storedVariables[name] = keyValue[name];
}
}
/* generate lines of micropy variable declarations */
var lines = [];
for (var name in micropyUtils.storedVariables) {
var variableName = name;
if (typeof micropyUtils.storedVariables[name] !== "function" && typeof micropyUtils.storedVariables[name] !== "object") {
var variableValue = micropyUtils.convertToString(micropyUtils.storedVariables[name]);
lines.push("" + variableName + " = " + variableValue);
}
}
return lines
}
//////////////////////////////////////////
// //
// Private Functions //
// //
//////////////////////////////////////////
// add local variables in which scope the utility tool is being used
micropyUtils.addVariables = function (object) {
for (var name in object) {
micropyUtils.storedVariables[name] = object[name];
}
}
// filter out unparsable variable declarations and process valid ones
micropyUtils.processString = function (input) {
var result = input.trim();
var removeRule = /[;]/g
result = result.replace(removeRule, "");
var doubleQuotes = /[",']/g
result = result.replace(doubleQuotes, "");
return result;
}
// return key value pair of variable declaration
micropyUtils.checkString = function (list) {
var result = {}; // {variable name: variable value}
// check if list starts with var
if (list[0] == "var") {
var variableName = list[1];
// check assignment operator
if (list[2] == "=") {
// assume the right hand side of assignment operator is only one term
var value = list[3];
result[variableName] = micropyUtils.convertFromString(value);
return result;
}
else {
return undefined;
}
}
else {
return undefined;
}
}
// convert string value to correct data type value
micropyUtils.convertFromString = function (value) {
// value is not a number
if (isNaN(parseInt(value))) {
// value is a bool
if (value.indexOf("true") > -1) {
return true;
}
else if (value.indexOf("false") > -1) {
return false;
}
// value is a string
else {
return value;
}
}
else {
// value is a number
var number = Number(value);
return number;
}
}
// convert datatype value to string value
micropyUtils.convertToString = function (value) {
console.log(value)
// value is a string, enclose with single quots and return
if (typeof value == "string") {
return "'" + value + "'";
}
else {
// value is a number
if (Number(value)) {
return "" + value;
}
// value is boolean
else {
if (value) {
return "True";
}
else {
return "False";
}
}
}
}
//////////////////////////////////////////
// //
// Main //
// //
//////////////////////////////////////////
// remember predeclared variables when this file is loaded
micropyUtils.remember();