sumorobot-web/sumorobot.js

630 lines
23 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 pythonEditor = 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 Python code */
var pythonEnabled = 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();
};
window.onload = function() {
/* function to update the control panel */
function updateControlPanel() {
/* hide all buttons and text fields */
$(".robot-id, .robot-nr, .btn-robot-nr").hide();
/* show the first button and text field */
$(".robot-id:eq(0), .robot-nr:eq(0), .btn-robot-nr:eq(0)").show();
/* adjust the buttons and text fields to be in the middle */
$(".input-group, .btn-group-robot").css("width", "20%");
$(".input-group, .btn-group-robot").css("margin-left", "40%");
/* hide the robot add button */
$(".btn-robot-add").hide();
/* populate robots IDs and buttons */
for (var i = 0; i < 5; i++) {
var id = getLocalStorageItem("sumorobot.robotID" + i);
if (id) {
$(".robot-id:eq(" + i + ")").val(id);
$(".robot-id:eq(" + i + "), .robot-nr:eq(" + i + "), .btn-robot-nr:eq(" + i + ")").show();
$(".input-group, .btn-group-robot").css("width", 20 + (i * 20) + "%");
$(".input-group, .btn-group-robot").css("margin-left", 40 - (i * 10) + "%");
} else {
/* when no robots yet added */
if (i != 0) {
/* show the robot add button */
$(".btn-robot-add").show();
$(".btn-group-robot").css("width", 20 + (i * 20) + "%");
$(".input-group, .btn-group-robot").css("margin-left", 40 - (i * 10) + "%");
/* add click listener to the robot add button */
$(".btn-robot-add").click(function() {
$(".robot-id:eq(" + i + "), .robot-nr:eq(" + i + "), .btn-robot-nr:eq(" + i + ")").show();
$(".input-group, .btn-group-robot").css("width", 20 + (i * 20) + "%");
$(".input-group, .btn-group-robot").css("margin-left", 40 - (i * 10) + "%");
$(this).hide();
});
}
break;
}
}
}
/* load the control panel */
updateControlPanel();
/* load ace editor */
pythonEditor = ace.edit("blocklyCode");
/* set the style */
pythonEditor.setTheme("ace/theme/textmate");
pythonEditor.session.setMode("ace/mode/python");
pythonEditor.session.setTabSize(2);
pythonEditor.setReadOnly(true);
/* disable scrolling warning */
pythonEditor.$blockScrolling = Infinity;
/* enable autocomplete */
ace.require("ace/ext/language_tools");
pythonEditor.setOptions({
enableSnippets: true,
enableLiveAutocompletion: true,
enableBasicAutocompletion: true
});
/* add autocomplete keywords */
pythonEditor.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"}
]);
}
});
/* change the if block to be more cheerful */
Blockly.Msg.LOGIC_HUE = '#44CC00';
Blockly.Constants.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_delay'] = {
init: function() {
this.setColour("#E64C00");
this.appendDummyInput()
.appendField("delay")
.appendField(new Blockly.FieldTextInput('1000',
Blockly.FieldNumber.numberValidator), 'DELAY');
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_delay'] = function(block) {
var code = 'sumorobot.sleep(' + parseFloat(block.getFieldValue('DELAY')) + ', "' + 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: '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);
/* 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)
}
/* 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 */
pythonEditor.setValue("\n" + sumocode.replace(/[,]?[ ]?"(.*?)"/g, ""));
pythonEditor.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 49: // 1
$(".btn-robot-nr:eq(0)").click();
break;
case 50: // 2
$(".btn-robot-nr:eq(1)").click();
break;
case 51: // 3
$(".btn-robot-nr:eq(2)").click();
break;
case 52: // 4
$(".btn-robot-nr:eq(3)").click();
break;
case 53: // 5
$(".btn-robot-nr:eq(4)").click();
break;
case 67: // c
updateControlPanel();
$("#panel").toggle();
break;
case 76: // l
$("#stream").toggle();
$("#blocklyCode").toggle();
break;
case 80: // p
$("#blocklyDiv").toggle();
$("#blocklyArea").toggle();
/* disable / enable ace editor */
pythonEditor.setReadOnly(pythonEnabled);
/* toggle python enabled */
pythonEnabled = !pythonEnabled;
if (pythonEnabled) {
/* get the saved Python code from local storage or set empty */
pythonEditor.setValue(getLocalStorageItem("sumorobot.python") || "");
/* add an input listener for the code editor */
pythonEditor.on("change", function() {
setLocalStorageItem("sumorobot.python", pythonEditor.getValue())
});
pythonEditor.clearSelection();
pythonEditor.focus();
} else {
/* remove input listener from the code editor */
pythonEditor.session.removeAllListeners("change");
/* fire CHANGE event in Blockly workspace to change the Python code */
var event = {type: Blockly.Events.CHANGE};
workspace.fireChangeListener(event);
pythonEditor.blur();
}
break;
case 82: // r
if (remoteControl)
$("#remote-disabled").click();
else
$("#remote-enabled").click();
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-close").click(function() {
$("#stream").hide();
});
/* start button listener */
$(".btn-start").click(function() {
sumostart = true;
/* if we are in Python mode */
if (pythonEnabled) {
/* send the code from the textarea to the SumoRobot */
sumorobot.send("start:" + pythonEditor.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);
});
/* remote control enable listener */
$("#remote-enabled").click(function() {
remoteControl = true;
});
/* remote control disable listener */
$("#remote-disabled").click(function() {
remoteControl = false;
});
/* robot number button listener */
$(".btn-robot-nr").click(function() {
/* extract and validate the selected robot ID */
var index = $(".btn-robot-nr").index($(this));
var robotID = $(".robot-id:eq(" + index + ")").val();
if (robotID.trim() === "") {
$(".robot-nr:eq(" + index + "), .robot-id:eq(" + index + ")").addClass("has-error");
return;
} else {
$(".robot-nr:eq(" + index + "), .robot-id:eq(" + index + ")").removeClass("has-error");
}
/* highlight the selected robot button */
$(".btn-robot-nr").removeClass("btn-selected");
$(this).addClass("btn-selected");
/* update robot IDs in local storage */
setLocalStorageItem("sumorobot.robotID" + index, 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);
}
}
}
});
}