From 2c2db94d0f12c1e191777076a8c664f063fa1e6f Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Tue, 3 Feb 2026 07:04:27 +1100 Subject: [PATCH 1/2] ESC panel: add per-ESC telemetry frame rate display Co-Authored-By: Claude Opus 4.5 --- dronecan_gui_tool/panels/esc_panel.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/dronecan_gui_tool/panels/esc_panel.py b/dronecan_gui_tool/panels/esc_panel.py index 4cec913..8907888 100644 --- a/dronecan_gui_tool/panels/esc_panel.py +++ b/dronecan_gui_tool/panels/esc_panel.py @@ -56,6 +56,11 @@ def __init__(self, esc_index, parent): self._temperature_label = QLabel('Temp: NC', self) self._rpm_label = QLabel('RPM: NC', self) self._power_rating_pct_label = QLabel('RAT: NC', self) + self._fps_label = QLabel('FPS: 0', self) + + self._fps_count = 0 + self._fps_window_start = time.time() + self._fps_last_msg_time = 0 layout = QHBoxLayout(self) @@ -67,6 +72,7 @@ def __init__(self, esc_index, parent): status_layout.addWidget(self._temperature_label) status_layout.addWidget(self._rpm_label) status_layout.addWidget(self._power_rating_pct_label) + status_layout.addWidget(self._fps_label) status_layout.addStretch() status_layout.addWidget(self._spinbox) status_layout.addWidget(self._zero_button) @@ -92,9 +98,25 @@ def view_mode_set_value(self, value): self._slider.setValue(value) self._spinbox.setValue(value) + def check_fps_timeout(self): + now = time.time() + if self._fps_last_msg_time > 0 and now - self._fps_last_msg_time >= 1.0: + self._fps_label.setText('FPS: 0.0') + self._fps_count = 0 + self._fps_window_start = now + self._fps_last_msg_time = 0 + def update_status(self, idx, error_count, voltage, current, temperature_celsius, rpm, power_rating_pct): if idx != self._index: return + now = time.time() + self._fps_count += 1 + self._fps_last_msg_time = now + elapsed = now - self._fps_window_start + if elapsed >= 1.0: + self._fps_label.setText(f'FPS: {self._fps_count / elapsed:.1f}') + self._fps_count = 0 + self._fps_window_start = now if error_count is not None: self._error_count_label.setText(f'Err: {error_count}') if voltage is not None: @@ -288,6 +310,8 @@ def _update_view_mode(self): def _do_broadcast(self): try: self._update_view_mode() + for sl in self._sliders: + sl.check_fps_timeout() if not self._view_mode.isChecked(): if not self._pause.isChecked(): if self._safety_enable.checkState(): From 510984ac11793783afe1978deeb4457a794b1dab Mon Sep 17 00:00:00 2001 From: Brian Viele Date: Fri, 30 Jan 2026 13:12:46 -0500 Subject: [PATCH 2/2] load-from-file: Updates to improve reliability of large .parm loads The present implementation of do_load_from_file has a few performance issues. 1) All ConfigGetSet() requests are queued as fast as possible, and do not give time for the downstream system to even respond before the next transaction is sent. 2) Even thought the index is known for each parameter in the table the name based request for setting is _always_ used. This causes very heavy bus loading, expecially when combined with (1) This commit make the following core updates: 1) Stores the index in the param set such that it can be utilized later for setting parameters. 2) Queues the request transactions at 20ms increments to give the downstream unit a chance to process the request before sending the next. Additionally in this commit, the fetch speed was increased from 100ms intervals between fetching parameters to 10ms. The 10ms and 20ms settings could be added to the application config to allow some tuning/tweaking on a per user basis, but for now, all tests show these are sufficient values. --- dronecan_gui_tool/version.py | 2 +- dronecan_gui_tool/widgets/node_properties.py | 49 ++++++++++++++------ 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/dronecan_gui_tool/version.py b/dronecan_gui_tool/version.py index c29544a..0cf73d8 100644 --- a/dronecan_gui_tool/version.py +++ b/dronecan_gui_tool/version.py @@ -8,7 +8,7 @@ # Andrew Tridgell # # -__version__ = 1, 2, 28 +__version__ = 1, 2, 29 diff --git a/dronecan_gui_tool/widgets/node_properties.py b/dronecan_gui_tool/widgets/node_properties.py index 6fa8d0e..03169ce 100644 --- a/dronecan_gui_tool/widgets/node_properties.py +++ b/dronecan_gui_tool/widgets/node_properties.py @@ -19,6 +19,7 @@ from .node_monitor import node_health_to_color, node_mode_to_color from .file_server import FileServer_PathKey from ..am32_rtttl import AM32_Rtttl +from typing import Optional logger = getLogger(__name__) @@ -26,6 +27,13 @@ REQUEST_PRIORITY = 30 +class ConfigParamEntry: + def __init__(self, index, response): + self._index = index + self._value = response.value + self._name = response.name + self._response = response; + self._sync=True class FieldValueWidget(QLineEdit): def __init__(self, parent, initial_value=None): @@ -656,7 +664,7 @@ def update_callback(value, is_melody=False): else: self._table.item(index, self.VALUE_COLUMN).setText(str(value)) - win = ConfigParamEditWindow(self, self._node, self._target_node_id, self._params[index], update_callback) + win = ConfigParamEditWindow(self, self._node, self._target_node_id, self._params[index]._response, update_callback) win.show() def _on_fetch_response(self, index, e): @@ -679,14 +687,17 @@ def _on_fetch_response(self, index, e): self.window().show_message('%d params fetched successfully', index) return - self._params.append(e.response) + # create a parameter structure including the index from the response + param = ConfigParamEntry(index, e.response) + + self._params.append(param) self._table.setRowCount(self._table.rowCount() + 1) self._table.set_row(self._table.rowCount() - 1, (index, e.response)) try: index += 1 self.window().show_message('Requesting index %d', index) - self._node.defer(0.1, lambda: self._node.request(dronecan.uavcan.protocol.param.GetSet.Request(index=index), + self._node.defer(0.01, lambda: self._node.request(dronecan.uavcan.protocol.param.GetSet.Request(index=index), self._target_node_id, partial(self._on_fetch_response, index), priority=REQUEST_PRIORITY)) @@ -740,9 +751,9 @@ def _do_save_to_file(self): print("save to file", param_file) f = open(param_file, "w") for p in self._params: - value = p.value - name = p.name - value_string = self.param_as_string(value, AM32_Rtttl.is_am32_melody_param(p)) + value = p._value + name = p._name + value_string = self.param_as_string(value, AM32_Rtttl.is_am32_melody_param(p._response)) if value_string: f.write("%s %s\n" % (name, value_string)) f.close() @@ -753,12 +764,12 @@ def _on_send_response(self, e): else: for i in range(len(self._params)): p = self._params[i] - name = str(p.name) + name = str(p._name) if name == str(e.response.name): logger.info('set %s to %s' % (name, self.param_as_string(e.response.value))) - self._table.item(i, self.VALUE_COLUMN).setText(self.param_as_string(e.response.value, AM32_Rtttl.is_am32_melody_param(p))) + self._table.item(i, self.VALUE_COLUMN).setText(self.param_as_string(e.response.value, AM32_Rtttl.is_am32_melody_param(p._response))) - def save_param(self, name, old_value, str_value): + def save_param(self, name, old_value, str_value, index: Optional[int] = None, delay: Optional[float] = None): value_type = dronecan.get_active_union_field(old_value) v = old_value @@ -774,8 +785,16 @@ def save_param(self, name, old_value, str_value): raise RuntimeError('bad parameter type on save') try: - request = dronecan.uavcan.protocol.param.GetSet.Request(name=name, value=v) - self._node.request(request, self._target_node_id, self._on_send_response, priority=REQUEST_PRIORITY) + if index is None: + request = dronecan.uavcan.protocol.param.GetSet.Request(name=name, value=v) + else: + request = dronecan.uavcan.protocol.param.GetSet.Request(index=index, value=v) + + if delay is None: + self._node.request(request, self._target_node_id, self._on_send_response, priority=REQUEST_PRIORITY) + else: + self._node.defer(delay, lambda: self._node.request(request, self._target_node_id, self._on_send_response, priority=REQUEST_PRIORITY)) + except Exception as ex: show_error('Node error', 'Could not send param set request', ex, self) @@ -787,11 +806,12 @@ def _do_load_from_file(self): return pdict = {} for p in self._params: - pdict[str(p.name)] = p.value + pdict[str(p._name)] = p param_file = os.path.normcase(os.path.abspath(param_file[0])) print("load from file", param_file) f = open(param_file, "r") + delay = 0.0 for line in f.readlines(): a = line.split() name = a[0] @@ -804,9 +824,10 @@ def _do_load_from_file(self): else: value = a[1] if name in pdict: - s = self.param_as_string(pdict[name]) + s = self.param_as_string(pdict[name]._value) if s != value: - self.save_param(name, pdict[name], value) + self.save_param(name, pdict[name]._value, value, pdict[name]._index, delay) + delay += 0.02 f.close() def _do_execute_opcode(self, opcode):