Skip to content

PINIO: update configurator for unified PINIO/PWM output#2579

Merged
sensei-hacker merged 6 commits into
iNavFlight:maintenance-10.xfrom
sensei-hacker:feature/unified-pinio-pwm-output
May 4, 2026
Merged

PINIO: update configurator for unified PINIO/PWM output#2579
sensei-hacker merged 6 commits into
iNavFlight:maintenance-10.xfrom
sensei-hacker:feature/unified-pinio-pwm-output

Conversation

@sensei-hacker

@sensei-hacker sensei-hacker commented Feb 28, 2026

Copy link
Copy Markdown
Member
image

Problem

Users sometimes need PINIO that isn't natively provided by the board, or want to do duty-cycle based PWM.
Turning a pin on and off is of course easy to do technically, but INAV didn't provide a way for users to configure that.

As a workaround, a clever user who needed a a PINIO might discover INAV had a special-case PWM duty-cycle mode tied specifically to the LED strip pin — primarily used for camera control (OSD joystick).
A clever user could use the Programming tab to set 0% or 100% duty cycle — a roundabout approach to make a PINIO out of a little-known feature.
This was useful but very limited: only that one pin could do duty-cycle PWM.

Meanwhile, the PINIO subsystem (USER1–USER4 outputs, controllable via the Modes tab) provided clean on/off GPIO control, but only for the one or or two pins specifically chosen by the manufacturer.

Then we had another special case for the LED pin - it could be used as a regular PWM output for a servo or whatever. Lots of special cases, handled by special-case code paths.

Solution

This PR is the configurator side of consolidating duty-cycle PWM control into the PINIO subsystem and PWM pin assignment system (firmware PR: iNavFlight/inav#11375), making it available on any PWM-capable timer output — not just the LED pin, removing the special cases and allowing the user to assign these functions to any capable output pin.

Users can now:

  • Set any timer output to "PINIO / DUTY CYCLE" mode in the mixer Output tab
  • Control its duty cycle directly from the Programming tab using the PINIO PWM condition with a channel index and duty value
  • See the correct USER number (USER1, USER2, etc.) displayed in the Output Mapping tab automatically — the firmware reports the channel assignment, so the configurator doesn't need to figure it out

No roundabout workarounds needed.

I expect a slight decrease in total lines of code by removing the special-case code paths and making it more general.

Testing

  • Transpiler tests: 28/28 passing
  • UI functional testing (output mapping USER label, PINIO PWM condition with two operands): pending physical FC connection

Notes

The LED Pin PWM programming condition is renamed to PINIO PWM. The second operand (duty cycle) is now enabled — operandA = channel index, operandB = duty. Users with existing LED Pin PWM conditions will see them relabeled and will need to set the operands correctly.

Companion firmware PR: iNavFlight/inav#11375

- outputMapping: detect PINIO outputs via specialLabels (firmware-assigned
  USER number) instead of timerOverrides cross-referencing; USER number
  derived directly from specialLabels value (2=USER1, 3=USER2, etc.)
- mixer: label PINIO timer output mode as "PINIO / DUTY CYCLE"
- logicConditions: rename condition 52 from "LED Pin PWM" to "PINIO PWM";
  enable second operand (operandB = duty cycle, operandA = channel index)
- transpiler: rename LED_PIN_PWM to PINIO_PWM throughout; update decompiler
  handler for two-operand pinioPwm(channel, duty) semantics
Fix operand order for backward compatibility: operandA=duty, operandB=pin
(0=LED pin, 1=USER1, 2=USER2, ...). Old LED_PIN_PWM configs with
operandA=duty and operandB=0 continue to work unchanged.

Rename condition label to "PWM on pin" to reflect operand order.

Add pwmOnPin(duty, pin) to JavaScript transpiler with full round-trip
support (compile JS->LC and decompile LC->JS). Function name mirrors
the "PWM on pin" label: duty first, then pin.

Also fix transformBodyStatement to dispatch CallExpression nodes, which
was silently dropping function calls in if-statement bodies.

Add round-trip tests: compile, decompile, and JS->LC->JS for LED pin,
USER1, and USER2.
@sensei-hacker sensei-hacker changed the base branch from maintenance-9.x to maintenance-10.x March 9, 2026 17:06
@sensei-hacker sensei-hacker added this to the 10.0 milestone Mar 9, 2026
- Fix LED label: use usageFlags (current assignment) instead of
  specialLabels (silkscreen), only label first pin on LED timer
- Remove bogus servosToGo guard that prevented LED labels
- Add TIM_USE_PINIO support so PINIO-overridden timer pins appear
  in the output count and get labeled in the function row
- Remove leftover console.log debug statements
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
11.2% Duplication on New Code (required ≤ 3%)
D Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@sensei-hacker sensei-hacker changed the base branch from maintenance-10.x to maintenance-9.x April 26, 2026 00:21
@sensei-hacker sensei-hacker changed the base branch from maintenance-9.x to maintenance-10.x April 26, 2026 00:21
Three bugs fixed in the two-pass timer priority algorithm:

1. Motor numbering: getOutputTable() numbered motors by array index,
   so dedicated motors (Pass 0) showed wrong motor numbers. Now does
   a two-pass pre-numbering: dedicated MOTORS outputs first, then auto.

2. B-type overflow: Pass 1 was missing the firmware's guard to skip
   outputs with dedicated overrides. A MOTORS-overridden pin whose
   quota was already full could incorrectly fall through to servo
   assignment. Matches firmware's explicit continue guard.

3. Type mismatch: setTimerOverride() stored $select.val() as a string,
   but all comparisons in getTimerMap() used strict === against number
   constants. This silently disabled the entire two-pass algorithm in
   the live UI (unit tests used numbers directly and didn't catch it).
   Fixed with parseInt() at the entry point.

Also add a change handler to timer override selects so the output
mapping preview updates live without needing to save first.

Add js/tests/outputMapping.test.mjs: 31 unit tests covering all timer
override combinations (all-auto, dedicated motors, dedicated servos,
mixed, overflow, interleaved, LED).
…m-output

Combine PINIO output support (HEAD) with timer priority fixes (merged branch):

- PINIO pre-assignment: outputs with specialLabels >= SPECIAL_LABEL_PINIO_BASE
  are assigned OUTPUT_TYPE_PINIO before the two-pass motor/servo loop, since
  PINIO identity comes from MSP specialLabels not from timer priority.
- Two-pass priority, motor numbering, B-type overflow guard, parseInt fix,
  and live change handler all carried forward from fix/output-mapping-timer-priority.
@sonarqubecloud

sonarqubecloud Bot commented May 4, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
4.3% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@sensei-hacker sensei-hacker marked this pull request as ready for review May 4, 2026 04:24
@qodo-code-review

Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Consolidate PINIO/PWM output control with unified configurator support

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Consolidate PINIO/PWM output control into unified system
  - Rename LED Pin PWM condition to PWM on pin with dual operands
  - Enable second operand for duty cycle control (operandA=duty, operandB=pin)
• Fix motor numbering to match firmware assignment order
  - Number motors in dedicated-first pass order, not physical array index
  - Add Pass 1 guard to prevent overflow dedicated outputs from being reassigned
• Add PINIO output type support to output mapping
  - Detect PINIO outputs via specialLabels instead of timer overrides
  - Display correct USER number (USER1-USER4) in output mapping table
  - Add PINIO / DUTY CYCLE option to mixer timer output mode selector
• Implement pwmOnPin(duty, pin) JavaScript transpiler function
  - Full round-trip support: compile JS→LC and decompile LC→JS
  - Backward compatible with old LED_PIN_PWM configurations
Diagram
flowchart LR
  A["User selects<br/>PINIO/DUTY CYCLE<br/>in Mixer"] --> B["Timer override<br/>set to mode 4"]
  B --> C["Output mapping<br/>detects via<br/>specialLabels"]
  C --> D["Display USER1-4<br/>in output table"]
  E["Programming tab<br/>PWM on pin condition"] --> F["operandA=duty<br/>operandB=pin"]
  F --> G["Transpile to<br/>pwmOnPin JS function"]
  G --> H["Firmware executes<br/>duty cycle control"]
Loading

Grey Divider

File Changes

1. js/logicConditionOperators.js ✨ Enhancement +3/-3

Rename LED Pin PWM to PWM on pin condition

js/logicConditionOperators.js


2. js/outputMapping.js 🐞 Bug fix +40/-9

Add PINIO output type detection and labeling

js/outputMapping.js


3. js/transpiler/transpiler/action_decompiler.js ✨ Enhancement +6/-6

Update decompiler for PINIO PWM two-operand semantics

js/transpiler/transpiler/action_decompiler.js


View more (8)
4. js/transpiler/transpiler/codegen.js ✨ Enhancement +9/-0

Add pwmOnPin code generation for PinioPwm actions

js/transpiler/transpiler/codegen.js


5. js/transpiler/transpiler/decompiler.js ✨ Enhancement +1/-1

Rename LED_PIN_PWM to PINIO_PWM operation constant

js/transpiler/transpiler/decompiler.js


6. js/transpiler/transpiler/inav_constants.js ✨ Enhancement +2/-2

Rename operation constant and update operation names

js/transpiler/transpiler/inav_constants.js


7. js/transpiler/transpiler/parser.js ✨ Enhancement +16/-0

Add pwmOnPin parser and fix CallExpression dispatch

js/transpiler/transpiler/parser.js


8. tabs/mixer.js ✨ Enhancement +5/-0

Add PINIO/DUTY CYCLE option to timer output selector

tabs/mixer.js


9. js/tests/outputMapping.test.mjs 🧪 Tests +765/-0

Comprehensive tests for two-pass timer priority algorithm

js/tests/outputMapping.test.mjs


10. js/transpiler/transpiler/tests/pinio_pwm.test.cjs 🧪 Tests +187/-0

Round-trip tests for pwmOnPin compile and decompile

js/transpiler/transpiler/tests/pinio_pwm.test.cjs


11. js/transpiler/transpiler/tests/run_pinio_pwm_tests.cjs 🧪 Tests +18/-0

Test runner for PINIO PWM round-trip validation

js/transpiler/transpiler/tests/run_pinio_pwm_tests.cjs


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (2)

Grey Divider


Action required

1. PINIO / DUTY CYCLE not localized 📘 Rule violation ⚙ Maintainability
Description
The new timer output mode option introduces hardcoded user-facing text in DOM HTML generation
instead of using i18next data-i18n keys. This prevents proper localization and violates the
project’s i18n requirements for UI text.
Code

tabs/mixer.js[R131-138]

                                '<option value=' + FC.OUTPUT_MAPPING.TIMER_OUTPUT_MODE_MOTORS + '' + (usageMode == FC.OUTPUT_MAPPING.TIMER_OUTPUT_MODE_MOTORS ? ' selected' : '')+ '>MOTORS</option>'+
                                '<option value=' + FC.OUTPUT_MAPPING.TIMER_OUTPUT_MODE_SERVOS + '' + (usageMode == FC.OUTPUT_MAPPING.TIMER_OUTPUT_MODE_SERVOS ? ' selected' : '')+ '>SERVOS</option>'+
                                '<option value=' + FC.OUTPUT_MAPPING.TIMER_OUTPUT_MODE_LED + '' + (usageMode == FC.OUTPUT_MAPPING.TIMER_OUTPUT_MODE_LED ? ' selected' : '')+ '>LED</option>'+
+                                '<option value=' + FC.OUTPUT_MAPPING.TIMER_OUTPUT_MODE_PINIO + '' + (usageMode == FC.OUTPUT_MAPPING.TIMER_OUTPUT_MODE_PINIO ? ' selected' : '')+ '>PINIO / DUTY CYCLE</option>'+
                            '</select>' +
                            '<label for="timer-output-' + t + '">' +
                                '<span> Timer ' + (parseInt(t) + 1) + '</span>' +
                            '</label>' +
Evidence
PR Compliance ID 3 requires localizable UI text to be rendered via i18next keys (e.g., data-i18n)
rather than hardcoded strings. The newly added option label PINIO / DUTY CYCLE is inserted as a
literal string in generated HTML.

CLAUDE.md
tabs/mixer.js[127-138]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A new user-facing label (`PINIO / DUTY CYCLE`) is inserted into the DOM as a hardcoded string, which bypasses i18next localization.

## Issue Context
Per i18n requirements, UI strings should be represented using `data-i18n` attributes (and corresponding locale keys) so translations work consistently.

## Fix Focus Areas
- tabs/mixer.js[127-138]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Missing pwmOnPin args guard 📘 Rule violation ☼ Reliability
Description
The new inav.override.pwmOnPin(duty, pin) parser path does not validate that both arguments are
present before transforming them. This can generate malformed actions (null operands) and lead to
incorrect codegen or confusing behavior instead of failing fast with a clear error/warning.
Code

js/transpiler/transpiler/parser.js[R342-352]

+    // inav.override.pwmOnPin(duty, pin)
+    // Name mirrors the operand order: pwm (duty=operandA) first, pin (operandB) second
+    if (expr.callee.type === 'MemberExpression' &&
+        expr.callee.object && expr.callee.object.type === 'MemberExpression' &&
+        expr.callee.object.object && expr.callee.object.object.name === 'inav' &&
+        expr.callee.object.property && expr.callee.object.property.name === 'override' &&
+        expr.callee.property && expr.callee.property.name === 'pwmOnPin') {
+      const duty = this.transformExpression(expr.arguments[0]);
+      const pin  = this.transformExpression(expr.arguments[1]);
+      return { type: 'PinioPwm', pin, duty, loc, range };
+    }
Evidence
PR Compliance ID 5 requires defensive guards/early returns before accessing nested properties or
operating on potentially malformed inputs. The new pwmOnPin handling unconditionally reads
expr.arguments[0] and expr.arguments[1] without validating argument count/presence, allowing
malformed calls like pwmOnPin(50) to proceed and produce a PinioPwm node with missing operands.

js/transpiler/transpiler/parser.js[342-352]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The parser’s new `pwmOnPin` call handling does not check that two arguments are provided before transforming them, which can yield a malformed `{ type: 'PinioPwm', ... }` node.

## Issue Context
User-authored program code is untrusted/malformed-prone. The parser should guard missing/invalid arguments and emit a clear warning/error + return `null` (or a safe fallback) rather than silently creating an action with `null` operands.

## Fix Focus Areas
- js/transpiler/transpiler/parser.js[342-352]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread tabs/mixer.js
Comment on lines 131 to 138
'<option value=' + FC.OUTPUT_MAPPING.TIMER_OUTPUT_MODE_MOTORS + '' + (usageMode == FC.OUTPUT_MAPPING.TIMER_OUTPUT_MODE_MOTORS ? ' selected' : '')+ '>MOTORS</option>'+
'<option value=' + FC.OUTPUT_MAPPING.TIMER_OUTPUT_MODE_SERVOS + '' + (usageMode == FC.OUTPUT_MAPPING.TIMER_OUTPUT_MODE_SERVOS ? ' selected' : '')+ '>SERVOS</option>'+
'<option value=' + FC.OUTPUT_MAPPING.TIMER_OUTPUT_MODE_LED + '' + (usageMode == FC.OUTPUT_MAPPING.TIMER_OUTPUT_MODE_LED ? ' selected' : '')+ '>LED</option>'+
'<option value=' + FC.OUTPUT_MAPPING.TIMER_OUTPUT_MODE_PINIO + '' + (usageMode == FC.OUTPUT_MAPPING.TIMER_OUTPUT_MODE_PINIO ? ' selected' : '')+ '>PINIO / DUTY CYCLE</option>'+
'</select>' +
'<label for="timer-output-' + t + '">' +
'<span> Timer ' + (parseInt(t) + 1) + '</span>' +
'</label>' +

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. pinio / duty cycle not localized 📘 Rule violation ⚙ Maintainability

The new timer output mode option introduces hardcoded user-facing text in DOM HTML generation
instead of using i18next data-i18n keys. This prevents proper localization and violates the
project’s i18n requirements for UI text.
Agent Prompt
## Issue description
A new user-facing label (`PINIO / DUTY CYCLE`) is inserted into the DOM as a hardcoded string, which bypasses i18next localization.

## Issue Context
Per i18n requirements, UI strings should be represented using `data-i18n` attributes (and corresponding locale keys) so translations work consistently.

## Fix Focus Areas
- tabs/mixer.js[127-138]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@sensei-hacker sensei-hacker merged commit fd9388a into iNavFlight:maintenance-10.x May 4, 2026
6 of 7 checks passed
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