diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 91e33056..741ea82a 100755 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -672,6 +672,9 @@ "receiverButtonRefresh": { "message": "Refresh" }, + "receiverButtonSticks": { + "message": "Control sticks" + }, "receiverDataRefreshed": { "message": "RC Tuning data refreshed" }, @@ -1146,5 +1149,41 @@ }, "ledStripEepromSaved": { "message": "EEPROM saved" + }, + "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" } -} +} \ No newline at end of file diff --git a/js/msp.js b/js/msp.js index 49676381..e2232272 100644 --- a/js/msp.js +++ b/js/msp.js @@ -1130,6 +1130,22 @@ MSP.crunch = function (code) { 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 * of the returned data to the given callback (or null for the data if an error occured). diff --git a/tabs/receiver.css b/tabs/receiver.css index fe1a3c4f..9c7c6903 100644 --- a/tabs/receiver.css +++ b/tabs/receiver.css @@ -299,6 +299,7 @@ position: absolute; bottom: 10px; } +.tab-receiver .sticks, .tab-receiver .update, .tab-receiver .refresh { display: block; @@ -317,6 +318,7 @@ border: 1px solid silver; background-color: #ececec; } +.tab-receiver .sticks, .tab-receiver .refresh { margin-right: 10px; } diff --git a/tabs/receiver.html b/tabs/receiver.html index f562050c..f20c8a98 100644 --- a/tabs/receiver.html +++ b/tabs/receiver.html @@ -86,5 +86,6 @@
+
diff --git a/tabs/receiver.js b/tabs/receiver.js index 0efb5f18..9e08e321 100644 --- a/tabs/receiver.js +++ b/tabs/receiver.js @@ -18,7 +18,12 @@ TABS.receiver.initialize = function (callback) { } 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() { @@ -52,7 +57,12 @@ TABS.receiver.initialize = function (callback) { }); // 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'), aux_index = 1; @@ -61,7 +71,7 @@ TABS.receiver.initialize = function (callback) { if (i < bar_names.length) { name = bar_names[i]; } else { - name = 'AUX ' + aux_index++; + name = chrome.i18n.getMessage("controlAxisAux" + (aux_index++)); } 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); }); + + $("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 () { var plot_update_rate = parseInt($(this).val(), 10); diff --git a/tabs/receiver_msp.css b/tabs/receiver_msp.css new file mode 100644 index 00000000..29ecf158 --- /dev/null +++ b/tabs/receiver_msp.css @@ -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; +} \ No newline at end of file diff --git a/tabs/receiver_msp.html b/tabs/receiver_msp.html new file mode 100644 index 00000000..fb1a5482 --- /dev/null +++ b/tabs/receiver_msp.html @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + +
+
+ + + + + +
+ +
+
+
+ + + + + +
+
+
+
+ +
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+

+ These sticks allow Cleanflight to be armed and tested without a transmitter or receiver being + present. However, this feature is not intended for flight and propellers must not be attached. +

+

+ This feature does not guarantee reliable control of your craft. Serious injury is likely to + result if propellers are left on. +

+ +
+ + \ No newline at end of file diff --git a/tabs/receiver_msp.js b/tabs/receiver_msp.js new file mode 100644 index 00000000..91fd9c24 --- /dev/null +++ b/tabs/receiver_msp.js @@ -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('
'); + + $(".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); +}); \ No newline at end of file