Skip to content

output assignment: firmware-authoritative MSP2 READ/QUERY API#11564

Open
sensei-hacker wants to merge 7 commits into
iNavFlight:maintenance-10.xfrom
sensei-hacker:feature/output-assignment-api-v2
Open

output assignment: firmware-authoritative MSP2 READ/QUERY API#11564
sensei-hacker wants to merge 7 commits into
iNavFlight:maintenance-10.xfrom
sensei-hacker:feature/output-assignment-api-v2

Conversation

@sensei-hacker
Copy link
Copy Markdown
Member

Problem

The Configurator's Mixer/Outputs tab re-implements the firmware's output assignment algorithm in JavaScript (getTimerMap()). When firmware and Configurator versions differ, the tab shows wrong pin assignments. On targets with explicit timer overrides (e.g. OUTPUT_MODE_MOTORS/OUTPUT_MODE_SERVOS set per-timer), the actual motor/servo pin mapping silently changes when firmware is upgraded independently of the Configurator.

Solution

Two new MSP messages make the firmware the sole authority on assignments:

MSP2_INAV_OUTPUT_ASSIGNMENT (0x210E) — READ

On tab load, the Configurator requests the finalized post-boot output assignment. Response: [(u8 outputIndex, u8 type, u8 number)] per assigned output, where type is 1=MOTOR or 2=SERVO and number is the 1-indexed motor/servo ordinal.

MSP2_INAV_QUERY_OUTPUT_ASSIGNMENT (0x210F) — QUERY/preview

Request: (u8 timerCount, [u8 timerId, u8 outputMode] × N). The Configurator sends proposed timer overrides; the firmware runs its assignment algorithm on those overrides (save/restore approach — no hardware side-effects) and returns the resulting assignment in the same format as READ. Enables live preview when the user changes a timer override dropdown, without save+reboot.

Old Configurator connected to new firmware: ignores the new messages, uses existing getTimerMap() (no regression).
New Configurator connected to old firmware: falls back to getTimerMap() automatically when the messages return error.

Changes

  • src/main/drivers/pwm_mapping.h: expose timMotorServoHardware_t; add OUTPUT_ASSIGNMENT_TYPE_* defines; declare pwmGetOutputAssignment() and pwmCalculateAssignment()
  • src/main/drivers/pwm_mapping.c: promote timOutputs to static timMotorServoHardware_t timOutputsStatic; add pwmGetOutputAssignment() getter; add pwmCalculateAssignment() — simulates assignment for proposed overrides using save/restore of timerHardware[].usageFlags and timerOverrides config
  • src/main/fc/fc_msp.c: READ handler in mspFcProcessOutCommand (#ifndef SITL_BUILD); QUERY handler in mspFCProcessInOutCommand (#ifndef SITL_BUILD) with timerCount clamped to HARDWARE_TIMER_DEFINITION_COUNT
  • src/main/msp/msp_protocol_v2_inav.h: reserve 0x210E and 0x210F

Testing

  • Built and flashed to BROTHERHOBBYF405V3 (STM32F405)
  • 0x210E READ: returns 8 valid tuples — Motor 1–4 on outputs 0–3, Servo 1–4 on outputs 4–7
  • 0x210F QUERY: sending current overrides returns result matching READ exactly
  • MSP v2 framing verified with protocol analyser (DVB-S2 CRC, $X> response frames)
  • Mixer tab in Configurator: direct assignment populates correctly, output table shows ["Motor 1", ..., "Servo 1", ...]

Code Review

Code review performed. Issues addressed:

  • savedFlags[] sized to TIMER_HW_MAX = 64 (not MAX_PWM_OUTPUTS) to cover all timer hardware entries; early-return guard added
  • timerCount clamped in QUERY handler to prevent excess loop iterations
  • OUTPUT_ASSIGNMENT_TYPE_LED removed (LED outputs conveyed via existing MSP2_INAV_OUTPUT_MAPPING_EXT2 usageFlags)
  • ISR safety documented: pwmCalculateAssignment() must be called only from the main loop / MSP task

Notes

Exposes the firmware's post-boot output assignment as an MSP message so
the Configurator no longer needs to mirror the assignment algorithm in JS.

## New MSP messages

MSP2_INAV_OUTPUT_ASSIGNMENT (0x210E) — READ
  Response: [(u8 outputIndex, u8 type, u8 number)] per assigned output
  type: 1=MOTOR 2=SERVO 3=LED; number: 1-indexed ordinal
  Sent once on Mixer tab load; Configurator uses this for display instead
  of running getTimerMap() locally.

MSP2_INAV_QUERY_OUTPUT_ASSIGNMENT (0x210F) — QUERY/preview
  Request: (u8 timerCount, [u8 timerId, u8 outputMode] × N)
  Response: same format as READ
  Lets Configurator preview assignments for proposed timer overrides
  without save+reboot. Firmware runs its own algorithm on the proposed
  overrides (save/restore approach, no hardware side-effects) and returns
  the result.

## Implementation

- pwm_mapping.h: expose timMotorServoHardware_t; add OUTPUT_ASSIGNMENT_TYPE_*
  defines; declare pwmGetOutputAssignment() and pwmCalculateAssignment()
- pwm_mapping.c: promote timOutputs to static; add getter and pure-function
  calculator (saves/restores timerHardware flags and timerOverrides config)
- fc_msp.c: READ handler in mspFcProcessOutCommand (#ifndef SITL_BUILD);
  QUERY handler in mspFCProcessInOutCommand (#ifndef SITL_BUILD)
- msp_protocol_v2_inav.h: reserve 0x210E and 0x210F

## Fallback

Old firmware returns error/no-data; Configurator falls back to getTimerMap()
(the existing JS algorithm). No behaviour change for users on older firmware.

Companion configurator PR: iNavFlight/inav-configurator#XXXX
@qodo-code-review
Copy link
Copy Markdown
Contributor

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

MAX_PWM_OUTPUTS expands to MAX_PWM_OUTPUT_PORTS which is not defined
in SITL targets. The struct and its dependent function declarations
are hardware-only; wrap them in #ifndef SITL_BUILD to match the
existing pattern used in fc_msp.c for the MSP handlers.
INAV has no runtime ASSERT macro — STATIC_ASSERT is compile-time only.
The ptrdiff_t bounds checks were unnecessary since timMotors/timServos
pointers are guaranteed to point within timerHardware[] by construction
in pwmBuildTimerOutputList(). Replace with the direct cast used in the
READ handler.
@github-actions
Copy link
Copy Markdown

Test firmware build ready — commit be6b399

Download firmware for PR #11564

234 targets built. Find your board's .hex file by name on that page (e.g. MATEKF405SE.hex). Files are individually downloadable — no GitHub login required.

Development build for testing only. Use Full Chip Erase when flashing.

The isMixerUsingServos parameter was marked UNUSED; the function
populated timServos[] with all servo-capable hardware pins regardless
of how many servos the mixer actually needs.

This caused MSP2_INAV_OUTPUT_ASSIGNMENT to report phantom servo
assignments — e.g., reporting 4 servo outputs on a quadcopter with
only 1 servo in the mixer.

Fix mirrors the existing motor pattern: only add a servo slot when
maxTimServoCount < servoCount, and skip all servo slots when
isMixerUsingServos is false.
pwmBuildTimerOutputList() previously used getServoCount() which reads
mixerProfiles->ServoMixers via computeServoCount().  That PG array is
never cleared by the Configurator's MSP2_INAV_SET_SERVO_MIXER handler
(which writes only to customServoMixers).  Stale rules in
mixerProfiles->ServoMixers survive settings-preserving firmware flashes
and cause phantom servo outputs even with an empty servo mixer.

Fix: count servo outputs directly from customServoMixers — the same PG
source that loadCustomServoMixer() and the CLI smix command use for the
current profile.  The inactive profile (dual-profile setups) continues
to use mixerServoMixersByIndex(nextMixerProfileIndex) which is populated
when the user saves a profile via the Configurator.
…tList

The inactive profile's mixerProfiles(j)->ServoMixers is a separate PG
that the Configurator never writes to via MSP2_INAV_SET_SERVO_MIXER,
so it can contain stale data from old configurations that survives
settings-preserving firmware upgrades.  Checking it caused phantom
servo outputs whenever that stale data had any non-zero rate rule.

Use only customServoMixers — the Configurator-facing PG — to determine
how many servo outputs to allocate.  Dual-profile output reallocation
is handled at runtime by outputProfileHotSwitch() when the user switches
mixer profiles.
Remove the isMixerUsingServos gate: that flag reads mixerProfiles->ServoMixers
which can disagree with customServoMixers in both directions (stale non-zero
data → phantom servos; clean data when real rules exist → zero servos). Scanning
customServoMixers directly and unconditionally is always correct — an empty mixer
(all rate=0) naturally produces servoCount=0 without needing the flag.
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.

1 participant