Added gamepad api for manual control
Esse commit está contido em:
+9
-1
@@ -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);
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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));
|
||||
@@ -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||{}));
|
||||
@@ -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()
|
||||
|
||||
Referência em uma Nova Issue
Bloquear um usuário