'use strict';
TABS.motors = {
previousDshotBidir: null,
previousFilterDynQ: null,
previousFilterDynCount: null,
analyticsChanges: {},
configHasChanged: false,
configChanges: {},
feature3DEnabled: false,
sensor: "gyro",
sensorGyroRate: 20,
sensorGyroScale: 2000,
sensorAccelRate: 20,
sensorAccelScale: 2,
sensorSelectValues: {
"gyroScale": {
"10" : 10,
"25" : 25,
"50" : 50,
"100" : 100,
"200" : 200,
"300" : 300,
"400" : 400,
"500" : 500,
"1000" : 1000,
"2000" : 2000,
},
"accelScale": {
"0.05" : 0.05,
"0.1" : 0.1,
"0.2" : 0.2,
"0.3" : 0.3,
"0.4" : 0.4,
"0.5" : 0.5,
"1" : 1,
"2" : 2,
},
},
// These are translated into proper Dshot values on the flight controller
DSHOT_PROTOCOL_MIN_VALUE: 0,
DSHOT_DISARMED_VALUE: 1000,
DSHOT_MAX_VALUE: 2000,
DSHOT_3D_NEUTRAL: 1500,
};
TABS.motors.initialize = async function (callback) {
const self = this;
self.armed = false;
self.escProtocolIsDshot = false;
self.configHasChanged = false;
self.configChanges = {};
// Update filtering defaults based on API version
const FILTER_DEFAULT = FC.getFilterDefaults();
if (GUI.active_tab != 'motors') {
GUI.active_tab = 'motors';
}
await MSP.promise(MSPCodes.MSP_STATUS);
await MSP.promise(MSPCodes.MSP_PID_ADVANCED);
await MSP.promise(MSPCodes.MSP_FEATURE_CONFIG);
await MSP.promise(MSPCodes.MSP_MIXER_CONFIG);
if (FC.MOTOR_CONFIG.use_dshot_telemetry || FC.MOTOR_CONFIG.use_esc_sensor) {
await MSP.promise(MSPCodes.MSP_MOTOR_TELEMETRY);
}
await MSP.promise(MSPCodes.MSP_MOTOR_CONFIG);
await MSP.promise(MSPCodes.MSP_MOTOR_3D_CONFIG);
await MSP.promise(MSPCodes.MSP2_MOTOR_OUTPUT_REORDERING);
await MSP.promise(MSPCodes.MSP_ADVANCED_CONFIG);
if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_42)) {
await MSP.promise(MSPCodes.MSP_FILTER_CONFIG);
}
if (semver.gte(FC.CONFIG.apiVersion, "1.8.0")) {
await MSP.promise(MSPCodes.MSP_ARMING_CONFIG);
}
load_html();
function load_html() {
$('#content').load("./tabs/motors.html", process_html);
}
function update_arm_status() {
self.armed = bit_check(FC.CONFIG.mode, 0);
}
function initSensorData() {
for (let i = 0; i < 3; i++) {
FC.SENSOR_DATA.accelerometer[i] = 0;
FC.SENSOR_DATA.gyroscope[i] = 0;
}
}
function initDataArray(length) {
const data = Array.from({length: length});
for (let i = 0; i < length; i++) {
data[i] = [];
data[i].min = -1;
data[i].max = 1;
}
return data;
}
function addSampleToData(data, sampleNumber, sensorData) {
for (let i = 0; i < data.length; i++) {
const dataPoint = sensorData[i];
data[i].push([sampleNumber, dataPoint]);
if (dataPoint < data[i].min) {
data[i].min = dataPoint;
}
if (dataPoint > data[i].max) {
data[i].max = dataPoint;
}
}
while (data[0].length > 300) {
for (const item of data) {
item.shift();
}
}
return sampleNumber + 1;
}
const margin = {top: 20, right: 30, bottom: 10, left: 20};
function updateGraphHelperSize(helpers) {
helpers.width = helpers.targetElement.width() - margin.left - margin.right;
helpers.height = helpers.targetElement.height() - margin.top - margin.bottom;
helpers.widthScale.range([0, helpers.width]);
helpers.heightScale.range([helpers.height, 0]);
helpers.xGrid.tickSize(-helpers.height, 0, 0);
helpers.yGrid.tickSize(-helpers.width, 0, 0);
}
function initGraphHelpers(selector, sampleNumber, heightDomain) {
const helpers = {selector: selector, targetElement: $(selector), dynamicHeightDomain: !heightDomain};
helpers.widthScale = d3.scale.linear()
.clamp(true)
.domain([(sampleNumber - 299), sampleNumber]);
helpers.heightScale = d3.scale.linear()
.clamp(true)
.domain(heightDomain || [1, -1]);
helpers.xGrid = d3.svg.axis();
helpers.yGrid = d3.svg.axis();
updateGraphHelperSize(helpers);
helpers.xGrid
.scale(helpers.widthScale)
.orient("bottom")
.ticks(5)
.tickFormat("");
helpers.yGrid
.scale(helpers.heightScale)
.orient("left")
.ticks(5)
.tickFormat("");
helpers.xAxis = d3.svg.axis()
.scale(helpers.widthScale)
.ticks(5)
.orient("bottom")
.tickFormat(function (d) {return d;});
helpers.yAxis = d3.svg.axis()
.scale(helpers.heightScale)
.ticks(5)
.orient("left")
.tickFormat(function (d) {return d;});
helpers.line = d3.svg.line()
.x(function (d) { return helpers.widthScale(d[0]); })
.y(function (d) { return helpers.heightScale(d[1]); });
return helpers;
}
function drawGraph(graphHelpers, data, sampleNumber) {
const svg = d3.select(graphHelpers.selector);
if (graphHelpers.dynamicHeightDomain) {
const limits = [];
$.each(data, function (idx, datum) {
limits.push(datum.min);
limits.push(datum.max);
});
graphHelpers.heightScale.domain(d3.extent(limits));
}
graphHelpers.widthScale.domain([(sampleNumber - 299), sampleNumber]);
svg.select(".x.grid").call(graphHelpers.xGrid);
svg.select(".y.grid").call(graphHelpers.yGrid);
svg.select(".x.axis").call(graphHelpers.xAxis);
svg.select(".y.axis").call(graphHelpers.yAxis);
const group = svg.select("g.data");
const lines = group.selectAll("path").data(data, function (d, i) {
return i;
});
lines.enter().append("path").attr("class", "line");
lines.attr('d', graphHelpers.line);
}
function update_model(mixer) {
const imgSrc = getMixerImageSrc(mixer, FC.MIXER_CONFIG.reverseMotorDir, FC.CONFIG.apiVersion);
$('.mixerPreview img').attr('src', imgSrc);
const motorOutputReorderConfig = new MotorOutputReorderConfig(100);
const domMotorOutputReorderDialogOpen = $('#motorOutputReorderDialogOpen');
const isMotorReorderingAvailable = (mixerList[mixer - 1].name in motorOutputReorderConfig)
&& (FC.MOTOR_OUTPUT_ORDER) && (FC.MOTOR_OUTPUT_ORDER.length > 0);
domMotorOutputReorderDialogOpen.toggle(isMotorReorderingAvailable);
self.escProtocolIsDshot = EscProtocols.IsProtocolDshot(FC.CONFIG.apiVersion, FC.PID_ADVANCED_CONFIG.fast_pwm_protocol);
}
function process_html() {
// translate to user-selected language
i18n.localizePage();
update_arm_status();
self.feature3DEnabled = FC.FEATURE_CONFIG.features.isEnabled('3D');
const motorsEnableTestModeElement = $('#motorsEnableTestMode');
self.analyticsChanges = {};
motorsEnableTestModeElement.prop('checked', false).trigger('change');
if (semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_42) || !(FC.MOTOR_CONFIG.use_dshot_telemetry || FC.MOTOR_CONFIG.use_esc_sensor)) {
$(".motor_testing .telemetry").hide();
} else {
// Hide telemetry from unused motors (to hide the tooltip in an empty blank space)
for (let i = FC.MOTOR_CONFIG.motor_count; i < FC.MOTOR_DATA.length; i++) {
$(`.motor_testing .telemetry .motor-${i}`).hide();
}
}
function setContentButtons(motorsTesting=false) {
$('.btn .tool').toggleClass("disabled", self.configHasChanged || motorsTesting);
$('.btn .save').toggleClass("disabled", !self.configHasChanged);
$('.btn .stop').toggleClass("disabled", !motorsTesting);
}
const defaultConfiguration = {
mixer: FC.MIXER_CONFIG.mixer,
reverseMotorSwitch: FC.MIXER_CONFIG.reverseMotorDir,
escprotocol: FC.PID_ADVANCED_CONFIG.fast_pwm_protocol + 1,
feature3: FC.FEATURE_CONFIG.features.isEnabled('MOTOR_STOP'),
feature9: FC.FEATURE_CONFIG.features.isEnabled('3D'),
feature20: FC.FEATURE_CONFIG.features.isEnabled('ESC_SENSOR'),
dshotBidir: FC.MOTOR_CONFIG.use_dshot_telemetry,
motorPoles: FC.MOTOR_CONFIG.motor_poles,
digitalIdlePercent: FC.PID_ADVANCED_CONFIG.digitalIdlePercent,
_3ddeadbandlow: FC.MOTOR_3D_CONFIG.deadband3d_low,
_3ddeadbandhigh: FC.MOTOR_3D_CONFIG.deadband3d_high,
_3dneutral: FC.MOTOR_3D_CONFIG.neutral,
unsyncedPWMSwitch: FC.PID_ADVANCED_CONFIG.use_unsyncedPwm,
unsyncedpwmfreq: FC.PID_ADVANCED_CONFIG.motor_pwm_rate,
minthrottle: FC.MOTOR_CONFIG.minthrottle,
maxthrottle: FC.MOTOR_CONFIG.maxthrottle,
mincommand: FC.MOTOR_CONFIG.mincommand,
};
setContentButtons();
// Stop motor testing on configuration changes
function disableHandler(e) {
if (e.target !== e.currentTarget) {
const item = e.target.id === '' ? e.target.name : e.target.id;
let value = e.target.type === "checkbox" ? e.target.checked : e.target.value;
switch (e.target.type) {
case "checkbox":
if (item === "reverseMotorSwitch") {
value = value === false ? 0 : 1;
}
break;
case "number":
value = isInt(value) ? parseInt(value) : parseFloat(value);
break;
case "select-one":
value = parseInt(value);
break;
default:
console.log(`Undefined case ${e.target.type} encountered, please check code`);
}
self.configChanges[item] = value;
if (item in defaultConfiguration) {
if (value !== defaultConfiguration[item]) {
self.configHasChanged = true;
} else {
delete self.configChanges[item];
if (Object.keys(self.configChanges).length === 0) {
console.log('All configuration changes reverted');
self.configHasChanged = false;
}
}
} else {
console.log(`Unknown item ${item} found with type ${e.target.type}, please add to the defaultConfiguration object.`);
self.configHasChanged = true;
}
// disables Motor Testing if settings are being changed (must save and reboot or undo changes).
motorsEnableTestModeElement.trigger("change");
setContentButtons();
}
e.stopPropagation();
}
// Add EventListener for configuration changes
document.querySelectorAll('.configuration').forEach(elem => elem.addEventListener('change', disableHandler));
/*
* MIXER
*/
const mixerListElement = $('select.mixerList');
for (let selectIndex = 0; selectIndex < mixerList.length; selectIndex++) {
mixerList.forEach(function (mixerEntry, mixerIndex) {
if (mixerEntry.pos === selectIndex) {
mixerListElement.append(``);
}
});
}
sortElement('select.mixerList');
function refreshMixerPreview() {
const mixer = FC.MIXER_CONFIG.mixer;
const reverse = FC.MIXER_CONFIG.reverseMotorDir ? "_reversed" : "";
$('.mixerPreview img').attr('src', `./resources/motor_order/${mixerList[mixer - 1].image}${reverse}.svg`);
}
const reverseMotorSwitchElement = $('#reverseMotorSwitch');
reverseMotorSwitchElement.change(function() {
FC.MIXER_CONFIG.reverseMotorDir = $(this).prop('checked') ? 1 : 0;
refreshMixerPreview();
});
reverseMotorSwitchElement.prop('checked', FC.MIXER_CONFIG.reverseMotorDir !== 0).change();
mixerListElement.change(function () {
const mixerValue = parseInt($(this).val());
let newValue;
if (mixerValue !== FC.MIXER_CONFIG.mixer) {
newValue = $(this).find('option:selected').text();
}
self.analyticsChanges['Mixer'] = newValue;
FC.MIXER_CONFIG.mixer = mixerValue;
refreshMixerPreview();
});
// select current mixer configuration
mixerListElement.val(FC.MIXER_CONFIG.mixer).change();
function validateMixerOutputs() {
MSP.promise(MSPCodes.MSP_MOTOR).then(() => {
const mixer = FC.MIXER_CONFIG.mixer;
const motors = mixerList[mixer - 1].motors;
// initialize for models with zero motors
self.numberOfValidOutputs = motors;
for (let i = 0; i < FC.MOTOR_DATA.length; i++) {
if (FC.MOTOR_DATA[i] === 0) {
self.numberOfValidOutputs = i;
if (motors > self.numberOfValidOutputs && motors > 0) {
const msg = i18n.getMessage('motorsDialogMixerReset', {
mixerName: mixerList[mixer - 1].name,
mixerMotors: motors,
outputs: self.numberOfValidOutputs,
});
showDialogMixerReset(msg);
}
return;
}
}
});
}
update_model(FC.MIXER_CONFIG.mixer);
// Reference: src/main/drivers/motor.h for motorPwmProtocolTypes_e;
const ESC_PROTOCOL_UNDEFINED = 9;
if (FC.PID_ADVANCED_CONFIG.fast_pwm_protocol !== ESC_PROTOCOL_UNDEFINED) {
validateMixerOutputs();
}
// Always start with default/empty sensor data array, clean slate all
initSensorData();
// Setup variables
let samplesGyro = 0;
const gyroData = initDataArray(3);
let gyroHelpers = initGraphHelpers('#graph', samplesGyro, [-2, 2]);
let gyroMaxRead = [0, 0, 0];
let samplesAccel = 0;
const accelData = initDataArray(3);
let accelHelpers = initGraphHelpers('#graph', samplesAccel, [-2, 2]);
let accelMaxRead = [0, 0, 0];
const accelOffset = [0, 0, 0];
let accelOffsetEstablished = false;
// cached elements
const motorVoltage = $('.motors-bat-voltage');
const motorMahDrawingElement = $('.motors-bat-mah-drawing');
const motorMahDrawnElement = $('.motors-bat-mah-drawn');
const rawDataTextElements = {
x: [],
y: [],
z: [],
rms: [],
};
$('.plot_control .x, .plot_control .y, .plot_control .z, .plot_control .rms').each(function () {
const el = $(this);
if (el.hasClass('x')) {
rawDataTextElements.x.push(el);
} else if (el.hasClass('y')) {
rawDataTextElements.y.push(el);
} else if (el.hasClass('z')) {
rawDataTextElements.z.push(el);
} else if (el.hasClass('rms')) {
rawDataTextElements.rms.push(el);
}
});
function loadScaleSelector(selectorValues, selectedValue) {
$('.tab-motors select[name="scale"]').find('option').remove();
$.each(selectorValues, function(key, val) {
$('.tab-motors select[name="scale"]').append(new Option(key, val));
});
$('.tab-motors select[name="scale"]').val(selectedValue);
}
function selectRefresh(refreshValue){
$('.tab-motors select[name="rate"]').val(refreshValue);
}
$('.tab-motors .sensor select').change(function(){
TABS.motors.sensor = $('.tab-motors select[name="sensor_choice"]').val();
ConfigStorage.set({'motors_tab_sensor_settings': {'sensor': TABS.motors.sensor}});
switch(TABS.motors.sensor){
case "gyro":
loadScaleSelector(TABS.motors.sensorSelectValues.gyroScale,
TABS.motors.sensorGyroScale);
selectRefresh(TABS.motors.sensorGyroRate);
break;
case "accel":
loadScaleSelector(TABS.motors.sensorSelectValues.accelScale,
TABS.motors.sensorAccelScale);
selectRefresh(TABS.motors.sensorAccelRate);
break;
}
$('.tab-motors .rate select:first').change();
});
$('.tab-motors .rate select, .tab-motors .scale select').change(function () {
const rate = parseInt($('.tab-motors select[name="rate"]').val(), 10);
const scale = parseFloat($('.tab-motors select[name="scale"]').val());
GUI.interval_kill_all(['motor_and_status_pull','motors_power_data_pull_slow']);
switch(TABS.motors.sensor) {
case "gyro":
ConfigStorage.set({'motors_tab_gyro_settings': {'rate': rate, 'scale': scale}});
TABS.motors.sensorGyroRate = rate;
TABS.motors.sensorGyroScale = scale;
gyroHelpers = initGraphHelpers('#graph', samplesGyro, [-scale, scale]);
GUI.interval_add('IMU_pull', function imu_data_pull() {
MSP.send_message(MSPCodes.MSP_RAW_IMU, false, false, update_gyro_graph);
}, rate, true);
break;
case "accel":
ConfigStorage.set({'motors_tab_accel_settings': {'rate': rate, 'scale': scale}});
TABS.motors.sensorAccelRate = rate;
TABS.motors.sensorAccelScale = scale;
accelHelpers = initGraphHelpers('#graph', samplesAccel, [-scale, scale]);
GUI.interval_add('IMU_pull', function imu_data_pull() {
MSP.send_message(MSPCodes.MSP_RAW_IMU, false, false, update_accel_graph);
}, rate, true);
break;
}
function update_accel_graph() {
if (!accelOffsetEstablished) {
for (let i = 0; i < 3; i++) {
accelOffset[i] = FC.SENSOR_DATA.accelerometer[i] * -1;
}
accelOffsetEstablished = true;
}
const accelWithOffset = [
accelOffset[0] + FC.SENSOR_DATA.accelerometer[0],
accelOffset[1] + FC.SENSOR_DATA.accelerometer[1],
accelOffset[2] + FC.SENSOR_DATA.accelerometer[2],
];
updateGraphHelperSize(accelHelpers);
samplesAccel = addSampleToData(accelData, samplesAccel, accelWithOffset);
drawGraph(accelHelpers, accelData, samplesAccel);
for (let i = 0; i < 3; i++) {
if (Math.abs(accelWithOffset[i]) > Math.abs(accelMaxRead[i])) {
accelMaxRead[i] = accelWithOffset[i];
}
}
computeAndUpdate(accelWithOffset, accelData, accelMaxRead);
}
function update_gyro_graph() {
const gyro = [
FC.SENSOR_DATA.gyroscope[0],
FC.SENSOR_DATA.gyroscope[1],
FC.SENSOR_DATA.gyroscope[2],
];
updateGraphHelperSize(gyroHelpers);
samplesGyro = addSampleToData(gyroData, samplesGyro, gyro);
drawGraph(gyroHelpers, gyroData, samplesGyro);
for (let i = 0; i < 3; i++) {
if (Math.abs(gyro[i]) > Math.abs(gyroMaxRead[i])) {
gyroMaxRead[i] = gyro[i];
}
}
computeAndUpdate(gyro, gyroData, gyroMaxRead);
}
function computeAndUpdate(sensor_data, data, max_read) {
let sum = 0.0;
for (let j = 0, jlength = data.length; j < jlength; j++) {
for (let k = 0, klength = data[j].length; k < klength; k++) {
sum += data[j][k][1]*data[j][k][1];
}
}
const rms = Math.sqrt(sum/(data[0].length+data[1].length+data[2].length));
rawDataTextElements.x[0].text(`${sensor_data[0].toFixed(2)} ( ${max_read[0].toFixed(2)} )`);
rawDataTextElements.y[0].text(`${sensor_data[1].toFixed(2)} ( ${max_read[1].toFixed(2)} )`);
rawDataTextElements.z[0].text(`${sensor_data[2].toFixed(2)} ( ${max_read[2].toFixed(2)} )`);
rawDataTextElements.rms[0].text(rms.toFixed(4));
}
});
// set refresh speeds according to configuration saved in storage
const result = ConfigStorage.get(['motors_tab_sensor_settings', 'motors_tab_gyro_settings', 'motors_tab_accel_settings']);
if (result.motors_tab_sensor_settings) {
$('.tab-motors select[name="sensor_choice"]').val(result.motors_tab_sensor_settings.sensor);
}
if (result.motors_tab_gyro_settings) {
TABS.motors.sensorGyroRate = result.motors_tab_gyro_settings.rate;
TABS.motors.sensorGyroScale = result.motors_tab_gyro_settings.scale;
}
if (result.motors_tab_accel_settings) {
TABS.motors.sensorAccelRate = result.motors_tab_accel_settings.rate;
TABS.motors.sensorAccelScale = result.motors_tab_accel_settings.scale;
}
$('.tab-motors .sensor select:first').change();
// Amperage
function power_data_pull() {
motorVoltage.text(i18n.getMessage('motorsVoltageValue', [FC.ANALOG.voltage]));
motorMahDrawingElement.text(i18n.getMessage('motorsADrawingValue', [FC.ANALOG.amperage.toFixed(2)]));
motorMahDrawnElement.text(i18n.getMessage('motorsmAhDrawnValue', [FC.ANALOG.mAhdrawn]));
}
GUI.interval_add('motors_power_data_pull_slow', power_data_pull, 250, true); // 4 fps
$('a.reset_max').click(function () {
gyroMaxRead = [0, 0, 0];
accelMaxRead = [0, 0, 0];
accelOffsetEstablished = false;
});
let rangeMin;
let rangeMax;
let neutral3d;
if (self.escProtocolIsDshot) {
rangeMin = self.DSHOT_DISARMED_VALUE;
rangeMax = self.DSHOT_MAX_VALUE;
neutral3d = self.DSHOT_3D_NEUTRAL;
} else {
rangeMin = FC.MOTOR_CONFIG.mincommand;
rangeMax = FC.MOTOR_CONFIG.maxthrottle;
//Arbitrary sanity checks
//Note: values may need to be revisited
neutral3d = (FC.MOTOR_3D_CONFIG.neutral > 1575 || FC.MOTOR_3D_CONFIG.neutral < 1425) ? 1500 : FC.MOTOR_3D_CONFIG.neutral;
}
let zeroThrottleValue = rangeMin;
if (self.feature3DEnabled) {
zeroThrottleValue = neutral3d;
}
const motorsWrapper = $('.motors .bar-wrapper');
for (let i = 0; i < 8; i++) {
motorsWrapper.append(`\
\
`);
}
$('div.sliders input').prop('min', rangeMin)
.prop('max', rangeMax);
$('div.values li:not(:last)').text(rangeMin);
const featuresElement = $('.tab-motors .features');
FC.FEATURE_CONFIG.features.generateElements(featuresElement);
/*
* ESC protocol
*/
const escProtocols = EscProtocols.GetAvailableProtocols(FC.CONFIG.apiVersion);
const escProtocolElement = $('select.escprotocol');
for (let j = 0; j < escProtocols.length; j++) {
escProtocolElement.append(``);
}
sortElement('select.escprotocol');
const unsyncedPWMSwitchElement = $("input[id='unsyncedPWMSwitch']");
const divUnsyncedPWMFreq = $('div.unsyncedpwmfreq');
unsyncedPWMSwitchElement.on("change", function () {
if ($(this).is(':checked')) {
divUnsyncedPWMFreq.show();
} else {
divUnsyncedPWMFreq.hide();
}
});
const dshotBidirElement = $('input[id="dshotBidir"]');
unsyncedPWMSwitchElement.prop('checked', FC.PID_ADVANCED_CONFIG.use_unsyncedPwm !== 0).trigger("change");
$('input[name="unsyncedpwmfreq"]').val(FC.PID_ADVANCED_CONFIG.motor_pwm_rate);
$('input[name="digitalIdlePercent"]').val(FC.PID_ADVANCED_CONFIG.digitalIdlePercent);
if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_42)) {
dshotBidirElement.prop('checked', FC.MOTOR_CONFIG.use_dshot_telemetry).trigger("change");
self.previousDshotBidir = FC.MOTOR_CONFIG.use_dshot_telemetry;
self.previousFilterDynQ = FC.FILTER_CONFIG.dyn_notch_q;
self.previousFilterDynCount = FC.FILTER_CONFIG.dyn_notch_count;
dshotBidirElement.on("change", function () {
const value = $(this).prop('checked');
const newValue = (value !== FC.MOTOR_CONFIG.use_dshot_telemetry) ? 'On' : 'Off';
self.analyticsChanges['BidirectionalDshot'] = newValue;
FC.MOTOR_CONFIG.use_dshot_telemetry = value;
if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) {
const rpmFilterIsDisabled = FC.FILTER_CONFIG.gyro_rpm_notch_harmonics === 0;
FC.FILTER_CONFIG.dyn_notch_count = self.previousFilterDynCount;
FC.FILTER_CONFIG.dyn_notch_q = self.previousFilterDynQ;
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 (value && !self.previousDshotBidir) {
FC.FILTER_CONFIG.dyn_notch_count = FILTER_DEFAULT.dyn_notch_count_rpm;
FC.FILTER_CONFIG.dyn_notch_q = FILTER_DEFAULT.dyn_notch_q_rpm;
} else if (!value && self.previousDshotBidir) {
FC.FILTER_CONFIG.dyn_notch_count = FILTER_DEFAULT.dyn_notch_count;
FC.FILTER_CONFIG.dyn_notch_q = FILTER_DEFAULT.dyn_notch_q;
}
};
if ((FC.MOTOR_CONFIG.use_dshot_telemetry !== self.previousDshotBidir) && !(rpmFilterIsDisabled)) {
GUI.showYesNoDialog(dialogDynFilterSettings);
} else {
FC.FILTER_CONFIG.dyn_notch_count = self.previousFilterDynCount;
FC.FILTER_CONFIG.dyn_notch_q = self.previousFilterDynQ;
}
}
});
$('input[name="motorPoles"]').val(FC.MOTOR_CONFIG.motor_poles);
}
$('#escProtocolTooltip').toggle(semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_42));
$('#escProtocolTooltipNoDSHOT1200').toggle(semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_42));
function updateVisibility() {
// Hide unused settings
const protocolName = $('select.escprotocol option:selected').text();
const protocolConfigured = protocolName !== 'DISABLED';
let digitalProtocol = false;
switch (protocolName) {
case 'DSHOT150':
case 'DSHOT300':
case 'DSHOT600':
case 'DSHOT1200':
case 'PROSHOT1000':
digitalProtocol = true;
break;
default:
}
const rpmFeaturesVisible = digitalProtocol && dshotBidirElement.is(':checked') || $("input[name='ESC_SENSOR']").is(':checked');
$('div.minthrottle').toggle(protocolConfigured && !digitalProtocol);
$('div.maxthrottle').toggle(protocolConfigured && !digitalProtocol);
$('div.mincommand').toggle(protocolConfigured && !digitalProtocol);
$('div.checkboxPwm').toggle(protocolConfigured && !digitalProtocol);
divUnsyncedPWMFreq.toggle(protocolConfigured && !digitalProtocol);
$('div.digitalIdlePercent').toggle(protocolConfigured && digitalProtocol);
if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_44)) {
$('input[name="digitalIdlePercent"]').prop('disabled', protocolConfigured && digitalProtocol && FC.ADVANCED_TUNING.idleMinRpm && FC.MOTOR_CONFIG.use_dshot_telemetry);
}
if (FC.ADVANCED_TUNING.idleMinRpm && FC.MOTOR_CONFIG.use_dshot_telemetry) {
const dynamicIdle = FC.ADVANCED_TUNING.idleMinRpm * 100;
$('span.digitalIdlePercentDisabled').text(i18n.getMessage('configurationDigitalIdlePercentDisabled', { dynamicIdle }));
} else {
$('span.digitalIdlePercentDisabled').text(i18n.getMessage('configurationDigitalIdlePercent'));
}
$('.escSensor').toggle(protocolConfigured && digitalProtocol);
$('div.checkboxDshotBidir').toggle(protocolConfigured && semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_42) && digitalProtocol);
$('div.motorPoles').toggle(protocolConfigured && rpmFeaturesVisible && semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_42));
$('.escMotorStop').toggle(protocolConfigured);
$('#escProtocolDisabled').toggle(!protocolConfigured);
//trigger change unsyncedPWMSwitch to show/hide Motor PWM freq input
unsyncedPWMSwitchElement.trigger("change");
}
escProtocolElement.val(FC.PID_ADVANCED_CONFIG.fast_pwm_protocol + 1);
escProtocolElement.on("change", function () {
const escProtocolValue = parseInt($(this).val()) - 1;
let newValue = undefined;
if (escProtocolValue !== FC.PID_ADVANCED_CONFIG.fast_pwm_protocol) {
newValue = $(this).find('option:selected').text();
}
self.analyticsChanges['EscProtocol'] = newValue;
updateVisibility();
}).trigger("change");
//trigger change dshotBidir and ESC_SENSOR to show/hide Motor Poles tab
dshotBidirElement.change(updateVisibility).trigger("change");
$("input[name='ESC_SENSOR']").on("change", updateVisibility).trigger("change");
// fill throttle
$('input[name="minthrottle"]').val(FC.MOTOR_CONFIG.minthrottle);
$('input[name="maxthrottle"]').val(FC.MOTOR_CONFIG.maxthrottle);
$('input[name="mincommand"]').val(FC.MOTOR_CONFIG.mincommand);
//fill 3D
$('.tab-motors ._3d').show();
$('input[name="_3ddeadbandlow"]').val(FC.MOTOR_3D_CONFIG.deadband3d_low);
$('input[name="_3ddeadbandhigh"]').val(FC.MOTOR_3D_CONFIG.deadband3d_high);
$('input[name="_3dneutral"]').val(FC.MOTOR_3D_CONFIG.neutral);
/*
* UI hooks
*/
function checkUpdate3dControls() {
if (FC.FEATURE_CONFIG.features.isEnabled('3D')) {
$('._3dSettings').show();
} else {
$('._3dSettings').hide();
}
}
$('input.feature', featuresElement).on("change", function () {
const element = $(this);
FC.FEATURE_CONFIG.features.updateData(element);
updateTabList(FC.FEATURE_CONFIG.features);
switch (element.attr('name')) {
case 'MOTOR_STOP':
break;
case '3D':
checkUpdate3dControls();
break;
default:
break;
}
});
$(featuresElement).filter('select').change(function () {
const element = $(this);
FC.FEATURE_CONFIG.features.updateData(element);
updateTabList(FC.FEATURE_CONFIG.features);
});
checkUpdate3dControls();
/*
* MOTOR TESTING
*/
function setSlidersDefault() {
// change all values to default
$('div.sliders input').val(zeroThrottleValue);
}
function setSlidersEnabled(isEnabled) {
if (isEnabled && !self.armed) {
$('div.sliders input').slice(0, self.numberOfValidOutputs).prop('disabled', false);
// unlock master slider
$('div.sliders input:last').prop('disabled', false);
} else {
setSlidersDefault();
// disable sliders / min max
$('div.sliders input').prop('disabled', true);
}
$('div.sliders input').trigger('input');
}
setSlidersDefault();
const ignoreKeys = [
'PageUp',
'PageDown',
'End',
'Home',
'ArrowUp',
'ArrowDown',
];
motorsEnableTestModeElement.on('change', function () {
let enabled = $(this).is(':checked');
// prevent or disable testing if configHasChanged flag is set.
if (self.configHasChanged) {
if (enabled) {
const message = i18n.getMessage('motorsDialogSettingsChanged');
showDialogSettingsChanged(message);
enabled = false;
}
// disable input
motorsEnableTestModeElement.prop('checked', false);
}
function disableMotorTest(e) {
if (motorsEnableTestModeElement.is(':checked')) {
if (!ignoreKeys.includes(e.code)) {
motorsEnableTestModeElement.prop('checked', false).trigger('change');
}
}
}
if (enabled) {
document.addEventListener('keydown', e => disableMotorTest(e));
// enable Status and Motor data pulling
GUI.interval_add('motor_and_status_pull', get_status, 50, true);
} else {
document.removeEventListener('keydown', e => disableMotorTest(e));
GUI.interval_remove("motor_and_status_pull");
}
setContentButtons(enabled);
setSlidersEnabled(enabled);
$('div.sliders input').trigger('input');
mspHelper.setArmingEnabled(enabled, enabled);
});
let bufferingSetMotor = [],
buffer_delay = false;
$('div.sliders input:not(.master)').on('input', function () {
const index = $(this).index();
let buffer = [];
$('div.values li').eq(index).text($(this).val());
for (let i = 0; i < self.numberOfValidOutputs; i++) {
const val = parseInt($('div.sliders input').eq(i).val());
buffer.push16(val);
}
bufferingSetMotor.push(buffer);
if (!buffer_delay) {
buffer_delay = setTimeout(function () {
buffer = bufferingSetMotor.pop();
MSP.send_message(MSPCodes.MSP_SET_MOTOR, buffer);
bufferingSetMotor = [];
buffer_delay = false;
}, 10);
}
});
$('div.sliders input.master').on('input', function () {
const val = $(this).val();
$('div.sliders input:not(:disabled, :last)').val(val);
$('div.values li:not(:last)').slice(0, self.numberOfValidOutputs).text(val);
$('div.sliders input:not(:last):first').trigger('input');
});
// check if motors are already spinning
let motorsRunning = false;
for (let i = 0; i < self.numberOfValidOutputs; i++) {
if (!self.feature3DEnabled) {
if (FC.MOTOR_DATA[i] > zeroThrottleValue) {
motorsRunning = true;
}
} else {
if ((FC.MOTOR_DATA[i] < FC.MOTOR_3D_CONFIG.deadband3d_low) || (FC.MOTOR_DATA[i] > FC.MOTOR_3D_CONFIG.deadband3d_high)) {
motorsRunning = true;
}
}
}
if (motorsRunning) {
motorsEnableTestModeElement.prop('checked', true).trigger('change');
// motors are running adjust sliders to current values
const sliders = $('div.sliders input:not(.master)');
let masterValue = FC.MOTOR_DATA[0];
for (let i = 0; i < FC.MOTOR_DATA.length; i++) {
if (FC.MOTOR_DATA[i] > 0) {
sliders.eq(i).val(FC.MOTOR_DATA[i]);
if (masterValue !== FC.MOTOR_DATA[i]) {
masterValue = false;
}
}
}
// only fire events when all values are set
sliders.trigger('input');
// slide master slider if condition is valid
if (masterValue) {
$('div.sliders input.master').val(masterValue)
.trigger('input');
}
}
// data pulling functions used inside interval timer
function get_status() {
// status needed for arming flag
MSP.send_message(MSPCodes.MSP_STATUS, false, false, get_motor_data);
}
function get_motor_data() {
MSP.send_message(MSPCodes.MSP_MOTOR, false, false, get_motor_telemetry_data);
}
function get_motor_telemetry_data() {
if (FC.MOTOR_CONFIG.use_dshot_telemetry || FC.MOTOR_CONFIG.use_esc_sensor) {
MSP.send_message(MSPCodes.MSP_MOTOR_TELEMETRY, false, false, update_ui);
} else {
update_ui();
}
}
function getMotorOutputs() {
const motorData = [];
const motorsTesting = motorsEnableTestModeElement.is(':checked');
for (let i = 0; i < self.numberOfValidOutputs; i++) {
motorData[i] = motorsTesting ? FC.MOTOR_DATA[i] : zeroThrottleValue;
}
return motorData;
}
const fullBlockScale = rangeMax - rangeMin;
function update_ui() {
const previousArmState = self.armed;
const blockHeight = $('div.m-block:first').height();
const motorValues = getMotorOutputs();
for (let i = 0; i < motorValues.length; i++) {
const motorValue = motorValues[i];
const barHeight = motorValue - rangeMin,
marginTop = blockHeight - (barHeight * (blockHeight / fullBlockScale)).clamp(0, blockHeight),
height = (barHeight * (blockHeight / fullBlockScale)).clamp(0, blockHeight),
color = parseInt(barHeight * 0.009);
$(`.motor-${i} .label`, motorsWrapper).text(motorValue);
$(`.motor-${i} .indicator`, motorsWrapper).css({
'margin-top' : `${marginTop}px`,
'height' : `${height}px`,
'background-color' : `rgba(255,187,0,1.${color})`,
});
if (i < FC.MOTOR_CONFIG.motor_count && (FC.MOTOR_CONFIG.use_dshot_telemetry || FC.MOTOR_CONFIG.use_esc_sensor)) {
const MAX_INVALID_PERCENT = 100,
MAX_VALUE_SIZE = 6;
let rpmMotorValue = FC.MOTOR_TELEMETRY_DATA.rpm[i];
// Reduce the size of the value if too big
if (rpmMotorValue > 999999) {
rpmMotorValue = `${(rpmMotorValue / 1000000).toFixed(5 - (rpmMotorValue / 1000000).toFixed(0).toString().length)}M`;
}
rpmMotorValue = rpmMotorValue.toString().padStart(MAX_VALUE_SIZE);
let telemetryText = i18n.getMessage('motorsRPM', {motorsRpmValue: rpmMotorValue});
if (FC.MOTOR_CONFIG.use_dshot_telemetry) {
let invalidPercent = FC.MOTOR_TELEMETRY_DATA.invalidPercent[i];
let classError = (invalidPercent > MAX_INVALID_PERCENT) ? "warning" : "";
invalidPercent = (invalidPercent / 100).toFixed(2).toString().padStart(MAX_VALUE_SIZE);
telemetryText += `
`;
telemetryText += i18n.getMessage('motorsRPMError', {motorsErrorValue: invalidPercent});
telemetryText += "";
}
if (FC.MOTOR_CONFIG.use_esc_sensor) {
let escTemperature = FC.MOTOR_TELEMETRY_DATA.temperature[i];
escTemperature = escTemperature.toString().padStart(MAX_VALUE_SIZE);
telemetryText += "
";
telemetryText += i18n.getMessage('motorsESCTemperature', {motorsESCTempValue: escTemperature});
}
$(`.motor_testing .telemetry .motor-${i}`).html(telemetryText);
}
}
//keep the following here so at least we get a visual cue of our motor setup
update_arm_status();
if (previousArmState !== self.armed) {
console.log('arm state change detected');
motorsEnableTestModeElement.change();
}
}
$('a.save').on('click', async function() {
// gather data that doesn't have automatic change event bound
FC.MOTOR_CONFIG.minthrottle = parseInt($('input[name="minthrottle"]').val());
FC.MOTOR_CONFIG.maxthrottle = parseInt($('input[name="maxthrottle"]').val());
FC.MOTOR_CONFIG.mincommand = parseInt($('input[name="mincommand"]').val());
if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_42)) {
FC.MOTOR_CONFIG.motor_poles = parseInt($('input[name="motorPoles"]').val());
}
FC.MOTOR_3D_CONFIG.deadband3d_low = parseInt($('input[name="_3ddeadbandlow"]').val());
FC.MOTOR_3D_CONFIG.deadband3d_high = parseInt($('input[name="_3ddeadbandhigh"]').val());
FC.MOTOR_3D_CONFIG.neutral = parseInt($('input[name="_3dneutral"]').val());
FC.PID_ADVANCED_CONFIG.fast_pwm_protocol = parseInt(escProtocolElement.val() - 1);
FC.PID_ADVANCED_CONFIG.use_unsyncedPwm = unsyncedPWMSwitchElement.is(':checked') ? 1 : 0;
FC.PID_ADVANCED_CONFIG.motor_pwm_rate = parseInt($('input[name="unsyncedpwmfreq"]').val());
FC.PID_ADVANCED_CONFIG.digitalIdlePercent = parseFloat($('input[name="digitalIdlePercent"]').val());
if (semver.gte(FC.CONFIG.apiVersion, "1.25.0") && semver.lt(FC.CONFIG.apiVersion, API_VERSION_1_41)) {
FC.PID_ADVANCED_CONFIG.gyroUse32kHz = $('input[id="gyroUse32kHz"]').is(':checked') ? 1 : 0;
}
await MSP.promise(MSPCodes.MSP_SET_FEATURE_CONFIG, mspHelper.crunch(MSPCodes.MSP_SET_FEATURE_CONFIG));
await MSP.promise(MSPCodes.MSP_SET_MIXER_CONFIG, mspHelper.crunch(MSPCodes.MSP_SET_MIXER_CONFIG));
await MSP.promise(MSPCodes.MSP_SET_MOTOR_CONFIG, mspHelper.crunch(MSPCodes.MSP_SET_MOTOR_CONFIG));
await MSP.promise(MSPCodes.MSP_SET_MOTOR_3D_CONFIG, mspHelper.crunch(MSPCodes.MSP_SET_MOTOR_3D_CONFIG));
await MSP.promise(MSPCodes.MSP_SET_ADVANCED_CONFIG, mspHelper.crunch(MSPCodes.MSP_SET_ADVANCED_CONFIG));
await MSP.promise(MSPCodes.MSP_SET_ARMING_CONFIG, mspHelper.crunch(MSPCodes.MSP_SET_ARMING_CONFIG));
if (semver.gte(FC.CONFIG.apiVersion, API_VERSION_1_42)) {
await MSP.promise(MSPCodes.MSP_SET_FILTER_CONFIG, mspHelper.crunch(MSPCodes.MSP_SET_FILTER_CONFIG));
}
await MSP.promise(MSPCodes.MSP_EEPROM_WRITE);
analytics.sendSaveAndChangeEvents(analytics.EVENT_CATEGORIES.FLIGHT_CONTROLLER, self.analyticsChanges, 'motors');
self.analyticsChanges = {};
self.configHasChanged = false;
reboot();
});
$('a.stop').on('click', () => motorsEnableTestModeElement.prop('checked', false).trigger('change'));
// get initial motor status values
get_status();
setup_motor_output_reordering_dialog(SetupEscDshotDirectionDialogCallback, zeroThrottleValue);
function SetupEscDshotDirectionDialogCallback() {
SetupdescDshotDirectionDialog(content_ready, zeroThrottleValue);
}
function content_ready() {
GUI.content_ready(callback);
}
content_ready();
}
async function reboot() {
GUI.log(i18n.getMessage('configurationEepromSaved'));
await MSP.promise(MSPCodes.MSP_SET_REBOOT);
reinitializeConnection(self);
}
function showDialogMixerReset(message) {
const dialogMixerReset = $('#dialog-mixer-reset')[0];
$('#dialog-mixer-reset-content').html(message);
if (!dialogMixerReset.hasAttribute('open')) {
dialogMixerReset.showModal();
$('#dialog-mixer-reset-confirmbtn').click(function() {
dialogMixerReset.close();
});
}
}
function showDialogSettingsChanged(message) {
const dialogSettingsChanged = $('#dialog-settings-changed')[0];
$('#dialog-settings-changed-content').html(message);
if (!dialogSettingsChanged.hasAttribute('open')) {
dialogSettingsChanged.showModal();
$('#dialog-settings-reset-confirmbtn').click(function() {
TABS.motors.refresh();
});
$('#dialog-settings-changed-confirmbtn').click(function() {
dialogSettingsChanged.close();
});
}
}
function setup_motor_output_reordering_dialog(callbackFunction, zeroThrottleValue)
{
const domDialogMotorOutputReorder = $('#dialogMotorOutputReorder');
const idleThrottleValue = zeroThrottleValue + 60;
const motorOutputReorderComponent = new MotorOutputReorderComponent($('#dialogMotorOutputReorderContent'),
callbackFunction, mixerList[FC.MIXER_CONFIG.mixer - 1].name,
zeroThrottleValue, idleThrottleValue);
$('#dialogMotorOutputReorder-closebtn').click(closeDialogMotorOutputReorder);
function closeDialogMotorOutputReorder()
{
domDialogMotorOutputReorder[0].close();
motorOutputReorderComponent.close();
$(document).off("keydown", onDocumentKeyPress);
}
function onDocumentKeyPress(event)
{
if (27 === event.which) {
closeDialogMotorOutputReorder();
}
}
$('#motorOutputReorderDialogOpen').click(function()
{
$(document).on("keydown", onDocumentKeyPress);
domDialogMotorOutputReorder[0].showModal();
});
}
function SetupdescDshotDirectionDialog(callbackFunction, zeroThrottleValue)
{
const domEscDshotDirectionDialog = $('#escDshotDirectionDialog');
const idleThrottleValue = zeroThrottleValue + 60;
const motorConfig = {
numberOfMotors: self.numberOfValidOutputs,
motorStopValue: zeroThrottleValue,
motorSpinValue: idleThrottleValue,
escProtocolIsDshot: self.escProtocolIsDshot,
};
const escDshotDirectionComponent = new EscDshotDirectionComponent(
$('#escDshotDirectionDialog-Content'), callbackFunction, motorConfig);
$('#escDshotDirectionDialog-closebtn').on("click", closeEscDshotDirectionDialog);
function closeEscDshotDirectionDialog()
{
domEscDshotDirectionDialog[0].close();
escDshotDirectionComponent.close();
$(document).off("keydown", onDocumentKeyPress);
}
function onDocumentKeyPress(event)
{
if (27 === event.which) {
closeEscDshotDirectionDialog();
}
}
$('#escDshotDirectionDialog-Open').click(function()
{
$(document).on("keydown", onDocumentKeyPress);
domEscDshotDirectionDialog[0].showModal();
});
callbackFunction();
}
};
TABS.motors.refresh = function (callback) {
const self = this;
GUI.tab_switch_cleanup(function() {
self.initialize();
if (callback) {
callback();
}
});
};
TABS.motors.cleanup = function (callback) {
if (callback) callback();
};