Add control sticks for MSP Rx, suitable for debugging CF without a Tx

10.3.x-maintenance
Nicholas Sherlock 2015-07-23 12:54:34 +12:00
parent fb46711659
commit ef0c637877
8 changed files with 455 additions and 4 deletions

View File

@ -672,6 +672,9 @@
"receiverButtonRefresh": { "receiverButtonRefresh": {
"message": "Refresh" "message": "Refresh"
}, },
"receiverButtonSticks": {
"message": "Control sticks"
},
"receiverDataRefreshed": { "receiverDataRefreshed": {
"message": "RC Tuning data <strong>refreshed</strong>" "message": "RC Tuning data <strong>refreshed</strong>"
}, },
@ -1146,5 +1149,41 @@
}, },
"ledStripEepromSaved": { "ledStripEepromSaved": {
"message": "EEPROM <span style=\"color: green\">saved</span>" "message": "EEPROM <span style=\"color: green\">saved</span>"
},
"controlAxisRoll": {
"message": "Roll"
},
"controlAxisPitch": {
"message": "Pitch"
},
"controlAxisYaw": {
"message": "Yaw"
},
"controlAxisThrottle": {
"message": "Throttle"
},
"controlAxisAux1": {
"message": "AUX 1"
},
"controlAxisAux2": {
"message": "AUX 2"
},
"controlAxisAux3": {
"message": "AUX 3"
},
"controlAxisAux4": {
"message": "AUX 4"
},
"controlAxisAux5": {
"message": "AUX 5"
},
"controlAxisAux6": {
"message": "AUX 6"
},
"controlAxisAux7": {
"message": "AUX 7"
},
"controlAxisAux8": {
"message": "AUX 8"
} }
} }

View File

@ -1130,6 +1130,22 @@ MSP.crunch = function (code) {
return buffer; return buffer;
}; };
/**
* Set raw Rx values over MSP protocol.
*
* Channels is an array of 16-bit unsigned integer channel values to be sent. 8 channels is probably the maximum.
*/
MSP.setRawRx = function(channels) {
var buffer = [];
for (var i = 0; i < channels.length; i++) {
buffer.push(specificByte(channels[i], 0));
buffer.push(specificByte(channels[i], 1));
}
MSP.send_message(MSP_codes.MSP_SET_RAW_RC, buffer, false);
}
/** /**
* Send a request to read a block of data from the dataflash at the given address and pass that address and a dataview * Send a request to read a block of data from the dataflash at the given address and pass that address and a dataview
* of the returned data to the given callback (or null for the data if an error occured). * of the returned data to the given callback (or null for the data if an error occured).

View File

@ -299,6 +299,7 @@
position: absolute; position: absolute;
bottom: 10px; bottom: 10px;
} }
.tab-receiver .sticks,
.tab-receiver .update, .tab-receiver .update,
.tab-receiver .refresh { .tab-receiver .refresh {
display: block; display: block;
@ -317,6 +318,7 @@
border: 1px solid silver; border: 1px solid silver;
background-color: #ececec; background-color: #ececec;
} }
.tab-receiver .sticks,
.tab-receiver .refresh { .tab-receiver .refresh {
margin-right: 10px; margin-right: 10px;
} }

View File

@ -86,5 +86,6 @@
<div class="buttons"> <div class="buttons">
<a class="update" href="#" i18n="receiverButtonSave"></a> <a class="update" href="#" i18n="receiverButtonSave"></a>
<a class="refresh" href="#" i18n="receiverButtonRefresh"></a> <a class="refresh" href="#" i18n="receiverButtonRefresh"></a>
<a class="sticks" href="#" i18n="receiverButtonSticks"></a>
</div> </div>
</div> </div>

View File

@ -18,7 +18,12 @@ TABS.receiver.initialize = function (callback) {
} }
function get_rc_map() { function get_rc_map() {
MSP.send_message(MSP_codes.MSP_RX_MAP, false, false, load_html); MSP.send_message(MSP_codes.MSP_RX_MAP, false, false, load_config);
}
// Fetch features so we can check if RX_MSP is enabled:
function load_config() {
MSP.send_message(MSP_codes.MSP_BF_CONFIG, false, false, load_html);
} }
function load_html() { function load_html() {
@ -52,7 +57,12 @@ TABS.receiver.initialize = function (callback) {
}); });
// generate bars // generate bars
var bar_names = ['Roll', 'Pitch', 'Yaw', 'Throttle'], var bar_names = [
chrome.i18n.getMessage('controlAxisRoll'),
chrome.i18n.getMessage('controlAxisPitch'),
chrome.i18n.getMessage('controlAxisYaw'),
chrome.i18n.getMessage('controlAxisThrottle')
],
bar_container = $('.tab-receiver .bars'), bar_container = $('.tab-receiver .bars'),
aux_index = 1; aux_index = 1;
@ -61,7 +71,7 @@ TABS.receiver.initialize = function (callback) {
if (i < bar_names.length) { if (i < bar_names.length) {
name = bar_names[i]; name = bar_names[i];
} else { } else {
name = 'AUX ' + aux_index++; name = chrome.i18n.getMessage("controlAxisAux" + (aux_index++));
} }
bar_container.append('\ bar_container.append('\
@ -302,6 +312,27 @@ TABS.receiver.initialize = function (callback) {
MSP.send_message(MSP_codes.MSP_SET_RC_TUNING, MSP.crunch(MSP_codes.MSP_SET_RC_TUNING), false, save_rc_map); MSP.send_message(MSP_codes.MSP_SET_RC_TUNING, MSP.crunch(MSP_codes.MSP_SET_RC_TUNING), false, save_rc_map);
}); });
$("a.sticks").click(function() {
var
windowWidth = 370,
windowHeight = 510;
chrome.app.window.create("/tabs/receiver_msp.html", {
id: "receiver_msp",
innerBounds: {
minWidth: windowWidth, minHeight: windowHeight,
width: windowWidth, height: windowHeight,
maxWidth: windowWidth, maxHeight: windowHeight
}
}, function(createdWindow) {
// Give the window a callback it can use to access our MSP object to send to CF
createdWindow.contentWindow.setRawRx = MSP.setRawRx;
});
});
// Only show the MSP control sticks if the MSP Rx feature is enabled
$("a.sticks").toggle(bit_check(BF_CONFIG.features, 14 /* RX_MSP */));
$('select[name="rx_refresh_rate"]').change(function () { $('select[name="rx_refresh_rate"]').change(function () {
var plot_update_rate = parseInt($(this).val(), 10); var plot_update_rate = parseInt($(this).val(), 10);

109
tabs/receiver_msp.css Normal file
View File

@ -0,0 +1,109 @@
body {
font-family: 'Segoe UI', Tahoma, sans-serif;
font-size: 12px;
color: #303030;
margin: 10px;
}
.control-gimbals {
/* A generous padding around the window edges ensures that we continue to receive mousemove events (since
* cursor stays in the window for longer)
*/
padding:25px;
padding-bottom:0;
text-align:center;
}
.control-gimbal {
position:relative;
width:120px;
height:120px;
background-color:#eee;
margin-left:1em;
margin-right:1em;
margin-bottom:2em;
display:inline-block;
border-radius:5px;
cursor:pointer;
}
.crosshair {
display:block;
position:absolute;
background-color:#ddd;
}
.crosshair-vert {
width:1px;
height:100%;
left:50%;
}
.crosshair-horz {
height:1px;
width:100%;
top:50%;
}
.gimbal-label {
display:block;
position:absolute;
text-align:center;
}
.gimbal-label-horz {
top:calc(100% + 0.5em);
width:100%;
}
.gimbal-label-vert {
transform:rotate(-90deg);
/*transform-origin:0% 100%;*/
top:calc(50% - 0.5em);
width:100%;
left:calc(-50% - 1em);
}
.control-stick {
background-color:rgba(255,50,50,1.0);
width:20px;
height:20px;
margin-left:-10px;
margin-top:-10px;
display:block;
border-radius:100%;
position:absolute;
cursor:pointer;
}
.control-slider {
margin:20px;
}
.tooltip {
position: absolute;
left: calc(100% + 24px);
top: 0;
}
.control-slider .slider {
margin-left:50px;
margin-right:50px;
}
.slider-label {
position:absolute;
text-align:right;
width:40px;
left:-65px;
}
.button-enable {
padding:0.5em;
font-size:110%;
margin-left:auto;
margin-right:auto;
display:block;
}

71
tabs/receiver_msp.html Normal file
View File

@ -0,0 +1,71 @@
<html>
<head>
<script type="text/javascript" src="/js/libraries/jquery-2.1.3.min.js"></script>
<script type="text/javascript" src="/js/libraries/jquery-ui-1.11.2.min.js"></script>
<script type="text/javascript" src="/js/libraries/jquery.nouislider.all.min.js"></script>
<script type="text/javascript" src="receiver_msp.js"></script>
<link type="text/css" rel="stylesheet" href="/js/libraries/jquery.nouislider.min.css">
<link type="text/css" rel="stylesheet" href="/js/libraries/jquery.nouislider.pips.min.css">
<link type="text/css" rel="stylesheet" href="receiver_msp.css" media="all" />
</head>
<body>
<div class="control-gimbals">
<div class="control-gimbal left">
<span class="gimbal-label gimbal-label-vert"></span>
<span class="gimbal-label gimbal-label-horz"></span>
<span class="crosshair crosshair-vert"></span>
<span class="crosshair crosshair-horz"></span>
<div class="control-stick">
</div>
</div>
<div class="control-gimbal right">
<span class="gimbal-label gimbal-label-vert"></span>
<span class="gimbal-label gimbal-label-horz"></span>
<span class="crosshair crosshair-vert"></span>
<span class="crosshair crosshair-horz"></span>
<div class="control-stick">
</div>
</div>
</div>
<div class="control-sliders">
<div class="control-slider">
<div class="slider">
<span class="slider-label"></span>
</div>
</div>
<div class="control-slider">
<div class="slider">
<span class="slider-label"></span>
</div>
</div>
<div class="control-slider">
<div class="slider">
<span class="slider-label"></span>
</div>
</div>
<div class="control-slider">
<div class="slider">
<span class="slider-label"></span>
</div>
</div>
</div>
<div class="warning">
<p>
These sticks allow Cleanflight to be armed and tested without a transmitter or receiver being
present. However, <strong>this feature is not intended for flight and propellers must not be attached.</strong>
</p>
<p>
This feature does not guarantee reliable control of your craft. <strong>Serious injury is likely to
result if propellers are left on.</strong>
</p>
<button class="button-enable" type="button">Enable controls</button>
</div>
</body>
</html>

182
tabs/receiver_msp.js Normal file
View File

@ -0,0 +1,182 @@
"use strict";
var
CHANNEL_MIN_VALUE = 1000,
CHANNEL_MID_VALUE = 1500,
CHANNEL_MAX_VALUE = 2000,
// What's the index of each channel in the MSP channel list?
channelMSPIndexes = {
roll: 0,
pitch: 1,
yaw: 2,
throttle: 3,
aux1: 4,
aux2: 5,
aux3: 6,
aux4: 7,
},
// Set reasonable initial stick positions (Mode 2)
stickValues = {
throttle: CHANNEL_MIN_VALUE,
pitch: CHANNEL_MID_VALUE,
roll: CHANNEL_MID_VALUE,
yaw: CHANNEL_MID_VALUE,
aux1: CHANNEL_MIN_VALUE,
aux2: CHANNEL_MIN_VALUE,
aux3: CHANNEL_MIN_VALUE,
aux4: CHANNEL_MIN_VALUE
},
// First the vertical axis, then the horizontal:
gimbals = [
["throttle", "yaw"],
["pitch", "roll"],
],
gimbalElems,
sliderElems,
enableTX = false;
function transmitChannels() {
var
channelValues = [0, 0, 0, 0, 0, 0, 0, 0];
if (!enableTX) {
return;
}
for (var stickName in stickValues) {
channelValues[channelMSPIndexes[stickName]] = stickValues[stickName];
}
// Callback given to us by the window creator so we can have it send data over MSP for us:
window.setRawRx(channelValues);
}
function stickPortionToChannelValue(portion) {
portion = Math.min(Math.max(portion, 0.0), 1.0);
return Math.round(portion * (CHANNEL_MAX_VALUE - CHANNEL_MIN_VALUE) + CHANNEL_MIN_VALUE);
}
function channelValueToStickPortion(channel) {
return (channel - CHANNEL_MIN_VALUE) / (CHANNEL_MAX_VALUE - CHANNEL_MIN_VALUE);
}
function updateControlPositions() {
for (var stickName in stickValues) {
var
stickValue = stickValues[stickName];
// Look for the gimbal which corresponds to this stick name
for (var gimbalIndex in gimbals) {
var
gimbal = gimbals[gimbalIndex],
gimbalElem = gimbalElems.get(gimbalIndex),
gimbalSize = $(gimbalElem).width(),
stickElem = $(".control-stick", gimbalElem);
if (gimbal[0] == stickName) {
stickElem.css('top', (1.0 - channelValueToStickPortion(stickValue)) * gimbalSize + "px");
break;
} else if (gimbal[1] == stickName) {
stickElem.css('left', channelValueToStickPortion(stickValue) * gimbalSize + "px");
break;
}
}
}
}
function handleGimbalMouseDrag(e) {
var
gimbal = $(gimbalElems.get(e.data.gimbalIndex)),
gimbalOffset = gimbal.offset(),
gimbalSize = gimbal.width();
stickValues[gimbals[e.data.gimbalIndex][0]] = stickPortionToChannelValue(1.0 - (e.pageY - gimbalOffset.top) / gimbalSize);
stickValues[gimbals[e.data.gimbalIndex][1]] = stickPortionToChannelValue((e.pageX - gimbalOffset.left) / gimbalSize);
updateControlPositions();
}
function localizeAxisNames() {
for (var gimbalIndex in gimbals) {
var
gimbal = gimbalElems.get(gimbalIndex);
$(".gimbal-label-vert", gimbal).text(chrome.i18n.getMessage("controlAxis" + gimbals[gimbalIndex][0]));
$(".gimbal-label-horz", gimbal).text(chrome.i18n.getMessage("controlAxis" + gimbals[gimbalIndex][1]));
}
for (var sliderIndex = 0; sliderIndex < 4; sliderIndex++) {
$(".slider-label", sliderElems.get(sliderIndex)).text(chrome.i18n.getMessage("controlAxisAux" + (sliderIndex + 1)));
}
}
$(document).ready(function() {
$(".button-enable").click(function() {
var
shrinkHeight = $(".warning").height();
$(".warning").slideUp("short", function() {
chrome.app.window.current().innerBounds.minHeight -= shrinkHeight;
chrome.app.window.current().innerBounds.height -= shrinkHeight;
chrome.app.window.current().innerBounds.maxHeight -= shrinkHeight;
});
enableTX = true;
});
gimbalElems = $(".control-gimbal");
sliderElems = $(".control-slider");
gimbalElems.each(function(gimbalIndex) {
$(this).on('mousedown', {gimbalIndex: gimbalIndex}, function(e) {
if (e.which == 1) { // Only move sticks on left mouse button
handleGimbalMouseDrag(e);
$(window).on('mousemove', {gimbalIndex: gimbalIndex}, handleGimbalMouseDrag);
}
});
});
$(".slider", sliderElems).each(function(sliderIndex) {
var
initialValue = stickValues["aux" + (sliderIndex + 1)];
$(this)
.noUiSlider({
start: initialValue,
range: {
min: CHANNEL_MIN_VALUE,
max: CHANNEL_MAX_VALUE
}
}).on('slide change set', function(e, value) {
value = Math.round(parseFloat(value));
stickValues["aux" + (sliderIndex + 1)] = value;
$(".tooltip", this).text(value);
});
$(this).append('<div class="tooltip"></div>');
$(".tooltip", this).text(initialValue);
});
/*
* Mouseup handler needs to be bound to the window in order to receive mouseup if mouse leaves window.
*/
$(window).mouseup(function(e) {
$(this).off('mousemove', handleGimbalMouseDrag);
});
localizeAxisNames();
updateControlPositions();
setInterval(transmitChannels, 100);
});