diff --git a/index.html b/index.html
index 9c8aa27..2e00d4b 100644
--- a/index.html
+++ b/index.html
@@ -43,6 +43,10 @@ body{
float:left;
width:50%;
}
+.log{
+ clear:both;
+ display:none;
+}
@@ -87,10 +91,13 @@ body{
Battery:
0%
+
no data
+
+
@@ -103,7 +110,7 @@ body{
var activeWaypoints = [];
var waypoints = [];
var follow = false;
-
+
var phoneIcon = L.icon({
iconUrl: 'images/iphone.png'
});
@@ -207,8 +214,9 @@ body{
follow = true
})
$('#manual').click(function(){
- // control via xbox
follow = false
+ clearCurrentTarget()
+ nodecopterGamepad.initGamepad(socket);
})
})
diff --git a/public/gamepad-calibration.js b/public/gamepad-calibration.js
new file mode 100644
index 0000000..fd182f6
--- /dev/null
+++ b/public/gamepad-calibration.js
@@ -0,0 +1,57 @@
+/*
+ * gamepad-calibration for the nodecopter-gamepad module.
+ *
+ * calibration is initialized with:
+ *
+ * nodecopter.initCalibrators([
+ * { min: -1, max: 1, center: 0 }, // left-x
+ * { min: -1, max: 1, center: 0 }, // left-y
+ * { min: -1, max: 1, center: 0 }, // right-x
+ * { min: -1, max: 1, center: 0 } // right-y
+ * ]);
+ *
+ * where min/max/center values are observed values from the gamepad for the maximum values.
+ * After initialization, calibrated values can be retrieved with:
+ *
+ * var leftX = nodecopter.getCalibratedValue(gamepad.axes, AXES.LEFT_ANALOGUE_HOR);
+ *
+ * @author Martin Schuhfuss
+ */
+(function(exports) {
+ "use strict";
+
+ var calibrators = [];
+
+ // creates a calibration-function for each of the calibration-datasets
+ exports.initCalibrators = function(calibrationData) {
+ return calibrators = calibrationData.map(function(cd) {
+ // intercept invalid calibration-data, ensure that `min < center < max`
+ if(cd.min >= cd.center) { throw new Error('invalid calibration-data (min >= center)'); }
+ if(cd.max <= cd.center) { throw new Error('invalid calibration-data (max <= center)'); }
+
+ var negDelta = Math.abs(cd.center - cd.min),
+ posDelta = Math.abs(cd.center - cd.max),
+ negScaling = 1/negDelta,
+ posScaling = 1/posDelta;
+
+ // the calibration-function calculates calibrated output-values from raw-inputs, clipped to range [-1, 1]
+ return function(rawValue) {
+ var centerOffset = rawValue - cd.center;
+
+ if(centerOffset < 0) {
+ return Math.max(-1, centerOffset * negScaling);
+ } else if(centerOffset > 0) {
+ return Math.min(1, centerOffset * posScaling);
+ } else { return 0.0; }
+ };
+ });
+ };
+
+ exports.getCalibratedValue = function(axesData, axisId) {
+ var rawValue = axesData[axisId];
+
+ if(!calibrators[axisId]) { return rawValue; }
+
+ return calibrators[axisId](rawValue);
+ };
+}((typeof exports === 'undefined')? (this.nodecopterGamepad = this.nodecopterGamepad||{}) : exports));
\ No newline at end of file
diff --git a/public/gamepad-client.js b/public/gamepad-client.js
new file mode 100644
index 0000000..82c5545
--- /dev/null
+++ b/public/gamepad-client.js
@@ -0,0 +1,194 @@
+(function(exports) {
+ "use strict";
+
+ // lookup-tables
+ var BTN = {
+ // Face (main) buttons
+ FACE_1: 0, FACE_2: 1, FACE_3: 2, FACE_4: 3,
+
+ // Top/bottom shoulder buttons
+ LEFT_SHOULDER: 4, RIGHT_SHOULDER: 5,
+ LEFT_SHOULDER_BOTTOM: 6, RIGHT_SHOULDER_BOTTOM: 7,
+
+ SELECT: 8, START: 9,
+
+ // Analogue stick-buttons (if depressible)
+ LEFT_ANALOGUE_STICK: 10,
+ RIGHT_ANALOGUE_STICK: 11,
+
+ // Directional (discrete) pad
+ PAD_TOP: 12, PAD_BOTTOM: 13, PAD_LEFT: 14, PAD_RIGHT: 15
+ };
+
+ var AXES = {
+ LEFT_ANALOGUE_HOR: 0, LEFT_ANALOGUE_VERT: 1,
+ RIGHT_ANALOGUE_HOR: 2, RIGHT_ANALOGUE_VERT: 3
+ };
+
+
+ var socket = null;
+
+ /**
+ * initializes the gamepad-controls
+ *
+ * @param websocket the websocket control-events are sent to
+ * @param options TODO...
+ * some ideas:
+ * - calibration-data
+ * - handler-callbacks for 'gamepadState'-events
+ * - button-configuration
+ */
+ exports.initGamepad = function(websocket, options) {
+ socket = websocket;
+
+ // kick-off control-loop
+ (function __controlLoop() {
+ requestAnimationFrame(__controlLoop);
+
+ // TODO: only works in newer versions of chrome, adapt for mozilla-APIā¦
+ var gamepad = navigator.webkitGetGamepads()[0];
+
+ // TODO: emit events gamepadConnect/gamepadDisconnect
+ if(!gamepad) { return; }
+
+ handleGamepadState(gamepad);
+ } ());
+ };
+
+ // initialize
+ var lastGamepadState = {
+ leftX: 0.0, leftY: 0.0,
+ btnStart: false, btnStop: false,
+ btnTurnCW: false, btnTurnCCW: false,
+ btnUp: false, btnDown: false
+ };
+
+ var getCalibratedValue = exports.getCalibratedValue || function(axes,idx) { return axes[idx]; };
+
+ function handleGamepadState(gamepad) {
+ var leftX, leftY;
+
+ leftX = getCalibratedValue(gamepad.axes, AXES.LEFT_ANALOGUE_HOR);
+ leftY = getCalibratedValue(gamepad.axes, AXES.LEFT_ANALOGUE_VERT);
+
+ // this is for better readability only and might be inlined.
+ // (I suspect the js-engine will likely inline it anyway).
+ var gamepadState = {
+ // toFixed(1) to prevent too many useless nav-packets
+ leftX: leftX.toFixed(1),
+ leftY: leftY.toFixed(1),
+
+ // buttons are converted to boolean for easier handling
+ btnStart: (1==gamepad.buttons[BTN.START]),
+ btnStop: (1==gamepad.buttons[BTN.SELECT]),
+ btnTurnCW: (1==gamepad.buttons[BTN.RIGHT_SHOULDER]),
+ btnTurnCCW: (1==gamepad.buttons[BTN.LEFT_SHOULDER]),
+ btnDown: (1==gamepad.buttons[BTN.FACE_1]),
+ btnUp: (1==gamepad.buttons[BTN.FACE_4]),
+
+ btnFlipFwd: (1==gamepad.buttons[BTN.PAD_TOP]),
+ btnFlipBwd: (1==gamepad.buttons[BTN.PAD_BOTTOM]),
+ btnFlipLeft: (1==gamepad.buttons[BTN.PAD_LEFT]),
+ btnFlipRight: (1==gamepad.buttons[BTN.PAD_RIGHT])
+ };
+
+ // ---- logging
+ // TODO: emit a gamepadState-event or something
+ document.querySelector('.log').innerHTML = JSON.stringify(gamepadState, null, 2);
+
+ // ---- analogue-stick left/right
+ var horiz=gamepadState.leftX;
+ if(horiz != lastGamepadState.leftX) {
+ if(horiz<0) { // negative: left
+ socket.emit('control', { action: 'left', speed: -horiz });
+ } else if(horiz>0) {
+ socket.emit('control', { action: 'right', speed: horiz });
+ } else { // == 0
+ socket.emit('control', { action: 'left', speed: 0 });
+ socket.emit('control', { action: 'right', speed: 0 });
+ }
+ }
+
+ // ---- analogue-stick up/down
+ var leftY=gamepadState.leftY;
+ if(leftY != lastGamepadState.leftY) {
+ if(leftY<0) { // negative: up
+ socket.emit('control', { action: 'front', speed: -leftY });
+ } else if(leftY>0) {
+ socket.emit('control', { action: 'back', speed: leftY });
+ } else { // == 0
+ socket.emit('control', { action: 'front', speed: 0 });
+ socket.emit('control', { action: 'back', speed: 0 });
+ }
+ }
+
+ // ---- takeoff/land
+ if(gamepadState.btnStart && !lastGamepadState.btnStart) {
+ socket.emit('control', { action: 'takeoffOrLand' });
+ }
+
+ // ---- stop-button
+ if(gamepadState.btnStop && !lastGamepadState.btnStop) {
+ socket.emit('control', { action: 'stop' });
+ }
+
+ // ---- up/down/cw/ccw buttons (TODO: add accelleration for more fine-grained control)
+ var evMap = {
+ btnUp: { action: 'up', mode: 'toggleSpeed' },
+ btnDown: { action: 'down', mode: 'toggleSpeed' },
+ btnTurnCW: { action: 'clockwise', mode: 'toggleSpeed' },
+ btnTurnCCW: { action: 'counterClockwise', mode: 'toggleSpeed' },
+
+ btnFlipFwd: { action: 'animate', animation: 'flipAhead', mode: 'trigger' },
+ btnFlipBwd: { action: 'animate', animation: 'flipBehind', mode: 'trigger' },
+ btnFlipLeft: { action: 'animate', animation: 'flipLeft', mode: 'trigger' },
+ btnFlipRight: { action: 'animate', animation: 'flipRight', mode: 'trigger' }
+ };
+
+ Object.keys(evMap).forEach(function(btnId) {
+ var evData = evMap[btnId],
+ curr = gamepadState[btnId],
+ last = lastGamepadState[btnId];
+
+ if('toggleSpeed' == evData.mode) {
+ if(curr && !last) { // btnPress
+ socket.emit('control', { action: evData.action, speed: 0.5 });
+ } else if(!curr && last) { // btnRelease
+ socket.emit('control', { action: evData.action, speed: 0 });
+ }
+ } else if('trigger' == evData.mode) {
+ if(curr && !last) {
+ socket.emit('control', { action: evData.action, animation: evData.animation, duration: 15 });
+ }
+ }
+ });
+
+ lastGamepadState = gamepadState;
+ };
+} ( (typeof exports === 'undefined')? (this.nodecopterGamepad = this.nodecopterGamepad||{}) : exports) );
+
+// RAF-Polyfill
+(function(window) {
+ var lastTime = 0;
+ var vendors = ['ms', 'moz', 'webkit', 'o'];
+ for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
+ window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
+ window.cancelAnimationFrame =
+ window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
+ }
+
+ if (!window.requestAnimationFrame)
+ window.requestAnimationFrame = function(callback, element) {
+ var currTime = new Date().getTime();
+ var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+ var id = window.setTimeout(function() { callback(currTime + timeToCall); },
+ timeToCall);
+ lastTime = currTime + timeToCall;
+ return id;
+ };
+
+ if (!window.cancelAnimationFrame)
+ window.cancelAnimationFrame = function(id) {
+ clearTimeout(id);
+ };
+}(window||{}));
\ No newline at end of file
diff --git a/server.js b/server.js
index cecb89b..af8e68c 100644
--- a/server.js
+++ b/server.js
@@ -24,6 +24,15 @@ io.set('destroy upgrade', false)
io.sockets.on('connection', function(socket) {
console.log('connection')
+ socket.on('control', function(ev) {
+ console.log('[control]', JSON.stringify(ev));
+ if(ev.action == 'animate'){
+ client.animate(ev.animation, ev.duration)
+ } else {
+ client[ev.action].call(client, ev.speed);
+ }
+ })
+
socket.on('takeoff', function(data){
console.log('takeoff', data)
client.takeoff()