UAVCAN: Configurable LED Light Control with Flexible Addressing (#26253)

* feat: implement UAVCAN LED control for individual light control and assignment

* uavcan led: nit-picks from review

* uavcan led: reduce maximum number of lights

to avoid unused parameters

* uavcan led: simplify anticolision on check

* uavcan led: correctly map 8-bit RGB to rgb565

* Trim param name character arrays to 17

16 characters + \0 termination

* uavcan led: final nit-picks

---------

Co-authored-by: Matthias Grob <maetugr@gmail.com>
This commit is contained in:
Claudio Chies
2026-02-16 09:39:48 -08:00
committed by GitHub
parent 32c94bd3b1
commit e52ce5c43b
9 changed files with 215 additions and 199 deletions
@@ -1056,9 +1056,6 @@
1 1 UAVCAN_EC_REV 0 6 1 1 UAVCAN_EC_REV 0 6
1 1 UAVCAN_ENABLE 2 6 1 1 UAVCAN_ENABLE 2 6
1 1 UAVCAN_LGT_ANTCL 2 6 1 1 UAVCAN_LGT_ANTCL 2 6
1 1 UAVCAN_LGT_LAND 0 6
1 1 UAVCAN_LGT_NAV 3 6
1 1 UAVCAN_LGT_STROB 1 6
1 1 UAVCAN_NODE_ID 1 6 1 1 UAVCAN_NODE_ID 1 6
1 1 UAVCAN_PUB_ARM 0 6 1 1 UAVCAN_PUB_ARM 0 6
1 1 UAVCAN_PUB_MBD 0 6 1 1 UAVCAN_PUB_MBD 0 6
+13
View File
@@ -281,6 +281,19 @@ PX4 DroneCAN parameters:
Select the specific CAN interface(s) used for ESC data output using the [UAVCAN_ESC_IFACE](../advanced_config/parameter_reference.md#UAVCAN_ESC_IFACE) parameter (all that all interfaces are selected by default). Select the specific CAN interface(s) used for ESC data output using the [UAVCAN_ESC_IFACE](../advanced_config/parameter_reference.md#UAVCAN_ESC_IFACE) parameter (all that all interfaces are selected by default).
Note that DroneCAN ESCs should be on their own dedicated CAN interface(s) because ESC messages can saturate the bus and starve other nodes of bandwidth. Note that DroneCAN ESCs should be on their own dedicated CAN interface(s) because ESC messages can saturate the bus and starve other nodes of bandwidth.
### Lights
PX4 can control LEDs via DroneCAN [LightsCommand](https://dronecan.github.io/Specification/7._List_of_standard_data_types/#lightscommand) messages.
Configuration:
1. Set [UAVCAN_LGT_NUM](../advanced_config/parameter_reference.md#UAVCAN_LGT_NUM) to the number of lights (0 disables). You might need to reopen the ground station to have parameters for new instances available.
2. For each light slot (0 to NUM-1), set:
- `UAVCAN_LGT_IDx`: The `light_id` matching your peripheral.
- `UAVCAN_LGT_FNx`: `Status` for system status colours, or `Anti-collision` for white beacon.
3. For anti-collision lights, [UAVCAN_LGT_ANTCL](../advanced_config/parameter_reference.md#UAVCAN_LGT_ANTCL) controls when they illuminate (off, armed, prearmed, always on).
4. Reboot for any changes to take effect.
## QGC CANNODE Parameter Configuration ## QGC CANNODE Parameter Configuration
QGroundControl can inspect and modify parameters belonging to CAN devices attached to the flight controller, provided the device are connected to the flight controller before QGC is started. QGroundControl can inspect and modify parameters belonging to CAN devices attached to the flight controller, provided the device are connected to the flight controller before QGC is started.
+13
View File
@@ -38,6 +38,19 @@ Instructions for integrating the motor/ESC using with DroneCAN can be found in [
These instructions walk you through setting the correct parameters for enabling the flight controller's DroneCAN drivers, setting the correct configuration parameters for communication with Vertiq modules on the DroneCAN bus, ESC configuration, and testing that your flight controller can properly control your modules over DroneCAN. These instructions walk you through setting the correct parameters for enabling the flight controller's DroneCAN drivers, setting the correct configuration parameters for communication with Vertiq modules on the DroneCAN bus, ESC configuration, and testing that your flight controller can properly control your modules over DroneCAN.
#### LED Configuration for Vertiq Modules
::: info
This configuration is only required if you have the optional [Vertiq LED module add-on](https://www.vertiq.co/add-ons).
Standard Vertiq ESC modules do not include LEDs.
:::
Vertiq LED Add-on modules have two LEDs per ESC (RGB for status, White for anti-collision).
See [DroneCAN Lights](../dronecan/index.md#lights) for configuration instructions.
The `light_id` for each LED is calculated as: `esc_index × 3 + BASE_ID`, where `BASE_ID` is 1 for RGB and 2 for White.
### DShot/PWM Configuration ### DShot/PWM Configuration
Instructions for integrating the motor/ESC using PWM and DShot can be found in [PWM and DShot Control with a Flight Controller](https://iqmotion.readthedocs.io/en/latest/tutorials/pwm_control_flight_controller.html). Instructions for integrating the motor/ESC using PWM and DShot can be found in [PWM and DShot Control with a Flight Controller](https://iqmotion.readthedocs.io/en/latest/tutorials/pwm_control_flight_controller.html).
+42
View File
@@ -1,3 +1,5 @@
__max_num_uavcan_lights: &max_num_uavcan_lights 3 # Needs to be equal to MAX_NUM_UAVCAN_LIGHTS constant
module_name: UAVCAN module_name: UAVCAN
parameters: parameters:
- group: UAVCAN - group: UAVCAN
@@ -24,6 +26,46 @@ parameters:
min: 1 min: 1
max: 255 max: 255
reboot_required: true reboot_required: true
UAVCAN_LGT_NUM:
description:
short: Number of UAVCAN lights to configure
long: |
Number of lights to control via UAVCAN LightsCommand messages.
Set to 0 to disable UAVCAN light control.
Each light uses two parameters: LGT_IDx for the light_id and LGT_FNx for the function.
type: int32
min: 0
max: *max_num_uavcan_lights
default: 1
reboot_required: true
UAVCAN_LGT_ID${i}:
description:
short: Light ${i} ID
long: |
specifies the light_id value for light ${i} in UAVCAN LightsCommand messages.
This determines which physical LED responds to commands for this light slot.
type: int32
num_instances: *max_num_uavcan_lights
instance_start: 0
min: 0
max: 255
default: 0
UAVCAN_LGT_FN${i}:
description:
short: Light ${i} function
long: |
Function assigned to light ${i}.
0: Status - displays system status colors
1: Anti-collision - white beacon controlled by LGT_ANTCL parameter
type: enum
num_instances: *max_num_uavcan_lights
instance_start: 0
min: 0
max: 1
default: 0
values:
0: Status Light
1: Anti-collision Light
actuator_output: actuator_output:
show_subgroups_if: 'UAVCAN_ENABLE>=3' show_subgroups_if: 'UAVCAN_ENABLE>=3'
config_parameters: config_parameters:
+119 -120
View File
@@ -32,6 +32,7 @@
****************************************************************************/ ****************************************************************************/
#include "rgbled.hpp" #include "rgbled.hpp"
#include <lib/mathlib/mathlib.h>
UavcanRGBController::UavcanRGBController(uavcan::INode &node) : UavcanRGBController::UavcanRGBController(uavcan::INode &node) :
ModuleParams(nullptr), ModuleParams(nullptr),
@@ -44,6 +45,38 @@ UavcanRGBController::UavcanRGBController(uavcan::INode &node) :
int UavcanRGBController::init() int UavcanRGBController::init()
{ {
// Cache number of lights (0 disables the feature)
_num_lights = math::min(static_cast<uint8_t>(_param_lgt_num.get()), MAX_NUM_UAVCAN_LIGHTS);
if (_num_lights == 0) {
return 0; // Disabled, don't start timer
}
// Cache parameter handles and values for each light
for (uint8_t i = 0; i < _num_lights; i++) {
char param_name[17];
// Light ID parameter
snprintf(param_name, sizeof(param_name), "UAVCAN_LGT_ID%u", i);
_light_id_params[i] = param_find(param_name);
if (_light_id_params[i] != PARAM_INVALID) {
int32_t light_id = 0;
param_get(_light_id_params[i], &light_id);
_light_ids[i] = static_cast<uint8_t>(light_id);
}
// Light function parameter
snprintf(param_name, sizeof(param_name), "UAVCAN_LGT_FN%u", i);
_light_fn_params[i] = param_find(param_name);
if (_light_fn_params[i] != PARAM_INVALID) {
int32_t light_fn = 0;
param_get(_light_fn_params[i], &light_fn);
_light_functions[i] = static_cast<LightFunction>(light_fn);
}
}
// Setup timer and call back function for periodic updates // Setup timer and call back function for periodic updates
_timer.setCallback(TimerCbBinder(this, &UavcanRGBController::periodic_update)); _timer.setCallback(TimerCbBinder(this, &UavcanRGBController::periodic_update));
_timer.startPeriodic(uavcan::MonotonicDuration::fromMSec(1000 / MAX_RATE_HZ)); _timer.startPeriodic(uavcan::MonotonicDuration::fromMSec(1000 / MAX_RATE_HZ));
@@ -52,142 +85,108 @@ int UavcanRGBController::init()
void UavcanRGBController::periodic_update(const uavcan::TimerEvent &) void UavcanRGBController::periodic_update(const uavcan::TimerEvent &)
{ {
bool publish_lights = false; // Early return if disabled or no lights configured
uavcan::equipment::indication::LightsCommand cmds; if (_num_lights == 0) {
return;
}
// Check for status color updates from led_controller
LedControlData led_control_data; LedControlData led_control_data;
if (_led_controller.update(led_control_data) == 1) { if (_led_controller.update(led_control_data) != 1) {
publish_lights = true; return; // No update, nothing to do
}
// RGB color in the standard 5-6-5 16-bit palette. // Compute status color from led_control_data
// Monocolor lights should interpret this as brightness setpoint: from zero (0, 0, 0) to full brightness (31, 63, 31). uavcan::equipment::indication::RGB565 status_color{};
uint8_t brightness = led_control_data.leds[0].brightness;
switch (led_control_data.leds[0].color) {
case led_control_s::COLOR_RED:
status_color = rgb888_to_rgb565(brightness, 0, 0);
break;
case led_control_s::COLOR_GREEN:
status_color = rgb888_to_rgb565(0, brightness, 0);
break;
case led_control_s::COLOR_BLUE:
status_color = rgb888_to_rgb565(0, 0, brightness);
break;
case led_control_s::COLOR_AMBER: // make it the same as yellow
case led_control_s::COLOR_YELLOW:
status_color = rgb888_to_rgb565(brightness, brightness, 0);
break;
case led_control_s::COLOR_PURPLE:
status_color = rgb888_to_rgb565(brightness, 0, brightness);
break;
case led_control_s::COLOR_CYAN:
status_color = rgb888_to_rgb565(0, brightness, brightness);
break;
case led_control_s::COLOR_WHITE:
status_color = rgb888_to_rgb565(brightness, brightness, brightness);
break;
default:
case led_control_s::COLOR_OFF:
break;
}
// Build and send light commands for all configured lights
uavcan::equipment::indication::LightsCommand light_command;
for (uint8_t i = 0; i < _num_lights; i++) {
uavcan::equipment::indication::SingleLightCommand cmd; uavcan::equipment::indication::SingleLightCommand cmd;
cmd.light_id = _light_ids[i];
uint8_t brightness = led_control_data.leds[0].brightness; switch (_light_functions[i]) {
case LightFunction::Status:
switch (led_control_data.leds[0].color) { cmd.color = status_color;
case led_control_s::COLOR_RED:
cmd.color.red = brightness >> 3;
cmd.color.green = 0;
cmd.color.blue = 0;
break; break;
case led_control_s::COLOR_GREEN: case LightFunction::AntiCollision:
cmd.color.red = 0; uint8_t brigtness = is_anticolision_on() ? 255 : 0;
cmd.color.green = brightness >> 2; cmd.color = rgb888_to_rgb565(brigtness, brigtness, brigtness);
cmd.color.blue = 0;
break;
case led_control_s::COLOR_BLUE:
cmd.color.red = 0;
cmd.color.green = 0;
cmd.color.blue = brightness >> 3;
break;
case led_control_s::COLOR_AMBER: // make it the same as yellow
// FALLTHROUGH
case led_control_s::COLOR_YELLOW:
cmd.color.red = (brightness / 2) >> 3;
cmd.color.green = (brightness / 2) >> 2;
cmd.color.blue = 0;
break;
case led_control_s::COLOR_PURPLE:
cmd.color.red = (brightness / 2) >> 3;
cmd.color.green = 0;
cmd.color.blue = (brightness / 2) >> 3;
break;
case led_control_s::COLOR_CYAN:
cmd.color.red = 0;
cmd.color.green = (brightness / 2) >> 2;
cmd.color.blue = (brightness / 2) >> 3;
break;
case led_control_s::COLOR_WHITE:
cmd.color.red = (brightness / 3) >> 3;
cmd.color.green = (brightness / 3) >> 2;
cmd.color.blue = (brightness / 3) >> 3;
break;
default: // led_control_s::COLOR_OFF
cmd.color.red = 0;
cmd.color.green = 0;
cmd.color.blue = 0;
break; break;
} }
cmds.commands.push_back(cmd); light_command.commands.push_back(cmd);
} }
if (_armed_sub.updated()) { _uavcan_pub_lights_cmd.broadcast(light_command);
publish_lights = true;
actuator_armed_s armed;
if (_armed_sub.copy(&armed)) {
/* Determine the current control mode
* If a light's control mode config >= current control mode, the light will be enabled
* Logic must match UAVCAN_LGT_* param values.
* @value 0 Always off
* @value 1 When autopilot is armed
* @value 2 When autopilot is prearmed
* @value 3 Always on
*/
uint8_t control_mode = 0;
if (armed.armed) {
control_mode = 1;
} else if (armed.prearmed) {
control_mode = 2;
} else {
control_mode = 3;
}
uavcan::equipment::indication::SingleLightCommand cmd;
// Beacons
cmd.light_id = uavcan::equipment::indication::SingleLightCommand::LIGHT_ID_ANTI_COLLISION;
cmd.color = brightness_to_rgb565(_param_mode_anti_col.get() >= control_mode ? 255 : 0);
cmds.commands.push_back(cmd);
// Strobes
cmd.light_id = uavcan::equipment::indication::SingleLightCommand::LIGHT_ID_STROBE;
cmd.color = brightness_to_rgb565(_param_mode_strobe.get() >= control_mode ? 255 : 0);
cmds.commands.push_back(cmd);
// Nav lights
cmd.light_id = uavcan::equipment::indication::SingleLightCommand::LIGHT_ID_RIGHT_OF_WAY;
cmd.color = brightness_to_rgb565(_param_mode_nav.get() >= control_mode ? 255 : 0);
cmds.commands.push_back(cmd);
// Landing lights
cmd.light_id = uavcan::equipment::indication::SingleLightCommand::LIGHT_ID_LANDING;
cmd.color = brightness_to_rgb565(_param_mode_land.get() >= control_mode ? 255 : 0);
cmds.commands.push_back(cmd);
}
}
if (publish_lights) {
_uavcan_pub_lights_cmd.broadcast(cmds);
}
} }
uavcan::equipment::indication::RGB565 UavcanRGBController::brightness_to_rgb565(uint8_t brightness) bool UavcanRGBController::is_anticolision_on()
{ {
// RGB color in the standard 5-6-5 16-bit palette. actuator_armed_s actuator_armed{};
// Monocolor lights should interpret this as brightness setpoint: from zero (0, 0, 0) to full brightness (31, 63, 31). _actuator_armed_sub.copy(&actuator_armed);
uavcan::equipment::indication::RGB565 color;
color.red = (31.0f * (float)brightness / 255.0f); switch (_param_uavcan_lgt_antcl.get()) {
color.green = (62.0f * (float)brightness / 255.0f); case 3: // Always on
color.blue = (31.0f * (float)brightness / 255.0f); return true;
return color; case 2: // When autopilot is prearmed
return actuator_armed.armed || actuator_armed.prearmed;
case 1: // When autopilot is armed
return actuator_armed.armed;
case 0: // Always off
default:
return false;
}
}
uavcan::equipment::indication::RGB565 UavcanRGBController::rgb888_to_rgb565(uint8_t red, uint8_t green, uint8_t blue)
{
// RGB565: Full brightness is (31, 63, 31), off is (0, 0, 0)
uavcan::equipment::indication::RGB565 rgb565{};
rgb565.red = (red * 31 + 127) / 255;
rgb565.green = (green * 63 + 127) / 255;
rgb565.blue = (blue * 31 + 127) / 255;
return rgb565;
} }
+24 -6
View File
@@ -54,9 +54,20 @@ private:
// Max update rate to avoid excessive bus traffic // Max update rate to avoid excessive bus traffic
static constexpr unsigned MAX_RATE_HZ = 20; static constexpr unsigned MAX_RATE_HZ = 20;
// Maximum number of configurable lights
static constexpr uint8_t MAX_NUM_UAVCAN_LIGHTS = 3;
// Light function types
enum class LightFunction : uint8_t {
Status = 0, // System status colors from led_control
AntiCollision = 1 // White beacon based on arm state
};
void periodic_update(const uavcan::TimerEvent &); void periodic_update(const uavcan::TimerEvent &);
uavcan::equipment::indication::RGB565 brightness_to_rgb565(uint8_t brightness); bool is_anticolision_on(); ///< Evaluates current on state of collision lights accordingt to UAVCAN_LGT_ANTCL
uavcan::equipment::indication::RGB565 rgb888_to_rgb565(uint8_t red, uint8_t green, uint8_t blue);
typedef uavcan::MethodBinder<UavcanRGBController *, void (UavcanRGBController::*)(const uavcan::TimerEvent &)> typedef uavcan::MethodBinder<UavcanRGBController *, void (UavcanRGBController::*)(const uavcan::TimerEvent &)>
TimerCbBinder; TimerCbBinder;
@@ -65,14 +76,21 @@ private:
uavcan::Publisher<uavcan::equipment::indication::LightsCommand> _uavcan_pub_lights_cmd; uavcan::Publisher<uavcan::equipment::indication::LightsCommand> _uavcan_pub_lights_cmd;
uavcan::TimerEventForwarder<TimerCbBinder> _timer; uavcan::TimerEventForwarder<TimerCbBinder> _timer;
uORB::Subscription _armed_sub{ORB_ID(actuator_armed)}; uORB::Subscription _actuator_armed_sub{ORB_ID(actuator_armed)};
LedController _led_controller; LedController _led_controller;
// Cached configuration (set during init, requires reboot to change)
uint8_t _num_lights{0};
uint8_t _light_ids[MAX_NUM_UAVCAN_LIGHTS] {};
LightFunction _light_functions[MAX_NUM_UAVCAN_LIGHTS] {};
// Cached parameter handles
param_t _light_id_params[MAX_NUM_UAVCAN_LIGHTS] {};
param_t _light_fn_params[MAX_NUM_UAVCAN_LIGHTS] {};
DEFINE_PARAMETERS( DEFINE_PARAMETERS(
(ParamInt<px4::params::UAVCAN_LGT_ANTCL>) _param_mode_anti_col, (ParamInt<px4::params::UAVCAN_LGT_NUM>) _param_lgt_num,
(ParamInt<px4::params::UAVCAN_LGT_STROB>) _param_mode_strobe, (ParamInt<px4::params::UAVCAN_LGT_ANTCL>) _param_uavcan_lgt_antcl
(ParamInt<px4::params::UAVCAN_LGT_NAV>) _param_mode_nav,
(ParamInt<px4::params::UAVCAN_LGT_LAND>) _param_mode_land
) )
}; };
+1 -67
View File
@@ -135,7 +135,7 @@ PARAM_DEFINE_INT32(UAVCAN_ECU_FUELT, 1);
* UAVCAN ANTI_COLLISION light operating mode * UAVCAN ANTI_COLLISION light operating mode
* *
* This parameter defines the minimum condition under which the system will command * This parameter defines the minimum condition under which the system will command
* the ANTI_COLLISION lights on * lights with anti-collision function to turn on (white).
* *
* 0 - Always off * 0 - Always off
* 1 - When autopilot is armed * 1 - When autopilot is armed
@@ -153,72 +153,6 @@ PARAM_DEFINE_INT32(UAVCAN_ECU_FUELT, 1);
*/ */
PARAM_DEFINE_INT32(UAVCAN_LGT_ANTCL, 2); PARAM_DEFINE_INT32(UAVCAN_LGT_ANTCL, 2);
/**
* UAVCAN STROBE light operating mode
*
* This parameter defines the minimum condition under which the system will command
* the STROBE lights on
*
* 0 - Always off
* 1 - When autopilot is armed
* 2 - When autopilot is prearmed
* 3 - Always on
*
* @min 0
* @max 3
* @value 0 Always off
* @value 1 When autopilot is armed
* @value 2 When autopilot is prearmed
* @value 3 Always on
* @reboot_required true
* @group UAVCAN
*/
PARAM_DEFINE_INT32(UAVCAN_LGT_STROB, 1);
/**
* UAVCAN RIGHT_OF_WAY light operating mode
*
* This parameter defines the minimum condition under which the system will command
* the RIGHT_OF_WAY lights on
*
* 0 - Always off
* 1 - When autopilot is armed
* 2 - When autopilot is prearmed
* 3 - Always on
*
* @min 0
* @max 3
* @value 0 Always off
* @value 1 When autopilot is armed
* @value 2 When autopilot is prearmed
* @value 3 Always on
* @reboot_required true
* @group UAVCAN
*/
PARAM_DEFINE_INT32(UAVCAN_LGT_NAV, 3);
/**
* UAVCAN LIGHT_ID_LANDING light operating mode
*
* This parameter defines the minimum condition under which the system will command
* the LIGHT_ID_LANDING lights on
*
* 0 - Always off
* 1 - When autopilot is armed
* 2 - When autopilot is prearmed
* 3 - Always on
*
* @min 0
* @max 3
* @value 0 Always off
* @value 1 When autopilot is armed
* @value 2 When autopilot is prearmed
* @value 3 Always on
* @reboot_required true
* @group UAVCAN
*/
PARAM_DEFINE_INT32(UAVCAN_LGT_LAND, 0);
/** /**
* publish Arming Status stream * publish Arming Status stream
* *
+2 -2
View File
@@ -107,7 +107,7 @@ uint8_t Modes::addExternalMode(const Modes::Mode &mode)
int matching_idx = -1; int matching_idx = -1;
for (int i = 0; i < MAX_NUM; ++i) { for (int i = 0; i < MAX_NUM; ++i) {
char hash_param_name[20]; char hash_param_name[17];
snprintf(hash_param_name, sizeof(hash_param_name), "COM_MODE%d_HASH", i); snprintf(hash_param_name, sizeof(hash_param_name), "COM_MODE%d_HASH", i);
const param_t handle = param_find(hash_param_name); const param_t handle = param_find(hash_param_name);
int32_t current_hash{}; int32_t current_hash{};
@@ -164,7 +164,7 @@ uint8_t Modes::addExternalMode(const Modes::Mode &mode)
if (new_mode_idx != -1 && !_modes[new_mode_idx].valid) { if (new_mode_idx != -1 && !_modes[new_mode_idx].valid) {
if (need_to_update_param) { if (need_to_update_param) {
char hash_param_name[20]; char hash_param_name[17];
snprintf(hash_param_name, sizeof(hash_param_name), "COM_MODE%d_HASH", new_mode_idx); snprintf(hash_param_name, sizeof(hash_param_name), "COM_MODE%d_HASH", new_mode_idx);
const param_t handle = param_find(hash_param_name); const param_t handle = param_find(hash_param_name);
@@ -80,7 +80,7 @@ void ActuatorEffectivenessControlSurfaces::updateParams()
// Helper to check if a PWM center parameter is enabled, and clamp it to valid range // Helper to check if a PWM center parameter is enabled, and clamp it to valid range
auto check_pwm_center = [](const char *prefix, int channel) -> bool { auto check_pwm_center = [](const char *prefix, int channel) -> bool {
char param_name[20]; char param_name[17];
snprintf(param_name, sizeof(param_name), "%s_CENT%d", prefix, channel); snprintf(param_name, sizeof(param_name), "%s_CENT%d", prefix, channel);
param_t param = param_find(param_name); param_t param = param_find(param_name);