sumorobot-web/assets/js/sumorobot.js

575 lines
21 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* disable enable hotkeys */
var hotkeys = true;
/* enable disable remote control */
var remoteControl = false;
/* the local/remote server URL */
//var robotServer = "10.42.0.1";
var robotServer = "iot.koodur.com";
/* ace editor object */
var codingEditor = null;
/* read only ace editor object */
var readOnlyCodingEditor = null;
/* the sumorobot code */
var sumocode = "";
/* the sumorobot object */
var sumorobot = null;
/* Blockly workspace */
var workspace = null;
/* the sumorobot state */
var sumostart = false;
/* disable / enable coding mode */
var codingEnabled = false;
/* disable / enable live stream */
var liveStreamVisible = false;
/* control_if block id */
var controlBlockId = "";
/* last hightlighted block id */
var lastHighlighted = "";
/* block highlight WebSocket */
var blockHighlight = null;
var Sumorobot = function(wsUri) {
/* assign the WebSocket URI */
this.wsUri = wsUri;
/* start connecting to the WebSocket */
this.connect();
/* to ping the robot */
this.watchdogTimer = null;
};
Sumorobot.prototype.connect = function() {
/* to have access to this object */
var self = this;
this.websocket = new WebSocket(this.wsUri);
/* when the WebSocket gets connected */
this.websocket.onopen = function(evt) {
console.log("INFO websocket connected");
/* setup a timer to ping the robot */
self.watchdogTimer = setInterval(function() {
/* send a ping to the robot */
//console.log("ping the robot")
self.send("ping");
}, 2000);
};
/* when the WebSocket closes */
this.websocket.onclose = function(evt) {
console.log("INFO websocket disconnected");
/* clear the pinging */
clearInterval(self.watchdogTimer);
/* Try to recnnect to the sumorobot */
self.connect();
};
/* when there is a message from the WebSocket */
this.websocket.onmessage = function(evt) {
/* when scope is received */
var data = evt.data.replace(/'/g, '"').toLowerCase();
console.log(data);
var battery = JSON.parse(data)["battery_voltage"];
$("#battery").html(battery + "V");
$("#battery").addClass("connected");
};
/* when there is an WebSocket error */
this.websocket.onerror = function(err) {
console.log("ERROR websocket error: " + err);
};
};
Sumorobot.prototype.send = function(msg) {
/* ready state constants: CONNECTING 0, OPEN 1, CLOSING 2, CLOSED 3 */
/* https://developer.mozilla.org/en-US/docs/Web/API/WebSocket */
if (this.websocket.readyState == 1) {
this.websocket.send(msg);
}
};
Sumorobot.prototype.close = function() {
/* close the WebSocket connection */
this.websocket.close();
};
/* function to set local storage */
function getLocalStorageItem(item) {
/* when the local storage doesn't exist, return empty string */
if (typeof(Storage) === "undefined") return "";
/* otherwise return item from the local storage*/
return localStorage.getItem(item);
}
/* function to set local storage */
function setLocalStorageItem(item, value) {
/* when local storage doesn't exist, return */
if (typeof(Storage) === "undefined") return;
/* otherwise set the item to the local storage */
localStorage.setItem(item, value)
}
window.onload = function() {
$("#robot-id").val(getLocalStorageItem("sumorobot.robotId"));
/* load read only ace editor */
readOnlyCodingEditor = ace.edit("readOnlyBlocklyCode");
/* set the style */
readOnlyCodingEditor.setTheme("ace/theme/textmate");
readOnlyCodingEditor.session.setMode("ace/mode/python");
readOnlyCodingEditor.session.setTabSize(2);
/* make as read only */
readOnlyCodingEditor.setReadOnly(true);
/* disable scrolling warning */
readOnlyCodingEditor.$blockScrolling = Infinity;
/* load ace editor */
codingEditor = ace.edit("blocklyCode");
/* set the style */
codingEditor.setTheme("ace/theme/textmate");
codingEditor.session.setMode("ace/mode/python");
codingEditor.session.setTabSize(2);
/* disable scrolling warning */
codingEditor.$blockScrolling = Infinity;
/* enable autocomplete */
ace.require("ace/ext/language_tools");
codingEditor.setOptions({
enableSnippets: true,
enableLiveAutocompletion: true,
enableBasicAutocompletion: true
});
/* add autocomplete keywords */
codingEditor.completers.push({
getCompletions: function(editor, session, pos, prefix, callback) {
callback(null, [
{value: "STOP", score: 1000, meta: "sumorobot"},
{value: "LEFT", score: 1000, meta: "sumorobot"},
{value: "RIGHT", score: 1000, meta: "sumorobot"},
{value: "FORWARD", score: 1000, meta: "sumorobot"},
{value: "BACKWARD", score: 1000, meta: "sumorobot"},
{value: "STATUS", score: 1000, meta: "sumorobot"},
{value: "LEFT_LINE", score: 1000, meta: "sumorobot"},
{value: "RIGHT_LINE", score: 1000, meta: "sumorobot"},
{value: "sumorobot", score: 1000, meta: "sumorobot"},
{value: "move", score: 1000, meta: "sumorobot"},
{value: "sleep", score: 1000, meta: "sumorobot"},
{value: "set_led", score: 1000, meta: "sumorobot"},
{value: "is_line", score: 1000, meta: "sumorobot"},
{value: "get_line", score: 1000, meta: "sumorobot"},
{value: "set_servo", score: 1000, meta: "sumorobot"},
{value: "is_opponent", score: 1000, meta: "sumorobot"},
{value: "calibrate_line", score: 1000, meta: "sumorobot"},
{value: "get_battery_voltage", score: 1000, meta: "sumorobot"},
{value: "get_opponent_distance", score: 1000, meta: "sumorobot"}
]);
}
});
/* set the code to the saved code from local storage or empty */
codingEditor.setValue(getLocalStorageItem("sumorobot.code") || "");
/* clear the selection after setting the value */
codingEditor.clearSelection();
/* add an change listener for the code editor */
codingEditor.on("change", function() {
/* when change occurs, save the new code to the localstorage */
setLocalStorageItem("sumorobot.code", codingEditor.getValue())
});
/* change the if block to be more cheerful */
Blockly.Msg.LOGIC_HUE = '#44CC00';
/* remote previous and next statement from control_if block */
Blockly.defineBlocksWithJsonArray([
{
"type": "controls_if",
"message0": "%{BKY_CONTROLS_IF_MSG_IF} %1",
"args0": [
{
"type": "input_value",
"name": "IF0",
"check": "Boolean"
}
],
"message1": "%{BKY_CONTROLS_IF_MSG_THEN} %1",
"args1": [
{
"type": "input_statement",
"name": "DO0"
}
],
"colour": "%{BKY_LOGIC_HUE}",
"helpUrl": "%{BKY_CONTROLS_IF_HELPURL}",
"mutator": "controls_if_mutator",
"extensions": ["controls_if_tooltip"]
}
]);
/* make control_if mutator icon bigger */
Blockly.Icon.prototype.renderIcon = function(cursorX) {
if (this.collapseHidden && this.block_.isCollapsed()) {
this.iconGroup_.setAttribute('display', 'none');
return cursorX;
}
this.iconGroup_.setAttribute('display', 'block');
var SIZE = 1.7;
var TOP_MARGIN = 2;
var LEFT_MARGIN = 5;
var width = this.SIZE;
if (this.block_.RTL) {
cursorX -= width;
}
this.iconGroup_.setAttribute('transform',
'translate(' + LEFT_MARGIN + ',' + TOP_MARGIN + ') scale(' + SIZE + ')');
this.computeIconLocation();
if (this.block_.RTL) {
cursorX -= Blockly.BlockSvg.SEP_SPACE_X;
} else {
cursorX += width + Blockly.BlockSvg.SEP_SPACE_X;
}
return cursorX;
};
/* when mouse click occures on Blockly workspace */
Blockly.utils.isRightButton = function(e) {
var target = e.target;
/* when control_if block is in use */
if (controlBlockId != "") {
/* when the user clicks anywhere outside the mutator and not on the mutator icon */
if (!$(target).is('.blocklyBubbleCanvas') && !$(target).parents().is('.blocklyBubbleCanvas')) {
if (!$(target).is('.blocklyIconGroup') && !$(target).parents().is('.blocklyIconGroup')) {
/* hide the mutator */
workspace.getBlockById(controlBlockId).mutator.setVisible(false);
}
}
}
/* disable right click on Blockly workspace */
return false;
};
Blockly.Blocks['sumorobot_sleep'] = {
init: function() {
this.setColour("#E64C00");
this.appendDummyInput()
.appendField("sleep")
.appendField(new Blockly.FieldTextInput('1000',
Blockly.FieldNumber.numberValidator), 'SLEEP');
this.setPreviousStatement(true);
this.setNextStatement(true);
}
};
Blockly.Blocks['sumorobot_move'] = {
init: function() {
var OPERATORS = [
['move stop', 'STOP'],
['move left', 'LEFT'],
['move right', 'RIGHT'],
['move forward', 'FORWARD'],
['move backward', 'BACKWARD']
];
this.setColour("#E60000");
var dropdown = new Blockly.FieldDropdown(OPERATORS);
this.appendDummyInput().appendField(dropdown, 'MOVE');
this.setPreviousStatement(true);
this.setNextStatement(true);
}
};
Blockly.Blocks['sumorobot_opponent'] = {
init: function() {
this.setColour("#0099E6");
this.appendDummyInput().appendField('opponent');
this.setOutput(true, 'Boolean');
}
};
Blockly.Blocks['sumorobot_line'] = {
init: function() {
var OPERATORS = [
['line left', 'LEFT'],
['line right', 'RIGHT']
];
this.setColour("#E6BF00");
var dropdown = new Blockly.FieldDropdown(OPERATORS);
this.appendDummyInput().appendField(dropdown, 'LINE');
this.setOutput(true, 'Boolean');
}
};
Blockly.Python['sumorobot_sleep'] = function(block) {
var code = 'sumorobot.sleep(' + parseFloat(block.getFieldValue('SLEEP')) + ', "' + block.id + '")\n';
return code;
};
Blockly.Python['sumorobot_move'] = function(block) {
var code = 'sumorobot.move(' + block.getFieldValue('MOVE') + ', "' + block.id + '")\n';
return code;
};
Blockly.Python['sumorobot_opponent'] = function(block) {
var code = 'sumorobot.is_opponent("' + block.id + '")';
return [code, Blockly.Python.ORDER_ATOMIC];
};
Blockly.Python['sumorobot_line'] = function(block) {
var code = 'sumorobot.is_line(' + block.getFieldValue('LINE') + ', "' + block.id + '")';
return [code, Blockly.Python.ORDER_ATOMIC];
};
/* inject Blockly */
var blocklyArea = document.getElementById('blocklyArea');
var blocklyDiv = document.getElementById('blocklyDiv');
workspace = Blockly.inject(blocklyDiv, {
scrollbars: false,
media: 'assets/blockly/media/',
trashcan: true,
sounds: true,
zoom: {
wheel: true,
controls: true,
startScale: 1.2
},
toolbox: document.getElementById('toolbox')
});
/* on Blockly resize */
var onresize = function(e) {
// compute the absolute coordinates and dimensions of blocklyArea.
var element = blocklyArea;
var x = 0;
var y = 0;
do {
x += element.offsetLeft;
y += element.offsetTop;
element = element.offsetParent;
} while (element);
/* position blocklyDiv over blocklyArea */
blocklyDiv.style.left = x + 'px';
blocklyDiv.style.top = y + 'px';
blocklyDiv.style.width = blocklyArea.offsetWidth + 'px';
blocklyDiv.style.height = blocklyArea.offsetHeight + 'px';
};
window.addEventListener('resize', onresize, false);
onresize();
Blockly.svgResize(workspace);
/* retrieve the blocks */
var xml = Blockly.Xml.textToDom(getLocalStorageItem("sumorobot.blockly"));
/* resume the blocks */
Blockly.Xml.domToWorkspace(xml, workspace);
/* on Blockly code change */
function onCodeChanged(event) {
/* if the if condition block was created */
if (event.type == Blockly.Events.CREATE && event.xml.getAttributeNode("type").nodeValue == "controls_if") {
/* remember the control_if block id */
controlBlockId = event.blockId;
/* get the control_if block object */
var block = workspace.getBlockById(event.blockId);
/* if the control_if block doesn't already have an else */
if (block.elseCount_ == 0) {
/* automatically add the else statement input */
block.elseCount_ = 1;
block.updateShape_();
}
/* if the if condition block was removed */
} else if (event.type == Blockly.Events.DELETE && event.oldXml.getAttributeNode("type").nodeValue == "controls_if") {
/* remove the control_if block id */
controlBlockId = "";
/* enable the if condition block */
workspace.updateToolbox(document.getElementById("toolbox"));
}
/* only process change and move commands */
if (event.type != Blockly.Events.CHANGE && event.type != Blockly.Events.MOVE) return;
/* generate code from the used blocks */
sumocode = Blockly.Python.workspaceToCode(workspace);
/* show the code in the ace editor, filter out block IDs */
readOnlyCodingEditor.setValue("\n" + sumocode.replace(/[,]?[ ]?"(.*?)"/g, ""));
readOnlyCodingEditor.clearSelection();
/* save the code to the local storage */
var xml = Blockly.Xml.workspaceToDom(workspace);
localStorage.setItem("sumorobot.blockly", Blockly.Xml.domToText(xml));
/* if control_if block is used */
if (controlBlockId != "") {
/* disable the if condition block */
workspace.updateToolbox(document.getElementById("toolbox_no_if"));
}
}
/* add a change listener to Blockly */
workspace.addChangeListener(onCodeChanged);
/* key down event */
$(document).keydown(function(e) {
/* if the hotkeys are disabled or the alt key is not pressed, don't use hotkeys */
if (hotkeys == false || e.altKey == false) return;
/* prevent typing in textfields */
e.preventDefault();
/* select the hotkey */
switch(e.which) {
case 32: // space bar
sumostart = !sumostart;
if (sumostart) {
$(".btn-start").addClass("hover");
$(".btn-start").click();
} else {
$(".btn-stop").addClass("hover");
$(".btn-stop").click();
}
break;
case 37: // left
if (remoteControl) sumorobot.send("left");
break;
case 38: // up
if (remoteControl) sumorobot.send("forward");
break;
case 39: // right
if (remoteControl) sumorobot.send("right");
break;
case 40: // down
if (remoteControl) sumorobot.send("backward");
break;
case 67: // c
$("#panel").toggle();
break;
case 76: // l
$("#stream").toggle();
/* toggle live steam visible */
liveStreamVisible = !liveStreamVisible;
/* if not in coding mode */
if (codingEnabled == false) {
$("#readOnlyBlocklyCode").toggle();
}
break;
case 80: // p
$("#blocklyDiv").toggle();
$("#blocklyArea").toggle();
$("#blocklyCode").toggle();
if (liveStreamVisible == false) {
$("#readOnlyBlocklyCode").toggle();
}
/* toggle coding enabled */
codingEnabled = !codingEnabled;
if (codingEnabled) {
/* resize the coding editor */
codingEditor.resize();
/* focus, so the user can start coding */
codingEditor.focus();
}
break;
case 82: // r
/* toggle remote control */
remoteControl = !remoteControl;
break;
case 83: // s
$(".btn-stop").addClass("hover");
$(".btn-stop").click();
break;
case 84: // t
sumorobot.send("calibrate_line");
break;
case 87: // w
$(".btn-start").addClass("hover");
$(".btn-start").click();
break;
}
});
/* key up event */
$(document).keyup(function(e) {
/* if the hotkeys are disabled or the focused element is a textarea or text input, don't use hotkeys */
if (hotkeys == false || e.altKey == false) return;
/* remove hover from buttons */
$('.btn').removeClass('hover');
/* if arrow keys */
if (e.which == 37 || e.which == 38 || e.which == 39 || e.which == 40) {
if (remoteControl) sumorobot.send("stop");
}
});
/* start button listener */
$(".btn-start").click(function() {
sumostart = true;
/* if we are in coding mode */
if (codingEnabled) {
/* send the code from the textarea to the SumoRobot */
sumorobot.send("start:" + codingEditor.getValue());
/* otherwise when we are in Blockly mode */
} else {
/* send the code from the blocks to the SumoRobot */
sumorobot.send("start:" + sumocode);
}
});
/* stop button listener */
$(".btn-stop").click(function() {
sumostart = false;
sumorobot.send("stop");
workspace.highlightBlock(lastHighlighted, false);
});
/* enter listener on robot ID field */
$("#robot-id").keypress(function(e) {
if (e.which == 13) {
/* simulate robot GO button click */
$(".btn-robot-go").click();
}
});
/* robot number button listener */
$(".btn-robot-go").click(function() {
/* extract and validate the selected robot ID */
var robotId = $("#robot-id").val().trim();
if (robotId === "" || /^(sumo-[a-f0-9]{6})$/.test(robotId) == false) {
$("#robot-id, #robot-label").addClass("has-error");
return;
} else {
$("#robot-id, #robot-label").removeClass("has-error");
}
/* update robot IDs in local storage */
setLocalStorageItem("sumorobot.robotId", robotId);
/* in case there is a open connection */
if (sumorobot && blockHighlight) {
/* close the connections */
sumorobot.close();
blockHighlight.close();
}
/* connect to the selected robots WebSocket */
sumorobot = new Sumorobot("ws://" + robotServer + ":80/p2p/browser/" + robotId + "/");
/* connect to the other block highlight WebSocket */
blockHighlight = new WebSocket("ws://" + robotServer + ":80/p2p/browser/" + robotId + "-highlight/");
/* when there is a message from the WebSocket */
blockHighlight.onmessage = function(evt) {
/* when scope is received */
if (evt.data.length == 20 && sumostart) {
workspace.highlightBlock(evt.data);
lastHighlighted = evt.data;
}
};
/* hide the configuration panel */
$("#panel").hide();
});
/* load the Mixer stream */
$("#stream").html('<iframe width="100%" height="100%" allowfullscreen="true" src="https://mixer.com/embed/player/14551694"></iframe>');
/* set a click listener on the document */
$(document).click(function(e) {
var target = e.target;
/* when control_if block is in use */
if (controlBlockId != "") {
/* when the user clicks anywhere outside the mutator and not on the mutator icon */
if (!$(target).is('.blocklyBubbleCanvas') && !$(target).parents().is('.blocklyBubbleCanvas')) {
if (!$(target).is('.blocklyIconGroup') && !$(target).parents().is('.blocklyIconGroup')) {
/* hide the mutator */
workspace.getBlockById(controlBlockId).mutator.setVisible(false);
}
}
}
});
}