From f06805d2a6116e06d2b00332f09f9561834dfe1a Mon Sep 17 00:00:00 2001 From: emersion Date: Sun, 14 Feb 2016 00:06:21 +0100 Subject: [PATCH 01/43] First experiments with mercury --- package.json | 4 +- public/index.html | 18 +++---- public/js/index.js | 78 +++++++++++++++++++++++++++++- public/js/quadcopter.js | 2 +- public/js/widget/controller-btn.js | 44 +++++++++++++++++ public/js/widget/power-btn.js | 46 ++++++++++++++++++ 6 files changed, 178 insertions(+), 14 deletions(-) create mode 100644 public/js/widget/controller-btn.js create mode 100644 public/js/widget/power-btn.js diff --git a/package.json b/package.json index fd5802a..298c476 100644 --- a/package.json +++ b/package.json @@ -26,16 +26,16 @@ "express-browserify-lite": "^0.1.2", "express-ws": "^0.2.6", "extend": "^2.0.1", + "global": "^4.3.0", "i2c-bus": "^0.11.1", "i2c-mpu6050": "^2.2.0", - "jquery": "^2.2.0", + "mercury": "^14.1.0", "mpu6050-dmp": "^0.0.6", "msgpack5": "^3.3.0", "node-pid-controller": "^0.1.2", "servoblaster": "^0.1.0", "smoothie": "^1.27.0", "stream-split": "^0.2.0", - "svg-injector": "^1.1.3", "through": "^2.3.8", "util": "^0.10.3" } diff --git a/public/index.html b/public/index.html index a7a1460..62ffdbe 100644 --- a/public/index.html +++ b/public/index.html @@ -8,16 +8,9 @@ - - - - - - - -
+ + + + + + + + diff --git a/public/js/index.js b/public/js/index.js index bd72755..14e566e 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -1,5 +1,78 @@ -var $ = require('jquery'); -var SVGInjector = require('svg-injector'); +'use strict'; + +var document = require('global/document'); +var hg = require('mercury'); +var h = require('mercury').h; +var Quadcopter = require('./quadcopter'); +var PowerBtn = require('./widget/power-btn'); +var ControllerBtn = require('./widget/controller-btn'); + +function App() { + var quad = new Quadcopter(); + + var state = hg.state({ + console: hg.array([]), + quad: quad, + powerBtn: PowerBtn(quad), + controllerBtn: ControllerBtn(quad), + channels: { + log: log + } + }); + + log(state, 'Connecting to server...'); + + state.quad.init(function (err) { + if (err) { + return log(state, { type: 'error', msg: err }); + } + log(state, 'Connected!'); + }); + + state.quad.on('error', function (msg) { + log(state, { type: 'error', msg: msg }); + }); + state.quad.on('info', function (msg) { + log(state, { type: 'info', msg: msg }); + }); + + return state; +} + +function log(state, data) { + if (typeof data === 'string') { + data = { msg: data }; + } + + state.console.push({ + type: data.type, + msg: data.msg + }); +} + +App.render = function (state) { + return h('div#app', [ + h('div#console.container-fluid', [ + h('pre', state.console.map(renderLog)) + ]), + h('hr'), + h('div.container-fluid', h('div.row', [ + h('div.col-lg-6.col-xs-12', [ + hg.partial(PowerBtn.render, state.powerBtn), + hg.partial(ControllerBtn.render, state.controllerBtn) + ]), + h('div.col-lg-6.col-xs-12', []) + ])) + ]); +}; + +function renderLog(item) { + return h('span.' + (item.type || 'log'), item.msg + '\n'); +} + +hg.app(document.body, App(), App.render); + +/*var SVGInjector = require('svg-injector'); var smoothie = require('smoothie'); var colors = require('./colors'); var keyBindings = require('./key-bindings'); @@ -623,3 +696,4 @@ $(function () { }); }); }); +*/ diff --git a/public/js/quadcopter.js b/public/js/quadcopter.js index 3409360..da2bf4a 100644 --- a/public/js/quadcopter.js +++ b/public/js/quadcopter.js @@ -104,4 +104,4 @@ Quadcopter.prototype.init = function (cb) { }); }; -module.exports = Quadcopter; \ No newline at end of file +module.exports = Quadcopter; diff --git a/public/js/widget/controller-btn.js b/public/js/widget/controller-btn.js new file mode 100644 index 0000000..dd134ff --- /dev/null +++ b/public/js/widget/controller-btn.js @@ -0,0 +1,44 @@ +'use strict'; + +var hg = require('mercury'); +var h = require('mercury').h; + +function ControllerBtn(quad) { + var state = hg.state({ + value: hg.value(null), + updaters: hg.array([]), + channels: { + change: change + } + }); + + hg.watch(state.value, function (val) { + if (!val || !quad.config) return; + quad.config.controller.updater = val; + //quad.cmd.send('config', quad.config); + }); + + quad.on('features', function (data) { + state.updaters.set(data.updaters); + }); + + return state; +} + +function change(state, value) { + state.value.set(value.controller); +} + +ControllerBtn.render = function (state) { + return h('div.form-inline', [ + h('label.control-label', { htmlFor: 'controller-btn' }, 'Controller:'), + h('select#controller-btn.form-control', { + name: 'controller', + 'ev-event': hg.sendChange(state.channels.change) + }, state.updaters.map(function (name) { + return h('option', name); + })) + ]); +}; + +module.exports = ControllerBtn; diff --git a/public/js/widget/power-btn.js b/public/js/widget/power-btn.js new file mode 100644 index 0000000..11161e2 --- /dev/null +++ b/public/js/widget/power-btn.js @@ -0,0 +1,46 @@ +'use strict'; + +var hg = require('mercury'); +var h = require('mercury').h; + +function PowerBtn(quad) { + var state = hg.state({ + value: hg.value(false), + channels: { + change: change + } + }); + + hg.watch(state.value, function (val) { + if (quad.enabled == val) return; + quad.cmd.send('enable', val); + }); + + quad.on('enabled', function (val) { + state.value.set(val); + }); + + return state; +} + +function change(state, value) { + state.value.set(value.enabled); +} + +PowerBtn.render = function (state) { + return h('div', { title: 'Alt+S to start/stop, Esc to stop' }, [ + h('div.switch', [ + h('input', { + type: 'checkbox', + id: 'power-switch', + name: 'enabled', + checked: state.value, + 'ev-event': hg.sendChange(state.channels.change) + }), + h('label', { htmlFor: 'power-switch' }) + ]), + h('label', { htmlFor: 'power-switch' }, 'POWER') + ]); +}; + +module.exports = PowerBtn; From 5ee2e26ca6a22fa5f6c49e45568601860e972750 Mon Sep 17 00:00:00 2001 From: emersion Date: Sun, 14 Feb 2016 14:59:34 +0100 Subject: [PATCH 02/43] Adds more widgets: mouse direction, charts, system summary --- public/assets/main.css | 6 +-- public/index.html | 35 +-------------- public/js/charts-build.js | 68 +++++++++++++++++++++++++++++ public/js/direction/mouse.js | 54 +++++++++++++++++++++++ public/js/event/drag.js | 33 ++++++++++++++ public/js/index.js | 48 +++++++++++++++----- public/js/widget/power-btn.js | 2 +- public/js/widget/streaming-chart.js | 39 +++++++++++++++++ public/js/widget/system-summary.js | 48 ++++++++++++++++++++ public/js/widget/tabs.js | 41 +++++++++++++++++ 10 files changed, 325 insertions(+), 49 deletions(-) create mode 100644 public/js/charts-build.js create mode 100644 public/js/direction/mouse.js create mode 100644 public/js/event/drag.js create mode 100644 public/js/widget/streaming-chart.js create mode 100644 public/js/widget/system-summary.js create mode 100644 public/js/widget/tabs.js diff --git a/public/assets/main.css b/public/assets/main.css index 090371c..3c70ae4 100644 --- a/public/assets/main.css +++ b/public/assets/main.css @@ -98,7 +98,7 @@ input[type="number"].form-control { color: rgb(0, 100, 0); } -.joystick { +.direction-mouse { position: relative; width: 300px; height: 300px; @@ -106,7 +106,7 @@ input[type="number"].form-control { border-radius: 50%; margin: 10px auto; } -.joystick .handle { +.direction-mouse .handle { position: absolute; width: 16px; height: 16px; @@ -115,7 +115,7 @@ input[type="number"].form-control { opacity: 0.2; pointer-events: none; } -.joystick.active .handle { +.direction-mouse:active .handle { opacity: 1; } diff --git a/public/index.html b/public/index.html index 62ffdbe..7633c29 100644 --- a/public/index.html +++ b/public/index.html @@ -10,40 +10,7 @@ - +
--> diff --git a/public/js/camera-preview.js b/public/js/camera-preview.js index 9278464..e173115 100644 --- a/public/js/camera-preview.js +++ b/public/js/camera-preview.js @@ -78,7 +78,7 @@ CameraPreview.prototype.start = function () { this._initPlayer(); this._startWebsocket(function () { - that.emit('start'); + that.emit('start', that.player.canvas); }); this._cmd.send('camera-preview', true); @@ -91,6 +91,8 @@ CameraPreview.prototype.stop = function () { //this.player.worker.terminate(); this._cmd.send('camera-preview', false); + + this.emit('stop'); }; CameraPreview.prototype.restart = function () { diff --git a/public/js/widget/camera-config.js b/public/js/widget/camera-config.js index 887d968..21f26a5 100644 --- a/public/js/widget/camera-config.js +++ b/public/js/widget/camera-config.js @@ -49,10 +49,4 @@ CameraConfig.render = function (state) { ]); }; -function renderProfile(state) { - if (!state) return h(); - - -} - module.exports = CameraConfig; diff --git a/public/js/widget/camera.js b/public/js/widget/camera.js index cceb9c0..a38ba9c 100644 --- a/public/js/widget/camera.js +++ b/public/js/widget/camera.js @@ -26,8 +26,11 @@ function Camera(quad) { } }); - cameraPreview.on('start', function () { - state.video.set(cameraPreview.player.canvas); + cameraPreview.on('start', function (canvas) { + state.video.set(canvas); + }); + cameraPreview.on('stop', function () { + state.video.set(null); }); cameraPreview.on('error', function (err) { console.error(err); // TODO: use UI console instead From 3c20410b8921d065a4c2f9f011c4ea7a314aebdc Mon Sep 17 00:00:00 2001 From: emersion Date: Sat, 20 Feb 2016 16:20:33 +0100 Subject: [PATCH 31/43] Add output option support to avconv recorder --- lib/camera/stream.js | 34 ++++++------------------------ public/js/widget/camera-profile.js | 5 +++++ 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/lib/camera/stream.js b/lib/camera/stream.js index 921b2a4..7099883 100644 --- a/lib/camera/stream.js +++ b/lib/camera/stream.js @@ -64,35 +64,15 @@ var avconv = function (options) { args.push(options.framerate); } - args.push('-'); - - return child.spawn('avconv', args, { - stdio: ['pipe', 'pipe', 'inherit'] - }); -}; - -// Doesn't support mp4 streaming -var cvlc = function () { - var args = [ - '-I', - 'dummy', - '-v', - 'v4l2:///dev/video0', - '--v4l2-chroma', - 'h264', - '--v4l2-width', - '800', - '--v4l2-height', - '600', - '--sout', - //'#standard{access=file,mux=mp4,dst=-}', - '#standard{access=http,mux=ts,dst=0.0.0.0:3001}', // TODO - ':demux=h264' - ]; + if (options.output) { + args.push(options.output); + } else { + args.push('-'); + } - // cvlc v4l2:///dev/video0 --v4l2-width 800 --v4l2-height 600 --v4l2-chroma h264 --sout '#standard{access=http,mux=ts,dst=0.0.0.0:12345}' -vvv + console.log('avconv', args.join(' ')); - return child.spawn('cvlc', args, { + return child.spawn('avconv', args, { stdio: ['pipe', 'pipe', 'inherit'] }); }; diff --git a/public/js/widget/camera-profile.js b/public/js/widget/camera-profile.js index 1aa5f28..e68ed42 100644 --- a/public/js/widget/camera-profile.js +++ b/public/js/widget/camera-profile.js @@ -71,6 +71,11 @@ function CameraProfile() { } function change(state, data) { + if (!state.isoSwitch.value()) { + delete data.ISO; + } + data.vstab = state.vstabSwitch.value(); + console.log(data); } From 43004005fed41fc106b91958154e3ec33247eec2 Mon Sep 17 00:00:00 2001 From: emersion Date: Sat, 20 Feb 2016 16:25:50 +0100 Subject: [PATCH 32/43] Hides camera if not available --- public/js/index.js | 75 ++++++---------------------------------------- 1 file changed, 9 insertions(+), 66 deletions(-) diff --git a/public/js/index.js b/public/js/index.js index 8306837..3d39a1b 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -59,11 +59,16 @@ function App() { calibrateBtn: CalibrateBtn(quad), charts: Charts(quad), camera: Camera(quad), + cameraAvailable: hg.value(false), config: Config(quad) }); quad.init(); + quad.on('features', function (features) { + state.cameraAvailable.set(features.hardware.indexOf('camera') !== -1); + }); + return state; } @@ -120,7 +125,9 @@ App.render = function (state) { ])), h('.container-fluid', hg.partial(Charts.render, state.charts)), - h('#camera', [ + h('#camera', { + style: { display: (state.cameraAvailable) ? 'block' : 'none' } + }, [ h('hr'), h('.container-fluid', hg.partial(Camera.render, state.camera)) ]), @@ -134,25 +141,7 @@ App.render = function (state) { hg.app(document.body, App(), App.render); -/*var SVGInjector = require('svg-injector'); -var smoothie = require('smoothie'); -var colors = require('./colors'); -var keyBindings = require('./key-bindings'); -var graphsExport = require('./graphs-export'); -var CameraPreview = require('./camera-preview'); -var QuadcopterSchema = require('./quad-schema'); -var Quadcopter = require('./quadcopter'); - -var input = { - Mouse: require('./input/mouse'), - DeviceOrientation: require('./input/device-orientation'), - Gamepad: require('./input/gamepad'), - Step: require('./input/step'), - Sine: require('./input/sine'), - Ramp: require('./input/ramp') -}; - -function init(quad) { +/*function init(quad) { keyBindings(quad); var mouseInput = new input.Mouse(quad.cmd, '#direction-input'); @@ -172,52 +161,6 @@ function init(quad) { var gamepadInput = new input.Gamepad(quad.cmd); } - $('#direction-type-tabs').tabs(); - - var stepInput = new input.Step(quad.cmd); - $('#direction-step').submit(function (event) { - event.preventDefault(); - - var data = $(this).serializeObject(); - - stepInput.start({ - x: parseFloat(data.x), - y: parseFloat(data.y), - z: parseFloat(data.z), - duration: parseFloat(data.duration) * 1000 - }); - }); - - var sineInput = new input.Sine(quad.cmd); - $('#direction-sine').submit(function (event) { - event.preventDefault(); - - var data = $(this).serializeObject(); - - sineInput.start({ - amplitude: parseFloat(data.amplitude), - frequency: parseFloat(data.frequency), - offset: parseFloat(data.offset), - axis: data.axis - }); - }); - $('#direction-sine-stop').click(function () { - sineInput.stop(); - }); - - var rampInput = new input.Ramp(quad.cmd); - $('#direction-ramp').submit(function (event) { - event.preventDefault(); - - var data = $(this).serializeObject(); - data.slope = parseFloat(data.slope); - - rampInput.start(data); - }); - $('#direction-ramp-stop').click(function () { - rampInput.stop(); - }); - $('#camera-config-preview').submit(function () { if (cameraPreview.isStarted()) { setTimeout(function () { From baf4aa2a0eee156691a032eec35471c9908d71a8 Mon Sep 17 00:00:00 2001 From: emersion Date: Sat, 20 Feb 2016 16:33:46 +0100 Subject: [PATCH 33/43] Adds camera record button --- lib/camera/index.js | 2 +- public/js/index.js | 17 ----------------- public/js/widget/camera.js | 11 ++++++++--- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/lib/camera/index.js b/lib/camera/index.js index de39e1c..874dc7f 100644 --- a/lib/camera/index.js +++ b/lib/camera/index.js @@ -116,7 +116,7 @@ class Camera extends EventEmitter { } startRecording() { - var outputPath = '/home/pi/output.h264'; + var outputPath = 'output.h264'; var opts = this.config.record; if (!this.config.previewWhenRecording) { diff --git a/public/js/index.js b/public/js/index.js index 3d39a1b..bbbe677 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -144,23 +144,6 @@ hg.app(document.body, App(), App.render); /*function init(quad) { keyBindings(quad); - var mouseInput = new input.Mouse(quad.cmd, '#direction-input'); - - if (input.Gamepad.isSupported()) { - var devOrientation = new input.Gamepad(quad.cmd); - - $('#orientation-switch').change(function () { - if ($(this).prop('checked')) { - devOrientation.start(); - } else { - devOrientation.stop(); - } - }); - } - if (input.Gamepad.isSupported()) { - var gamepadInput = new input.Gamepad(quad.cmd); - } - $('#camera-config-preview').submit(function () { if (cameraPreview.isStarted()) { setTimeout(function () { diff --git a/public/js/widget/camera.js b/public/js/widget/camera.js index a38ba9c..3eeee57 100644 --- a/public/js/widget/camera.js +++ b/public/js/widget/camera.js @@ -12,6 +12,7 @@ function Camera(quad) { var state = hg.state({ config: CameraConfig(quad), video: hg.value(), + recording: hg.value(false), channels: { play: function () { cameraPreview.play(); @@ -22,7 +23,11 @@ function Camera(quad) { stop: function () { cameraPreview.stop(); }, - record: function () {} + record: function () { + var recording = !state.recording(); + state.recording.set(recording); + quad.cmd.send('camera-record', recording); + } } }); @@ -52,7 +57,7 @@ Camera.render = function (state) { h('button.btn.btn-default', { 'ev-click': hg.sendClick(state.channels.pause) }, h('span.glyphicon.glyphicon-pause')), - h('button.btn.btn-default', { + h('button.btn.btn-default' + ((state.recording) ? '.active' : ''), { 'ev-click': hg.sendClick(state.channels.stop) }, h('span.glyphicon.glyphicon-stop')) ]), @@ -62,7 +67,7 @@ Camera.render = function (state) { 'ev-click': hg.sendClick(state.channels.record) }, h('span.glyphicon.glyphicon-record')), h('br'), - h('p.text-danger', { style: { display: 'none' } }, 'Recording...') + h('p.text-danger', { style: { display: (state.recording) ? 'block' : 'none' } }, 'Recording...') ]) ]), ]); From 98b267fd0696feb177ed4051ddaf47781259c988 Mon Sep 17 00:00:00 2001 From: emersion Date: Sat, 20 Feb 2016 16:43:42 +0100 Subject: [PATCH 34/43] Prevent base updater from appearing in list --- lib/controller.js | 18 ++++++++++-------- lib/controller/{base.js => _base.js} | 0 lib/controller/dummy.js | 2 +- lib/controller/physics-quantif.js | 2 +- lib/controller/physics.js | 2 +- lib/controller/rate-simple.js | 2 +- lib/controller/stabilize-simple.js | 2 +- public/js/index.js | 9 --------- 8 files changed, 15 insertions(+), 22 deletions(-) rename lib/controller/{base.js => _base.js} (100%) diff --git a/lib/controller.js b/lib/controller.js index 96e6586..6517053 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -5,14 +5,16 @@ var path = require('path'); var EventEmitter = require('events').EventEmitter; function loadUpdaters() { - var dir = 'controller'; - - var updaters = {}; - fs.readdirSync(__dirname+'/'+dir).forEach(function (file) { - var name = path.basename(file, '.js'); - updaters[name] = require('./'+dir+'/'+name); - }); - return updaters; + var dir = 'controller'; + + var updaters = {}; + fs.readdirSync(__dirname+'/'+dir).forEach(function (file) { + var name = path.basename(file, '.js'); + if (name[0] === '_') return; + + updaters[name] = require('./'+dir+'/'+name); + }); + return updaters; } var updaters = loadUpdaters(); diff --git a/lib/controller/base.js b/lib/controller/_base.js similarity index 100% rename from lib/controller/base.js rename to lib/controller/_base.js diff --git a/lib/controller/dummy.js b/lib/controller/dummy.js index 0343221..968b3cb 100644 --- a/lib/controller/dummy.js +++ b/lib/controller/dummy.js @@ -1,6 +1,6 @@ 'use strict'; -var BaseUpdater = require('./base'); +var BaseUpdater = require('./_base'); /** * A dummy updater. diff --git a/lib/controller/physics-quantif.js b/lib/controller/physics-quantif.js index 118d858..a0a49a0 100644 --- a/lib/controller/physics-quantif.js +++ b/lib/controller/physics-quantif.js @@ -1,6 +1,6 @@ 'use strict'; -var BaseUpdater = require('./base'); +var BaseUpdater = require('./_base'); /** * The physics updater. diff --git a/lib/controller/physics.js b/lib/controller/physics.js index 1c648ee..aff7b6f 100644 --- a/lib/controller/physics.js +++ b/lib/controller/physics.js @@ -1,6 +1,6 @@ 'use strict'; -var BaseUpdater = require('./base'); +var BaseUpdater = require('./_base'); /** * The physics updater. diff --git a/lib/controller/rate-simple.js b/lib/controller/rate-simple.js index 590f8bc..9d3593d 100644 --- a/lib/controller/rate-simple.js +++ b/lib/controller/rate-simple.js @@ -1,7 +1,7 @@ 'use strict'; var PidController = require('node-pid-controller'); -var BaseUpdater = require('./base'); +var BaseUpdater = require('./_base'); /** * A simple rate updater. diff --git a/lib/controller/stabilize-simple.js b/lib/controller/stabilize-simple.js index fea2b15..7e3cb97 100644 --- a/lib/controller/stabilize-simple.js +++ b/lib/controller/stabilize-simple.js @@ -1,7 +1,7 @@ 'use strict'; var PidController = require('node-pid-controller'); -var BaseUpdater = require('./base'); +var BaseUpdater = require('./_base'); var forEachPid = Symbol(); diff --git a/public/js/index.js b/public/js/index.js index bbbe677..4df06f5 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -270,14 +270,5 @@ $(function () { // PID controller config $('#controller-btn').val(cfg.controller.updater); }); - - quad.on('features', function (features) { - if (features.hardware.indexOf('camera') === -1) { - $('#camera').hide(); - } - }); - - var cameraConfigHtml = $('#camera-config-inputs').html(); - $('#camera-config-preview, #camera-config-record').html(cameraConfigHtml); }); */ From 0da161460ce7feef41110db24d66c3015272f2eb Mon Sep 17 00:00:00 2001 From: emersion Date: Sat, 20 Feb 2016 19:46:06 +0100 Subject: [PATCH 35/43] Adds basic chart recorder --- public/js/chart-recorder.js | 38 ++++++++++++++++++++++++++---- public/js/index.js | 6 ++++- public/js/widget/chart-recorder.js | 29 ++++++++++++++++++----- 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/public/js/chart-recorder.js b/public/js/chart-recorder.js index 5a83d42..c4f357d 100644 --- a/public/js/chart-recorder.js +++ b/public/js/chart-recorder.js @@ -1,21 +1,40 @@ 'use strict'; var flatten = require('expand-flatten').flatten; +var exportFile = require('./export'); module.exports = function (quad) { var isRecording = false; var header = []; var lines = []; + var firstOrientation = true; function appendOrientation(data) { data = flatten(data); - // TODO + if (firstOrientation) { + firstOrientation = false; + header = header.concat(Object.keys(data)); + } + + var line = new Array(header.length); + for (var i = 0; i < header.length; i++) { + var key = header[i]; - lines.push(); + if (typeof data[key] === 'undefined') { + continue; + } + + line[i] = data[key]; + } + lines.push(line); } - function appendMotorsSpeed() { + function appendMotorsSpeed(data) { + // TODO + } + + function appendCmd(data) { // TODO } @@ -27,15 +46,26 @@ module.exports = function (quad) { quad.on('orientation', appendOrientation); quad.on('motors-speed', appendMotorsSpeed); + quad.cmd.on('orientation', appendCmd); }, stop: function () { if (!isRecording) return; quad.removeListener('orientation', appendOrientation); quad.removeListener('motors-speed', appendMotorsSpeed); + quad.cmd.removeListener('orientation', appendCmd); }, reset: function () { lines = []; + }, + export: function () { + var csv = header.join(',') + '\n'; + + for (var i = 0; i < lines.length; i++) { + csv += lines[i].join(',') + '\n'; + } + + return exportFile({ body: csv, type: 'text/csv' }); } }; -}; \ No newline at end of file +}; diff --git a/public/js/index.js b/public/js/index.js index 4df06f5..f09850f 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -19,6 +19,7 @@ var Outline = require('./widget/outline'); var MotorsSummary = require('./widget/motors-summary'); var OrientationSummary = require('./widget/orientation-summary'); var CalibrateBtn = require('./widget/calibrate-btn'); +var ChartRecorder = require('./widget/chart-recorder'); var Camera = require('./widget/camera'); var Config = require('./widget/config'); @@ -57,6 +58,7 @@ function App() { motorsSummary: MotorsSummary(quad), orientationSummary: OrientationSummary(quad), calibrateBtn: CalibrateBtn(quad), + chartRecorder: ChartRecorder(quad), charts: Charts(quad), camera: Camera(quad), cameraAvailable: hg.value(false), @@ -120,7 +122,9 @@ App.render = function (state) { hg.partial(Outline.Front.render, state.outline.front), hg.partial(Outline.Right.render, state.outline.right), hg.partial(OrientationSummary.render, state.orientationSummary), - hg.partial(CalibrateBtn.render, state.calibrateBtn) + hg.partial(CalibrateBtn.render, state.calibrateBtn), + ' ', + hg.partial(ChartRecorder.render, state.chartRecorder) ]), ])), h('.container-fluid', hg.partial(Charts.render, state.charts)), diff --git a/public/js/widget/chart-recorder.js b/public/js/widget/chart-recorder.js index f3afa03..18027cd 100644 --- a/public/js/widget/chart-recorder.js +++ b/public/js/widget/chart-recorder.js @@ -2,32 +2,49 @@ var hg = require('mercury'); var h = require('mercury').h; +var Recorder = require('../chart-recorder'); function ChartRecorder(quad) { - return hg.state({ + var recorder = Recorder(quad); + + var state = hg.state({ + recording: hg.value(false), channels: { start: function (state) { - // TODO + recorder.start(); + state.recording.set(true); }, stop: function (state) { - // TODO + recorder.stop(); + state.recording.set(false); }, export: function (state) { - // TODO + recorder.export(); } } }); + + return state; } ChartRecorder.render = function (state) { return h('.btn-group', [ h('button.btn.btn-danger', { - title: 'record', + title: 'Record', + style: { display: (!state.recording) ? 'block' : 'none' }, 'ev-click': hg.sendClick(state.channels.start) }, h('span.glyphicon.glyphicon-record')), + h('button.btn.btn-danger', { + title: 'Stop recording', + style: { display: (state.recording) ? 'block' : 'none' }, + 'ev-click': hg.sendClick(state.channels.stop) + }, h('span.glyphicon.glyphicon-stop')), h('button.btn.btn-default', { 'ev-click': hg.sendClick(state.channels.export) - }, 'Export') + }, 'Export'), + h('p.text-danger', { + style: { display: (state.recording) ? 'block' : 'none' } + }, 'Recording...') ]); }; From 82150e7c55f89f4f39a37756c5a3e0b954d60177 Mon Sep 17 00:00:00 2001 From: emersion Date: Sun, 21 Feb 2016 20:53:44 +0100 Subject: [PATCH 36/43] Fully functional chart recorder --- public/js/chart-recorder.js | 57 +++++++++++++++++++----------- public/js/widget/chart-recorder.js | 23 ++++++------ 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/public/js/chart-recorder.js b/public/js/chart-recorder.js index c4f357d..382cc4f 100644 --- a/public/js/chart-recorder.js +++ b/public/js/chart-recorder.js @@ -5,44 +5,59 @@ var exportFile = require('./export'); module.exports = function (quad) { var isRecording = false; - var header = []; + var startedAt = 0; + var header = ['time']; var lines = []; - var firstOrientation = true; - function appendOrientation(data) { - data = flatten(data); + function createAppender(prefix) { + var isFirst = true; - if (firstOrientation) { - firstOrientation = false; - header = header.concat(Object.keys(data)); + function newline() { + var line = new Array(header.length); + line[0] = (Date.now() - startedAt) / 1000; + lines.push(line); + return line; } - var line = new Array(header.length); - for (var i = 0; i < header.length; i++) { - var key = header[i]; + return function append(data) { + if (prefix) { + data = { [prefix]: data }; + } + + data = flatten(data); - if (typeof data[key] === 'undefined') { - continue; + if (isFirst) { + isFirst = false; + header = header.concat(Object.keys(data)); } - line[i] = data[key]; - } - lines.push(line); - } + var line = lines[lines.length - 1] || newline(); + for (var i = 0; i < header.length; i++) { + var key = header[i]; - function appendMotorsSpeed(data) { - // TODO - } + if (typeof data[key] === 'undefined') { + continue; + } + + if (typeof line[i] !== 'undefined') { + line = newline(); + } - function appendCmd(data) { - // TODO + line[i] = data[key]; + } + }; } + var appendOrientation = createAppender(); + var appendMotorsSpeed = createAppender('motorsSpeed'); + var appendCmd = createAppender('cmd'); + return { start: function () { if (isRecording) return; this.reset(); + startedAt = Date.now(); quad.on('orientation', appendOrientation); quad.on('motors-speed', appendMotorsSpeed); diff --git a/public/js/widget/chart-recorder.js b/public/js/widget/chart-recorder.js index 18027cd..571c986 100644 --- a/public/js/widget/chart-recorder.js +++ b/public/js/widget/chart-recorder.js @@ -28,23 +28,24 @@ function ChartRecorder(quad) { } ChartRecorder.render = function (state) { - return h('.btn-group', [ - h('button.btn.btn-danger', { + var recordBtn; + if (!state.recording) { + recordBtn = h('button.btn.btn-danger', { title: 'Record', - style: { display: (!state.recording) ? 'block' : 'none' }, 'ev-click': hg.sendClick(state.channels.start) - }, h('span.glyphicon.glyphicon-record')), - h('button.btn.btn-danger', { + }, h('span.glyphicon.glyphicon-record')); + } else { + recordBtn = h('button.btn.btn-danger', { title: 'Stop recording', - style: { display: (state.recording) ? 'block' : 'none' }, 'ev-click': hg.sendClick(state.channels.stop) - }, h('span.glyphicon.glyphicon-stop')), + }, h('span.glyphicon.glyphicon-stop')); + } + + return h('.btn-group', [ + recordBtn, h('button.btn.btn-default', { 'ev-click': hg.sendClick(state.channels.export) - }, 'Export'), - h('p.text-danger', { - style: { display: (state.recording) ? 'block' : 'none' } - }, 'Recording...') + }, 'Export') ]); }; From e7a0ddb3d34d9ab8257fc19c3291f05f64619956 Mon Sep 17 00:00:00 2001 From: emersion Date: Sun, 21 Feb 2016 21:05:12 +0100 Subject: [PATCH 37/43] Cleanup --- public/index.html | 23 ----- public/js/chart-recorder.js | 5 +- public/js/graphs-export.js | 71 --------------- public/js/index.js | 122 +------------------------- public/js/input/device-orientation.js | 19 ---- public/js/input/gamepad.js | 62 ------------- public/js/input/mouse.js | 70 --------------- public/js/input/ramp.js | 23 ----- public/js/input/sine.js | 27 ------ public/js/input/step.js | 31 ------- 10 files changed, 5 insertions(+), 448 deletions(-) delete mode 100644 public/js/graphs-export.js delete mode 100644 public/js/input/device-orientation.js delete mode 100644 public/js/input/gamepad.js delete mode 100644 public/js/input/mouse.js delete mode 100644 public/js/input/ramp.js delete mode 100644 public/js/input/sine.js delete mode 100644 public/js/input/step.js diff --git a/public/index.html b/public/index.html index 11b4bd5..02b61a5 100644 --- a/public/index.html +++ b/public/index.html @@ -10,29 +10,6 @@ - - diff --git a/public/js/chart-recorder.js b/public/js/chart-recorder.js index 382cc4f..1e2f914 100644 --- a/public/js/chart-recorder.js +++ b/public/js/chart-recorder.js @@ -77,7 +77,10 @@ module.exports = function (quad) { var csv = header.join(',') + '\n'; for (var i = 0; i < lines.length; i++) { - csv += lines[i].join(',') + '\n'; + csv += lines[i].map(function (val) { + if (typeof val === 'undefined') return ''; + return val; + }).join(',') + '\n'; } return exportFile({ body: csv, type: 'text/csv' }); diff --git a/public/js/graphs-export.js b/public/js/graphs-export.js deleted file mode 100644 index 92122bc..0000000 --- a/public/js/graphs-export.js +++ /dev/null @@ -1,71 +0,0 @@ -var header, data; -var isRecording = false; - -var graphsExport = { - start: function () { - isRecording = true; - this._startTime = new Date().getTime(); - this.reset(); - }, - stop: function () { - isRecording = false; - }, - reset: function () { - header = ['timestamp']; - data = []; - }, - isRecording: function () { - return isRecording; - }, - append: function (name, timestamp, dataset) { - if (!isRecording) return; - - var lastline = data[data.length - 1]; - - var line = new Array(header.length); - line[0] = (timestamp - this._startTime)/1000; - - var hasPushedNewData = false; - for (var key in dataset) { - var value = dataset[key]; - if (typeof value == 'undefined') continue; - var headerKey = name+'.'+key; - var i = header.indexOf(headerKey); - if (i >= 0) { - if (lastline && !hasPushedNewData && typeof lastline[i] != 'number') { - lastline[i] = value; - } else { - line[i] = value; - hasPushedNewData = true; - } - } else { - header.push(headerKey); - if (lastline) { - lastline.push(value); - } else { - line.push(value); - hasPushedNewData = true; - } - } - } - - if (hasPushedNewData) { - data.push(line); - } - }, - toCsv: function () { - if (!data || !header) return; - - var csv = ''; - csv += header.join(','); - - for (var i = 0; i < data.length; i++) { - var line = data[i].join(','); - csv += '\n'+line; - } - - return csv; - } -}; - -module.exports = graphsExport; diff --git a/public/js/index.js b/public/js/index.js index f09850f..7859f6b 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -155,124 +155,4 @@ hg.app(document.body, App(), App.render); }, 500); } }); - - $('#camera-record-btn').click(function () { - $(this).toggleClass('active'); - var enabled = $(this).is('.active'); - sendCommand('camera-record', enabled); - $('#camera-status-recording').toggle(enabled); - }); - - $('#sensor-record-btn').click(function () { - if (graphsExport.isRecording()) { - graphsExport.stop(); - } else { - graphsExport.start(); - } - $('#sensor-status-recording').toggle(graphsExport.isRecording()); - }); - $('#sensor-export-btn').click(function () { - var csv = graphsExport.toCsv(); - if (!csv) return; - var blob = new Blob([csv], { type: 'text/csv' }); - var url = URL.createObjectURL(blob); - window.open(url); - }); -} - -$(function () { - var quad = new Quadcopter(); - - // Add target to graphs export - (function () { - var target; - quad.cmd.on('orientation', function (t) { - target = t; - }); - quad.on('motors-speed', function () { - var timestamp = new Date().getTime(); - if (graphs.axes) { - for (var axis in target) { - if (graphs.axes.indexOf(axis) == -1) { - delete target[axis]; - } - } - } - graphsExport.append('target', timestamp, target) - }); - })(); - - quad.on('motors-speed', function (speeds) { - var exportedSpeeds = speeds.slice(); - if (graphs.axes) { - if (graphs.axes.indexOf('x') == -1) { - exportedSpeeds[0] = undefined; - exportedSpeeds[2] = undefined; - } - if (graphs.axes.indexOf('y') == -1) { - exportedSpeeds[1] = undefined; - exportedSpeeds[3] = undefined; - } - } - graphsExport.append('motors-speed', timestamp, exportedSpeeds); - }); - - quad.on('orientation', function (orientation) { - // Graphs - var timestamp = new Date().getTime(); - - function appendAxes(name, data) { - var axes = graphs.axes || ['x', 'y', 'z']; - - var exportedData = {}; - for (var i = 0; i < axes.length; i++) { - var axis = axes[i]; - if (typeof data[axis] == 'undefined') continue; - - var value = data[axis]; - exportedData[axis] = value; - } - - graphsExport.append(name, timestamp, exportedData); - } - - appendAxes('gyro', orientation.gyro); - appendAxes('accel', orientation.accel); - appendAxes('rotation', orientation.rotation); - }); - - // TODO: handle multiple config changes - quad.once('config', function (cfg) { - // Camera config - function initCameraConfigForm(form, type) { - var $form = $(form); - - $form.find('input,select').each(function (i, input) { - handleInput(input, 'camera.'+type); - }); - $form.submit(function (event) { - event.preventDefault(); - - sendCommand('config', cfg); - }); - - $form.find('#ISO-switch').change(function () { - if ($(this).prop('checked')) { - var val = parseInt($form.find('[name="ISO"]').val()); - accessor('camera.preview.ISO', val); - } else { - accessor('camera.preview.ISO', null); - } - }); - $form.find('[name="ISO"]').change(function () { - $form.find('#ISO-switch').prop('checked', true); - }); - } - initCameraConfigForm('#camera-config-preview', 'preview'); - initCameraConfigForm('#camera-config-record', 'record'); - - // PID controller config - $('#controller-btn').val(cfg.controller.updater); - }); -}); -*/ +}*/ diff --git a/public/js/input/device-orientation.js b/public/js/input/device-orientation.js deleted file mode 100644 index 4948ec7..0000000 --- a/public/js/input/device-orientation.js +++ /dev/null @@ -1,19 +0,0 @@ -function DeviceOrientationInput(cmd) { - if (!DeviceOrientationInput.isSupported()) { - throw new Error('DeviceOrientation not supported'); - } - - window.addEventListener('deviceorientation', function (event) { - cmd.send('orientation', { - x: event.beta, // front-to-back tilt in degrees, where front is positive - y: event.gamma, // left-to-right tilt in degrees, where right is positive - z: event.alpha - }); - }, false); -} - -DeviceOrientationInput.isSupported = function () { - return (typeof window.DeviceOrientationEvent != 'undefined'); -}; - -module.exports = DeviceOrientationInput; diff --git a/public/js/input/gamepad.js b/public/js/input/gamepad.js deleted file mode 100644 index e8657bb..0000000 --- a/public/js/input/gamepad.js +++ /dev/null @@ -1,62 +0,0 @@ -var $ = require('jquery'); - -function GamepadInput(cmd) { - if (!GamepadInput.isSupported()) { - throw new Error('Gamepad API not supported'); - } - - var that = this; - - window.addEventListener('gamepadconnected', function (e) { - console.log("Gamepad connected at index %d: %s. %d buttons, %d axes.", - e.gamepad.index, e.gamepad.id, - e.gamepad.buttons.length, e.gamepad.axes.length); - log('Gamepad "'+e.gamepad.id+'" connected.'); - - that.gamepad = navigator.getGamepads()[e.gamepad.index]; - that.calibrate(); - that._loop(cmd); - }); - - window.addEventListener('gamepaddisconnected', function (e) { - log('Gamepad "'+e.gamepad.id+'" disconnected.'); - }); -} - -GamepadInput.isSupported = function () { - return (!!navigator.getGamepads || !!navigator.webkitGetGamepads); -}; - -GamepadInput.prototype._loop = function (cmd) { - console.log(this.gamepad); - - var that = this; - - var interval = setInterval(function () { // TODO - if (!that.gamepad.connected) { - clearInterval(interval); - return; - } - - var cal = that.calibration; - var axes = that.gamepad.axes.map(function (value, i) { - if (cal && cal[i]) { - return value - cal[i]; - } - return value; - }); - - cmd.send('orientation', { - x: axes[0] * 90, - y: axes[1] * 90, - z: axes[2] * 90 - }); - cmd.send('power', - axes[3]); - }, 500); -}; - -GamepadInput.prototype.calibrate = function () { - this.calibration = $.extend([], this.gamepad.axes); -}; - -module.exports = GamepadInput; diff --git a/public/js/input/mouse.js b/public/js/input/mouse.js deleted file mode 100644 index bcfb521..0000000 --- a/public/js/input/mouse.js +++ /dev/null @@ -1,70 +0,0 @@ -var $ = require('jquery'); - -function MouseInput(cmd, el) { - var that = this; - - this.joystick = $(el); - this.handle = this.joystick.find('.handle'); - - var joystickSize = { - width: this.joystick.width(), - height: this.joystick.height() - }; - var handleSize = { - width: this.handle.width(), - height: this.handle.height() - }; - - this.pressed = true; - - var offset; - this.joystick.on('mousedown mousemove mouseup', function (event) { - if ((event.type == 'mousemove' && event.buttons) || event.type == 'mousedown') { - if (!that.pressed) { - that.joystick.addClass('active'); - that.pressed = true; - - offset = that.joystick.offset(); - } - - var x = event.pageX - offset.left - handleSize.width/2, - y = event.pageY - offset.top - handleSize.height/2; - - // In degrees - cmd.send('orientation', { - x: (x - joystickSize.width/2) / joystickSize.width * 90, // front-to-back tilt in degrees, where front is positive - y: (y - joystickSize.height/2) / joystickSize.height * 90, // left-to-right tilt in degrees, where right is positive - z: 0 - }); - } else { - if (that.pressed) { - that.joystick.removeClass('active'); - that.handle.css({ - left: joystickSize.width/2 - handleSize.width/2, - top: joystickSize.height/2 - handleSize.height/2 - }); - that.pressed = false; - - cmd.send('orientation', { - x: 0, - y: 0, - z: 0 - }); - } - } - }); - this.joystick.trigger('mouseup'); - - cmd.on('orientation', function (data) { - that.handle.css({ - left: joystickSize.width/2 - handleSize.width/2 + data.x/90*joystickSize.width, - top: joystickSize.height/2 - handleSize.height/2 + data.y/90*joystickSize.height - }); - }); -} - -MouseInput.isSupported = function () { - return true; -}; - -module.exports = MouseInput; diff --git a/public/js/input/ramp.js b/public/js/input/ramp.js deleted file mode 100644 index 2ceea74..0000000 --- a/public/js/input/ramp.js +++ /dev/null @@ -1,23 +0,0 @@ -function RampInput(cmd) { - this._cmd = cmd; -} - -RampInput.prototype.start = function(opts) { - this.stop(); - - var cmd = this._cmd; - - var startedAt = (new Date()).getTime(); - this._interval = setInterval(function () { - var t = ((new Date()).getTime() - startedAt) / 1000; - var orientation = { x: 0, y: 0, z: 0 }; - orientation[opts.axis] = opts.slope * t; - cmd.send('orientation', orientation); - }, 200); -}; - -RampInput.prototype.stop = function () { - clearInterval(this._interval); -}; - -module.exports = RampInput; diff --git a/public/js/input/sine.js b/public/js/input/sine.js deleted file mode 100644 index 8e7e71d..0000000 --- a/public/js/input/sine.js +++ /dev/null @@ -1,27 +0,0 @@ -function SineInput(cmd) { - this._cmd = cmd; -} - -SineInput.prototype.start = function(opts) { - this.stop(); - - var cmd = this._cmd; - - var A = opts.amplitude, - f = opts.frequency, - phi = opts.offset; - - var startedAt = (new Date()).getTime(); - this._interval = setInterval(function () { - var t = ((new Date()).getTime() - startedAt) / 1000; - var orientation = { x: 0, y: 0, z: 0 }; - orientation[opts.axis] = A * Math.sin(2 * Math.PI * f * t + phi); - cmd.send('orientation', orientation); - }, 200); -}; - -SineInput.prototype.stop = function () { - clearInterval(this._interval); -}; - -module.exports = SineInput; diff --git a/public/js/input/step.js b/public/js/input/step.js deleted file mode 100644 index 16f3478..0000000 --- a/public/js/input/step.js +++ /dev/null @@ -1,31 +0,0 @@ -function StepInput(cmd) { - this._cmd = cmd; -} - -StepInput.prototype.start = function(opts) { - this.stop(); - - var cmd = this._cmd; - - cmd.send('orientation', { - x: opts.x, - y: opts.y, - z: opts.z - }); - - if (opts.duration) { - this._timeout = setTimeout(function () { - cmd.send('orientation', { - x: 0, - y: 0, - z: 0 - }); - }, opts.duration); - } -}; - -StepInput.prototype.stop = function () { - clearTimeout(this._timeout); -}; - -module.exports = StepInput; From bc58bf6756bc160bfded4c36a1b2d1db7c17466f Mon Sep 17 00:00:00 2001 From: emersion Date: Mon, 22 Feb 2016 09:24:44 +0100 Subject: [PATCH 38/43] Adds camera profile editing, adds bitrate support in avconv --- config.js | 3 +-- lib/camera/stream.js | 7 ++++++- public/js/widget/camera-config.js | 17 +++++++---------- public/js/widget/camera-profile.js | 19 ++++++++++++++++--- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/config.js b/config.js index 9b128c8..37969ed 100644 --- a/config.js +++ b/config.js @@ -62,9 +62,8 @@ module.exports = { "vflip": false, "shutter": null, "drc": null, - "mode": 1, + "mode": 0, "bitrate": null, - "width": 400, "height": 400, "framerate": 10, diff --git a/lib/camera/stream.js b/lib/camera/stream.js index 7099883..ba81166 100644 --- a/lib/camera/stream.js +++ b/lib/camera/stream.js @@ -37,7 +37,7 @@ var avconv = function (options) { '-f', 'video4linux2', '-r', '30000/1001', '-i', '/dev/video0', - '-b:a', '2M', // ? + //'-b:a', '2M', // ? '-bt', '4M', '-vcodec', 'libx264', //'-pass', '1', @@ -64,6 +64,11 @@ var avconv = function (options) { args.push(options.framerate); } + if (options.bitrate) { + args.push('-b'); + args.push(options.bitrate); + } + if (options.output) { args.push(options.output); } else { diff --git a/public/js/widget/camera-config.js b/public/js/widget/camera-config.js index 21f26a5..e45e7f0 100644 --- a/public/js/widget/camera-config.js +++ b/public/js/widget/camera-config.js @@ -22,16 +22,13 @@ function CameraConfig(quad) { state.profiles.preview.config.set(config.camera.preview); state.profiles.record.config.set(config.camera.record); }); - - quad.cmd.on('config', function (config) { - if (!config.camera) return; - - if (config.camera.preview) { - state.profiles.preview.config.set(extend(state.config.preview(), config.camera.preview)); - } - if (config.camera.record) { - state.profiles.record.config.set(extend(state.config.record(), config.camera.record)); - } + quad.once('config', function (config) { + state.profiles.preview.config(function (config) { + quad.setConfig({ camera: { preview: config } }); + }); + state.profiles.record.config(function (config) { + quad.setConfig({ camera: { record: config } }); + }); }); return state; diff --git a/public/js/widget/camera-profile.js b/public/js/widget/camera-profile.js index e68ed42..41eaf11 100644 --- a/public/js/widget/camera-profile.js +++ b/public/js/widget/camera-profile.js @@ -2,6 +2,7 @@ var hg = require('mercury'); var h = require('mercury').h; +var extend = require('extend'); var Switch = require('../component/switch'); var formGroup = require('../component/form-group'); @@ -42,7 +43,7 @@ var meterings = [ ]; var drcs = [ - 'off', + '', 'low', 'medium', 'high' @@ -71,12 +72,24 @@ function CameraProfile() { } function change(state, data) { + delete data.value; // TODO + if (!state.isoSwitch.value()) { delete data.ISO; } data.vstab = state.vstabSwitch.value(); - console.log(data); + var current = state.config(); + for (var key in current) { + if (typeof current[key] === 'number') { + data[key] = parseFloat(data[key]) || 0; + } + if (data[key] === '') { + data[key] = null; + } + } + + state.config.set(extend({}, current, data)); } CameraProfile.render = function (state) { @@ -150,7 +163,7 @@ CameraProfile.render = function (state) { formGroup(h('abbr', { title: 'Dynamic Range Compression' }, 'DRC'), h('select.form-control', { name: 'drc' }, drcs.map(function (item) { - return h('option', { selected: (state.config.drc === item) }, item); + return h('option', { value: item, selected: (state.config.drc === item) }, item || 'off'); }))), formGroup('Mode', h('select.form-control', { name: 'mode' From dc57b2b6ad5504b5095399153885ce52debcdbd9 Mon Sep 17 00:00:00 2001 From: emersion Date: Fri, 4 Mar 2016 14:59:06 +0100 Subject: [PATCH 39/43] (Very) small typos --- lib/quadcopter.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/quadcopter.js b/lib/quadcopter.js index f4645f8..f4f5dc5 100644 --- a/lib/quadcopter.js +++ b/lib/quadcopter.js @@ -36,13 +36,13 @@ class Quadcopter extends QuadcopterBase { that._resetMotorsSpeeds(true); }, function (err) { - console.error('WARN: cannot load motors', err); + console.error('WARN: cannot load motors:', err); }); // Then Inertial Measurement Unit console.log('Loading sensors...'); var promise = loadSensors(this).catch(function (err) { - console.error('WARN: cannot load sensors', err); + console.error('WARN: cannot load sensors:', err); }).then(function (sensors) { that.sensors = sensors || {}; if (that.sensors.mpu6050) { @@ -58,7 +58,7 @@ class Quadcopter extends QuadcopterBase { return that.camera.check().then(function () { that.features.push('camera'); }, function (err) { - console.error('WARN: camera not available', err); + console.error('WARN: camera not available:', err); }); }).then(function () { // Ready :-) From 82425f70ef0908f5d483d46aea01253950acb369 Mon Sep 17 00:00:00 2001 From: emersion Date: Fri, 4 Mar 2016 15:33:42 +0100 Subject: [PATCH 40/43] Begins to send direction --- package.json | 1 + public/js/direction-sender.js | 28 ++++++++++++++++++++++++++++ public/js/index.js | 4 ++++ 3 files changed, 33 insertions(+) create mode 100644 public/js/direction-sender.js diff --git a/package.json b/package.json index 34183e0..8d639c9 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "servoblaster": "^0.1.0", "smoothie": "^1.27.0", "stream-split": "^0.2.0", + "throttle-function": "^0.1.0", "through": "^2.3.8", "util": "^0.10.3" } diff --git a/public/js/direction-sender.js b/public/js/direction-sender.js new file mode 100644 index 0000000..b0c6d06 --- /dev/null +++ b/public/js/direction-sender.js @@ -0,0 +1,28 @@ +var throttle = require('throttle-function'); + +module.exports = function (cmd) { + return function (state) { + if (!state.x || !state.y) { + throw new Error('Provided state is missing values x, y'); + } + + function update() { + console.log('update'); + cmd.send('orientation', { + x: state.x(), + y: state.y(), + z: (state.z) ? state.z() : 0 + }); + } + + var throttled = throttle(update, 100); + + state.x(throttled); + state.y(throttled); + if (state.z) { + state.z(throttled); + } + + // TODO: power + }; +}; diff --git a/public/js/index.js b/public/js/index.js index 7859f6b..01dee0d 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -23,6 +23,7 @@ var ChartRecorder = require('./widget/chart-recorder'); var Camera = require('./widget/camera'); var Config = require('./widget/config'); +var DirectionSender = require('./direction-sender'); var MouseDirection = require('./direction/mouse'); var DeviceOrientationDirection = require('./direction/device-orientation'); var GamepadDirection = require('./direction/gamepad'); @@ -71,6 +72,9 @@ function App() { state.cameraAvailable.set(features.hardware.indexOf('camera') !== -1); }); + var sender = DirectionSender(quad.cmd); + sender(state.direction.mouse); + return state; } From 4fac4296829ab52300227ac15fb105ae0e6be701 Mon Sep 17 00:00:00 2001 From: emersion Date: Sat, 5 Mar 2016 11:29:50 +0100 Subject: [PATCH 41/43] Direction sender is now operational --- package.json | 2 +- public/js/direction-sender.js | 3 +-- public/js/index.js | 4 +++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 8d639c9..5868676 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "servoblaster": "^0.1.0", "smoothie": "^1.27.0", "stream-split": "^0.2.0", - "throttle-function": "^0.1.0", + "throttleit": "^1.0.0", "through": "^2.3.8", "util": "^0.10.3" } diff --git a/public/js/direction-sender.js b/public/js/direction-sender.js index b0c6d06..3506ec5 100644 --- a/public/js/direction-sender.js +++ b/public/js/direction-sender.js @@ -1,4 +1,4 @@ -var throttle = require('throttle-function'); +var throttle = require('throttleit'); module.exports = function (cmd) { return function (state) { @@ -7,7 +7,6 @@ module.exports = function (cmd) { } function update() { - console.log('update'); cmd.send('orientation', { x: state.x(), y: state.y(), diff --git a/public/js/index.js b/public/js/index.js index 01dee0d..37570d5 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -73,7 +73,9 @@ function App() { }); var sender = DirectionSender(quad.cmd); - sender(state.direction.mouse); + ['mouse', 'deviceOrientation', 'gamepad', 'step', 'sine', 'ramp'].forEach(function (key) { + sender(state.direction[key]); + }); return state; } From d676c1ae0b3152e552b17564ad44b8920b7897b5 Mon Sep 17 00:00:00 2001 From: emersion Date: Sat, 5 Mar 2016 15:19:06 +0100 Subject: [PATCH 42/43] Moves rotation input to mouse direction widget --- public/assets/main.css | 12 ++++- public/js/direction/mouse.js | 71 +++++++++++++++++++++++++----- public/js/event/drag.js | 54 ++++++++++++----------- public/js/index.js | 15 +++---- public/js/widget/controller-btn.js | 2 +- public/js/widget/rotation-input.js | 38 ---------------- 6 files changed, 106 insertions(+), 86 deletions(-) delete mode 100644 public/js/widget/rotation-input.js diff --git a/public/assets/main.css b/public/assets/main.css index a99d5a2..587187b 100644 --- a/public/assets/main.css +++ b/public/assets/main.css @@ -121,7 +121,17 @@ input[type="number"].form-control { opacity: 0.2; pointer-events: none; } -.direction-mouse:active .handle { +.direction-mouse .handle .tooltip { + width: 100px; + top: -35px; + left: -42px; +} +.direction-mouse:active .handle, +.direction-mouse.active .handle { + opacity: 1; +} +.direction-mouse:active .handle .tooltip, +.direction-mouse.active .handle .tooltip { opacity: 1; } diff --git a/public/js/direction/mouse.js b/public/js/direction/mouse.js index 9d763ca..eb21858 100644 --- a/public/js/direction/mouse.js +++ b/public/js/direction/mouse.js @@ -2,7 +2,9 @@ var hg = require('mercury'); var h = require('mercury').h; +var roundTo = require('round-to'); var sendDrag = require('../event/drag'); +var AttributeHook = require('../hook/attribute'); var WIDTH = 300, HEIGHT = 300; var HANDLE_WIDTH = 16, HANDLE_HEIGHT = 16; @@ -16,8 +18,12 @@ function MouseDirection() { return hg.state({ x: hg.value(0), y: hg.value(0), + z: hg.value(0), + range: hg.value(90), channels: { - moveHandle: moveHandle + moveHandle: moveHandle, + changeRange: changeRange, + changeRotation: changeRotation } }); } @@ -34,20 +40,61 @@ function moveHandle(state, data) { return; } - state.x.set(x); - state.y.set(y); + var range = state.range(); + state.x.set(x * range); + state.y.set(y * range); +} + +function changeRange(state, data) { + state.range.set(parseFloat(data.range)); +} + +function changeRotation(state, data) { + state.z.set(parseFloat(data.rotation)); } MouseDirection.render = function (state) { - var x = center.x + state.x*WIDTH/2; - var y = center.y + state.y*HEIGHT/2; - - return h('.direction-mouse', { - 'ev-mousedown': sendDrag(state.channels.moveHandle) - }, [ - h('.handle', { - style: { left: x + 'px', top: y + 'px' } - }) + var x = center.x + state.x/state.range*WIDTH/2; + var y = center.y + state.y/state.range*HEIGHT/2; + + return h('.row', [ + h('.col-sm-9', [ + h('.direction-mouse', { + 'ev-mousedown': sendDrag(state.channels.moveHandle) + }, [ + h('.handle' + ((state.x !== 0 || state.y !== 0) ? '.active' : ''), { + style: { left: x + 'px', top: y + 'px' } + }, [ + h('.tooltip.top', [ + h('.tooltip-arrow'), + h('.tooltip-inner', roundTo(state.x, 2) + ', ' + roundTo(state.y, 2)) + ]) + ]) + ]), + h('.form-inline', [ + h('label.control-label', { htmlFor: 'direction-mouse-range-input' }, 'Range:'), + h('input#direction-mouse-range-input.form-control', { + type: 'number', + name: 'range', + 'ev-event': hg.sendChange(state.channels.changeRange), + value: state.range + }), + ' °' + ]) + ]), + h('.col-sm-3', [ + h('span', 'Rotation'), + h('input', { + type: 'range', + name: 'rotation', + value: state.z, + min: -90, + max: 90, + step: 5, + orient: AttributeHook('vertical'), + 'ev-event': hg.sendChange(state.channels.changeRotation) + }) + ]) ]); }; diff --git a/public/js/event/drag.js b/public/js/event/drag.js index c5efc21..67ede95 100644 --- a/public/js/event/drag.js +++ b/public/js/event/drag.js @@ -4,30 +4,32 @@ var hg = require('mercury'); var extend = require('extend'); module.exports = hg.BaseEvent(function (ev, broadcast) { - var data = this.data; - var delegator = hg.Delegator(); - - var offset = ev.target.getBoundingClientRect(); - - function onmove(ev) { - broadcast(extend(data, { - down: true, - x: ev.clientX - offset.left, - y: ev.clientY - offset.top - })); - } - - function onup(ev) { - delegator.unlistenTo('mousemove'); - delegator.removeGlobalEventListener('mousemove', onmove); - delegator.removeGlobalEventListener('mouseup', onup); - - broadcast(extend(data, { - down: false - })); - } - - delegator.listenTo('mousemove'); - delegator.addGlobalEventListener('mousemove', onmove); - delegator.addGlobalEventListener('mouseup', onup); + var data = this.data; + var delegator = hg.Delegator(); + + var offset = ev.target.getBoundingClientRect(); + + function onmove(ev) { + broadcast(extend(data, { + down: true, + x: ev.clientX - offset.left, + y: ev.clientY - offset.top + })); + } + + function onup(ev) { + delegator.unlistenTo('mousemove'); + delegator.removeGlobalEventListener('mousemove', onmove); + delegator.removeGlobalEventListener('mouseup', onup); + + broadcast(extend(data, { + down: false + })); + } + + onmove(ev); + + delegator.listenTo('mousemove'); + delegator.addGlobalEventListener('mousemove', onmove); + delegator.addGlobalEventListener('mouseup', onup); }); diff --git a/public/js/index.js b/public/js/index.js index 37570d5..ae339b5 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -14,7 +14,6 @@ var ControllerBtn = require('./widget/controller-btn'); var SystemSummary = require('./widget/system-summary'); var Charts = require('./widget/charts'); var PowerInput = require('./widget/power-input'); -var RotationInput = require('./widget/rotation-input'); var Outline = require('./widget/outline'); var MotorsSummary = require('./widget/motors-summary'); var OrientationSummary = require('./widget/orientation-summary'); @@ -40,7 +39,7 @@ function App() { enableBtn: EnableBtn(quad), controllerBtn: ControllerBtn(quad), systemSummary: SystemSummary(quad), - directionTabs: Tabs(['custom', 'step', 'sine', 'ramp']), + directionTabs: Tabs(['mouse', 'device', 'step', 'sine', 'ramp']), direction: hg.struct({ mouse: MouseDirection(), deviceOrientation: DeviceOrientationDirection(), @@ -50,7 +49,6 @@ function App() { ramp: RampDirection() }), powerInput: PowerInput(quad), - rotationInput: RotationInput(quad), outline: hg.struct({ top: Outline.Top(quad), front: Outline.Front(quad), @@ -97,13 +95,15 @@ App.render = function (state) { h('hr'), h('.container-fluid', h('.row', [ - h('.col-lg-3.col-xs-6.text-center', [ // .hidden-xs.hidden-sm + h('.col-lg-4.col-xs-9.text-center', [ h('div', [ h('strong', 'Direction'), hg.partial(Tabs.render, state.directionTabs) ]), - Tabs.renderContainer(state.directionTabs, 'custom', [ - hg.partial(MouseDirection.render, state.direction.mouse), + Tabs.renderContainer(state.directionTabs, 'mouse', [ // .hidden-xs.hidden-sm + hg.partial(MouseDirection.render, state.direction.mouse) + ]), + Tabs.renderContainer(state.directionTabs, 'device', [ hg.partial(DeviceOrientationDirection.render, state.direction.deviceOrientation), hg.partial(GamepadDirection.render, state.direction.gamepad) ]), @@ -117,8 +117,7 @@ App.render = function (state) { hg.partial(RampDirection.render, state.direction.ramp) ]) ]), - h('.col-lg-2.col-sm-3.col-xs-6.text-center', hg.partial(PowerInput.render, state.powerInput)), - h('.col-lg-1.col-sm-3.col-xs-6.text-center', hg.partial(RotationInput.render, state.rotationInput)), + h('.col-lg-2.col-xs-3.text-center', hg.partial(PowerInput.render, state.powerInput)), h('span.clearfix.visible-sm'), h('.col-lg-3.col-sm-6.col-xs-12.text-center', [ hg.partial(Outline.Top.render, state.outline.top), diff --git a/public/js/widget/controller-btn.js b/public/js/widget/controller-btn.js index 138f61a..97365b6 100644 --- a/public/js/widget/controller-btn.js +++ b/public/js/widget/controller-btn.js @@ -27,7 +27,7 @@ function ControllerBtn(quad) { } ControllerBtn.render = function (state) { - return h('div.form-inline', [ + return h('.form-inline', [ h('label.control-label', { htmlFor: 'controller-btn' }, 'Controller:'), h('select#controller-btn.form-control', { name: 'controller', diff --git a/public/js/widget/rotation-input.js b/public/js/widget/rotation-input.js deleted file mode 100644 index 275a8be..0000000 --- a/public/js/widget/rotation-input.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; - -var hg = require('mercury'); -var h = require('mercury').h; -var AttributeHook = require('../hook/attribute'); - -function RotationInput(quad) { - // TODO - - return hg.state({ - value: hg.value(0), - channels: { - change: change - } - }); -} - -function change(state, data) { - state.value.set(parseFloat(data.power) / 100); -} - -RotationInput.render = function (state) { - return h('div', [ - h('strong', 'Rotation'), - h('input', { - type: 'range', - name: 'power', - value: state.value * 100, - min: -100, - max: 100, - step: 5, - orient: AttributeHook('vertical'), - 'ev-event': hg.sendChange(state.channels.change) - }) - ]); -}; - -module.exports = RotationInput; From b63026b3f2f1268f4ae82cdf5b3483de89c207fa Mon Sep 17 00:00:00 2001 From: emersion Date: Sat, 5 Mar 2016 20:02:25 +0100 Subject: [PATCH 43/43] Send new config when changed --- public/js/widget/config.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/public/js/widget/config.js b/public/js/widget/config.js index c61f40c..7e244f3 100644 --- a/public/js/widget/config.js +++ b/public/js/widget/config.js @@ -13,7 +13,7 @@ function Config(quad) { channels: { change: function (state, data) { data = cast(state.config(), expand(data)); - console.log('change', data); // TODO + quad.cmd.send('config', data); }, export: function (state) { exportFile({ @@ -36,15 +36,20 @@ function Config(quad) { } function cast(from, to) { - for (var name in to) { - if (typeof from[name] === 'object') { - to[name] = cast(from[name], to[name]); - } else if (typeof from[name] == 'number') { - if (typeof to[name] === 'string') { - to[name] = parseFloat(to[name]); - } + if (from instanceof Array) { + to = Object.keys(to).map(function (key, i) { + return cast(from[i], to[key]); + }); + } else if (typeof from === 'object') { + for (var key in to) { + to[key] = cast(from[key], to[key]); + } + } else if (typeof from === 'number') { + if (typeof to === 'string') { + to = parseFloat(to); } } + return to; }