Skip to content

added ChannelConfig message for configuration of multi-actuator setups - #80

Open
alexcekay wants to merge 1 commit into
dronecan:masterfrom
alexcekay:pr-actuator-channel-config
Open

added ChannelConfig message for configuration of multi-actuator setups#80
alexcekay wants to merge 1 commit into
dronecan:masterfrom
alexcekay:pr-actuator-channel-config

Conversation

@alexcekay

@alexcekay alexcekay commented May 1, 2026

Copy link
Copy Markdown

Context

This started off as a specific feature needed by our company but I think others could also benefit from it.

The current ArrayCommand setup works great for simple setups or in case one can assign individual actuator IDs to each servo. It already gets harder to use when one has a general CAN to PWM board with multiple output channels in which case a mapping on each node is needed.

In bigger setups that are produced at scale all of this does not scale well as changing the setup requires connecting to individual servos or individual CAN to PWM boards. To deploy such setups it is easier to set up all nodes once and only change the configuration on the flight controller.

Solution

A new message ChannelConfig is introduced which originates from the flight controller and is received by servos / CAN to PWM boards. In it, the flight controller can specify which actuator (using its ID) should handle a Command on which of its channels. All of it is 0-based. For example when ArrayCommand contains two commands, it could specify that actuator 0 should handle Command 0 on its channel 0 and actuator 1 should handle Command 1 on its channel 3.

This scales well to more complex setups where one has CAN to PWM boards in each wing and tail of a larger fixed-wing. After assigning actuator IDs to each of the nodes, one can dynamically change which node handles what on which channel.

Compatibility

This message is optional. If the flight controller does not send it or an actuator does not react to it, the current behavior stays unchanged. The idea is that the nodes have a parameter that controls whether they react to this message and only process it when enabled. When enabled, they have to wait for one ChannelConfig before processing ArrayCommand. The frequency of ChannelConfig can be very low as it should not change frequently (or at all) after startup.

@dakejahl

dakejahl commented May 9, 2026

Copy link
Copy Markdown
Contributor

This is great, definitely needed. Can you provided a concrete example? We'd want to make sure this lands in AM32 at the same time.

@alexcekay

Copy link
Copy Markdown
Author

@dakejahl for sure.

Setup

A concrete example for a large fixed-wing with a couple of meters wing-span.
In this case there is one PX4 CAN Node in each wing and at the back. All are connected via CAN to the flight controller. The PX4 CAN Nodes are connected to their respective servos (ailerons, elevator, rudder).

Node configuration

The user needs to assign an actuator ID to each PX4 CAN Node. This can be done via a parameter or for example HW switches. As an example:

  • Left aileron node: 0
  • Right aileron node: 1
  • Rear node: 2

In addition the user has to set a parameter that states that this CAN Node should listen to ChannelConfig messages and not execute ArrayCommand before the first ChannelConfig is received.

Flight-controller configuration

On the flight-controller each output function can be assigned a actuator_id and actuator_channel that handles it. I prepared the PX4 relevant change, can upstream soon. It is basically a set of parameters. As an example:

Actuator actuator_id actuator_channel
Left aileron 0 0
Right aileron 1 0
Elevator 2 0
Rudder 2 1

Logic

The flight-controller uses its parameters to build the correct ChannelConfig message. Let's assume that the index in ArrayCommand are:

  • Index 0: Left aileron
  • Index 1: Right aileron
  • Index 2: Elevator
  • Index 3: Rudder

In this case the message is:

actuator_id: [0, 1, 2, 2, 255, 255, ...]
actuator_channel: [0, 0, 0, 1, 255, 255, ...]

The PX4 Nodes can use this information to see which ArrayCommand they should react to and on which of its local output channels they should map this to.

Mapping two different functions to exactly the same actuator_id and actuator_channel is undefined behavior and represents a configuration error.

@alexcekay
alexcekay force-pushed the pr-actuator-channel-config branch 2 times, most recently from a44e063 to 8411852 Compare May 18, 2026 12:13
@alexcekay
alexcekay marked this pull request as ready for review May 18, 2026 12:13
@IamPete1

Copy link
Copy Markdown
Contributor

Dosn't this just move the conplexity from the nodes on to the main flight controller? Is that exactly the oposite of what we should be trying to do? Why not just change the param on the node?

Its is posible that the PX4 actuator config makes this more apealing. I dont think it would add anything for AP.

@alexcekay

alexcekay commented May 18, 2026

Copy link
Copy Markdown
Author

@IamPete1,

basically yes, this is mostly a configuration topic. In complex systems with a lot of nodes that are produced in mass it's easier to have the configuration centralized.

With this and HW DIP switches on the nodes to set the actuator ID one can have an assembly line that does not require to ever connect to one of the nodes and change its configuration. If later the config should be changed it's only required to map differently on the flight controller.

Connecting to the nodes individually to change params does not allow producing systems in a quick and easy to repeat manner for non-technical users. The way presented here is repeatable and easy to use, thus avoids an error-prone manual process. Setting params from the flight-controller is also hard as it requires knowing the NodeID and probing for the correct actuator ID to set the correct indexes a node should listen to.

For smaller/hobby users or other projects it's of course possible to simply not use/implement this message.

@IamPete1

Copy link
Copy Markdown
Contributor

In production settings why not just bake the config into the firmware so you flash once and are done? Again, its possible that this is easy in AP but not in PX4.

@alexcekay

alexcekay commented May 18, 2026

Copy link
Copy Markdown
Author

@IamPete1:

Really good point.

The main reason is that the individual nodes would require individual firmware in this case.

Let's take left aileron, right aileron as an example. If both are handled by two different CAN nodes (one in each wing) one would need to create two firmware variants. One firmware variant would react to index 0, the other to index 1 using the baked in settings. Apart from that the firmware would be exactly the same as it's the same hardware product.

This complicates the manufacturing process of the nodes itself as one would need different SKUs for the node variants. The node manufacturer would basically need to flash half of the nodes with variant 1 and half with variant 2. As soon as a rear node is needed the next variant is needed.

The people at the assembly line would in this case have identical looking nodes but have to take care to not mix them. Alternatives like having physical different looking nodes increase manufacturing complexity of the nodes.

The solution presented here allows one to keep using the same firmware on all nodes and configure everything centrally. In addition after the nodes have been produced they can simply be installed into the vehicle without needing to connect to them manually again. Firmware update from the flight-controller is simple as they all have the same one. The solution also does not cause problems for the ones that don't want to use it. Thus, I think it is a good solution as others especially OEMs will also face the same problem as soon as they want to manufacture complex CAN based systems.

It is also very easy to use in the QGC actuator config:
image

  • Servo 1 and Servo 2 are both mapped to actuator 1 on it's channel 1 and 2
  • Servo 3 and Servo 4 are both mapped to actuator 2 on it's channel 1 and 2
  • One can directly change the mapping in this view instantly.

@alexcekay

alexcekay commented May 26, 2026

Copy link
Copy Markdown
Author

Hi @tridge, @peterbarker would also love to hear your opinion regarding this topic.
Can also move it to a /px4 namespace if this does not benefit ArduPilot.

@peterbarker

Copy link
Copy Markdown
Contributor

Hi @tridge, @peterbarker would also love to hear your opinion regarding this topic. Can also move it to a /px4 namespace if this does not benefit ArduPilot.

Certainly I think such a generic message should be evaluated for adding into a common namespace rather than into a new one.

I don't think moving between namespaces is a thing in DroneCAN-land.

I'll have a think

@peterbarker

Copy link
Copy Markdown
Contributor

The main reason is that the individual nodes would require individual firmware in this case.

Hang on, back up. That seems pretty dire.

Mr Hall was assuming that this would be a simple parameter-set (done via DroneCAN), not an entirely different different SKU requirement!

If I'm thinking right then end to-end on ArduPilot:

  • the autopilot has a mask of which servos to send via DroneCAN
  • the ArrayCommand is packed only with values for actuators in the bitmask (ensuring minimal bus utilisation)
  • the actuator ID corresponds to the servo-output-channel on the autopilot
  • AP_Periph allows full remapping of the servo-output-channel-on-autopilot into servo-output-channel-on-periph via SERVOn_FUNCTION parameters.

I think what this message proposal would do would add a message that AP_Periph could use to set the SERVOn_FUNCTION parameters, allowing the mapping to be configured in a standardised manner.

So a few issues:

  • the message doesn't say anything about persistence. Persistence would be an absolute requirement as the same output can be dangerous on one actuator but not another. You don't want to have to configure everything at runtime, that would be actively dangerous in other ways, too.
  • the message here needs clarification around "node" and "actuator". I think the current wording is "every node must handle every actuator" and that's just wrong as it currently stands, given AP_Periph's mapping system
  • the configuration can already be handled via parameter setting in AP_Periph through the normal parameter protocol. Given this is a one-time-setup thing, I'm really not convinced we need a separate message to carry the mapping information. It would allow for standardised configuration interfaces, but... it also has (a) maintenance overhead and (b) flash overhead.

@alexcekay

Copy link
Copy Markdown
Author

Hi @peterbarker,

thanks for the feedback.

Hang on, back up. That seems pretty dire.

Mr Hall was assuming that this would be a simple parameter-set (done via DroneCAN), not an entirely different different SKU requirement!

To clarify: that line was in answer to the suggestion of baking config into firmware at build time, not a description of the current proposal.

I think what this message proposal would do would add a message that AP_Periph could use to set the SERVOn_FUNCTION parameters, allowing the mapping to be configured in a standardised manner.

Yes exactly, it allows centrally setting which node handles which actuator on which of its outputs.

the message here needs clarification around "node" and "actuator". I think the current wording is "every node must handle every actuator" and that's just wrong as it currently stands, given AP_Periph's mapping system

This is true, to clarify: when changing params on the node (in ArduPilot's case AP_Periph) you can already get exactly what you'd gain from this message.

But that's exactly the piece ChannelConfig isn't trying to replace. To state this clearly: the problem isn't that this can't be solved at all today, it's that the mapping is currently specified on a per-node basis. To make a left-aileron node and a right-aileron node (same hardware, same firmware) react to different parts of ArrayCommand, someone has to address each node individually by its CAN Node ID and issue a separate parameter-set call per node. As one doesn't know which Node ID corresponds to which physical location it is required to manually check this.

The actual pain point is for systems at scale, we're talking about thousands of drones produced per day. It's a one-time setup, but done thousands of times, and that causes hours of work that can be completely avoided. For a lot of other params you can set the same value to every node, that's not the problem here. The problem is specifically that different nodes need different values depending on their physical position.

The proposal here sidesteps that by being broadcast. Every node sees it, and matches against an actuator_id it already knows about itself (e.g. via a HW DIP switch or another out-of-band method). It's a config-distribution mechanism, not a new remap mechanism.

The message is of course not needed when someone builds a couple of drones, the existing systems are enough for that. This message is entirely optional.

the message doesn't say anything about persistence. Persistence would be an absolute requirement as the same output can be dangerous on one actuator but not another. You don't want to have to configure everything at runtime, that would be actively dangerous in other ways, too.

Agreed, I'll add this to the message description. The persistence lives on the flight controller. The way I currently have it implemented in a PX4 branch:

  • The node is configured to use ChannelConfig via a single parameter. Once enabled, it does not react to ArrayCommand at all until the first ChannelConfig is received.
  • The FC holds the valid mapping and that stays consistent across reboots, the nodes only ever use whatever mapping the FC last gave them via ChannelConfig.
  • ChannelConfig is sent periodically at low frequency. So even if an individual frame is lost or a node is not up yet the node will pick it up on the next resend.

@IamPete1

IamPete1 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

I still think this is madness. It would require a actuator ID and actuator channel to be configured on the flight controller. For ArduPilot this would be 64 new parameters. There are several other ways to streamline complex configurations.

  • Just have a firmware for each node that comes with the correct parameters baked in.

  • If muti firmware is too hard of a requirement have one firmware that looks at the dip switches and sets the correct config itself.

  • If firmware setting its own config for more than one parameter is too much effort have the dip switch set the node ID. Then the flight controller (or a python script that connects via the flight controller) can set the correct parameters on each node based on its ID.

@alexcekay

alexcekay commented Jul 8, 2026

Copy link
Copy Markdown
Author

@IamPete1:

I still think this is madness. It would require an actuator ID and actuator channel to be configured on the flight controller. For ArduPilot this would be 64 new parameters.

Here are the measurements from the reference implementation I created for PX4:

px4_fmu-v5x [Total VM Diff: 2396 byte (0.12 %)]
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.1% +2.35Ki  +0.1% +2.35Ki    .text
    +0.3%    +560  +0.3%    +560    [section .text]
    +0.3%    +476  +0.3%    +476    g_cromfs_image
    +1.6%    +248  +1.6%    +248    px4::parameters
    [NEW]    +228  [NEW]    +228    UavcanChannelConfigPublisher::publish_channel_config()
    [NEW]    +176  [NEW]    +176    UavcanChannelConfigPublisher::UavcanChannelConfigPublisher()
    [NEW]    +148  [NEW]    +148    _GLOBAL__sub_I__ZN28UavcanChannelConfigPublisherC2ERN6uavcan5INodeE
    +1.6%    +128  +1.6%    +128    uavcan::GlobalDataTypeRegistry::registerDataType<>()
     +34%     +92   +34%     +92    uavcan::Publisher<>::Publisher()
    [NEW]     +80  [NEW]     +80    UavcanChannelConfigPublisher::init()
     +12%     +56   +12%     +56    uavcan::TimerEventForwarder<>::handleTimerEvent()
    [NEW]     +42  [NEW]     +42    uavcan::Array<>::encodeImpl()
     +13%     +40   +13%     +40    uavcan::TimerEventForwarder<>::~TimerEventForwarder()
    +1.7%     +32  +1.7%     +32    px4::parameters_type
    +1.9%     +24  +1.9%     +24    uavcan::MethodBinder<>::operator bool()
    +4.3%     +24  +4.3%     +24    uavcan::StaticTransferBuffer<>
     +12%     +24   +12%     +24    uavcan::TimerEventForwarder<>
    +3.7%     +20  +3.7%     +20    _GLOBAL__sub_I__ZN10UavcanNode9_instanceE
    +3.7%     +20  +3.7%     +20    _GLOBAL__sub_I__ZN13UavcanServersC2ERN6uavcan5INodeERNS0_17NodeInfoRetrieverE
    +1.5%     +16  +1.5%     +16    UavcanNode::UavcanNode()
    +3.0%     +16  +3.0%     +16    UavcanNode::~UavcanNode()
   -100.4%     -42 -100.4%     -42    [27 Others]
  +1.0%      +4  +1.0%      +4    .init_section
  -0.1%     -16  -0.1%     -16    .ramfunc
     +14%      +1   +14%      +1    __stm32_i2c_set_bytes_to_transfer_veneer
   -12.5%      -1 -12.5%      -1    __getpid_veneer
    -1.7%      -4  -1.7%      -4    param_get
    -1.4%      -4  -1.4%      -4    param_get_default_value
   -25.0%      -4 -25.0%      -4    param_get_index
    -8.3%      -4  -8.3%      -4    param_get_system_default_value
  +0.4%  +202Ki  +0.1% +2.34Ki    TOTAL

The entire feature (not just params) costs 0.12% of FLASH on a standard F7/H7 flight controller. In PX4 its 30 params rather than 64, but even in this case the param part is mostly:

   +1.6%    +248  +1.6%    +248    px4::parameters
    +1.7%     +32  +1.7%     +32    px4::parameters_type

So even if you double this it's still marginal FLASH usage. The cost for persistence is of course way lower as this param FLASH cost contains all the metadata and so on. The persistence cost (actual stored param values) is a fraction of that since the FLASH diff above includes parameter metadata and the default value table including strings, which is not needed for the actual persistence.

The params also integrate directly into QGC's actuator configuration tab as shown in a screenshot above. Users can see and change the full mapping in one place, no need to work with raw parameter values.

Regarding your points:

Just have a firmware for each node that comes with the correct parameters baked in.

This requires different firmware builds per physical position, we discussed this above in detail.

If muti firmware is too hard of a requirement have one firmware that looks at the dip switches and sets the correct config itself.

This means the integrator now needs to own a fork of the node firmware (assuming the node he wants to use even is open-source). ChannelConfig does not require this, integrators can just use HW that supports it and don't need a SW dev maintaining this. I think it makes way more sense to solve this once instead of having all integrators needing to create node forks.

If firmware setting its own config for more than one parameter is too much effort have the dip switch set the node ID. Then the flight controller (or a python script that connects via the flight controller) can set the correct parameters on each node based on its ID.

  • This adds a sync need when implemented on the FC: Now you need logic running on the FC which describes which params need to be set for which Node ID, it has to wait until the node is online, so wait for NodeStatus before actually doing the param write. I think his logic is more complex to get right than what is proposed here. It is also harder to understand and less transparent than just having a look at QGC actuator tab and directly see which node handles what.
  • The Python script adds another maintenance dimension that would be completely avoided when the config is stored on the FC. No need to maintain the script and its configuration. However, the more problematic part with this is field replacement, now you need to give your FlightOps team the Python script or tell them what exactly they need to configure for the node with ID 1. With the ChannelConfig you just grab the HW (it's the same FW for all of them), set the DIP switch and replace the broken unit. No tools, no script, no knowledge about any of the parameter needed by FlightOps.

@IamPete1

IamPete1 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

So even if you double this it's still marginal FLASH usage. The cost for persistence is of course way lower as this param FLASH cost contains all the metadata and so on. The persistence cost (actual stored param values) is a fraction of that since the FLASH diff above includes parameter metadata and the default value table including strings, which is not needed for the actual persistence.

Its not the flash cost of the parameters I care about. Its the parameters themselves, it is more parameters to download every time you connect to the vehicle. Its more parameters to track and manage through configuration and updates.

I don't think the custom firmware or python script is really a genuine issue, one would hope that the companies making thousands of vehicles per day would be able to manage a small script. You could even make the python script look exactly like the QGC interface you propose (or get it into QGC directly). Just is would have to reach in and set parameters on the nodes not on the flight controller.

  • The Python script adds another maintenance dimension that would be completely avoided when the config is stored on the FC. No need to maintain the script and its configuration. However, the more problematic part with this is field replacement, now you need to give your FlightOps team the Python script or tell them what exactly they need to configure for the node with ID 1. With the ChannelConfig you just grab the HW (it's the same FW for all of them), set the DIP switch and replace the broken unit. No tools, no script, no knowledge about any of the parameter needed by FlightOps.

This approach relies on using dynamic node allocation. That means when the vehicle has a node reporting that it is unhealthy you have no idea where on the vehicle it is to replace it. You would have search through all the nodes manualy.

I just don't think this config message is actually solving a genuine problem. It doesn't add anything that a GCS or config script can't already do.

@alexcekay
alexcekay force-pushed the pr-actuator-channel-config branch from 6764c13 to 853b56f Compare July 8, 2026 12:24
@alexcekay

alexcekay commented Jul 8, 2026

Copy link
Copy Markdown
Author

I think both sides made enough arguments in this thread to see the direction this is heading.

Let's be honest we have very different perspectives on the problem space this message addresses and could continue discussing here indefinitely and would not reach a common understanding. For me it looks like you guys don't face this kind of problem with similar constrains so there is no need for you to have it solved.

This leads to different trade-offs being chosen, which in your case end up with preferring to not implement anything new. For us it's an easy choice: 30 (or 64) params don't matter at all when on most systems more than 1000 are already downloaded. It gets you a transparent centrally managed system that solves all of the discussed problems with a simple DSDL message and minimal code changes in PX4.

The use cases and design rationale are documented in this PR and anyone is free to evaluate. For anyone interested I can provide the reference implementation in PX4 for the FC and the Node it's small and we use it on our systems already.

I think all of the participants who until now voiced their opinions in this PR will not be able to agree on basically anything regarding this PR. Thus we'll just continue using this in our vendor namespace, I'll leave this PR open for others who face similar problems in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants