1
0
Fork 0
mirror of https://github.com/betaflight/betaflight-configurator.git synced 2025-07-19 06:15:13 +03:00
betaflight-configurator/src/js/tabs/pid_tuning.js
fgiudice98 70a8364e5c Add rates type
Add rates type selection with working graphs and max angular speed

Fix deadband

MSP changes and fix actual rates limit

Small style changes and fixes

Fixes

Fixed the code pointed out in the review
Various sonar fixes
Updated the values of actual rates setting
Reduced logo size
Added analytics to rates type

Minor fix + rounding by step value

Now the calculation of the angle rate takes into account the values rounded by the step value (especially visible with values that are in deg/s)

Various fixes

Angle rate calculation in different functions
Fix for value step validation
Sonar fixes (mostly variables)

Force refresh to prevent errors ...

... when changing rates type after saving to eeprom

Logos refinement + minor changes

Touch to trigger travis

Fix Raceflight rate
2020-03-10 12:51:00 +01:00

2549 lines
116 KiB
JavaScript

'use strict';
TABS.pid_tuning = {
RATE_PROFILE_MASK: 128,
showAllPids: false,
updating: true,
dirty: false,
currentProfile: null,
currentRateProfile: null,
currentRatesType: null,
RATES_TYPE: {
BETAFLIGHT: 0,
RACEFLIGHT: 1,
KISS: 2,
ACTUAL: 3,
QUICKRATES: 4,
},
SETPOINT_WEIGHT_RANGE_LOW: 2.55,
SETPOINT_WEIGHT_RANGE_HIGH: 20,
SETPOINT_WEIGHT_RANGE_LEGACY: 2.54,
activeSubtab: 'pid',
analyticsChanges: {},
};
TABS.pid_tuning.initialize = function (callback) {
var self = this;
if (GUI.active_tab !== 'pid_tuning') {
GUI.active_tab = 'pid_tuning';
self.activeSubtab = 'pid';
}
// Update filtering defaults based on API version
var FILTER_DEFAULT = FC.getFilterDefaults();
// 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.apiVersion, "1.16.0")) {
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() {
return MSP.promise(MSPCodes.MSP_RC_DEADBAND);
}).then(function() {
return MSP.promise(MSPCodes.MSP_MOTOR_CONFIG);
}).then(function() {
MSP.send_message(MSPCodes.MSP_MIXER_CONFIG, false, false, load_html);
});
function load_html() {
$('#content').load("./tabs/pid_tuning.html", process_html);
}
function pid_and_rc_to_form() {
self.setProfile();
if (semver.gte(CONFIG.apiVersion, "1.20.0")) {
self.setRateProfile();
}
// Fill in the data from PIDs array
// For each pid name
PID_names.forEach(function(elementPid, indexPid) {
// Look into the PID table to a row with the name of the pid
var searchRow = $('.pid_tuning .' + elementPid + ' input');
// Assign each value
searchRow.each(function (indexInput) {
if (PIDs[indexPid][indexInput] !== undefined) {
$(this).val(PIDs[indexPid][indexInput]);
}
});
});
// 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.apiVersion, "1.16.0")) {
$('input[id="vbatpidcompensation"]').prop('checked', ADVANCED_TUNING.vbatPidCompensation !== 0);
}
if (semver.gte(CONFIG.apiVersion, "1.16.0")) {
$('#pid-tuning .delta select').val(ADVANCED_TUNING.deltaMethod);
}
if (semver.gte(CONFIG.apiVersion, "1.16.0")) {
$('.pid_tuning input[name="rc_rate_yaw"]').val(RC_tuning.rcYawRate.toFixed(2));
$('.pid_filter input[name="gyroLowpassFrequency"]').val(FILTER_CONFIG.gyro_lowpass_hz);
$('.pid_filter input[name="dtermLowpassFrequency"]').val(FILTER_CONFIG.dterm_lowpass_hz);
$('.pid_filter input[name="yawLowpassFrequency"]').val(FILTER_CONFIG.yaw_lowpass_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.apiVersion, "1.20.0")
|| semver.gte(CONFIG.apiVersion, "1.16.0") && FEATURE_CONFIG.features.isEnabled('SUPEREXPO_RATES')) {
$('#pid-tuning .rate').text(i18n.getMessage("pidTuningSuperRate"));
} else {
$('#pid-tuning .rate').text(i18n.getMessage("pidTuningRate"));
}
if (semver.gte(CONFIG.apiVersion, "1.20.0")) {
$('.pid_filter input[name="gyroNotch1Frequency"]').val(FILTER_CONFIG.gyro_notch_hz);
$('.pid_filter input[name="gyroNotch1Cutoff"]').val(FILTER_CONFIG.gyro_notch_cutoff);
$('.pid_filter input[name="dTermNotchFrequency"]').val(FILTER_CONFIG.dterm_notch_hz);
$('.pid_filter input[name="dTermNotchCutoff"]').val(FILTER_CONFIG.dterm_notch_cutoff);
var dtermSetpointTransitionNumberElement = $('input[name="dtermSetpointTransition-number"]');
if (semver.gte(CONFIG.apiVersion, "1.38.0")) {
dtermSetpointTransitionNumberElement.attr('min', 0.00);
} else {
dtermSetpointTransitionNumberElement.attr('min', 0.01);
}
dtermSetpointTransitionNumberElement.val(ADVANCED_TUNING.dtermSetpointTransition / 100);
$('input[name="dtermSetpoint-number"]').val(ADVANCED_TUNING.dtermSetpointWeight / 100);
} else {
$('.pid_filter .newFilter').hide();
}
if (semver.gte(CONFIG.apiVersion, "1.21.0")) {
$('.pid_filter input[name="gyroNotch2Frequency"]').val(FILTER_CONFIG.gyro_notch2_hz);
$('.pid_filter input[name="gyroNotch2Cutoff"]').val(FILTER_CONFIG.gyro_notch2_cutoff);
} else {
$('.pid_filter .gyroNotch2').hide();
}
if (semver.gte(CONFIG.apiVersion, "1.24.0")) {
$('.pid_tuning input[name="angleLimit"]').val(ADVANCED_TUNING.levelAngleLimit);
$('.pid_tuning input[name="sensitivity"]').val(ADVANCED_TUNING.levelSensitivity);
} else {
$('.pid_sensitivity').hide();
}
if (semver.gte(CONFIG.apiVersion, "1.36.0")) {
$('.pid_filter select[name="dtermLowpassType"]').val(FILTER_CONFIG.dterm_lowpass_type);
$('.antigravity input[name="itermThrottleThreshold"]').val(ADVANCED_TUNING.itermThrottleThreshold);
$('.antigravity input[name="itermAcceleratorGain"]').val(ADVANCED_TUNING.itermAcceleratorGain / 1000);
if (FEATURE_CONFIG.features.isEnabled('ANTI_GRAVITY')) {
$('.antigravity').show();
} else {
$('.antigravity').hide();
}
var antiGravitySwitch = $('#antiGravitySwitch');
antiGravitySwitch.prop('checked', ADVANCED_TUNING.itermAcceleratorGain !== 1000);
antiGravitySwitch.change(function() {
var checked = $(this).is(':checked');
if (checked) {
$('.antigravity input[name="itermAcceleratorGain"]').val(Math.max(ADVANCED_TUNING.itermAcceleratorGain / 1000, 1.1));
$('.antigravity .suboption').show();
if (ADVANCED_TUNING.antiGravityMode == 0) {
$('.antigravity .antiGravityThres').hide();
}
if (semver.gte(CONFIG.apiVersion, "1.40.0")) {
$('.antigravity .antiGravityMode').show();
} else {
$('.antigravity .antiGravityMode').hide();
}
} else {
$('.antigravity select[id="antiGravityMode"]').val(0);
$('.antigravity input[name="itermAcceleratorGain"]').val(1);
$('.antigravity .suboption').hide();
}
});
antiGravitySwitch.change();
} else {
$('.dtermLowpassType').hide();
$('.antigravity').hide();
}
if (semver.gte(CONFIG.apiVersion, "1.37.0")) {
$('.pid_tuning input[name="rc_rate_pitch"]').val(RC_tuning.rcPitchRate.toFixed(2));
$('.pid_tuning input[name="rc_pitch_expo"]').val(RC_tuning.RC_PITCH_EXPO.toFixed(2));
}
if (semver.gte(CONFIG.apiVersion, "1.39.0")) {
$('.pid_filter input[name="gyroLowpass2Frequency"]').val(FILTER_CONFIG.gyro_lowpass2_hz);
$('.pid_filter select[name="gyroLowpassType"]').val(FILTER_CONFIG.gyro_lowpass_type);
$('.pid_filter select[name="gyroLowpass2Type"]').val(FILTER_CONFIG.gyro_lowpass2_type);
$('.pid_filter input[name="dtermLowpass2Frequency"]').val(FILTER_CONFIG.dterm_lowpass2_hz);
// We load it again because the limits are now bigger than in 1.16.0
$('.pid_filter input[name="gyroLowpassFrequency"]').attr("max","16000");
$('.pid_filter input[name="gyroLowpassFrequency"]').val(FILTER_CONFIG.gyro_lowpass_hz);
//removes 5th column which is Feedforward
$('#pid_main .pid_titlebar2 th').attr('colspan', 4);
} else {
$('.gyroLowpass2').hide();
$('.gyroLowpass2Type').hide();
$('.dtermLowpass2').hide();
$('#pid_main .pid_titlebar2 th').attr('colspan', 4);
}
if (semver.gte(CONFIG.apiVersion, "1.40.0")) {
// I Term Rotation
$('input[id="itermrotation"]').prop('checked', ADVANCED_TUNING.itermRotation !== 0);
// Smart Feed Forward
$('input[id="smartfeedforward"]').prop('checked', ADVANCED_TUNING.smartFeedforward !== 0);
// I Term Relax
var itermRelaxCheck = $('input[id="itermrelax"]');
itermRelaxCheck.prop('checked', ADVANCED_TUNING.itermRelax !== 0);
$('select[id="itermrelaxAxes"]').val(ADVANCED_TUNING.itermRelax > 0 ? ADVANCED_TUNING.itermRelax : 1);
$('select[id="itermrelaxType"]').val(ADVANCED_TUNING.itermRelaxType);
$('input[name="itermRelaxCutoff"]').val(ADVANCED_TUNING.itermRelaxCutoff);
if (semver.gte(CONFIG.apiVersion, "1.43.0")) {
$('.itermrelax input[name="itermRelaxCutoff"]').attr("max","50");
}
itermRelaxCheck.change(function() {
var checked = $(this).is(':checked');
if (checked) {
$('.itermrelax .suboption').show();
if (semver.gte(CONFIG.apiVersion, "1.42.0")) {
$('.itermRelaxCutoff').show();
} else {
$('.itermRelaxCutoff').hide();
}
} else {
$('.itermrelax .suboption').hide();
}
});
itermRelaxCheck.change();
// Absolute Control
var absoluteControlGainNumberElement = $('input[name="absoluteControlGain-number"]');
absoluteControlGainNumberElement.val(ADVANCED_TUNING.absoluteControlGain).trigger('input');
// Throttle Boost
var throttleBoostNumberElement = $('input[name="throttleBoost-number"]');
throttleBoostNumberElement.val(ADVANCED_TUNING.throttleBoost).trigger('input');
// Acro Trainer
var acroTrainerAngleLimitNumberElement = $('input[name="acroTrainerAngleLimit-number"]');
acroTrainerAngleLimitNumberElement.val(ADVANCED_TUNING.acroTrainerAngleLimit).trigger('input');
// Yaw D
$('.pid_tuning .YAW input[name="d"]').val(PIDs[2][2]); // PID Yaw D
// Feedforward
$('.pid_tuning .ROLL input[name="f"]').val(ADVANCED_TUNING.feedforwardRoll);
$('.pid_tuning .PITCH input[name="f"]').val(ADVANCED_TUNING.feedforwardPitch);
$('.pid_tuning .YAW input[name="f"]').val(ADVANCED_TUNING.feedforwardYaw);
$('#pid_main .pid_titlebar2 th').attr('colspan', 5);
var feedforwardTransitionNumberElement = $('input[name="feedforwardTransition-number"]');
feedforwardTransitionNumberElement.val(ADVANCED_TUNING.feedforwardTransition / 100);
// AntiGravity Mode
var antiGravityModeSelect = $('.antigravity select[id="antiGravityMode"]');
antiGravityModeSelect.change(function () {
var antiGravityModeValue = $('.antigravity select[id="antiGravityMode"]').val();
// Smooth removes threshold
if (antiGravityModeValue == 0) {
$('.antiGravityThres').hide();
} else {
$('.antiGravityThres').show();
}
});
antiGravityModeSelect.val(ADVANCED_TUNING.antiGravityMode).change();
} else {
$('.itermrotation').hide();
$('.smartfeedforward').hide();
$('.itermrelax').hide();
$('.absoluteControlGain').hide();
$('.throttleBoost').hide();
$('.acroTrainerAngleLimit').hide();
$('.pid_tuning .YAW input[name="d"]').hide();
// Feedforward column
$('#pid_main tr :nth-child(6)').hide();
$('#pid-tuning .feedforwardTransition').hide();
}
if (semver.gte(CONFIG.apiVersion, "1.41.0")) {
$('select[id="throttleLimitType"]').val(RC_tuning.throttleLimitType);
$('.throttle_limit input[name="throttleLimitPercent"]').val(RC_tuning.throttleLimitPercent);
$('.pid_filter select[name="dtermLowpass2Type"]').val(FILTER_CONFIG.dterm_lowpass2_type);
$('.pid_filter input[name="gyroLowpassDynMinFrequency"]').val(FILTER_CONFIG.gyro_lowpass_dyn_min_hz);
$('.pid_filter input[name="gyroLowpassDynMaxFrequency"]').val(FILTER_CONFIG.gyro_lowpass_dyn_max_hz);
$('.pid_filter select[name="gyroLowpassDynType"]').val(FILTER_CONFIG.gyro_lowpass_type);
$('.pid_filter input[name="dtermLowpassDynMinFrequency"]').val(FILTER_CONFIG.dterm_lowpass_dyn_min_hz);
$('.pid_filter input[name="dtermLowpassDynMaxFrequency"]').val(FILTER_CONFIG.dterm_lowpass_dyn_max_hz);
$('.pid_filter select[name="dtermLowpassDynType"]').val(FILTER_CONFIG.dterm_lowpass_type);
$('.pid_tuning input[name="dMinRoll"]').val(ADVANCED_TUNING.dMinRoll);
$('.pid_tuning input[name="dMinPitch"]').val(ADVANCED_TUNING.dMinPitch);
$('.pid_tuning input[name="dMinYaw"]').val(ADVANCED_TUNING.dMinYaw);
$('.dminGroup input[name="dMinGain"]').val(ADVANCED_TUNING.dMinGain);
$('.dminGroup input[name="dMinAdvance"]').val(ADVANCED_TUNING.dMinAdvance);
$('input[id="useIntegratedYaw"]').prop('checked', ADVANCED_TUNING.useIntegratedYaw !== 0);
//dmin column
$('#pid_main .pid_titlebar2 th').attr('colspan', 6);
} else {
$('.throttle_limit').hide();
$('.gyroLowpassDyn').hide();
$('.dtermLowpassDyn').hide();
$('.dtermLowpass2TypeGroup').hide();
$('.dminGroup').hide();
$('.dMinDisabledNote').hide();
//dmin column
$('#pid_main tr :nth-child(5)').hide();
$('.integratedYaw').hide();
}
if (semver.gte(CONFIG.apiVersion, "1.42.0")) {
$('.smartfeedforward').hide();
if (FEATURE_CONFIG.features.isEnabled('DYNAMIC_FILTER')) {
$('.dynamicNotch').show();
} else {
$('.dynamicNotch').hide();
}
$('.dynamicNotchRange').toggle(semver.lt(CONFIG.apiVersion, "1.43.0"));
$('.pid_filter select[name="dynamicNotchRange"]').val(FILTER_CONFIG.dyn_notch_range);
$('.pid_filter input[name="dynamicNotchWidthPercent"]').val(FILTER_CONFIG.dyn_notch_width_percent);
$('.pid_filter input[name="dynamicNotchQ"]').val(FILTER_CONFIG.dyn_notch_q);
$('.pid_filter input[name="dynamicNotchMinHz"]').val(FILTER_CONFIG.dyn_notch_min_hz);
if (semver.gte(CONFIG.apiVersion, "1.43.0")) {
$('.pid_filter input[name="dynamicNotchMinHz"]').attr("max","250");
$('.pid_filter input[name="dynamicNotchMaxHz"]').val(FILTER_CONFIG.dyn_notch_max_hz);
} else {
$('.dynamicNotchMaxHz').hide();
}
$('.rpmFilter').toggle(MOTOR_CONFIG.use_dshot_telemetry);
$('.pid_filter input[name="rpmFilterHarmonics"]').val(FILTER_CONFIG.gyro_rpm_notch_harmonics);
$('.pid_filter input[name="rpmFilterMinHz"]').val(FILTER_CONFIG.gyro_rpm_notch_min_hz);
$('.pid_filter #rpmFilterEnabled').change(function() {
let harmonics = $('.pid_filter input[name="rpmFilterHarmonics"]').val();
let checked = $(this).is(':checked') && harmonics != 0;
$('.pid_filter input[name="rpmFilterHarmonics"]').attr('disabled', !checked);
$('.pid_filter input[name="rpmFilterMinHz"]').attr('disabled', !checked);
if (harmonics == 0) {
$('.pid_filter input[name="rpmFilterHarmonics"]').val(FILTER_DEFAULT.gyro_rpm_notch_harmonics);
}
}).prop('checked', FILTER_CONFIG.gyro_rpm_notch_harmonics != 0).change();
} else {
$('.itermRelaxCutoff').hide();
$('.dynamicNotch').hide();
$('.rpmFilter').hide();
}
if (semver.gte(CONFIG.apiVersion, "1.43.0")) {
$('.pid_tuning input[name="motorLimit"]').val(ADVANCED_TUNING.motorOutputLimit);
$('.pid_tuning input[name="cellCount"]').val(ADVANCED_TUNING.autoProfileCellCount);
$('input[name="idleMinRpm-number"]').val(ADVANCED_TUNING.idleMinRpm);
} else {
$('.motorOutputLimit').hide();
$('.idleMinRpm').hide();
}
if (semver.gte(CONFIG.apiVersion, "1.43.0")) {
const ratesTypeListElement = $('select[id="ratesType"]'); // generates list
const ratesList = [
{name: "Betaflight"},
{name: "Raceflight"},
{name: "KISS"},
{name: "Actual"},
{name: "QuickRates"},
];
// add future rates types here with CONFIG.apiVersion check
for (let i = 0; i < ratesList.length; i++) {
ratesTypeListElement.append(`<option value="${i}">${ratesList[i].name}</option>`);
}
self.currentRatesType = RC_tuning.rates_type;
ratesTypeListElement.val(self.currentRatesType);
self.changeRatesType(self.currentRatesType); // update rate type code when updating the tab
} else {
$('.rates_type').hide();
}
$('input[id="useIntegratedYaw"]').change(function() {
var checked = $(this).is(':checked');
$('#pidTuningIntegratedYawCaution').toggle(checked);
}).change();
function adjustDMin(dElement, dMinElement) {
var dValue = parseInt(dElement.val());
var dMinValue = parseInt(dMinElement.val());
var dMinLimit = Math.min(Math.max(dValue - 1, 0), 100);
if (dMinValue > dMinLimit) {
dMinElement.val(dMinLimit);
}
dMinElement.attr("max", dMinLimit);
}
$('.pid_tuning .ROLL input[name="d"]').change(function() {
var dMinElement= $('.pid_tuning input[name="dMinRoll"]');
adjustDMin($(this), dMinElement);
}).change();
$('.pid_tuning .PITCH input[name="d"]').change(function() {
var dMinElement= $('.pid_tuning input[name="dMinPitch"]');
adjustDMin($(this), dMinElement);
}).change();
$('.pid_tuning .YAW input[name="d"]').change(function() {
var dMinElement= $('.pid_tuning input[name="dMinYaw"]');
adjustDMin($(this), dMinElement);
}).change();
if (semver.gte(CONFIG.apiVersion, "1.41.0")) {
var dMinSwitch = $('#dMinSwitch');
dMinSwitch.prop('checked', ADVANCED_TUNING.dMinRoll > 0 || ADVANCED_TUNING.dMinPitch > 0 || ADVANCED_TUNING.dMinYaw > 0);
dMinSwitch.change(function() {
var checked = $(this).is(':checked');
if (checked) {
if ($('.pid_tuning input[name="dMinRoll"]').val() == 0 && $('.pid_tuning input[name="dMinPitch"]').val() == 0 && $('.pid_tuning input[name="dMinYaw"]').val() == 0) {
// when enabling dmin set its value based on 0.57x of actual dmax, dmin is limited to 100
$('.pid_tuning input[name="dMinRoll"]').val(Math.min(Math.round($('.pid_tuning .ROLL input[name="d"]').val() * 0.57), 100));
$('.pid_tuning input[name="dMinPitch"]').val(Math.min(Math.round($('.pid_tuning .PITCH input[name="d"]').val() * 0.57), 100));
$('.pid_tuning input[name="dMinYaw"]').val(Math.min(Math.round($('.pid_tuning .YAW input[name="d"]').val() * 0.57), 100));
} else {
$('.pid_tuning input[name="dMinRoll"]').val(ADVANCED_TUNING.dMinRoll);
$('.pid_tuning input[name="dMinPitch"]').val(ADVANCED_TUNING.dMinPitch);
$('.pid_tuning input[name="dMinYaw"]').val(ADVANCED_TUNING.dMinYaw);
}
$('.dMinDisabledNote').hide();
$('.dminGroup .suboption').show();
$('#pid_main tr :nth-child(5)').show();
$('#pid_main .pid_titlebar2 th').attr('colspan', 6);
} else {
$('.pid_tuning input[name="dMinRoll"]').val(0);
$('.pid_tuning input[name="dMinPitch"]').val(0);
$('.pid_tuning input[name="dMinYaw"]').val(0);
$('.dMinDisabledNote').show();
$('.dminGroup .suboption').hide();
$('#pid_main tr :nth-child(5)').hide();
$('#pid_main .pid_titlebar2 th').attr('colspan', 5);
}
});
dMinSwitch.change();
}
$('input[id="gyroNotch1Enabled"]').change(function() {
var checked = $(this).is(':checked');
var hz = FILTER_CONFIG.gyro_notch_hz > 0 ? FILTER_CONFIG.gyro_notch_hz : FILTER_DEFAULT.gyro_notch_hz;
$('.pid_filter input[name="gyroNotch1Frequency"]').val(checked ? hz : 0).attr('disabled', !checked)
.attr("min", checked ? 1 : 0).change();
$('.pid_filter input[name="gyroNotch1Cutoff"]').attr('disabled', !checked).change();
});
$('input[id="gyroNotch2Enabled"]').change(function() {
var checked = $(this).is(':checked');
var hz = FILTER_CONFIG.gyro_notch2_hz > 0 ? FILTER_CONFIG.gyro_notch2_hz : FILTER_DEFAULT.gyro_notch2_hz;
$('.pid_filter input[name="gyroNotch2Frequency"]').val(checked ? hz : 0).attr('disabled', !checked)
.attr("min", checked ? 1 : 0).change();
$('.pid_filter input[name="gyroNotch2Cutoff"]').attr('disabled', !checked).change();
});
$('input[id="dtermNotchEnabled"]').change(function() {
var checked = $(this).is(':checked');
var hz = FILTER_CONFIG.dterm_notch_hz > 0 ? FILTER_CONFIG.dterm_notch_hz : FILTER_DEFAULT.dterm_notch_hz;
$('.pid_filter input[name="dTermNotchFrequency"]').val(checked ? hz : 0).attr('disabled', !checked)
.attr("min", checked ? 1 : 0).change();
$('.pid_filter input[name="dTermNotchCutoff"]').attr('disabled', !checked).change();
});
$('input[id="gyroLowpassEnabled"]').change(function() {
var checked = $(this).is(':checked');
var disabledByDynamicLowpass = $('input[id="gyroLowpassDynEnabled"]').is(':checked');
var cutoff = FILTER_CONFIG.gyro_lowpass_hz > 0 ? FILTER_CONFIG.gyro_lowpass_hz : FILTER_DEFAULT.gyro_lowpass_hz;
var type = FILTER_CONFIG.gyro_lowpass_hz > 0 ? FILTER_CONFIG.gyro_lowpass_type : FILTER_DEFAULT.gyro_lowpass_type;
$('.pid_filter input[name="gyroLowpassFrequency"]').val((checked || disabledByDynamicLowpass) ? cutoff : 0).attr('disabled', !checked);
$('.pid_filter select[name="gyroLowpassType"]').val(type).attr('disabled', !checked);
if (checked) {
$('input[id="gyroLowpassDynEnabled"]').prop('checked', false).change();
}
self.updateFilterWarning();
});
$('input[id="gyroLowpassDynEnabled"]').change(function() {
var checked = $(this).is(':checked');
var cutoff_min = FILTER_DEFAULT.gyro_lowpass_dyn_min_hz;
var type = FILTER_DEFAULT.gyro_lowpass_type;
if (FILTER_CONFIG.gyro_lowpass_dyn_min_hz > 0 && FILTER_CONFIG.gyro_lowpass_dyn_min_hz < FILTER_CONFIG.gyro_lowpass_dyn_max_hz) {
cutoff_min = FILTER_CONFIG.gyro_lowpass_dyn_min_hz;
type = FILTER_CONFIG.gyro_lowpass_type;
}
$('.pid_filter input[name="gyroLowpassDynMinFrequency"]').val(checked ? cutoff_min : 0).attr('disabled', !checked);
$('.pid_filter input[name="gyroLowpassDynMaxFrequency"]').attr('disabled', !checked);
$('.pid_filter select[name="gyroLowpassDynType"]').val(type).attr('disabled', !checked);
if (checked) {
$('input[id="gyroLowpassEnabled"]').prop('checked', false).change();
} else if (FILTER_CONFIG.gyro_lowpass_hz > 0 && !$('input[id="gyroLowpassEnabled"]').is(':checked')) {
$('input[id="gyroLowpassEnabled"]').prop('checked', true).change();
}
self.updateFilterWarning();
});
$('input[id="gyroLowpass2Enabled"]').change(function() {
var checked = $(this).is(':checked');
var cutoff = FILTER_CONFIG.gyro_lowpass2_hz > 0 ? FILTER_CONFIG.gyro_lowpass2_hz : FILTER_DEFAULT.gyro_lowpass2_hz;
var type = FILTER_CONFIG.gyro_lowpass2_hz > 0 ? FILTER_CONFIG.gyro_lowpass2_type : FILTER_DEFAULT.gyro_lowpass2_type;
$('.pid_filter input[name="gyroLowpass2Frequency"]').val(checked ? cutoff : 0).attr('disabled', !checked);
$('.pid_filter select[name="gyroLowpass2Type"]').val(type).attr('disabled', !checked);
});
$('input[id="dtermLowpassEnabled"]').change(function() {
var checked = $(this).is(':checked');
var disabledByDynamicLowpass = $('input[id="dtermLowpassDynEnabled"]').is(':checked');
var cutoff = FILTER_CONFIG.dterm_lowpass_hz > 0 ? FILTER_CONFIG.dterm_lowpass_hz : FILTER_DEFAULT.dterm_lowpass_hz;
var type = FILTER_CONFIG.dterm_lowpass_hz > 0 ? FILTER_CONFIG.dterm_lowpass_type : FILTER_DEFAULT.dterm_lowpass_type;
$('.pid_filter input[name="dtermLowpassFrequency"]').val((checked || disabledByDynamicLowpass) ? cutoff : 0).attr('disabled', !checked);
$('.pid_filter select[name="dtermLowpassType"]').val(type).attr('disabled', !checked);
if (checked) {
$('input[id="dtermLowpassDynEnabled"]').prop('checked', false).change();
}
self.updateFilterWarning();
});
$('input[id="dtermLowpassDynEnabled"]').change(function() {
var checked = $(this).is(':checked');
var cutoff_min = FILTER_DEFAULT.dterm_lowpass_dyn_min_hz;
var type = FILTER_DEFAULT.dterm_lowpass_type;
if (FILTER_CONFIG.dterm_lowpass_dyn_min_hz > 0 && FILTER_CONFIG.dterm_lowpass_dyn_min_hz < FILTER_CONFIG.dterm_lowpass_dyn_max_hz) {
cutoff_min = FILTER_CONFIG.dterm_lowpass_dyn_min_hz;
type = FILTER_CONFIG.dterm_lowpass_type;
}
$('.pid_filter input[name="dtermLowpassDynMinFrequency"]').val(checked ? cutoff_min : 0).attr('disabled', !checked);
$('.pid_filter input[name="dtermLowpassDynMaxFrequency"]').attr('disabled', !checked);
$('.pid_filter select[name="dtermLowpassDynType"]').val(type).attr('disabled', !checked);
if (checked) {
$('input[id="dtermLowpassEnabled"]').prop('checked', false).change();
} else if (FILTER_CONFIG.dterm_lowpass_hz > 0 && !$('input[id="dtermLowpassEnabled"]').is(':checked')) {
$('input[id="dtermLowpassEnabled"]').prop('checked', true).change();
}
self.updateFilterWarning();
});
$('input[id="dtermLowpass2Enabled"]').change(function() {
var checked = $(this).is(':checked');
var cutoff = FILTER_CONFIG.dterm_lowpass2_hz > 0 ? FILTER_CONFIG.dterm_lowpass2_hz : FILTER_DEFAULT.dterm_lowpass2_hz;
var type = FILTER_CONFIG.dterm_lowpass2_hz > 0 ? FILTER_CONFIG.dterm_lowpass2_type : FILTER_DEFAULT.dterm_lowpass2_type;
$('.pid_filter input[name="dtermLowpass2Frequency"]').val(checked ? cutoff : 0).attr('disabled', !checked);
$('.pid_filter select[name="dtermLowpass2Type"]').val(type).attr('disabled', !checked);
});
$('input[id="yawLowpassEnabled"]').change(function() {
var checked = $(this).is(':checked');
var cutoff = FILTER_CONFIG.yaw_lowpass_hz > 0 ? FILTER_CONFIG.yaw_lowpass_hz : FILTER_DEFAULT.yaw_lowpass_hz;
$('.pid_filter input[name="yawLowpassFrequency"]').val(checked ? cutoff : 0).attr('disabled', !checked);
});
// The notch cutoff must be smaller than the notch frecuency
function adjustNotchCutoff(frequencyName, cutoffName) {
var frecuency = parseInt($(".pid_filter input[name='" + frequencyName + "']").val());
var cutoff = parseInt($(".pid_filter input[name='" + cutoffName + "']").val());
// Change the max and refresh the value if needed
var maxCutoff = frecuency == 0 ? 0 : frecuency - 1;
$(".pid_filter input[name='" + cutoffName + "']").attr("max", maxCutoff);
if (cutoff >= frecuency) {
$(".pid_filter input[name='" + cutoffName + "']").val(maxCutoff);
}
}
$('input[name="gyroNotch1Frequency"]').change(function() {
adjustNotchCutoff("gyroNotch1Frequency", "gyroNotch1Cutoff");
}).change();
$('input[name="gyroNotch2Frequency"]').change(function() {
adjustNotchCutoff("gyroNotch2Frequency", "gyroNotch2Cutoff");
}).change();
$('input[name="dTermNotchFrequency"]').change(function() {
adjustNotchCutoff("dTermNotchFrequency", "dTermNotchCutoff");
}).change();
// Initial state of the filters: enabled or disabled
$('input[id="gyroNotch1Enabled"]').prop('checked', FILTER_CONFIG.gyro_notch_hz != 0).change();
$('input[id="gyroNotch2Enabled"]').prop('checked', FILTER_CONFIG.gyro_notch2_hz != 0).change();
$('input[id="dtermNotchEnabled"]').prop('checked', FILTER_CONFIG.dterm_notch_hz != 0).change();
$('input[id="gyroLowpassEnabled"]').prop('checked', FILTER_CONFIG.gyro_lowpass_hz != 0).change();
$('input[id="gyroLowpassDynEnabled"]').prop('checked', FILTER_CONFIG.gyro_lowpass_dyn_min_hz != 0 && FILTER_CONFIG.gyro_lowpass_dyn_min_hz < FILTER_CONFIG.gyro_lowpass_dyn_max_hz).change();
$('input[id="gyroLowpass2Enabled"]').prop('checked', FILTER_CONFIG.gyro_lowpass2_hz != 0).change();
$('input[id="dtermLowpassEnabled"]').prop('checked', FILTER_CONFIG.dterm_lowpass_hz != 0).change();
$('input[id="dtermLowpassDynEnabled"]').prop('checked', FILTER_CONFIG.dterm_lowpass_dyn_min_hz != 0 && FILTER_CONFIG.dterm_lowpass_dyn_min_hz < FILTER_CONFIG.dterm_lowpass_dyn_max_hz).change();
$('input[id="dtermLowpass2Enabled"]').prop('checked', FILTER_CONFIG.dterm_lowpass2_hz != 0).change();
$('input[id="yawLowpassEnabled"]').prop('checked', FILTER_CONFIG.yaw_lowpass_hz != 0).change();
self.updatePIDColors();
}
function form_to_pid_and_rc() {
// Fill in the data from PIDs array
// Catch all the changes and stuff the inside PIDs array
// For each pid name
PID_names.forEach(function(elementPid, indexPid) {
// Look into the PID table to a row with the name of the pid
var searchRow = $('.pid_tuning .' + elementPid + ' input');
// Assign each value
searchRow.each(function (indexInput) {
if ($(this).val()) {
PIDs[indexPid][indexInput] = parseFloat($(this).val());
}
});
});
// catch RC_tuning changes
const pitch_rate_e = $('.pid_tuning input[name="pitch_rate"]');
const roll_rate_e = $('.pid_tuning input[name="roll_rate"]');
const yaw_rate_e = $('.pid_tuning input[name="yaw_rate"]');
const rc_rate_pitch_e = $('.pid_tuning input[name="rc_rate_pitch"]');
const rc_rate_e = $('.pid_tuning input[name="rc_rate"]');
const rc_rate_yaw_e = $('.pid_tuning input[name="rc_rate_yaw"]');
const rc_pitch_expo_e = $('.pid_tuning input[name="rc_pitch_expo"]');
const rc_expo_e = $('.pid_tuning input[name="rc_expo"]');
const rc_yaw_expo_e = $('.pid_tuning input[name="rc_yaw_expo"]');
RC_tuning.roll_pitch_rate = parseFloat($('.pid_tuning input[name="roll_pitch_rate"]').val());
RC_tuning.RC_RATE = parseFloat(rc_rate_e.val());
RC_tuning.roll_rate = parseFloat(roll_rate_e.val());
RC_tuning.pitch_rate = parseFloat(pitch_rate_e.val());
RC_tuning.yaw_rate = parseFloat(yaw_rate_e.val());
RC_tuning.RC_EXPO = parseFloat(rc_expo_e.val());
RC_tuning.RC_YAW_EXPO = parseFloat(rc_yaw_expo_e.val());
RC_tuning.rcYawRate = parseFloat(rc_rate_yaw_e.val());
RC_tuning.rcPitchRate = parseFloat(rc_rate_pitch_e.val());
RC_tuning.RC_PITCH_EXPO = parseFloat(rc_pitch_expo_e.val());
if (semver.gte(CONFIG.apiVersion, "1.43.0")) {
switch(self.currentRatesType) {
case self.RATES_TYPE.RACEFLIGHT:
RC_tuning.pitch_rate = parseFloat(pitch_rate_e.val()) / 100;
RC_tuning.roll_rate = parseFloat(roll_rate_e.val()) / 100;
RC_tuning.yaw_rate = parseFloat(yaw_rate_e.val()) / 100;
RC_tuning.rcPitchRate = parseFloat(rc_rate_pitch_e.val()) / 1000;
RC_tuning.RC_RATE = parseFloat(rc_rate_e.val()) / 1000;
RC_tuning.rcYawRate = parseFloat(rc_rate_yaw_e.val()) / 1000;
RC_tuning.RC_PITCH_EXPO = parseFloat(rc_pitch_expo_e.val()) / 100;
RC_tuning.RC_EXPO = parseFloat(rc_expo_e.val()) / 100;
RC_tuning.RC_YAW_EXPO = parseFloat(rc_yaw_expo_e.val()) / 100;
break;
case self.RATES_TYPE.ACTUAL:
RC_tuning.pitch_rate = parseFloat(pitch_rate_e.val()) / 1000;
RC_tuning.roll_rate = parseFloat(roll_rate_e.val()) / 1000;
RC_tuning.yaw_rate = parseFloat(yaw_rate_e.val()) / 1000;
RC_tuning.rcPitchRate = parseFloat(rc_rate_pitch_e.val()) / 1000;
RC_tuning.RC_RATE = parseFloat(rc_rate_e.val()) / 1000;
RC_tuning.rcYawRate = parseFloat(rc_rate_yaw_e.val()) / 1000;
break;
case self.RATES_TYPE.QUICKRATES:
RC_tuning.pitch_rate = parseFloat(pitch_rate_e.val()) / 1000;
RC_tuning.roll_rate = parseFloat(roll_rate_e.val()) / 1000;
RC_tuning.yaw_rate = parseFloat(yaw_rate_e.val()) / 1000;
break;
// add future rates types here
default: // BetaFlight
break;
}
}
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_lowpass_hz = parseInt($('.pid_filter input[name="gyroLowpassFrequency"]').val());
FILTER_CONFIG.dterm_lowpass_hz = parseInt($('.pid_filter input[name="dtermLowpassFrequency"]').val());
FILTER_CONFIG.yaw_lowpass_hz = parseInt($('.pid_filter input[name="yawLowpassFrequency"]').val());
if (semver.gte(CONFIG.apiVersion, "1.16.0") && !semver.gte(CONFIG.apiVersion, "1.20.0")) {
FEATURE_CONFIG.features.updateData($('input[name="SUPEREXPO_RATES"]'));
}
if (semver.gte(CONFIG.apiVersion, "1.16.0")) {
ADVANCED_TUNING.vbatPidCompensation = $('input[id="vbatpidcompensation"]').is(':checked') ? 1 : 0;
}
if (semver.gte(CONFIG.apiVersion, "1.16.0")) {
ADVANCED_TUNING.deltaMethod = $('#pid-tuning .delta select').val();
}
if (semver.gte(CONFIG.apiVersion, "1.20.0")) {
ADVANCED_TUNING.dtermSetpointTransition = parseInt($('input[name="dtermSetpointTransition-number"]').val() * 100);
ADVANCED_TUNING.dtermSetpointWeight = parseInt($('input[name="dtermSetpoint-number"]').val() * 100);
FILTER_CONFIG.gyro_notch_hz = parseInt($('.pid_filter input[name="gyroNotch1Frequency"]').val());
FILTER_CONFIG.gyro_notch_cutoff = parseInt($('.pid_filter input[name="gyroNotch1Cutoff"]').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());
if (semver.gte(CONFIG.apiVersion, "1.21.0")) {
FILTER_CONFIG.gyro_notch2_hz = parseInt($('.pid_filter input[name="gyroNotch2Frequency"]').val());
FILTER_CONFIG.gyro_notch2_cutoff = parseInt($('.pid_filter input[name="gyroNotch2Cutoff"]').val());
}
}
if (semver.gte(CONFIG.apiVersion, "1.24.0")) {
ADVANCED_TUNING.levelAngleLimit = parseInt($('.pid_tuning input[name="angleLimit"]').val());
ADVANCED_TUNING.levelSensitivity = parseInt($('.pid_tuning input[name="sensitivity"]').val());
}
if (semver.gte(CONFIG.apiVersion, "1.36.0")) {
FILTER_CONFIG.dterm_lowpass_type = $('.pid_filter select[name="dtermLowpassType"]').val();
ADVANCED_TUNING.itermThrottleThreshold = parseInt($('.antigravity input[name="itermThrottleThreshold"]').val());
ADVANCED_TUNING.itermAcceleratorGain = parseInt($('.antigravity input[name="itermAcceleratorGain"]').val() * 1000);
}
if (semver.gte(CONFIG.apiVersion, "1.39.0")) {
FILTER_CONFIG.gyro_lowpass2_hz = parseInt($('.pid_filter input[name="gyroLowpass2Frequency"]').val());
FILTER_CONFIG.gyro_lowpass_type = parseInt($('.pid_filter select[name="gyroLowpassType"]').val());
FILTER_CONFIG.gyro_lowpass2_type = parseInt($('.pid_filter select[name="gyroLowpass2Type"]').val());
FILTER_CONFIG.dterm_lowpass2_hz = parseInt($('.pid_filter input[name="dtermLowpass2Frequency"]').val());
}
if (semver.gte(CONFIG.apiVersion, "1.40.0")) {
ADVANCED_TUNING.itermRotation = $('input[id="itermrotation"]').is(':checked') ? 1 : 0;
ADVANCED_TUNING.smartFeedforward = $('input[id="smartfeedforward"]').is(':checked') ? 1 : 0;
ADVANCED_TUNING.itermRelax = $('input[id="itermrelax"]').is(':checked') ? $('select[id="itermrelaxAxes"]').val() : 0;
ADVANCED_TUNING.itermRelaxType = $('select[id="itermrelaxType"]').val();
ADVANCED_TUNING.itermRelaxCutoff = parseInt($('input[name="itermRelaxCutoff"]').val());
ADVANCED_TUNING.absoluteControlGain = $('input[name="absoluteControlGain-number"]').val();
ADVANCED_TUNING.throttleBoost = $('input[name="throttleBoost-number"]').val();
ADVANCED_TUNING.acroTrainerAngleLimit = $('input[name="acroTrainerAngleLimit-number"]').val();
ADVANCED_TUNING.feedforwardRoll = parseInt($('.pid_tuning .ROLL input[name="f"]').val());
ADVANCED_TUNING.feedforwardPitch = parseInt($('.pid_tuning .PITCH input[name="f"]').val());
ADVANCED_TUNING.feedforwardYaw = parseInt($('.pid_tuning .YAW input[name="f"]').val());
ADVANCED_TUNING.feedforwardTransition = parseInt($('input[name="feedforwardTransition-number"]').val() * 100);
ADVANCED_TUNING.antiGravityMode = $('select[id="antiGravityMode"]').val();
}
if (semver.gte(CONFIG.apiVersion, "1.41.0")) {
RC_tuning.throttleLimitType = $('select[id="throttleLimitType"]').val();
RC_tuning.throttleLimitPercent = parseInt($('.throttle_limit input[name="throttleLimitPercent"]').val());
FILTER_CONFIG.dterm_lowpass2_type = $('.pid_filter select[name="dtermLowpass2Type"]').val();
FILTER_CONFIG.gyro_lowpass_dyn_min_hz = parseInt($('.pid_filter input[name="gyroLowpassDynMinFrequency"]').val());
FILTER_CONFIG.gyro_lowpass_dyn_max_hz = parseInt($('.pid_filter input[name="gyroLowpassDynMaxFrequency"]').val());
FILTER_CONFIG.dterm_lowpass_dyn_min_hz = parseInt($('.pid_filter input[name="dtermLowpassDynMinFrequency"]').val());
FILTER_CONFIG.dterm_lowpass_dyn_max_hz = parseInt($('.pid_filter input[name="dtermLowpassDynMaxFrequency"]').val());
if (FILTER_CONFIG.gyro_lowpass_dyn_min_hz > 0 && FILTER_CONFIG.gyro_lowpass_dyn_min_hz < FILTER_CONFIG.gyro_lowpass_dyn_max_hz ) {
FILTER_CONFIG.gyro_lowpass_type = $('.pid_filter select[name="gyroLowpassDynType"]').val();
}
if (FILTER_CONFIG.dterm_lowpass_dyn_min_hz > 0 && FILTER_CONFIG.dterm_lowpass_dyn_min_hz < FILTER_CONFIG.dterm_lowpass_dyn_max_hz ) {
FILTER_CONFIG.dterm_lowpass_type = $('.pid_filter select[name="dtermLowpassDynType"]').val();
}
ADVANCED_TUNING.dMinRoll = parseInt($('.pid_tuning input[name="dMinRoll"]').val());
ADVANCED_TUNING.dMinPitch = parseInt($('.pid_tuning input[name="dMinPitch"]').val());
ADVANCED_TUNING.dMinYaw = parseInt($('.pid_tuning input[name="dMinYaw"]').val());
ADVANCED_TUNING.dMinGain = parseInt($('.dminGroup input[name="dMinGain"]').val());
ADVANCED_TUNING.dMinAdvance = parseInt($('.dminGroup input[name="dMinAdvance"]').val());
ADVANCED_TUNING.useIntegratedYaw = $('input[id="useIntegratedYaw"]').is(':checked') ? 1 : 0;
}
if (semver.gte(CONFIG.apiVersion, "1.42.0")) {
FILTER_CONFIG.dyn_notch_range = parseInt($('.pid_filter select[name="dynamicNotchRange"]').val());
FILTER_CONFIG.dyn_notch_width_percent = parseInt($('.pid_filter input[name="dynamicNotchWidthPercent"]').val());
FILTER_CONFIG.dyn_notch_q = parseInt($('.pid_filter input[name="dynamicNotchQ"]').val());
FILTER_CONFIG.dyn_notch_min_hz = parseInt($('.pid_filter input[name="dynamicNotchMinHz"]').val());
let rpmFilterEnabled = $('.pid_filter #rpmFilterEnabled').is(':checked');
FILTER_CONFIG.gyro_rpm_notch_harmonics = rpmFilterEnabled ? parseInt($('.pid_filter input[name="rpmFilterHarmonics"]').val()) : 0;
FILTER_CONFIG.gyro_rpm_notch_min_hz = parseInt($('.pid_filter input[name="rpmFilterMinHz"]').val());
}
if (semver.gte(CONFIG.apiVersion, "1.43.0")) {
FILTER_CONFIG.dyn_notch_max_hz = parseInt($('.pid_filter input[name="dynamicNotchMaxHz"]').val());
ADVANCED_TUNING.motorOutputLimit = parseInt($('.pid_tuning input[name="motorLimit"]').val());
ADVANCED_TUNING.autoProfileCellCount = parseInt($('.pid_tuning input[name="cellCount"]').val());
ADVANCED_TUNING.idleMinRpm = parseInt($('input[name="idleMinRpm-number"]').val());
const selectedRatesType = $('select[id="ratesType"]').val(); // send analytics for rates type
let selectedRatesTypeName = null;
if (selectedRatesType !== RC_tuning.rates_type) {
selectedRatesTypeName = $('select[id="ratesType"]').find('option:selected').text();
}
self.analyticsChanges['RatesType'] = selectedRatesTypeName;
RC_tuning.rates_type = selectedRatesType;
}
}
function showAllPids() {
// Hide all optional elements
$('.pid_optional tr').hide(); // Hide all rows
$('.pid_optional table').hide(); // Hide tables
$('.pid_optional').hide(); // Hide general div
// Only show rows supported by the firmware
PID_names.forEach(function(elementPid) {
// Show rows for the PID
$('.pid_tuning .' + elementPid).show();
// Show titles and other elements needed by the PID
$('.needed_by_' + elementPid).show();
});
// Special case
if (semver.lt(CONFIG.apiVersion, "1.24.0")) {
$('#pid_sensitivity').hide();
}
}
function hideUnusedPids() {
if (!have_sensor(CONFIG.activeSensors, 'acc')) {
$('#pid_accel').hide();
}
var hideSensorPid = function(element, sensorReady) {
var isVisible = element.is(":visible");
if (!isVisible || !sensorReady) {
element.hide();
isVisible = false;
}
return isVisible;
}
var isVisibleBaroMagGps = false;
isVisibleBaroMagGps |= hideSensorPid($('#pid_baro'), have_sensor(CONFIG.activeSensors, 'baro') || have_sensor(CONFIG.activeSensors, 'sonar'));
isVisibleBaroMagGps |= hideSensorPid($('#pid_mag'), have_sensor(CONFIG.activeSensors, 'mag'));
isVisibleBaroMagGps |= hideSensorPid($('#pid_gps'), have_sensor(CONFIG.activeSensors, 'GPS'));
if (!isVisibleBaroMagGps) {
$('#pid_baro_mag_gps').hide();
}
}
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 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.apiVersion, "1.16.0")) {
useLegacyCurve = true;
}
self.rateCurve = new RateCurve(useLegacyCurve);
function printMaxAngularVel(rate, rcRate, rcExpo, useSuperExpo, deadband, limit, maxAngularVelElement) {
var maxAngularVel = self.rateCurve.getMaxAngularVel(rate, rcRate, rcExpo, useSuperExpo, deadband, limit).toFixed(0);
maxAngularVelElement.text(maxAngularVel);
return maxAngularVel;
}
function drawCurve(rate, rcRate, rcExpo, useSuperExpo, deadband, limit, maxAngularVel, colour, yOffset, context) {
context.save();
context.strokeStyle = colour;
context.translate(0, yOffset);
self.rateCurve.draw(rate, rcRate, rcExpo, useSuperExpo, deadband, limit, maxAngularVel, context);
context.restore();
}
function process_html() {
if (semver.gte(CONFIG.apiVersion, "1.16.0") && !semver.gte(CONFIG.apiVersion, "1.20.0")) {
FEATURE_CONFIG.features.generateElements($('.tab-pid_tuning .features'));
} else {
$('.tab-pid_tuning .pidTuningFeatures').hide();
}
if (semver.lt(CONFIG.apiVersion, "1.39.0")) {
$('input[name="dtermSetpoint-number"]').attr('max', self.SETPOINT_WEIGHT_RANGE_LEGACY);
}
// translate to user-selected language
i18n.localizePage();
// 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,
rc_rate_pitch: RC_tuning.rcPitchRate,
rc_pitch_expo: RC_tuning.RC_PITCH_EXPO,
superexpo: FEATURE_CONFIG.features.isEnabled('SUPEREXPO_RATES'),
deadband: RC_DEADBAND_CONFIG.deadband,
yawDeadband: RC_DEADBAND_CONFIG.yaw_deadband,
roll_rate_limit: RC_tuning.roll_rate_limit,
pitch_rate_limit: RC_tuning.pitch_rate_limit,
yaw_rate_limit: RC_tuning.yaw_rate_limit
};
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.apiVersion, "1.16.0")) {
self.currentRates.rc_rate_yaw = self.currentRates.rc_rate;
}
if (semver.gte(CONFIG.apiVersion, "1.20.0")) {
self.currentRates.superexpo = true;
}
if (semver.gte(CONFIG.apiVersion, "1.36.0")) {
$('.pid_tuning input[name="sensitivity"]').hide();
$('.pid_tuning .levelSensitivityHeader').empty();
}
if (semver.lt(CONFIG.apiVersion, "1.37.0")) {
self.currentRates.rc_rate_pitch = self.currentRates.rc_rate;
self.currentRates.rc_expo_pitch = self.currentRates.rc_expo;
}
if (semver.gte(CONFIG.apiVersion, "1.43.0")) {
switch(RC_tuning.rates_type) {
case self.RATES_TYPE.RACEFLIGHT:
self.currentRates.roll_rate *= 100;
self.currentRates.pitch_rate *= 100;
self.currentRates.yaw_rate *= 100;
self.currentRates.rc_rate *= 1000;
self.currentRates.rc_rate_yaw *= 1000;
self.currentRates.rc_rate_pitch *= 1000;
self.currentRates.rc_expo *= 100;
self.currentRates.rc_yaw_expo *= 100;
self.currentRates.rc_pitch_expo *= 100;
break;
case self.RATES_TYPE.ACTUAL:
self.currentRates.roll_rate *= 1000;
self.currentRates.pitch_rate *= 1000;
self.currentRates.yaw_rate *= 1000;
self.currentRates.rc_rate *= 1000;
self.currentRates.rc_rate_yaw *= 1000;
self.currentRates.rc_rate_pitch *= 1000;
break;
case self.RATES_TYPE.QUICKRATES:
self.currentRates.roll_rate *= 1000;
self.currentRates.pitch_rate *= 1000;
self.currentRates.yaw_rate *= 1000;
break;
// add future rates types here
default: // BetaFlight
break;
}
}
$('.tab-pid_tuning .tab-container .pid').on('click', () => activateSubtab('pid'));
$('.tab-pid_tuning .tab-container .rates').on('click', () => activateSubtab('rates'));
$('.tab-pid_tuning .tab-container .filter').on('click', () => activateSubtab('filter'));
function loadProfilesList() {
var numberOfProfiles = 3;
if (semver.gte(CONFIG.apiVersion, "1.20.0")
&& CONFIG.numProfiles === 2) {
numberOfProfiles = 2;
}
var profileElements = [];
for (var i=0; i<numberOfProfiles; i++) {
profileElements.push(i18n.getMessage("pidTuningProfileOption",[(i + 1)]));
}
return profileElements;
}
function loadRateProfilesList() {
var numberOfRateProfiles = 6;
if (semver.lt(CONFIG.apiVersion, "1.37.0")) {
numberOfRateProfiles = 3;
}
var rateProfileElements = [];
for (var i=0; i<numberOfRateProfiles; i++) {
rateProfileElements.push(i18n.getMessage("pidTuningRateProfileOption",[(i + 1)]));
}
return rateProfileElements;
}
// This vars are used here for populate the profile (and rate profile) selector AND in the copy profile (and rate profile) window
var selectRateProfileValues = loadRateProfilesList();
var selectProfileValues = loadProfilesList();
function populateProfilesSelector(selectProfileValues) {
var profileSelect = $('select[name="profile"]');
selectProfileValues.forEach(function(value, key) {
profileSelect.append('<option value="' + key + '">' + value + '</option>');
});
}
populateProfilesSelector(selectProfileValues);
function populateRateProfilesSelector(selectRateProfileValues) {
var rateProfileSelect = $('select[name="rate_profile"]');
selectRateProfileValues.forEach(function(value, key) {
rateProfileSelect.append('<option value="' + key + '">' + value + '</option>');
});
}
populateRateProfilesSelector(selectRateProfileValues);
var showAllButton = $('#showAllPids');
function updatePidDisplay() {
if (!self.showAllPids) {
hideUnusedPids();
showAllButton.text(i18n.getMessage("pidTuningShowAllPids"));
} else {
showAllPids();
showAllButton.text(i18n.getMessage("pidTuningHideUnusedPids"));
}
}
showAllPids();
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(i18n.getMessage('pidTuningProfileReset'));
});
});
});
$('.tab-pid_tuning select[name="profile"]').change(function () {
self.currentProfile = parseInt($(this).val());
self.updating = true;
$(this).prop('disabled', 'true');
MSP.promise(MSPCodes.MSP_SELECT_SETTING, [self.currentProfile]).then(function () {
self.refresh(function () {
self.updating = false;
$('.tab-pid_tuning select[name="profile"]').prop('disabled', 'false');
CONFIG.profile = self.currentProfile;
GUI.log(i18n.getMessage('pidTuningLoadedProfile', [self.currentProfile + 1]));
});
});
});
if (semver.gte(CONFIG.apiVersion, "1.20.0")) {
$('.tab-pid_tuning select[name="rate_profile"]').change(function () {
self.currentRateProfile = parseInt($(this).val());
self.updating = true;
$(this).prop('disabled', 'true');
MSP.promise(MSPCodes.MSP_SELECT_SETTING, [self.currentRateProfile + self.RATE_PROFILE_MASK]).then(function () {
self.refresh(function () {
self.updating = false;
$('.tab-pid_tuning select[name="rate_profile"]').prop('disabled', 'false');
CONFIG.rateProfile = self.currentRateProfile;
GUI.log(i18n.getMessage('pidTuningLoadedRateProfile', [self.currentRateProfile + 1]));
});
});
});
var dtermTransitionNumberElement = $('input[name="dtermSetpointTransition-number"]');
var dtermTransitionWarningElement = $('#pid-tuning .dtermSetpointTransitionWarning');
function checkUpdateDtermTransitionWarning(value) {
if (value > 0 && value < 0.1) {
dtermTransitionWarningElement.show();
} else {
dtermTransitionWarningElement.hide();
}
}
checkUpdateDtermTransitionWarning(dtermTransitionNumberElement.val());
//Use 'input' event for coupled controls to allow synchronized update
dtermTransitionNumberElement.on('input', function () {
checkUpdateDtermTransitionWarning($(this).val());
});
} else {
$('.tab-pid_tuning .rate_profile').hide();
$('#pid-tuning .dtermSetpointTransition').hide();
$('#pid-tuning .dtermSetpoint').hide();
}
if (!semver.gte(CONFIG.apiVersion, "1.16.0")) {
$('#pid-tuning .delta').hide();
$('.tab-pid_tuning .note').hide();
}
// Add a name to each row of PIDs if empty
$('.pid_tuning tr').each(function(){
for(i = 0; i < PID_names.length; i++) {
if($(this).hasClass(PID_names[i])) {
var firstColumn = $(this).find('td:first');
if (!firstColumn.text()) {
firstColumn.text(PID_names[i]);
}
}
}
});
// DTerm filter options
function loadFilterTypeValues() {
var filterTypeValues = [];
filterTypeValues.push("PT1");
filterTypeValues.push("BIQUAD");
if (semver.lt(CONFIG.apiVersion, "1.39.0")) {
filterTypeValues.push("FIR");
}
return filterTypeValues;
}
function populateFilterTypeSelector(name, selectDtermValues) {
var dtermFilterSelect = $('select[name="' + name + '"]');
selectDtermValues.forEach(function(value, key) {
dtermFilterSelect.append('<option value="' + key + '">' + value + '</option>');
});
}
// Added in API 1.42.0
function loadDynamicNotchRangeValues() {
var dynamicNotchRangeValues = [
"HIGH", "MEDIUM", "LOW", "AUTO",
];
return dynamicNotchRangeValues;
}
function populateDynamicNotchRangeSelect(selectDynamicNotchRangeValues) {
var dynamicNotchRangeSelect = $('select[name="dynamicNotchRange"]');
selectDynamicNotchRangeValues.forEach(function(value, key) {
dynamicNotchRangeSelect.append('<option value="' + key + '">' + value + '</option>');
});
}
if (semver.gte(CONFIG.apiVersion, "1.42.0")) {
populateDynamicNotchRangeSelect(loadDynamicNotchRangeValues());
}
populateFilterTypeSelector('gyroLowpassType', loadFilterTypeValues());
populateFilterTypeSelector('gyroLowpassDynType', loadFilterTypeValues());
populateFilterTypeSelector('gyroLowpass2Type', loadFilterTypeValues());
populateFilterTypeSelector('dtermLowpassType', loadFilterTypeValues());
populateFilterTypeSelector('dtermLowpass2Type', loadFilterTypeValues());
populateFilterTypeSelector('dtermLowpassDynType', loadFilterTypeValues());
pid_and_rc_to_form();
function activateSubtab(subtabName) {
const names = ['pid', 'rates', 'filter'];
if (!names.includes(subtabName)) {
console.debug('Invalid subtab name: "' + subtabName + '"');
return;
}
for (name of names) {
const el = $('.tab-pid_tuning .subtab-' + name);
el[name == subtabName ? 'show' : 'hide']();
}
$('.tab-pid_tuning .tab-container .tab').removeClass('active');
$('.tab-pid_tuning .tab-container .' + subtabName).addClass('active');
self.activeSubtab = subtabName;
if (subtabName == 'rates') {
// force drawing of throttle curve once the throttle curve container element is available
// deferring drawing like this is needed to acquire the exact pixel size of the canvas
redrawThrottleCurve(true);
}
}
activateSubtab(self.activeSubtab);
var pidController_e = $('select[name="controller"]');
if (semver.lt(CONFIG.apiVersion, "1.31.0")) {
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.lt(CONFIG.apiVersion, "1.20.0")) {
pidControllerList = [
{name: ""},
{name: "Integer"},
{name: "Float"}
]
} else {
pidControllerList = [
{name: "Legacy"},
{name: "Betaflight"}
]
}
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(i18n.getMessage('pidTuningUpgradeFirmwareToChangePidController', [CONFIG.apiVersion, CONFIGURATOR.pidControllerChangeMinApiVersion]));
pidController_e.empty();
pidController_e.append('<option value="">Unknown</option>');
pidController_e.prop('disabled', true);
}
} else {
$('.tab-pid_tuning div.controller').hide();
self.updatePidControllerParameters();
}
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 (semver.gte(CONFIG.apiVersion, "1.37.0")) {
$('.pid_tuning .bracket').hide();
$('.pid_tuning input[name=rc_rate]').parent().attr('class', 'pid_data');
$('.pid_tuning input[name=rc_rate]').parent().attr('rowspan', 1);
$('.pid_tuning input[name=rc_expo]').parent().attr('class', 'pid_data');
$('.pid_tuning input[name=rc_expo]').parent().attr('rowspan', 1);
} else {
$('.pid_tuning input[name=rc_rate_pitch]').parent().hide();
$('.pid_tuning input[name=rc_pitch_expo]').parent().hide();
}
if (useLegacyCurve) {
$('.new_rates').hide();
}
// Getting the DOM elements for curve display
var rcCurveElement = $('.rate_curve canvas#rate_curve_layer0').get(0),
curveContext = rcCurveElement.getContext("2d"),
updateNeeded = true,
maxAngularVel;
// make these variables global scope so that they can be accessed by the updateRates function.
self.maxAngularVelRollElement = $('.pid_tuning .maxAngularVelRoll');
self.maxAngularVelPitchElement = $('.pid_tuning .maxAngularVelPitch');
self.maxAngularVelYawElement = $('.pid_tuning .maxAngularVelYaw');
rcCurveElement.width = 1000;
rcCurveElement.height = 1000;
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) {
const stepValue = parseFloat(targetElement.prop('step')); // adjust value to match step (change only the result, not the the actual value)
if (stepValue != null) {
targetValue = Math.round(targetValue / stepValue) * stepValue;
}
self.currentRates[targetElement.attr('name')] = targetValue;
updateNeeded = true;
}
if (targetElement.attr('name') === 'rc_rate' && semver.lt(CONFIG.apiVersion, "1.16.0")) {
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;
}
if (targetElement.attr('name') === 'rc_rate' && semver.lt(CONFIG.apiVersion, "1.37.0")) {
self.currentRates.rc_rate_pitch = targetValue;
}
if (targetElement.attr('name') === 'rc_expo' && semver.lt(CONFIG.apiVersion, "1.37.0")) {
self.currentRates.rc_pitch_expo = targetValue;
}
if (targetElement.attr('id') === 'ratesType' && semver.gte(CONFIG.apiVersion, "1.43.0")) {
self.changeRatesType(targetValue);
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 = curveContext.canvas.width / curveContext.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, self.currentRates.roll_rate_limit, self.maxAngularVelRollElement),
printMaxAngularVel(self.currentRates.pitch_rate, self.currentRates.rc_rate_pitch, self.currentRates.rc_pitch_expo, self.currentRates.superexpo, self.currentRates.deadband, self.currentRates.pitch_rate_limit, self.maxAngularVelPitchElement),
printMaxAngularVel(self.currentRates.yaw_rate, self.currentRates.rc_rate_yaw, self.currentRates.rc_yaw_expo, self.currentRates.superexpo, self.currentRates.yawDeadband, self.currentRates.yaw_rate_limit, self.maxAngularVelYawElement));
// make maxAngularVel multiple of 200deg/s so that the auto-scale doesn't keep changing for small changes of the maximum curve
maxAngularVel = self.rateCurve.setMaxAngularVel(maxAngularVel);
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, self.currentRates.roll_rate_limit, maxAngularVel, '#ff0000', 0, curveContext);
drawCurve(self.currentRates.pitch_rate, self.currentRates.rc_rate_pitch, self.currentRates.rc_pitch_expo, self.currentRates.superexpo, self.currentRates.deadband, self.currentRates.pitch_rate_limit, 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, self.currentRates.yaw_rate_limit, 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');
function redrawThrottleCurve(forced) {
if (!forced && !TABS.pid_tuning.checkThrottle()) {
return;
}
/*
Quadratic curve formula taken from:
https://stackoverflow.com/a/9195706/176210
*/
function getQBezierValue(t, p1, p2, p3) {
var iT = 1 - t;
return iT * iT * p1 + 2 * iT * t * p2 + t * t * p3;
}
function getQuadraticCurvePoint(startX, startY, cpX, cpY, endX, endY, position) {
return {
x: getQBezierValue(position, startX, cpX, endX),
y: getQBezierValue(position, startY, cpY, endY),
};
}
/* --- */
// 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;
}
throttleCurve.width = throttleCurve.height *
(throttleCurve.clientWidth / throttleCurve.clientHeight);
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);
let thrPercent = (RC.channels[3] - 1000) / 1000,
thrpos = thrPercent <= mid
? getQuadraticCurvePoint(0, canvasHeight, midxl, midyl, midx, midy, thrPercent * (1.0 / mid))
: getQuadraticCurvePoint(midx, midy, midxr, midyr, canvasWidth, 0, (thrPercent - mid) * (1.0 / (1.0 - mid)));
// 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();
context.beginPath();
context.arc(thrpos.x, thrpos.y, 4, 0, 2 * Math.PI);
context.fillStyle = context.strokeStyle;
context.fill();
context.save();
let fontSize = 10;
context.font = fontSize + "pt Verdana, Arial, sans-serif";
let realthr = thrPercent * 100.0,
expothr = 100 - (thrpos.y / canvasHeight) * 100.0,
thrlabel = Math.round(thrPercent <= 0 ? 0 : realthr) + "%" +
" = " + Math.round(thrPercent <= 0 ? 0 : expothr) + "%",
textWidth = context.measureText(thrlabel);
context.fillStyle = '#000';
context.scale(textWidth / throttleCurve.clientWidth, 1);
context.fillText(thrlabel, 5, 5 + fontSize);
context.restore();
}
$('.throttle input')
.on('input change', () => setTimeout(() => redrawThrottleCurve(true), 0));
TABS.pid_tuning.throttleDrawInterval = setInterval(redrawThrottleCurve, 100);
$('a.refresh').click(function () {
self.refresh(function () {
GUI.log(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);
});
}
});
var dialogCopyProfile = $('.dialogCopyProfile')[0];
var DIALOG_MODE_PROFILE = 0;
var DIALOG_MODE_RATEPROFILE = 1;
var dialogCopyProfileMode;
if (semver.gte(CONFIG.apiVersion, "1.36.0")) {
var selectProfile = $('.selectProfile');
var selectRateProfile = $('.selectRateProfile');
$.each(selectProfileValues, function(key, value) {
if (key != CONFIG.profile)
selectProfile.append(new Option(value, key));
});
$.each(selectRateProfileValues, function(key, value) {
if (key != CONFIG.rateProfile)
selectRateProfile.append(new Option(value, key));
});
$('.copyprofilebtn').click(function() {
$('.dialogCopyProfile').find('.contentProfile').show();
$('.dialogCopyProfile').find('.contentRateProfile').hide();
dialogCopyProfileMode = DIALOG_MODE_PROFILE;
dialogCopyProfile.showModal();
});
$('.copyrateprofilebtn').click(function() {
$('.dialogCopyProfile').find('.contentProfile').hide();
$('.dialogCopyProfile').find('.contentRateProfile').show();
dialogCopyProfileMode = DIALOG_MODE_RATEPROFILE;
dialogCopyProfile.showModal();
});
$('.dialogCopyProfile-cancelbtn').click(function() {
dialogCopyProfile.close();
});
$('.dialogCopyProfile-confirmbtn').click(function() {
switch(dialogCopyProfileMode) {
case DIALOG_MODE_PROFILE:
COPY_PROFILE.type = DIALOG_MODE_PROFILE; // 0 = pid profile
COPY_PROFILE.dstProfile = parseInt(selectProfile.val());
COPY_PROFILE.srcProfile = CONFIG.profile;
MSP.send_message(MSPCodes.MSP_COPY_PROFILE, mspHelper.crunch(MSPCodes.MSP_COPY_PROFILE), false, close_dialog);
break;
case DIALOG_MODE_RATEPROFILE:
COPY_PROFILE.type = DIALOG_MODE_RATEPROFILE; // 1 = rate profile
COPY_PROFILE.dstProfile = parseInt(selectRateProfile.val());
COPY_PROFILE.srcProfile = CONFIG.rateProfile;
MSP.send_message(MSPCodes.MSP_COPY_PROFILE, mspHelper.crunch(MSPCodes.MSP_COPY_PROFILE), false, close_dialog);
break;
default:
close_dialog();
break;
}
function close_dialog() {
dialogCopyProfile.close();
}
});
} else {
$('.copyprofilebtn').hide();
$('.copyrateprofilebtn').hide();
}
if (semver.gte(CONFIG.apiVersion, "1.42.0")) {
// filter and tuning sliders
TuningSliders.initialize();
self.analyticsChanges = {};
// UNSCALED non expert slider constrain values
const NON_EXPERT_SLIDER_MAX = 1.25;
const NON_EXPERT_SLIDER_MIN = 0.7;
$('input[name="expertModeCheckbox"]').change(function() {
if (TuningSliders.expertMode !== $(this).is(':checked')) {
TuningSliders.setExpertMode($(this).is(':checked'));
TuningSliders.updatePidSlidersDisplay();
TuningSliders.updateFilterSlidersDisplay();
}
});
$('#dMinSwitch').change(function() {
TuningSliders.setDMinFeatureEnabled($(this).is(':checked'));
// switch dmin and dmax values on dmin on/off if sliders available
if (!TuningSliders.pidSlidersUnavailable) {
if (TuningSliders.dMinFeatureEnabled) {
ADVANCED_TUNING.dMinRoll = PIDs[0][2];
ADVANCED_TUNING.dMinPitch = PIDs[1][2];
ADVANCED_TUNING.dMinYaw = PIDs[2][2];
} else {
PIDs[0][2] = ADVANCED_TUNING.dMinRoll;
PIDs[1][2] = ADVANCED_TUNING.dMinPitch;
PIDs[2][2] = ADVANCED_TUNING.dMinYaw;
}
TuningSliders.calculateNewPids();
}
});
// integrated yaw doesn't work with sliders therefore sliders are disabled
$('input[id="useIntegratedYaw"]').change(() => TuningSliders.updatePidSlidersDisplay());
// pid sliders inputs
$('#tuningMasterSlider, #tuningPDRatioSlider, #tuningPDGainSlider, #tuningResponseSlider').on('input', function() {
const slider = $(this);
// adjust step for more smoothness above 1x
if (slider.val() >= 1) {
slider.attr('step', 0.05);
} else {
slider.attr('step', 0.1);
}
if (!TuningSliders.expertMode) {
if (slider.val() > NON_EXPERT_SLIDER_MAX) {
slider.val(NON_EXPERT_SLIDER_MAX);
} else if (slider.val() < NON_EXPERT_SLIDER_MIN) {
slider.val(NON_EXPERT_SLIDER_MIN);
}
}
const scaledValue = TuningSliders.scaleSliderValue(slider.val());
if (slider.is('#tuningMasterSlider')) {
TuningSliders.MasterSliderValue = scaledValue;
} else if (slider.is('#tuningPDRatioSlider')) {
TuningSliders.PDRatioSliderValue = scaledValue;
} else if (slider.is('#tuningPDGainSlider')) {
TuningSliders.PDGainSliderValue = scaledValue;
} else if (slider.is('#tuningResponseSlider')) {
TuningSliders.ResponseSliderValue = scaledValue;
}
TuningSliders.calculateNewPids();
self.analyticsChanges['PidTuningSliders'] = "On";
});
$('#tuningMasterSlider, #tuningPDRatioSlider, #tuningPDGainSlider, #tuningResponseSlider').mousedown(function() {
// adjust step for more smoothness above 1x on mousedown
const slider = $(this);
if (slider.val() >= 1) {
slider.attr('step', 0.05);
} else {
slider.attr('step', 0.1);
}
});
$('#tuningMasterSlider, #tuningPDRatioSlider, #tuningPDGainSlider, #tuningResponseSlider').mouseup(function() {
// readjust dmin maximums
$('.pid_tuning .ROLL input[name="d"]').change();
$('.pid_tuning .PITCH input[name="d"]').change();
$('.pid_tuning .YAW input[name="d"]').change();
TuningSliders.updatePidSlidersDisplay();
});
// reset to middle with double click
$('#tuningMasterSlider, #tuningPDRatioSlider, #tuningPDGainSlider, #tuningResponseSlider').dblclick(function() {
const slider = $(this);
slider.val(1);
if (slider.is('#tuningMasterSlider')) {
TuningSliders.MasterSliderValue = 1;
} else if (slider.is('#tuningPDRatioSlider')) {
TuningSliders.PDRatioSliderValue = 1;
} else if (slider.is('#tuningPDGainSlider')) {
TuningSliders.PDGainSliderValue = 1;
} else if (slider.is('#tuningResponseSlider')) {
TuningSliders.ResponseSliderValue = 1;
}
TuningSliders.calculateNewPids();
TuningSliders.updatePidSlidersDisplay();
});
// enable PID sliders button
$('a.buttonPidTuningSliders').click(function() {
// if values were previously changed manually and then sliders are reactivated, reset pids to previous valid values if available, else default
TuningSliders.resetPidSliders();
// disable integrated yaw when enabling sliders
if ($('input[id="useIntegratedYaw"]').is(':checked')) {
$('input[id="useIntegratedYaw"]').prop('checked', true).click();
}
self.analyticsChanges['PidTuningSliders'] = "On";
});
// filter slider inputs
$('#tuningGyroFilterSlider, #tuningDTermFilterSlider').on('input', function() {
const slider = $(this);
if (!TuningSliders.expertMode) {
if (slider.val() > NON_EXPERT_SLIDER_MAX) {
slider.val(NON_EXPERT_SLIDER_MAX);
} else if (slider.val() < NON_EXPERT_SLIDER_MIN) {
slider.val(NON_EXPERT_SLIDER_MIN);
}
}
const scaledValue = TuningSliders.scaleSliderValue(slider.val());
if (slider.is('#tuningGyroFilterSlider')) {
TuningSliders.gyroFilterSliderValue = scaledValue;
TuningSliders.calculateNewGyroFilters();
self.analyticsChanges['GyroFilterTuningSlider'] = "On";
} else if (slider.is('#tuningDTermFilterSlider')) {
TuningSliders.dtermFilterSliderValue = scaledValue;
TuningSliders.calculateNewDTermFilters();
self.analyticsChanges['DTermFilterTuningSlider'] = "On";
}
});
$('#tuningGyroFilterSlider, #tuningDTermFilterSlider').mouseup(function() {
TuningSliders.updateFilterSlidersDisplay();
});
// reset to middle with double click
$('#tuningGyroFilterSlider, #tuningDTermFilterSlider').dblclick(function() {
const slider = $(this);
slider.val(1);
if (slider.is('#tuningGyroFilterSlider')) {
TuningSliders.gyroFilterSliderValue = 1;
TuningSliders.calculateNewGyroFilters();
} else if (slider.is('#tuningDTermFilterSlider')) {
TuningSliders.dtermFilterSliderValue = 1;
TuningSliders.calculateNewDTermFilters();
}
TuningSliders.updateFilterSlidersDisplay();
});
// enable PID sliders button
$('a.buttonFilterTuningSliders').click(function() {
if (TuningSliders.filterGyroSliderUnavailable) {
// update switchery dynamically based on defaults
$('input[id="gyroLowpassDynEnabled"]').prop('checked', false).click();
$('input[id="gyroLowpassEnabled"]').prop('checked', true).click();
$('input[id="gyroLowpass2Enabled"]').prop('checked', false).click();
TuningSliders.resetGyroFilterSlider();
self.analyticsChanges['GyroFilterTuningSlider'] = "On";
}
if (TuningSliders.filterDTermSliderUnavailable) {
$('input[id="dtermLowpassDynEnabled"]').prop('checked', false).click();
$('input[id="dtermLowpassEnabled"]').prop('checked', true).click();
$('input[id="dtermLowpass2Enabled"]').prop('checked', false).click();
TuningSliders.resetDTermFilterSlider();
self.analyticsChanges['DTermFilterTuningSlider'] = "On";
}
});
// update on pid table inputs
$('#pid_main input').on('input', function() {
TuningSliders.updatePidSlidersDisplay();
self.analyticsChanges['PidTuningSliders'] = "Off";
});
// update on filter value or type changes
$('.pid_filter tr:not(.newFilter) input, .pid_filter tr:not(.newFilter) select').on('input', function() {
TuningSliders.updateFilterSlidersDisplay();
if (TuningSliders.filterGyroSliderUnavailable) {
self.analyticsChanges['GyroFilterTuningSlider'] = "Off";
}
if (TuningSliders.filterDTermSliderUnavailable) {
self.analyticsChanges['DTermFilterTuningSlider'] = "Off";
}
});
// update on filter switch changes
$('.pid_filter tr:not(.newFilter) .inputSwitch input').change(() => $('.pid_filter input').triggerHandler('input'));
$('.tuningHelp').hide();
} else {
$('.tuningPIDSliders').hide();
$('.tuningFilterSliders').hide();
$('.slidersDisabled').hide();
$('.slidersWarning').hide();
$('.nonExpertModeSlidersNote').hide();
$('.tuningHelpSliders').hide();
}
if (semver.gte(CONFIG.apiVersion, "1.16.0")) {
$('#pid-tuning .delta select').change(function() {
self.setDirty(true);
});
}
if (semver.lt(CONFIG.apiVersion, "1.31.0")) {
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) && semver.lt(CONFIG.apiVersion, "1.31.0")) {
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 () {
return MSP.promise(MSPCodes.MSP_SET_PID_ADVANCED, mspHelper.crunch(MSPCodes.MSP_SET_PID_ADVANCED));
}).then(function () {
self.updatePIDColors();
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 () {
return MSP.promise(MSPCodes.MSP_EEPROM_WRITE);
}).then(function () {
self.updating = false;
self.setDirty(false);
GUI.log(i18n.getMessage('pidTuningEepromSaved'));
self.refresh();
});
analytics.sendChangeEvents(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, self.analyticsChanges);
self.analyticsChanges = {};
});
// 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.getReceiverData, 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.getReceiverData = 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'));
$('.tab-pid_tuning .tab-container .rates').on('click', $.proxy(this.model.resize, this.model));
$('.tab-pid_tuning .tab-container .rates').on('click', $.proxy(this.updateRatesLabels, this));
$(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 (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, this.currentRates.roll_rate_limit),
pitch = delta * this.rateCurve.rcCommandRawToDegreesPerSecond(RC.channels[1], this.currentRates.pitch_rate, this.currentRates.rc_rate_pitch, this.currentRates.rc_pitch_expo, this.currentRates.superexpo, this.currentRates.deadband, this.currentRates.pitch_rate_limit),
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.currentRates.yaw_rate_limit);
this.model.rotateBy(-degToRad(pitch), -degToRad(yaw), -degToRad(roll));
if (this.checkRC()) this.updateRatesLabels(); // has the RC data changed ?
}
};
TABS.pid_tuning.cleanup = function (callback) {
var self = this;
if (self.model) {
$(window).off('resize', $.proxy(self.model.resize, self.model));
self.model.dispose();
}
$(window).off('resize', $.proxy(this.updateRatesLabels, this));
self.keepRendering = false;
clearInterval(TABS.pid_tuning.throttleDrawInterval);
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.apiVersion, "1.20.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 (!self.updating && !self.dirty) {
var changedProfile = false;
if (self.currentProfile !== CONFIG.profile) {
self.setProfile();
changedProfile = true;
}
var changedRateProfile = false;
if (semver.gte(CONFIG.apiVersion, "1.20.0")
&& updateRateProfile
&& self.currentRateProfile !== CONFIG.rateProfile) {
self.setRateProfile();
changedRateProfile = true;
}
if (changedProfile || changedRateProfile) {
self.refresh(function () {
if (changedProfile) {
GUI.log(i18n.getMessage('pidTuningReceivedProfile', [CONFIG.profile + 1]));
CONFIG.profile = self.currentProfile;
}
if (changedRateProfile) {
GUI.log(i18n.getMessage('pidTuningReceivedRateProfile', [CONFIG.rateProfile + 1]));
CONFIG.rateProfile = self.currentRateProfile
}
});
}
}
}
};
TABS.pid_tuning.checkRC = function() {
// Function monitors for change in the primary axes rc received data and returns true if a change is detected.
if (!this.oldRC) { this.oldRC = [RC.channels[0], RC.channels[1], RC.channels[2]]; }
// Monitor RC.channels and detect change of value;
var rateCurveUpdateRequired = false;
for(var i=0; i<this.oldRC.length; i++) { // has the value changed ?
if(this.oldRC[i] != RC.channels[i]) {
this.oldRC[i] = RC.channels[i];
rateCurveUpdateRequired = true; // yes, then an update of the values displayed on the rate curve graph is required
}
}
return rateCurveUpdateRequired;
};
TABS.pid_tuning.checkThrottle = function() {
// Function monitors for change in the received rc throttle data and returns true if a change is detected.
if (!this.oldThrottle) {
this.oldThrottle = RC.channels[3];
return true;
}
var updateRequired = this.oldThrottle != RC.channels[3];
this.oldThrottle = RC.channels[3];
return updateRequired;
};
TABS.pid_tuning.updatePidControllerParameters = function () {
if (semver.gte(CONFIG.apiVersion, "1.20.0") && semver.lt(CONFIG.apiVersion, "1.31.0") && $('.tab-pid_tuning select[name="controller"]').val() === '0') {
$('.pid_tuning .YAW_JUMP_PREVENTION').show();
$('#pid-tuning .delta').show();
$('#pid-tuning .dtermSetpointTransition').hide();
$('#pid-tuning .dtermSetpoint').hide();
} else {
$('.pid_tuning .YAW_JUMP_PREVENTION').hide();
if (semver.gte(CONFIG.apiVersion, "1.40.0")) {
$('#pid-tuning .dtermSetpointTransition').hide();
$('#pid-tuning .dtermSetpoint').hide();
} else {
$('#pid-tuning .dtermSetpointTransition').show();
$('#pid-tuning .dtermSetpoint').show();
}
$('#pid-tuning .delta').hide();
}
};
TABS.pid_tuning.updateRatesLabels = function() {
var self = this;
if (!self.rateCurve.useLegacyCurve && self.rateCurve.maxAngularVel) {
var drawAxisLabel = function(context, axisLabel, x, y, align, color) {
context.fillStyle = color || '#000000' ;
context.textAlign = align || 'center';
context.fillText(axisLabel, x, y);
};
var drawBalloonLabel = function(context, 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(context.font);
// calculate the width and height required for the balloon
const width = (context.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
context.fillStyle = colors.color || '#ffffff' ;
context.strokeStyle = colors.border || '#000000' ;
// correct x position to account for window scaling
x *= context.canvas.clientWidth/context.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>context.height) y=context.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;
context.beginPath();
context.moveTo(x + DEFAULT_RADIUS, y);
context.lineTo(x + width - DEFAULT_RADIUS, y);
context.quadraticCurveTo(x + width, y, x + width, y + DEFAULT_RADIUS);
if(align=='right') { // point is to the right
context.lineTo(x + width, y + DEFAULT_RADIUS + pointerLength);
context.lineTo(x + width + DEFAULT_OFFSET, pointerY); // point
context.lineTo(x + width, y + height - DEFAULT_RADIUS - pointerLength);
}
context.lineTo(x + width, y + height - DEFAULT_RADIUS);
context.quadraticCurveTo(x + width, y + height, x + width - DEFAULT_RADIUS, y + height);
context.lineTo(x + DEFAULT_RADIUS, y + height);
context.quadraticCurveTo(x, y + height, x, y + height - DEFAULT_RADIUS);
if(align=='left') { // point is to the left
context.lineTo(x, y + height - DEFAULT_RADIUS - pointerLength);
context.lineTo(x - DEFAULT_OFFSET, pointerY); // point
context.lineTo(x, y + DEFAULT_RADIUS - pointerLength);
}
context.lineTo(x, y + DEFAULT_RADIUS);
context.quadraticCurveTo(x, y, x + DEFAULT_RADIUS, y);
context.closePath();
// fill in the balloon background
context.fill();
context.stroke();
// and add the label
drawAxisLabel(context, axisLabel, x + (width/2), y + (height + fontSize)/2 - 4, 'center', colors.text);
};
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 rcStickElement = $('.rate_curve canvas#rate_curve_layer1').get(0);
if(rcStickElement) {
rcStickElement.width = 1000;
rcStickElement.height = 1000;
var stickContext = rcStickElement.getContext("2d");
stickContext.save();
var
maxAngularVelRoll = self.maxAngularVelRollElement.text() + ' deg/s',
maxAngularVelPitch = self.maxAngularVelPitchElement.text() + ' deg/s',
maxAngularVelYaw = self.maxAngularVelYawElement.text() + ' deg/s',
currentValues = [],
balloonsDirty = [],
curveHeight = rcStickElement.height,
curveWidth = rcStickElement.width,
maxAngularVel = self.rateCurve.maxAngularVel,
windowScale = (400 / stickContext.canvas.clientHeight),
rateScale = (curveHeight / 2) / maxAngularVel,
lineScale = stickContext.canvas.width / stickContext.canvas.clientWidth,
textScale = stickContext.canvas.clientHeight / 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";
}
if(RC.channels[0] && RC.channels[1] && RC.channels[2]) {
currentValues.push(self.rateCurve.drawStickPosition(RC.channels[0], self.currentRates.roll_rate, self.currentRates.rc_rate, self.currentRates.rc_expo, self.currentRates.superexpo, self.currentRates.deadband, self.currentRates.roll_rate_limit, maxAngularVel, stickContext, '#FF8080') + ' deg/s');
currentValues.push(self.rateCurve.drawStickPosition(RC.channels[1], self.currentRates.pitch_rate, self.currentRates.rc_rate_pitch, self.currentRates.rc_pitch_expo, self.currentRates.superexpo, self.currentRates.deadband, self.currentRates.pitch_rate_limit, maxAngularVel, stickContext, '#80FF80') + ' deg/s');
currentValues.push(self.rateCurve.drawStickPosition(RC.channels[2], self.currentRates.yaw_rate, self.currentRates.rc_rate_yaw, self.currentRates.rc_yaw_expo, self.currentRates.superexpo, self.currentRates.yawDeadband, self.currentRates.yaw_rate_limit, maxAngularVel, stickContext, '#8080FF') + ' deg/s');
} else {
currentValues = [];
}
stickContext.lineWidth = lineScale;
// use a custom scale so that the text does not appear stretched
stickContext.scale(textScale, 1);
// add the maximum range label
drawAxisLabel(stickContext, maxAngularVel.toFixed(0) + ' deg/s', ((curveWidth / 2) - 10) / textScale, 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);}}
];
// show warning message if any axis angular velocity exceeds 1800d/s
const MAX_RATE_WARNING = 1800;
const warningRates = (parseInt(maxAngularVelRoll) > MAX_RATE_WARNING || parseInt(maxAngularVelPitch) > MAX_RATE_WARNING
|| parseInt(maxAngularVelYaw) > MAX_RATE_WARNING);
$('.maxRateWarning').toggle(warningRates);
// 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
if(currentValues[0] && currentValues[1] && currentValues[2]) {
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();
}
}
};
TABS.pid_tuning.updateFilterWarning = function() {
var gyroDynamicLowpassEnabled = $('input[id="gyroLowpassDynEnabled"]').is(':checked');
var gyroLowpass1Enabled = $('input[id="gyroLowpassEnabled"]').is(':checked');
var dtermDynamicLowpassEnabled = $('input[id="dtermLowpassDynEnabled"]').is(':checked');
var dtermLowpass1Enabled = $('input[id="dtermLowpassEnabled"]').is(':checked');
var warning_e = $('#pid-tuning .filterWarning');
var warningDynamicNotch_e = $('#pid-tuning .dynamicNotchWarning');
if (!(gyroDynamicLowpassEnabled || gyroLowpass1Enabled) || !(dtermDynamicLowpassEnabled || dtermLowpass1Enabled)) {
warning_e.show();
} else {
warning_e.hide();
}
if (semver.gte(CONFIG.apiVersion, "1.42.0")) {
if (FEATURE_CONFIG.features.isEnabled('DYNAMIC_FILTER')) {
warningDynamicNotch_e.hide();
} else {
warningDynamicNotch_e.show();
}
} else {
warningDynamicNotch_e.hide();
}
};
TABS.pid_tuning.updatePIDColors = function(clear = false) {
const setTuningElementColor = function(element, mspValue, currentValue) {
if (clear) {
element.css({ "background-color": "transparent" });
return;
}
if (currentValue === undefined || mspValue === undefined) {
return;
}
const change = (currentValue - mspValue) / 50;
element.css({ "background-color": cssUtil.getColorForPercentage(change, cssUtil.colorTables.pidSlider) });
};
PID_names.forEach(function(elementPid, indexPid) {
$(".pid_tuning ." + elementPid + " input").each(function(indexInput) {
setTuningElementColor($(this), PIDS_ACTIVE[indexPid][indexInput], PIDs[indexPid][indexInput]);
});
});
setTuningElementColor($('.pid_tuning input[name="dMinRoll"]'), ADVANCED_TUNING_ACTIVE.dMinRoll, ADVANCED_TUNING.dMinRoll);
setTuningElementColor($('.pid_tuning input[name="dMinPitch"]'), ADVANCED_TUNING_ACTIVE.dMinPitch, ADVANCED_TUNING.dMinPitch);
setTuningElementColor($('.pid_tuning input[name="dMinYaw"]'), ADVANCED_TUNING_ACTIVE.dMinYaw, ADVANCED_TUNING.dMinYaw);
setTuningElementColor($('.pid_tuning .ROLL input[name="f"]'), ADVANCED_TUNING_ACTIVE.feedforwardRoll, ADVANCED_TUNING.feedforwardRoll);
setTuningElementColor($('.pid_tuning .PITCH input[name="f"]'), ADVANCED_TUNING_ACTIVE.feedforwardPitch, ADVANCED_TUNING.feedforwardPitch);
setTuningElementColor($('.pid_tuning .YAW input[name="f"]'), ADVANCED_TUNING_ACTIVE.feedforwardYaw, ADVANCED_TUNING.feedforwardYaw);
};
TABS.pid_tuning.changeRatesType = function(rateTypeID) {
let self = this;
const dialogRatesType = $('.dialogRatesType')[0];
let sameRatesType = true;
self.currentRatesType = rateTypeID;
if (self.currentRatesType !== RC_tuning.rates_type) {
sameRatesType = false;
dialogRatesType.showModal();
$('.dialogRatesType-cancelbtn').click(function() {
sameRatesType = true;
self.currentRatesType = RC_tuning.rates_type;
$('.rates_type select[id="ratesType"]').val(RC_tuning.rates_type);
self.changeRatesTypeLogo();
self.changeRatesSystem(sameRatesType);
dialogRatesType.close();
});
$('.dialogRatesType-confirmbtn').click(function() {
self.changeRatesTypeLogo();
self.changeRatesSystem(sameRatesType);
dialogRatesType.close();
});
} else {
self.changeRatesTypeLogo();
self.changeRatesSystem(sameRatesType);
}
};
TABS.pid_tuning.changeRatesSystem = function(sameType) {
let self = this;
let rcRateMax = 2.55, rcRateMin = 0.01, rcRateStep = 0.01;
let rateMax = 1.0, rateMin = 0, rateStep = 0.01;
let expoMax = 1.0, expoMin = 0, expoStep = 0.01;
const pitch_rate_e = $('.pid_tuning input[name="pitch_rate"]');
const roll_rate_e = $('.pid_tuning input[name="roll_rate"]');
const yaw_rate_e = $('.pid_tuning input[name="yaw_rate"]');
const rc_rate_pitch_e = $('.pid_tuning input[name="rc_rate_pitch"]');
const rc_rate_e = $('.pid_tuning input[name="rc_rate"]');
const rc_rate_yaw_e = $('.pid_tuning input[name="rc_rate_yaw"]');
const rc_pitch_expo_e = $('.pid_tuning input[name="rc_pitch_expo"]');
const rc_expo_e = $('.pid_tuning input[name="rc_expo"]');
const rc_yaw_expo_e = $('.pid_tuning input[name="rc_yaw_expo"]');
const rcRateLabel = $('#pid-tuning .pid_titlebar .rc_rate');
const rateLabel = $('#pid-tuning .pid_titlebar .rate');
const rcExpoLabel = $('#pid-tuning .pid_titlebar .rc_expo');
// default values for betaflight curve. all the default values produce the same betaflight default curve (or at least near enough)
let rcRateDefault = (1).toFixed(2), rateDefault = (0.7).toFixed(2), expoDefault = (0).toFixed(2);
if (sameType) { // if selected rates type is different from the saved one, set values to default instead of reading
pitch_rate_e.val(RC_tuning.pitch_rate.toFixed(2));
roll_rate_e.val(RC_tuning.roll_rate.toFixed(2));
yaw_rate_e.val(RC_tuning.yaw_rate.toFixed(2));
rc_rate_pitch_e.val(RC_tuning.rcPitchRate.toFixed(2));
rc_rate_e.val(RC_tuning.RC_RATE.toFixed(2));
rc_rate_yaw_e.val(RC_tuning.rcYawRate.toFixed(2));
rc_pitch_expo_e.val(RC_tuning.RC_PITCH_EXPO.toFixed(2));
rc_expo_e.val(RC_tuning.RC_EXPO.toFixed(2));
rc_yaw_expo_e.val(RC_tuning.RC_YAW_EXPO.toFixed(2));
}
switch(self.currentRatesType) {
case self.RATES_TYPE.RACEFLIGHT:
rcRateLabel.text(i18n.getMessage("pidTuningRcRateRaceflight"));
rateLabel.text(i18n.getMessage("pidTuningRateRaceflight"));
rcExpoLabel.text(i18n.getMessage("pidTuningRcExpoRaceflight"));
rcRateMax = 2000;
rcRateMin = 10;
rcRateStep = 10;
rateMax = 255;
rateStep = 1;
expoMax = 100;
expoStep = 1;
if (sameType) {
pitch_rate_e.val((RC_tuning.pitch_rate * 100).toFixed(0));
roll_rate_e.val((RC_tuning.roll_rate * 100).toFixed(0));
yaw_rate_e.val((RC_tuning.yaw_rate * 100).toFixed(0));
rc_rate_pitch_e.val((RC_tuning.rcPitchRate * 1000).toFixed(0));
rc_rate_e.val((RC_tuning.RC_RATE * 1000).toFixed(0));
rc_rate_yaw_e.val((RC_tuning.rcYawRate * 1000).toFixed(0));
rc_pitch_expo_e.val((RC_tuning.RC_PITCH_EXPO * 100).toFixed(0));
rc_expo_e.val((RC_tuning.RC_EXPO * 100).toFixed(0));
rc_yaw_expo_e.val((RC_tuning.RC_YAW_EXPO * 100).toFixed(0));
} else {
rcRateDefault = (370).toFixed(0);
rateDefault = (80).toFixed(0);
expoDefault = (50).toFixed(0);
}
break;
case self.RATES_TYPE.KISS:
rcRateLabel.text(i18n.getMessage("pidTuningRcRate"));
rateLabel.text(i18n.getMessage("pidTuningRcRateRaceflight"));
rcExpoLabel.text(i18n.getMessage("pidTuningRcExpoKISS"));
rateMax = 0.99;
break;
case self.RATES_TYPE.ACTUAL:
rcRateLabel.text(i18n.getMessage("pidTuningRcRateActual"));
rateLabel.text(i18n.getMessage("pidTuningRateQuickRates"));
rcExpoLabel.text(i18n.getMessage("pidTuningRcExpoRaceflight"));
rateMax = 2000;
rateStep = 10;
rcRateMax = 2000;
rcRateMin = 10;
rcRateStep = 10;
if (sameType) {
pitch_rate_e.val((RC_tuning.pitch_rate * 1000).toFixed(0));
roll_rate_e.val((RC_tuning.roll_rate * 1000).toFixed(0));
yaw_rate_e.val((RC_tuning.yaw_rate * 1000).toFixed(0));
rc_rate_pitch_e.val((RC_tuning.rcPitchRate * 1000).toFixed(0));
rc_rate_e.val((RC_tuning.RC_RATE * 1000).toFixed(0));
rc_rate_yaw_e.val((RC_tuning.rcYawRate * 1000).toFixed(0));
} else {
rcRateDefault = (200).toFixed(0);
rateDefault = (670).toFixed(0);
expoDefault = (0.54).toFixed(2);
}
break;
case self.RATES_TYPE.QUICKRATES:
rcRateLabel.text(i18n.getMessage("pidTuningRcRate"));
rateLabel.text(i18n.getMessage("pidTuningRateQuickRates"));
rcExpoLabel.text(i18n.getMessage("pidTuningRcExpoRaceflight"));
rateMax = 2000;
rateStep = 10;
if (sameType) {
pitch_rate_e.val((RC_tuning.pitch_rate * 1000).toFixed(0));
roll_rate_e.val((RC_tuning.roll_rate * 1000).toFixed(0));
yaw_rate_e.val((RC_tuning.yaw_rate * 1000).toFixed(0));
} else {
rateDefault = (670).toFixed(0);
}
break;
// add future rates types here
default: // BetaFlight
rcRateLabel.text(i18n.getMessage("pidTuningRcRate"));
rateLabel.text(i18n.getMessage("pidTuningRate"));
rcExpoLabel.text(i18n.getMessage("pidTuningRcExpo"));
break;
}
const rc_rate_input_c = $('#pid-tuning input[class="rc_rate_input"]');
const rate_input_c = $('#pid-tuning input[class="rate_input"]');
const expo_input_c = $('#pid-tuning input[class="expo_input"]');
if (!sameType) {
rate_input_c.val(rateDefault);
rc_rate_input_c.val(rcRateDefault);
expo_input_c.val(expoDefault);
}
rc_rate_input_c.attr({"max":rcRateMax, "min":rcRateMin, "step":rcRateStep}).change();
rate_input_c.attr({"max":rateMax, "min":rateMin, "step":rateStep}).change();
expo_input_c.attr({"max":expoMax, "min":expoMin, "step":expoStep}).change();
if (sameType) {
self.setDirty(false);
}
};
TABS.pid_tuning.changeRatesTypeLogo = function() {
let self = this;
const ratesLogoElement = $('.rates_type img[id="ratesLogo"]');
switch(self.currentRatesType) {
case self.RATES_TYPE.RACEFLIGHT:
ratesLogoElement.attr("src", "../images/rate_logos/raceflight.svg");
break;
case self.RATES_TYPE.KISS:
ratesLogoElement.attr("src", "../images/rate_logos/kiss.svg");
break;
case self.RATES_TYPE.ACTUAL:
ratesLogoElement.attr("src", "../images/rate_logos/actual.svg");
break;
case self.RATES_TYPE.QUICKRATES:
ratesLogoElement.attr("src", "../images/rate_logos/quickrates.svg");
break;
// add future rates types here
default: // BetaFlight
ratesLogoElement.attr("src", "../images/rate_logos/betaflight.svg");
break;
}
};