Skip to content
Open
2 changes: 1 addition & 1 deletion js/configurator_main.js
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ $(function() {
$('#languageOption').val(i18n.getCurrentLanguage());

$('#languageOption').on('change', () => {
i18n.changeLanguage($('#languageOption').val());
i18n.changeLanguage($('#languageOption').val()).then(GUI.updateProfileNames);
});

// Set the value of the unit type
Expand Down
2 changes: 2 additions & 0 deletions js/fc.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ var FC = {
SENSOR_DATA: null,
MOTOR_DATA: null,
SERVO_DATA: null,
PROFILE_NAMES: null,
GPS_DATA: null,
ADSB_VEHICLES: null,
ADSB_LIMITS: null,
Expand Down Expand Up @@ -294,6 +295,7 @@ var FC = {

this.MOTOR_DATA = new Array(8);
this.SERVO_DATA = new Array(16);
this.PROFILE_NAMES = null;

this.GPS_DATA = {
fix: 0,
Expand Down
18 changes: 17 additions & 1 deletion js/gui.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import MSP from './msp';
import FC from './fc';
import interval from './intervals';
import { scaleRangeInt } from './helpers';
import i18n from './localization';
import i18n from './localization';
import { profileOptionLabel } from './profileNames';
import mspDeduplicationQueue from "./msp/mspDeduplicationQueue";
import mspQueue from './serial_queue';

Expand Down Expand Up @@ -242,6 +243,21 @@ GUI_control.prototype.updateProfileChange = function(refresh) {
}
};

// Label the header profile dropdowns with the user-defined profile names (FC.PROFILE_NAMES),
// falling back to the plain translated labels when the firmware has none.
GUI_control.prototype.updateProfileNames = function () {
const names = FC.PROFILE_NAMES;
const relabel = function ($select, messageKey, list) {
$select.find('option').each(function (index) {
$(this).text(profileOptionLabel(i18n.getMessage(messageKey + (index + 1)), list ? list[index] : ''));
});
};

relabel($('#profilechange'), 'sensorProfile', names ? names.control : null);
relabel($('#batteryprofilechange'), 'sensorBatteryProfile', names ? names.battery : null);
relabel($('#mixerprofilechange'), 'mixerProfile', names ? names.mixer : null);
};

GUI_control.prototype.fillSelect = function ($element, values, currentValue, unit) {
if (unit == null) {
unit = '';
Expand Down
2 changes: 1 addition & 1 deletion js/localization.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ i18n.getLanguages = function() {

i18n.changeLanguage = function(languageSelected) {
store.set('userLanguage', languageSelected);
i18next.changeLanguage(i18n.getValidLocale(languageSelected));
return i18next.changeLanguage(i18n.getValidLocale(languageSelected));
};

i18n.localize = function (reTranslate = false) {
Expand Down
1 change: 1 addition & 0 deletions js/msp/MSPCodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ var MSPCodes = {
MSP2_INAV_EZ_TUNE_SET: 0x2071,

MSP2_INAV_SELECT_MIXER_PROFILE: 0x2080,
MSP2_INAV_PROFILE_NAMES: 0x2082,

MSP2_ADSB_VEHICLE_LIST: 0x2090,
MSP2_ADSB_LIMITS: 0x2091,
Expand Down
18 changes: 18 additions & 0 deletions js/msp/MSPHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import Waypoint from './../waypoint';
import mspDeduplicationQueue from './mspDeduplicationQueue';
import mspStatistics from './mspStatistics';
import settingsCache from './../settingsCache';
import { parseProfileNames } from './../profileNames';
import {Geozone, GeozoneVertex, GeozoneShapes } from './../geozone';
import { parseDronecanAsyncRequestResponse } from './../dronecanAsyncRequestParse';

Expand Down Expand Up @@ -165,6 +166,10 @@ var mspHelper = (function () {
if (profile_changed > 0 || wasUninitialized) {
GUI.updateProfileChange(profile_changed);
}
if (wasUninitialized) {
// first status of this connection: fetch the profile names for the header dropdowns
MSP.send_message(MSPCodes.MSP2_INAV_PROFILE_NAMES, false, false);
}
break;

case MSPCodes.MSP_ACTIVEBOXES:
Expand Down Expand Up @@ -721,6 +726,15 @@ var mspHelper = (function () {
break;
case MSPCodes.MSP_EEPROM_WRITE:
console.log('Settings Saved in EEPROM');
// a save may have renamed the active profile
MSP.send_message(MSPCodes.MSP2_INAV_PROFILE_NAMES, false, false);
break;

case MSPCodes.MSP2_INAV_PROFILE_NAMES:
// null = firmware without the message (unsupported reply) or a malformed payload
FC.PROFILE_NAMES = dataHandler.unsupported ? null : parseProfileNames(data);
GUI.updateProfileNames();
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
GUI.active_tab?.onProfileNamesChanged?.();
break;
case MSPCodes.MSP_DEBUGMSG:
for (var ii = 0; ii < data.byteLength; ii++) {
Expand Down Expand Up @@ -3866,6 +3880,10 @@ var mspHelper = (function () {
MSP.send_message(MSPCodes.MSP2_INAV_MIXER, false, false, callback);
};

self.loadProfileNames = function (callback) {
MSP.send_message(MSPCodes.MSP2_INAV_PROFILE_NAMES, false, false, callback);
};

self.saveMixerConfig = function (callback) {
MSP.send_message(MSPCodes.MSP2_INAV_SET_MIXER, mspHelper.crunch(MSPCodes.MSP2_INAV_SET_MIXER), false, callback);
};
Expand Down
68 changes: 68 additions & 0 deletions js/profileNames.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use strict';

// Decoder for MSP2_INAV_PROFILE_NAMES: the user-defined names of every
// control, battery and mixer profile slot in one reply.
//
// uint8 maxNameLength (MAX_PROFILE_NAME_LENGTH in the firmware)
// uint8 controlProfileCount, then per slot: uint8 length + that many bytes
// uint8 batteryProfileCount, then per slot as above
// uint8 mixerProfileCount, then per slot as above
//
// An unnamed slot has length 0 and decodes to an empty string.

function readNameList(data, cursor) {
if (cursor.offset >= data.byteLength) {
return null;
}

const count = data.getUint8(cursor.offset++);
const names = [];

for (let i = 0; i < count; i++) {
if (cursor.offset >= data.byteLength) {
return null;
}
const length = data.getUint8(cursor.offset++);
if (cursor.offset + length > data.byteLength) {
return null;
}
let name = '';
for (let c = 0; c < length; c++) {
name += String.fromCodePoint(data.getUint8(cursor.offset++));
}
names.push(name);
}

return names;
}

/**
* @param {DataView} data payload of an MSP2_INAV_PROFILE_NAMES reply
* @returns {{maxLength: number, control: string[], battery: string[], mixer: string[]}|null}
* null when the payload is truncated or malformed
*/
export function parseProfileNames(data) {
if (!data || data.byteLength < 4) {
return null;
}

const cursor = { offset: 0 };
const maxLength = data.getUint8(cursor.offset++);
const control = readNameList(data, cursor);
const battery = readNameList(data, cursor);
const mixer = readNameList(data, cursor);

if (control === null || battery === null || mixer === null || cursor.offset !== data.byteLength) {
return null;
}

return { maxLength: maxLength, control: control, battery: battery, mixer: mixer };
}

/**
* Label for a profile selector entry: the translated base label, plus the
* user's name when the slot has one.
*/
export function profileOptionLabel(baseLabel, name) {
return name ? baseLabel + ': ' + name : baseLabel;
}
21 changes: 21 additions & 0 deletions locale/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2135,6 +2135,18 @@
"setMixerProfile": {
"message": "Setting Mixer Profile: <strong style=\"color: #37a8db\">$1</strong>"
},
"controlProfileName": {
"message": "Control profile name"
},
"batteryProfileName": {
"message": "Battery profile name"
},
"mixerProfileName": {
"message": "Mixer profile name"
},
"profileNameHelp": {
"message": "Optional name for this profile slot, shown next to its number in the profile dropdowns at the top. Up to 12 characters. Needs INAV 10 firmware."
},
"setBatteryProfile": {
"message": "Setting Battery Profile: <strong style=\"color: #37a8db\">$1</strong>"
},
Expand Down Expand Up @@ -4877,6 +4889,15 @@
"osdElement_ACTIVE_PROFILE": {
"message": "Show the active profile"
},
"osdElement_CONTROL_PROFILE_NAME": {
"message": "Name of the active control profile (slot symbol and number when unnamed)"
},
"osdElement_BATTERY_PROFILE_NAME": {
"message": "Name of the active battery profile (battery symbol and number when unnamed)"
},
"osdElement_MIXER_PROFILE_NAME": {
"message": "Name of the active mixer profile (M and number when unnamed)"
},
"osdElement_LEVEL_PIDS": {
"message": "Level PIDs"
},
Expand Down
7 changes: 7 additions & 0 deletions tabs/configuration.html
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@
<div class="spacer_box_title" data-i18n="configurationVoltageCurrentSensor"></div>
</div>
<div class="spacer_box">
<div class="number">
<input type="text" id="battery_profile_name" data-setting="battery_profile_name" />
<label for="battery_profile_name">
<span data-i18n="batteryProfileName">Battery profile name</span>
</label>
<div for="battery_profile_name" class="helpicon cf_tip" data-i18n_title="profileNameHelp"></div>
</div>
<div class="features batteryVoltage"></div>
<div class="select">
<select id="vbat_meter_type" data-setting="vbat_meter_type"></select>
Expand Down
7 changes: 7 additions & 0 deletions tabs/mixer.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
<div class="spacer_box_title" data-i18n="platformConfiguration"></div>
</div>
<div class="spacer_box">
<div class="number">
<input type="text" id="mixer_profile_name" data-setting="mixer_profile_name" />
<label for="mixer_profile_name">
<span data-i18n="mixerProfileName">Mixer profile name</span>
</label>
<div for="mixer_profile_name" class="helpicon cf_tip" data-i18n_title="profileNameHelp"></div>
</div>
<div class="select">
<select id="platform-type"></select>
<label for="platform-type">
Expand Down
30 changes: 30 additions & 0 deletions tabs/osd.js
Original file line number Diff line number Diff line change
Expand Up @@ -2243,6 +2243,30 @@ OSD.constants = {
return FONT.symbol(SYM.PROFILE) + '1';
}
},
{
name: 'CONTROL_PROFILE_NAME',
id: 172,
preview: function(osd_data) {
const name = FC.PROFILE_NAMES?.control?.[FC.CONFIG.profile];
return name ? name.toUpperCase() : FONT.symbol(SYM.PROFILE) + (FC.CONFIG.profile + 1);
}
},
{
name: 'BATTERY_PROFILE_NAME',
id: 173,
preview: function(osd_data) {
const name = FC.PROFILE_NAMES?.battery?.[FC.CONFIG.battery_profile];
return name ? name.toUpperCase() : FONT.symbol(SYM.BATT) + (FC.CONFIG.battery_profile + 1);
}
},
{
name: 'MIXER_PROFILE_NAME',
id: 174,
preview: function(osd_data) {
const name = FC.PROFILE_NAMES?.mixer?.[FC.CONFIG.mixer_profile];
return name ? name.toUpperCase() : 'M' + (FC.CONFIG.mixer_profile + 1);
}
},
{
name: 'ROLL_PIDS',
id: 16,
Expand Down Expand Up @@ -4727,6 +4751,12 @@ function updatePanServoPreview() {
OSD.GUI.updatePreviews();
}

osdTab.onProfileNamesChanged = function () {
if (GUI.active_tab === osdTab && $('.tab-osd').length && OSD.data?.items) {
OSD.GUI.updatePreviews();
}
};

osdTab.cleanup = function (callback) {
PortHandler.flush_callbacks();

Expand Down
9 changes: 9 additions & 0 deletions tabs/pid_tuning.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@
<span class="subtab__header_label" for="subtab-mechanics" data-i18n="pidTuning_Mechanics"></span>
</div>
<div id="subtab-pid" class="subtab__content subtab__content--current">
<div class="cf_column profile-name-column">
<div class="number">
<input type="text" id="control_profile_name" data-setting="control_profile_name" />
<label for="control_profile_name">
<span data-i18n="controlProfileName">Control profile name</span>
</label>
<div for="control_profile_name" class="helpicon cf_tip" data-i18n_title="profileNameHelp"></div>
</div>
</div>
<div class="cf_column right" style="margin-top: -6px;">
<div class="default_btn resetbt">
<a href="#" class="action-resetDefaults" data-i18n="pidTuning_SelectNewDefaults"></a>
Expand Down
62 changes: 62 additions & 0 deletions tests/profile-names.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env node
/**
* Tests for parseProfileNames() / profileOptionLabel() (js/profileNames.js),
* the decoder for MSP2_INAV_PROFILE_NAMES used to label the header profile
* dropdowns.
*/

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { parseProfileNames, profileOptionLabel } from '../js/profileNames.js';

function buildPayload(maxLength, lists) {
const bytes = [maxLength];
for (const list of lists) {
bytes.push(list.length);
for (const name of list) {
bytes.push(name.length);
for (const ch of name) {
bytes.push(ch.charCodeAt(0));
}
}
}
return new DataView(Uint8Array.from(bytes).buffer);
}

test('decodes three name lists with empty slots', () => {
const view = buildPayload(12, [['Cruise', '', 'Sport'], ['4S', '6S HV', ''], ['Quad', 'Plane']]);

assert.deepEqual(parseProfileNames(view), {
maxLength: 12,
control: ['Cruise', '', 'Sport'],
battery: ['4S', '6S HV', ''],
mixer: ['Quad', 'Plane'],
});
});

test('handles a single mixer profile and all-empty names', () => {
const view = buildPayload(12, [['', '', ''], ['', '', ''], ['']]);
const names = parseProfileNames(view);

assert.equal(names.mixer.length, 1);
assert.deepEqual(names.control, ['', '', '']);
});

test('rejects truncated, trailing and empty payloads', () => {
const good = buildPayload(12, [['A'], ['B'], ['C']]);

assert.equal(parseProfileNames(new DataView(good.buffer.slice(0, good.byteLength - 1))), null);

const trailing = new Uint8Array(good.byteLength + 1);
trailing.set(new Uint8Array(good.buffer));
assert.equal(parseProfileNames(new DataView(trailing.buffer)), null);

assert.equal(parseProfileNames(new DataView(new ArrayBuffer(0))), null);
assert.equal(parseProfileNames(null), null);
});

test('option label appends the name only when there is one', () => {
assert.equal(profileOptionLabel('Control Profile 1', 'Cruise'), 'Control Profile 1: Cruise');
assert.equal(profileOptionLabel('Control Profile 2', ''), 'Control Profile 2');
assert.equal(profileOptionLabel('Control Profile 3', undefined), 'Control Profile 3');
});
Loading