You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
inav-configurator/tabs/outputs.js

761 lines
27 KiB
JavaScript

/*global helper,MSP,MSPChainerClass,googleAnalytics,GUI,mspHelper,MOTOR_RULES,TABS,$,MSPCodes,ANALOG,MOTOR_DATA,chrome,PLATFORM_MULTIROTOR,BF_CONFIG,PLATFORM_TRICOPTER,SERVO_RULES,FC,SERVO_CONFIG,SENSOR_DATA,REVERSIBLE_MOTORS,MISC,MIXER_CONFIG,OUTPUT_MAPPING*/
'use strict';
TABS.outputs = {
allowTestMode: false,
feature3DEnabled: false,
feature3DSupported: false
};
TABS.outputs.initialize = function (callback) {
var self = this;
self.armed = false;
self.feature3DSupported = false;
self.allowTestMode = true;
var $motorsEnableTestMode;
if (GUI.active_tab !== 'outputs') {
GUI.active_tab = 'outputs';
googleAnalytics.sendAppView('Outputs');
}
var loadChainer = new MSPChainerClass();
loadChainer.setChain([
mspHelper.loadMiscV2,
mspHelper.loadBfConfig,
mspHelper.load3dConfig,
mspHelper.loadMotors,
mspHelper.loadMotorMixRules,
mspHelper.loadServoMixRules,
mspHelper.loadMixerConfig,
mspHelper.loadServoMixRules,
mspHelper.loadServoConfiguration,
mspHelper.loadOutputMapping,
mspHelper.loadRcData,
mspHelper.loadAdvancedConfig,
]);
loadChainer.setExitPoint(load_html);
loadChainer.execute();
update_arm_status();
var saveChainer = new MSPChainerClass();
saveChainer.setChain([
saveSettings,
mspHelper.sendServoConfigurations,
mspHelper.saveAdvancedConfig,
mspHelper.saveBfConfig,
mspHelper.saveMiscV2,
mspHelper.saveToEeprom
]);
saveChainer.setExitPoint(function () {
GUI.log(chrome.i18n.getMessage('eeprom_saved_ok'));
MOTOR_RULES.cleanup();
});
function load_html() {
GUI.load("./tabs/outputs.html", Settings.processHtml(onLoad));
}
function saveSettings(onComplete) {
Settings.saveInputs().then(onComplete);
}
function onLoad() {
self.feature3DEnabled = bit_check(BF_CONFIG.features, 12);
process_motors();
process_servos();
processConfiguration();
if (semver.gte(CONFIG.flightControllerVersion, "2.4.0")) {
$('.requires-v2_4').show();
} else {
$('.requires-v2_4').hide();
}
finalize();
}
function getMotorOutputValue(value) {
if (!self.feature3DEnabled) {
let valueNormalized = value - MISC.mincommand;
let maxThrottleNormalized = MISC.maxthrottle - MISC.mincommand;
return Math.round(100 * valueNormalized / maxThrottleNormalized) + "%";
} else {
return value;
}
}
function processConfiguration() {
let escProtocols = FC.getEscProtocols(),
servoRates = FC.getServoRates(),
$idlePercent = $('#throttle_idle'),
$idleInfoBox = $("#throttle_idle-info"),
$motorStopWarningBox = $("#motor-stop-warning"),
$reversibleMotorBox = $("#reversible-esc-info")
function buildMotorRates() {
var protocolData = escProtocols[ADVANCED_CONFIG.motorPwmProtocol];
$escRate.find('option').remove();
for (var i in protocolData.rates) {
if (protocolData.rates.hasOwnProperty(i)) {
$escRate.append('<option value="' + i + '">' + protocolData.rates[i] + '</option>');
}
}
/*
* If rate from FC is not on the list, add a new entry
*/
if ($escRate.find('[value="' + ADVANCED_CONFIG.motorPwmRate + '"]').length == 0) {
$escRate.append('<option value="' + ADVANCED_CONFIG.motorPwmRate + '">' + ADVANCED_CONFIG.motorPwmRate + 'Hz</option>');
}
$idleInfoBox.hide();
if (ADVANCED_CONFIG.motorPwmProtocol >= 5) {
$('.hide-for-shot').hide();
if ($idlePercent.val() > 7.0) {
$idleInfoBox.html(chrome.i18n.getMessage('throttleIdleDigitalInfo'));
$idleInfoBox.addClass('ok-box');
$idleInfoBox.show();
}
} else {
$('.hide-for-shot').show();
if ($idlePercent.val() > 10.0) {
$idleInfoBox.html(chrome.i18n.getMessage('throttleIdleAnalogInfo'));
$idleInfoBox.addClass('ok-box');
$idleInfoBox.show();
}
}
if (protocolData.message !== null) {
$('#esc-protocol-warning').html(chrome.i18n.getMessage(protocolData.message));
$('#esc-protocol-warning').show();
} else {
$('#esc-protocol-warning').hide();
}
}
let $escProtocol = $('#esc-protocol');
let $escRate = $('#esc-rate');
for (i in escProtocols) {
if (escProtocols.hasOwnProperty(i)) {
var protocolData = escProtocols[i];
$escProtocol.append('<option value="' + i + '">' + protocolData.name + '</option>');
}
}
$escProtocol.val(ADVANCED_CONFIG.motorPwmProtocol);
buildMotorRates();
$escRate.val(ADVANCED_CONFIG.motorPwmRate);
$escProtocol.change(function () {
ADVANCED_CONFIG.motorPwmProtocol = $(this).val();
buildMotorRates();
ADVANCED_CONFIG.motorPwmRate = escProtocols[ADVANCED_CONFIG.motorPwmProtocol].defaultRate;
$escRate.val(ADVANCED_CONFIG.motorPwmRate);
});
$escRate.change(function () {
ADVANCED_CONFIG.motorPwmRate = $(this).val();
});
$idlePercent.change(buildMotorRates);
$("#esc-protocols").show();
let $servoRate = $('#servo-rate');
for (i in servoRates) {
if (servoRates.hasOwnProperty(i)) {
$servoRate.append('<option value="' + i + '">' + servoRates[i] + '</option>');
}
}
/*
* If rate from FC is not on the list, add a new entry
*/
if ($servoRate.find('[value="' + ADVANCED_CONFIG.servoPwmRate + '"]').length == 0) {
$servoRate.append('<option value="' + ADVANCED_CONFIG.servoPwmRate + '">' + ADVANCED_CONFIG.servoPwmRate + 'Hz</option>');
}
$servoRate.val(ADVANCED_CONFIG.servoPwmRate);
$servoRate.change(function () {
ADVANCED_CONFIG.servoPwmRate = $(this).val();
});
$('#servo-rate-container').show();
helper.features.updateUI($('.tab-motors'), BF_CONFIG.features);
GUI.simpleBind();
let $reversibleMotorCheckbox = $('#feature-12');
function showHideReversibleMotorInfo() {
const reversibleMotorEnabled = $reversibleMotorCheckbox.is(':checked');
console.log(reversibleMotorEnabled);
if (reversibleMotorEnabled) {
$reversibleMotorBox.show();
} else {
$reversibleMotorBox.hide();
}
}
$reversibleMotorCheckbox.change(showHideReversibleMotorInfo);
showHideReversibleMotorInfo();
let $motorStopCheckbox = $('#feature-4');
function showHideMotorStopWarning() {
const platformNeedsMotorStop = [PLATFORM_AIRPLANE, PLATFORM_ROVER, PLATFORM_BOAT].includes(MIXER_CONFIG.platformType);
const motorStopEnabled = $motorStopCheckbox.is(':checked');
if (platformNeedsMotorStop && motorStopEnabled || !platformNeedsMotorStop && !motorStopEnabled) {
$motorStopWarningBox.hide();
} else {
$motorStopWarningBox.show();
}
}
$motorStopCheckbox.change(showHideMotorStopWarning);
showHideMotorStopWarning();
}
function update_arm_status() {
self.armed = FC.isModeEnabled('ARM');
}
function initSensorData() {
for (var i = 0; i < 3; i++) {
SENSOR_DATA.accelerometer[i] = 0;
}
}
function initDataArray(length) {
var data = new Array(length);
for (var i = 0; i < length; i++) {
data[i] = [];
data[i].min = -1;
data[i].max = 1;
}
return data;
}
function addSampleToData(data, sampleNumber, sensorData) {
for (var i = 0; i < data.length; i++) {
var 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 > 40) {
for (i = 0; i < data.length; i++) {
data[i].shift();
}
}
return sampleNumber + 1;
}
function update_model(val) {
if (MIXER_CONFIG.appliedMixerPreset == -1) return;
$('.mixerPreview img').attr('src', './resources/motor_order/'
+ helper.mixer.getById(val).image + '.svg');
}
function process_servos() {
let $tabServos = $(".tab-servos"),
$servoEmptyTableInfo = $('#servoEmptyTableInfo'),
$servoConfigTableContainer = $('#servo-config-table-container'),
$servoConfigTable = $('#servo-config-table');
if (SERVO_CONFIG.length == 0) {
$tabServos.addClass("is-hidden");
return;
}
function renderServos(name, alternate, obj) {
$servoConfigTable.append('\
<tr> \
<td class="text-center">' + name + '</td>\
<td class="middle"><input type="number" min="500" max="2500" value="' + SERVO_CONFIG[obj].middle + '" /></td>\
<td class="min"><input type="number" min="500" max="2500" value="' + SERVO_CONFIG[obj].min + '" /></td>\
<td class="max"><input type="number" min="500" max="2500" value="' + SERVO_CONFIG[obj].max + '" /></td>\
<td class="text-center rate">\
<td class="text-center reverse">\
</td>\
</tr> \
');
let $currentRow = $servoConfigTable.find('tr:last');
//This routine is pre 2.0 only
if (SERVO_CONFIG[obj].indexOfChannelToForward >= 0) {
$currentRow.find('td.channel input').eq(SERVO_CONFIG[obj].indexOfChannelToForward).prop('checked', true);
}
// adding select box and generating options
$currentRow.find('td.rate').append(
'<input class="rate-input" type="number" min="' + FC.MIN_SERVO_RATE + '" max="' + FC.MAX_SERVO_RATE + '" value="' + Math.abs(SERVO_CONFIG[obj].rate) + '" />'
);
$currentRow.find('td.reverse').append(
'<input type="checkbox" class="reverse-input togglemedium" ' + (SERVO_CONFIG[obj].rate < 0 ? ' checked ' : '') + '/>'
);
$currentRow.data('info', { 'obj': obj });
$currentRow.append('<td class="text-center output"></td>');
let output,
outputString;
if (MIXER_CONFIG.platformType == PLATFORM_MULTIROTOR || MIXER_CONFIG.platformType == PLATFORM_TRICOPTER) {
output = OUTPUT_MAPPING.getMrServoOutput(usedServoIndex);
} else {
output = OUTPUT_MAPPING.getFwServoOutput(usedServoIndex);
}
if (output === null) {
outputString = "-";
} else {
outputString = "S" + output;
}
$currentRow.find('.output').html(outputString);
//For 2.0 and above hide a row when servo is not configured
if (!SERVO_RULES.isServoConfigured(obj)) {
$currentRow.hide();
} else {
usedServoIndex++;
}
}
function servos_update() {
$servoConfigTable.find('tr:not(".main")').each(function () {
var info = $(this).data('info');
var selection = $('.channel input', this);
var channelIndex = parseInt(selection.index(selection.filter(':checked')));
if (channelIndex == -1) {
channelIndex = undefined;
}
SERVO_CONFIG[info.obj].indexOfChannelToForward = channelIndex;
SERVO_CONFIG[info.obj].middle = parseInt($('.middle input', this).val());
SERVO_CONFIG[info.obj].min = parseInt($('.min input', this).val());
SERVO_CONFIG[info.obj].max = parseInt($('.max input', this).val());
var rate = parseInt($('.rate-input', this).val());
if ($('.reverse-input', this).is(':checked')) {
rate = -rate;
}
SERVO_CONFIG[info.obj].rate = rate;
});
//Save configuration to FC
saveChainer.execute();
}
// drop previous table
$servoConfigTable.find('tr:not(:first)').remove();
let usedServoIndex = 0;
for (let servoIndex = 0; servoIndex < SERVO_RULES.getServoCount(); servoIndex++) {
renderServos('Servo ' + servoIndex, '', servoIndex);
}
if (usedServoIndex == 0) {
// No servos configured
$servoEmptyTableInfo.show();
$servoConfigTableContainer.hide();
} else {
$servoEmptyTableInfo.hide();
$servoConfigTableContainer.show();
}
// UI hooks for dynamically generated elements
$('table.directions select, table.directions input, #servo-config-table select, #servo-config-table input').change(function () {
if ($('div.live input').is(':checked')) {
// apply small delay as there seems to be some funky update business going wrong
helper.timeout.add('servos_update', servos_update, 10);
}
});
$('a.update').click(function () {
helper.features.reset();
helper.features.fromUI($('.tab-motors'));
helper.features.execute(servos_update);
});
$('a.save').click(function () {
saveChainer.setExitPoint(function () {
//noinspection JSUnresolvedVariable
GUI.log(chrome.i18n.getMessage('configurationEepromSaved'));
GUI.tab_switch_cleanup(function () {
MSP.send_message(MSPCodes.MSP_SET_REBOOT, false, false, function () {
GUI.log(chrome.i18n.getMessage('deviceRebooting'));
GUI.handleReconnect($('.tab_outputs a'));
});
});
});
helper.features.reset();
helper.features.fromUI($('.tab-motors'));
helper.features.execute(servos_update);
});
}
function process_motors() {
$motorsEnableTestMode = $('#motorsEnableTestMode');
if (self.feature3DEnabled && !self.feature3DSupported) {
self.allowTestMode = false;
}
$motorsEnableTestMode.prop('checked', false);
$motorsEnableTestMode.prop('disabled', true);
update_model(MIXER_CONFIG.appliedMixerPreset);
// Always start with default/empty sensor data array, clean slate all
initSensorData();
// Setup variables
var samples_accel_i = 0,
accel_data = initDataArray(3),
accel_max_read = [0, 0, 0],
accel_offset = [0, 0, 0],
accel_offset_established = false;
let $rmsHelper = $(".acc-rms"),
$currentHelper = $(".current-current"),
$voltageHelper = $(".current-voltage");
// timer initialization
helper.interval.killAll(['motor_and_status_pull', 'global_data_refresh', 'msp-load-update']);
helper.mspBalancedInterval.flush();
helper.interval.add('IMU_pull', function () {
/*
* Enable balancer
*/
if (helper.mspQueue.shouldDrop()) {
update_accel_graph();
return;
}
MSP.send_message(MSPCodes.MSP_RAW_IMU, false, false, update_accel_graph);
}, 25, true);
helper.interval.add('ANALOG_pull', function () {
$currentHelper.html(ANALOG.amperage.toFixed(2));
$voltageHelper.html(ANALOG.voltage.toFixed(2));
}, 100, true);
function update_accel_graph() {
if (!accel_offset_established) {
for (var i = 0; i < 3; i++) {
accel_offset[i] = SENSOR_DATA.accelerometer[i] * -1;
}
accel_offset_established = true;
}
var accel_with_offset = [
accel_offset[0] + SENSOR_DATA.accelerometer[0],
accel_offset[1] + SENSOR_DATA.accelerometer[1],
accel_offset[2] + SENSOR_DATA.accelerometer[2]
];
samples_accel_i = addSampleToData(accel_data, samples_accel_i, accel_with_offset);
// Compute RMS of acceleration in displayed period of time
// This is particularly useful for motor balancing as it
// eliminates the need for external tools
var sum = 0.0;
for (var j = 0; j < accel_data.length; j++)
for (var k = 0; k < accel_data[j].length; k++)
sum += accel_data[j][k][1] * accel_data[j][k][1];
let rms = Math.sqrt(sum / (accel_data[0].length + accel_data[1].length + accel_data[2].length));
$rmsHelper.text(rms.toFixed(4));
for (var i = 0; i < 3; i++) {
if (Math.abs(accel_with_offset[i]) > Math.abs(accel_max_read[i])) accel_max_read[i] = accel_with_offset[i];
}
}
let motors_wrapper = $('.motors .bar-wrapper'),
servos_wrapper = $('.servos .bar-wrapper'),
$motorTitles = $('.motor-titles'),
$motorSliders = $('.motor-sliders'),
$motorValues = $('.motor-values');
for (let i = 0; i < MOTOR_RULES.getNumberOfConfiguredMotors(); i++) {
const motorNumber = i + 1;
motors_wrapper.append('\
<div class="m-block motor-' + i + '">\
<div class="meter-bar">\
<div class="label"></div>\
<div class="indicator">\
<div class="label">\
<div class="label"></div>\
</div>\
</div>\
</div>\
</div>\
');
$motorTitles.append('<li title="Motor - ' + motorNumber + '">' + motorNumber + '</li>');
$motorSliders.append('<div class="motor-slider-container"><input type="range" min="1000" max="2000" value="1000" disabled="disabled"/></div>');
$motorValues.append('<li>0%</li>');
}
$motorSliders.append('<div class="motor-slider-container"><input type="range" min="1000" max="2000" value="1000" disabled="disabled" class="master"/></div>');
$motorValues.append('<li style="font-weight: bold" data-i18n="motorsMaster"></li>');
for (let i = 0; i < SERVO_RULES.getServoCount(); i++) {
let opacity = "";
if (!SERVO_RULES.isServoConfigured(15 - i)) {
opacity = ' style="opacity: 0.2"';
}
servos_wrapper.append('\
<div class="m-block servo-' + (15 - i) + '" ' + opacity + '>\
<div class="meter-bar">\
<div class="label"></div>\
<div class="indicator">\
<div class="label">\
<div class="label"></div>\
</div>\
</div>\
</div>\
</div>\
');
}
var $slidersInput = $('div.sliders input');
$slidersInput.prop('min', MISC.mincommand);
$slidersInput.prop('max', MISC.maxthrottle);
$('div.values li:not(:last)').text(MISC.mincommand);
if (self.feature3DEnabled && self.feature3DSupported) {
//Arbitrary sanity checks
//Note: values may need to be revisited
if (REVERSIBLE_MOTORS.neutral > 1575 || REVERSIBLE_MOTORS.neutral < 1425)
REVERSIBLE_MOTORS.neutral = 1500;
$slidersInput.val(REVERSIBLE_MOTORS.neutral);
} else {
$slidersInput.val(MISC.mincommand);
}
if (self.allowTestMode) {
// UI hooks
var buffering_set_motor = [],
buffer_delay = false;
$('div.sliders input:not(.master)').on('input', function () {
var index = $('div.sliders input:not(.master)').index(this),
buffer = [],
i;
$('div.values li').eq(index).text(getMotorOutputValue($(this).val()));
for (i = 0; i < 8; i++) {
var val = parseInt($('div.sliders input').eq(i).val());
buffer.push(lowByte(val));
buffer.push(highByte(val));
}
buffering_set_motor.push(buffer);
if (!buffer_delay) {
buffer_delay = setTimeout(function () {
buffer = buffering_set_motor.pop();
MSP.send_message(MSPCodes.MSP_SET_MOTOR, buffer);
buffering_set_motor = [];
buffer_delay = false;
}, 10);
}
});
}
$('div.sliders input.master').on('input', function () {
var val = $(this).val();
$('div.sliders input:not(:disabled, :last)').val(val);
$('div.values li:not(:last)').slice(0, MOTOR_RULES.getNumberOfConfiguredMotors()).text(getMotorOutputValue(val));
$('div.sliders input:not(:last):first').trigger('input');
});
$motorsEnableTestMode.change(function () {
if ($(this).is(':checked')) {
$slidersInput.slice(0, MOTOR_RULES.getNumberOfConfiguredMotors()).prop('disabled', false);
// unlock master slider
$('div.sliders input:last').prop('disabled', false);
} else {
// disable sliders / min max
$slidersInput.prop('disabled', true);
// change all values to default
if (self.feature3DEnabled && self.feature3DSupported) {
$slidersInput.val(REVERSIBLE_MOTORS.neutral);
} else {
$slidersInput.val(MISC.mincommand);
}
$slidersInput.trigger('input');
}
});
// check if motors are already spinning
var motors_running = false;
for (var i = 0; i < MOTOR_RULES.getNumberOfConfiguredMotors(); i++) {
if (!self.feature3DEnabled) {
if (MOTOR_DATA[i] > MISC.mincommand) {
motors_running = true;
break;
}
} else {
if ((MOTOR_DATA[i] < REVERSIBLE_MOTORS.deadband_low) || (MOTOR_DATA[i] > REVERSIBLE_MOTORS.deadband_high)) {
motors_running = true;
break;
}
}
}
if (motors_running) {
if (!self.armed && self.allowTestMode) {
$motorsEnableTestMode.prop('checked', true);
}
// motors are running adjust sliders to current values
var sliders = $('div.sliders input:not(.master)');
var master_value = MOTOR_DATA[0];
for (var i = 0; i < MOTOR_DATA.length; i++) {
if (MOTOR_DATA[i] > 0) {
sliders.eq(i).val(MOTOR_DATA[i]);
if (master_value != MOTOR_DATA[i]) {
master_value = false;
}
}
}
// only fire events when all values are set
sliders.trigger('input');
// slide master slider if condition is valid
if (master_value) {
$('div.sliders input.master').val(master_value);
$('div.sliders input.master').trigger('input');
}
}
$motorsEnableTestMode.change();
function getPeriodicMotorOutput() {
if (helper.mspQueue.shouldDrop()) {
getPeriodicServoOutput();
return;
}
MSP.send_message(MSPCodes.MSP_MOTOR, false, false, getPeriodicServoOutput);
}
function getPeriodicServoOutput() {
if (helper.mspQueue.shouldDrop()) {
update_ui();
return;
}
MSP.send_message(MSPCodes.MSP_SERVO, false, false, update_ui);
}
var full_block_scale = MISC.maxthrottle - MISC.mincommand;
function update_ui() {
var previousArmState = self.armed,
block_height = $('div.m-block:first').height(),
data,
margin_top,
height,
color,
i;
for (i = 0; i < MOTOR_DATA.length; i++) {
data = MOTOR_DATA[i] - MISC.mincommand;
margin_top = block_height - (data * (block_height / full_block_scale)).clamp(0, block_height);
height = (data * (block_height / full_block_scale)).clamp(0, block_height);
color = parseInt(data * 0.009);
$('.motor-' + i + ' .label', motors_wrapper).text(getMotorOutputValue(MOTOR_DATA[i]));
$('.motor-' + i + ' .indicator', motors_wrapper).css({ 'margin-top': margin_top + 'px', 'height': height + 'px', 'background-color': '#37a8db' + color + ')' });
}
// servo indicators are still using old (not flexible block scale), it will be changed in the future accordingly
for (i = 0; i < SERVO_DATA.length; i++) {
data = SERVO_DATA[i] - 1000;
margin_top = block_height - (data * (block_height / 1000)).clamp(0, block_height);
height = (data * (block_height / 1000)).clamp(0, block_height);
color = parseInt(data * 0.009);
$('.servo-' + i + ' .label', servos_wrapper).text(SERVO_DATA[i]);
$('.servo-' + i + ' .indicator', servos_wrapper).css({ 'margin-top': margin_top + 'px', 'height': height + 'px', 'background-color': '#37a8db' + color + ')' });
}
//keep the following here so at least we get a visual cue of our motor setup
update_arm_status();
if (!self.allowTestMode) return;
if (self.armed) {
$motorsEnableTestMode.prop('disabled', true);
$motorsEnableTestMode.prop('checked', false);
} else {
if (self.allowTestMode) {
$motorsEnableTestMode.prop('disabled', false);
}
}
if (previousArmState != self.armed) {
console.log('arm state change detected');
$motorsEnableTestMode.change();
}
}
// enable Status and Motor data pulling
helper.interval.add('motor_and_status_pull', getPeriodicMotorOutput, 100, true);
}
function finalize() {
localize();
GUI.content_ready(callback);
}
};
TABS.outputs.cleanup = function (callback) {
if (callback) callback();
};