Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 26 additions & 1 deletion docs/Rx.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ bind_msp_rx <port>

## MultiWii serial protocol (MSP RX)

Allows you to use MSP commands as the RC input. Up to 18 channels are supported.
Allows you to use MSP commands as the RC input. Up to 34 channels are supported.
Note:
* It is necessary to update `MSP_SET_RAW_RC` at 5Hz or faster.
* `MSP_SET_RAW_RC` uses the defined RC channel map
Expand All @@ -213,6 +213,31 @@ Note:

Enables the use of a joystick in the INAV SITL with a flight simulator. See the [SITL documentation](SITL/SITL.md).

## MSP Auxiliary RC Channel Overlay (MSP2_INAV_SET_AUX_RC)

Allows extending the available RC channel count beyond the native RC link capacity using `MSP2_INAV_SET_AUX_RC` (`0x2230`). This is a lightweight, bandwidth-efficient alternative to `MSP_SET_RAW_RC` for auxiliary channels only.

**Key properties:**
- Controls **CH13–CH32** only (CH1–CH12 are protected and rejected)
- Configurable resolution: 2-bit (3 positions), 4-bit (~71µs steps), 8-bit (~3.9µs steps), or 16-bit (raw PWM)
- Value `0` = skip (no update) — previous value persists indefinitely
- No flight mode or special configuration required — always active
- Does **not** affect failsafe detection
- Recommended to send with `MSP_FLAG_DONT_REPLY` (`flags=0x01`) on telemetry passthrough links

**Typical use case:** A Lua script on the radio sends `MSP2_INAV_SET_AUX_RC` via SmartPort/CRSF/ELRS telemetry passthrough to control auxiliary functions (lights, camera triggers, gimbal modes) on channels beyond the RC link's native capacity.

**Priority order** (last writer wins):
1. Primary RX (SBUS, CRSF, FPort, etc.)
2. MSP RC Override (if active)
3. **MSP AUX Overlay** (CH13–CH32)

**Important:** For serial RX protocols, the firmware cannot detect which channels the sender actively uses. If AUX_RC targets a channel that the RX link also sends, AUX_RC will override it. Configure the start channel above your RC link's active channel range.

When MSP is the primary RX provider (`receiver_type = MSP`), channels covered by `MSP_SET_RAW_RC` are automatically protected. Channels in the `msp_override_channels` bitmask are also protected when MSP RC Override mode is active.

See the [MSP documentation](development/msp/README.md) for the full message format.

## Configuration

The receiver type can be set from the configurator or CLI.
Expand Down
26 changes: 26 additions & 0 deletions docs/development/msp/msp_messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -10943,6 +10943,32 @@
"notes": "All attitude angles are in deci-degrees.",
"description": "Provides estimates of current attitude, local NEU position, and velocity."
},
"MSP2_INAV_SET_AUX_RC": {
"code": 8752,
"mspv": 2,
"request": {
"payload": [
{
"name": "definitionByte",
"ctype": "uint8_t",
"desc": "Packed start channel and resolution. Bits 7-3: start channel index (valid range 12-31 for CH13-CH32; 0-11 rejected as error). Bits 2-0: resolution mode (0=2-bit, 1=4-bit, 2=8-bit, 3=16-bit; 4-7 reserved/error).",
"units": ""
},
{
"name": "channelData",
"ctype": "uint8_t",
"desc": "Packed channel values, sequential from start channel. Number of channels is derived from data size and resolution. Value 0 means skip (no update). Sub-byte modes (2-bit, 4-bit) are packed MSB-first. 2-bit values 1-3 map to 1000/1500/2000us. 4-bit values 1-15 map to 1000 + (val-1)*1000/14 us. 8-bit values 1-255 map to 1000 + (val-1)*1000/254 us. 16-bit values are direct PWM, clamped to 750-2250us.",
"units": "PWM (encoded)",
"array": true,
"array_size": 0
}
]
},
"reply": null,
"variable_len": true,
"notes": "CH1-CH12 (index 0-11) are protected and will return `MSP_RESULT_ERROR`. Payload size must be 2-49 bytes. Constraint: `startChannel + channelCount <= 32`. Values persist until overwritten; no timeout. Applied as a post-RX overlay in `calculateRxChannelsAndUpdateFailsafe()` after MSP RC Override but before failsafe. Does not require `USE_RX_MSP` or MSP-RC-OVERRIDE flight mode. Does not affect failsafe detection. When MSP is the primary RX provider, channels covered by `MSP_SET_RAW_RC` are automatically skipped. Channels in the `mspOverrideChannels` bitmask are skipped when MSP RC Override mode is active. Recommended to send with `MSP_FLAG_DONT_REPLY` (flags=0x01) to save bandwidth on telemetry passthrough links. 16-bit mode requires even number of data bytes and values are clamped to 750-2250us.",
"description": "Bandwidth-efficient auxiliary RC channel update. Sets CH13-CH32 with configurable resolution (2/4/8/16-bit) without affecting primary flight controls. Designed for extending channel count beyond native RC link capacity via MSP passthrough."
},
"MSP2_BETAFLIGHT_BIND": {
"code": 12288,
"mspv": 2,
Expand Down
106 changes: 106 additions & 0 deletions src/main/fc/fc_msp.c
Original file line number Diff line number Diff line change
Expand Up @@ -2412,6 +2412,111 @@ static mspResult_e mspFcProcessInCommand(uint16_t cmdMSP, sbuf_t *src)
}
break;
#endif

case MSP2_INAV_SET_AUX_RC:
{
// Max valid payload: 1 def byte + 24 channels × 2 bytes (16-bit) = 49 bytes
if (dataSize < 2 || dataSize > 49) {
return MSP_RESULT_ERROR;
}

const uint8_t defByte = sbufReadU8(src);
const uint8_t startChannel = defByte >> 3; // Bits 7-3: start channel index (0-31)
const uint8_t resolutionMode = defByte & 0x07; // Bits 2-0: resolution

// Safety: CH1-CH12 (index 0-11) are protected
if (startChannel < 12) {
return MSP_RESULT_ERROR;
}

const uint8_t dataBytes = dataSize - 1;
uint8_t channelCount;
uint8_t bitsPerChannel;

switch (resolutionMode) {
case 0: // 2-bit
bitsPerChannel = 2;
channelCount = dataBytes * 4;
break;
case 1: // 4-bit
bitsPerChannel = 4;
channelCount = dataBytes * 2;
break;
case 2: // 8-bit
bitsPerChannel = 8;
channelCount = dataBytes;
break;
case 3: // 16-bit
bitsPerChannel = 16;
if (dataBytes % 2 != 0) {
return MSP_RESULT_ERROR;
}
channelCount = dataBytes / 2;
break;
default:
return MSP_RESULT_ERROR;
}

if (channelCount == 0 || startChannel + channelCount > 32) {
return MSP_RESULT_ERROR;
}

// Decode and apply channel values
if (bitsPerChannel >= 8) {
// Byte-aligned modes: 8-bit and 16-bit
for (int i = 0; i < channelCount; i++) {
uint16_t rawValue;
if (bitsPerChannel == 16) {
rawValue = sbufReadU16(src);
} else {
rawValue = sbufReadU8(src);
}

if (rawValue == 0) {
continue; // skip: no update
}

uint16_t pwmValue;
if (bitsPerChannel == 16) {
pwmValue = constrain(rawValue, 750, 2250);
} else {
// 8-bit: 1-255 → 1000-2000
pwmValue = 1000 + ((uint32_t)(rawValue - 1) * 1000) / 254;
}

rxMspAuxOverlaySet(startChannel + i, pwmValue);
}
} else {
// Sub-byte modes: 2-bit and 4-bit
const uint8_t mask = (1 << bitsPerChannel) - 1;
const uint8_t channelsPerByte = 8 / bitsPerChannel;
int ch = 0;

for (int byteIdx = 0; byteIdx < (int)dataBytes && ch < channelCount; byteIdx++) {
const uint8_t dataByte = sbufReadU8(src);
for (int sub = channelsPerByte - 1; sub >= 0 && ch < channelCount; sub--, ch++) {
const uint8_t rawValue = (dataByte >> (sub * bitsPerChannel)) & mask;

if (rawValue == 0) {
continue; // skip: no update
}

uint16_t pwmValue;
if (bitsPerChannel == 2) {
// 2-bit: 1→1000, 2→1500, 3→2000
pwmValue = 1000 + (rawValue - 1) * 500;
} else {
// 4-bit: 1-15 → 1000-2000
pwmValue = 1000 + ((uint32_t)(rawValue - 1) * 1000) / 14;
}

rxMspAuxOverlaySet(startChannel + ch, pwmValue);
}
}
}
}
break;

case MSP2_COMMON_SET_MOTOR_MIXER:
sbufReadU8Safe(&tmp_u8, src);
if ((dataSize == 9) && (tmp_u8 < MAX_SUPPORTED_MOTORS)) {
Expand Down Expand Up @@ -4508,6 +4613,7 @@ mspResult_e mspFcProcessCommand(mspPacket_t *cmd, mspPacket_t *reply, mspPostPro
sbuf_t *dst = &reply->buf;
sbuf_t *src = &cmd->buf;
const uint16_t cmdMSP = cmd->cmd;

// initialize reply by default
reply->cmd = cmd->cmd;

Expand Down
2 changes: 2 additions & 0 deletions src/main/msp/msp_protocol_v2_inav.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,5 @@

#define MSP2_INAV_SET_WP_INDEX 0x2221 //in message jump to waypoint N during active WP mission; payload: U8 wp_index (0-based, relative to mission start)
#define MSP2_INAV_SET_CRUISE_HEADING 0x2223 //in message set heading while in Cruise/Course Hold mode; payload: I32 heading_centidegrees (0-35999)

#define MSP2_INAV_SET_AUX_RC 0x2230
7 changes: 7 additions & 0 deletions src/main/rx/msp.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

static uint16_t mspFrame[MAX_SUPPORTED_RC_CHANNEL_COUNT];
static bool rxMspFrameDone = false;
static uint8_t mspLastChannelCount = 0;

static uint16_t rxMspReadRawRC(const rxRuntimeConfig_t *rxRuntimeConfigPtr, uint8_t chan)
{
Expand All @@ -49,9 +50,15 @@ void rxMspFrameReceive(uint16_t *frame, int channelCount)
mspFrame[i] = 0;
}

mspLastChannelCount = channelCount;
rxMspFrameDone = true;
}

uint8_t rxMspGetLastChannelCount(void)
{
return mspLastChannelCount;
}

static uint8_t rxMspFrameStatus(rxRuntimeConfig_t *rxRuntimeConfig)
{
UNUSED(rxRuntimeConfig);
Expand Down
1 change: 1 addition & 0 deletions src/main/rx/msp.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@

void rxMspFrameReceive(uint16_t *frame, int channelCount);
void rxMspInit(const rxConfig_t *rxConfig, rxRuntimeConfig_t *rxRuntimeConfig);
uint8_t rxMspGetLastChannelCount(void);
35 changes: 35 additions & 0 deletions src/main/rx/rx.c
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ static bool isRxSuspended = false;

static rcChannel_t rcChannels[MAX_SUPPORTED_RC_CHANNEL_COUNT];

// MSP aux channel overlay: non-zero values override rcChannels[].data for CH9-CH32
static uint16_t mspAuxOverlay[MAX_SUPPORTED_RC_CHANNEL_COUNT];

rxLinkStatistics_t rxLinkStatistics;
rxRuntimeConfig_t rxRuntimeConfig;
static uint8_t rcSampleIndex = 0;
Expand Down Expand Up @@ -512,6 +515,31 @@ bool calculateRxChannelsAndUpdateFailsafe(timeUs_t currentTimeUs)
}
#endif

// Apply MSP aux channel overlay (CH13-CH32)
{
int overlayStart = 12;
#ifdef USE_RX_MSP
// When MSP is the primary RX, skip channels covered by MSP_SET_RAW_RC
if (rxConfig()->receiverType == RX_TYPE_MSP) {
const uint8_t mspChannels = rxMspGetLastChannelCount();
if (mspChannels > overlayStart) {
overlayStart = mspChannels;
}
}
#endif
for (int i = overlayStart; i < 32; i++) {
if (mspAuxOverlay[i] > 0) {
#if defined(USE_RX_MSP) && defined(USE_MSP_RC_OVERRIDE)
// Skip channels controlled by MSP RC Override when active
if (IS_RC_MODE_ACTIVE(BOXMSPRCOVERRIDE) && (rxConfig()->mspOverrideChannels & (1U << i))) {
continue;
}
#endif
rcChannels[i].data = mspAuxOverlay[i];
}
}
}

// Update failsafe
if (rxFlightChannelsValid && rxSignalReceived) {
failsafeOnValidDataReceived();
Expand Down Expand Up @@ -663,6 +691,13 @@ int16_t rxGetChannelValue(unsigned channelNumber)
}
}

void rxMspAuxOverlaySet(uint8_t channelIndex, uint16_t value)
{
if (channelIndex >= 12 && channelIndex < 32) {
mspAuxOverlay[channelIndex] = value;
}
}

void lqTrackerReset(rxLinkQualityTracker_e * lqTracker)
{
lqTracker->lastUpdatedMs = millis();
Expand Down
4 changes: 4 additions & 0 deletions src/main/rx/rx.h
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,7 @@ void resumeRxSignal(void);
// filtering and some extra processing like value holding
// during failsafe.
int16_t rxGetChannelValue(unsigned channelNumber);

// MSP aux channel overlay (CH13-CH32). Sets a channel value that persists
// across RX update cycles. value=0 ignores that channel and skips it.
void rxMspAuxOverlaySet(uint8_t channelIndex, uint16_t value);
Loading