Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
7923528
feat: add DroneCAN tab to configurator (phases 1-2, WIP)
daijoubu Jun 1, 2026
a59c7b9
fix: resolve DroneCAN tab initialization and display bugs
daijoubu Jun 1, 2026
450e5a3
refactor(dronecan): match standard tab HTML structure and styling
daijoubu Jun 1, 2026
5ce95ce
feat(dronecan): populate node name in table via cached NODE_INFO requ…
daijoubu Jun 1, 2026
550555f
feat(dronecan): add last seen to node detail; remove uptime from node…
daijoubu Jun 1, 2026
9f9f2f0
fix(dronecan): align health badge colours with Mission Planner/QGC co…
daijoubu Jun 1, 2026
31aefd7
feat(dronecan): add bus configuration section (Phase 5)
daijoubu Jun 1, 2026
c5648bf
feat(dronecan): add tab icon (CAN bus topology)
daijoubu Jun 1, 2026
d0a5d9b
fix(fc): remove merge artifacts from fc.js
daijoubu Jun 1, 2026
b469b84
fix(dronecan): reset DRONECAN state on disconnect, clear nameCache on…
daijoubu Jun 1, 2026
c7703fb
fix(dronecan): address code review findings #3 #4 #7 #8 #10
daijoubu Jun 1, 2026
7eafe82
feat(gps): add CRSF and DroneCAN to GPS protocol dropdown
daijoubu Jun 1, 2026
32627de
refactor(gps): move protocol dropdown above port/baud selects
daijoubu Jun 1, 2026
e04e21a
fix(gps): use neutral grey for DroneCAN info note, not blue
daijoubu Jun 1, 2026
1a28bf9
fix(gps): scope DroneCAN info note style to span, not container div
daijoubu Jun 1, 2026
672af5a
fix(gps): replace info note box style with plain italic text
daijoubu Jun 1, 2026
2cd8e29
refactor(gps): move DroneCAN info note inside protocol select element
daijoubu Jun 1, 2026
b1617f5
test(dronecan): add MSP NODE_INFO 71-byte decoder spec test (red)
daijoubu Jun 2, 2026
1f24fb3
feat(dronecan): Phase 6 — decode and display node SW/HW version in de…
daijoubu Jun 2, 2026
af551c8
feat(dronecan): async poll pattern for GetNodeInfo and param GetSet UI
daijoubu Jun 3, 2026
431fc91
feat(dronecan): Save to EEPROM, Restart Node, live detail panel, stal…
daijoubu Jun 3, 2026
104ec7f
fix(dronecan): sign-extend int64 param values correctly
daijoubu Jun 3, 2026
19072d5
fix(dronecan): code review cleanup — guards, selector scoping, font-size
daijoubu Jun 3, 2026
ed606ff
fix(dronecan): code review pass 2 — XSS escaping, latent offset comme…
daijoubu Jun 3, 2026
0269d47
fix(dronecan): code review pass 3 — bounds safety, stale guards, cons…
daijoubu Jun 3, 2026
b14db68
fix(dronecan): code review pass 5 — escaping, bounds, refactor
daijoubu Jun 3, 2026
3596ed9
feat(dronecan): display param range and validate write against min/max
daijoubu Jun 3, 2026
40c4152
i18n(dronecan): replace hardcoded strings with i18n.getMessage() calls
daijoubu Jun 3, 2026
6fed3f3
fix(dronecan): code review pass — named constants, decodeNumeric guar…
daijoubu Jun 4, 2026
97a74e2
fix(dronecan): code review pass — named constants, i18n column header…
daijoubu Jun 4, 2026
13b0349
fix(dronecan): guard against NaN input freezing Write button
daijoubu Jun 4, 2026
cb26f3f
fix(dronecan): code review pass — service ID constants, PARAM_TYPE sc…
daijoubu Jun 4, 2026
641b9aa
fix(dronecan): add DRONECAN_SERVICE_* constants to MSPHelper IIFE scope
daijoubu Jun 4, 2026
00f3235
feat(dronecan): code review pass 6 fixes
daijoubu Jun 4, 2026
0905d6c
fix(dronecan): add DRONECAN_ASYNC_STATE_READY constant to MSPHelper s…
daijoubu Jun 4, 2026
743a83e
fix(dronecan): resolve SonarQube quality gate failures on PR #2671
daijoubu Jul 4, 2026
4748f32
fix(dronecan): flatten nested ternary and use Number.NaN in write-val…
daijoubu Jul 5, 2026
f2856fa
fix(dronecan): validate FC node ID range before saving
daijoubu Jun 6, 2026
e16a877
fix(dronecan): retry stale async poll results instead of failing imme…
daijoubu Jun 6, 2026
5d25ae9
fix(dronecan): fix dead CSS selector and remove unused locale key
daijoubu Jun 6, 2026
040c8ad
feat(gps): show DroneCAN GPS node ID field when DroneCAN provider sel…
daijoubu Jun 10, 2026
ecb337b
feat(gps): hide serial/u-blox controls when DroneCAN provider selected
daijoubu Jun 10, 2026
70061e8
feat(dronecan): add DroneCAN battery ID filter field to configuration…
daijoubu Jun 11, 2026
24c667d
fix(gps): correct dronecan_gps_node_id max from 125 to 127
daijoubu Jul 6, 2026
8e14df5
fix(ui): decouple conditional row visibility from generic setting aut…
daijoubu Jul 6, 2026
a878697
fix(ui): resolve battery-ID row findings from second review pass
daijoubu Jul 7, 2026
6c16d7d
fix(ui): resolve findings from third review pass
daijoubu Jul 7, 2026
a3d4b41
fix(a11y): add English fallback text for new i18n labels
daijoubu Jul 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions images/icons/cf_icon_dronecan_grey.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions images/icons/cf_icon_dronecan_white.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,13 @@ <h2 class="groundstation-telemetry__header" data-i18n="gsTelemetry"></h2>
<li class="tab_led_strip">
<a href="#" data-i18n="tabLedStrip" class="tabicon ic_led" title="LED Strip"></a>
</li>
<li class="tab_dronecan">
<a href="#" data-i18n="tabDroneCAN" class="tabicon ic_dronecan" title="DroneCAN"></a>
</li>

</ul>
</li>

<!-- Data Logging -->
<li class="nav-group">
<div class="group-header" role="button" tabindex="0" aria-expanded="false">
Expand Down
93 changes: 5 additions & 88 deletions js/configurator_main.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import configurationTab from './../tabs/configuration';
import pidTuningTab from './../tabs/pid_tuning';
import receiverTab from './../tabs/receiver';
import gpsTab from './../tabs/gps';
import dronecanTab from './../tabs/dronecan';
import magnetometerTab from './../tabs/magnetometer';
import missionControlTab from './../tabs/mission_control';
import mixerTab from './../tabs/mixer';
Expand Down Expand Up @@ -116,7 +117,7 @@ $(function() {
var ui_tabs = $('#tabs > ul');
$('a', ui_tabs).on('click', function() {

if ($(this).parent().hasClass("tab_help")) {
if ($(this).parent().hasClass("tab_help") || $(this).parent().hasClass("nav-toggle-all")) {
return;
}

Expand Down Expand Up @@ -231,6 +232,9 @@ $(function() {
case 'gps':
gpsTab.initialize(content_ready);
break;
case 'dronecan':
dronecanTab.initialize(content_ready);
break;
case 'magnetometer':
magnetometerTab.initialize(content_ready);
break;
Expand Down Expand Up @@ -360,84 +364,6 @@ $(function() {
// Update button state on initialization
updateToggleAllButton();


// Accordion Navigation Groups
$('.group-header').on('click', function(e) {
e.stopPropagation(); // Prevent triggering tab click
const header = $(this);
const items = header.next('.group-items');

// Toggle this group
header.toggleClass('active');
items.toggleClass('expanded');

// Update aria-expanded for accessibility
header.attr('aria-expanded', header.hasClass('active'));

// Update the expand/collapse all button state
updateToggleAllButton();
});

// Keyboard accessibility for accordion headers
$('.group-header').on('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
$(this).trigger('click');
}
});

function updateToggleAllButton() {
const allExpanded = $('.nav-group .group-header.active').length === $('.nav-group .group-header').length;
const $expandIcon = $('#toggleAllGroups .expand-icon');
const $collapseIcon = $('#toggleAllGroups .collapse-icon');
const $toggleText = $('#toggleAllGroups .toggle-text');

if (allExpanded) {
$expandIcon.hide();
$collapseIcon.show();
$toggleText.attr('data-i18n', 'navCollapseAll');
$toggleText.text(i18n.getMessage('navCollapseAll'));
} else {
$expandIcon.show();
$collapseIcon.hide();
$toggleText.attr('data-i18n', 'navExpandAll');
$toggleText.text(i18n.getMessage('navExpandAll'));
}
}

// Expand/Collapse All Toggle
$('#toggleAllGroups').on('click', function(e) {
e.preventDefault();
const allExpanded = $('.nav-group .group-header.active').length === $('.nav-group .group-header').length;

if (allExpanded) {
// Collapse all except first
$('.nav-group .group-header').removeClass('active').attr('aria-expanded', 'false');
$('.nav-group .group-items').removeClass('expanded');
$('#tabs ul.mode-connected .nav-group:first-child .group-header').addClass('active').attr('aria-expanded', 'true');
$('#tabs ul.mode-connected .nav-group:first-child .group-items').addClass('expanded');
store.set('expand_all_groups', false);
} else {
// Expand all
$('.nav-group .group-header').addClass('active').attr('aria-expanded', 'true');
$('.nav-group .group-items').addClass('expanded');
store.set('expand_all_groups', true);
}

updateToggleAllButton();
});

// Initialize: apply saved expand all preference or expand first group by default
if (store.get('expand_all_groups', false)) {
$('.nav-group .group-header').addClass('active').attr('aria-expanded', 'true');
$('.nav-group .group-items').addClass('expanded');
} else {
$('#tabs ul.mode-connected .nav-group:first-child .group-header').addClass('active').attr('aria-expanded', 'true');
$('#tabs ul.mode-connected .nav-group:first-child .group-items').addClass('expanded');
}

updateToggleAllButton();

// options
$('#options').on('click', function() {
var el = $(this);
Expand Down Expand Up @@ -477,15 +403,6 @@ $(function() {
store.set('disable_3d_acceleration', check);
});

if (store.get('disable_3d_acceleration', false)) {
$('div.disable_3d_acceleration input').prop('checked', true);
}

$('div.disable_3d_acceleration input').on('change', function () {
var check = $(this).is(':checked');
store.set('disable_3d_acceleration', check);
});

$('div.statistics input').on('change', function () {
var check = $(this).is(':checked');
});
Expand Down
11 changes: 10 additions & 1 deletion js/fc.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ var FC = {
EZ_TUNE: null,
FLIGHT_MODES: null,
GEOZONES: null,
DRONECAN_NODES: [],
DRONECAN_ASYNC_REQUEST: null,
DRONECAN_ASYNC_RESULT: null,

restartRequired: false,
MAX_SERVO_RATE: 125,
Expand Down Expand Up @@ -611,6 +614,10 @@ var FC = {
items: [],
};

this.DRONECAN_NODES = [];
this.DRONECAN_ASYNC_REQUEST = null;
this.DRONECAN_ASYNC_RESULT = null;

},
getOutputUsages: function() {
return {
Expand Down Expand Up @@ -666,7 +673,9 @@ var FC = {
return [
'UBLOX',
'MSP',
'FAKE'
'CRSF',
'FAKE',
'DRONECAN'
];
},
getGpsBaudRates: function () {
Expand Down
1 change: 1 addition & 0 deletions js/gui.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ var GUI_control = function () {
'cli',
'configuration',
'gps',
'dronecan',
'magnetometer',
'led_strip',
'logging',
Expand Down
4 changes: 4 additions & 0 deletions js/msp/MSPCodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ var MSPCodes = {
MSP2_INAV_LOGIC_CONDITIONS_SINGLE: 0x203B,
MSP2_INAV_LOGIC_CONDITIONS_CONFIGURED: 0x203C,

MSP2_INAV_DRONECAN_NODES: 0x2042,
MSP2_INAV_DRONECAN_ASYNC_REQUEST: 0x2043,
MSP2_INAV_DRONECAN_ASYNC_RESULT: 0x2044,

MSP2_INAV_LED_STRIP_CONFIG_EX: 0x2048,
MSP2_INAV_SET_LED_STRIP_CONFIG_EX: 0x2049,

Expand Down
130 changes: 130 additions & 0 deletions js/msp/MSPHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@
var mspHelper = (function () {
var self = {};

const PARAM_TYPE_INT = 1;
const PARAM_TYPE_FLOAT = 2;
const PARAM_TYPE_BOOL = 3;
const PARAM_TYPE_STRING = 4;
const DRONECAN_SERVICE_GETNODEINFO = 1;
const DRONECAN_SERVICE_RESTART_NODE = 5;
const DRONECAN_SERVICE_EXECUTE_OPCODE = 10;
const DRONECAN_SERVICE_PARAM_GETSET = 11;
const DRONECAN_ASYNC_STATE_READY = 2;

self.sensorStatusEx = null;

self.setSensorStatusEx = function (cb) {
Expand Down Expand Up @@ -1557,6 +1567,126 @@
console.log('Safehome points saved');
break;

case MSPCodes.MSP2_INAV_DRONECAN_NODES:
FC.DRONECAN_NODES = [];
if (data.byteLength > 0) {
const count = data.getUint8(0);
for (let i = 0; i < count; i++) {
const offset = 1 + i * 13;
if (offset + 13 > data.byteLength) break;
FC.DRONECAN_NODES.push({
nodeID: data.getUint8(offset),
health: data.getUint8(offset + 1),
mode: data.getUint8(offset + 2),
last_seen_ms: data.getUint32(offset + 3, true),
uptime_sec: data.getUint32(offset + 7, true),
vendor_status_code: data.getUint16(offset + 11, true),
});
}
}
break;

case MSPCodes.MSP2_INAV_DRONECAN_ASYNC_REQUEST:
if (data.byteLength >= 2) {
FC.DRONECAN_ASYNC_REQUEST = {
status: data.getUint8(0),
seq: data.getUint8(1),
};
}
break;

case MSPCodes.MSP2_INAV_DRONECAN_ASYNC_RESULT:
if (data.byteLength >= 5) {
const state = data.getUint8(0);
const seq = data.getUint8(1);
const service_id = data.getUint16(2, true);
const node_id = data.getUint8(4);
const result = { state, seq, service_id, node_id };

if (state === DRONECAN_ASYNC_STATE_READY) {
try {
let offset = 5;
if (service_id === DRONECAN_SERVICE_RESTART_NODE || service_id === DRONECAN_SERVICE_EXECUTE_OPCODE) {
result.ok = data.getUint8(offset) !== 0;
} else {
const name_len = data.getUint8(offset++);
result.name = String.fromCharCode(

Check warning on line 1613 in js/msp/MSPHelper.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String.fromCodePoint()` over `String.fromCharCode()`.

See more on https://sonarcloud.io/project/issues?id=iNavFlight_inav-configurator&issues=AZ895t2Smh95II8BLC8P&open=AZ895t2Smh95II8BLC8P&pullRequest=2673
...new Uint8Array(data.buffer, data.byteOffset + offset, name_len));
offset += name_len;

if (service_id === DRONECAN_SERVICE_GETNODEINFO) {
result.sw_major = data.getUint8(offset++);
result.sw_minor = data.getUint8(offset++);
result.sw_optional_field_flags = data.getUint8(offset++);
result.sw_vcs_commit = data.getUint32(offset, true); offset += 4;
result.hw_major = data.getUint8(offset++);
result.hw_minor = data.getUint8(offset++);
result.hw_unique_id = new Uint8Array(
data.buffer, data.byteOffset + offset, 16).slice(); // copy; don't hold a live view into the MSP buffer
} else if (service_id === DRONECAN_SERVICE_PARAM_GETSET) {
result.value_type = data.getUint8(offset++);
switch (result.value_type) {
case PARAM_TYPE_INT: { // 8 bytes, little-endian lo/hi
const lo = data.getUint32(offset, true);
const hi = data.getUint32(offset + 4, true);
const big = BigInt(hi) * BigInt(0x100000000) + BigInt(lo);
const signed = big >= (1n << 63n) ? big - (1n << 64n) : big;
result.value = (signed >= BigInt(Number.MIN_SAFE_INTEGER) &&
signed <= BigInt(Number.MAX_SAFE_INTEGER))
? Number(signed) : signed;
offset += 8;
break;
}
case PARAM_TYPE_FLOAT: // 4 bytes
result.value = data.getFloat32(offset, true);
offset += 4;
break;
case PARAM_TYPE_BOOL: // 1 byte
result.value = data.getUint8(offset) !== 0;
offset += 1;
break;
case PARAM_TYPE_STRING: { // 1-byte length prefix + data
const slen = data.getUint8(offset++);
result.value = String.fromCharCode(

Check warning on line 1650 in js/msp/MSPHelper.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String.fromCodePoint()` over `String.fromCharCode()`.

See more on https://sonarcloud.io/project/issues?id=iNavFlight_inav-configurator&issues=AZ895t2Smh95II8BLC8Q&open=AZ895t2Smh95II8BLC8Q&pullRequest=2673
...new Uint8Array(data.buffer, data.byteOffset + offset, slen));
offset += slen;
break;
}
default:
result.value = null;
}
// min/max are NumericValue (EMPTY, INT, or FLOAT); only present for INT and FLOAT params
if (result.value_type === PARAM_TYPE_INT || result.value_type === PARAM_TYPE_FLOAT) {
const decodeNumeric = () => {

Check warning on line 1660 in js/msp/MSPHelper.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to always return the same type.

See more on https://sonarcloud.io/project/issues?id=iNavFlight_inav-configurator&issues=AZ895t2Smh95II8BLC8R&open=AZ895t2Smh95II8BLC8R&pullRequest=2673
const type = data.getUint8(offset++);
if (type === PARAM_TYPE_INT) {
const lo = data.getUint32(offset, true);
const hi = data.getUint32(offset + 4, true);
offset += 8;
const big = BigInt(hi) * BigInt(0x100000000) + BigInt(lo);
const signed = big >= (1n << 63n) ? big - (1n << 64n) : big;
return (signed >= BigInt(Number.MIN_SAFE_INTEGER) &&
signed <= BigInt(Number.MAX_SAFE_INTEGER))
? Number(signed) : signed;
} else if (type === PARAM_TYPE_FLOAT) {
const v = data.getFloat32(offset, true);
offset += 4;
return v;
}
return undefined; // EMPTY — no range provided
};
result.min = decodeNumeric();
result.max = decodeNumeric();
}
}
}
} catch (e) { /* truncated or malformed response — result fields may be partial */ }

Check warning on line 1683 in js/msp/MSPHelper.js

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Handle this exception or don't catch it at all.

See more on https://sonarcloud.io/project/issues?id=iNavFlight_inav-configurator&issues=AZ895t2Smh95II8BLC8S&open=AZ895t2Smh95II8BLC8S&pullRequest=2673
}

FC.DRONECAN_ASYNC_RESULT = result;
}
break;

case MSPCodes.MSP2_INAV_FW_APPROACH:
FC.FW_APPROACH.put(new FwApproach(
data.getUint8(0),
Expand Down
Loading
Loading