Added gamepad api for manual control

Esse commit está contido em:
Andrew Nesbitt
2013-10-02 23:33:52 +01:00
commit de0202a0f2
4 arquivos alterados com 270 adições e 2 exclusões
+9 -1
Ver Arquivo
@@ -43,6 +43,10 @@ body{
float:left;
width:50%;
}
.log{
clear:both;
display:none;
}
</style>
<link rel="stylesheet" href="/leaflet.css" />
@@ -87,10 +91,13 @@ body{
Battery:
<span class='battery'>0</span>%
</div>
<pre class="log">no data</pre>
</div>
<script src="http://maps.google.com/maps/api/js?v=3.2&sensor=false"></script>
<script src="leaflet.js"></script>
<script src="gamepad-client.js"></script>
<script src="gamepad-calibration.js"></script>
<script src="leaflet-google.js"></script>
<script src="/dronestream/nodecopter-client.js"></script>
<script src="jquery.js"></script>
@@ -207,8 +214,9 @@ body{
follow = true
})
$('#manual').click(function(){
// control via xbox
follow = false
clearCurrentTarget()
nodecopterGamepad.initGamepad(socket);
})
})
+57
Ver Arquivo
@@ -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 <m.schuhfuss@gmail.com>
*/
(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));
+194
Ver Arquivo
@@ -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||{}));
+9
Ver Arquivo
@@ -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()