'use strict'; TABS.pid_tuning = { RATE_PROFILE_MASK: 128, showAllPids: false, updating: true, dirty: false, previousFilterDynQ: null, previousFilterDynCount: null, currentProfile: null, currentRateProfile: null, currentRatesType: null, previousRatesType: null, SETPOINT_WEIGHT_RANGE_LOW: 2.55, SETPOINT_WEIGHT_RANGE_HIGH: 20, SETPOINT_WEIGHT_RANGE_LEGACY: 2.54, activeSubtab: 'pid', analyticsChanges: {}, CONFIGURATOR_PIDS: [], CONFIGURATOR_ADVANCED_TUNING: {}, CONFIGURATOR_FILTER_CONFIG: {}, CONFIGURATOR_RC_TUNING: {}, CONFIGURATOR_FEATURE_CONFIG: {}, CONFIGURATOR_TUNING_SLIDERS: {}, }; TABS.pid_tuning.initialize = function (callback) { const self = this; if (GUI.active_tab !== 'pid_tuning') { GUI.active_tab = 'pid_tuning'; self.activeSubtab = 'pid'; } // Update filtering and pid defaults based on API version const FILTER_DEFAULT = FC.getFilterDefaults(); const PID_DEFAULT = FC.getPidDefaults(); // requesting MSP_STATUS manually because it contains FC.CONFIG.profile MSP.promise(MSPCodes.MSP_STATUS).then(function() { if (semver.gte(FC.CONFIG.apiVersion, CONFIGURATOR.API_VERSION_MIN_SUPPORTED_PID_CONTROLLER_CHANGE)) { 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(FC.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() { let promise; if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { promise = MSP.promise(MSPCodes.MSP_SIMPLIFIED_TUNING); } return promise; }).then(function() { MSP.send_message(MSPCodes.MSP_MIXER_CONFIG, false, false, load_html); }); function load_html() { $('#content').load("./tabs/pid_tuning.html", process_html); } const vbatpidcompensationIsUsed = semver.gte(FC.CONFIG.apiVersion, "1.16.0") && semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_44); function pid_and_rc_to_form() { self.setProfile(); if (semver.gte(FC.CONFIG.apiVersion, "1.20.0")) { self.setRateProfile(); } // Fill in the data from PIDs array for each pid name FC.PID_NAMES.forEach(function(elementPid, indexPid) { // Look into the PID table to a row with the name of the pid const searchRow = $(`.pid_tuning .${elementPid} input`); // Assign each value searchRow.each((indexInput, element) => { if (FC.PIDS[indexPid][indexInput] !== undefined) { $(element).val(FC.PIDS_ACTIVE[indexPid][indexInput]); } }); }); // Fill in data from RC_tuning object $('.pid_tuning input[name="rc_rate"]').val(FC.RC_TUNING.RC_RATE.toFixed(2)); $('.pid_tuning input[name="roll_pitch_rate"]').val(FC.RC_TUNING.roll_pitch_rate.toFixed(2)); $('.pid_tuning input[name="roll_rate"]').val(FC.RC_TUNING.roll_rate.toFixed(2)); $('.pid_tuning input[name="pitch_rate"]').val(FC.RC_TUNING.pitch_rate.toFixed(2)); $('.pid_tuning input[name="yaw_rate"]').val(FC.RC_TUNING.yaw_rate.toFixed(2)); $('.pid_tuning input[name="rc_expo"]').val(FC.RC_TUNING.RC_EXPO.toFixed(2)); $('.pid_tuning input[name="rc_yaw_expo"]').val(FC.RC_TUNING.RC_YAW_EXPO.toFixed(2)); $('.throttle input[name="mid"]').val(FC.RC_TUNING.throttle_MID.toFixed(2)); $('.throttle input[name="expo"]').val(FC.RC_TUNING.throttle_EXPO.toFixed(2)); $('.tpa input[name="tpa"]').val(FC.RC_TUNING.dynamic_THR_PID.toFixed(2)); $('.tpa input[name="tpa-breakpoint"]').val(FC.RC_TUNING.dynamic_THR_breakpoint); if (semver.lt(FC.CONFIG.apiVersion, "1.10.0")) { $('.pid_tuning input[name="rc_yaw_expo"]').hide(); $('.pid_tuning input[name="rc_expo"]').attr("rowspan", "3"); } $('.vbatpidcompensation').toggle(vbatpidcompensationIsUsed); $('input[id="vbatpidcompensation"]').prop('checked', FC.ADVANCED_TUNING.vbatPidCompensation !== 0); if (semver.gte(FC.CONFIG.apiVersion, "1.16.0")) { $('#pid-tuning .delta select').val(FC.ADVANCED_TUNING.deltaMethod); } if (semver.gte(FC.CONFIG.apiVersion, "1.16.0")) { $('.pid_tuning input[name="rc_rate_yaw"]').val(FC.RC_TUNING.rcYawRate.toFixed(2)); $('.pid_filter input[name="gyroLowpassFrequency"]').val(FC.FILTER_CONFIG.gyro_lowpass_hz); $('.pid_filter input[name="dtermLowpassFrequency"]').val(FC.FILTER_CONFIG.dterm_lowpass_hz); $('.pid_filter input[name="yawLowpassFrequency"]').val(FC.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(FC.CONFIG.apiVersion, "1.20.0") || semver.gte(FC.CONFIG.apiVersion, "1.16.0") && FC.FEATURE_CONFIG.features.isEnabled('SUPEREXPO_RATES')) { $('#pid-tuning .rate').text(i18n.getMessage("pidTuningSuperRate")); } else { $('#pid-tuning .rate').text(i18n.getMessage("pidTuningRate")); } if (semver.gte(FC.CONFIG.apiVersion, "1.20.0")) { $('.pid_filter input[name="gyroNotch1Frequency"]').val(FC.FILTER_CONFIG.gyro_notch_hz); $('.pid_filter input[name="gyroNotch1Cutoff"]').val(FC.FILTER_CONFIG.gyro_notch_cutoff); $('.pid_filter input[name="dTermNotchFrequency"]').val(FC.FILTER_CONFIG.dterm_notch_hz); $('.pid_filter input[name="dTermNotchCutoff"]').val(FC.FILTER_CONFIG.dterm_notch_cutoff); const dtermSetpointTransitionNumberElement = $('input[name="dtermSetpointTransition-number"]'); if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_38)) { dtermSetpointTransitionNumberElement.attr('min', 0.00); } else { dtermSetpointTransitionNumberElement.attr('min', 0.01); } dtermSetpointTransitionNumberElement.val(FC.ADVANCED_TUNING.dtermSetpointTransition / 100); $('input[name="dtermSetpoint-number"]').val(FC.ADVANCED_TUNING.dtermSetpointWeight / 100); } else { $('.pid_filter .newFilter').hide(); } if (semver.gte(FC.CONFIG.apiVersion, "1.21.0")) { $('.pid_filter input[name="gyroNotch2Frequency"]').val(FC.FILTER_CONFIG.gyro_notch2_hz); $('.pid_filter input[name="gyroNotch2Cutoff"]').val(FC.FILTER_CONFIG.gyro_notch2_cutoff); } else { $('.pid_filter .gyroNotch2').hide(); } if (semver.gte(FC.CONFIG.apiVersion, "1.24.0")) { $('.pid_tuning input[name="angleLimit"]').val(FC.ADVANCED_TUNING.levelAngleLimit); $('.pid_tuning input[name="sensitivity"]').val(FC.ADVANCED_TUNING.levelSensitivity); } else { $('.pid_sensitivity').hide(); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_36)) { $('.pid_filter select[name="dtermLowpassType"]').val(FC.FILTER_CONFIG.dterm_lowpass_type); $('.antigravity input[name="itermThrottleThreshold"]').val(FC.ADVANCED_TUNING.itermThrottleThreshold); $('.antigravity input[name="itermAcceleratorGain"]').val(FC.ADVANCED_TUNING.itermAcceleratorGain / 1000); if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { $('.antigravity input[name="itermAcceleratorGain"]').attr("min","0.1"); } const antiGravitySwitch = $('#antiGravitySwitch'); const ITERM_ACCELERATOR_GAIN_OFF = semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44) ? 0 : 1000; antiGravitySwitch.prop('checked', FC.ADVANCED_TUNING.itermAcceleratorGain !== ITERM_ACCELERATOR_GAIN_OFF); antiGravitySwitch.change(function() { const checked = $(this).is(':checked'); if (checked) { if (FC.ADVANCED_TUNING.itermAcceleratorGain === ITERM_ACCELERATOR_GAIN_OFF) { const DEFAULT_ACCELERATOR_GAIN = semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_43) ? 3.5 : 1.1; $('.antigravity input[name="itermAcceleratorGain"]').val(DEFAULT_ACCELERATOR_GAIN); } else { const itermAcceleratorGain = (FC.ADVANCED_TUNING.itermAcceleratorGain / 1000); $('.antigravity input[name="itermAcceleratorGain"]').val(itermAcceleratorGain); } $('.antigravity .suboption').show(); if (FC.ADVANCED_TUNING.antiGravityMode == 0) { $('.antigravity .antiGravityThres').hide(); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_40)) { $('.antigravity .antiGravityMode').show(); } else { $('.antigravity .antiGravityMode').hide(); } } else { $('.antigravity select[id="antiGravityMode"]').val(0); $('.antigravity input[name="itermAcceleratorGain"]').val(ITERM_ACCELERATOR_GAIN_OFF / 1000); $('.antigravity .suboption').hide(); } }); antiGravitySwitch.change(); } else { $('.dtermLowpassType').hide(); $('.antigravity').hide(); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_37)) { $('.pid_tuning input[name="rc_rate_pitch"]').val(FC.RC_TUNING.rcPitchRate.toFixed(2)); $('.pid_tuning input[name="rc_pitch_expo"]').val(FC.RC_TUNING.RC_PITCH_EXPO.toFixed(2)); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_39)) { $('.pid_filter input[name="gyroLowpass2Frequency"]').val(FC.FILTER_CONFIG.gyro_lowpass2_hz); $('.pid_filter select[name="gyroLowpassType"]').val(FC.FILTER_CONFIG.gyro_lowpass_type); $('.pid_filter select[name="gyroLowpass2Type"]').val(FC.FILTER_CONFIG.gyro_lowpass2_type); $('.pid_filter input[name="dtermLowpass2Frequency"]').val(FC.FILTER_CONFIG.dterm_lowpass2_hz); $('.pid_filter select[name="dtermLowpass2Type"]').val(FC.FILTER_CONFIG.dterm_lowpass2_type); // 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(FC.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(FC.CONFIG.apiVersion, API_VERSION_1_40)) { // I Term Rotation $('input[id="itermrotation"]').prop('checked', FC.ADVANCED_TUNING.itermRotation !== 0); // Smart Feed Forward $('input[id="smartfeedforward"]').prop('checked', FC.ADVANCED_TUNING.smartFeedforward !== 0); // I Term Relax const itermRelaxCheck = $('input[id="itermrelax"]'); itermRelaxCheck.prop('checked', FC.ADVANCED_TUNING.itermRelax !== 0); $('select[id="itermrelaxAxes"]').val(FC.ADVANCED_TUNING.itermRelax > 0 ? FC.ADVANCED_TUNING.itermRelax : 1); $('select[id="itermrelaxType"]').val(FC.ADVANCED_TUNING.itermRelaxType); $('input[name="itermRelaxCutoff"]').val(FC.ADVANCED_TUNING.itermRelaxCutoff); if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_43)) { $('.itermrelax input[name="itermRelaxCutoff"]').attr("max","50"); } itermRelaxCheck.change(function() { const checked = $(this).is(':checked'); if (checked) { $('.itermrelax .suboption').show(); if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_42)) { $('.itermRelaxCutoff').show(); } else { $('.itermRelaxCutoff').hide(); } } else { $('.itermrelax .suboption').hide(); } }); itermRelaxCheck.change(); // Absolute Control const absoluteControlGainNumberElement = $('input[name="absoluteControlGain-number"]'); absoluteControlGainNumberElement.val(FC.ADVANCED_TUNING.absoluteControlGain).trigger('input'); // Throttle Boost const throttleBoostNumberElement = $('input[name="throttleBoost-number"]'); throttleBoostNumberElement.val(FC.ADVANCED_TUNING.throttleBoost).trigger('input'); // Acro Trainer const acroTrainerAngleLimitNumberElement = $('input[name="acroTrainerAngleLimit-number"]'); acroTrainerAngleLimitNumberElement.val(FC.ADVANCED_TUNING.acroTrainerAngleLimit).trigger('input'); // Yaw D $('.pid_tuning .YAW input[name="d"]').val(FC.PIDS[2][2]); // PID Yaw D // Feedforward $('.pid_tuning .ROLL input[name="f"]').val(FC.ADVANCED_TUNING.feedforwardRoll); $('.pid_tuning .PITCH input[name="f"]').val(FC.ADVANCED_TUNING.feedforwardPitch); $('.pid_tuning .YAW input[name="f"]').val(FC.ADVANCED_TUNING.feedforwardYaw); $('#pid_main .pid_titlebar2 th').attr('colspan', 5); const feedforwardTransitionNumberElement = $('input[name="feedforwardTransition-number"]'); feedforwardTransitionNumberElement.val(FC.ADVANCED_TUNING.feedforwardTransition / 100); // AntiGravity Mode const antiGravityModeSelect = $('.antigravity select[id="antiGravityMode"]'); antiGravityModeSelect.change(function () { const antiGravityModeValue = $('.antigravity select[id="antiGravityMode"]').val(); // Smooth removes threshold if (antiGravityModeValue == 0) { $('.antiGravityThres').hide(); } else { $('.antiGravityThres').show(); } }); antiGravityModeSelect.val(FC.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 .feedforwardGroup').hide(); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_41)) { $('select[id="throttleLimitType"]').val(FC.RC_TUNING.throttleLimitType); $('.throttle_limit input[name="throttleLimitPercent"]').val(FC.RC_TUNING.throttleLimitPercent); $('.pid_filter input[name="gyroLowpassDynMinFrequency"]').val(FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz); $('.pid_filter input[name="gyroLowpassDynMaxFrequency"]').val(FC.FILTER_CONFIG.gyro_lowpass_dyn_max_hz); $('.pid_filter select[name="gyroLowpassDynType"]').val(FC.FILTER_CONFIG.gyro_lowpass_type); $('.pid_filter input[name="dtermLowpassDynMinFrequency"]').val(FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz); $('.pid_filter input[name="dtermLowpassDynMaxFrequency"]').val(FC.FILTER_CONFIG.dterm_lowpass_dyn_max_hz); $('.pid_filter select[name="dtermLowpassDynType"]').val(FC.FILTER_CONFIG.dterm_lowpass_type); $('.pid_tuning input[name="dMinRoll"]').val(FC.ADVANCED_TUNING.dMinRoll); $('.pid_tuning input[name="dMinPitch"]').val(FC.ADVANCED_TUNING.dMinPitch); $('.pid_tuning input[name="dMinYaw"]').val(FC.ADVANCED_TUNING.dMinYaw); $('.dminGroup input[name="dMinGain"]').val(FC.ADVANCED_TUNING.dMinGain); $('.dminGroup input[name="dMinAdvance"]').val(FC.ADVANCED_TUNING.dMinAdvance); $('input[id="useIntegratedYaw"]').prop('checked', FC.ADVANCED_TUNING.useIntegratedYaw !== 0); //dmin column $('#pid_main .pid_titlebar2 th').attr('colspan', 6); } else { $('.throttle_limit').hide(); $('.gyroLowpassDynLegacy').hide(); $('.dtermLowpassDynLegacy').hide(); $('.dtermLowpass2TypeGroup').hide(); $('.dminGroup').hide(); $('.dMinDisabledNote').hide(); //dmin column $('#pid_main tr :nth-child(5)').hide(); $('.integratedYaw').hide(); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_42)) { $('.smartfeedforward').hide(); // Dynamic Notch Filter const sampleRateHz = FC.CONFIG.sampleRateHz / FC.PID_ADVANCED_CONFIG.pid_process_denom; let isDynamicNotchActive = FC.FEATURE_CONFIG.features.isEnabled('DYNAMIC_FILTER'); isDynamicNotchActive = isDynamicNotchActive || FC.FILTER_CONFIG.dyn_notch_count !== 0; isDynamicNotchActive = isDynamicNotchActive && sampleRateHz >= 2000; if (semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_44)) { if (isDynamicNotchActive) { $('.dynamicNotch span.inputSwitch').hide(); } else { $('.dynamicNotch').hide(); } } const dynamicNotchRange_e = $('.pid_filter select[name="dynamicNotchRange"]'); const dynamicNotchWidthPercent_e = $('.pid_filter input[name="dynamicNotchWidthPercent"]'); const dynamicNotchCount_e = $('.pid_filter input[name="dynamicNotchCount"]'); const dynamicNotchQ_e = $('.pid_filter input[name="dynamicNotchQ"]'); const dynamicNotchMinHz_e = $('.pid_filter input[name="dynamicNotchMinHz"]'); const dynamicNotchMaxHz_e = $('.pid_filter input[name="dynamicNotchMaxHz"]'); dynamicNotchRange_e.val(FC.FILTER_CONFIG.dyn_notch_range); dynamicNotchWidthPercent_e.val(FC.FILTER_CONFIG.dyn_notch_width_percent); dynamicNotchCount_e.val(FC.FILTER_CONFIG.dyn_notch_count); dynamicNotchQ_e.val(FC.FILTER_CONFIG.dyn_notch_q); dynamicNotchMinHz_e.val(FC.FILTER_CONFIG.dyn_notch_min_hz); dynamicNotchMaxHz_e.val(FC.FILTER_CONFIG.dyn_notch_max_hz); if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_43)) { dynamicNotchMinHz_e.attr("max","250"); } else { $('.dynamicNotchMaxHz').hide(); } $('.pid_filter input[id="dynamicNotchEnabled"]').on('change', function() { const count = parseInt(dynamicNotchCount_e.val()); const checked = $(this).is(':checked'); if (checked && !count) { dynamicNotchCount_e.val(FILTER_DEFAULT.dyn_notch_count); } $('.dynamicNotch span.suboption').toggle(checked); $('.dynamicNotchRange').toggle(semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_43) && checked); $('.dynamicNotchMaxHz').toggle(semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_43) && checked); $('.dynamicNotchWidthPercent').toggle(semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_44) && checked); $('.dynamicNotchCount').toggle(semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44) && checked); }).prop('checked', isDynamicNotchActive).trigger('change'); // RPM Filter $('.rpmFilter').toggle(FC.MOTOR_CONFIG.use_dshot_telemetry); const rpmFilterHarmonics_e = $('.pid_filter input[name="rpmFilterHarmonics"]'); const rpmFilterMinHz_e = $('.pid_filter input[name="rpmFilterMinHz"]'); rpmFilterHarmonics_e.val(FC.FILTER_CONFIG.gyro_rpm_notch_harmonics); rpmFilterMinHz_e.val(FC.FILTER_CONFIG.gyro_rpm_notch_min_hz); $('.pid_filter #rpmFilterEnabled').on('change', function() { const harmonics = rpmFilterHarmonics_e.val(); const checked = $(this).is(':checked') && harmonics !== 0; rpmFilterHarmonics_e.attr('disabled', !checked); rpmFilterMinHz_e.attr('disabled', !checked); self.previousFilterDynQ = FC.FILTER_CONFIG.dyn_notch_q; self.previousFilterDynCount = FC.FILTER_CONFIG.dyn_notch_count; if (harmonics == 0) { rpmFilterHarmonics_e.val(FILTER_DEFAULT.gyro_rpm_notch_harmonics); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { const dialogDynFilterSettings = { title: i18n.getMessage("dialogDynFiltersChangeTitle"), text: i18n.getMessage("dialogDynFiltersChangeNote"), buttonYesText: i18n.getMessage("presetsWarningDialogYesButton"), buttonNoText: i18n.getMessage("presetsWarningDialogNoButton"), buttonYesCallback: () => _dynFilterChange(), buttonNoCallback: null, }; const _dynFilterChange = function() { if (checked) { dynamicNotchCount_e.val(FILTER_DEFAULT.dyn_notch_count_rpm); dynamicNotchQ_e.val(FILTER_DEFAULT.dyn_notch_q_rpm); } else { dynamicNotchCount_e.val(FILTER_DEFAULT.dyn_notch_count); dynamicNotchQ_e.val(FILTER_DEFAULT.dyn_notch_q); } }; if (checked !== (FC.FILTER_CONFIG.gyro_rpm_notch_harmonics !== 0)) { GUI.showYesNoDialog(dialogDynFilterSettings); } else { dynamicNotchCount_e.val(self.previousFilterDynCount); dynamicNotchQ_e.val(self.previousFilterDynQ); } } $('.rpmFilter span.suboption').toggle(checked); }).prop('checked', FC.FILTER_CONFIG.gyro_rpm_notch_harmonics !== 0).trigger('change'); } else { $('.itermRelaxCutoff').hide(); $('.dynamicNotch').hide(); $('.rpmFilter').hide(); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_43)) { $('.tab-pid_tuning input[name="motorLimit"]').val(FC.ADVANCED_TUNING.motorOutputLimit); $('.tab-pid_tuning input[name="cellCount"]').val(FC.ADVANCED_TUNING.autoProfileCellCount); $('input[name="idleMinRpm-number"]').val(FC.ADVANCED_TUNING.idleMinRpm).prop('disabled', !FC.MOTOR_CONFIG.use_dshot_telemetry); if (FC.MOTOR_CONFIG.use_dshot_telemetry) { $('span.pidTuningIdleMinRpmDisabled').text(i18n.getMessage('pidTuningIdleMinRpm')); } else { $('span.pidTuningIdleMinRpmDisabled').text(i18n.getMessage('pidTuningIdleMinRpmDisabled')); } } else { $('.motorOutputLimit').hide(); $('.idleMinRpm').hide(); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_43)) { 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 FC.CONFIG.apiVersion check for (let i = 0; i < ratesList.length; i++) { ratesTypeListElement.append(``); } self.currentRatesType = FC.RC_TUNING.rates_type; self.previousRatesType = null; ratesTypeListElement.val(self.currentRatesType); self.changeRatesType(self.currentRatesType); // update rate type code when updating the tab } else { self.currentRatesType = null; self.previousRatesType = null; $('.rates_type').hide(); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { // hide legacy filter switches $('.gyroLowpassLegacy').hide(); $('.gyroLowpassDynLegacy').hide(); $('.dtermLowpassLegacy').hide(); $('.dtermLowpassDynLegacy').hide(); $('.pid_filter input[name="dtermLowpassExpo"]').val(FC.FILTER_CONFIG.dyn_lpf_curve_expo); } else { // hide firmware filter switches $('.gyroLowpass').hide(); $('.dtermLowpass').hide(); // Previous html attributes for legacy sliders $('.pid_tuning .ROLL input[name="p"]').attr("max", "200"); $('.pid_tuning .ROLL input[name="i"]').attr("max", "200"); $('.pid_tuning .ROLL input[name="d"]').attr("max", "200"); $('.pid_tuning .ROLL input[name="dMinPitch"]').attr("max", "100"); $('.pid_tuning .PITCH input[name="p"]').attr("max", "200"); $('.pid_tuning .PITCH input[name="i"]').attr("max", "200"); $('.pid_tuning .PITCH input[name="d"]').attr("max", "200"); $('.pid_tuning .PITCH input[name="dMinPitch"]').attr("max", "100"); $('.pid_tuning .YAW input[name="p"]').attr("max", "200"); $('.pid_tuning .YAW input[name="i"]').attr("max", "200"); $('.pid_tuning .YAW input[name="d"]').attr("max", "200"); $('.pid_tuning .YAW input[name="dMinPitch"]').attr("max", "100"); } // Feedforward if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { $('select[id="feedforwardAveraging"]').val(FC.ADVANCED_TUNING.feedforward_averaging); $('input[name="feedforwardSmoothFactor"]').val(FC.ADVANCED_TUNING.feedforward_smooth_factor); $('input[name="feedforwardBoost"]').val(FC.ADVANCED_TUNING.feedforward_boost); $('input[name="feedforwardMaxRateLimit"]').val(FC.ADVANCED_TUNING.feedforward_max_rate_limit); $('input[name="feedforwardJitterFactor"]').val(FC.ADVANCED_TUNING.feedforward_jitter_factor); // Vbat Sag Compensation const vbatSagCompensationCheck = $('input[id="vbatSagCompensation"]'); vbatSagCompensationCheck.prop('checked', FC.ADVANCED_TUNING.vbat_sag_compensation !== 0); $('input[name="vbatSagValue"]').val(FC.ADVANCED_TUNING.vbat_sag_compensation > 0 ? FC.ADVANCED_TUNING.vbat_sag_compensation : 100); vbatSagCompensationCheck.change(function() { const checked = $(this).is(':checked'); $('.vbatSagCompensation .suboption').toggle(checked); }).change(); // Thrust Linearization const thrustLinearizationCheck = $('input[id="thrustLinearization"]'); thrustLinearizationCheck.prop('checked', FC.ADVANCED_TUNING.thrustLinearization !== 0); $('input[name="thrustLinearValue"]').val(FC.ADVANCED_TUNING.thrustLinearization > 0 ? FC.ADVANCED_TUNING.thrustLinearization : 20); thrustLinearizationCheck.change(function() { const checked = $(this).is(':checked'); $('.thrustLinearization .suboption').toggle(checked); }).change(); } else { $('.vbatSagCompensation').hide(); $('.thrustLinearization').hide(); if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_40)) { $('.pid_tuning .ROLL input[name="f"]').val(FC.ADVANCED_TUNING.feedforwardRoll > 0 ? FC.ADVANCED_TUNING.feedforwardRoll : PID_DEFAULT[4]); $('.pid_tuning .PITCH input[name="f"]').val(FC.ADVANCED_TUNING.feedforwardPitch > 0 ? FC.ADVANCED_TUNING.feedforwardPitch : PID_DEFAULT[9]); $('.pid_tuning .YAW input[name="f"]').val(FC.ADVANCED_TUNING.feedforwardYaw > 0 ? FC.ADVANCED_TUNING.feedforwardYaw : PID_DEFAULT[14]); $('span.feedforwardOption').hide(); } else { $('.feedforwardGroup').hide(); } } $('input[id="useIntegratedYaw"]').change(function() { const checked = $(this).is(':checked'); // 4.3 firmware has RP mode. $('#pidTuningIntegratedYawCaution').toggle(checked); }).change(); // if user decreases Dmax, don't allow Dmin above Dmax function adjustDMin(dElement, dMinElement) { const dValue = parseInt(dElement.val()); const dMinValue = parseInt(dMinElement.val()); let dMinLimit = Math.min(Math.max(dValue - 1, 0), 100); if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { dMinLimit = Math.min(Math.max(dValue, 0), 250); } else { dMinElement.attr("max", dMinLimit); } if (dMinValue > dMinLimit) { dMinElement.val(dMinLimit); } } $('.pid_tuning .ROLL input[name="d"]').change(function() { const dMinElement= $('.pid_tuning input[name="dMinRoll"]'); adjustDMin($(this), dMinElement); }).change(); $('.pid_tuning .PITCH input[name="d"]').change(function() { const dMinElement= $('.pid_tuning input[name="dMinPitch"]'); adjustDMin($(this), dMinElement); }).change(); $('.pid_tuning .YAW input[name="d"]').change(function() { const dMinElement= $('.pid_tuning input[name="dMinYaw"]'); adjustDMin($(this), dMinElement); }).change(); // if user increases Dmin, don't allow Dmax below Dmin function adjustD(dMinElement, dElement) { if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { const dValue2 = parseInt(dElement.val()); const dMinValue2 = parseInt(dMinElement.val()); const dLimit = Math.min(Math.max(dMinValue2, 0), 250); if (dValue2 < dLimit) { dElement.val(dLimit); } } } $('.pid_tuning input[name="dMinRoll"]').change(function() { const dElement= $('.pid_tuning .ROLL input[name="d"]'); adjustD($(this), dElement); }).change(); $('.pid_tuning input[name="dMinPitch"]').change(function() { const dElement= $('.pid_tuning .PITCH input[name="d"]'); adjustD($(this), dElement); }).change(); $('.pid_tuning input[name="dMinYaw"]').change(function() { const dElement= $('.pid_tuning .YAW input[name="d"]'); adjustD($(this), dElement); }).change(); $('.pid_tuning .ROLL input[name="d"]').change(function() { const dMinElement= $('.pid_tuning input[name="dMinRoll"]'); adjustDMin($(this), dMinElement); }).change(); $('.pid_tuning .PITCH input[name="d"]').change(function() { const dMinElement= $('.pid_tuning input[name="dMinPitch"]'); adjustDMin($(this), dMinElement); }).change(); $('.pid_tuning .YAW input[name="d"]').change(function() { const dMinElement= $('.pid_tuning input[name="dMinYaw"]'); adjustDMin($(this), dMinElement); }).change(); // dMinSwitch toggle - renamed to Dynamic Damping and disabled in 4.3 if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_41)) { const dMinSwitch = $('#dMinSwitch'); if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { const box = document.getElementById('dMinSwitch'); if (box.parentNode) { box.parentNode.removeChild(box); } $('.dMinDisabledNote').hide(); } else { dMinSwitch.prop('checked', FC.ADVANCED_TUNING.dMinRoll > 0 || FC.ADVANCED_TUNING.dMinPitch > 0 || FC.ADVANCED_TUNING.dMinYaw > 0); dMinSwitch.on('change', function() { const 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)); if (semver.eq(FC.CONFIG.apiVersion, API_VERSION_1_43)) { $('.pid_tuning input[name="dMinRoll"]').val(Math.min(Math.round($('.pid_tuning .ROLL input[name="d"]').val() * 0.65), 100)); $('.pid_tuning input[name="dMinPitch"]').val(Math.min(Math.round($('.pid_tuning .PITCH input[name="d"]').val() * 0.65), 100)); $('.pid_tuning input[name="dMinYaw"]').val(Math.min(Math.round($('.pid_tuning .YAW input[name="d"]').val() * 0.65), 100)); } } else { $('.pid_tuning input[name="dMinRoll"]').val(FC.ADVANCED_TUNING.dMinRoll); $('.pid_tuning input[name="dMinPitch"]').val(FC.ADVANCED_TUNING.dMinPitch); $('.pid_tuning input[name="dMinYaw"]').val(FC.ADVANCED_TUNING.dMinYaw); } $('.dMinDisabledNote').hide(); $('.dminGroup .suboption').show(); $('#pid_main tr :nth-child(5)').show(); $('#pid_main .pid_titlebar2 th').attr('colspan', 6); $('.derivativeText').text(i18n.getMessage("pidTuningDMax")); } else { $('.dMinDisabledNote').show(); $('.dminGroup .suboption').hide(); $('#pid_main tr :nth-child(5)').hide(); $('#pid_main .pid_titlebar2 th').attr('colspan', 5); $('.derivativeText').text(i18n.getMessage("pidTuningDerivative")); $('.pid_tuning input[name="dMinRoll"]').val(0); $('.pid_tuning input[name="dMinPitch"]').val(0); $('.pid_tuning input[name="dMinYaw"]').val(0); } }).trigger('change'); } } $('input[id="gyroNotch1Enabled"]').change(function() { const checked = $(this).is(':checked'); const hz = FC.FILTER_CONFIG.gyro_notch_hz > 0 ? FC.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(); $('.gyroNotch1 span.suboption').toggle(checked); }); $('input[id="gyroNotch2Enabled"]').change(function() { const checked = $(this).is(':checked'); const hz = FC.FILTER_CONFIG.gyro_notch2_hz > 0 ? FC.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(); $('.gyroNotch2 span.suboption').toggle(checked); }); $('input[id="dtermNotchEnabled"]').change(function() { const checked = $(this).is(':checked'); const hz = FC.FILTER_CONFIG.dterm_notch_hz > 0 ? FC.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(); $('.dtermNotch span.suboption').toggle(checked); }); // gyro filter selectors const gyroLowpassDynMinFrequency = $('.pid_filter input[name="gyroLowpassDynMinFrequency"]'); const gyroLowpassDynMaxFrequency = $('.pid_filter input[name="gyroLowpassDynMaxFrequency"]'); const gyroLowpassFrequency = $('.pid_filter input[name="gyroLowpassFrequency"]'); const gyroLowpass2Frequency = $('.pid_filter input[name="gyroLowpass2Frequency"]'); const gyroLowpassType = $('.pid_filter select[name="gyroLowpassType"]'); const gyroLowpass2Type = $('.pid_filter select[name="gyroLowpass2Type"]'); const gyroLowpassDynType = $('.pid_filter select[name="gyroLowpassDynType"]'); const gyroLowpassDynEnabled = $('.pid_filter input[id="gyroLowpassDynEnabled"]'); const gyroLowpassEnabled = $('.pid_filter input[id="gyroLowpassEnabled"]'); const gyroLowpass2Enabled = $('.pid_filter input[id="gyroLowpass2Enabled"]'); const gyroLowpassOption = $('.gyroLowpass span.suboption'); const gyroLowpassOptionStatic = $('.gyroLowpass span.suboption.static'); const gyroLowpassOptionDynamic = $('.gyroLowpass span.suboption.dynamic'); const gyroLowpass2Option = $('.gyroLowpass2 span.suboption'); const gyroLowpassFilterMode = $('.pid_filter select[name="gyroLowpassFilterMode"]'); // dterm filter selectors const dtermLowpassDynMinFrequency = $('.pid_filter input[name="dtermLowpassDynMinFrequency"]'); const dtermLowpassDynMaxFrequency = $('.pid_filter input[name="dtermLowpassDynMaxFrequency"]'); const dtermLowpassFrequency = $('.pid_filter input[name="dtermLowpassFrequency"]'); const dtermLowpass2Frequency = $('.pid_filter input[name="dtermLowpass2Frequency"]'); const dtermLowpassType = $('.pid_filter select[name="dtermLowpassType"]'); const dtermLowpass2Type = $('.pid_filter select[name="dtermLowpass2Type"]'); const dtermLowpassDynType = $('.pid_filter select[name="dtermLowpassDynType"]'); const dtermLowpassDynEnabled = $('.pid_filter input[id="dtermLowpassDynEnabled"]'); const dtermLowpassEnabled = $('input[id="dtermLowpassEnabled"]'); const dtermLowpass2Enabled = $('input[id="dtermLowpass2Enabled"]'); const dtermLowpassOption = $('.dtermLowpass span.suboption'); const dtermLowpassOptionStatic = $('.dtermLowpass span.suboption.static'); const dtermLowpassOptionDynamic = $('.dtermLowpass span.suboption.dynamic'); const dtermLowpass2Option = $('.dtermLowpass2 span.suboption'); const dtermLowpassFilterMode = $('.pid_filter select[name="dtermLowpassFilterMode"]'); if (semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_44)) { // Legacy filter selectors for lowpass 1 and 2 gyroLowpassEnabled.change(function() { const checked = $(this).is(':checked'); const disabledByDynamicLowpass = gyroLowpassDynEnabled.is(':checked'); const cutoff = FC.FILTER_CONFIG.gyro_lowpass_hz > 0 ? FC.FILTER_CONFIG.gyro_lowpass_hz : FILTER_DEFAULT.gyro_lowpass_hz; const type = FC.FILTER_CONFIG.gyro_lowpass_hz > 0 ? FC.FILTER_CONFIG.gyro_lowpass_type : FILTER_DEFAULT.gyro_lowpass_type; gyroLowpassFrequency.val((checked || disabledByDynamicLowpass) ? cutoff : 0).attr('disabled', !checked); gyroLowpassType.each((i, el) => $(el).val(type).attr('disabled', !checked)); if (checked) { gyroLowpassDynEnabled.prop('checked', false).change(); } self.updateFilterWarning(); }); gyroLowpassDynEnabled.change(function() { const checked = $(this).is(':checked'); let cutoff_min = FILTER_DEFAULT.gyro_lowpass_dyn_min_hz; let type = FILTER_DEFAULT.gyro_lowpass_type; if (FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz > 0 && FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz < FC.FILTER_CONFIG.gyro_lowpass_dyn_max_hz) { cutoff_min = FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz; type = FC.FILTER_CONFIG.gyro_lowpass_type; } gyroLowpassDynMinFrequency.val(checked ? cutoff_min : 0).attr('disabled', !checked); gyroLowpassDynMaxFrequency.attr('disabled', !checked); gyroLowpassDynType.each((i, el) => $(el).val(type).attr('disabled', !checked)); if (checked) { gyroLowpassEnabled.prop('checked', false).change(); } else if (FC.FILTER_CONFIG.gyro_lowpass_hz > 0 && !gyroLowpassEnabled.is(':checked')) { gyroLowpassEnabled.prop('checked', true).change(); } self.updateFilterWarning(); }); gyroLowpass2Enabled.change(function() { const checked = $(this).is(':checked'); const cutoff = FC.FILTER_CONFIG.gyro_lowpass2_hz > 0 ? FC.FILTER_CONFIG.gyro_lowpass2_hz : FILTER_DEFAULT.gyro_lowpass2_hz; const type = FC.FILTER_CONFIG.gyro_lowpass2_hz > 0 ? FC.FILTER_CONFIG.gyro_lowpass2_type : FILTER_DEFAULT.gyro_lowpass2_type; gyroLowpass2Frequency.val(checked ? cutoff : 0).attr('disabled', !checked); gyroLowpass2Type.each((i, el) => $(el).val(type).attr('disabled', !checked)); }); dtermLowpassEnabled.change(function() { const checked = $(this).is(':checked'); const disabledByDynamicLowpass = dtermLowpassDynEnabled.is(':checked'); const cutoff = FC.FILTER_CONFIG.dterm_lowpass_hz > 0 ? FC.FILTER_CONFIG.dterm_lowpass_hz : FILTER_DEFAULT.dterm_lowpass_hz; const type = FC.FILTER_CONFIG.dterm_lowpass_hz > 0 ? FC.FILTER_CONFIG.dterm_lowpass_type : FILTER_DEFAULT.dterm_lowpass_type; dtermLowpassFrequency.val((checked || disabledByDynamicLowpass) ? cutoff : 0).attr('disabled', !checked); dtermLowpassType.each((i, el) => $(el).val(type).attr('disabled', !checked)); if (checked) { dtermLowpassDynEnabled.prop('checked', false).change(); } self.updateFilterWarning(); }); dtermLowpassDynEnabled.change(function() { const checked = $(this).is(':checked'); let cutoff_min = FILTER_DEFAULT.dterm_lowpass_dyn_min_hz; let type = FILTER_DEFAULT.dterm_lowpass_type; if (FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz > 0 && FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz < FC.FILTER_CONFIG.dterm_lowpass_dyn_max_hz) { cutoff_min = FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz; type = FC.FILTER_CONFIG.dterm_lowpass_type; } dtermLowpassDynMinFrequency.val(checked ? cutoff_min : 0).attr('disabled', !checked); dtermLowpassDynMaxFrequency.attr('disabled', !checked); dtermLowpassDynType.each((i, el) => $(el).val(type).attr('disabled', !checked)); if (checked) { dtermLowpassEnabled.prop('checked', false).change(); } else if (FC.FILTER_CONFIG.dterm_lowpass_hz > 0 && !dtermLowpassEnabled.is(':checked')) { dtermLowpassEnabled.prop('checked', true).change(); } self.updateFilterWarning(); }); dtermLowpass2Enabled.change(function() { const checked = $(this).is(':checked'); const cutoff = FC.FILTER_CONFIG.dterm_lowpass2_hz > 0 ? FC.FILTER_CONFIG.dterm_lowpass2_hz : FILTER_DEFAULT.dterm_lowpass2_hz; const type = FC.FILTER_CONFIG.dterm_lowpass2_hz > 0 ? FC.FILTER_CONFIG.dterm_lowpass2_type : FILTER_DEFAULT.dterm_lowpass2_type; dtermLowpass2Frequency.val(checked ? cutoff : 0).attr('disabled', !checked); dtermLowpass2Type.each((i, el) => $(el).val(type).attr('disabled', !checked)); }); } else { // firmware 4.3 filter selectors for lowpass 1 and 2; sliders are not yet initialized here gyroLowpassEnabled.change(function() { const checked = $(this).is(':checked'); if (checked) { if (FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz > 0 || FC.FILTER_CONFIG.gyro_lowpass_hz > 0) { // lowpass1 is enabled, set the master switch on, show the label, mode selector and type fields gyroLowpassFilterMode.val(FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz > 0 ? 1 : 0).change(); } else { // lowpass 1 is disabled, set the master switch off, only show label // user is trying to enable the lowpass filter, but it was off (both cutoffs are zero) // initialise in dynamic mode with values at sliders, or use defaults gyroLowpassFilterMode.val(1).change(); } } else { // the user is disabling Lowpass 1 so set everything to zero gyroLowpassDynMinFrequency.val(0); gyroLowpassDynMaxFrequency.val(0); gyroLowpassFrequency.val(0); self.calculateNewGyroFilters(); } gyroLowpassOption.toggle(checked); gyroLowpassOptionStatic.toggle(checked && FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz === 0); gyroLowpassOptionDynamic.toggle(checked && FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz !== 0); }); gyroLowpassFilterMode.change(function() { const dynMode = parseInt($(this).val()); const cutoff = FC.FILTER_CONFIG.gyro_lowpass_hz > 0 ? FC.FILTER_CONFIG.gyro_lowpass_hz : FILTER_DEFAULT.gyro_lowpass_hz; const cutoffMin = FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz > 0 ? FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz : FILTER_DEFAULT.gyro_lowpass_dyn_min_hz; const cutoffMax = FC.FILTER_CONFIG.gyro_lowpass_dyn_max_hz > 0 ? FC.FILTER_CONFIG.gyro_lowpass_dyn_max_hz : FILTER_DEFAULT.gyro_lowpass_dyn_max_hz; gyroLowpassFrequency.val(dynMode ? 0 : cutoff); gyroLowpassDynMinFrequency.val(dynMode ? cutoffMin : 0); gyroLowpassDynMaxFrequency.val(dynMode ? cutoffMax : 0); self.calculateNewGyroFilters(); gyroLowpassOptionStatic.toggle(!dynMode); gyroLowpassOptionDynamic.toggle(!!dynMode); }); // switch gyro lpf2 gyroLowpass2Enabled.change(function() { const checked = $(this).is(':checked'); const cutoff = FC.FILTER_CONFIG.gyro_lowpass2_hz > 0 ? FC.FILTER_CONFIG.gyro_lowpass2_hz : FILTER_DEFAULT.gyro_lowpass2_hz; gyroLowpass2Frequency.val(checked ? cutoff : 0).attr('disabled', !checked); self.calculateNewGyroFilters(); gyroLowpass2Option.toggle(checked); self.updateFilterWarning(); }); dtermLowpassEnabled.change(function() { const checked = $(this).is(':checked'); if (checked) { if (FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz > 0 || FC.FILTER_CONFIG.dterm_lowpass_hz > 0) { // lowpass1 is enabled, set the master switch on, show the label, mode selector and type fields dtermLowpassFilterMode.val(FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz > 0 ? 1 : 0).change(); } else { // lowpass 1 is disabled, set the master switch off, only show label // user is trying to enable the lowpass filter, but it was off (both cutoffs are zero) // initialise in dynamic mode with values at sliders, or use defaults dtermLowpassFilterMode.val(1).change(); } } else { // the user is disabling Lowpass 1 so set everything to zero dtermLowpassDynMinFrequency.val(0); dtermLowpassDynMaxFrequency.val(0); dtermLowpassFrequency.val(0); self.calculateNewDTermFilters(); } dtermLowpassOption.toggle(checked); dtermLowpassOptionStatic.toggle(checked && FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz === 0); dtermLowpassOptionDynamic.toggle(checked && FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz !== 0); }); dtermLowpassFilterMode.change(function() { const dynMode = parseInt($(this).val()); const cutoff = FC.FILTER_CONFIG.dterm_lowpass_hz > 0 ? FC.FILTER_CONFIG.dterm_lowpass_hz : FILTER_DEFAULT.dterm_lowpass_hz; const cutoffMin = FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz > 0 ? FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz : FILTER_DEFAULT.dterm_lowpass_dyn_min_hz; const cutoffMax = FC.FILTER_CONFIG.dterm_lowpass_dyn_max_hz > 0 ? FC.FILTER_CONFIG.dterm_lowpass_dyn_max_hz : FILTER_DEFAULT.dterm_lowpass_dyn_max_hz; dtermLowpassFrequency.val(dynMode ? 0 : cutoff); dtermLowpassDynMinFrequency.val(dynMode ? cutoffMin : 0); dtermLowpassDynMaxFrequency.val(dynMode ? cutoffMax : 0); self.calculateNewDTermFilters(); dtermLowpassOptionStatic.toggle(!dynMode); dtermLowpassOptionDynamic.toggle(!!dynMode); }); dtermLowpass2Enabled.change(function() { const checked = $(this).is(':checked'); const cutoff = FC.FILTER_CONFIG.dterm_lowpass2_hz > 0 ? FC.FILTER_CONFIG.dterm_lowpass2_hz : FILTER_DEFAULT.dterm_lowpass2_hz; dtermLowpass2Frequency.val(checked ? cutoff : 0).attr('disabled', !checked); self.calculateNewDTermFilters(); dtermLowpass2Option.toggle(checked); self.updateFilterWarning(); }); } $('input[id="yawLowpassEnabled"]').change(function() { const checked = $(this).is(':checked'); const cutoff = FC.FILTER_CONFIG.yaw_lowpass_hz > 0 ? FC.FILTER_CONFIG.yaw_lowpass_hz : FILTER_DEFAULT.yaw_lowpass_hz; $('.pid_filter input[name="yawLowpassFrequency"]').val(checked ? cutoff : 0).attr('disabled', !checked); $('.yawLowpass span.suboption').toggle(checked); }); // The notch cutoff must be smaller than the notch frecuency function adjustNotchCutoff(frequencyName, cutoffName) { const frecuency = parseInt($(`.pid_filter input[name='${frequencyName}']`).val()); const cutoff = parseInt($(`.pid_filter input[name='${cutoffName}']`).val()); // Change the max and refresh the value if needed const 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', FC.FILTER_CONFIG.gyro_notch_hz !== 0).change(); $('input[id="gyroNotch2Enabled"]').prop('checked', FC.FILTER_CONFIG.gyro_notch2_hz !== 0).change(); $('input[id="dtermNotchEnabled"]').prop('checked', FC.FILTER_CONFIG.dterm_notch_hz !== 0).change(); if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { gyroLowpassEnabled.prop('checked', FC.FILTER_CONFIG.gyro_lowpass_hz !== 0 || FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz !== 0).change(); dtermLowpassEnabled.prop('checked', FC.FILTER_CONFIG.dterm_lowpass_hz !== 0 || FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz !== 0).change(); } else { gyroLowpassEnabled.prop('checked', FC.FILTER_CONFIG.gyro_lowpass_hz !== 0).change(); gyroLowpassDynEnabled.prop('checked', FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz !== 0 && FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz < FC.FILTER_CONFIG.gyro_lowpass_dyn_max_hz).change(); dtermLowpassEnabled.prop('checked', FC.FILTER_CONFIG.dterm_lowpass_hz !== 0).change(); dtermLowpassDynEnabled.prop('checked', FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz !== 0 && FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz < FC.FILTER_CONFIG.dterm_lowpass_dyn_max_hz).change(); } gyroLowpass2Enabled.prop('checked', FC.FILTER_CONFIG.gyro_lowpass2_hz !== 0).change(); dtermLowpass2Enabled.prop('checked', FC.FILTER_CONFIG.dterm_lowpass2_hz !== 0).change(); $('input[id="yawLowpassEnabled"]').prop('checked', FC.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 FC.PID_NAMES.forEach(function(elementPid, indexPid) { // Look into the PID table to a row with the name of the pid const searchRow = $(`.pid_tuning .${elementPid} input`); // Assign each value searchRow.each(function (indexInput) { if ($(this).val()) { FC.PIDS[indexPid][indexInput] = parseInt($(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"]'); FC.RC_TUNING.roll_pitch_rate = parseFloat($('.pid_tuning input[name="roll_pitch_rate"]').val()); FC.RC_TUNING.RC_RATE = parseFloat(rc_rate_e.val()); FC.RC_TUNING.roll_rate = parseFloat(roll_rate_e.val()); FC.RC_TUNING.pitch_rate = parseFloat(pitch_rate_e.val()); FC.RC_TUNING.yaw_rate = parseFloat(yaw_rate_e.val()); FC.RC_TUNING.RC_EXPO = parseFloat(rc_expo_e.val()); FC.RC_TUNING.RC_YAW_EXPO = parseFloat(rc_yaw_expo_e.val()); FC.RC_TUNING.rcYawRate = parseFloat(rc_rate_yaw_e.val()); FC.RC_TUNING.rcPitchRate = parseFloat(rc_rate_pitch_e.val()); FC.RC_TUNING.RC_PITCH_EXPO = parseFloat(rc_pitch_expo_e.val()); if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_43)) { switch (self.currentRatesType) { case FC.RATES_TYPE.RACEFLIGHT: FC.RC_TUNING.pitch_rate = parseFloat(pitch_rate_e.val()) / 100; FC.RC_TUNING.roll_rate = parseFloat(roll_rate_e.val()) / 100; FC.RC_TUNING.yaw_rate = parseFloat(yaw_rate_e.val()) / 100; FC.RC_TUNING.rcPitchRate = parseFloat(rc_rate_pitch_e.val()) / 1000; FC.RC_TUNING.RC_RATE = parseFloat(rc_rate_e.val()) / 1000; FC.RC_TUNING.rcYawRate = parseFloat(rc_rate_yaw_e.val()) / 1000; FC.RC_TUNING.RC_PITCH_EXPO = parseFloat(rc_pitch_expo_e.val()) / 100; FC.RC_TUNING.RC_EXPO = parseFloat(rc_expo_e.val()) / 100; FC.RC_TUNING.RC_YAW_EXPO = parseFloat(rc_yaw_expo_e.val()) / 100; break; case FC.RATES_TYPE.ACTUAL: FC.RC_TUNING.pitch_rate = parseFloat(pitch_rate_e.val()) / 1000; FC.RC_TUNING.roll_rate = parseFloat(roll_rate_e.val()) / 1000; FC.RC_TUNING.yaw_rate = parseFloat(yaw_rate_e.val()) / 1000; FC.RC_TUNING.rcPitchRate = parseFloat(rc_rate_pitch_e.val()) / 1000; FC.RC_TUNING.RC_RATE = parseFloat(rc_rate_e.val()) / 1000; FC.RC_TUNING.rcYawRate = parseFloat(rc_rate_yaw_e.val()) / 1000; break; case FC.RATES_TYPE.QUICKRATES: FC.RC_TUNING.pitch_rate = parseFloat(pitch_rate_e.val()) / 1000; FC.RC_TUNING.roll_rate = parseFloat(roll_rate_e.val()) / 1000; FC.RC_TUNING.yaw_rate = parseFloat(yaw_rate_e.val()) / 1000; break; // add future rates types here default: // BetaFlight break; } } FC.RC_TUNING.throttle_MID = parseFloat($('.throttle input[name="mid"]').val()); FC.RC_TUNING.throttle_EXPO = parseFloat($('.throttle input[name="expo"]').val()); FC.RC_TUNING.dynamic_THR_PID = parseFloat($('.tpa input[name="tpa"]').val()); FC.RC_TUNING.dynamic_THR_breakpoint = parseInt($('.tpa input[name="tpa-breakpoint"]').val()); FC.FILTER_CONFIG.gyro_lowpass_hz = parseInt($('.pid_filter input[name="gyroLowpassFrequency"]').val()); FC.FILTER_CONFIG.dterm_lowpass_hz = parseInt($('.pid_filter input[name="dtermLowpassFrequency"]').val()); FC.FILTER_CONFIG.yaw_lowpass_hz = parseInt($('.pid_filter input[name="yawLowpassFrequency"]').val()); if (vbatpidcompensationIsUsed) { const element = $('input[id="vbatpidcompensation"]'); const value = element.is(':checked') ? 1 : 0; let analyticsValue = undefined; if (value !== FC.ADVANCED_TUNING.vbatPidCompensation) { analyticsValue = element.is(':checked'); } self.analyticsChanges['VbatPidCompensation'] = analyticsValue; FC.ADVANCED_TUNING.vbatPidCompensation = value; } if (semver.gte(FC.CONFIG.apiVersion, "1.16.0")) { FC.ADVANCED_TUNING.deltaMethod = $('#pid-tuning .delta select').val(); } if (semver.gte(FC.CONFIG.apiVersion, "1.20.0")) { FC.ADVANCED_TUNING.dtermSetpointTransition = parseInt($('input[name="dtermSetpointTransition-number"]').val() * 100); FC.ADVANCED_TUNING.dtermSetpointWeight = parseInt($('input[name="dtermSetpoint-number"]').val() * 100); FC.FILTER_CONFIG.gyro_notch_hz = parseInt($('.pid_filter input[name="gyroNotch1Frequency"]').val()); FC.FILTER_CONFIG.gyro_notch_cutoff = parseInt($('.pid_filter input[name="gyroNotch1Cutoff"]').val()); FC.FILTER_CONFIG.dterm_notch_hz = parseInt($('.pid_filter input[name="dTermNotchFrequency"]').val()); FC.FILTER_CONFIG.dterm_notch_cutoff = parseInt($('.pid_filter input[name="dTermNotchCutoff"]').val()); if (semver.gte(FC.CONFIG.apiVersion, "1.21.0")) { FC.FILTER_CONFIG.gyro_notch2_hz = parseInt($('.pid_filter input[name="gyroNotch2Frequency"]').val()); FC.FILTER_CONFIG.gyro_notch2_cutoff = parseInt($('.pid_filter input[name="gyroNotch2Cutoff"]').val()); } } if (semver.gte(FC.CONFIG.apiVersion, "1.24.0")) { FC.ADVANCED_TUNING.levelAngleLimit = parseInt($('.pid_tuning input[name="angleLimit"]').val()); FC.ADVANCED_TUNING.levelSensitivity = parseInt($('.pid_tuning input[name="sensitivity"]').val()); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_36)) { FC.FILTER_CONFIG.dterm_lowpass_type = parseInt($('.pid_filter select[name="dtermLowpassType"]').val()); FC.ADVANCED_TUNING.itermThrottleThreshold = parseInt($('.antigravity input[name="itermThrottleThreshold"]').val()); FC.ADVANCED_TUNING.itermAcceleratorGain = parseInt($('.antigravity input[name="itermAcceleratorGain"]').val() * 1000); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_39)) { FC.FILTER_CONFIG.gyro_lowpass2_hz = parseInt($('.pid_filter input[name="gyroLowpass2Frequency"]').val()); FC.FILTER_CONFIG.gyro_lowpass_type = parseInt($('.pid_filter select[name="gyroLowpassType"]').val()); FC.FILTER_CONFIG.gyro_lowpass2_type = parseInt($('.pid_filter select[name="gyroLowpass2Type"]').val()); FC.FILTER_CONFIG.dterm_lowpass2_hz = parseInt($('.pid_filter input[name="dtermLowpass2Frequency"]').val()); FC.FILTER_CONFIG.dterm_lowpass2_type = parseInt($('.pid_filter select[name="dtermLowpass2Type"]').val()); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_40)) { FC.ADVANCED_TUNING.itermRotation = $('input[id="itermrotation"]').is(':checked') ? 1 : 0; FC.ADVANCED_TUNING.smartFeedforward = $('input[id="smartfeedforward"]').is(':checked') ? 1 : 0; FC.ADVANCED_TUNING.itermRelax = $('input[id="itermrelax"]').is(':checked') ? $('select[id="itermrelaxAxes"]').val() : 0; FC.ADVANCED_TUNING.itermRelaxType = $('select[id="itermrelaxType"]').val(); FC.ADVANCED_TUNING.itermRelaxCutoff = parseInt($('input[name="itermRelaxCutoff"]').val()); FC.ADVANCED_TUNING.absoluteControlGain = $('input[name="absoluteControlGain-number"]').val(); FC.ADVANCED_TUNING.throttleBoost = $('input[name="throttleBoost-number"]').val(); FC.ADVANCED_TUNING.acroTrainerAngleLimit = $('input[name="acroTrainerAngleLimit-number"]').val(); FC.ADVANCED_TUNING.feedforwardRoll = parseInt($('.pid_tuning .ROLL input[name="f"]').val()); FC.ADVANCED_TUNING.feedforwardPitch = parseInt($('.pid_tuning .PITCH input[name="f"]').val()); FC.ADVANCED_TUNING.feedforwardYaw = parseInt($('.pid_tuning .YAW input[name="f"]').val()); FC.ADVANCED_TUNING.feedforwardTransition = parseInt($('input[name="feedforwardTransition-number"]').val() * 100); FC.ADVANCED_TUNING.antiGravityMode = $('select[id="antiGravityMode"]').val(); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_41)) { FC.RC_TUNING.throttleLimitType = $('select[id="throttleLimitType"]').val(); FC.RC_TUNING.throttleLimitPercent = parseInt($('.throttle_limit input[name="throttleLimitPercent"]').val()); FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz = parseInt($('.pid_filter input[name="gyroLowpassDynMinFrequency"]').val()); FC.FILTER_CONFIG.gyro_lowpass_dyn_max_hz = parseInt($('.pid_filter input[name="gyroLowpassDynMaxFrequency"]').val()); FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz = parseInt($('.pid_filter input[name="dtermLowpassDynMinFrequency"]').val()); FC.FILTER_CONFIG.dterm_lowpass_dyn_max_hz = parseInt($('.pid_filter input[name="dtermLowpassDynMaxFrequency"]').val()); if (semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_44)) { if (FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz > 0 && FC.FILTER_CONFIG.gyro_lowpass_dyn_min_hz < FC.FILTER_CONFIG.gyro_lowpass_dyn_max_hz ) { FC.FILTER_CONFIG.gyro_lowpass_type = $('.pid_filter select[name="gyroLowpassDynType"]').val(); } if (FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz > 0 && FC.FILTER_CONFIG.dterm_lowpass_dyn_min_hz < FC.FILTER_CONFIG.dterm_lowpass_dyn_max_hz ) { FC.FILTER_CONFIG.dterm_lowpass_type = $('.pid_filter select[name="dtermLowpassDynType"]').val(); } } FC.ADVANCED_TUNING.dMinRoll = parseInt($('.pid_tuning input[name="dMinRoll"]').val()); FC.ADVANCED_TUNING.dMinPitch = parseInt($('.pid_tuning input[name="dMinPitch"]').val()); FC.ADVANCED_TUNING.dMinYaw = parseInt($('.pid_tuning input[name="dMinYaw"]').val()); FC.ADVANCED_TUNING.dMinGain = parseInt($('.dminGroup input[name="dMinGain"]').val()); FC.ADVANCED_TUNING.dMinAdvance = parseInt($('.dminGroup input[name="dMinAdvance"]').val()); FC.ADVANCED_TUNING.useIntegratedYaw = $('input[id="useIntegratedYaw"]').is(':checked') ? 1 : 0; } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_42)) { FC.FILTER_CONFIG.dyn_notch_range = parseInt($('.pid_filter select[name="dynamicNotchRange"]').val()); FC.FILTER_CONFIG.dyn_notch_width_percent = parseInt($('.pid_filter input[name="dynamicNotchWidthPercent"]').val()); FC.FILTER_CONFIG.dyn_notch_q = parseInt($('.pid_filter input[name="dynamicNotchQ"]').val()); FC.FILTER_CONFIG.dyn_notch_min_hz = parseInt($('.pid_filter input[name="dynamicNotchMinHz"]').val()); const rpmFilterEnabled = $('.pid_filter #rpmFilterEnabled').is(':checked'); FC.FILTER_CONFIG.gyro_rpm_notch_harmonics = rpmFilterEnabled ? parseInt($('.pid_filter input[name="rpmFilterHarmonics"]').val()) : 0; FC.FILTER_CONFIG.gyro_rpm_notch_min_hz = parseInt($('.pid_filter input[name="rpmFilterMinHz"]').val()); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_43)) { FC.FILTER_CONFIG.dyn_notch_max_hz = parseInt($('.pid_filter input[name="dynamicNotchMaxHz"]').val()); FC.ADVANCED_TUNING.motorOutputLimit = parseInt($('.tab-pid_tuning input[name="motorLimit"]').val()); FC.ADVANCED_TUNING.autoProfileCellCount = parseInt($('.tab-pid_tuning input[name="cellCount"]').val()); FC.ADVANCED_TUNING.idleMinRpm = parseInt($('input[name="idleMinRpm-number"]').val()); const selectedRatesType = $('select[id="ratesType"]').val(); // send analytics for rates type let selectedRatesTypeName = undefined; if (selectedRatesType !== FC.RC_TUNING.rates_type) { selectedRatesTypeName = $('select[id="ratesType"]').find('option:selected').text(); } self.analyticsChanges['RatesType'] = selectedRatesTypeName; FC.RC_TUNING.rates_type = selectedRatesType; } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { FC.ADVANCED_TUNING.feedforward_averaging = $('select[id="feedforwardAveraging"]').val(); FC.ADVANCED_TUNING.feedforward_smooth_factor = parseInt($('input[name="feedforwardSmoothFactor"]').val()); FC.ADVANCED_TUNING.feedforward_boost = parseInt($('input[name="feedforwardBoost"]').val()); FC.ADVANCED_TUNING.feedforward_max_rate_limit = parseInt($('input[name="feedforwardMaxRateLimit"]').val()); FC.ADVANCED_TUNING.feedforward_jitter_factor = parseInt($('input[name="feedforwardJitterFactor"]').val()); FC.FILTER_CONFIG.dyn_lpf_curve_expo = parseInt($('.pid_filter input[name="dtermLowpassDynExpo"]').val()); FC.ADVANCED_TUNING.vbat_sag_compensation = $('input[id="vbatSagCompensation"]').is(':checked') ? parseInt($('input[name="vbatSagValue"]').val()) : 0; FC.ADVANCED_TUNING.thrustLinearization = $('input[id="thrustLinearization"]').is(':checked') ? parseInt($('input[name="thrustLinearValue"]').val()) : 0; FC.FILTER_CONFIG.dyn_lpf_curve_expo = parseInt($('.pid_filter input[name="dtermLowpassExpo"]').val()); const dynamicNotchEnabled = $('.pid_filter input[id="dynamicNotchEnabled"]').is(':checked'); FC.FILTER_CONFIG.dyn_notch_count = dynamicNotchEnabled ? parseInt($('.pid_filter input[name="dynamicNotchCount"]').val()) : 0; FC.TUNING_SLIDERS.slider_pids_mode = TuningSliders.sliderPidsMode; //round slider values to nearest multiple of 5 and passes to the FW. Avoid dividing calc by (* x 100)/5 = 20 FC.TUNING_SLIDERS.slider_master_multiplier = Math.round(TuningSliders.sliderMasterMultiplier * 20) * 5; FC.TUNING_SLIDERS.slider_d_gain = Math.round(TuningSliders.sliderDGain * 20) * 5; FC.TUNING_SLIDERS.slider_pi_gain = Math.round(TuningSliders.sliderPIGain * 20) * 5; FC.TUNING_SLIDERS.slider_feedforward_gain = Math.round(TuningSliders.sliderFeedforwardGain * 20) * 5; FC.TUNING_SLIDERS.slider_i_gain = Math.round(TuningSliders.sliderIGain * 20) * 5; FC.TUNING_SLIDERS.slider_dmax_gain = Math.round(TuningSliders.sliderDMaxGain * 20) * 5; FC.TUNING_SLIDERS.slider_roll_pitch_ratio = Math.round(TuningSliders.sliderRollPitchRatio * 20) * 5; FC.TUNING_SLIDERS.slider_pitch_pi_gain = Math.round(TuningSliders.sliderPitchPIGain * 20) * 5; FC.TUNING_SLIDERS.slider_dterm_filter = TuningSliders.sliderDTermFilter; FC.TUNING_SLIDERS.slider_dterm_filter_multiplier = Math.round(TuningSliders.sliderDTermFilterMultiplier * 20) * 5; FC.TUNING_SLIDERS.slider_gyro_filter = TuningSliders.sliderGyroFilter; FC.TUNING_SLIDERS.slider_gyro_filter_multiplier = Math.round(TuningSliders.sliderGyroFilterMultiplier * 20) * 5; } } 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 FC.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(FC.CONFIG.apiVersion, "1.24.0")) { $('#pid_sensitivity').hide(); } } function hideUnusedPids() { if (!have_sensor(FC.CONFIG.activeSensors, 'acc')) { $('#pid_accel').hide(); } const hideSensorPid = function(element, sensorReady) { let isVisible = element.is(":visible"); if (!isVisible || !sensorReady) { element.hide(); isVisible = false; } return isVisible; }; let isVisibleBaroMagGps = false; isVisibleBaroMagGps |= hideSensorPid($('#pid_baro'), have_sensor(FC.CONFIG.activeSensors, 'baro') || have_sensor(FC.CONFIG.activeSensors, 'sonar')); isVisibleBaroMagGps |= hideSensorPid($('#pid_mag'), have_sensor(FC.CONFIG.activeSensors, 'mag')); isVisibleBaroMagGps |= hideSensorPid($('#pid_gps'), have_sensor(FC.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) { let value = parseFloat(element.val()); if (value < parseFloat(element.prop('min')) || value > parseFloat(element.prop('max'))) { value = undefined; } return value; } let useLegacyCurve = false; if (!semver.gte(FC.CONFIG.apiVersion, "1.16.0")) { useLegacyCurve = true; } self.rateCurve = new RateCurve(useLegacyCurve); if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_36)) { $('.pid_tuning input[name="sensitivity"]').hide(); $('.pid_tuning .levelSensitivityHeader').empty(); } function printMaxAngularVel(rate, rcRate, rcExpo, useSuperExpo, deadband, limit, maxAngularVelElement) { const 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() { TABS.pid_tuning.isHtmlProcessing = true; FC.FEATURE_CONFIG.features.generateElements($('.tab-pid_tuning .features')); if (semver.lt(FC.CONFIG.apiVersion, "1.16.0") || semver.gte(FC.CONFIG.apiVersion, "1.20.0")) { $('.tab-pid_tuning .pidTuningSuperexpoRates').hide(); } if (semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_39)) { $('input[name="dtermSetpoint-number"]').attr('max', self.SETPOINT_WEIGHT_RANGE_LEGACY); } // translate to user-selected language i18n.localizePage(); self.currentRates = self.rateCurve.getCurrentRates(); $('.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() { let numberOfProfiles = 3; if (semver.gte(FC.CONFIG.apiVersion, "1.20.0") && FC.CONFIG.numProfiles === 2) { numberOfProfiles = 2; } const profileElements = []; for (let i=0; i${value}`); }); } populateProfilesSelector(selectProfileValues); function populateRateProfilesSelector(_selectRateProfileValues) { const rateProfileSelect = $('select[name="rate_profile"]'); _selectRateProfileValues.forEach(function(value, key) { rateProfileSelect.append(``); }); } populateRateProfilesSelector(selectRateProfileValues); const 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(); }); $('#resetPidProfile').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('pidTuningPidProfileReset')); }); }); }); $('.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'); FC.CONFIG.profile = self.currentProfile; GUI.log(i18n.getMessage('pidTuningLoadedProfile', [self.currentProfile + 1])); }); }); }); if (semver.gte(FC.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'); FC.CONFIG.rateProfile = self.currentRateProfile; self.currentRates = self.rateCurve.getCurrentRates(); GUI.log(i18n.getMessage('pidTuningLoadedRateProfile', [self.currentRateProfile + 1])); }); }); }); const dtermTransitionNumberElement = $('input[name="dtermSetpointTransition-number"]'); const 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(FC.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 (const pidName of FC.PID_NAMES) { if ($(this).hasClass(pidName)) { const firstColumn = $(this).find('td:first'); if (!firstColumn.text()) { firstColumn.text(pidName); } } } }); // DTerm filter options function loadFilterTypeValues() { const filterTypeValues = []; filterTypeValues.push("PT1"); filterTypeValues.push("BIQUAD"); if (semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_39)) { filterTypeValues.push("FIR"); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { filterTypeValues.push("PT2"); filterTypeValues.push("PT3"); } return filterTypeValues; } function populateFilterTypeSelector(name, selectDtermValues) { const dtermFilterSelect = $(`select[name="${name}"]`); selectDtermValues.forEach(function(value, key) { dtermFilterSelect.append(``); }); } // Added in API 1.42.0 function loadDynamicNotchRangeValues() { return [ "HIGH", "MEDIUM", "LOW", "AUTO" ]; } function populateDynamicNotchRangeSelect(selectDynamicNotchRangeValues) { const dynamicNotchRangeSelect = $('select[name="dynamicNotchRange"]'); selectDynamicNotchRangeValues.forEach(function(value, key) { dynamicNotchRangeSelect.append(``); }); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_42)) { populateDynamicNotchRangeSelect(loadDynamicNotchRangeValues()); } populateFilterTypeSelector('gyroLowpassType', loadFilterTypeValues()); populateFilterTypeSelector('gyroLowpass2Type', loadFilterTypeValues()); populateFilterTypeSelector('dtermLowpassType', loadFilterTypeValues()); populateFilterTypeSelector('dtermLowpass2Type', loadFilterTypeValues()); if (semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_44)) { populateFilterTypeSelector('gyroLowpassDynType', 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); const pidController_e = $('select[name="controller"]'); if (semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_31)) { let pidControllerList; if (semver.lt(FC.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(FC.CONFIG.apiVersion, "1.20.0")) { pidControllerList = [ {name: ""}, {name: "Integer"}, {name: "Float"}, ]; } else { pidControllerList = [ {name: "Legacy"}, {name: "Betaflight"}, ]; } for (let i = 0; i < pidControllerList.length; i++) { pidController_e.append(``); } if (semver.gte(FC.CONFIG.apiVersion, CONFIGURATOR.API_VERSION_MIN_SUPPORTED_PID_CONTROLLER_CHANGE)) { pidController_e.val(FC.PID.controller); self.updatePidControllerParameters(); } else { GUI.log(i18n.getMessage('pidTuningUpgradeFirmwareToChangePidController', [FC.CONFIG.apiVersion, CONFIGURATOR.API_VERSION_MIN_SUPPORTED_PID_CONTROLLER_CHANGE])); pidController_e.empty(); pidController_e.append(''); pidController_e.prop('disabled', true); } } else { $('.tab-pid_tuning div.controller').hide(); self.updatePidControllerParameters(); } if (semver.lt(FC.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(FC.CONFIG.apiVersion, API_VERSION_1_37)) { $('.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 const rcCurveElement = $('.rate_curve canvas#rate_curve_layer0').get(0); const curveContext = rcCurveElement.getContext("2d"); let updateNeeded = true; let 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 const targetElement = $(event.target); let 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(FC.CONFIG.apiVersion, "1.16.0")) { self.currentRates.rc_rate_yaw = targetValue; } if (targetElement.attr('name') === 'roll_pitch_rate' && semver.lt(FC.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(FC.CONFIG.apiVersion, API_VERSION_1_37)) { self.currentRates.rc_rate_pitch = targetValue; } if (targetElement.attr('name') === 'rc_expo' && semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_37)) { self.currentRates.rc_pitch_expo = targetValue; } if (targetElement.attr('id') === 'ratesType' && semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_43)) { self.changeRatesType(targetValue); updateNeeded = true; } } else { // no event was passed, just force a graph update updateNeeded = true; } if (updateNeeded) { const curveHeight = rcCurveElement.height; const curveWidth = rcCurveElement.width; const 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', function () { const element = $(this); FC.FEATURE_CONFIG.features.updateData(element); 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) { const 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 const throttleMidE = $('.throttle input[name="mid"]'); const throttleExpoE = $('.throttle input[name="expo"]'); const mid = parseFloat(throttleMidE.val()); const expo = parseFloat(throttleExpoE.val()); const throttleCurve = $('.throttle .throttle_curve canvas').get(0); const 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); const canvasHeight = throttleCurve.height; const canvasWidth = throttleCurve.width; // math magic by englishman const midx = canvasWidth * mid; const midxl = midx * 0.5; const midxr = (((canvasWidth - midx) * 0.5) + midx); const midy = canvasHeight - (midx * (canvasHeight / canvasWidth)); const midyl = canvasHeight - ((canvasHeight - midy) * 0.5 *(expo + 1)); const midyr = (midy / 2) * (expo + 1); let thrPercent = (FC.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')); }); }); // exclude integratedYaw from setDirty for 4.3 as it uses RP mode. $('#pid-tuning').find('input').each(function (k, item) { if ($(item).attr('class') !== "feature toggle" && $(item).attr('class') !== "nonProfile") { $(item).change(function () { self.setDirty(true); }); } }); const dialogCopyProfile = $('.dialogCopyProfile')[0]; const DIALOG_MODE_PROFILE = 0; const DIALOG_MODE_RATEPROFILE = 1; let dialogCopyProfileMode; if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_36)) { const selectProfile = $('.selectProfile'); const selectRateProfile = $('.selectRateProfile'); $.each(selectProfileValues, function(key, value) { if (key !== FC.CONFIG.profile) { selectProfile.append(new Option(value, key)); } }); $.each(selectRateProfileValues, function(key, value) { if (key !== FC.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: FC.COPY_PROFILE.type = DIALOG_MODE_PROFILE; // 0 = pid profile FC.COPY_PROFILE.dstProfile = parseInt(selectProfile.val()); FC.COPY_PROFILE.srcProfile = FC.CONFIG.profile; MSP.send_message(MSPCodes.MSP_COPY_PROFILE, mspHelper.crunch(MSPCodes.MSP_COPY_PROFILE), false, close_dialog); break; case DIALOG_MODE_RATEPROFILE: FC.COPY_PROFILE.type = DIALOG_MODE_RATEPROFILE; // 1 = rate profile FC.COPY_PROFILE.dstProfile = parseInt(selectRateProfile.val()); FC.COPY_PROFILE.srcProfile = FC.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(); } /* * TuningSliders */ if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_42)) { // filter and tuning sliders TuningSliders.initialize(); // UNSCALED non expert slider constrain values const NON_EXPERT_SLIDER_MAX = semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44) ? 1.4 : 1.25; const NON_EXPERT_SLIDER_MIN = 0.7; const SLIDER_STEP_LOWER = 0.05; const SLIDER_STEP_UPPER = semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44) ? 0.05 : 0.1; const sliderPidsModeSelect = $('#sliderPidsModeSelect'); const sliderGyroFilterModeSelect = $('#sliderGyroFilterModeSelect'); const sliderDTermFilterModeSelect = $('#sliderDTermFilterModeSelect'); const useIntegratedYaw = $('input[id="useIntegratedYaw"]'); useIntegratedYaw.on('change', () => { if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { // set slider to RP mode if Integrated Yaw is enabled and sliders are enabled if (useIntegratedYaw.is(':checked') && TuningSliders.sliderPidsMode) { sliderPidsModeSelect.val(1).trigger('change'); } } else { // disable sliders if Integrated Yaw is enabled or Slider PID mode is set to OFF TuningSliders.updatePidSlidersDisplay(); } }); // trigger Slider Display update when PID / Filter mode is changed if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { sliderPidsModeSelect.on('change', function () { const setMode = parseInt($(this).val()); TuningSliders.sliderPidsMode = setMode; TuningSliders.calculateNewPids(); TuningSliders.updatePidSlidersDisplay(); // disable Integrated Yaw when going into RPY mode if (setMode === 2) { useIntegratedYaw.prop('checked', false).trigger('change'); } }); sliderGyroFilterModeSelect.change(function() { const mode = parseInt($(this).find(':selected').val()); if (mode === 1) { TuningSliders.gyroFilterSliderEnable(); } else { TuningSliders.gyroFilterSliderDisable(); } }); sliderDTermFilterModeSelect.change(function() { const mode = parseInt($(this).find(':selected').val()); if (mode !== 0) { TuningSliders.dtermFilterSliderEnable(); } else { TuningSliders.dtermFilterSliderDisable(); } }); } let allPidTuningSliders; if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { allPidTuningSliders = $('#sliderMasterMultiplier, #sliderDGain, #sliderPIGain, #sliderFeedforwardGain, #sliderIGain, #sliderDMaxGain, #sliderRollPitchRatio, #sliderPitchPIGain'); $('.tab-pid-tuning .legacySlider').hide(); } else { allPidTuningSliders = $('#sliderMasterMultiplierLegacy, #sliderPDRatio, #sliderPDGain, #sliderFeedforwardGainLegacy'); $('.tab-pid_tuning .advancedSlider').hide(); $('.tab-pid-tuning .baseSlider').hide(); $('.tab-pid_tuning .sliderMode').hide(); } allPidTuningSliders.on('input mouseup', function() { const slider = $(this); if (semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_44)) { if (slider.val() >= 1) { slider.attr('step', SLIDER_STEP_LOWER); } else { slider.attr('step', SLIDER_STEP_UPPER); } } 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); } } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { const sliderValue = isInt(slider.val()) ? parseInt(slider.val()) : parseFloat(slider.val()); if (slider.is('#sliderDGain')) { TuningSliders.sliderDGain = sliderValue; } else if (slider.is('#sliderPIGain')) { TuningSliders.sliderPIGain = sliderValue; } else if (slider.is('#sliderFeedforwardGain')) { TuningSliders.sliderFeedforwardGain = sliderValue; } else if (slider.is('#sliderDMaxGain')) { TuningSliders.sliderDMaxGain = sliderValue; } else if (slider.is('#sliderIGain')) { TuningSliders.sliderIGain = sliderValue; } else if (slider.is('#sliderRollPitchRatio')) { TuningSliders.sliderRollPitchRatio = sliderValue; } else if (slider.is('#sliderPitchPIGain')) { TuningSliders.sliderPitchPIGain = sliderValue; } else if (slider.is('#sliderMasterMultiplier')) { TuningSliders.sliderMasterMultiplier = sliderValue; } } else { const sliderValue = TuningSliders.scaleSliderValue(slider.val()); if (slider.is('#sliderMasterMultiplierLegacy')) { TuningSliders.sliderMasterMultiplierLegacy = sliderValue; } else if (slider.is('#sliderPDRatio')) { TuningSliders.sliderPDRatio = sliderValue; } else if (slider.is('#sliderPDGain')) { TuningSliders.sliderPDGain = sliderValue; } else if (slider.is('#sliderFeedforwardGainLegacy')) { TuningSliders.sliderFeedforwardGainLegacy = sliderValue; } } self.calculateNewPids(); self.analyticsChanges['PidTuningSliders'] = "On"; }); // reset to middle with double click allPidTuningSliders.dblclick(function() { const slider = $(this); let value; if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { if (slider.is('#sliderDGain')) { value = FC.DEFAULT_TUNING_SLIDERS.slider_d_gain / 100; TuningSliders.sliderDGain = value; } else if (slider.is('#sliderPIGain')) { value = FC.DEFAULT_TUNING_SLIDERS.slider_pi_gain / 100; TuningSliders.sliderPIGain = value; } else if (slider.is('#sliderFeedforwardGain')) { value = FC.DEFAULT_TUNING_SLIDERS.slider_feedforward_gain / 100; TuningSliders.sliderFeedforwardGain = value; } else if (slider.is('#sliderDMaxGain')) { value = FC.DEFAULT_TUNING_SLIDERS.slider_dmax_gain / 100; TuningSliders.sliderDMaxGain = value; } else if (slider.is('#sliderIGain')) { value = FC.DEFAULT_TUNING_SLIDERS.slider_i_gain / 100; TuningSliders.sliderIGain = value; } else if (slider.is('#sliderRollPitchRatio')) { value = FC.DEFAULT_TUNING_SLIDERS.slider_roll_pitch_ratio / 100; TuningSliders.sliderRollPitchRatio = value; } else if (slider.is('#sliderPitchPIGain')) { value = FC.DEFAULT_TUNING_SLIDERS.slider_pitch_pi_gain / 100; TuningSliders.sliderPitchPIGain = value; } else if (slider.is('#sliderMasterMultiplier')) { value = FC.DEFAULT_TUNING_SLIDERS.slider_master_multiplier / 100; TuningSliders.sliderMasterMultiplier = value; } } else { value = 1; if (slider.is('#sliderMasterMultiplierLegacy')) { TuningSliders.sliderMasterMultiplierLegacy = 1; } else if (slider.is('#sliderPDRatio')) { TuningSliders.sliderPDRatio = 1; } else if (slider.is('#sliderPDGain')) { TuningSliders.sliderPDGain = 1; } else if (slider.is('#sliderFeedforwardGainLegacy')) { TuningSliders.sliderFeedforwardGainLegacy = 1; } } slider.val(value); self.calculateNewPids(); }); // enable filter sliders inputs const allFilterTuningSliders = $('#sliderGyroFilterMultiplier, #sliderDTermFilterMultiplier'); allFilterTuningSliders.on('input mouseup', function() { const slider = $(this); if (!TuningSliders.expertMode) { if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { const NON_EXPERT_SLIDER_MIN_GYRO = 0.5; const NON_EXPERT_SLIDER_MAX_GYRO = 1.5; const NON_EXPERT_SLIDER_MIN_DTERM = 0.8; const NON_EXPERT_SLIDER_MAX_DTERM = 1.2; if (slider.is('#sliderGyroFilterMultiplier')) { if (slider.val() > NON_EXPERT_SLIDER_MAX_GYRO) { slider.val(NON_EXPERT_SLIDER_MAX_GYRO); } else if (slider.val() < NON_EXPERT_SLIDER_MIN_GYRO) { slider.val(NON_EXPERT_SLIDER_MIN_GYRO); } } else if (slider.is('#sliderDTermFilterMultiplier')) { if (slider.val() > NON_EXPERT_SLIDER_MAX_DTERM) { slider.val(NON_EXPERT_SLIDER_MAX_DTERM); } else if (slider.val() < NON_EXPERT_SLIDER_MIN_DTERM) { slider.val(NON_EXPERT_SLIDER_MIN_DTERM); } } } else { 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); } } } let sliderValue = isInt(slider.val()) ? parseInt(slider.val()) : parseFloat(slider.val()); if (semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_44)) { sliderValue = TuningSliders.scaleSliderValue(slider.val()); } if (slider.is('#sliderGyroFilterMultiplier')) { TuningSliders.sliderGyroFilterMultiplier = sliderValue; self.calculateNewGyroFilters(); self.analyticsChanges['GyroFilterTuningSlider'] = "On"; } else if (slider.is('#sliderDTermFilterMultiplier')) { TuningSliders.sliderDTermFilterMultiplier = sliderValue; self.calculateNewDTermFilters(); self.analyticsChanges['DTermFilterTuningSlider'] = "On"; } }); // reset to middle with double click allFilterTuningSliders.dblclick(function() { const slider = $(this); slider.val(1); if (slider.is('#sliderGyroFilterMultiplier')) { TuningSliders.sliderGyroFilterMultiplier = 1; self.calculateNewGyroFilters(); } else if (slider.is('#sliderDTermFilterMultiplier')) { TuningSliders.sliderDTermFilterMultiplier = 1; self.calculateNewDTermFilters(); } }); // update on filter value or type changes $('.pid_filter tr:not(.newFilter) input, .pid_filter tr:not(.newFilter) select').on('input', function(e) { if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { // because legacy / firmware slider inputs for lowpass1 are duplicate the value isn't updated so set it here. if (e.target.type === 'number') { $(`.pid_filter input[name="${e.target.name}"]`).val(e.target.value); } else if (e.target.type === 'select-one') { $(`.pid_filter select[name="${e.target.name}"]`).val(e.target.value); } } else { TuningSliders.updateFilterSlidersDisplay(); } if (TuningSliders.GyroSliderUnavailable) { self.analyticsChanges['GyroFilterTuningSlider'] = "Off"; } if (TuningSliders.DTermSliderUnavailable) { self.analyticsChanges['DTermFilterTuningSlider'] = "Off"; } }); // update on filter switch changes $('.pid_filter tr:not(.newFilter) .inputSwitch input').change(() => { $('.pid_filter input').triggerHandler('input'); self.setDirty(true); }); $('.tuningHelp').hide(); // LEGACY SLIDERS CODE if (semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_44)) { $('#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) { FC.ADVANCED_TUNING.dMinRoll = FC.PIDS[0][2]; FC.ADVANCED_TUNING.dMinPitch = FC.PIDS[1][2]; FC.ADVANCED_TUNING.dMinYaw = FC.PIDS[2][2]; } else { FC.PIDS[0][2] = FC.ADVANCED_TUNING.dMinRoll; FC.PIDS[1][2] = FC.ADVANCED_TUNING.dMinPitch; FC.PIDS[2][2] = FC.ADVANCED_TUNING.dMinYaw; } TuningSliders.calculateNewPids(); } }); allPidTuningSliders.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(); }); // enable PID sliders button (legacy) $('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(); TuningSliders.updatePidSlidersDisplay(); // disable integrated yaw when enabling sliders if ($('input[id="useIntegratedYaw"]').is(':checked')) { $('input[id="useIntegratedYaw"]').prop('checked', true).click(); } self.analyticsChanges['PidTuningSliders'] = "On"; }); // enable Filter sliders button (legacy sliders) $('a.buttonFilterTuningSliders').click(function() { if (TuningSliders.GyroSliderUnavailable) { // 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.DTermSliderUnavailable) { $('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"; }); } } else { // semver.lt API_VERSION_1_42 $('.tuningPIDSliders').hide(); $('.tuningFilterSliders').hide(); $('.slidersDisabled').hide(); $('.slidersWarning').hide(); $('.nonExpertModeSlidersNote').hide(); $('.tuningHelpSliders').hide(); } if (semver.gte(FC.CONFIG.apiVersion, "1.16.0")) { $('#pid-tuning .delta select').change(function() { self.setDirty(true); }); } if (semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_31)) { 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 () { let promise; if (semver.gte(FC.CONFIG.apiVersion, CONFIGURATOR.API_VERSION_MIN_SUPPORTED_PID_CONTROLLER_CHANGE) && semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_31)) { FC.PID.controller = pidController_e.val(); promise = MSP.promise(MSPCodes.MSP_SET_PID_CONTROLLER, mspHelper.crunch(MSPCodes.MSP_SET_PID_CONTROLLER)); } else { console.log(`Unsupported API version: ${FC.CONFIG.apiVersion}`); } return promise; }) .then(() => MSP.promise(MSPCodes.MSP_SET_PID, mspHelper.crunch(MSPCodes.MSP_SET_PID))) .then(() => MSP.promise(MSPCodes.MSP_SET_PID_ADVANCED, mspHelper.crunch(MSPCodes.MSP_SET_PID_ADVANCED))) .then(() => { self.updatePIDColors(); return MSP.promise(MSPCodes.MSP_SET_FILTER_CONFIG, mspHelper.crunch(MSPCodes.MSP_SET_FILTER_CONFIG)); }) .then(() => MSP.promise(MSPCodes.MSP_SET_RC_TUNING, mspHelper.crunch(MSPCodes.MSP_SET_RC_TUNING))) .then(() => MSP.promise(MSPCodes.MSP_SET_FEATURE_CONFIG, mspHelper.crunch(MSPCodes.MSP_SET_FEATURE_CONFIG))) .then(() => semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44) ? MSP.promise(MSPCodes.MSP_SET_SIMPLIFIED_TUNING, mspHelper.crunch(MSPCodes.MSP_SET_SIMPLIFIED_TUNING)) : true) .then(() => MSP.promise(MSPCodes.MSP_EEPROM_WRITE)) .then(() => { self.updating = false; self.setDirty(false); GUI.log(i18n.getMessage('pidTuningEepromSaved')); self.refresh(); }); analytics.sendSaveAndChangeEvents(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, self.analyticsChanges, 'pid_tuning'); 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); self.analyticsChanges = {}; GUI.content_ready(callback); TABS.pid_tuning.isHtmlProcessing = false; } }; 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 (FC.RC.channels[0] && FC.RC.channels[1] && FC.RC.channels[2]) { const delta = this.clock.getDelta(); const roll = delta * this.rateCurve.rcCommandRawToDegreesPerSecond( FC.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, ); const pitch = delta * this.rateCurve.rcCommandRawToDegreesPerSecond( FC.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, ); const yaw = delta * this.rateCurve.rcCommandRawToDegreesPerSecond( FC.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) { const 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) { const self = this; GUI.tab_switch_cleanup(function () { self.initialize(); self.setDirty(false); if (callback) { callback(); } }); }; TABS.pid_tuning.setProfile = function () { const self = this; self.currentProfile = FC.CONFIG.profile; $('.tab-pid_tuning select[name="profile"]').val(self.currentProfile); }; TABS.pid_tuning.setRateProfile = function () { const self = this; self.currentRateProfile = FC.CONFIG.rateProfile; $('.tab-pid_tuning select[name="rate_profile"]').val(self.currentRateProfile); }; TABS.pid_tuning.setDirty = function (isDirty) { const self = this; self.dirty = isDirty; $('.tab-pid_tuning select[name="profile"]').prop('disabled', isDirty); if (semver.gte(FC.CONFIG.apiVersion, "1.20.0")) { $('.tab-pid_tuning select[name="rate_profile"]').prop('disabled', isDirty); } }; TABS.pid_tuning.checkUpdateProfile = function (updateRateProfile) { const self = this; if (GUI.active_tab === 'pid_tuning') { if (!self.updating && !self.dirty) { let changedProfile = false; if (self.currentProfile !== FC.CONFIG.profile) { self.setProfile(); changedProfile = true; } let changedRateProfile = false; if (semver.gte(FC.CONFIG.apiVersion, "1.20.0") && updateRateProfile && self.currentRateProfile !== FC.CONFIG.rateProfile) { self.setRateProfile(); changedRateProfile = true; } if (changedProfile || changedRateProfile) { self.updating = true; self.refresh(function () { self.updating = false; if (changedProfile) { GUI.log(i18n.getMessage('pidTuningReceivedProfile', [FC.CONFIG.profile + 1])); FC.CONFIG.profile = self.currentProfile; } if (changedRateProfile) { GUI.log(i18n.getMessage('pidTuningReceivedRateProfile', [FC.CONFIG.rateProfile + 1])); FC.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 = [FC.RC.channels[0], FC.RC.channels[1], FC.RC.channels[2]]; } // Monitor FC.RC.channels and detect change of value; let rateCurveUpdateRequired = false; for (let i = 0; i < this.oldRC.length; i++) { // has the value changed ? if (this.oldRC[i] !== FC.RC.channels[i]) { this.oldRC[i] = FC.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 = FC.RC.channels[3]; return true; } const updateRequired = this.oldThrottle !== FC.RC.channels[3]; this.oldThrottle = FC.RC.channels[3]; return updateRequired; }; TABS.pid_tuning.updatePidControllerParameters = function () { if (semver.gte(FC.CONFIG.apiVersion, "1.20.0") && semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_31) && $('.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(FC.CONFIG.apiVersion, API_VERSION_1_40)) { $('#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() { const self = this; if (!self.rateCurve.useLegacyCurve && self.rateCurve.maxAngularVel) { const drawAxisLabel = function(context, axisLabel, x, y, align, color) { context.fillStyle = color || '#000000' ; context.textAlign = align || 'center'; context.fillText(axisLabel, x, y); }; const 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 (let 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}); const 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'}, }; const rcStickElement = $('.rate_curve canvas#rate_curve_layer1').get(0); if (rcStickElement) { rcStickElement.width = 1000; rcStickElement.height = 1000; const stickContext = rcStickElement.getContext("2d"); stickContext.save(); const maxAngularVelRoll = `${self.maxAngularVelRollElement.text()} deg/s`; const maxAngularVelPitch = `${self.maxAngularVelPitchElement.text()} deg/s`; const maxAngularVelYaw = `${self.maxAngularVelYawElement.text()} deg/s`; let currentValues = []; let balloonsDirty = []; const curveHeight = rcStickElement.height; const curveWidth = rcStickElement.width; const maxAngularVel = self.rateCurve.maxAngularVel; const windowScale = (400 / stickContext.canvas.clientHeight); const rateScale = (curveHeight / 2) / maxAngularVel; const lineScale = stickContext.canvas.width / stickContext.canvas.clientWidth; const 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 (FC.RC.channels[0] && FC.RC.channels[1] && FC.RC.channels[2]) { currentValues.push(`${self.rateCurve.drawStickPosition(FC.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(FC.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(FC.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 const 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 (const balloon of balloons) { balloon.balloon(); } stickContext.restore(); } } }; TABS.pid_tuning.calculateNewPids = function() { if (!TABS.pid_tuning.isHtmlProcessing) { TuningSliders.calculateNewPids(); } }; TABS.pid_tuning.calculateNewGyroFilters = function() { if (!TABS.pid_tuning.isHtmlProcessing) { if (TuningSliders.sliderGyroFilter) { TuningSliders.calculateNewGyroFilters(); } } }; TABS.pid_tuning.calculateNewDTermFilters = function() { if (!TABS.pid_tuning.isHtmlProcessing) { if (TuningSliders.sliderDTermFilter) { TuningSliders.calculateNewDTermFilters(); } } }; TABS.pid_tuning.updateFilterWarning = function() { const gyroLowpassFilterMode = parseInt($('.pid_filter select[name="gyroLowpassFilterMode"]').val()); const gyroDynamicLowpassEnabled = gyroLowpassFilterMode === 1; const gyroLowpass1Enabled = !gyroLowpassFilterMode; const dtermLowpassFilterMode = parseInt($('.pid_filter select[name="dtermLowpassFilterMode"]').val()); const dtermDynamicLowpassEnabled = dtermLowpassFilterMode === 1; const dtermLowpass1Enabled = !dtermLowpassFilterMode; const warningE = $('#pid-tuning .filterWarning'); const warningDynamicNotchE = $('#pid-tuning .dynamicNotchWarning'); if (!(gyroDynamicLowpassEnabled || gyroLowpass1Enabled) || !(dtermDynamicLowpassEnabled || dtermLowpass1Enabled)) { warningE.show(); } else { warningE.hide(); } if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_42) && semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_44)) { if (FC.FEATURE_CONFIG.features.isEnabled('DYNAMIC_FILTER')) { warningDynamicNotchE.hide(); } else { warningDynamicNotchE.show(); } } else { warningDynamicNotchE.hide(); } }; TABS.pid_tuning.updatePIDColors = function(clear = false) { if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { return; } 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) }); }; FC.PID_NAMES.forEach(function(elementPid, indexPid) { $(`.pid_tuning .${elementPid} input`).each(function(indexInput) { setTuningElementColor($(this), FC.PIDS_ACTIVE[indexPid][indexInput], FC.PIDS[indexPid][indexInput]); }); }); setTuningElementColor($('.pid_tuning input[name="dMinRoll"]'), FC.ADVANCED_TUNING_ACTIVE.dMinRoll, FC.ADVANCED_TUNING.dMinRoll); setTuningElementColor($('.pid_tuning input[name="dMinPitch"]'), FC.ADVANCED_TUNING_ACTIVE.dMinPitch, FC.ADVANCED_TUNING.dMinPitch); setTuningElementColor($('.pid_tuning input[name="dMinYaw"]'), FC.ADVANCED_TUNING_ACTIVE.dMinYaw, FC.ADVANCED_TUNING.dMinYaw); setTuningElementColor($('.pid_tuning .ROLL input[name="f"]'), FC.ADVANCED_TUNING_ACTIVE.feedforwardRoll, FC.ADVANCED_TUNING.feedforwardRoll); setTuningElementColor($('.pid_tuning .PITCH input[name="f"]'), FC.ADVANCED_TUNING_ACTIVE.feedforwardPitch, FC.ADVANCED_TUNING.feedforwardPitch); setTuningElementColor($('.pid_tuning .YAW input[name="f"]'), FC.ADVANCED_TUNING_ACTIVE.feedforwardYaw, FC.ADVANCED_TUNING.feedforwardYaw); }; TABS.pid_tuning.changeRatesType = function(rateTypeID) { const self = this; const dialogRatesType = $('.dialogRatesType')[0]; if (self.previousRatesType == null) { self.currentRatesType = rateTypeID; self.changeRatesTypeLogo(); self.changeRatesSystem(true); self.previousRatesType = self.currentRatesType; return; } if (!dialogRatesType.hasAttribute('open')) { dialogRatesType.showModal(); $('.dialogRatesType-cancelbtn').click(function() { $('.rates_type select[id="ratesType"]').val(self.currentRatesType); self.previousRatesType = self.currentRatesType; dialogRatesType.close(); }); $('.dialogRatesType-confirmbtn').click(function() { self.currentRatesType = rateTypeID; self.changeRatesTypeLogo(); self.changeRatesSystem(false); self.previousRatesType = self.currentRatesType; dialogRatesType.close(); FC.RC_TUNING.rates_type = self.currentRatesType; self.currentRates = self.rateCurve.getCurrentRates(); }); } }; TABS.pid_tuning.changeRatesSystem = function(sameType) { const self = this; let rcRateMax = 2.55, rcRateMin = 0.01, rcRateStep = 0.01; let rateMax = 1.0, rateStep = 0.01; let expoMax = 1.0, expoStep = 0.01; const rateMin = 0; const expoMin = 0; 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(FC.RC_TUNING.pitch_rate.toFixed(2)); roll_rate_e.val(FC.RC_TUNING.roll_rate.toFixed(2)); yaw_rate_e.val(FC.RC_TUNING.yaw_rate.toFixed(2)); rc_rate_pitch_e.val(FC.RC_TUNING.rcPitchRate.toFixed(2)); rc_rate_e.val(FC.RC_TUNING.RC_RATE.toFixed(2)); rc_rate_yaw_e.val(FC.RC_TUNING.rcYawRate.toFixed(2)); rc_pitch_expo_e.val(FC.RC_TUNING.RC_PITCH_EXPO.toFixed(2)); rc_expo_e.val(FC.RC_TUNING.RC_EXPO.toFixed(2)); rc_yaw_expo_e.val(FC.RC_TUNING.RC_YAW_EXPO.toFixed(2)); } switch (self.currentRatesType) { case FC.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((FC.RC_TUNING.pitch_rate * 100).toFixed(0)); roll_rate_e.val((FC.RC_TUNING.roll_rate * 100).toFixed(0)); yaw_rate_e.val((FC.RC_TUNING.yaw_rate * 100).toFixed(0)); rc_rate_pitch_e.val((FC.RC_TUNING.rcPitchRate * 1000).toFixed(0)); rc_rate_e.val((FC.RC_TUNING.RC_RATE * 1000).toFixed(0)); rc_rate_yaw_e.val((FC.RC_TUNING.rcYawRate * 1000).toFixed(0)); rc_pitch_expo_e.val((FC.RC_TUNING.RC_PITCH_EXPO * 100).toFixed(0)); rc_expo_e.val((FC.RC_TUNING.RC_EXPO * 100).toFixed(0)); rc_yaw_expo_e.val((FC.RC_TUNING.RC_YAW_EXPO * 100).toFixed(0)); } else { rcRateDefault = (370).toFixed(0); rateDefault = (80).toFixed(0); expoDefault = (50).toFixed(0); } break; case FC.RATES_TYPE.KISS: rcRateLabel.text(i18n.getMessage("pidTuningRcRate")); rateLabel.text(i18n.getMessage("pidTuningRcRateRaceflight")); rcExpoLabel.text(i18n.getMessage("pidTuningRcExpoKISS")); rateMax = 0.99; break; case FC.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((FC.RC_TUNING.pitch_rate * 1000).toFixed(0)); roll_rate_e.val((FC.RC_TUNING.roll_rate * 1000).toFixed(0)); yaw_rate_e.val((FC.RC_TUNING.yaw_rate * 1000).toFixed(0)); rc_rate_pitch_e.val((FC.RC_TUNING.rcPitchRate * 1000).toFixed(0)); rc_rate_e.val((FC.RC_TUNING.RC_RATE * 1000).toFixed(0)); rc_rate_yaw_e.val((FC.RC_TUNING.rcYawRate * 1000).toFixed(0)); } else if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) { rcRateDefault = (70).toFixed(0); rateDefault = (670).toFixed(0); expoDefault = (0).toFixed(2); } else { rcRateDefault = (200).toFixed(0); rateDefault = (670).toFixed(0); expoDefault = (0.54).toFixed(2); } break; case FC.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((FC.RC_TUNING.pitch_rate * 1000).toFixed(0)); roll_rate_e.val((FC.RC_TUNING.roll_rate * 1000).toFixed(0)); yaw_rate_e.val((FC.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() { const self = this; const ratesLogoElement = $('.rates_type img[id="ratesLogo"]'); switch (self.currentRatesType) { case FC.RATES_TYPE.RACEFLIGHT: ratesLogoElement.attr("src", "./images/rate_logos/raceflight.svg"); break; case FC.RATES_TYPE.KISS: ratesLogoElement.attr("src", "./images/rate_logos/kiss.svg"); break; case FC.RATES_TYPE.ACTUAL: ratesLogoElement.attr("src", "./images/rate_logos/actual.svg"); break; case FC.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; } }; TABS.pid_tuning.expertModeChanged = function(expertModeEnabled) { TuningSliders.setExpertMode(expertModeEnabled); };