Skip to content

fix(pl18): Sound Playing will Sometimes Sluggish - v2.11#7472

Draft
richardclli wants to merge 2 commits into
2.11from
richardclli/fix-pl18-sound-sluggish
Draft

fix(pl18): Sound Playing will Sometimes Sluggish - v2.11#7472
richardclli wants to merge 2 commits into
2.11from
richardclli/fix-pl18-sound-sluggish

Conversation

@richardclli

@richardclli richardclli commented Jun 18, 2026

Copy link
Copy Markdown
Member

Branch 2.10 is much better, so some change is backport the handling of dma buffer underrun from 2.10 branch

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for new radio boards: FlySky PA01, PL18U, ST16, and HelloRadioSky V14
    • Added Korean language support
    • New "One Log Per Day" logging option in general settings
    • Enhanced companion application workflow for automated builds
  • Improvements

    • Modernized build infrastructure and updated firmware dependencies
    • Removed 32-bit Windows companion support
    • Enhanced input and logical switch configuration handling

Restore DAC DMA underrun interrupt handler (DMAUDRIE1) removed in 6d6c449.
When audio buffers starve during flash erase, the underrun ISR stops DMA
within ~31us instead of letting circular DMA replay stale data indefinitely.

Add RTOS yield to flash_wait_for_not_busy() so the audio task gets CPU time
during long flash erases (45-400ms), reducing starvation at the source.
@richardclli richardclli added this to the 2.11.7 milestone Jun 18, 2026
@richardclli richardclli marked this pull request as draft June 18, 2026 08:58
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

EdgeTX 2.11 "Jolly Mon" release preparation: version bumped to revision 6, dev containers and CI images pinned to 2.11. Four new radio boards added (FlySky PA01, PL18U, ST16, HelloRadioSky V14) across companion and firmware. Companion storage layer mutated to preserve hardware calibration on write. New automated release-drafter workflow and companion CI orchestrator added. Multiple model-editing behavior fixes.


Release infrastructure and CI overhaul

Layer / File(s) Summary
Version bump and dev-environment pinning
CMakeLists.txt, README.md, .devcontainer/devcontainer.json, .gitpod.yml, .gitignore
VERSION_REVISION"6", CODENAME"Jolly Mon"; all dev environments pinned to 2.11; /ys_Doc added to gitignore.
CI workflow consolidation and image pinning
.github/workflows/actions.yml, .github/workflows/companion.yml, .github/workflows/linux_cpn.yml, .github/workflows/macosx_cpn.yml, .github/workflows/win_cpn-64.yml, .github/workflows/nightly.yml
actions.yml switches to paths-allowlist triggers and pins containers to 2.11; linux/macos/win64 companion workflows converted to workflow_call-only; win_cpn-32.yml deleted; new companion.yml orchestrator dispatches to all three.
Automated release-draft workflow
.github/workflows/release-drafter.yml
New 214-line workflow: validates tag against codename, runs git-cliff, downloads all CI artifacts, zips them, and creates a draft release with pre-release detection.
git-cliff changelog configuration
cliff.toml
New cliff.toml with Jinja body template grouping commits by scope (firmware/companion/color) with emoji group labels and a catch-all.

New hardware support: FlySky PA01/PL18U/ST16 and HelloRadioSky V14

Layer / File(s) Summary
Board enum, predicates, and build-system registration
companion/src/firmwares/boards.h, companion/src/CMakeLists.txt, radio/src/CMakeLists.txt, fw.json, .github/ISSUE_TEMPLATE/bug-report.yml
Four new Board::Type entries; IS_FLYSKY_PA01/PL18U/ST16, IS_HELLORADIOSKY_V14, IS_STM32H7 predicates; companion and radio CMakeLists add PCB targets and FLAVOUR mappings.
Board capability mappings
companion/src/firmwares/boards.cpp, companion/src/firmwares/generalsettings.cpp
New boards wired into all get* functions: FourCC, EEPROM/flash size, LCD dimensions, analog lookup, board name, default modules, battery ranges, and Bluetooth name defaults.
Firmware variant registration and capability dispatch
companion/src/firmwares/opentx/opentxinterface.cpp, companion/src/firmwares/opentx/opentxinterface.h, companion/src/firmwares/moduledata.cpp, companion/src/generaledit/hardware.cpp
Registers PA01, PL18U, ST16, HelloRadioSky V14 firmware blocks; adds STM32H7 Sensors branch, rotary-encoder and BacklightLevelMin expansions; broadens AFHDS3 module availability; adds Korean language.
Firmware/radio build system updates
radio/src/CMakeLists.txt, radio/src/FreeRTOSConfig.h, radio/src/audio.cpp, radio/src/audio.h, radio/src/bitmaps/*/CMakeLists.txt
ST16/PA01 PCB target branches, POWER_LED_BLUE option, NANO default ON, timer queue/stack depth doubled; bitmap CMakeLists unified to bm320/480/800_* naming without RLE.

Companion storage layer refactor

Layer / File(s) Summary
Storage interface: mutable write and GeneralSettings load
companion/src/storage/storage.h, companion/src/storage/storage.cpp, companion/src/storage/etx.h, companion/src/storage/etx.cpp, companion/src/storage/sdcard.h, companion/src/storage/sdcard.cpp, companion/src/storage/yaml.h
write changed from const RadioData& to RadioData& throughout the hierarchy; Storage gains fileExists()/getStorageFormat() helpers and a load(GeneralSettings&) override; factory-iteration loop centralized.
Calibration preservation on labeled/SD write
companion/src/storage/labeled.cpp, companion/src/storage/labeled.h, companion/src/storage/minizinterface.h
LabelsStorageFormat::write reads existing RADIO/radio.yml to copy hardware calibration fields before overwriting; new loadRadioSettings() helper; miniz -Wunused-function warnings suppressed.

Companion app model-editing and UI improvements

Layer / File(s) Summary
RawSourceRange conversion/validation API
companion/src/firmwares/rawsource.h, companion/src/firmwares/rawsource.cpp
Adds toDisplay/toRaw/validateDisplay/validateRaw; reworks GVAR range with RANGE_DELTA/ABS_FUNCTION; fixes SOURCE_TYPE_SPECIAL to use abs(index).
ModelData sortInputs, updateSourceNumRef, ScriptData sizing
companion/src/constants.h, companion/src/firmwares/modeldata.h, companion/src/firmwares/modeldata.cpp, companion/src/firmwares/input_data.cpp, companion/src/firmwares/mixdata.cpp
Sensor limit → 75, script limits/constants updated; sortInputs() and updateSourceNumRef() added; updateReference/updateCurveRef route weight/offset through new helper; channel sub-component off-by-one fixes.
LogicalSwitch panel refactor
companion/src/modeledit/logicalswitches.h, companion/src/modeledit/logicalswitches.cpp, companion/src/modelprinter.cpp
Duration/Delay columns unconditional; all handlers use LogicalSwitchData& reference; VOFS uses RawSourceRange::toRaw/toDisplay/validateDisplay; model printer uses fixed-point decimal precision.
Input swap and expo panel fixes
companion/src/modeledit/inputs.h, companion/src/modeledit/inputs.cpp, companion/src/modeledit/expodialog.cpp, companion/src/modeledit/flightmodes.cpp
cmInputSwapData simplified to chn-swap + sortInputs(); expo dialog channel display offset fixed; GVar own-value initialized from configured range bounds.
Custom functions: RGB LED scripts
companion/src/modeledit/customfunctions.h, companion/src/modeledit/customfunctions.cpp, companion/src/firmwares/customfunctiondata.cpp, companion/src/firmwares/customisation_data.h
scriptsSetRGB member for /SCRIPTS/RGBLED; changed-gating applied consistently; FuncRGBLed parameters enabled; widget/option buffer sizes increased.
General settings UI additions
companion/src/firmwares/generalsettings.h, companion/src/firmwares/edgetx/yaml_generalsettings.cpp, companion/src/generaledit/generalsetup.cpp, companion/src/generaledit/generalsetup.h, companion/src/generaledit/generalsetup.ui, companion/src/generaledit/generaledit.cpp, companion/src/generaledit/CMakeLists.txt, companion/src/translations.cpp
oneLogPerDay field serialized and exposed as checkbox; Calibration tab removed; Korean TTS added; EDGETX_APP_TRANSLATIONS_PATH env var; haptic/beeper/warning widgets rearranged.
YAML model encoding defensive fixes
companion/src/firmwares/edgetx/yaml_modeldata.cpp
Trim encoding switched to per-iteration single struct; function switch LED color decode guards node existence.
Simulator launch overhaul and SimulatorOptions v4
companion/src/simulation/simulator.h, companion/src/simulator.cpp, companion/src/helpers.cpp, companion/src/mainwindow.cpp, companion/src/mdichild.h, companion/src/mdichild.cpp, companion/src/modeledit/colorcustomscreens.cpp, companion/src/simulation/telemetryprovidercrossfire.ui, companion/src/simulation/simulateduiwidget.cpp
SimulatorOptions v4 adds flags; non-Apple platforms launch simulator via QProcess+QTemporaryDir; --flags CLI option; navigationUpdated signal wired to updateMenus; color screen buttons enlarged.
Misc companion fixes
companion/src/datamodels/compounditemmodels.cpp, companion/src/modeledit/mixerdialog.cpp, companion/src/modeledit/modeledit.cpp, companion/src/radiointerface.cpp, companion/src/wizarddata.cpp, companion/src/updates/updateinterface.cpp, cmake/Fetch*.cmake, cmake/GenericDefinitions.cmake
Switch GlobalFunctionsContext fix; mixer source group expanded; Flight Modes tab label translated; findMassstoragePath variable rename; wizard board-dependent logic removed; update preparation reordered; dependency version bumps; CMP0023 policy removed.

Sequence Diagram(s)

sequenceDiagram
    participant Companion
    participant helpers_cpp as helpers.cpp startSimulation
    participant QTemporaryDir
    participant QProcess
    participant SimulatorBinary

    Companion->>helpers_cpp: startSimulation()
    helpers_cpp->>QTemporaryDir: create temp dir
    helpers_cpp->>QTemporaryDir: write RadioData (settings + models)
    helpers_cpp->>helpers_cpp: build CLI args (--profile, --flags, tempdir)
    helpers_cpp->>QProcess: execute(simulatorBinary, args)
    QProcess->>SimulatorBinary: launch
    SimulatorBinary-->>QProcess: exit code
    QProcess-->>helpers_cpp: return code
    helpers_cpp->>Companion: show error dialog on failure / cleanup simuData
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • EdgeTX/edgetx#7234: Directly extends the same storage-layer classes (etx.*, labeled.*, sdcard.*, storage.*) whose write signatures are mutated in this PR, making it a direct dependency.
  • EdgeTX/edgetx#7414: Overlaps on the same PA01/PL18U/ST16/HelloRadioSky V14 companion board wiring (boards.*, rawsource.*, related constants/UI), CI container pinning, and devcontainer changes.

Suggested labels

bug/regression ↩️

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The pull request lacks a description entirely. The author provided no summary of changes, objectives, or context for review. Add a comprehensive pull request description including: a summary of changes, which issue(s) it fixes (appears to be #7472 based on PR metadata), and any relevant implementation details or testing notes.
Title check ⚠️ Warning The title 'fix(pl18): Sound Playing will Sometimes Sluggish - v2.11' is partially related to the changeset. While it references PL18 and sound issues, the PR encompasses a version release (v2.11) with extensive changes across CI/CD workflows, firmware board support, Companion features, and storage systems—far beyond addressing a single sound sluggishness issue. Revise the title to accurately reflect the full scope as a release/version-bump PR, such as 'chore: Release v2.11 (Jolly Mon)' or 'feat: Add PL18U and ST16 support, update to v2.11', and address the sound issue within the PR description/changelog rather than the title.
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch richardclli/fix-pl18-sound-sluggish

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@richardclli richardclli changed the title fix(pl18): sound sluggish fix(pl18): Sound Playing will Sometimes Sluggish Jun 18, 2026
spi_flash.cpp is compiled for both bootloader and main firmware.
The bootloader does not link FreeRTOS, so RTOS_WAIT_MS(1) causes
undefined reference to vTaskDelay. Guard with BOOT and use
bare-metal delay_ms(1) for the bootloader instead.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
companion/src/modeledit/customfunctions.cpp (1)

434-440: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clamp Inc/Dec GV values before displaying them.

The constant branch clamps cfn.param, but the Inc/Dec branch only clamps the QDoubleSpinBox display range. If a loaded model has an out-of-range increment after a GV range change, the UI shows a clipped value while the underlying cfn.param remains invalid.

Proposed fix
         if (cfn.adjustMode == FUNC_ADJUST_GVAR_INCDEC) {
-          double rng = abs(model->gvarData[gvidx].getMax() - model->gvarData[gvidx].getMin());
-          rng *= model->gvarData[gvidx].multiplierGet();
+          const int rawRange = abs(model->gvarData[gvidx].getMax() - model->gvarData[gvidx].getMin());
+          double rng = rawRange * model->gvarData[gvidx].multiplierGet();
           fswtchParam[i]->setMinimum(-rng);
           fswtchParam[i]->setMaximum(rng);
+          if (cfn.param < -rawRange) {
+            cfn.param = -rawRange;
+            emit modified();
+          } else if (cfn.param > rawRange) {
+            cfn.param = rawRange;
+            emit modified();
+          }
           fswtchParam[i]->setValue(cfn.param * model->gvarData[gvidx].multiplierGet());
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/modeledit/customfunctions.cpp` around lines 434 - 440, In the
FUNC_ADJUST_GVAR_INCDEC branch, clamp cfn.param to the valid range (between -rng
and rng) before multiplying it by the multiplier and setting it in
fswtchParam[i]. Currently, the code only clamps the display range via setMinimum
and setMaximum, but does not clamp the underlying cfn.param value itself. Use
std::clamp or similar logic to ensure cfn.param is clamped to the range before
it is multiplied by the multiplier in the setValue call.
🟠 Major comments (22)
.github/workflows/nightly.yml-20-22 (1)

20-22: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

New targets can produce .uf2, but nightly packaging still omits them.

With added targets (Line 20/Line 21/Line 40), nightly artifacts may miss firmware files because upload still only includes *.bin, while tools/build-gh.sh emits .uf2 for some boards.

Suggested fix
       - name: Package firmware ${{ matrix.target }}
         uses: actions/upload-artifact@v4
         with:
           name: edgetx-firmware-nightly-${{ matrix.target }}
           path: |
             fw.json
             LICENSE
             *.bin
+            *.uf2

Also applies to: 40-40

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/nightly.yml around lines 20 - 22, The nightly workflow is
configured to upload only `.bin` artifacts, but the newly added targets (pl18,
pl18ev, pl18u, pa01, t12, t12max) produce `.uf2` firmware files as well. Update
the artifact upload configuration in the nightly workflow to include both
`*.bin` and `*.uf2` file patterns so that firmware files generated by all
targets are properly packaged in nightly releases.
.github/workflows/win_cpn-64.yml-56-56 (1)

56-56: ⚠️ Potential issue | 🟠 Major

Pin all GitHub Actions to immutable commit SHAs.

Lines 27, 56, 61, and 83 reference mutable action tags. Pin all actions to full commit SHAs (e.g., actions/checkout@abc123def...) to enforce deterministic and trusted workflow execution, preventing potential supply chain attacks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/win_cpn-64.yml at line 56, Replace all mutable GitHub
Actions version tags with immutable full commit SHAs in the workflow file. On
lines 27, 56, 61, and 83, change all action references (such as
actions/checkout@v4) from using version tags to using the complete commit SHA
format (e.g., actions/checkout@<full-commit-sha>). This ensures deterministic
and secure workflow execution by preventing any changes to action behavior
through tag mutations. Verify each action reference is updated to use a pinned
commit hash rather than a mutable tag like `@v4`.

Source: Linters/SAST tools

.github/workflows/companion.yml-7-7 (1)

7-7: ⚠️ Potential issue | 🟠 Major

Use glob-compatible branch patterns instead of regex syntax.

Lines 7 and 21 contain [0-9]+.[0-9]+, but GitHub Actions branch filters use glob patterns, not regex. In glob syntax, + and . are literal characters, so this pattern won't match version branches like 2.11. Change to [0-9]*.[0-9]* to match zero or more digits on each side of the literal dot.

Suggested patch
     branches:
       - 'main'
-      - '[0-9]+.[0-9]+'
+      - '[0-9]*.[0-9]*'
@@
     branches:
       - 'main'
-      - '[0-9]+.[0-9]+'
+      - '[0-9]*.[0-9]*'

Also applies to: 21-21

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/companion.yml at line 7, The branch filter patterns at
lines 7 and 21 use regex syntax instead of GitHub Actions glob syntax. In glob
patterns, the `+` quantifier and `.` metacharacter are treated as literal
characters, so the pattern `[0-9]+.[0-9]+` will not match version branches like
2.11. Replace both occurrences of the pattern `[0-9]+.[0-9]+` with
`[0-9]*.[0-9]*` to correctly match zero or more digits on each side of the
literal dot using glob-compatible syntax.
.github/workflows/release-drafter.yml-22-22 (1)

22-22: ⚠️ Potential issue | 🟠 Major

Pin action references to immutable commit SHAs.

Lines 22 and 162 use mutable version tags (@v4 and @v2). Pin to full commit SHAs instead for supply-chain safety and policy compliance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-drafter.yml at line 22, The action references in
the release-drafter workflow file are using mutable version tags (`@v4` on line 22
and `@v2` on line 162) which pose supply-chain security risks. Replace both
mutable version tag references with their full immutable commit SHAs. For the
actions/checkout@v4 reference on line 22, resolve the current v4 tag to its
corresponding commit SHA. Similarly, update line 162 to use the full commit SHA
instead of `@v2`. This ensures that the workflow always runs the exact same
version of each action regardless of future tag movements.

Source: Linters/SAST tools

.github/workflows/macosx_cpn.yml-26-29 (1)

26-29: ⚠️ Potential issue | 🟠 Major

Set persist-credentials: false on the checkout action to prevent credential leakage.

The workflow archives artifacts (line 74) after checking out the repository. With the default persist-credentials: true, the authentication token is written to .git/config and could be included in archived artifacts, creating a security exposure.

Suggested patch
      - name: Check out the repo
        uses: actions/checkout@v4
        with:
+          persist-credentials: false
           submodules: recursive
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/macosx_cpn.yml around lines 26 - 29, The checkout action
in the "Check out the repo" step uses the default persist-credentials setting,
which writes the authentication token to .git/config and could expose it in
archived artifacts. Add the `persist-credentials: false` parameter to the
actions/checkout@v4 action to prevent this security exposure. This will ensure
credentials are not retained in the repository configuration that might be
included in artifact archives.

Source: Linters/SAST tools

.github/workflows/release-drafter.yml-64-71 (1)

64-71: ⚠️ Potential issue | 🟠 Major

Pin git-cliff version and verify checksums.

This step pulls latest at runtime and executes it without integrity verification, making releases non-reproducible and increasing supply-chain risk. Git-cliff provides SHA512 checksums on its release pages that should be used to verify the downloaded tarball.

Suggested patch (pattern)
-      - name: Install git-cliff
+      - name: Install git-cliff
+        env:
+          GIT_CLIFF_VERSION: "2.13.1"
         run: |
-          VERSION=$(wget -qO- https://api.github.com/repos/orhun/git-cliff/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/^v//')
-          FILENAME="git-cliff-${VERSION}-x86_64-unknown-linux-gnu.tar.gz"
-          wget -O git-cliff.tar.gz "https://github.com/orhun/git-cliff/releases/download/v${VERSION}/${FILENAME}"
+          FILENAME="git-cliff-${GIT_CLIFF_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
+          wget -O git-cliff.tar.gz "https://github.com/orhun/git-cliff/releases/download/v${GIT_CLIFF_VERSION}/${FILENAME}"
+          wget -O git-cliff.tar.gz.sha512 "https://github.com/orhun/git-cliff/releases/download/v${GIT_CLIFF_VERSION}/git-cliff-${GIT_CLIFF_VERSION}-x86_64-unknown-linux-gnu.tar.gz.sha512"
+          sha512sum -c git-cliff.tar.gz.sha512
           tar -xzvf git-cliff.tar.gz
-          mv git-cliff-${VERSION}/git-cliff /usr/local/bin/
+          mv git-cliff-${GIT_CLIFF_VERSION}/git-cliff /usr/local/bin/
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-drafter.yml around lines 64 - 71, The "Install
git-cliff" step dynamically fetches the latest version without pinning a
specific version and does not verify the integrity of the downloaded tarball,
creating reproducibility and supply-chain security issues. Replace the dynamic
latest version fetch with a hardcoded pinned version number, then add a checksum
verification step that downloads the SHA512 checksums from the git-cliff release
page and validates the downloaded tarball before extracting and installing it.
This ensures deterministic builds and prevents execution of potentially tampered
binaries.
.github/workflows/companion.yml-1-4 (1)

1-4: ⚠️ Potential issue | 🟠 Major

Add explicit least-privilege workflow permissions.

This orchestrator workflow lacks an explicit permissions: block and inherits broader default permissions than necessary. Following the principle of least privilege, add a permissions declaration at the workflow level.

Suggested patch
 name: Companion

 on:
   push:
+permissions:
+  contents: read
+
 jobs:

Also applies to: 37-51

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/companion.yml around lines 1 - 4, The Companion workflow
lacks an explicit permissions block at the workflow level, which causes it to
inherit broader default permissions than necessary. Add a permissions
declaration immediately after the on trigger section in the workflow file to
follow the principle of least privilege. Define only the specific permissions
required for this orchestrator workflow to function, explicitly setting all
others to the minimum required access level or denying them as appropriate.

Source: Linters/SAST tools

.github/workflows/win_cpn-64.yml-55-58 (1)

55-58: ⚠️ Potential issue | 🟠 Major

Set persist-credentials: false on checkout.

This artifact-producing workflow doesn't require authenticated Git operations after checkout. Disabling credential persistence follows the principle of least privilege and prevents the token from being available in subsequent steps.

Suggested patch
       - name: Check out the repo
         uses: actions/checkout@v4
         with:
+          persist-credentials: false
           submodules: recursive
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/win_cpn-64.yml around lines 55 - 58, The
actions/checkout@v4 action in the workflow is missing the persist-credentials
configuration option. Add persist-credentials: false to the with section of the
checkout action to follow the principle of least privilege and prevent Git
credentials from persisting in subsequent workflow steps, since this
artifact-producing workflow doesn't require authenticated Git operations after
the initial checkout.

Source: Linters/SAST tools

companion/src/modeledit/inputs.cpp-671-673 (1)

671-673: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix leaked temporary buffer and enforce null-termination when swapping input names.

The current swap leaks QByteArray and can leave inputNames[*] unterminated for max-length names.

Proposed fix
-  QByteArray *tname = new QByteArray(model->inputNames[idx2]);
-  strncpy(model->inputNames[idx2], model->inputNames[idx1], sizeof(model->inputNames[idx2]) - 1);
-  strncpy(model->inputNames[idx1], tname->data(), sizeof(model->inputNames[idx1]) - 1);
+  QByteArray tname(model->inputNames[idx2], sizeof(model->inputNames[idx2]));
+  strncpy(model->inputNames[idx2], model->inputNames[idx1], sizeof(model->inputNames[idx2]) - 1);
+  model->inputNames[idx2][sizeof(model->inputNames[idx2]) - 1] = '\0';
+  strncpy(model->inputNames[idx1], tname.constData(), sizeof(model->inputNames[idx1]) - 1);
+  model->inputNames[idx1][sizeof(model->inputNames[idx1]) - 1] = '\0';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/modeledit/inputs.cpp` around lines 671 - 673, The swap
operation for input names has two issues: the temporary QByteArray object
allocated with new is never deleted, causing a memory leak, and strncpy may not
null-terminate the strings if the source name uses the full buffer size. To fix
this, replace the dynamically allocated QByteArray pointer with a
stack-allocated QByteArray temporary (remove the new keyword), and after each
strncpy call in the swap sequence, explicitly set the final byte to null
character to ensure proper null-termination of model->inputNames[idx2] and
model->inputNames[idx1].
companion/src/CMakeLists.txt-365-366 (1)

365-366: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix the ST16 flavour variable typo.

Line 366 sets FLAVOR, but the surrounding logic uses FLAVOUR. This prevents ST16 from flowing through the same packaging/variant path as other targets.

Suggested fix
 elseif(PCB STREQUAL ST16)
-  set(FLAVOR st16)
+  set(FLAVOUR st16)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/CMakeLists.txt` around lines 365 - 366, In the CMakeLists.txt
file, the ST16 configuration block contains a spelling inconsistency where the
variable is set as `FLAVOR` instead of `FLAVOUR`. This mismatch prevents ST16
from following the same packaging and variant configuration path as other
targets. Change the variable name in the `set(FLAVOR st16)` statement to
`set(FLAVOUR st16)` to align with the spelling convention used throughout the
rest of the CMake configuration logic for other PCB types.
companion/src/firmwares/opentx/opentxinterface.cpp-354-356 (1)

354-356: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add PA01/ST16 to HasIntModuleFlySky board checks.

HasIntModuleFlySky currently excludes BOARD_FLYSKY_PA01 and BOARD_FLYSKY_ST16, but Boards::getDefaultInternalModules() maps both to MODULE_TYPE_FLYSKY_AFHDS3. This mismatch can disable FlySky-module behavior on those targets.

Suggested fix
     case HasIntModuleFlySky:
       return  id.contains("afhds2a") || id.contains("afhds3") ||
-              IS_FLYSKY_NV14(board) || IS_FLYSKY_EL18(board) || IS_FAMILY_PL18(board);
+              IS_FLYSKY_NV14(board) || IS_FLYSKY_EL18(board) || IS_FAMILY_PL18(board) ||
+              IS_FLYSKY_PA01(board) || IS_FLYSKY_ST16(board);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/firmwares/opentx/opentxinterface.cpp` around lines 354 - 356,
The HasIntModuleFlySky function is missing board checks for BOARD_FLYSKY_PA01
and BOARD_FLYSKY_ST16, which should be included in the return statement
alongside the existing board checks (IS_FLYSKY_NV14, IS_FLYSKY_EL18,
IS_FAMILY_PL18). Add conditional checks for these two missing boards in the same
manner as the existing board macros to ensure they are properly recognized as
having internal FlySky modules, matching the mappings in
Boards::getDefaultInternalModules().
companion/src/firmwares/moduledata.cpp-88-90 (1)

88-90: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Exclude the full PL18 family in the XJT blocks.

Line 88 and Line 90 exclude only IS_FLYSKY_PL18(board), but Line 101 treats IS_FAMILY_PL18(board) as the shared family. This can leave PL18U/PL18EV incorrectly eligible for XJT options.

Suggested fix
-            return !IS_ACCESS_RADIO(board, id) && !IS_FAMILY_T16(board) && !IS_FAMILY_T12(board) && !IS_FLYSKY_NV14(board) && !IS_FLYSKY_EL18(board) && !IS_FLYSKY_PL18(board) && !IS_FLYSKY_ST16(board);
+            return !IS_ACCESS_RADIO(board, id) && !IS_FAMILY_T16(board) && !IS_FAMILY_T12(board) && !IS_FLYSKY_NV14(board) && !IS_FLYSKY_EL18(board) && !IS_FAMILY_PL18(board) && !IS_FLYSKY_ST16(board);
...
-            return !(IS_ACCESS_RADIO(board, id) || id.contains("eu")) && !IS_FAMILY_T16(board) && !IS_FAMILY_T12(board) && !IS_FLYSKY_NV14(board) && !IS_FLYSKY_EL18(board) && !IS_FLYSKY_PL18(board) && !IS_FLYSKY_ST16(board);
+            return !(IS_ACCESS_RADIO(board, id) || id.contains("eu")) && !IS_FAMILY_T16(board) && !IS_FAMILY_T12(board) && !IS_FLYSKY_NV14(board) && !IS_FLYSKY_EL18(board) && !IS_FAMILY_PL18(board) && !IS_FLYSKY_ST16(board);

Also applies to: 101-101

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/firmwares/moduledata.cpp` around lines 88 - 90, The XJT pulse
blocks (PULSES_PXX_XJT and PULSES_PXX_XJT_D8) currently exclude only
IS_FLYSKY_PL18(board), but this leaves PL18 variants like PL18U and PL18EV
incorrectly eligible for XJT options since line 101 uses IS_FAMILY_PL18(board)
as the family-level check. Replace all occurrences of IS_FLYSKY_PL18(board) with
IS_FAMILY_PL18(board) in both the PULSES_PXX_XJT case and PULSES_PXX_XJT_D8 case
to properly exclude the entire PL18 family from XJT support.
cmake/FetchMiniz.cmake-19-20 (1)

19-20: ⚠️ Potential issue | 🟠 Major

Guard the warning-suppression flag by compiler.

-Wno-unused-function is not a portable option for all compilers; applying it unconditionally to miniz will cause build failures on Windows/MSVC builds, particularly if WARNINGS_AS_ERRORS is enabled. Use a CMake generator expression to apply the flag only to GCC/Clang.

Suggested fix
-# suppress compiler warnings for this add-in only
-target_compile_options(miniz PRIVATE -Wno-unused-function)
+# suppress compiler warnings for this add-in only
+target_compile_options(miniz PRIVATE
+  $<$<COMPILE_LANG_AND_ID:C,GNU,Clang,AppleClang>:-Wno-unused-function>
+)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmake/FetchMiniz.cmake` around lines 19 - 20, The target_compile_options call
for the miniz target applies the -Wno-unused-function flag unconditionally,
which causes build failures on MSVC builds when WARNINGS_AS_ERRORS is enabled
since this flag is not supported by MSVC. Wrap the compiler flag using a CMake
generator expression with $<CXX_COMPILER_ID:...> to conditionally apply the
-Wno-unused-function flag only when the compiler is GCC or Clang, excluding MSVC
from receiving this unsupported option.
companion/src/firmwares/rawsource.cpp-107-107 (1)

107-107: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize and bounds-check the GVAR index before indexing.

Line 107 uses index - 1 directly. Negative source indices are supported elsewhere via abs(index), so a negative GVAR source can index before gvarData.

Proposed fix
-      GVarData gv = model->gvarData[index - 1];
+      const int gvIndex = abs(index) - 1;
+      if (!model || gvIndex < 0 || gvIndex >= CPN_MAX_GVARS)
+        break;
+
+      GVarData gv = model->gvarData[gvIndex];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/firmwares/rawsource.cpp` at line 107, The code at line 107 in
the gvarData access uses index - 1 directly without normalizing negative
indices, which can cause out-of-bounds access when index is negative or zero.
Normalize the index using abs(index) before subtracting 1 to get the array
position, similar to how negative source indices are handled elsewhere in the
codebase. Additionally, add bounds-checking to ensure the resulting index is
within the valid range of the gvarData array before accessing it with the
expression model->gvarData[index - 1].
companion/src/firmwares/rawsource.cpp-122-125 (1)

122-125: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve zero in absolute GVAR ranges that cross zero.

For a GVAR range like -100..100, Lines 123-124 produce 100..100, so the UI can no longer select valid absolute thresholds such as 0..99.

Proposed fix
       if (flags & RANGE_ABS_FUNCTION) {
-        result.min = (flags & RANGE_DELTA_FUNCTION) ? 0 : abs(result.min);
-        result.max = abs(result.max);
+        const double min = result.min;
+        const double max = result.max;
+        result.min = (min <= 0 && max >= 0) ? 0 : std::min(abs(min), abs(max));
+        result.max = std::max(abs(min), abs(max));
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/firmwares/rawsource.cpp` around lines 122 - 125, The absolute
GVAR range handling in the block where flags & RANGE_ABS_FUNCTION is checked
does not properly preserve zero for ranges that cross zero. When a range spans
from negative to positive values (like -100..100), the current logic taking
abs(result.min) loses the ability to select zero as a valid threshold. Modify
the logic in this block to check if the original range crosses zero by examining
whether result.min is negative and result.max is positive, then set result.min
to 0 for such ranges instead of taking its absolute value, ensuring that valid
thresholds like 0..99 remain selectable in the UI.
companion/src/firmwares/customisation_data.h-36-40 (1)

36-40: ⚠️ Potential issue | 🟠 Major

Update YAML serializer for NV14 to match new buffer size.

YAML serializers mostly align with the new sizes, but yaml_datastructs_nv14.cpp at line 790 defines widgetName with size 12 instead of 20. Update it to match WIDGET_NAME_LEN=20 to prevent widget names from being truncated when read by the firmware.

Mismatch found
yaml_datastructs_nv14.cpp:790: YAML_STRING("widgetName", 12),
// Should be:
YAML_STRING("widgetName", 20),

Other YAML files correctly specify the sizes for stringValue=12, widgetName=20, and options array with count 10.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/firmwares/customisation_data.h` around lines 36 - 40, The YAML
serializer in yaml_datastructs_nv14.cpp has a size mismatch for the widgetName
field. In yaml_datastructs_nv14.cpp, locate the YAML_STRING macro definition for
"widgetName" (currently specifying size 12) and update the size parameter from
12 to 20 to match the WIDGET_NAME_LEN constant defined in customisation_data.h.
This will ensure widget names are not truncated when read by the firmware.
companion/src/storage/storage.h-54-54 (1)

54-54: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

load(GeneralSettings&) is effectively non-functional for current concrete formats

Line 54 now defaults to return false;, but Storage::load(GeneralSettings&) dispatches to this virtual path. In this cohort, YamlFormat, LabelsStorageFormat (and thus EtxFormat / SdcardFormat) don’t override load(GeneralSettings&), so the call path always fails.

🔧 Proposed fix (safe fallback in base class)
-    virtual bool load(GeneralSettings & generalSettings) { return false; }
+    virtual bool load(GeneralSettings & generalSettings)
+    {
+      RadioData radioData;
+      if (!load(radioData))
+        return false;
+      generalSettings = radioData.generalSettings;
+      return true;
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/storage/storage.h` at line 54, The virtual method
`load(GeneralSettings&)` in the base Storage class currently returns false by
default, but concrete implementations like YamlFormat, LabelsStorageFormat,
EtxFormat, and SdcardFormat do not override this method, causing all calls to
fail. Either implement the `load(GeneralSettings&)` method in each concrete
format class to handle loading general settings appropriately for that format,
or provide a safe fallback implementation in the base Storage class that
properly handles the GeneralSettings parameter instead of unconditionally
returning false.
companion/src/storage/labeled.cpp-219-219 (1)

219-219: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Constrain the cleanup regex to intended filenames.

Line 219’s [0-9s]+ also matches names like model1s.yml and modelss.yml, so cleanup can delete files outside the intended modelNN.yml / legacy models.yml set.

Proposed fix
-  const std::regex yml_regex("MODELS/(model([0-9s]+)\\.yml)", std::regex_constants::icase);
+  const std::regex yml_regex("MODELS/((model[0-9]+|models)\\.yml)", std::regex_constants::icase);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/storage/labeled.cpp` at line 219, The regex pattern in the
yml_regex declaration at line 219 uses the character class [0-9s]+, which
incorrectly matches both digits and the letter 's' in any combination, allowing
filenames like model1s.yml and modelss.yml to match unintentionally. Fix the
regex to constrain it to only match the intended filenames: numbered models like
modelNN.yml (where NN contains only digits) and the legacy models.yml form.
Replace the overly permissive [0-9s]+ pattern with a more specific pattern that
matches either a sequence of digits only, or uses alternation to separately
handle the models.yml case, ensuring the cleanup operation only targets the
correct set of files.
companion/src/storage/storage.cpp-142-149 (1)

142-149: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a GeneralSettings load override for labeled formats.

Line 143 dispatches to StorageFormat::load(GeneralSettings&), but the provided format declarations only show the base overload returning false. Add an override that delegates to loadRadioSettings(...), otherwise Storage::load(generalSettings) cannot succeed for ETX/SD-card storage.

Proposed fix
--- a/companion/src/storage/labeled.h
+++ b/companion/src/storage/labeled.h
@@
-    virtual bool load(RadioData & radioData);
+    virtual bool load(RadioData & radioData);
+    virtual bool load(GeneralSettings & generalSettings) override;
     virtual bool write(RadioData & radioData);
--- a/companion/src/storage/labeled.cpp
+++ b/companion/src/storage/labeled.cpp
@@
 bool LabelsStorageFormat::write(RadioData & radioData)
 {
@@
 }
 
+bool LabelsStorageFormat::load(GeneralSettings & generalSettings)
+{
+  return loadRadioSettings(generalSettings);
+}
+
 bool LabelsStorageFormat::loadRadioSettings(GeneralSettings & generalSettings)
 {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/storage/storage.cpp` around lines 142 - 149, The StorageFormat
class lacks an override for the load method that accepts GeneralSettings as a
parameter, causing load operations to fail for labeled formats like ETX/SD-card
storage. Add a load(GeneralSettings&) method override to the StorageFormat class
(or the relevant derived format classes) that properly delegates to the
loadRadioSettings(...) method instead of relying on the base implementation that
returns false. This will allow the load call at line 143 to succeed when
format->load(generalSettings) is invoked.
companion/src/storage/labeled.cpp-187-193 (1)

187-193: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Stage calibration preservation outside the caller object.

Line 187 mutates radioData before later writeFile, deleteFile, and model writes can fail. If the write returns false, the open document can still retain target-radio calibration; serialize a local copy or restore on failure.

Proposed fix
 bool LabelsStorageFormat::write(RadioData & radioData)
 {
+  RadioData dataToWrite = radioData;
+
   // TODO
@@
     if (loadRadioSettings(gsCur)) {
-      GeneralSettings & gsNew = radioData.generalSettings;
+      GeneralSettings & gsNew = dataToWrite.generalSettings;
       gsNew.txCurrentCalibration = gsCur.txCurrentCalibration;
       gsNew.txVoltageCalibration = gsCur.txVoltageCalibration;
@@
   QByteArray radioSettingsBuffer;
-  if (!writeRadioSettingsToYaml(radioData.generalSettings, radioSettingsBuffer))
+  if (!writeRadioSettingsToYaml(dataToWrite.generalSettings, radioSettingsBuffer))
     return false;
@@
-  for (const auto& model : radioData.models) {
+  for (const auto& model : dataToWrite.models) {
@@
   if (hasLabels) {
     QByteArray labelsListBuffer;
-    if (!writeLabelsListToYaml(radioData, labelsListBuffer) || !writeFile(labelsListBuffer, "MODELS/labels.yml"))
+    if (!writeLabelsListToYaml(dataToWrite, labelsListBuffer) || !writeFile(labelsListBuffer, "MODELS/labels.yml"))
       return false;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/storage/labeled.cpp` around lines 187 - 193, The code mutates
radioData.generalSettings through the gsNew reference by copying calibration
values (txCurrentCalibration, txVoltageCalibration, and inputConfig calib
values) before the subsequent write operations that could fail. If those writes
return false, the document will still contain the unwanted calibration
modifications. Either create a local copy of the calibration values before
mutation and restore them if any subsequent write operation fails, or defer the
mutation of radioData until after all write and model operations have succeeded
and confirmed their success.
companion/src/simulator.cpp-235-244 (1)

235-244: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve explicit --flags 0 instead of treating it as standalone.

The non-Apple launcher passes --flags 0 for normal Companion launches, but line 379 maps zero to SIMULATOR_FLAGS_STANDALONE. Default to standalone only when --flags is absent, then pass the parsed value through unchanged.

Proposed fix
-  int flags = 0;
-
+  simOptions->flags = SIMULATOR_FLAGS_STANDALONE;
   if (cliOptions.isSet(optFlags)) {
     bool chk;
-    flags = cliOptions.value(optFlags).toInt(&chk);
-    if (!chk)
-      flags = 0;
+    int flags = cliOptions.value(optFlags).toInt(&chk);
+    if (!chk) {
+      showHelp(cliOptions, QApplication::translate("SimulatorMain", "Invalid flags value: %1").arg(cliOptions.value(optFlags)));
+      return CommandLineExitErr;
+    }
+    simOptions->flags = flags;
+    cliOptsFound = true;
   }
-
-  simOptions->flags = flags;
-  SimulatorMainWindow * mainWindow = new SimulatorMainWindow(nullptr, simOptions.simulatorId, (simOptions.flags ? simOptions.flags : SIMULATOR_FLAGS_STANDALONE));
+  SimulatorMainWindow * mainWindow = new SimulatorMainWindow(nullptr, simOptions.simulatorId, simOptions.flags);

Also applies to: 379-379

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/simulator.cpp` around lines 235 - 244, The current code treats
an explicitly provided `--flags 0` the same as when the `--flags` option is not
provided at all, but you need to preserve the explicit zero value. Instead of
always defaulting flags to zero and letting the downstream logic at line 379 map
zero to SIMULATOR_FLAGS_STANDALONE, you should only apply the standalone default
when `--flags` is completely absent (when cliOptions.isSet(optFlags) is false).
When the flag is explicitly set, pass the parsed integer value through unchanged
to simOptions->flags, even if that value is zero.
companion/src/simulation/simulator.h-135-135 (1)

135-135: ⚠️ Potential issue | 🟠 Major

Fix serialization: read flags in v4 and initialize the field.

Line 158 incorrectly reads simulatorId again instead of flags. Additionally, flags is uninitialized and never deserialized, breaking stream alignment between operator<< and operator>>.

Fixes
-    quint8 flags;                     // Flags passed from Companion
+    quint8 flags = 0;                 // Flags passed from Companion
         if (o.m_version >= 3)
           in >> o.simulatorId;
         if (o.m_version >= 4)
-          in >> o.simulatorId;
+          in >> o.flags;
+        else
+          o.flags = 0;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/simulation/simulator.h` at line 135, The deserialization
operator>> is incorrectly reading simulatorId twice instead of reading the flags
field. Locate the operator>> method that deserializes the simulator data, find
where simulatorId is being read a second time (around line 158), and change that
line to read flags instead. This ensures the flags field is properly
deserialized and maintains correct stream alignment with the operator<<
serialization method.
🟡 Minor comments (10)
.github/workflows/release-drafter.yml-21-25 (1)

21-25: ⚠️ Potential issue | 🟡 Minor

Add persist-credentials: false to checkout for defense-in-depth security.

While this workflow doesn't critically depend on persisted credentials (no authenticated Git pushes, and GITHUB_TOKEN is explicitly managed), setting persist-credentials: false follows security best practices by avoiding unnecessary token storage in git config.

Suggested patch
      - name: Check out the repo
        uses: actions/checkout@v4
        with:
          ref: ${{ inputs.tag }}
          fetch-depth: 0
+         persist-credentials: false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-drafter.yml around lines 21 - 25, Add the
`persist-credentials: false` parameter to the `actions/checkout@v4` action
configuration in the release-drafter.yml workflow. This should be added as a new
line under the `with` section that already contains `ref` and `fetch-depth`,
following GitHub Actions security best practices to prevent unnecessary token
storage in git configuration.

Source: Linters/SAST tools

.github/ISSUE_TEMPLATE/bug-report.yml-89-89 (1)

89-89: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Include PL18U in the transmitter option label.

Line 89 omits PL18U even though this PR adds PL18U board support elsewhere, so reports for that radio will be misclassified.

Suggested fix
-        - Flysky PL18/PL18EV
+        - Flysky PL18/PL18EV/PL18U
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/ISSUE_TEMPLATE/bug-report.yml at line 89, The transmitter option
label "Flysky PL18/PL18EV" on line 89 is missing PL18U even though this PR adds
PL18U board support elsewhere. Update this option label to include PL18U
alongside PL18 and PL18EV so that users with the PL18U transmitter can
accurately select their device when filing bug reports.
companion/src/firmwares/rawsource.cpp-35-38 (1)

35-38: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep getValue consistent with the new conversion path.

Line 37 still ignores offset, so existing callers of getValue() will display different values from toDisplay() whenever a range has a non-zero offset.

Proposed fix
 float RawSourceRange::getValue(int value)
 {
-  return float(value) * step; // TODO + offset ??
+  return float(toDisplay(value));
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/firmwares/rawsource.cpp` around lines 35 - 38, The getValue
method in the RawSourceRange class is not applying the offset parameter in its
calculation, causing inconsistency with the toDisplay() method when ranges have
non-zero offsets. Modify the getValue method to include the offset in the
calculation along with the step multiplication, ensuring that both getValue and
toDisplay produce consistent results for the same input value.
companion/src/modeledit/logicalswitches.cpp-45-47 (1)

45-47: ⚠️ Potential issue | 🟡 Minor

Headers should match field visibility logic.

The Duration/Delay columns are shown in table headers unconditionally (lines 45–47), but their visibility in the UI is conditional based on switch function family (line 433 disables DELAY for EDGE family, lines 465–466 gate visibility via masks). Additionally, the print output gate at modelprinter.cpp:641 checks LogicalSwitchesExt (which always returns true in the only implementation), making that check redundant. Either make headers conditional to match field visibility, or verify that unconditional headers are the intended design.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/modeledit/logicalswitches.cpp` around lines 45 - 47, The
headerLabels construction unconditionally includes "Duration" and "Delay" column
headers, but the visibility of these columns is conditionally controlled
elsewhere based on switch function family (lines 433 disable DELAY for EDGE
family, lines 465-466 gate visibility via masks). Either make the headerLabels
construction conditional to match the same visibility logic that gates the
actual field visibility in the table, or verify that having these headers
unconditionally while columns are conditionally hidden is the intended design.
Review the visibility mask logic and function family checks to ensure the
headers align with what columns are actually shown to the user.
companion/src/storage/storage.cpp-91-96 (1)

91-96: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve backend write errors before deleting the format.

Storage::write and Storage::writeModel return false without copying format->error(), so detailed errors set by EtxFormat, SdcardFormat, or YamlFormat are lost.

Proposed fix
 bool Storage::write(RadioData & radioData)
 {
   bool ret = false;
   StorageFormat *format = getStorageFormat();

   if (format) {
     ret = format->write(radioData);
+    if (!ret)
+      setError(format->error());
     delete format;
   }

   return ret;
 }

 bool Storage::writeModel(const RadioData & radioData, const int modelIndex)
 {
   bool ret = false;
   StorageFormat *format = getStorageFormat();

   if (format) {
     ret = format->writeModel(radioData, modelIndex);
+    if (!ret)
+      setError(format->error());
     delete format;
   }

   return ret;
 }

Also applies to: 104-109

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/storage/storage.cpp` around lines 91 - 96, The issue is that
when `format->write(radioData)` completes and the format object is deleted, the
error details from `format->error()` are lost before they can be preserved. To
fix this in the Storage::write method (and similarly in Storage::writeModel),
capture the error message from format->error() before calling delete format,
store it in an appropriate member variable or error tracking mechanism, and then
proceed with the deletion and return statement. This ensures that error
information from EtxFormat, SdcardFormat, or YamlFormat implementations is
preserved and accessible to the caller.
companion/src/storage/labeled.cpp-196-199 (1)

196-199: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep the detailed RADIO/radio.yml error.

loadRadioSettings() already sets the path and YAML exception details; Line 197 replaces that with a generic string.

Proposed fix
     } else {
-      setError("Error reading current settings from radio");
       return false;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/storage/labeled.cpp` around lines 196 - 199, The setError call
on line 197 is replacing the detailed error information that was already set by
loadRadioSettings() with a generic string. Remove the setError call in the else
block (the one that sets "Error reading current settings from radio") to
preserve the detailed error context including the RADIO/radio.yml path and YAML
exception details that loadRadioSettings() has already captured.
companion/src/helpers.cpp-399-416 (1)

399-416: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clean up simuData and the QProcess on the non-Apple path.

The temporary-directory/storage error paths return before deleting simuData, and the heap-allocated QProcess is never deleted after execute().

Proposed cleanup
   else {
     QString resultMsg = QCoreApplication::translate("Companion", "Error creating temporary directory for models and settings.");
     QMessageBox::critical(NULL, QCoreApplication::translate("Companion", "Simulator Error"), resultMsg);
+    delete simuData;
     return;
   }
@@
   if (!storage.write(*simuData)) {
     QString resultMsg = QCoreApplication::translate("Companion", "Error writing models and settings to temporary directory.");
     QMessageBox::critical(NULL, QCoreApplication::translate("Companion", "Simulator Error"), resultMsg);
+    delete simuData;
     return;
   }
@@
   if (result != 0)
     QMessageBox::critical(NULL, QCoreApplication::translate("Companion", "Simulator Error"), resultMsg);
 
-  if (simuData)
-    delete simuData;
+  delete simu;
+  delete simuData;

Also applies to: 434-454

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/helpers.cpp` around lines 399 - 416, The code has memory leaks
in the error handling paths. In the tmpDir.isValid() check failure and the
storage.write() failure sections, the function returns without deleting the
heap-allocated simuData object. Additionally, the QProcess object created and
executed in the code section around lines 434-454 is never deleted after
execute() completes. Add cleanup code to delete simuData before each error
return statement in these validation and write error paths, and delete the
QProcess object after the execute() call completes to prevent memory leaks.
companion/src/generaledit/generalsetup.ui-1475-1481 (1)

1475-1481: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace stale copy-pasted help text.

The switch delay help text describes LCD contrast, and the beeper mode help still describes numeric volume levels that no longer match the combo items.

Proposed text cleanup
-        <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
-p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;LCD Screen Contrast&lt;/span&gt;&lt;/p&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;Values can be 20-45&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+        <string>Delay before playing the switch mid-position sound, in milliseconds.</string>
-        <string>Beeper volume
-
-0 - Quiet.  No beeps at all.
-1 - No Keys.  Normal beeps but menu keys do not beep.
-2 - Normal.
-3 - Loud.
-4 - Extra loud.</string>
+        <string>Beeper mode.
+
+Quiet - No beeps.
+Alarms Only - Alarm beeps only.
+No Keys - Normal beeps, but menu keys do not beep.
+All - All beeps enabled.</string>

Also applies to: 2488-2495

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/generaledit/generalsetup.ui` around lines 1475 - 1481, The
whatsThis property contains stale help text describing LCD Screen Contrast with
values 20-45, but this text is actually for a switch delay control and does not
match what the control does. Replace this help text with appropriate description
of switch delay functionality. Additionally, fix similar stale help text in the
beeper mode section (around lines 2488-2495) where the help describes numeric
volume levels that no longer match the actual combo box items available. Replace
these whatsThis properties with accurate descriptions that correspond to what
each control actually controls.
companion/src/generaledit/generalsetup.ui-2540-2544 (1)

2540-2544: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove the stray > from the new label.

This will render as >One log per day in the settings panel.

Proposed fix
-        <string>&gt;One log per day</string>
+        <string>One log per day</string>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/generaledit/generalsetup.ui` around lines 2540 - 2544, The
label text property in the QLabel widget named label_3 has a stray `>` character
at the beginning of the string. Remove this character from the text property so
it reads "One log per day" instead of ">One log per day".
companion/src/helpers.cpp-447-448 (1)

447-448: ⚠️ Potential issue | 🟡 Minor

Use QString::number(result) to format the exit code as decimal text.

QString(result) passes the integer to the constructor as a character code, not a numeric value. The string concatenation at line 448 requires QString::number() to properly convert the exit code to its decimal representation.

Proposed fix
-    resultMsg = QCoreApplication::translate("Companion", "Exited with result code:") % QString(result);
+    resultMsg = QCoreApplication::translate("Companion", "Exited with result code:") % QString::number(result);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/helpers.cpp` around lines 447 - 448, In the else if block where
the resultMsg variable is being assigned, replace the QString(result)
constructor call with QString::number(result) to properly convert the integer
exit code to its decimal text representation. The QString constructor with an
integer argument treats the value as a character code rather than a numeric
value that needs to be formatted as decimal text.
🧹 Nitpick comments (2)
cmake/FetchYamlCpp.cmake (1)

8-8: Add yaml-cpp version reference to the pinned commit.

Line 8 pins a raw commit SHA, which is reproducible but difficult to audit. Add an inline comment documenting the release version or PR reference (e.g., # yaml-cpp 0.9.0).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmake/FetchYamlCpp.cmake` at line 8, The GIT_TAG line pinning the yaml-cpp
commit is not documented with version information, making it difficult to audit.
Add an inline comment to the GIT_TAG variable that documents the corresponding
release version or PR reference (for example, indicating this is yaml-cpp 0.9.0
or a specific PR) to improve code maintainability and auditability while
preserving the reproducible commit SHA.
companion/src/modelprinter.cpp (1)

571-571: ⚡ Quick win

Use RawSourceRange::toDisplay() for printer formatting.

Line 571 duplicates the new conversion formula and keeps a stale TODO; using the helper keeps UI and printed output on the same path.

Proposed refactor
-      result += QString::number((double)(range.step * ls.val2 /*TODO+ source.getRawOffset(model)*/) + range.offset, 'f', range.decimals);
+      result += QString::number(range.toDisplay(ls.val2), 'f', range.decimals);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/modelprinter.cpp` at line 571, The code on line 571 manually
duplicates the conversion formula for formatting range values instead of using
the helper method RawSourceRange::toDisplay(). Replace the manual calculation
(which multiplies range.step by ls.val2, adds range.offset, and formats with
decimals) with a call to the RawSourceRange::toDisplay() helper method to keep
the printer formatting consistent with the UI output path and remove the stale
TODO comment about source.getRawOffset().

Comment on lines +29 to +30
TAG="${{ inputs.tag }}"
if gh release view "$TAG" --json isDraft --jq '.isDraft' 2>/dev/null | grep -q true; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/release-drafter.yml | head -150

Repository: EdgeTX/edgetx

Length of output: 6573


Pass workflow inputs via env and validate tag format before use.

Direct interpolation of ${{ inputs.tag }} into shell scripts creates injection risk. Use environment variables and validate the tag format with a case statement pattern.

Suggested patch (pattern)
-      - name: Clean up any previous drafts for this version
+      - name: Clean up any previous drafts for this version
+        env:
+          INPUT_TAG: ${{ inputs.tag }}
         run: |
-          TAG="${{ inputs.tag }}"
+          TAG="$INPUT_TAG"
+          case "$TAG" in
+            v[0-9]*.[0-9]*.[0-9]*|v[0-9]*.[0-9]*.[0-9]*-rc*|v[0-9]*.[0-9]*.[0-9]*-beta*|v[0-9]*.[0-9]*.[0-9]*-alpha*) ;;
+            *) echo "::error::Invalid tag format: $TAG"; exit 1 ;;
+          esac
           if gh release view "$TAG" --json isDraft --jq '.isDraft' 2>/dev/null | grep -q true; then
             echo "Deleting existing draft release for tag: $TAG"
             gh release delete "$TAG" --yes
           fi

Apply to lines 29, 41, 76, 80, 81, and 110 (line 84 and 111–112 use previously set variables).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
TAG="${{ inputs.tag }}"
if gh release view "$TAG" --json isDraft --jq '.isDraft' 2>/dev/null | grep -q true; then
env:
INPUT_TAG: ${{ inputs.tag }}
run: |
TAG="$INPUT_TAG"
case "$TAG" in
v[0-9]*.[0-9]*.[0-9]*|v[0-9]*.[0-9]*.[0-9]*-rc*|v[0-9]*.[0-9]*.[0-9]*-beta*|v[0-9]*.[0-9]*.[0-9]*-alpha*) ;;
*) echo "::error::Invalid tag format: $TAG"; exit 1 ;;
esac
if gh release view "$TAG" --json isDraft --jq '.isDraft' 2>/dev/null | grep -q true; then
echo "Deleting existing draft release for tag: $TAG"
gh release delete "$TAG" --yes
fi
🧰 Tools
🪛 zizmor (1.25.2)

[error] 29-29: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release-drafter.yml around lines 29 - 30, The workflow
uses inputs.tag directly in shell commands without validation, creating a
potential injection risk. After setting the TAG environment variable on line 29,
add validation before using it in any shell command by implementing a case
statement pattern to verify the tag matches expected format. Apply this same
validation pattern to all other locations where the tag input is referenced
(lines 41, 76, 80, 81, and 110), ensuring the tag is sanitized and validated
before being passed to the gh release commands and other shell operations.

Source: Linters/SAST tools

Comment on lines +1461 to +1471
ExpoData sortedExpoData[CPN_MAX_EXPOS];
int destidx = 0;

QMap<int, int>::const_iterator i;
for (i = map.constBegin(); i != map.constEnd(); ++i) {
memcpy(&sortedExpoData[destidx], &expoData[i.value()], sizeof(ExpoData));
destidx++;
}

memcpy(&expoData[0], &sortedExpoData[0], CPN_MAX_EXPOS * sizeof(ExpoData));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Initialize sortedExpoData before full-buffer copy.

Line 1461 allocates sortedExpoData without initialization, and Line 1470 copies the entire array back. When not all slots are populated, uninitialized stack data is written into expoData, which can corrupt model entries.

🛠️ Suggested fix
-  ExpoData sortedExpoData[CPN_MAX_EXPOS];
+  ExpoData sortedExpoData[CPN_MAX_EXPOS] = {};
   int destidx = 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ExpoData sortedExpoData[CPN_MAX_EXPOS];
int destidx = 0;
QMap<int, int>::const_iterator i;
for (i = map.constBegin(); i != map.constEnd(); ++i) {
memcpy(&sortedExpoData[destidx], &expoData[i.value()], sizeof(ExpoData));
destidx++;
}
memcpy(&expoData[0], &sortedExpoData[0], CPN_MAX_EXPOS * sizeof(ExpoData));
}
ExpoData sortedExpoData[CPN_MAX_EXPOS] = {};
int destidx = 0;
QMap<int, int>::const_iterator i;
for (i = map.constBegin(); i != map.constEnd(); ++i) {
memcpy(&sortedExpoData[destidx], &expoData[i.value()], sizeof(ExpoData));
destidx++;
}
memcpy(&expoData[0], &sortedExpoData[0], CPN_MAX_EXPOS * sizeof(ExpoData));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@companion/src/firmwares/modeldata.cpp` around lines 1461 - 1471, The
sortedExpoData array is declared but not initialized, and when the loop only
partially fills it (copying from expoData using memcpy), the remaining
uninitialized stack memory persists and gets copied back into expoData on the
final memcpy call, corrupting model entries. Initialize the entire
sortedExpoData array immediately after declaration using memset to zero out all
CPN_MAX_EXPOS slots before the QMap iteration loop begins populating individual
entries.

Comment thread radio/src/audio.h
Comment on lines +93 to +96
#elif defined(AUDIO_SPI)
#define AUDIO_BUFFER_COUNT (2) // smaller than Taranis since there is also a buffer on the ADC chip
#elif defined(PCBPL18)
#define AUDIO_BUFFER_COUNT (10) // PL18 need more buffer for smooth audio
#elif defined(STORAGE_USE_SPI_FLASH)
#define AUDIO_BUFFER_COUNT (10) // SPI Flash need more buffer for smooth audio

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== audio.h selection block =="
nl -ba radio/src/audio.h | sed -n '84,112p'

echo
echo "== where AUDIO_SPI / STORAGE_USE_SPI_FLASH are defined =="
rg -n -C2 '\b(AUDIO_SPI|STORAGE_USE_SPI_FLASH)\b' --iglob '*CMakeLists.txt' --iglob '*.cmake' --iglob '*.h'

echo
echo "== board compile-definition context for new FlySky targets =="
rg -n -C3 'PL18U|PL18EV|PL18|ST16|PA01|target_compile_definitions|add_definitions' --iglob '*CMakeLists.txt'

Repository: EdgeTX/edgetx

Length of output: 148


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== audio.h selection block (lines 84-112) =="
cat -n radio/src/audio.h | sed -n '84,112p'

echo
echo "== where AUDIO_SPI / STORAGE_USE_SPI_FLASH are defined =="
rg -n '\b(AUDIO_SPI|STORAGE_USE_SPI_FLASH)\b' -C2 radio/src/

echo
echo "== CMake board definitions for FlySky targets and SPI settings =="
rg -n 'PL18U|PL18EV|PL18|ST16|PA01|AUDIO_SPI|STORAGE_USE_SPI_FLASH' -C2 --iglob '*CMakeLists.txt' --iglob '*.cmake'

Repository: EdgeTX/edgetx

Length of output: 16360


Macro precedence bug: SPI-flash targets incorrectly assigned smaller audio buffer.

The #elif chain evaluates conditions in order. PL18 and ST16 targets define both AUDIO_SPI and STORAGE_USE_SPI_FLASH in their hal.h files. Line 93's AUDIO_SPI condition matches first, assigning AUDIO_BUFFER_COUNT = 2, bypassing line 95's buffer-10 assignment intended for SPI-flash storage. This causes SPI-flash boards to use an undersized buffer despite the explicit comment that "SPI Flash need more buffer for smooth audio."

Reorder the conditions so the more specific SPI-flash requirement takes precedence, or use a combined condition that accounts for boards defining both.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@radio/src/audio.h` around lines 93 - 96, The preprocessor conditions for
AUDIO_BUFFER_COUNT have a precedence issue where targets defining both AUDIO_SPI
and STORAGE_USE_SPI_FLASH match the AUDIO_SPI condition first (line 93) with
value 2, bypassing the intended STORAGE_USE_SPI_FLASH condition (line 95) with
value 10. Reorder these conditions so the STORAGE_USE_SPI_FLASH check comes
before the AUDIO_SPI check, since SPI-flash storage is the more specific
requirement that needs the larger buffer. This ensures boards like PL18 and ST16
that define both macros will correctly receive the buffer-10 assignment.

@philmoz

philmoz commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

This PR looks to have been created against the wrong branch - main instead of 2.11.

The branch in GH is based on 2.11 so the PR should be as well.

@richardclli

richardclli commented Jun 19, 2026

Copy link
Copy Markdown
Member Author

Yeah, it is just a placeholder. I need sometime to check if this PR works or not. This is why the status is Draft.

@richardclli richardclli changed the base branch from main to 2.11 June 22, 2026 07:58
@richardclli richardclli changed the title fix(pl18): Sound Playing will Sometimes Sluggish fix(pl18): Sound Playing will Sometimes Sluggish - v2.11 Jun 22, 2026
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.

2 participants