1
0
Fork 0
mirror of https://github.com/betaflight/betaflight-configurator.git synced 2025-07-24 00:35:26 +03:00
betaflight-configurator/tabs/pid_tuning.js
Gary Keeble 3a77c7fade Rates Graph Update
Prevent the balloon labels from overlapping,
Add dynamic stick position indicators to rates graph
Add minimum font size to text (for low DPI monitors).
Extend the length of the pointer on the balloons
Multi-Layer Canvas'
Add window resize triggers
Add current stick position values
Remove 360deg axes lines and code tidy
2016-09-17 08:03:00 +01:00

1211 lines
50 KiB
JavaScript
Executable file

'use strict';
TABS.pid_tuning = {
RATE_PROFILE_MASK: 128,
showAllPids: false,
updating: true,
dirty: false,
currentProfile: null,
currentRateProfile: null
};
TABS.pid_tuning.initialize = function (callback) {
var self = this;
if (GUI.active_tab !== 'pid_tuning') {
GUI.active_tab = 'pid_tuning';
}
// requesting MSP_STATUS manually because it contains CONFIG.profile
MSP.promise(MSPCodes.MSP_STATUS).then(function() {
if (semver.gte(CONFIG.apiVersion, CONFIGURATOR.pidControllerChangeMinApiVersion)) {
return MSP.promise(MSPCodes.MSP_PID_CONTROLLER);
}
}).then(function() {
return MSP.promise(MSPCodes.MSP_PIDNAMES)
}).then(function() {
return MSP.promise(MSPCodes.MSP_PID);
}).then(function () {
if (semver.gte(CONFIG.flightControllerVersion, "2.9.0") && semver.lt(CONFIG.flightControllerVersion, "2.9.1")) {
return MSP.promise(MSPCodes.MSP_SPECIAL_PARAMETERS);
}
}).then(function() {
if (semver.gte(CONFIG.flightControllerVersion, "2.8.2")) {
return MSP.promise(MSPCodes.MSP_PID_ADVANCED);
}
}).then(function() {
return MSP.promise(MSPCodes.MSP_RC_TUNING);
}).then(function() {
return MSP.promise(MSPCodes.MSP_FILTER_CONFIG);
}).then(function() {
var promise = true;
if (CONFIG.flightControllerIdentifier === "BTFL" && semver.gte(CONFIG.flightControllerVersion, "2.8.0")) {
promise = MSP.promise(MSPCodes.MSP_BF_CONFIG);
}
return promise;
}).then(function() {
var promise = true;
if (semver.gte(CONFIG.apiVersion, "1.15.0")) {
promise = MSP.promise(MSPCodes.MSP_RC_DEADBAND);
}
return promise;
}).then(function() {
$('#content').load("./tabs/pid_tuning.html", process_html);
});
function pid_and_rc_to_form() {
self.setProfile();
if (semver.gte(CONFIG.flightControllerVersion, "3.0.0")) {
self.setRateProfile();
}
// Fill in the data from PIDs array
var i = 0;
$('.pid_tuning .ROLL input').each(function () {
switch (i) {
case 0:
$(this).val(PIDs[0][i++]);
break;
case 1:
$(this).val(PIDs[0][i++]);
break;
case 2:
$(this).val(PIDs[0][i++]);
break;
}
});
i = 0;
$('.pid_tuning .PITCH input').each(function () {
switch (i) {
case 0:
$(this).val(PIDs[1][i++]);
break;
case 1:
$(this).val(PIDs[1][i++]);
break;
case 2:
$(this).val(PIDs[1][i++]);
break;
}
});
i = 0;
$('.pid_tuning .YAW input').each(function () {
switch (i) {
case 0:
$(this).val(PIDs[2][i++]);
break;
case 1:
$(this).val(PIDs[2][i++]);
break;
}
});
$('.pid_tuning .YAW_JUMP_PREVENTION input').each(function () {
switch (i) {
case 2:
$(this).val(PIDs[2][i++]);
break;
}
});
i = 0;
$('.pid_tuning .ALT input').each(function () {
switch (i) {
case 0:
$(this).val(PIDs[3][i++]);
break;
case 1:
$(this).val(PIDs[3][i++]);
break;
case 2:
$(this).val(PIDs[3][i++]);
break;
}
});
i = 0;
$('.pid_tuning .Pos input').each(function () {
$(this).val(PIDs[4][i++]);
});
i = 0;
$('.pid_tuning .PosR input').each(function () {
switch (i) {
case 0:
$(this).val(PIDs[5][i++]);
break;
case 1:
$(this).val(PIDs[5][i++]);
break;
case 2:
$(this).val(PIDs[5][i++]);
break;
}
});
i = 0;
$('.pid_tuning .NavR input').each(function () {
switch (i) {
case 0:
$(this).val(PIDs[6][i++]);
break;
case 1:
$(this).val(PIDs[6][i++]);
break;
case 2:
$(this).val(PIDs[6][i++]);
break;
}
});
i = 0;
$('.pid_tuning .ANGLE input').each(function () {
switch (i) {
case 0:
$(this).val(PIDs[7][i++]);
break;
}
});
$('.pid_tuning .HORIZON input').each(function () {
switch (i) {
case 1:
$(this).val(PIDs[7][i++]);
break;
case 2:
$(this).val(PIDs[7][i++]);
break;
}
});
i = 0;
$('.pid_tuning .MAG input').each(function () {
$(this).val(PIDs[8][i++]);
});
i = 0;
$('.pid_tuning .Vario input').each(function () {
switch (i) {
case 0:
$(this).val(PIDs[9][i++]);
break;
case 1:
$(this).val(PIDs[9][i++]);
break;
case 2:
$(this).val(PIDs[9][i++]);
break;
}
});
// Fill in data from RC_tuning object
$('.pid_tuning input[name="rc_rate"]').val(RC_tuning.RC_RATE.toFixed(2));
$('.pid_tuning input[name="roll_pitch_rate"]').val(RC_tuning.roll_pitch_rate.toFixed(2));
$('.pid_tuning input[name="roll_rate"]').val(RC_tuning.roll_rate.toFixed(2));
$('.pid_tuning input[name="pitch_rate"]').val(RC_tuning.pitch_rate.toFixed(2));
$('.pid_tuning input[name="yaw_rate"]').val(RC_tuning.yaw_rate.toFixed(2));
$('.pid_tuning input[name="rc_expo"]').val(RC_tuning.RC_EXPO.toFixed(2));
$('.pid_tuning input[name="rc_yaw_expo"]').val(RC_tuning.RC_YAW_EXPO.toFixed(2));
$('.throttle input[name="mid"]').val(RC_tuning.throttle_MID.toFixed(2));
$('.throttle input[name="expo"]').val(RC_tuning.throttle_EXPO.toFixed(2));
$('.tpa input[name="tpa"]').val(RC_tuning.dynamic_THR_PID.toFixed(2));
$('.tpa input[name="tpa-breakpoint"]').val(RC_tuning.dynamic_THR_breakpoint);
if (semver.lt(CONFIG.apiVersion, "1.10.0")) {
$('.pid_tuning input[name="rc_yaw_expo"]').hide();
$('.pid_tuning input[name="rc_expo"]').attr("rowspan", "3");
}
if (semver.gte(CONFIG.flightControllerVersion, "2.8.1")) {
$('input[id="vbatpidcompensation"]').prop('checked', ADVANCED_TUNING.vbatPidCompensation !== 0);
}
if (semver.gte(CONFIG.flightControllerVersion, "2.8.2")) {
$('#pid-tuning .delta select').val(ADVANCED_TUNING.deltaMethod);
}
if (semver.gte(CONFIG.flightControllerVersion, '2.9.0')) {
$('.pid_tuning input[name="rc_rate_yaw"]').val(RC_tuning.rcYawRate.toFixed(2));
$('.pid_filter input[name="gyroLowpassFrequency"]').val(FILTER_CONFIG.gyro_soft_lpf_hz);
$('.pid_filter input[name="dtermLowpassFrequency"]').val(FILTER_CONFIG.dterm_lpf_hz);
$('.pid_filter input[name="yawLowpassFrequency"]').val(FILTER_CONFIG.yaw_lpf_hz);
} else {
$('.tab-pid_tuning .subtab-filter').hide();
$('.tab-pid_tuning .tab_container').hide();
$('.pid_tuning input[name="rc_rate_yaw"]').hide();
}
if (semver.gte(CONFIG.flightControllerVersion, "3.0.0")
|| semver.gte(CONFIG.flightControllerVersion, "2.8.0") && BF_CONFIG.features.isEnabled('SUPEREXPO_RATES')) {
$('#pid-tuning .rate').text(chrome.i18n.getMessage("pidTuningSuperRate"));
} else {
$('#pid-tuning .rate').text(chrome.i18n.getMessage("pidTuningRate"));
}
if (semver.gte(CONFIG.flightControllerVersion, '3.0.0')) {
$('.pid_filter input[name="gyroNotchFrequency"]').val(FILTER_CONFIG.gyro_soft_notch_hz);
$('.pid_filter input[name="gyroNotchCutoff"]').val(FILTER_CONFIG.gyro_soft_notch_cutoff);
$('.pid_filter input[name="dTermNotchFrequency"]').val(FILTER_CONFIG.dterm_notch_hz);
$('.pid_filter input[name="dTermNotchCutoff"]').val(FILTER_CONFIG.dterm_notch_cutoff);
$('input[name="ptermSetpoint-number"]').val(ADVANCED_TUNING.ptermSetpointWeight / 100);
$('input[name="ptermSetpoint-range"]').val(ADVANCED_TUNING.ptermSetpointWeight / 100);
$('input[name="dtermSetpoint-number"]').val(ADVANCED_TUNING.dtermSetpointWeight / 100);
$('input[name="dtermSetpoint-range"]').val(ADVANCED_TUNING.dtermSetpointWeight / 100);
} else {
$('.pid_filter .newFilter').hide();
}
}
function form_to_pid_and_rc() {
// Fill in the data from PIDs array
// Catch all the changes and stuff the inside PIDs array
var i = 0;
$('table.pid_tuning tr.ROLL .pid_data input').each(function () {
PIDs[0][i++] = parseFloat($(this).val());
});
i = 0;
$('table.pid_tuning tr.PITCH .pid_data input').each(function () {
PIDs[1][i++] = parseFloat($(this).val());
});
i = 0;
$('table.pid_tuning tr.YAW .pid_data input').each(function () {
PIDs[2][i++] = parseFloat($(this).val());
});
$('table.pid_tuning tr.YAW_JUMP_PREVENTION .pid_data input').each(function () {
PIDs[2][i++] = parseFloat($(this).val());
});
i = 0;
$('table.pid_tuning tr.ALT input').each(function () {
PIDs[3][i++] = parseFloat($(this).val());
});
i = 0;
$('table.pid_tuning tr.Vario input').each(function () {
PIDs[9][i++] = parseFloat($(this).val());
});
i = 0;
$('table.pid_tuning tr.Pos input').each(function () {
PIDs[4][i++] = parseFloat($(this).val());
});
i = 0;
$('table.pid_tuning tr.PosR input').each(function () {
PIDs[5][i++] = parseFloat($(this).val());
});
i = 0;
$('table.pid_tuning tr.NavR input').each(function () {
PIDs[6][i++] = parseFloat($(this).val());
});
i = 0;
$('table.pid_tuning tr.ANGLE input').each(function () {
PIDs[7][i++] = parseFloat($(this).val());
});
$('table.pid_tuning tr.HORIZON input').each(function () {
PIDs[7][i++] = parseFloat($(this).val());
});
i = 0;
$('table.pid_tuning tr.MAG input').each(function () {
PIDs[8][i++] = parseFloat($(this).val());
});
// catch RC_tuning changes
RC_tuning.RC_RATE = parseFloat($('.pid_tuning input[name="rc_rate"]').val());
RC_tuning.roll_pitch_rate = parseFloat($('.pid_tuning input[name="roll_pitch_rate"]').val());
RC_tuning.roll_rate = parseFloat($('.pid_tuning input[name="roll_rate"]').val());
RC_tuning.pitch_rate = parseFloat($('.pid_tuning input[name="pitch_rate"]').val());
RC_tuning.yaw_rate = parseFloat($('.pid_tuning input[name="yaw_rate"]').val());
RC_tuning.RC_EXPO = parseFloat($('.pid_tuning input[name="rc_expo"]').val());
RC_tuning.RC_YAW_EXPO = parseFloat($('.pid_tuning input[name="rc_yaw_expo"]').val());
RC_tuning.rcYawRate = parseFloat($('.pid_tuning input[name="rc_rate_yaw"]').val());
RC_tuning.throttle_MID = parseFloat($('.throttle input[name="mid"]').val());
RC_tuning.throttle_EXPO = parseFloat($('.throttle input[name="expo"]').val())
RC_tuning.dynamic_THR_PID = parseFloat($('.tpa input[name="tpa"]').val());
RC_tuning.dynamic_THR_breakpoint = parseInt($('.tpa input[name="tpa-breakpoint"]').val());
FILTER_CONFIG.gyro_soft_lpf_hz = parseInt($('.pid_filter input[name="gyroLowpassFrequency"]').val());
FILTER_CONFIG.dterm_lpf_hz = parseInt($('.pid_filter input[name="dtermLowpassFrequency"]').val());
FILTER_CONFIG.yaw_lpf_hz = parseInt($('.pid_filter input[name="yawLowpassFrequency"]').val());
if (semver.gte(CONFIG.flightControllerVersion, "2.8.0") && !semver.gte(CONFIG.flightControllerVersion, "3.0.0")) {
BF_CONFIG.features.updateData($('input[name="SUPEREXPO_RATES"]'));
}
if (semver.gte(CONFIG.flightControllerVersion, "2.8.1")) {
ADVANCED_TUNING.vbatPidCompensation = $('input[id="vbatpidcompensation"]').is(':checked') ? 1 : 0;
}
if (semver.gte(CONFIG.flightControllerVersion, "2.8.2")) {
ADVANCED_TUNING.deltaMethod = $('#pid-tuning .delta select').val();
}
if (semver.gte(CONFIG.flightControllerVersion, '3.0.0')) {
ADVANCED_TUNING.ptermSetpointWeight = parseInt($('input[name="ptermSetpoint-number"]').val() * 100);
ADVANCED_TUNING.dtermSetpointWeight = parseInt($('input[name="dtermSetpoint-number"]').val() * 100);
FILTER_CONFIG.gyro_soft_notch_hz = parseInt($('.pid_filter input[name="gyroNotchFrequency"]').val());
FILTER_CONFIG.gyro_soft_notch_cutoff = parseInt($('.pid_filter input[name="gyroNotchCutoff"]').val());
FILTER_CONFIG.dterm_notch_hz = parseInt($('.pid_filter input[name="dTermNotchFrequency"]').val());
FILTER_CONFIG.dterm_notch_cutoff = parseInt($('.pid_filter input[name="dTermNotchCutoff"]').val());
}
}
function showAllPids() {
$('.tab-pid_tuning .pid_tuning').show();
}
function hideUnusedPids() {
$('.tab-pid_tuning .pid_tuning').hide();
$('#pid_main').show();
if (have_sensor(CONFIG.activeSensors, 'acc')) {
$('#pid_accel').show();
$('#pid_level').show();
}
var showTitle = false;
if (have_sensor(CONFIG.activeSensors, 'baro') ||
have_sensor(CONFIG.activeSensors, 'sonar')) {
$('#pid_baro').show();
showTitle = true;
}
if (have_sensor(CONFIG.activeSensors, 'mag')) {
$('#pid_mag').show();
showTitle = true;
}
if (BF_CONFIG.features.isEnabled('GPS')) {
$('#pid_gps').show();
showTitle = true;
}
if (showTitle) {
$('#pid_optional').show();
}
}
function drawAxes(curveContext, width, height) {
curveContext.strokeStyle = '#000000';
curveContext.lineWidth = 4;
// Horizontal
curveContext.beginPath();
curveContext.moveTo(0, height / 2);
curveContext.lineTo(width, height / 2);
curveContext.stroke();
// Vertical
curveContext.beginPath();
curveContext.moveTo(width / 2, 0);
curveContext.lineTo(width / 2, height);
curveContext.stroke();
}
function plotStickPosition(curveContext, rcData, rate, rcRate, rcExpo, superExpoActive, deadband, maxAngularVel, stickColor) {
const DEFAULT_SIZE = 60; // canvas units, relative size of the stick indicator (larger value is smaller indicator)
const rateScaling = (curveContext.canvas.height / 2) / maxAngularVel;
var currentValue = self.rateCurve.rcCommandRawToDegreesPerSecond(rcData, rate, rcRate, rcExpo, superExpoActive, deadband);
if(rcData!=undefined) {
curveContext.save();
curveContext.fillStyle = stickColor || '#000000';
curveContext.translate(curveContext.canvas.width/2, curveContext.canvas.height/2);
curveContext.beginPath();
curveContext.arc(rcData-1500, -rateScaling * currentValue, curveContext.canvas.height / DEFAULT_SIZE, 0, 2 * Math.PI);
curveContext.fill();
curveContext.restore();
}
return currentValue.toFixed(0); // The calculated value in deg/s is returned from the function call for further processing.
}
function drawAxisLabel(curveContext, axisLabel, x, y, align, color) {
curveContext.fillStyle = color || '#000000' ;
curveContext.textAlign = align || 'center';
curveContext.fillText(axisLabel, x, y);
}
function drawBalloonLabel(curveContext, axisLabel, x, y, align, colors, dirty) {
/**
* curveContext is the canvas to draw on
* axisLabel is the string to display in the center of the balloon
* x, y are the coordinates of the point of the balloon
* align is whether the balloon appears to the left (align 'right') or right (align left) of the x,y coordinates
* colors is an object defining color, border and text are the fill color, border color and text color of the balloon
*/
const DEFAULT_OFFSET = 125; // in canvas scale; this is the horizontal length of the pointer
const DEFAULT_RADIUS = 10; // in canvas scale, this is the radius around the balloon
const DEFAULT_MARGIN = 5; // in canvas scale, this is the margin around the balloon when it overlaps
const fontSize = parseInt(curveContext.font);
// calculate the width and height required for the balloon
const width = (curveContext.measureText(axisLabel).width * 1.2);
const height = fontSize * 1.5; // the balloon is bigger than the text height
const pointerY = y; // always point to the required Y coordinate, even if we move the balloon itself to keep it on the canvas
// setup balloon background
curveContext.fillStyle = colors.color || '#ffffff' ;
curveContext.strokeStyle = colors.border || '#000000' ;
// correct x position to account for window scaling
x *= curveContext.canvas.clientWidth/curveContext.canvas.clientHeight;
// adjust the coordinates for determine where the balloon background should be drawn
x += ((align=='right')?-(width + DEFAULT_OFFSET):0) + ((align=='left')?DEFAULT_OFFSET:0);
y -= (height/2); if(y<0) y=0; else if(y>curveContext.height) y=curveContext.height; // prevent balloon from going out of canvas
// check that the balloon does not already overlap
for(var i=0; i<dirty.length; i++) {
if((x>=dirty[i].left && x<=dirty[i].right) || (x+width>=dirty[i].left && x+width<=dirty[i].right)) { // does it overlap horizontally
if((y>=dirty[i].top && y<=dirty[i].bottom) || (y+height>=dirty[i].top && y+height<=dirty[i].bottom )) { // this overlaps another balloon
// snap above or snap below
if(y<=(dirty[i].bottom - dirty[i].top) / 2 && (dirty[i].top - height) > 0) {
y = dirty[i].top - height;
} else { // snap down
y = dirty[i].bottom;
}
}
}
}
// Add the draw area to the dirty array
dirty.push({left:x, right:x+width, top:y-DEFAULT_MARGIN, bottom:y+height+DEFAULT_MARGIN});
var pointerLength = (height - 2 * DEFAULT_RADIUS ) / 6;
curveContext.beginPath();
curveContext.moveTo(x + DEFAULT_RADIUS, y);
curveContext.lineTo(x + width - DEFAULT_RADIUS, y);
curveContext.quadraticCurveTo(x + width, y, x + width, y + DEFAULT_RADIUS);
if(align=='right') { // point is to the right
curveContext.lineTo(x + width, y + DEFAULT_RADIUS + pointerLength);
curveContext.lineTo(x + width + DEFAULT_OFFSET, pointerY); // point
curveContext.lineTo(x + width, y + height - DEFAULT_RADIUS - pointerLength);
}
curveContext.lineTo(x + width, y + height - DEFAULT_RADIUS);
curveContext.quadraticCurveTo(x + width, y + height, x + width - DEFAULT_RADIUS, y + height);
curveContext.lineTo(x + DEFAULT_RADIUS, y + height);
curveContext.quadraticCurveTo(x, y + height, x, y + height - DEFAULT_RADIUS);
if(align=='left') { // point is to the left
curveContext.lineTo(x, y + height - DEFAULT_RADIUS - pointerLength);
curveContext.lineTo(x - DEFAULT_OFFSET, pointerY); // point
curveContext.lineTo(x, y + DEFAULT_RADIUS - pointerLength);
}
curveContext.lineTo(x, y + DEFAULT_RADIUS);
curveContext.quadraticCurveTo(x, y, x + DEFAULT_RADIUS, y);
curveContext.closePath();
// fill in the balloon background
curveContext.fill();
curveContext.stroke();
// and add the label
drawAxisLabel(curveContext, axisLabel, x + (width/2), y + (height + fontSize)/2 - 4, 'center', colors.text);
}
function checkInput(element) {
var value = parseFloat(element.val());
if (value < parseFloat(element.prop('min'))
|| value > parseFloat(element.prop('max'))) {
value = undefined;
}
return value;
}
var useLegacyCurve = false;
if (!semver.gte(CONFIG.flightControllerVersion, "2.8.0")) {
useLegacyCurve = true;
}
self.rateCurve = new RateCurve(useLegacyCurve);
function printMaxAngularVel(rate, rcRate, rcExpo, useSuperExpo, deadband, maxAngularVelElement) {
var maxAngularVel = self.rateCurve.getMaxAngularVel(rate, rcRate, rcExpo, useSuperExpo, deadband).toFixed(0);
maxAngularVelElement.text(maxAngularVel);
return maxAngularVel;
}
function drawCurve(rate, rcRate, rcExpo, useSuperExpo, maxAngularVel, deadband, colour, yOffset, context) {
context.save();
context.strokeStyle = colour;
context.translate(0, yOffset);
self.rateCurve.draw(rate, rcRate, rcExpo, useSuperExpo, maxAngularVel, deadband, context);
context.restore();
}
function process_html() {
if (semver.gte(CONFIG.flightControllerVersion, "2.8.0") && !semver.gte(CONFIG.flightControllerVersion, "3.0.0")) {
BF_CONFIG.features.generateElements($('.tab-pid_tuning .features'));
} else {
$('.tab-pid_tuning .pidTuningFeatures').hide();
}
// translate to user-selected language
localize();
// Local cache of current rates
self.currentRates = {
roll_rate: RC_tuning.roll_rate,
pitch_rate: RC_tuning.pitch_rate,
yaw_rate: RC_tuning.yaw_rate,
rc_rate: RC_tuning.RC_RATE,
rc_rate_yaw: RC_tuning.rcYawRate,
rc_expo: RC_tuning.RC_EXPO,
rc_yaw_expo: RC_tuning.RC_YAW_EXPO,
superexpo: BF_CONFIG.features.isEnabled('SUPEREXPO_RATES'),
deadband: RC_deadband.deadband,
yawDeadband: RC_deadband.yaw_deadband
};
if (semver.lt(CONFIG.apiVersion, "1.7.0")) {
self.currentRates.roll_rate = RC_tuning.roll_pitch_rate;
self.currentRates.pitch_rate = RC_tuning.roll_pitch_rate;
}
if (semver.lt(CONFIG.flightControllerVersion, "2.8.1")) {
self.currentRates.rc_rate_yaw = self.currentRates.rc_rate;
}
if (semver.gte(CONFIG.flightControllerVersion, "3.0.0")) {
self.currentRates.superexpo = true;
}
$('.tab-pid_tuning .tab_container .pid').on('click', function () {
$('.tab-pid_tuning .subtab-pid').show();
$('.tab-pid_tuning .subtab-filter').hide();
$('.tab-pid_tuning .tab_container td').removeClass('active');
$(this).addClass('active');
});
$('.tab-pid_tuning .tab_container .filter').on('click', function () {
$('.tab-pid_tuning .subtab-filter').show();
$('.tab-pid_tuning .subtab-pid').hide();
$('.tab-pid_tuning .tab_container td').removeClass('active');
$(this).addClass('active');
});
var showAllButton = $('#showAllPids');
function updatePidDisplay() {
if (!self.showAllPids) {
hideUnusedPids();
showAllButton.text(chrome.i18n.getMessage("pidTuningShowAllPids"));
} else {
showAllPids();
showAllButton.text(chrome.i18n.getMessage("pidTuningHideUnusedPids"));
}
}
updatePidDisplay();
showAllButton.on('click', function(){
self.showAllPids = !self.showAllPids;
updatePidDisplay();
});
$('#resetProfile').on('click', function(){
self.updating = true;
MSP.promise(MSPCodes.MSP_SET_RESET_CURR_PID).then(function () {
self.refresh(function () {
self.updating = false;
GUI.log(chrome.i18n.getMessage('pidTuningProfileReset'));
});
});
});
$('.tab-pid_tuning select[name="profile"]').change(function () {
self.currentProfile = parseInt($(this).val());
self.updating = true;
MSP.promise(MSPCodes.MSP_SELECT_SETTING, [self.currentProfile]).then(function () {
self.refresh(function () {
self.updating = false;
GUI.log(chrome.i18n.getMessage('pidTuningLoadedProfile', [self.currentProfile + 1]));
});
});
});
if (semver.gte(CONFIG.flightControllerVersion, "3.0.0")) {
$('.tab-pid_tuning select[name="rate_profile"]').change(function () {
self.currentRateProfile = parseInt($(this).val());
self.updating = true;
MSP.promise(MSPCodes.MSP_SELECT_SETTING, [self.currentRateProfile + self.RATE_PROFILE_MASK]).then(function () {
self.refresh(function () {
self.updating = false;
GUI.log(chrome.i18n.getMessage('pidTuningLoadedRateProfile', [self.currentRateProfile + 1]));
});
});
});
var ptermNumberElement = $('input[name="ptermSetpoint-number"]');
var ptermRangeElement = $('input[name="ptermSetpoint-range"]');
ptermNumberElement.change(function () {
ptermRangeElement.val($(this).val());
});
ptermRangeElement.change(function () {
ptermNumberElement.val($(this).val());
});
var dtermNumberElement = $('input[name="dtermSetpoint-number"]');
var dtermRangeElement = $('input[name="dtermSetpoint-range"]');
dtermNumberElement.change(function () {
dtermRangeElement.val($(this).val());
});
dtermRangeElement.change(function () {
dtermNumberElement.val($(this).val());
});
} else {
$('.tab-pid_tuning .rate_profile').hide();
$('#pid-tuning .ptermSetpoint').hide();
$('#pid-tuning .dtermSetpoint').hide();
}
if (!semver.gte(CONFIG.flightControllerVersion, "2.8.2")) {
$('#pid-tuning .delta').hide();
$('.tab-pid_tuning .note').hide();
}
$('.pid_tuning tr').each(function(){
for(i = 0; i < PID_names.length; i++) {
if($(this).hasClass(PID_names[i])) {
$(this).find('td:first').text(PID_names[i]);
}
}
});
pid_and_rc_to_form();
var pidController_e = $('select[name="controller"]');
var pidControllerList;
if (semver.lt(CONFIG.apiVersion, "1.14.0")) {
pidControllerList = [
{ name: "MultiWii (Old)"},
{ name: "MultiWii (rewrite)"},
{ name: "LuxFloat"},
{ name: "MultiWii (2.3 - latest)"},
{ name: "MultiWii (2.3 - hybrid)"},
{ name: "Harakiri"}
]
} else if (semver.gte(CONFIG.flightControllerVersion, "3.0.0")) {
pidControllerList = [
{ name: "Legacy"},
{ name: "Betaflight"},
]
} else {
pidControllerList = [
{ name: ""},
{ name: "Integer"},
{ name: "Float"},
]
}
for (var i = 0; i < pidControllerList.length; i++) {
pidController_e.append('<option value="' + (i) + '">' + pidControllerList[i].name + '</option>');
}
if (semver.gte(CONFIG.apiVersion, CONFIGURATOR.pidControllerChangeMinApiVersion)) {
pidController_e.val(PID.controller);
self.updatePidControllerParameters();
} else {
GUI.log(chrome.i18n.getMessage('pidTuningUpgradeFirmwareToChangePidController', [CONFIG.apiVersion, CONFIGURATOR.pidControllerChangeMinApiVersion]));
pidController_e.empty();
pidController_e.append('<option value="">Unknown</option>');
pidController_e.prop('disabled', true);
}
if (semver.lt(CONFIG.apiVersion, "1.7.0")) {
$('.tpa .tpa-breakpoint').hide();
$('.pid_tuning .roll_rate').hide();
$('.pid_tuning .pitch_rate').hide();
} else {
$('.pid_tuning .roll_pitch_rate').hide();
}
if (useLegacyCurve) {
$('.new_rates').hide();
}
// Getting the DOM elements for curve display
var rcCurveElement = $('.rate_curve canvas').get(0),
rcStickElement = $('.rate_curve canvas').get(1),
curveContext = rcCurveElement.getContext("2d"),
stickContext = rcStickElement.getContext("2d"),
maxAngularVelRollElement = $('.pid_tuning .maxAngularVelRoll'),
maxAngularVelPitchElement = $('.pid_tuning .maxAngularVelPitch'),
maxAngularVelYawElement = $('.pid_tuning .maxAngularVelYaw'),
updateNeeded = true,
maxAngularVel;
rcCurveElement.width = 1000;
rcCurveElement.height = 1000;
rcStickElement.width = 1000;
rcStickElement.height = 1000;
self.updateRatesLabels = function() {
if (!useLegacyCurve && maxAngularVel) {
stickContext.save();
const BALLOON_COLORS = {
roll : {color: 'rgba(255,128,128,0.4)', border: 'rgba(255,128,128,0.6)', text: '#000000'},
pitch : {color: 'rgba(128,255,128,0.4)', border: 'rgba(128,255,128,0.6)', text: '#000000'},
yaw : {color: 'rgba(128,128,255,0.4)', border: 'rgba(128,128,255,0.6)', text: '#000000'}
};
var maxAngularVelRoll = maxAngularVelRollElement.text() + ' deg/s',
maxAngularVelPitch = maxAngularVelPitchElement.text() + ' deg/s',
maxAngularVelYaw = maxAngularVelYawElement.text() + ' deg/s',
currentValues = [],
balloonsDirty = [],
curveHeight = rcStickElement.height,
curveWidth = rcStickElement.width,
windowScale = (400 / stickContext.canvas.clientHeight),
rateScale = (curveHeight / 2) / maxAngularVel,
lineScale = stickContext.canvas.width / stickContext.canvas.clientWidth;
stickContext.clearRect(0, 0, curveWidth, curveHeight);
// calculate the fontSize based upon window scaling
if(windowScale <= 1) {
stickContext.font = "24pt Verdana, Arial, sans-serif";
} else {
stickContext.font = (24 * windowScale) + "pt Verdana, Arial, sans-serif";
}
currentValues.push(plotStickPosition(stickContext, RC.channels[0], self.currentRates.roll_rate, self.currentRates.rc_rate, self.currentRates.rc_expo, self.currentRates.superexpo, self.currentRates.deadband, maxAngularVel, '#FF8080') + ' deg/s');
currentValues.push(plotStickPosition(stickContext, RC.channels[1], self.currentRates.roll_rate, self.currentRates.rc_rate, self.currentRates.rc_expo, self.currentRates.superexpo, self.currentRates.deadband, maxAngularVel, '#80FF80') + ' deg/s');
currentValues.push(plotStickPosition(stickContext, RC.channels[2], self.currentRates.yaw_rate, self.currentRates.rc_rate_yaw, self.currentRates.rc_yaw_expo, self.currentRates.superexpo, self.currentRates.yawDeadband, maxAngularVel, '#8080FF') + ' deg/s');
stickContext.lineWidth = 1 * lineScale;
// use a custom scale so that the text does not appear stretched
stickContext.scale(stickContext.canvas.clientHeight/stickContext.canvas.clientWidth,1);
// add the maximum range label
drawAxisLabel(stickContext, maxAngularVel.toFixed(0) + ' deg/s', curveContext.canvas.clientWidth/curveContext.canvas.clientHeight * ((curveWidth / 2) - 10), parseInt(stickContext.font)*1.2, 'right');
// and then the balloon labels.
balloonsDirty = []; // reset the dirty balloon draw area (for overlap detection)
// create an array of balloons to draw
var balloons = [
{value: parseInt(maxAngularVelRoll), balloon: function() {drawBalloonLabel(stickContext, maxAngularVelRoll, curveWidth, rateScale * (maxAngularVel - parseInt(maxAngularVelRoll)), 'right', BALLOON_COLORS.roll, balloonsDirty);}},
{value: parseInt(maxAngularVelPitch), balloon: function() {drawBalloonLabel(stickContext, maxAngularVelPitch, curveWidth, rateScale * (maxAngularVel - parseInt(maxAngularVelPitch)), 'right', BALLOON_COLORS.pitch, balloonsDirty);}},
{value: parseInt(maxAngularVelYaw), balloon: function() {drawBalloonLabel(stickContext, maxAngularVelYaw, curveWidth, rateScale * (maxAngularVel - parseInt(maxAngularVelYaw)), 'right', BALLOON_COLORS.yaw, balloonsDirty);}}
];
// and sort them in descending order so the largest value is at the top always
balloons.sort(function(a,b) {return (b.value - a.value)});
// add the current rc values
balloons.push(
{value: parseInt(currentValues[0]), balloon: function() {drawBalloonLabel(stickContext, currentValues[0], 10, 150, 'none', BALLOON_COLORS.roll, balloonsDirty);}},
{value: parseInt(currentValues[1]), balloon: function() {drawBalloonLabel(stickContext, currentValues[1], 10, 250, 'none', BALLOON_COLORS.pitch, balloonsDirty);}},
{value: parseInt(currentValues[2]), balloon: function() {drawBalloonLabel(stickContext, currentValues[2], 10, 350, 'none', BALLOON_COLORS.yaw, balloonsDirty);}}
);
// then display them on the chart
for(var i=0; i<balloons.length; i++) balloons[i].balloon();
stickContext.restore();
}
};
function updateRates (event) {
setTimeout(function () { // let global validation trigger and adjust the values first
if(event) { // if an event is passed, then use it
var targetElement = $(event.target),
targetValue = checkInput(targetElement);
if (self.currentRates.hasOwnProperty(targetElement.attr('name')) && targetValue !== undefined) {
self.currentRates[targetElement.attr('name')] = targetValue;
updateNeeded = true;
}
if (targetElement.attr('name') === 'rc_rate' && semver.lt(CONFIG.flightControllerVersion, "2.8.1")) {
self.currentRates.rc_rate_yaw = targetValue;
}
if (targetElement.attr('name') === 'roll_pitch_rate' && semver.lt(CONFIG.apiVersion, "1.7.0")) {
self.currentRates.roll_rate = targetValue;
self.currentRates.pitch_rate = targetValue;
updateNeeded = true;
}
if (targetElement.attr('name') === 'SUPEREXPO_RATES') {
self.currentRates.superexpo = targetElement.is(':checked');
updateNeeded = true;
}
} else { // no event was passed, just force a graph update
updateNeeded = true;
}
if (updateNeeded) {
var curveHeight = rcCurveElement.height;
var curveWidth = rcCurveElement.width;
var lineScale = stickContext.canvas.width / stickContext.canvas.clientWidth;
curveContext.clearRect(0, 0, curveWidth, curveHeight);
if (!useLegacyCurve) {
maxAngularVel = Math.max(
printMaxAngularVel(self.currentRates.roll_rate, self.currentRates.rc_rate, self.currentRates.rc_expo, self.currentRates.superexpo, self.currentRates.deadband, maxAngularVelRollElement),
printMaxAngularVel(self.currentRates.pitch_rate, self.currentRates.rc_rate, self.currentRates.rc_expo, self.currentRates.superexpo, self.currentRates.deadband, maxAngularVelPitchElement),
printMaxAngularVel(self.currentRates.yaw_rate, self.currentRates.rc_rate_yaw, self.currentRates.rc_yaw_expo, self.currentRates.superexpo, self.currentRates.yawDeadband, maxAngularVelYawElement));
// make maxAngularVel multiple of 200deg/s so that the auto-scale doesn't keep changing for small changes of the maximum curve
maxAngularVel = Math.ceil(maxAngularVel/200) * 200;
drawAxes(curveContext, curveWidth, curveHeight);
} else {
maxAngularVel = 0;
}
curveContext.lineWidth = 2 * lineScale;
drawCurve(self.currentRates.roll_rate, self.currentRates.rc_rate, self.currentRates.rc_expo, self.currentRates.superexpo, self.currentRates.deadband, maxAngularVel, '#ff0000', 0, curveContext);
drawCurve(self.currentRates.pitch_rate, self.currentRates.rc_rate, self.currentRates.rc_expo, self.currentRates.superexpo, self.currentRates.deadband, maxAngularVel, '#00ff00', -4, curveContext);
drawCurve(self.currentRates.yaw_rate, self.currentRates.rc_rate_yaw, self.currentRates.rc_yaw_expo, self.currentRates.superexpo, self.currentRates.yawDeadband, maxAngularVel, '#0000ff', 4, curveContext);
self.updateRatesLabels();
updateNeeded = false;
}
}, 0);
};
// UI Hooks
// curves
$('input.feature').on('input change', updateRates);
$('.pid_tuning').on('input change', updateRates).trigger('input');
$('.throttle input').on('input change', function () {
setTimeout(function () { // let global validation trigger and adjust the values first
var throttleMidE = $('.throttle input[name="mid"]'),
throttleExpoE = $('.throttle input[name="expo"]'),
mid = parseFloat(throttleMidE.val()),
expo = parseFloat(throttleExpoE.val()),
throttleCurve = $('.throttle .throttle_curve canvas').get(0),
context = throttleCurve.getContext("2d");
// local validation to deal with input event
if (mid >= parseFloat(throttleMidE.prop('min')) &&
mid <= parseFloat(throttleMidE.prop('max')) &&
expo >= parseFloat(throttleExpoE.prop('min')) &&
expo <= parseFloat(throttleExpoE.prop('max'))) {
// continue
} else {
return;
}
var canvasHeight = throttleCurve.height;
var canvasWidth = throttleCurve.width;
// math magic by englishman
var midx = canvasWidth * mid,
midxl = midx * 0.5,
midxr = (((canvasWidth - midx) * 0.5) + midx),
midy = canvasHeight - (midx * (canvasHeight / canvasWidth)),
midyl = canvasHeight - ((canvasHeight - midy) * 0.5 *(expo + 1)),
midyr = (midy / 2) * (expo + 1);
// draw
context.clearRect(0, 0, canvasWidth, canvasHeight);
context.beginPath();
context.moveTo(0, canvasHeight);
context.quadraticCurveTo(midxl, midyl, midx, midy);
context.moveTo(midx, midy);
context.quadraticCurveTo(midxr, midyr, canvasWidth, 0);
context.lineWidth = 2;
context.strokeStyle = '#ffbb00';
context.stroke();
}, 0);
}).trigger('input');
$('a.refresh').click(function () {
self.refresh(function () {
GUI.log(chrome.i18n.getMessage('pidTuningDataRefreshed'));
});
});
$('#pid-tuning').find('input').each(function (k, item) {
if ($(item).attr('class') !== "feature toggle"
&& $(item).attr('class') !== "nonProfile") {
$(item).change(function () {
self.setDirty(true);
});
}
});
if (semver.gte(CONFIG.flightControllerVersion, "2.8.2")) {
$('#pid-tuning .delta select').change(function() {
self.setDirty(true);
});
}
pidController_e.change(function () {
self.setDirty(true);
self.updatePidControllerParameters();
});
// update == save.
$('a.update').click(function () {
form_to_pid_and_rc();
self.updating = true;
Promise.resolve(true)
.then(function () {
var promise;
if (semver.gte(CONFIG.apiVersion, CONFIGURATOR.pidControllerChangeMinApiVersion)) {
PID.controller = pidController_e.val();
promise = MSP.promise(MSPCodes.MSP_SET_PID_CONTROLLER, mspHelper.crunch(MSPCodes.MSP_SET_PID_CONTROLLER));
}
return promise;
}).then(function () {
return MSP.promise(MSPCodes.MSP_SET_PID, mspHelper.crunch(MSPCodes.MSP_SET_PID));
}).then(function () {
if (semver.gte(CONFIG.flightControllerVersion, "2.9.0") && semver.lt(CONFIG.flightControllerVersion, "3.0.0")) {
return MSP.promise(MSPCodes.MSP_SET_SPECIAL_PARAMETERS, mspHelper.crunch(MSPCodes.MSP_SET_SPECIAL_PARAMETERS));
}
}).then(function () {
if (semver.gte(CONFIG.flightControllerVersion, "2.8.2")) {
return MSP.promise(MSPCodes.MSP_SET_PID_ADVANCED, mspHelper.crunch(MSPCodes.MSP_SET_PID_ADVANCED));
}
}).then(function () {
if (semver.gte(CONFIG.flightControllerVersion, "2.8.1")) {
return MSP.promise(MSPCodes.MSP_SET_FILTER_CONFIG, mspHelper.crunch(MSPCodes.MSP_SET_FILTER_CONFIG));
}
}).then(function () {
return MSP.promise(MSPCodes.MSP_SET_RC_TUNING, mspHelper.crunch(MSPCodes.MSP_SET_RC_TUNING));
}).then(function () {
if (semver.gte(CONFIG.flightControllerVersion, "2.8.0")) {
return MSP.promise(MSPCodes.MSP_SET_BF_CONFIG, mspHelper.crunch(MSPCodes.MSP_SET_BF_CONFIG));
}
}).then(function () {
return MSP.promise(MSPCodes.MSP_EEPROM_WRITE);
}).then(function () {
self.updating = false;
self.setDirty(false);
GUI.log(chrome.i18n.getMessage('pidTuningEepromSaved'));
});
});
// Setup model for rates preview
self.initRatesPreview();
self.renderModel();
self.updating = false;
// enable RC data pulling for rates preview
GUI.interval_add('receiver_pull', self.getRecieverData, true);
// status data pulled via separate timer with static speed
GUI.interval_add('status_pull', function status_pull() {
MSP.send_message(MSPCodes.MSP_STATUS);
}, 250, true);
GUI.content_ready(callback);
}
};
TABS.pid_tuning.getRecieverData = function () {
MSP.send_message(MSPCodes.MSP_RC, false, false);
};
TABS.pid_tuning.initRatesPreview = function () {
this.keepRendering = true;
this.model = new Model($('.rates_preview'), $('.rates_preview canvas'));
$(window).on('resize', $.proxy(this.model.resize, this.model));
$(window).on('resize', $.proxy(this.updateRatesLabels, this));
};
TABS.pid_tuning.renderModel = function () {
if (this.keepRendering) { requestAnimationFrame(this.renderModel.bind(this)); }
if (!this.clock) { this.clock = new THREE.Clock(); }
if (!this.oldRC) {this.oldRC = [RC.channels[0], RC.channels[1], RC.channels[2]];}
if (this.updateRequired==null) this.updateRequired = new Object(false);
if (RC.channels[0] && RC.channels[1] && RC.channels[2]) {
var delta = this.clock.getDelta();
var roll = delta * this.rateCurve.rcCommandRawToDegreesPerSecond(RC.channels[0], this.currentRates.roll_rate, this.currentRates.rc_rate, this.currentRates.rc_expo, this.currentRates.superexpo, this.currentRates.deadband),
pitch = delta * this.rateCurve.rcCommandRawToDegreesPerSecond(RC.channels[1], this.currentRates.pitch_rate, this.currentRates.rc_rate, this.currentRates.rc_expo, this.currentRates.superexpo, this.currentRates.deadband),
yaw = delta * this.rateCurve.rcCommandRawToDegreesPerSecond(RC.channels[2], this.currentRates.yaw_rate, this.currentRates.rc_rate_yaw, this.currentRates.rc_yaw_expo, this.currentRates.superexpo, this.currentRates.yawDeadband);
this.model.rotateBy(-degToRad(pitch), -degToRad(yaw), -degToRad(roll));
this.updateRequired = false;
for(var i=0; i<this.oldRC.length; i++) {
if(this.oldRC[i] != RC.channels[i]) {
this.oldRC[i] = RC.channels[i];
this.updateRequired = true;
}
}
if(this.updateRequired) { //TODO : find a way to trigger on screen resize and change update rate
this.updateRequired = false;
this.updateRatesLabels();
} // trigger a rate graph update if the RC value has changed
}
};
TABS.pid_tuning.cleanup = function (callback) {
var self = this;
if (self.model) {
$(window).off('resize', $.proxy(self.model.resize, self.model));
}
$(window).off('resize', $.proxy(this.updateRatesLabels, this));
self.keepRendering = false;
if (callback) callback();
};
TABS.pid_tuning.refresh = function (callback) {
var self = this;
GUI.tab_switch_cleanup(function () {
self.initialize();
self.setDirty(false);
if (callback) {
callback();
}
});
}
TABS.pid_tuning.setProfile = function () {
var self = this;
self.currentProfile = CONFIG.profile;
$('.tab-pid_tuning select[name="profile"]').val(self.currentProfile);
}
TABS.pid_tuning.setRateProfile = function () {
var self = this;
self.currentRateProfile = CONFIG.rateProfile;
$('.tab-pid_tuning select[name="rate_profile"]').val(self.currentRateProfile);
}
TABS.pid_tuning.setDirty = function (isDirty) {
var self = this;
self.dirty = isDirty;
$('.tab-pid_tuning select[name="profile"]').prop('disabled', isDirty);
if (semver.gte(CONFIG.flightControllerVersion, "3.0.0")) {
$('.tab-pid_tuning select[name="rate_profile"]').prop('disabled', isDirty);
}
}
TABS.pid_tuning.checkUpdateProfile = function (updateRateProfile) {
var self = this;
if (GUI.active_tab === 'pid_tuning') {
if (semver.gte(CONFIG.flightControllerVersion, "3.0.0")
&& CONFIG.numProfiles === 2) {
$('.tab-pid_tuning select[name="profile"] .profile3').hide();
}
if (!self.updating && !self.dirty) {
var changedProfile = false;
if (self.currentProfile !== CONFIG.profile) {
self.setProfile();
changedProfile = true;
}
var changedRateProfile = false;
if (semver.gte(CONFIG.flightControllerVersion, "3.0.0")
&& updateRateProfile
&& self.currentRateProfile !== CONFIG.rateProfile) {
self.setRateProfile();
changedRateProfile = true;
}
if (changedProfile || changedRateProfile) {
self.refresh(function () {
if (changedProfile) {
GUI.log(chrome.i18n.getMessage('pidTuningReceivedProfile', [CONFIG.profile + 1]));
}
if (changedRateProfile) {
GUI.log(chrome.i18n.getMessage('pidTuningReceivedRateProfile', [CONFIG.rateProfile + 1]));
}
});
}
}
}
}
TABS.pid_tuning.updatePidControllerParameters = function () {
if (semver.gte(CONFIG.flightControllerVersion, "3.0.0")) {
if ($('.tab-pid_tuning select[name="controller"]').val() === '0') {
$('.pid_tuning .YAW_JUMP_PREVENTION').show();
$('#pid-tuning .delta').show();
$('#pid-tuning .ptermSetpoint').hide();
$('#pid-tuning .dtermSetpoint').hide();
} else {
$('.pid_tuning .YAW_JUMP_PREVENTION').hide();
$('#pid-tuning .ptermSetpoint').show();
$('#pid-tuning .dtermSetpoint').show();
$('#pid-tuning .delta').hide();
}
}
}