mirror of
https://github.com/betaflight/betaflight-configurator.git
synced 2025-07-23 00:05:22 +03:00
Merge pull request #2623 from haslinghuis/handle_expert_settings
This commit is contained in:
commit
bd2640b2af
5 changed files with 126 additions and 84 deletions
|
@ -45,7 +45,7 @@ const D_MIN_RATIO = 0.85;
|
|||
|
||||
TuningSliders.saveInitialSettings = function () {
|
||||
if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) {
|
||||
this.initialSettings.sliderPidsModeSelect = FC.TUNING_SLIDERS.slider_pids_mode;
|
||||
this.initialSettings.sliderPidsMode = FC.TUNING_SLIDERS.slider_pids_mode;
|
||||
this.initialSettings.sliderDGain = FC.TUNING_SLIDERS.slider_d_gain / 100;
|
||||
this.initialSettings.sliderPIGain = FC.TUNING_SLIDERS.slider_pi_gain / 100;
|
||||
this.initialSettings.sliderFeedforwardGain = FC.TUNING_SLIDERS.slider_feedforward_gain / 100;
|
||||
|
@ -63,7 +63,7 @@ TuningSliders.saveInitialSettings = function () {
|
|||
|
||||
TuningSliders.restoreInitialSettings = function () {
|
||||
if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) {
|
||||
FC.TUNING_SLIDERS.slider_pids_mode = this.initialSettings.sliderPidsModeSelect;
|
||||
FC.TUNING_SLIDERS.slider_pids_mode = this.initialSettings.sliderPidsMode;
|
||||
|
||||
FC.TUNING_SLIDERS.slider_d_gain = Math.round(this.initialSettings.sliderDGain * 20) * 5;
|
||||
FC.TUNING_SLIDERS.slider_pi_gain = Math.round(this.initialSettings.sliderPIGain * 20) * 5;
|
||||
|
@ -126,17 +126,68 @@ TuningSliders.initialize = function() {
|
|||
this.updateFilterSlidersDisplay();
|
||||
};
|
||||
|
||||
TuningSliders.updateExpertModeSlidersDisplay = function() {
|
||||
const NON_EXPERT_SLIDER_MIN = 70;
|
||||
const NON_EXPERT_SLIDER_MAX = 140;
|
||||
const NON_EXPERT_SLIDER_MIN_GYRO = 50;
|
||||
const NON_EXPERT_SLIDER_MAX_GYRO = 150;
|
||||
const NON_EXPERT_SLIDER_MIN_DTERM = 80;
|
||||
const NON_EXPERT_SLIDER_MAX_DTERM = 120;
|
||||
|
||||
const dGain = FC.TUNING_SLIDERS.slider_d_gain < NON_EXPERT_SLIDER_MIN || FC.TUNING_SLIDERS.slider_d_gain > NON_EXPERT_SLIDER_MAX;
|
||||
const piGain = FC.TUNING_SLIDERS.slider_pi_gain < NON_EXPERT_SLIDER_MIN || FC.TUNING_SLIDERS.slider_pi_gain > NON_EXPERT_SLIDER_MAX;
|
||||
const ffGain = FC.TUNING_SLIDERS.slider_feedforward_gain < NON_EXPERT_SLIDER_MIN || FC.TUNING_SLIDERS.slider_feedforward_gain > NON_EXPERT_SLIDER_MAX;
|
||||
|
||||
const dMaxGain = FC.TUNING_SLIDERS.slider_dmax_gain !== FC.DEFAULT_TUNING_SLIDERS.slider_dmax_gain;
|
||||
const iGain = FC.TUNING_SLIDERS.slider_i_gain !== FC.DEFAULT_TUNING_SLIDERS.slider_i_gain;
|
||||
const rpRatio = FC.TUNING_SLIDERS.slider_roll_pitch_ratio !== FC.DEFAULT_TUNING_SLIDERS.slider_roll_pitch_ratio;
|
||||
const rpIGain = FC .TUNING_SLIDERS.slider_pitch_pi_gain !== FC.DEFAULT_TUNING_SLIDERS.slider_pitch_pi_gain;
|
||||
const master = FC.TUNING_SLIDERS.slider_master_multiplier !== FC.DEFAULT_TUNING_SLIDERS.slider_master_multiplier;
|
||||
|
||||
const gyro = FC.TUNING_SLIDERS.slider_gyro_filter_multiplier < NON_EXPERT_SLIDER_MIN_GYRO || FC.TUNING_SLIDERS.slider_gyro_filter_multiplier > NON_EXPERT_SLIDER_MAX_GYRO;
|
||||
const dterm = FC.TUNING_SLIDERS.slider_dterm_filter_multiplier < NON_EXPERT_SLIDER_MIN_DTERM || FC.TUNING_SLIDERS.slider_dterm_filter_multiplier > NON_EXPERT_SLIDER_MAX_DTERM;
|
||||
|
||||
const basic = dGain || piGain || ffGain;
|
||||
const advanced = dMaxGain || iGain || rpRatio || rpIGain || master;
|
||||
|
||||
$('#sliderDGain').prop('disabled', dGain && !this.expertMode);
|
||||
$('#sliderPIGain').prop('disabled', piGain && !this.expertMode);
|
||||
$('#sliderFeedforwardGain').prop('disabled', ffGain && !this.expertMode);
|
||||
|
||||
$('#sliderDMaxGain').prop('disabled', !this.expertMode);
|
||||
$('#sliderIGain').prop('disabled', !this.expertMode);
|
||||
$('#sliderRollPitchRatio').prop('disabled', !this.expertMode);
|
||||
$('#sliderPitchPIGain').prop('disabled', !this.expertMode);
|
||||
$('#sliderMasterMultiplier').prop('disabled', !this.expertMode);
|
||||
|
||||
$('#sliderGyroFilterMultiplier').prop('disabled', gyro && !this.expertMode);
|
||||
$('#sliderDTermFilterMultiplier').prop('disabled', dterm && !this.expertMode);
|
||||
|
||||
$('.baseSliderDGain').toggleClass('disabledSliders', dGain && !this.expertMode);
|
||||
$('.baseSliderPIGain').toggleClass('disabledSliders', piGain && !this.expertMode);
|
||||
$('.baseSliderFeedforwardGain').toggleClass('disabledSliders', ffGain && !this.expertMode);
|
||||
|
||||
$('.advancedSlider').toggleClass('disabledSliders', !this.expertMode);
|
||||
|
||||
$('.sliderGyroFilter').toggleClass('disabledSliders', gyro && !this.expertMode);
|
||||
$('.sliderDtermFilter').toggleClass('disabledSliders', dterm && !this.expertMode);
|
||||
|
||||
$('.advancedSliderDmaxGain').toggle(dMaxGain || this.expertMode);
|
||||
$('.advancedSliderIGain').toggle(iGain || this.expertMode);
|
||||
$('.advancedSliderRollPitchRatio').toggle(rpRatio || this.expertMode);
|
||||
$('.advancedSliderPitchPIGain').toggle(rpIGain || this.expertMode);
|
||||
$('.advancedSliderMaster').toggle(master || this.expertMode);
|
||||
|
||||
$('.expertSettingsDetectedNote').toggle((basic || advanced) && !this.expertMode);
|
||||
};
|
||||
|
||||
TuningSliders.setExpertMode = function(expertModeEnabled) {
|
||||
this.expertMode = expertModeEnabled;
|
||||
|
||||
if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) {
|
||||
document.getElementById('sliderDMaxGain').disabled = !this.expertMode;
|
||||
document.getElementById('sliderIGain').disabled = !this.expertMode;
|
||||
document.getElementById('sliderRollPitchRatio').disabled = !this.expertMode;
|
||||
document.getElementById('sliderPitchPIGain').disabled = !this.expertMode;
|
||||
document.getElementById('sliderMasterMultiplier').disabled = !this.expertMode;
|
||||
|
||||
$('.advancedSlider').toggleClass('disabledSliders', !this.expertMode);
|
||||
this.updateExpertModeSlidersDisplay();
|
||||
|
||||
$('.tab-pid_tuning .legacySlider').hide();
|
||||
$('.legacyNonExpertModeSlidersNote').hide();
|
||||
$('.subtab-pid .nonExpertModeSlidersNote').toggle(!this.pidSlidersUnavailable && !this.expertMode);
|
||||
|
@ -146,6 +197,7 @@ TuningSliders.setExpertMode = function(expertModeEnabled) {
|
|||
$('.tab-pid_tuning .baseSlider').hide();
|
||||
$('.tab-pid_tuning .advancedSlider').hide();
|
||||
$('.nonExpertModeSlidersNote').hide();
|
||||
$('.expertSettingsDetectedNote').hide();
|
||||
$('.subtab-pid .legacyNonExpertModeSlidersNote').toggle(!this.pidSlidersUnavailable && !this.expertMode);
|
||||
$('.subtab-filter .legacyNonExpertModeSlidersNote').toggle((!this.GyroSliderUnavailable || !this.DTermSliderUnavailable) && !this.expertMode);
|
||||
}
|
||||
|
@ -348,14 +400,6 @@ TuningSliders.legacyUpdateFilterSlidersDisplay = function() {
|
|||
}
|
||||
};
|
||||
|
||||
TuningSliders.updateSwitchBoxes = function() {
|
||||
const FF_SWITCH = FC.ADVANCED_TUNING.feedforwardRoll || FC.ADVANCED_TUNING.feedforwardPitch || FC.ADVANCED_TUNING.feedforwardYaw;
|
||||
$('input[id="feedforwardGroup"]').prop('checked', FF_SWITCH).trigger('change');
|
||||
|
||||
const DMIN_SWITCH = FC.PIDS[0][2] !== FC.ADVANCED_TUNING.dMinRoll || FC.PIDS[1][2] !== FC.ADVANCED_TUNING.dMinPitch || FC.PIDS[2][2] !== FC.ADVANCED_TUNING.dMinYaw;
|
||||
$('#dMinSwitch').prop('checked', DMIN_SWITCH).trigger('change');
|
||||
};
|
||||
|
||||
TuningSliders.updateSlidersWarning = function(slidersUnavailable = false) {
|
||||
const WARNING_P_GAIN = 70;
|
||||
let WARNING_I_GAIN = 120;
|
||||
|
@ -396,7 +440,7 @@ TuningSliders.updatePidSlidersDisplay = function() {
|
|||
|
||||
let rows = 3;
|
||||
if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) {
|
||||
rows = FC.TUNING_SLIDERS.slider_pids_mode === 1 ? 2 : 3;
|
||||
rows = this.sliderPidsMode === 1 ? 2 : 3;
|
||||
} else {
|
||||
this.calculateNewPids(true);
|
||||
}
|
||||
|
@ -494,10 +538,14 @@ TuningSliders.updateFilterSlidersDisplay = function() {
|
|||
|
||||
TuningSliders.updateFormPids = function(updateSlidersOnly = false) {
|
||||
if (!updateSlidersOnly) {
|
||||
let rows = 3;
|
||||
if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) {
|
||||
rows = this.sliderPidsMode === 1 ? 2 : 3;
|
||||
}
|
||||
FC.PID_NAMES.forEach(function (elementPid, indexPid) {
|
||||
const pidElements = $(`.pid_tuning .${elementPid} input`);
|
||||
pidElements.each(function (indexInput) {
|
||||
if (indexPid < 3 && indexInput < 3) {
|
||||
if (indexPid < rows && indexInput < rows) {
|
||||
$(this).val(FC.PIDS[indexPid][indexInput]);
|
||||
}
|
||||
});
|
||||
|
@ -591,7 +639,7 @@ TuningSliders.calculateNewPids = function(updateSlidersOnly = false) {
|
|||
// values get set both into forms and their respective variables
|
||||
|
||||
if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) {
|
||||
FC.TUNING_SLIDERS.slider_pids_mode = parseInt($('#sliderPidsModeSelect').val());
|
||||
FC.TUNING_SLIDERS.slider_pids_mode = this.sliderPidsMode;
|
||||
//rounds slider values to nearies multiple of 5 and passes to the FW. Avoid dividing calc by (* x 100)/5 = 20
|
||||
FC.TUNING_SLIDERS.slider_d_gain = Math.round(this.sliderDGain * 20) * 5;
|
||||
FC.TUNING_SLIDERS.slider_pi_gain = Math.round(this.sliderPIGain * 20) * 5;
|
||||
|
@ -608,7 +656,6 @@ TuningSliders.calculateNewPids = function(updateSlidersOnly = false) {
|
|||
.then(() => {
|
||||
this.updateFormPids(updateSlidersOnly);
|
||||
this.updateSlidersWarning();
|
||||
this.updateSwitchBoxes();
|
||||
});
|
||||
} else {
|
||||
this.legacyCalculatePids(updateSlidersOnly);
|
||||
|
|
|
@ -499,10 +499,6 @@ TABS.pid_tuning.initialize = function (callback) {
|
|||
|
||||
// Feedforward
|
||||
if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) {
|
||||
const feedforwardGroupCheck = $('input[id="feedforwardGroup"]');
|
||||
const PID_FEEDFORWARD = FC.ADVANCED_TUNING.feedforwardRoll || FC.ADVANCED_TUNING.feedforwardPitch || FC.ADVANCED_TUNING.feedforwardYaw;
|
||||
feedforwardGroupCheck.prop('checked', PID_FEEDFORWARD);
|
||||
$('.feedforwardGroupCheckbox').addClass('switchery-disabled');
|
||||
$('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);
|
||||
|
@ -529,10 +525,6 @@ TABS.pid_tuning.initialize = function (callback) {
|
|||
$('.thrustLinearization .suboption').toggle(checked);
|
||||
}).change();
|
||||
} else {
|
||||
const checkbox = document.getElementById('feedforwardGroup');
|
||||
if (checkbox.parentNode) {
|
||||
checkbox.parentNode.removeChild(checkbox);
|
||||
}
|
||||
$('.vbatSagCompensation').hide();
|
||||
$('.thrustLinearization').hide();
|
||||
|
||||
|
@ -628,9 +620,11 @@ TABS.pid_tuning.initialize = function (callback) {
|
|||
const dMinSwitch = $('#dMinSwitch');
|
||||
|
||||
if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) {
|
||||
$('.dMinGroupCheckbox').addClass('switchery-disabled');
|
||||
const box = document.getElementById('dMinSwitch');
|
||||
if (box.parentNode) {
|
||||
box.parentNode.removeChild(box);
|
||||
}
|
||||
$('.dMinDisabledNote').hide();
|
||||
self.updateGuiElements();
|
||||
} else {
|
||||
dMinSwitch.prop('checked', FC.ADVANCED_TUNING.dMinRoll > 0 || FC.ADVANCED_TUNING.dMinPitch > 0 || FC.ADVANCED_TUNING.dMinYaw > 0);
|
||||
|
||||
|
@ -668,8 +662,8 @@ TABS.pid_tuning.initialize = function (callback) {
|
|||
$('.pid_tuning input[name="dMinYaw"]').val(0);
|
||||
}
|
||||
});
|
||||
dMinSwitch.trigger('change');
|
||||
}
|
||||
dMinSwitch.trigger('change');
|
||||
}
|
||||
|
||||
$('input[id="gyroNotch1Enabled"]').change(function() {
|
||||
|
@ -861,7 +855,7 @@ TABS.pid_tuning.initialize = function (callback) {
|
|||
// Assign each value
|
||||
searchRow.each(function (indexInput) {
|
||||
if ($(this).val()) {
|
||||
FC.PIDS[indexPid][indexInput] = parseFloat($(this).val());
|
||||
FC.PIDS[indexPid][indexInput] = parseInt($(this).val());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1806,7 +1800,6 @@ TABS.pid_tuning.initialize = function (callback) {
|
|||
&& $(item).attr('class') !== "nonProfile") {
|
||||
$(item).change(function () {
|
||||
self.setDirty(true);
|
||||
self.sliderRetainConfiguration = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -1903,11 +1896,10 @@ TABS.pid_tuning.initialize = function (callback) {
|
|||
|
||||
if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) {
|
||||
if (self.sliderRetainConfiguration) {
|
||||
self.setDirty(true);
|
||||
self.setDirty(true);
|
||||
} else {
|
||||
TuningSliders.saveInitialSettings();
|
||||
}
|
||||
|
||||
sliderPidsModeSelect.val(FC.TUNING_SLIDERS.slider_pids_mode);
|
||||
} else {
|
||||
$('#dMinSwitch').change(function() {
|
||||
|
@ -1934,25 +1926,22 @@ TABS.pid_tuning.initialize = function (callback) {
|
|||
// trigger Slider Display update when PID mode is changed
|
||||
if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) {
|
||||
$('select[id="sliderPidsModeSelect"]').on('change', function () {
|
||||
const originalMode = TuningSliders.initialSettings.sliderPidsModeSelect;
|
||||
const setMode = parseInt($(this).val());
|
||||
|
||||
TuningSliders.sliderPidsMode = setMode;
|
||||
TuningSliders.calculateNewPids();
|
||||
TuningSliders.updateFormPids();
|
||||
TuningSliders.updatePidSlidersDisplay();
|
||||
|
||||
const allowRP = originalMode === 0 && setMode === 0;
|
||||
const allowRPY = originalMode < 2 && originalMode === setMode;
|
||||
const disableRP = !!setMode;
|
||||
const disableY = setMode > 1;
|
||||
|
||||
$('#pid_main .ROLL .pid_data input').each(function() {
|
||||
$(this).prop('disabled', !allowRP);
|
||||
});
|
||||
|
||||
$('#pid_main .PITCH .pid_data input').each(function() {
|
||||
$(this).prop('disabled', !allowRP);
|
||||
$('#pid_main .ROLL .pid_data input, #pid_main .PITCH .pid_data input').each(function() {
|
||||
$(this).prop('disabled', disableRP);
|
||||
});
|
||||
|
||||
$('#pid_main .YAW .pid_data input').each(function() {
|
||||
$(this).prop('disabled', !allowRPY);
|
||||
$(this).prop('disabled', disableY);
|
||||
});
|
||||
}).trigger('change');
|
||||
}
|
||||
|
@ -2019,7 +2008,6 @@ TABS.pid_tuning.initialize = function (callback) {
|
|||
}
|
||||
}
|
||||
TuningSliders.calculateNewPids();
|
||||
self.updateGuiElements();
|
||||
self.analyticsChanges['PidTuningSliders'] = "On";
|
||||
});
|
||||
if (semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_44)) {
|
||||
|
@ -2029,9 +2017,6 @@ TABS.pid_tuning.initialize = function (callback) {
|
|||
$('.pid_tuning .PITCH input[name="d"]').change();
|
||||
$('.pid_tuning .YAW input[name="d"]').change();
|
||||
});
|
||||
} else {
|
||||
TuningSliders.updatePidSlidersDisplay();
|
||||
TuningSliders.updateSlidersWarning();
|
||||
}
|
||||
// reset to middle with double click
|
||||
allPidTuningSliders.dblclick(function() {
|
||||
|
@ -2079,14 +2064,13 @@ TABS.pid_tuning.initialize = function (callback) {
|
|||
slider.val(value);
|
||||
|
||||
TuningSliders.calculateNewPids();
|
||||
TuningSliders.updatePidSlidersDisplay();
|
||||
});
|
||||
|
||||
// enable PID sliders button
|
||||
$('a.buttonPidTuningSliders').click(function() {
|
||||
// set Slider PID mode to RP(Y) when re-enabling Sliders
|
||||
if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) {
|
||||
const firmwareMode = TuningSliders.initialSettings.sliderPidsModeSelect;
|
||||
const firmwareMode = TuningSliders.initialSettings.sliderPidsMode;
|
||||
const workingMode = firmwareMode === 1 ? 1 : 2;
|
||||
|
||||
if (firmwareMode !== workingMode) {
|
||||
|
@ -2200,11 +2184,18 @@ TABS.pid_tuning.initialize = function (callback) {
|
|||
|
||||
// update on pid table inputs
|
||||
$('#pid_main input').on('input', function() {
|
||||
TuningSliders.updatePidSlidersDisplay();
|
||||
self.analyticsChanges['PidTuningSliders'] = "Off";
|
||||
if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) {
|
||||
self.sliderRetainConfiguration = true;
|
||||
} else {
|
||||
TuningSliders.updatePidSlidersDisplay();
|
||||
self.analyticsChanges['PidTuningSliders'] = "Off";
|
||||
}
|
||||
});
|
||||
// update on filter value or type changes
|
||||
$('.pid_filter tr:not(.newFilter) input, .pid_filter tr:not(.newFilter) select').on('input', function() {
|
||||
if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) {
|
||||
self.sliderRetainConfiguration = true;
|
||||
}
|
||||
TuningSliders.updateFilterSlidersDisplay();
|
||||
if (TuningSliders.GyroSliderUnavailable) {
|
||||
self.analyticsChanges['GyroFilterTuningSlider'] = "Off";
|
||||
|
@ -2729,6 +2720,10 @@ TABS.pid_tuning.updateFilterWarning = function() {
|
|||
};
|
||||
|
||||
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" });
|
||||
|
@ -2757,23 +2752,6 @@ TABS.pid_tuning.updatePIDColors = function(clear = false) {
|
|||
setTuningElementColor($('.pid_tuning .YAW input[name="f"]'), FC.ADVANCED_TUNING_ACTIVE.feedforwardYaw, FC.ADVANCED_TUNING.feedforwardYaw);
|
||||
};
|
||||
|
||||
TABS.pid_tuning.updateGuiElements = function() {
|
||||
const rollF = parseInt($('.pid_tuning .ROLL input[name="f"]').val());
|
||||
const pitchF = parseInt($('.pid_tuning .PITCH input[name="f"]').val());
|
||||
const yawF = parseInt($('.pid_tuning .YAW input[name="f"]').val());
|
||||
const FF_SWITCH = rollF || pitchF || yawF;
|
||||
$('input[id="feedforwardGroup"]').prop('checked', FF_SWITCH).trigger('change');
|
||||
|
||||
const dRoll = parseInt($('.pid_tuning .ROLL input[name="d"]').val());
|
||||
const dPitch = parseInt($('.pid_tuning .PITCH input[name="d"]').val());
|
||||
const dYaw = parseInt($('.pid_tuning .YAW input[name="d"]').val());
|
||||
const dMinRoll = parseInt($('.pid_tuning input[name="dMinRoll"]').val());
|
||||
const dMinPitch = parseInt($('.pid_tuning input[name="dMinPitch"]').val());
|
||||
const dMinYaw = parseInt($('.pid_tuning input[name="dMinYaw"]').val());
|
||||
const DMAX_GAIN_SWITCH = dRoll !== dMinRoll || dPitch !== dMinPitch || dYaw !== dMinYaw;
|
||||
$('#dMinSwitch').prop('checked', DMAX_GAIN_SWITCH).trigger('change');
|
||||
};
|
||||
|
||||
TABS.pid_tuning.changeRatesType = function(rateTypeID) {
|
||||
const self = this;
|
||||
const dialogRatesType = $('.dialogRatesType')[0];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue