Files
clover/clover_blocks/www/main.js
Oleg Kalachev 2f6125ce54 Implement block programming (using Blockly) (#272)
* Clover Blockly: add first blocks set

* Adjust Blockly settings

* Fix get_position output type

* Add screenshot

* Rename readme.md to README.md

* Resize screenshot

* Add package.xml

* Little change

* Fixes

* Add python_compressed to blockly

* Implement some of the Clover blocks in Python

* Make Python indentation 4 spaces

* Fixes to Python blocks implementation

* Implement set_velocity block in Python

* Implement wait_arrival block in Python

* Fix indentation in Python implementation of blocks

* Fix

* Fix land_wait template

* Set reserved words in Python

* Change default frame_id to aruco_map in get_position block

* Fix

* Move blocks definitions to blocks.js

* Get rid of missing favicon error

* Simplify navigate

* Rearrange layout, add tabs

* Generate Python code

* Small style change

* -console.log

* Code style

* Use modules

* Move modules to the header

* Correct order for ROS definitions + generating "backend" code

* Fix rangefinder_distance block

* simple_offboard: commands to change only yaw and yaw rate

* Implement set_yaw block

* Start working on Blockly documentation

* Implement print block with a topic

* Unneeded code

* Little fixes

* Fix indentation

* Fixes

* Fix wait_arival, get_distance

* Implement running Blockly programs, implement prompt block, fixes

* Add land button

* Little change

* Fix reserved words + little fixes

* +x for main.py

* Simplify run button

* Auto-save and load workspace

* Make land button work

* Handle exceptions

* Minor change

* Add help URL for blocks

* Fix

* Implement arrived block

* Mark blockly and highlight.js as linguist-vendored

* Add forgotten CMakeLists.txt

* Add wait checkbox to set_yaw block

* Disable run button when disconnected

* Add message and service files

* Add some comments

* Add tooltip to some blocks

* Implement GPIO blocks

* Don’t latch print message to prevent duplication

* Prevent duplication prompts

* Add ROS init code to backend code anyways

* Make GPIO blocks color a constant

* Minor fix

* More correctly update blocks on input value changes

* Minor fixes

* Remove unneeded readonly attribute

* Add marker ID shadow blocks to toolbox

* Add lacking reserved words

* Fix frame id generation for complex marker id expressions

* Consider frame_id in set_yaw block

* Shorten ros module import

* Implement stop service

* Disable and enable run button correctly

* Don’t print KeyboardInterrupt exceptions

* Put notifications to notifications element

* Add 'running' mark

* Disable signal in backend python code

* Sleep a little bit to let rospy initialize publishers

* Remove accidental code

* Make ROS namespace and private namespace constants

* editorconfig-lint: don’t check Blockly code

* Use private namespace constant in Python generator

* Implement ~running topic to display current program status more robustly

* Make navigate tolerance and sleep time constants

* Make set_leds and and set_effect services proxies persistent

* Replace a number with constant

* Limit ~block topic publishing rate
Otherwise messages get queued making the frontend to freeze

* Improve internal documentation

* Append 'map' to frames list

* Return degrees in get_attitude block

* Move getting yaw in a separate block

* Improve block tooltips

* Add some more files to editorconfig-lint excludes

* Add get_yaw block to toolbox

* Implement get_time block

* Implement ~store and ~load services for storing user programs

* Set auto_arm only in take_off block

* Minor CSS fixes

* Make 'Python' tab textarea-like

* Implement saving and loading programs

* Adjust styles

* Retrieve only .xml files in load service

* Forgotten code

* Documentation on store and load services

* Add some examples

* Add blocks programming arg to launch file

* Update docs

* Add package’s dependencies

* Add dependency

* Add title to select

* Fix syntax

* Minor fix in docs

* Add forgotten roslib.js

* Run user program in the same process

* Use print function for print block in Python 2

* Add variables example

* Fix url

* Add functions example

* Fix set_servo block

* Fix gpio_read block

* Update blocks screenshot

* Update docs

* Update docs

* Fix set_effect block

* Minor fix in example

* Add setpoint block, remove set_velocity from toolbox

* Remove unused modules

* Unused variable

* Add English article skeleton

* Clarify backend node link error

* Remove unused variable

* Update documentation

* Fix link to documentation

* Add Blockly logo

* Update English article

* Add Blocks programming link to the main page

* Minor change

* Add catkin_install_python to CMakeLists.txt

* Make navigate tolerance and sleep time configurable

* Add minor todo

* Add blockly examples directory to editorconfig-lint excludes

* Rename main node to clover_blocks

* Add a warning to the old blocks programming article

* Fix editorconfig-lint exclude
2020-09-30 17:07:03 +03:00

314 lines
7.7 KiB
JavaScript

import * as ros from './ros.js';
import './blocks.js';
import {generateCode, generateUserCode} from './python.js';
// Tabs
document.getElementById('tabs').addEventListener('click', function(e) {
var tab = e.target.getAttribute('data-tab');
if (tab) {
for (let elem of e.target.parentElement.querySelectorAll('[data-tab]')) {
elem.classList.remove('selected');
}
e.target.classList.add('selected');
document.body.setAttribute('data-tab', tab);
}
});
var workspace = Blockly.inject('blockly', {
toolbox: document.getElementById('toolbox'),
grid: {
spacing: 25,
length: 3,
colour: '#ccc',
snap: true
},
zoom: { controls: true, wheel: true },
media: 'blockly/media/',
});
function readParams() {
return Promise.all([
ros.readParam('navigate_tolerance', true, 0.2),
ros.readParam('sleep_time', true, 0.2)
]);
}
var ready = readParams(); // initialization complete promise
var pythonArea = document.getElementById('python');
// update Python code
workspace.addChangeListener(function(e) {
ready.then(function() {
pythonArea.innerHTML = generateUserCode(workspace);
hljs.highlightBlock(pythonArea);
});
});
var running = false;
var runRequest = false;
new ROSLIB.Topic({ ros: ros.ros, name: ros.priv + 'block', messageType: 'std_msgs/String' }).subscribe(function(msg) {
workspace.highlightBlock(msg.data);
runRequest = false;
update();
});
new ROSLIB.Topic({ ros: ros.ros, name: ros.priv + 'running' }).subscribe(function(msg) {
running = msg.data;
runRequest = false;
if (!running) {
workspace.highlightBlock('');
}
update();
});
var notifElem = document.getElementById('notifications');
function z(n) { return (n < 10 ? '0' : '') + n; } // add leading zero
new ROSLIB.Topic({ ros: ros.ros, name: ros.priv + 'print', messageType: 'std_msgs/String'}).subscribe(function(msg) {
var d = new Date(); // TODO: use rosgraph_msgs/Log?
var timestamp = `${z(d.getHours())}:${z(d.getMinutes())}:${z(d.getSeconds())}`;
notifElem.innerHTML += `${timestamp}: ${msg.data}\n`;
notifElem.scrollTop = notifElem.scrollHeight;
});
new ROSLIB.Topic({ ros: ros.ros, name: ros.priv + 'error', messageType: 'std_msgs/String'}).subscribe(function(msg) {
alert('Error: ' + msg.data);
});
var runButton = document.getElementById('run');
function update() {
document.body.classList.toggle('running', running);
runButton.disabled = !ros.ros.isConnected || runRequest || running;
}
var shownPrompts = new Set();
new ROSLIB.Topic({ ros: ros.ros, name: ros.priv + 'prompt', messageType: 'clover_blocks/Prompt'}).subscribe(function(msg) {
if (shownPrompts.has(msg.id)) return;
shownPrompts.add(msg.id);
var response = prompt(msg.message);
new ROSLIB.Topic({
ros: ros.ros,
name: ros.priv + 'input/' + msg.id,
messageType: 'std_msgs/String',
latch: true
}).publish(new ROSLIB.Message({ data: response }));
});
window.stopProgram = function() {
ros.stopService.callService(new ROSLIB.ServiceRequest(), function(res) {
if (!res.success) alert(res.message);
}, err => alert(err))
}
ros.ros.on('connection', update);
ros.ros.on('close', update);
ready.then(() => runButton.disabled = false);
window.runProgram = function() {
if (!confirm('Run program?')) return;
runRequest = true;
update();
var code = generateCode(workspace);
console.log(code);
ros.runService.callService(new ROSLIB.ServiceRequest({ code: code } ), function(res) {
if (!res.success) {
runRequest = false;
update();
alert(res.message);
}
}, function(err) {
runRequest = false;
update();
alert(err);
})
}
window.land = function() {
window.stopProgram();
ros.landService.callService(new ROSLIB.ServiceRequest(), function(result) {
}, function(err) {
alert('Unable to land: ' + err);
});
}
function getProgramXml() {
var xmlDom = Blockly.Xml.workspaceToDom(workspace);
return Blockly.Xml.domToPrettyText(xmlDom);
}
function setProgramXml(xml) {
workspace.clear();
if (xml) {
let xmlDom = Blockly.Xml.textToDom(xml);
Blockly.Xml.domToWorkspace(xmlDom, workspace);
}
}
workspace.addChangeListener(function(e) {
localStorage.setItem('xml', getProgramXml());
});
var programSelect = document.querySelector('#program-name');
var userPrograms = programSelect.querySelector('optgroup[data-type=user]');
var examplePrograms = programSelect.querySelector('optgroup[data-type=example]');
var programs = {};
var program = '';
function loadWorkspace() {
var xml = localStorage.getItem('xml');
if (xml) {
setProgramXml(xml);
}
program = localStorage.getItem('program') || '';
}
loadWorkspace();
function loadPrograms() {
ros.loadService.callService(new ROSLIB.ServiceRequest(), function(res) {
if (!res.success) alert(res.message);
for (let i = 0; i < res.names.length; i++) {
let name = res.names[i];
let program = res.programs[i];
let option = document.createElement('option');
option.innerHTML = res.names[i];
if (name.startsWith('examples/')) {
examplePrograms.appendChild(option);
} else {
userPrograms.appendChild(option);
}
programs[name] = program;
}
if (program) {
programSelect.value = program;
}
updateChanged();
}, function(err) {
document.querySelector('.backend-fail').style.display = 'inline';
alert(`Error loading programs list.\n\nHave you enabled clover_blocks in clover.launch?`);
runButton.disabled = true;
})
}
loadPrograms();
function getProgramName() {
if (programSelect.value.startsWith('@')) {
return ''
}
return programSelect.value;
}
function updateChanged() {
var name = program;
document.body.classList.toggle('changed', name in programs && (programs[name].trim() != getProgramXml().trim()));
}
workspace.addChangeListener(function(e) {
if (e instanceof Blockly.Events.Change || e instanceof Blockly.Events.Create || e instanceof Blockly.Events.Delete) {
updateChanged();
}
});
function saveProgram() {
var name = getProgramName();
if (!name) {
name = prompt('Enter new program name:');
if (!name) {
programSelect.value = program;
return;
}
if (!name.endsWith('.xml')) {
name += '.xml';
}
let option = document.createElement('option');
option.innerHTML = name;
userPrograms.appendChild(option);
}
let xml = getProgramXml();
ros.storeService.callService(new ROSLIB.ServiceRequest({
name: name,
program: xml
}), function(result) {
if (!result.success) {
alert(result.message);
return;
}
console.log(result.message);
programSelect.blur();
program = name;
localStorage.setItem('program', name);
programs[name] = xml;
programSelect.value = program;
updateChanged();
}, function(err) {
// TODO: restore previous state correctly
alert('Unable to store: ' + err);
programSelect.blur();
programSelect.value = program;
});
}
window.addEventListener('keydown', function(e) {
if ((e.metaKey || e.ctrlKey) && e.key == 's') { // ctrl+s/cmd+s
e.preventDefault();
if (!document.body.classList.contains('changed')) { // if not changed, ignore
return;
}
saveProgram();
}
});
programSelect.addEventListener('change', function(e) {
if (programSelect.value == '@clear') {
if (!confirm('Clear workspace?')) {
programSelect.value = program;
return;
}
localStorage.removeItem('program');
program = '';
setProgramXml('');
programSelect.value = program;
programSelect.blur();
} else if (programSelect.value == '@save') {
saveProgram();
} else {
// load program
if (program == '' || document.body.classList.contains('changed')) {
if (!confirm('Discard changes?')) {
programSelect.value = program;
return;
}
}
let name = programSelect.value;
let lastProgram = getProgramXml();
programSelect.blur();
try {
setProgramXml(programs[name]);
program = name;
localStorage.setItem('program', name);
} catch(e) {
alert(e);
setProgramXml(lastProgram);
program = ''
programSelect.value = program;
}
updateChanged();
}
});