/* 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(); /* resize the Python code editor */ pythonEditor.resize(); /* 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(''); /* 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); } } } }); }