diff --git a/.github/actions/build_companion/action.yml b/.github/actions/build_companion/action.yml new file mode 100644 index 000000000..101e92a59 --- /dev/null +++ b/.github/actions/build_companion/action.yml @@ -0,0 +1,64 @@ +name: 'Build Companion' +inputs: + os: + description: 'OS / Architecture' + required: true + qt-version: + description: 'Qt version' + default: '6.9.3' + qt-arch: + description: 'Qt architecture' + required: runner.os != 'Linux' + sdl2-version: + description: 'SDL2 version' + default: '2.32.8' + +runs: + using: "composite" + steps: + + - name: Install Qt + if: runner.os != 'Linux' + uses: jdpurcell/install-qt-action@v5 + env: + AQT_CONFIG: ${{ github.workspace }}/tools/aqt-settings.ini + with: + version: ${{ inputs.qt-version }} + modules: 'qtmultimedia qtserialport' + arch: ${{ inputs.qt-arch }} + cache: true + cache-key-prefix: 'install-qt-action-${{ inputs.qt-arch }}' + + - name: Setup SDL2 + if: runner.os != 'Linux' + shell: bash + env: + ACTION_PATH: ${{ github.action_path }} + SDL2_VERSION: ${{ inputs.sdl2-version }} + run: | + "${ACTION_PATH}/setup-sdl2.sh" ${SDL2_VERSION} + + - name: Setup MSVC + if: runner.os == 'Windows' + shell: pwsh + run: ${{ github.action_path }}/setup-msvc.ps1 + + - name: Build + shell: bash + env: + CMAKE_BUILD_TYPE: 'Release' + run: | + mkdir output && \ + CMAKE_PREFIX_PATH="$QT_ROOT_DIR;$SDL2_ROOT" \ + tools/build-companion.sh "$(pwd)" "$(pwd)/output/" + + - name: Compose release filename + run: echo "artifact_name=edgetx-cpn-${{ inputs.os }}-${GITHUB_REF##*/}" >> $GITHUB_ENV + shell: bash + + - name: Archive production artifacts + uses: actions/upload-artifact@v7 + with: + name: "${{ env.artifact_name }}" + path: ${{github.workspace}}/output + retention-days: 15 diff --git a/.github/actions/build_companion/setup-msvc.ps1 b/.github/actions/build_companion/setup-msvc.ps1 new file mode 100644 index 000000000..29d5d6aa6 --- /dev/null +++ b/.github/actions/build_companion/setup-msvc.ps1 @@ -0,0 +1,39 @@ +# setup-msvc.ps1 +# Runs vcvarsall.bat and exports only the new/changed environment +# variables to GitHub Actions, avoiding clobbering existing ones. + +param( + [string]$Arch = "x64" +) + +# Find VS installation +$vsWhere = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" +$vsPath = & $vsWhere -latest -property installationPath +$vcvars = Join-Path $vsPath "VC\Auxiliary\Build\vcvarsall.bat" + +if (-not (Test-Path $vcvars)) { + Write-Error "vcvarsall.bat not found at: $vcvars" + exit 1 +} + +# Snapshot environment before +$before = @{} +Get-ChildItem env: | ForEach-Object { $before[$_.Name] = $_.Value } + +# Run vcvarsall.bat and import its environment into this process +cmd /c "`"$vcvars`" $Arch && set" | ForEach-Object { + if ($_ -match '^([^=]+)=(.*)$') { + [System.Environment]::SetEnvironmentVariable($Matches[1], $Matches[2]) + } +} + +# Export only changed/new variables to GitHub Actions +$count = 0 +Get-ChildItem env: | ForEach-Object { + if ($before[$_.Name] -ne $_.Value) { + "$($_.Name)=$($_.Value)" | Out-File -Append -Encoding utf8 $env:GITHUB_ENV + $count++ + } +} + +Write-Host "Exported $count environment variables to GITHUB_ENV" diff --git a/.github/actions/build_companion/setup-sdl2.sh b/.github/actions/build_companion/setup-sdl2.sh new file mode 100755 index 000000000..dc317846e --- /dev/null +++ b/.github/actions/build_companion/setup-sdl2.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# setup-sdl2.sh +# Downloads/installs SDL2 for Windows and macOS. +# Linux is handled separately via a Docker container. + +set -euo pipefail + +VERSION="${1:?Usage: setup-sdl2.sh }" + +case "$RUNNER_OS" in + Windows) + ARCHIVE="SDL2-devel-${VERSION}-VC.zip" + URL="https://github.com/libsdl-org/SDL/releases/download/release-${VERSION}/${ARCHIVE}" + DEST="${RUNNER_TEMP}/SDL2" + + echo "Downloading SDL2 ${VERSION} (Windows/MSVC)..." + curl -fsSL -o "${RUNNER_TEMP}/${ARCHIVE}" "${URL}" + unzip -q "${RUNNER_TEMP}/${ARCHIVE}" -d "${DEST}" + + SDL2_ROOT="${DEST}/SDL2-${VERSION}" + echo "SDL2_ROOT=${SDL2_ROOT}" >> "${GITHUB_ENV}" + ;; + + macOS) + ARCHIVE="SDL2-${VERSION}.dmg" + URL="https://github.com/libsdl-org/SDL/releases/download/release-${VERSION}/${ARCHIVE}" + + echo "Downloading SDL2 ${VERSION} (macOS framework)..." + curl -fsSL -o "${RUNNER_TEMP}/${ARCHIVE}" "${URL}" + + hdiutil attach "${RUNNER_TEMP}/${ARCHIVE}" -quiet -mountpoint "${RUNNER_TEMP}/sdl2-mnt" + sudo cp -R "${RUNNER_TEMP}/sdl2-mnt/SDL2.framework" /Library/Frameworks/ + hdiutil detach "${RUNNER_TEMP}/sdl2-mnt" -quiet + ;; + + Linux) + echo "Skipping — SDL2 is provided by the Docker container" + ;; + + *) + echo "::error::Unsupported RUNNER_OS: ${RUNNER_OS}" + exit 1 + ;; +esac + +echo "SDL2 ${VERSION} setup complete for ${RUNNER_OS}" diff --git a/.github/actions/python_dependencies/action.yml b/.github/actions/python_dependencies/action.yml new file mode 100644 index 000000000..f1915b651 --- /dev/null +++ b/.github/actions/python_dependencies/action.yml @@ -0,0 +1,17 @@ +name: 'Setup WASI SDK' +inputs: + python-version: + description: 'Python version' + default: '3.13' + +runs: + using: "composite" + steps: + + - uses: actions/setup-python@v6 + with: + python-version: ${{ inputs.python-version }} + cache: 'pip' # caching pip dependencies + + - run: python3 -m pip install -r requirements.txt + shell: bash diff --git a/.github/actions/wasi_sdk/action.yml b/.github/actions/wasi_sdk/action.yml new file mode 100644 index 000000000..c3d7da30e --- /dev/null +++ b/.github/actions/wasi_sdk/action.yml @@ -0,0 +1,20 @@ +name: 'Setup WASI SDK' +inputs: + arch: + description: 'OS / Architecture (ex: arm64-macos)' + default: 'arm64-macos' + version: + description: 'SDK version' + default: '25' + +runs: + using: "composite" + steps: + + - name: Install WASI SDK + if: steps.cache-wasi-sdk.outputs.cache-hit != 'true' + shell: bash + run: | + mkdir /tmp/wasi-sdk + curl -L https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${{ inputs.version }}/wasi-sdk-${{ inputs.version }}.0-${{ inputs.arch }}.tar.gz | tar xz --strip-components 1 -C /tmp/wasi-sdk + sudo mv /tmp/wasi-sdk /opt/wasi-sdk diff --git a/.github/workflows/build_fw.yml b/.github/workflows/build_fw.yml index ca9dc3b05..5bb985110 100644 --- a/.github/workflows/build_fw.yml +++ b/.github/workflows/build_fw.yml @@ -15,9 +15,6 @@ on: - '.gitmodules' - 'CMakeLists.txt' pull_request: - branches: - - 'main' - - '[0-9]+.[0-9]+' paths: *trigger-paths jobs: @@ -30,7 +27,7 @@ jobs: strategy: matrix: target: - - x9dp2019 + - x9dp2019;x9e - tx15 - tx16s - tx16smk3 @@ -38,20 +35,14 @@ jobs: - el18 - pl18 - pl18ev - - t12max + - t12max;t14 - t15 - t16 - t18 - - tlite #STM32F2 - - t20 - - t14 - - t20v2 - - tx12 #STM32F2 + - t20;t20v2 - x10 - x12s - - x9e - - mt12 - - gx12 + - mt12;gx12 - nb4p - st16 - t15pro @@ -65,7 +56,6 @@ jobs: uses: actions/checkout@v6 with: submodules: recursive - # fetch-depth: 0 # https://github.com/actions/checkout#Fetch-all-history-for-all-tags-and-branches - name: Test ${{ matrix.target }} env: @@ -86,28 +76,21 @@ jobs: matrix: target: - nv14;el18 - - pl18;pl18ev;pl18u - - pa01 - - t12max - - t15;t16;t18 - - zorro;pocket;mt12;commando8 + - pl18;pl18ev + - pl18u;pa01 + - t15;t16 + - t14;t18;mt12 + - zorro;pocket;commando8 - tprov2;tpros;bumblebee - - t20;t20v2;t14 - - tx12mk2;boxer;gx12 - - tx15 - - tx16s - - tx16smk3 - - f16 + - mt12;gx12;t20;t20v2 + - t12max;tx12mk2;boxer + - tx15;tx16s + - x12s;f16 - v12;v14;v16 - x10;x10express - - x12s - - x7access - - x9dp2019 - - x9e;x9e-hall - - mt12;gx12 - - nb4p - - st16 - - t15pro + - x7access;x9dp2019;x9e;x9e-hall + - nb4p;st16 + - tx16smk3;t14;t15pro container: image: ghcr.io/edgetx/edgetx-dev:latest volumes: @@ -144,7 +127,6 @@ jobs: runs-on: ubuntu-latest steps: - name: Compose release filename - # https://stackoverflow.com/questions/58033366/how-to-get-current-branch-within-github-actions run: echo "artifact_name=edgetx-firmware-${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Merge firmware artifact packages diff --git a/.github/workflows/companion.yml b/.github/workflows/companion.yml new file mode 100644 index 000000000..7a1f52bca --- /dev/null +++ b/.github/workflows/companion.yml @@ -0,0 +1,164 @@ +name: Companion + +on: + push: + branches: + - 'main' + tags: + - v* + paths: + - '.github/workflows/companion.yml' + - 'companion/**' + - 'tools/build-companion.sh' + + pull_request: + paths: + - '.github/workflows/companion.yml' + - 'companion/**' + - 'tools/build-companion.sh' + + workflow_dispatch: + +jobs: + modules: + name: WASM Modules (${{ matrix.group.id }}) + runs-on: macos-15 + strategy: + matrix: + group: + - id: color-1 + target: [nv14, el18, pl18, pl18ev] + - id: color-2 + target: [pl18u, t15, t16, t18] + - id: bw-1 + target: [zorro, pocket, mt12, commando8, tprov2, tpros, bumblebee, t14] + - id: bw-2 + target: [mt12, gx12, t20, t20v2, t12max, tx12mk2, boxer] + - id: color-3 + target: [tx15, tx16s, x12s, f16] + - id: mixed-1 + target: [v12, v14, v16, x10, x10express] + - id: bw-3 + target: [x7access, x9dp2019, x9e, x9e-hall] + - id: color-4 + target: [nb4p, st16, pa01] + - id: color-5 + target: [t15pro, tx16smk3] + + steps: + - name: Check out the repo + uses: actions/checkout@v5 + with: + submodules: true + + - name: Setup Python Dependencies + uses: ./.github/actions/python_dependencies + + - name: Setup WASI SDK + uses: ./.github/actions/wasi_sdk + + - name: Build modules + shell: bash + env: + FLAVOR: ${{ join(matrix.group.target, ';') }} + CMAKE_BUILD_TYPE: 'Release' + run: | + mkdir output && \ + tools/build-wasm-modules.sh "$(pwd)" "$(pwd)/output/" + + - name: Archive production artifacts + uses: actions/upload-artifact@v7 + with: + name: wasm-modules-${{ matrix.group.id }} + path: ${{github.workspace}}/output + if-no-files-found: error + retention-days: 15 + + build-linux: + name: Linux Companion + runs-on: ubuntu-latest + needs: modules + + container: + image: ghcr.io/edgetx/edgetx-dev:latest + volumes: + - ${{ github.workspace }}:/src + + steps: + - name: Check out the repo + uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Download WASM modules + uses: actions/download-artifact@v7 + with: + pattern: wasm-modules-* + path: wasm-modules + merge-multiple: true + + - name: Setup and Build + uses: ./.github/actions/build_companion + with: + os: 'linux' + + build-macos: + name: macOS Companion + runs-on: macos-15 + needs: modules + + env: + CMAKE_OSX_ARCHITECTURES: 'x86_64;arm64' + CMAKE_OSX_DEPLOYMENT_TARGET: '11.0' + + steps: + - name: Check out the repo + uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Download WASM modules + uses: actions/download-artifact@v7 + with: + pattern: wasm-modules-* + path: wasm-modules + merge-multiple: true + + - name: Setup and Build + uses: ./.github/actions/build_companion + with: + os: 'macos' + qt-arch: 'clang_64' + + build-win64: + name: Windows Companion + runs-on: windows-2022 + needs: modules + + env: + CMAKE_GENERATOR: 'Ninja' + CXX: 'clang++' + CC: 'clang' + + defaults: + run: + shell: bash + + steps: + - name: Check out the repo + uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Download WASM modules + uses: actions/download-artifact@v7 + with: + pattern: wasm-modules-* + path: wasm-modules + merge-multiple: true + + - name: Setup and Build + uses: ./.github/actions/build_companion + with: + os: 'win64' + qt-arch: 'win64_msvc2022_64' diff --git a/.github/workflows/linux_cpn.yml b/.github/workflows/linux_cpn.yml deleted file mode 100644 index 28aaae30f..000000000 --- a/.github/workflows/linux_cpn.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Linux Companion - -on: - push: - branches: - - 'main' - - '[0-9]+.[0-9]+' - tags: - - v* - paths: &trigger-paths - - '.github/workflows/linux_cpn.yml' - - 'companion/**' - - 'tools/build-companion.sh' - - pull_request: - branches: - - 'main' - - '[0-9]+.[0-9]+' - paths: *trigger-paths - - workflow_dispatch: - -env: - # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) - BUILD_TYPE: Release - -jobs: - build: - runs-on: ubuntu-latest - if: | - github.event_name != 'pull_request' || - !contains(github.event.pull_request.labels.*.name, 'ci: skip-cpn-linux') - container: - image: ghcr.io/edgetx/edgetx-dev:latest - volumes: - - ${{ github.workspace }}:/src - - steps: - - name: Check out the repo - uses: actions/checkout@v6 - with: - submodules: recursive - - - name: Build - working-directory: ${{github.workspace}} - shell: bash - env: - CMAKE_BUILD_PARALLEL_LEVEL: 4 - run: | - mkdir output && mkdir logs && \ - tools/build-companion.sh "$(pwd)" "$(pwd)/output/" 2>&1 | tee logs/build-main.log - - - name: Collect build logs - if: always() - shell: bash - run: | - # Create logs directory if it doesn't exist - mkdir -p logs - - # Copy all build logs from the build directory - if [ -d "build" ]; then - find build -name "*.log" -type f -exec cp {} logs/ \; 2>/dev/null || true - find build -name "CMakeOutput.log" -type f -exec cp {} logs/ \; 2>/dev/null || true - find build -name "CMakeError.log" -type f -exec cp {} logs/ \; 2>/dev/null || true - fi - - - name: Compose release filename - if: always() - # https://stackoverflow.com/questions/58033366/how-to-get-current-branch-within-github-actions - run: echo "artifact_name=edgetx-cpn-linux-${GITHUB_REF##*/}" >> $GITHUB_ENV - - - name: Archive production artifacts - if: success() - uses: actions/upload-artifact@v7 - with: - name: "${{ env.artifact_name }}" - path: ${{github.workspace}}/output - retention-days: 15 - - - name: Archive build logs - if: always() - uses: actions/upload-artifact@v7 - with: - name: "${{ env.artifact_name }}-logs" - path: ${{github.workspace}}/logs - retention-days: 30 diff --git a/.github/workflows/macosx_cpn.yml b/.github/workflows/macosx_cpn.yml deleted file mode 100644 index 9698faf0b..000000000 --- a/.github/workflows/macosx_cpn.yml +++ /dev/null @@ -1,114 +0,0 @@ -name: MacOSX Companion - -on: - push: - branches: - - 'main' - - '[0-9]+.[0-9]+' - tags: - - v* - paths: &trigger-paths - - '.github/workflows/macosx_cpn.yml' - - 'companion/**' - - 'tools/build-companion.sh' - - pull_request: - branches: - - 'main' - - '[0-9]+.[0-9]+' - paths: *trigger-paths - - workflow_dispatch: - -env: - # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) - BUILD_TYPE: Release - QT_VERSION: "6.9.3" - -jobs: - build: - runs-on: macos-15-intel - if: | - github.event_name != 'pull_request' || - !contains(github.event.pull_request.labels.*.name, 'ci: skip-cpn-macos') - steps: - - name: Select XCode version - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: '16.0' - - - name: Check out the repo - uses: actions/checkout@v6 - with: - submodules: recursive - - - name: Setup Python 3.12 - uses: actions/setup-python@v6 - with: - python-version: '3.12' - - - name: Create Build Environment - # Some projects don't allow in-source building, so create a separate build directory - # We'll use this as our working directory for all subsequent commands - run: cmake -E make_directory ${{github.workspace}}/build - - - name: Install Qt - uses: jdpurcell/install-qt-action@v5 - env: - AQT_CONFIG: ${{ github.workspace }}/tools/aqt-settings.ini - with: - version: ${{ env.QT_VERSION }} - modules: 'qtmultimedia qtserialport' - setup-python: 'false' - cache: true - cache-key-prefix: 'install-qt-action-macOS' - - - name: Install dependencies - run: python3 -m pip install --upgrade pip Pillow lz4 clang jinja2 pydantic - - - name: Install SDL2 - run: brew install SDL2 - - - name: Build - working-directory: ${{github.workspace}} - shell: bash - env: - CMAKE_BUILD_PARALLEL_LEVEL: 3 - run: | - mkdir output && mkdir logs && \ - tools/build-companion.sh "$(pwd)" "$(pwd)/output/" 2>&1 | tee logs/build-main.log - - - name: Collect build logs - if: always() - shell: bash - run: | - # Create logs directory if it doesn't exist - mkdir -p logs - - # Copy all build logs from the build directory - if [ -d "build" ]; then - find build -name "*.log" -type f -exec cp {} logs/ \; 2>/dev/null || true - find build -name "CMakeOutput.log" -type f -exec cp {} logs/ \; 2>/dev/null || true - find build -name "CMakeError.log" -type f -exec cp {} logs/ \; 2>/dev/null || true - fi - - - name: Compose release filename - if: always() - # https://stackoverflow.com/questions/58033366/how-to-get-current-branch-within-github-actions - run: echo "artifact_name=edgetx-cpn-osx-${GITHUB_REF##*/}" >> $GITHUB_ENV - - - name: Archive production artifacts - if: success() - uses: actions/upload-artifact@v7 - with: - name: "${{ env.artifact_name }}" - path: ${{github.workspace}}/output - retention-days: 15 - - - name: Archive build logs - if: always() - uses: actions/upload-artifact@v7 - with: - name: "${{ env.artifact_name }}-logs" - path: ${{github.workspace}}/logs - retention-days: 30 diff --git a/.github/workflows/win_cpn-64.yml b/.github/workflows/win_cpn-64.yml deleted file mode 100644 index 65e77a4a9..000000000 --- a/.github/workflows/win_cpn-64.yml +++ /dev/null @@ -1,116 +0,0 @@ -name: Windows Companion 64-bit - -on: - push: - branches: - - 'main' - - '[0-9]+.[0-9]+' - tags: - - v* - paths: &trigger-paths - - '.github/workflows/win_cpn-64.yml' - - 'companion/**' - - 'tools/build-companion.sh' - - pull_request: - branches: - - 'main' - - '[0-9]+.[0-9]+' - paths: *trigger-paths - - workflow_dispatch: - -env: - # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) - CMAKE_BUILD_TYPE: "Release" - CMAKE_GENERATOR: "Ninja" - QT_VERSION: "6.9.0" - QT_ARCH: "win64_msvc2022_64" - SDL_VERSION: "2.32.8" - CXX: "clang++" - CC: "clang" - -jobs: - build: - runs-on: windows-2022 - if: | - github.event_name != 'pull_request' || - !contains(github.event.pull_request.labels.*.name, 'ci: skip-cpn-win') - defaults: - run: - shell: bash - - steps: - - name: Check out the repo - uses: actions/checkout@v6 - with: - submodules: recursive - - - name: Install Qt - uses: jdpurcell/install-qt-action@v5 - env: - AQT_CONFIG: ${{ github.workspace }}/tools/aqt-settings.ini - with: - cache: true - cache-key-prefix: 'install-qt-action-${{ env.QT_ARCH }}' - version: ${{ env.QT_VERSION }} - arch: ${{ env.QT_ARCH }} - modules: 'qtmultimedia qtserialport' - - - name: Setup SDL2 - id: setup-sdl2 - uses: libsdl-org/setup-sdl@v1 - with: - version: ${{ env.SDL_VERSION }} - build-type: "Release" - - - name: Install Python Dependencies - run: | - echo $PATH - $Python3_ROOT_DIR/python3.exe -m pip install clang jinja2 lz4 pillow pydantic - - - name: Setup MSVC - uses: ilammy/msvc-dev-cmd@v1 - - - name: Build - env: - CMAKE_BUILD_PARALLEL_LEVEL: 4 - run: | - mkdir output && mkdir logs && \ - CMAKE_PREFIX_PATH="$QT_ROOT_DIR;$SDL2_ROOT" \ - tools/build-companion.sh "$(pwd)" "$(pwd)/output/" 2>&1 | tee logs/build-main.log - - - name: Collect build logs - if: always() - shell: bash - run: | - # Create logs directory if it doesn't exist - mkdir -p logs - - # Copy all build logs from the build directory - if [ -d "build" ]; then - find build -name "*.log" -type f -exec cp {} logs/ \; 2>/dev/null || true - find build -name "CMakeOutput.log" -type f -exec cp {} logs/ \; 2>/dev/null || true - find build -name "CMakeError.log" -type f -exec cp {} logs/ \; 2>/dev/null || true - fi - - - name: Compose release filename - if: always() - # https://stackoverflow.com/questions/58033366/how-to-get-current-branch-within-github-actions - run: echo "artifact_name=edgetx-cpn-win64-${GITHUB_REF##*/}" >> $GITHUB_ENV - - - name: Archive production artifacts - if: success() - uses: actions/upload-artifact@v7 - with: - name: "${{ env.artifact_name }}" - path: ${{github.workspace}}/output - retention-days: 15 - - - name: Archive build logs - if: always() - uses: actions/upload-artifact@v7 - with: - name: "${{ env.artifact_name }}-logs" - path: ${{github.workspace}}/logs - retention-days: 30 diff --git a/CMakeLists.txt b/CMakeLists.txt index 63f2e5cbf..621d85bda 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ -cmake_minimum_required(VERSION 3.13) +cmake_minimum_required(VERSION 3.21) project(EdgeTX) set(VERSION_MAJOR "3") @@ -8,6 +8,8 @@ set(VERSION_REVISION "0") set(CODENAME "dev") string(TIMESTAMP BUILD_YEAR "%Y") +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") + if(MINGW OR WIN32) set(CMAKE_OBJECT_PATH_MAX 200) endif() @@ -86,6 +88,28 @@ if(EdgeTX_SUPERBUILD) set(PARALLEL_FLAG --parallel) endif() + # Webasm targets + if(ENABLE_WASM) + include(FetchWasiSDK) + find_package(WasiSDK REQUIRED) + + ExternalProject_Add(wasm + SOURCE_DIR ${CMAKE_SOURCE_DIR} + BINARY_DIR ${CMAKE_BINARY_DIR}/wasm + CMAKE_ARGS + ${CMAKE_ARGS} + -Wno-dev + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + -DCMAKE_TOOLCHAIN_FILE=${WASI_SDK_PATH}/share/cmake/wasi-sdk-pthread.cmake + CMAKE_CACHE_ARGS + -DEdgeTX_SUPERBUILD:BOOL=0 + -DNATIVE_BUILD:BOOL=1 + -DDISABLE_COMPANION:BOOL=1 + INSTALL_COMMAND "" + EXCLUDE_FROM_ALL TRUE + ) + endif() + add_custom_target(configure DEPENDS native-configure arm-none-eabi-configure) @@ -103,6 +127,13 @@ if(EdgeTX_SUPERBUILD) COMMAND ${CMAKE_COMMAND} --build native --target simu ${PARALLEL_FLAG} ) + if(ENABLE_WASM) + add_custom_target(wasi-module + COMMAND ${CMAKE_COMMAND} --build wasm --target wasi-module --parallel + DEPENDS wasm-configure + ) + endif() + add_custom_target(companion COMMAND ${CMAKE_COMMAND} --build native --target companion ${PARALLEL_FLAG} DEPENDS native-configure @@ -137,8 +168,6 @@ if(EdgeTX_SUPERBUILD) return() endif() -enable_language(ASM) - set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/cmake) include(GenericDefinitions) @@ -148,11 +177,14 @@ else() message(STATUS "EdgeTX ${VERSION_PREFIX}${VERSION}${VERSION_SUFFIX} @ ${GIT_STR}") endif() -if(NATIVE_BUILD) +if(NATIVE_BUILD AND NOT WASI) include(NativeTargets) else() # Prevent CMake warnings set(IGNORE "${LIBUSB1_ROOT_DIR}" "${OPENSSL_ROOT_DIR}") endif() +# Prevent CMake warnings +set(IGNORE "${ENABLE_WASM}") + add_subdirectory(${RADIO_SRC_DIR}) diff --git a/CMakePresets.json b/CMakePresets.json index 9f44720a8..86ad11bb6 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -9,6 +9,22 @@ "cacheVariables": { "EdgeTX_SUPERBUILD": false, "NATIVE_BUILD": false + }, + "warnings": { + "dev": false + } + }, + { + "name": "companion", + "displayName": "Companion", + "binaryDir": "build/companion", + "toolchainFile": "cmake/toolchain/native.cmake", + "cacheVariables": { + "EdgeTX_SUPERBUILD": false, + "NATIVE_BUILD": true + }, + "warnings": { + "dev": false } }, { @@ -34,6 +50,14 @@ "firmware" ] }, + { + "name": "companion", + "displayName": "Companion", + "configurePreset": "companion", + "targets": [ + "companion" + ] + }, { "name": "simu", "displayName": "SDL Simu", @@ -51,4 +75,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/cmake/CopyWasmModules.cmake b/cmake/CopyWasmModules.cmake new file mode 100644 index 000000000..6f5c9e853 --- /dev/null +++ b/cmake/CopyWasmModules.cmake @@ -0,0 +1,5 @@ +# Copy all WASM simulator modules from SRC_DIR to DST_DIR at build time. +# Invoked via cmake -P so the pattern is evaluated on every build. +file(INSTALL "${SRC_DIR}/" DESTINATION "${DST_DIR}" + FILES_MATCHING PATTERN "edgetx-*-simulator.wasm" + MESSAGE_NEVER) diff --git a/cmake/FetchRsDfu.cmake b/cmake/FetchRsDfu.cmake index 5880a71f4..c6cce9fff 100644 --- a/cmake/FetchRsDfu.cmake +++ b/cmake/FetchRsDfu.cmake @@ -14,7 +14,9 @@ else() message(FATAL_ERROR "Unsupported platform: ${CMAKE_SYSTEM_NAME}") endif() -if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64") +if(APPLE) + set(RS_DFU_ARCH "universal") +elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64") set(RS_DFU_ARCH "x86_64") elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64") set(RS_DFU_ARCH "aarch64") diff --git a/cmake/FetchWamr.cmake b/cmake/FetchWamr.cmake new file mode 100644 index 000000000..abce2f89a --- /dev/null +++ b/cmake/FetchWamr.cmake @@ -0,0 +1,63 @@ +# Fetch and configure WAMR (WebAssembly Micro Runtime) +cmake_minimum_required(VERSION 3.14) + +include(FetchContent) + +# WAMR build configuration +set(WAMR_BUILD_INTERP 1) +set(WAMR_BUILD_FAST_INTERP 0) +set(WAMR_BUILD_AOT 0) +set(WAMR_BUILD_JIT 0) +set(WAMR_BUILD_LIBC_BUILTIN 1) +set(WAMR_BUILD_LIBC_WASI 1) +set(WAMR_BUILD_LIB_WASI_THREADS 1) +set(WAMR_BUILD_SHARED_MEMORY 1) +set(WAMR_BUILD_BULK_MEMORY 1) +set(WAMR_BUILD_REF_TYPES 1) +set(WAMR_BUILD_SIMD 0) +set(WAMR_BUILD_EXCE_HANDLING 1) +set(WAMR_DISABLE_HW_BOUND_CHECK 1) + +FetchContent_Declare( + wamr + GIT_REPOSITORY https://github.com/bytecodealliance/wasm-micro-runtime + GIT_TAG WAMR-2.4.4 + GIT_SHALLOW TRUE +) + +# Populate separately so we can patch before building +FetchContent_GetProperties(wamr) +if(NOT wamr_POPULATED) + FetchContent_Populate(wamr) + + # WAMR 2.4.4 win_file.c has trailing backslashes in comments that + # clang treats as line continuations, causing compile errors. + execute_process( + COMMAND git apply "${CMAKE_CURRENT_LIST_DIR}/wamr-fix-win-backslash-comments.patch" + WORKING_DIRECTORY "${wamr_SOURCE_DIR}" + RESULT_VARIABLE _patch_result + ) + if(_patch_result) + message(WARNING "WAMR win_file.c patch failed (may already be applied)") + endif() + + add_subdirectory("${wamr_SOURCE_DIR}" "${wamr_BINARY_DIR}" EXCLUDE_FROM_ALL) + + if(WIN32) + # WAMR unconditionally adds -lm and -ldl as PUBLIC link libraries, + # which don't exist on Windows. + get_target_property(_vmlib_libs vmlib INTERFACE_LINK_LIBRARIES) + if(_vmlib_libs) + list(REMOVE_ITEM _vmlib_libs "-lm" "-ldl") + set_target_properties(vmlib PROPERTIES INTERFACE_LINK_LIBRARIES "${_vmlib_libs}") + endif() + + # WAMR headers use __declspec(dllimport/dllexport) on MSVC-compatible + # compilers, but we link vmlib statically. Override both API macros to + # empty for vmlib itself (PUBLIC = PRIVATE + INTERFACE). + target_compile_definitions(vmlib PUBLIC + WASM_RUNTIME_API_EXTERN= + WASM_API_EXTERN= + ) + endif() +endif() diff --git a/cmake/FetchWasiSDK.cmake b/cmake/FetchWasiSDK.cmake new file mode 100644 index 000000000..5c54a7315 --- /dev/null +++ b/cmake/FetchWasiSDK.cmake @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.14) + +set(WASI_SDK_VERSION "25") +set(WASI_SDK_FULL_VERSION "${WASI_SDK_VERSION}.0") + +# Determine platform and architecture +if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux") + set(WASI_SDK_PLATFORM "linux") +elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin") + set(WASI_SDK_PLATFORM "macos") +elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") + set(WASI_SDK_PLATFORM "windows") +else() + message(FATAL_ERROR "Unsupported platform: ${CMAKE_HOST_SYSTEM_NAME}") +endif() + +if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64") + set(WASI_SDK_ARCH "x86_64") +elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "aarch64|arm64") + set(WASI_SDK_ARCH "arm64") +else() + message(FATAL_ERROR "Unsupported Linux architecture: ${CMAKE_HOST_SYSTEM_PROCESSOR}") +endif() + +set(WASI_SDK_BASE_URL "https://github.com/WebAssembly/wasi-sdk/releases/download") +set(WASI_SDK_FILENAME "wasi-sdk-${WASI_SDK_VERSION}.0-${WASI_SDK_ARCH}-${WASI_SDK_PLATFORM}") +set(WASI_SDK_URL "${WASI_SDK_BASE_URL}/wasi-sdk-${WASI_SDK_VERSION}/${WASI_SDK_FILENAME}.tar.gz") + +set(FETCHCONTENT_QUIET FALSE) + +include(FetchContent) +FetchContent_Declare( + WasiSDK + URL ${WASI_SDK_URL} + DOWNLOAD_EXTRACT_TIMESTAMP true + FIND_PACKAGE_ARGS +) + +FetchContent_MakeAvailable(WasiSDK) + +set(WASI_SDK_PATH ${wasisdk_SOURCE_DIR}) diff --git a/cmake/FindWasiSDK.cmake b/cmake/FindWasiSDK.cmake new file mode 100644 index 000000000..e7cebdeaf --- /dev/null +++ b/cmake/FindWasiSDK.cmake @@ -0,0 +1,47 @@ +# FindWasiSDK.cmake +# Module to find WASI SDK installation +# +# This module defines the following variables: +# WasiSDK_FOUND - True if WASI SDK is found +# WasiSDK_ROOT_DIR - Root directory of WASI SDK installation +# +# Usage: +# set(WASI_SDK_PATH "/path/to/wasi-sdk") +# find_package(WasiSDK REQUIRED) + +cmake_minimum_required(VERSION 3.10) + +# Use WASI_SDK_PATH if provided, otherwise try to find it +if(WASI_SDK_PATH) + set(_wasi_sdk_search_paths "${WASI_SDK_PATH}") +else() + # Default search paths for common WASI SDK installations + set(_wasi_sdk_search_paths + "$ENV{WASI_SDK_PATH}" + "/opt/wasi-sdk" + ) +endif() + +# Find the WASI SDK root directory by looking for the toolchain file +find_path(WasiSDK_ROOT_DIR + NAMES share/cmake/wasi-sdk.cmake + PATHS ${_wasi_sdk_search_paths} + DOC "WASI SDK root directory" + NO_DEFAULT_PATH +) + +# Handle find_package arguments +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(WasiSDK + DEFAULT_MSG + WasiSDK_ROOT_DIR +) + +# Mark variables as advanced +mark_as_advanced( + WasiSDK_ROOT_DIR +) + +if(WasiSDK_FOUND) + set(WASI_SDK_PATH ${WasiSDK_ROOT_DIR}) +endif() diff --git a/cmake/NativeTargets.cmake b/cmake/NativeTargets.cmake index c89c375d3..09800822c 100644 --- a/cmake/NativeTargets.cmake +++ b/cmake/NativeTargets.cmake @@ -39,24 +39,24 @@ include(FetchGtest) add_custom_target(tests-radio COMMAND ${CMAKE_CURRENT_BINARY_DIR}/gtests-radio DEPENDS gtests-radio - ) +) if(Qt6Core_FOUND AND NOT DISABLE_COMPANION) add_subdirectory(${COMPANION_SRC_DIRECTORY}) add_custom_target(tests-companion COMMAND ${CMAKE_CURRENT_BINARY_DIR}/gtests-companion DEPENDS gtests-companion - ) + ) add_custom_target(gtests DEPENDS gtests-radio gtests-companion - ) + ) add_custom_target(tests DEPENDS tests-radio tests-companion ) else() add_custom_target(gtests DEPENDS gtests-radio - ) + ) add_custom_target(tests DEPENDS tests-radio ) diff --git a/cmake/wamr-fix-win-backslash-comments.patch b/cmake/wamr-fix-win-backslash-comments.patch new file mode 100644 index 000000000..598601ba7 --- /dev/null +++ b/cmake/wamr-fix-win-backslash-comments.patch @@ -0,0 +1,29 @@ +diff --git a/core/shared/platform/windows/win_file.c b/core/shared/platform/windows/win_file.c +index 55ea77a..df67705 100644 +--- a/core/shared/platform/windows/win_file.c ++++ b/core/shared/platform/windows/win_file.c +@@ -1295,13 +1295,13 @@ os_readlinkat(os_file_handle handle, const char *path, char *buf, + + if (wbufsize >= 4 && wbuf[0] == L'\\' && wbuf[1] == L'?' + && wbuf[2] == L'?' && wbuf[3] == L'\\') { +- // Starts with \??\ ++ // Has \??\ prefix + if (wbufsize >= 6 + && ((wbuf[4] >= L'A' && wbuf[4] <= L'Z') + || (wbuf[4] >= L'a' && wbuf[4] <= L'z')) + && wbuf[5] == L':' && (wbufsize == 6 || wbuf[6] == L'\\')) + { +- // \??\:\ ++ // \??\:\ path + wbuf += 4; + wbufsize -= 4; + } +@@ -1310,7 +1310,7 @@ os_readlinkat(os_file_handle handle, const char *path, char *buf, + && (wbuf[6] == L'C' || wbuf[6] == L'c') + && wbuf[7] == L'\\') + { +- // \??\UNC\\\ - make sure the final path looks like \\\\ ++ // \??\UNC\\\ - make sure the final path looks like \\\\ path + wbuf += 6; + wbuf[0] = L'\\'; + wbufsize -= 6; diff --git a/companion/src/CMakeLists.txt b/companion/src/CMakeLists.txt index 9eb2d05d5..7225914a7 100644 --- a/companion/src/CMakeLists.txt +++ b/companion/src/CMakeLists.txt @@ -1,4 +1,5 @@ set(SIMULATOR_INSTALL_PREFIX "" CACHE STRING "Alternative simulator library search path") +set(SIMULATOR_WASM_DIR "" CACHE PATH "Directory containing .wasm simulator modules to bundle") set(COMPANION_NAME "companion") set(SIMULATOR_NAME "simulator") @@ -271,6 +272,19 @@ target_link_libraries(${COMPANION_NAME} PRIVATE PrintTargetReport("${COMPANION_NAME}") +# Copy WASM simulator modules next to executables for local dev +if(SIMULATOR_WASM_DIR) + foreach(_target ${COMPANION_NAME} ${SIMULATOR_NAME}) + add_custom_command(TARGET ${_target} POST_BUILD + COMMAND ${CMAKE_COMMAND} + -DSRC_DIR=${SIMULATOR_WASM_DIR} + -DDST_DIR=$ + -P ${CMAKE_SOURCE_DIR}/cmake/CopyWasmModules.cmake + COMMENT "Copying WASM simulator modules to ${_target} output" + ) + endforeach() +endif() + ############# Standalone simulator ############### set(simu_SRCS simulator.cpp ) @@ -399,8 +413,8 @@ elseif(WIN32) endif(OPENSSL_FOUND) if(TARGET SDL2::SDL2) - install(FILES $ - DESTINATION ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}) + install(IMPORTED_RUNTIME_ARTIFACTS SDL2::SDL2 + RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}) endif() include(FindMSVCRedist) @@ -414,23 +428,23 @@ elseif(APPLE) # Qt + Cmake + Mac is poorly documented. A lot of this is guesswork # and trial and error. Do not hesitate to fix it for the better set(COMPANION_DISPLAY_NAME "EdgeTX Companion") - set_target_properties(${COMPANION_NAME} PROPERTIES - MACOSX_RPATH TRUE - MACOSX_BUNDLE TRUE - MACOSX_BUNDLE_BUNDLE_NAME "${COMPANION_DISPLAY_NAME}" - MACOSX_BUNDLE_INFO_PLIST ${COMPANION_TARGETS_DIR}/MacOSXBundleInfo.plist.in - - INSTALL_RPATH "@executable_path/../Frameworks" - BUILD_WITH_INSTALL_RPATH TRUE - ) - # This the name that the user will see in the generated DMG and what the application # will be called under /Applications. We include the version string to make installing # different versions side-by-side set(COMPANION_OSX_APP_BUNDLE_NAME "${COMPANION_DISPLAY_NAME} ${VERSION_FAMILY}") set(MACOSX_BUNDLE_GUI_IDENTIFIER "org.edgetx.companion") - set(companion_app_dir "companion.app") + set_target_properties(${COMPANION_NAME} PROPERTIES + OUTPUT_NAME "${COMPANION_OSX_APP_BUNDLE_NAME}" + MACOSX_RPATH TRUE + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_BUNDLE_NAME "${COMPANION_DISPLAY_NAME}" + MACOSX_BUNDLE_INFO_PLIST ${COMPANION_TARGETS_DIR}/MacOSXBundleInfo.plist.in + INSTALL_RPATH "@executable_path/../Frameworks" + BUILD_WITH_INSTALL_RPATH TRUE + ) + + set(companion_app_dir "${COMPANION_OSX_APP_BUNDLE_NAME}.app") set(companion_res_dir "${companion_app_dir}/Contents/Resources") # Only Companion is distributed for macOS @@ -457,7 +471,7 @@ elseif(APPLE) ) install(FILES ${MAC_ICON_FILE} - DESTINATION ${companion_res_dir} + DESTINATION "${companion_res_dir}" COMPONENT Runtime) endif() @@ -545,7 +559,7 @@ elseif(APPLE) OUTPUT_SCRIPT qt_deploy_companion CONTENT " qt_deploy_runtime_dependencies( - EXECUTABLE ${companion_app_dir} + EXECUTABLE \"${companion_app_dir}\" NO_TRANSLATIONS NO_COMPILER_RUNTIME GENERATE_QT_CONF @@ -556,7 +570,15 @@ elseif(APPLE) # Run macdeployqt install(SCRIPT ${qt_deploy_companion} COMPONENT Runtime) - # Fix Code Signing and rename bundle + # Bundle SDL2 + if(TARGET SDL2::SDL2) + install(IMPORTED_RUNTIME_ARTIFACTS SDL2::SDL2 + FRAMEWORK DESTINATION "${companion_app_dir}/Contents/Frameworks" + LIBRARY DESTINATION "${companion_app_dir}/Contents/Frameworks" + COMPONENT Runtime) + endif() + + # Re-sign bundle after all contents are installed install(CODE " set(app_dir \"\${CMAKE_INSTALL_PREFIX}/${companion_app_dir}\") execute_process(COMMAND \${CMAKE_COMMAND} -E echo \"Re-signing: \${app_dir}\") @@ -564,7 +586,6 @@ elseif(APPLE) COMMAND codesign --remove-signature \${app_dir} COMMAND codesign --force --deep -s - \${app_dir} ) - file(RENAME \"\${app_dir}\" \"\${CMAKE_INSTALL_PREFIX}/${COMPANION_OSX_APP_BUNDLE_NAME}.app\") " COMPONENT Runtime) else() diff --git a/companion/src/firmwares/boardjson.cpp b/companion/src/firmwares/boardjson.cpp index 3f4d99d41..ea02fb2f9 100644 --- a/companion/src/firmwares/boardjson.cpp +++ b/companion/src/firmwares/boardjson.cpp @@ -31,6 +31,7 @@ static const StringTagMappingTable inputTypesLookupTable = { {std::to_string(Board::AIT_FLEX), "FLEX"}, {std::to_string(Board::AIT_VBAT), "VBAT"}, {std::to_string(Board::AIT_RTC_BAT), "RTC_BAT"}, + {std::to_string(Board::AIT_LUX), "LUX"}, {std::to_string(Board::AIT_SWITCH), "SWITCH"}, }; @@ -119,18 +120,13 @@ void BoardJson::afterLoadFixups(Board::Type board, InputsTable * inputs, Switche } } - if (IS_RADIOMASTER_TX16SMK3(board)) { - if (getInputIndex(inputs, "LIGHT", Board::LVT_TAG) < 0) { - InputDefn defn; - defn.type = AIT_FLEX; - defn.tag = "LIGHT"; - defn.name = "Ambient light"; - defn.shortName = "Light"; - defn.flexType = FLEX_POT; - defn.inverted = false; - defn.cfgYaml = Board::LVT_TAG; - defn.refYaml = Board::LVT_TAG; // non-default - inputs->insert(inputs->end(), defn); + // Set default labels for LUX inputs if not provided by JSON + for (auto &defn : *inputs) { + if (defn.type == AIT_LUX) { + if (defn.name.empty()) + defn.name = "Ambient light"; + if (defn.shortName.empty()) + defn.shortName = "Light"; } } diff --git a/companion/src/firmwares/boards.h b/companion/src/firmwares/boards.h index 07bbe0739..860ac9e08 100644 --- a/companion/src/firmwares/boards.h +++ b/companion/src/firmwares/boards.h @@ -307,6 +307,7 @@ namespace Board { AIT_FLEX, AIT_VBAT, AIT_RTC_BAT, + AIT_LUX, AIT_SWITCH, }; diff --git a/companion/src/firmwares/generalsettings.cpp b/companion/src/firmwares/generalsettings.cpp index 7a8341b5a..9fb5634f9 100644 --- a/companion/src/firmwares/generalsettings.cpp +++ b/companion/src/firmwares/generalsettings.cpp @@ -92,6 +92,7 @@ bool GeneralSettings::isInputAvailable(int index) const const InputConfig &config = inputConfig[index]; return (config.type == Board::AIT_STICK || + config.type == Board::AIT_LUX || (config.type == Board::AIT_FLEX && config.flexType != Board::FLEX_NONE)); } @@ -124,10 +125,11 @@ bool GeneralSettings::isInputPot(int index) const if (isInputAvailable(index)) { const InputConfig &config = inputConfig[index]; - return (config.type == Board::AIT_FLEX && + return (config.type == Board::AIT_LUX || + (config.type == Board::AIT_FLEX && (config.flexType == Board::FLEX_POT || config.flexType == Board::FLEX_POT_CENTER || - config.flexType == Board::FLEX_MULTIPOS)); + config.flexType == Board::FLEX_MULTIPOS))); } return false; @@ -227,7 +229,7 @@ void GeneralSettings::init() backlightOffBright = IS_FAMILY_HORUS_OR_T16(board) ? 20 : 0; backgroundVolume = 1; - speakerVolume = 12; + speakerVolume = 0; wavVolume = 2; hatsMode = HATSMODE_SWITCHABLE; diff --git a/companion/src/simulation/CMakeLists.txt b/companion/src/simulation/CMakeLists.txt index b238a9154..fa32a0424 100644 --- a/companion/src/simulation/CMakeLists.txt +++ b/companion/src/simulation/CMakeLists.txt @@ -22,6 +22,7 @@ set(${PROJECT_NAME}_NAMES serialportsdialog hostserialconnector simulateduiwidgetGeneric + wasmsimulatorinterface ) if(TARGET SDL2::SDL2) @@ -54,10 +55,13 @@ add_library(${PROJECT_NAME} ${${PROJECT_NAME}_SRCS} ) +include(FetchWamr) + target_link_libraries(${PROJECT_NAME} PRIVATE ${CPN_COMMON_LIB} Qt::Svg + vmlib ) if(TARGET SDL2::SDL2) diff --git a/companion/src/simulation/radiouiaction.h b/companion/src/simulation/radiouiaction.h index 739b3d432..9093dafb8 100644 --- a/companion/src/simulation/radiouiaction.h +++ b/companion/src/simulation/radiouiaction.h @@ -118,21 +118,9 @@ class RadioUiAction : public QObject // Note: Qt::KeypadModifier is required for arrow keys as they are considered part of the keypad if (((keyEvent->modifiers() == Qt::NoModifier) || (keyEvent->modifiers() & Qt::ShiftModifier) || (keyEvent->modifiers() & Qt::KeypadModifier)) && m_keys.contains(keyEvent->key())) { - // qDebug() << "Event:" << event->type() << "Key:" << QString("0x%1").arg(keyEvent->key(), 8, 16, QLatin1Char( '0' )) << "Text:" << keyEvent->text() << "found"; trigger(event->type() == QEvent::KeyPress); return true; } - else { - // qDebug() << "Key not found"; - // qDebug() << "Event:" << event->type() << - // "Key:" << QString("0x%1").arg(keyEvent->key(), 8, 16, QLatin1Char( '0' )) << - // "Text:" << keyEvent->text() << - // "Shift:" << (bool)(keyEvent->modifiers() & Qt::ShiftModifier) << - // "Keypad:" << (bool)(keyEvent->modifiers() & Qt::KeypadModifier) << - // "Ctrl:" << (bool)(keyEvent->modifiers() & Qt::ControlModifier) << - // "Alt:" << (bool)(keyEvent->modifiers() & Qt::AltModifier) << - // "Meta:" << (bool)(keyEvent->modifiers() & Qt::MetaModifier); - } } return QObject::eventFilter(obj, event); diff --git a/companion/src/simulation/simulateduiwidget.cpp b/companion/src/simulation/simulateduiwidget.cpp index ca574c72f..465d1980a 100644 --- a/companion/src/simulation/simulateduiwidget.cpp +++ b/companion/src/simulation/simulateduiwidget.cpp @@ -174,9 +174,16 @@ void SimulatedUIWidget::wheelEvent(QWheelEvent * event) { if (event->angleDelta().isNull()) return; - // steps can be negative or positive to determine direction (negative is UP/LEFT scroll) - QPoint numSteps = event->angleDelta() / 8 / 15 * -1; // one step per 15deg - emit simulatorWheelEvent(numSteps.y()); + + // Accumulate sub-step deltas (macOS trackpad sends pixel-level values) + static constexpr int STEP_THRESHOLD = 120; + m_wheelAccumulator += event->angleDelta().y() * -1; + + int steps = m_wheelAccumulator / STEP_THRESHOLD; + if (steps != 0) { + m_wheelAccumulator -= steps * STEP_THRESHOLD; + emit simulatorWheelEvent(steps); + } event->accept(); } diff --git a/companion/src/simulation/simulateduiwidget.h b/companion/src/simulation/simulateduiwidget.h index c37947f83..3b2cc6452 100644 --- a/companion/src/simulation/simulateduiwidget.h +++ b/companion/src/simulation/simulateduiwidget.h @@ -149,6 +149,7 @@ class SimulatedUIWidget : public QWidget unsigned int m_backLight; int m_beepShow; int m_beepVal; + int m_wheelAccumulator = 0; static int strKeyToInt(std::string key); diff --git a/companion/src/simulation/simulator.h b/companion/src/simulation/simulator.h index e00ebd4f6..1ea87922e 100644 --- a/companion/src/simulation/simulator.h +++ b/companion/src/simulation/simulator.h @@ -155,7 +155,7 @@ struct SimulatorOptions if (o.m_version >= 3) in >> o.simulatorId; if (o.m_version >= 4) - in >> o.simulatorId; + in >> o.flags; } else { qWarning() << "Error loading SimulatorOptions, saved version not valid:" << o.m_version << "Expected <=" << SIMULATOR_OPTIONS_VERSION; diff --git a/companion/src/simulation/simulatorinterface.cpp b/companion/src/simulation/simulatorinterface.cpp index 188ae2bb6..2789d8b50 100644 --- a/companion/src/simulation/simulatorinterface.cpp +++ b/companion/src/simulation/simulatorinterface.cpp @@ -20,17 +20,15 @@ */ #include "simulatorinterface.h" +#include "wasmsimulatorinterface.h" #include "customdebug.h" #include "version.h" +#include "firmwares/eeprominterface.h" #include #include -#if defined _MSC_VER || !defined __GNUC__ - #include -#endif - -QMap> SimulatorLoader::registeredSimulators; +QMap SimulatorLoader::registeredSimulators; QStringList SimulatorLoader::getAvailableSimulators() { @@ -39,29 +37,40 @@ QStringList SimulatorLoader::getAvailableSimulators() int SimulatorLoader::registerSimulators(const QDir & dir) { - QStringList filters; -#if defined(__APPLE__) - filters << "libedgetx-*-simulator.dylib"; -#elif defined(WIN32) || defined(__CYGWIN__) - filters << "libedgetx-*-simulator.dll"; -#else - filters << "libedgetx-*-simulator.so"; -#endif - registeredSimulators.clear(); + QStringList wasmFilters; + wasmFilters << "edgetx-*-simulator.wasm"; - qCDebug(simulatorInterfaceLoader) << "Searching for simulators in" << dir.path() << "matching pattern" << filters; + qCDebug(simulatorInterfaceLoader) << "Searching for WASM simulators in" + << dir.path(); - foreach(QString filename, dir.entryList(filters, QDir::Files)) { - QString simuName(filename.mid(3, filename.lastIndexOf('-') - 3)); - QString libPath(dir.path() + "/" + filename); + foreach (QString filename, dir.entryList(wasmFilters, QDir::Files)) { + // Extract board name: "edgetx--simulator.wasm" + QString simuName = filename; + simuName.remove(0, 7); // remove "edgetx-" + simuName.truncate(simuName.lastIndexOf("-simulator")); - if (getAvailableSimulators().contains(simuName)) + if (registeredSimulators.contains(simuName)) continue; - registeredSimulators.insert(simuName, {libPath, nullptr}); + QString wasmPath = dir.path() + "/" + filename; + + // Resolve board type from name + Board::Type boardType = Board::BOARD_UNKNOWN; + Firmware * fw = Firmware::getFirmwareForId(QString("edgetx-") + simuName); + if (fw) + boardType = fw->getBoard(); + + auto * factory = new WasmSimulatorFactory(wasmPath, simuName, boardType); + registeredSimulators.insert(simuName, factory); + + qCDebug(simulatorInterfaceLoader) << "Registered WASM simulator:" + << simuName; } - qCDebug(simulatorInterfaceLoader) << "Found libraries:" << (registeredSimulators.size() ? registeredSimulators.keys() : QStringList() << "none"); + qCDebug(simulatorInterfaceLoader) + << "Found WASM modules:" + << (registeredSimulators.size() ? registeredSimulators.keys() + : QStringList() << "none"); return registeredSimulators.size(); } @@ -78,7 +87,8 @@ void SimulatorLoader::registerSimulators() if (QDir::isAbsolutePath(SIMULATOR_LIB_SEARCH_PATH)) { dir.setPath(SIMULATOR_LIB_SEARCH_PATH); } else { - dir.setPath(QCoreApplication::applicationDirPath() + "/" SIMULATOR_LIB_SEARCH_PATH); + dir.setPath(QCoreApplication::applicationDirPath() + "/" + SIMULATOR_LIB_SEARCH_PATH); } #endif registerSimulators(dir.absolutePath()); @@ -86,98 +96,49 @@ void SimulatorLoader::registerSimulators() void SimulatorLoader::unregisterSimulators() { - for(QPair lib : registeredSimulators) { - if (lib.second) - delete lib.second; - } + qDeleteAll(registeredSimulators); + registeredSimulators.clear(); } QString SimulatorLoader::findSimulatorByName(const QString & name) { int pos; - QString ret; QString simuName = name; - while(1) { - qCDebug(simulatorInterfaceLoader) << "searching" << simuName << "simulator"; - if (registeredSimulators.contains(simuName)) { - ret = simuName; - break; - } + // Strip "edgetx-" prefix if present (registered names don't have it) + if (simuName.startsWith("edgetx-")) + simuName.remove(0, 7); + + while (1) { + qCDebug(simulatorInterfaceLoader) << "searching" << simuName + << "simulator"; + if (registeredSimulators.contains(simuName)) + return simuName; if ((pos = simuName.lastIndexOf('-')) <= 0) break; simuName = simuName.mid(0, pos); - if (simuName.count('-') == 0) - break; } - return ret; + return QString(); } SimulatorInterface * SimulatorLoader::loadSimulator(const QString & name) { - SimulatorInterface * si = nullptr; QString simuName = findSimulatorByName(name); if (simuName.isEmpty()) { qWarning() << "Simulator" << name << "not found."; - return si; + return nullptr; } - QPair libInfo = registeredSimulators.value(simuName, {QString(), nullptr}); - QString libPath = libInfo.first; - QLibrary *lib = libInfo.second; - qCDebug(simulatorInterfaceLoader) << "Trying to load simulator in " << libPath; - - if (!lib) { - lib = new QLibrary(libPath); - if (lib) - registeredSimulators.insert(simuName, {libPath, lib}); - else { - qWarning() << "Unable to load library"; - return si; - } - } - - SimulatorFactory * factory; - RegisterSimulator registerFunc = (RegisterSimulator)lib->resolve("registerSimu"); - if (registerFunc && (factory = registerFunc()) && (si = factory->create())) { - quint8 instance = lib->property("instances_used").toUInt(); - lib->setProperty("instances_used", ++instance); - qCDebug(simulatorInterfaceLoader) << "Loaded" << factory->name() << "simulator instance" << instance; - delete factory; - } - else { - qWarning() << "Library error" << libPath << lib->errorString(); - } + SimulatorInterface * si = registeredSimulators[simuName]->create(); + if (si) + qCDebug(simulatorInterfaceLoader) << "Loaded simulator:" << simuName; return si; } bool SimulatorLoader::unloadSimulator(const QString & name) { - bool ret = false; - QString simuName = findSimulatorByName(name); - - if (simuName.isEmpty()) - return ret; - - QLibrary * lib = registeredSimulators.value(simuName).second; - - if (lib && lib->isLoaded()) { - quint8 instance = lib->property("instances_used").toUInt(); - lib->setProperty("instances_used", --instance); - if (!instance) { - ret = lib->unload(); - qCDebug(simulatorInterfaceLoader) << "Unloading" << simuName << "(" << lib->fileName() << ")" << "result:" << ret; - } - else { - ret = true; - qCDebug(simulatorInterfaceLoader) << "Simulator" << simuName << "instances remaining:" << instance; - } - } - else { - qCDebug(simulatorInterfaceLoader) << "Simulator library for " << simuName << "already unloaded."; - } - - return ret; + Q_UNUSED(name); + return true; } diff --git a/companion/src/simulation/simulatorinterface.h b/companion/src/simulation/simulatorinterface.h index 2d9850eed..c24dd8e97 100644 --- a/companion/src/simulation/simulatorinterface.h +++ b/companion/src/simulation/simulatorinterface.h @@ -31,7 +31,6 @@ #include #include #include -#include #include #include @@ -177,7 +176,6 @@ class SimulatorInterface : public QObject void started(); void stopped(); void heartbeat(qint32 loops, qint64 timestamp); - void runtimeError(const QString & error); void lcdChange(bool backlightEnable); void phaseChanged(qint8 phase, const QString & name); void channelOutValueChange(quint8 index, qint32 value, qint32 limit); @@ -216,8 +214,6 @@ class SimulatorLoader static bool unloadSimulator(const QString & name); protected: - typedef SimulatorFactory * (*RegisterSimulator)(); - static int registerSimulators(const QDir & dir); - static QMap> registeredSimulators; + static QMap registeredSimulators; }; diff --git a/companion/src/simulation/simulatormainwindow.cpp b/companion/src/simulation/simulatormainwindow.cpp index 66759ee30..6a2e62668 100644 --- a/companion/src/simulation/simulatormainwindow.cpp +++ b/companion/src/simulation/simulatormainwindow.cpp @@ -123,11 +123,16 @@ SimulatorMainWindow::SimulatorMainWindow(QWidget *parent, const QString & simula ui->menuView->insertSeparator(ui->actionToggleMenuBar); ui->menuView->insertAction(ui->actionToggleMenuBar, ui->toolBar->toggleViewAction()); - // Hide some actions based on simulator capabilities. - if(!m_simulator->getCapability(SimulatorInterface::CAP_LUA)) - ui->actionReloadLua->setDisabled(true); - if(!m_simulator->getCapability(SimulatorInterface::CAP_TELEM_FRSKY_SPORT)) - m_telemetryDockWidget->toggleViewAction()->setDisabled(true); + // Disable capability-dependent actions until the simulator starts and + // the WASM module is loaded (getCapability returns 0 before that). + ui->actionReloadLua->setDisabled(true); + m_telemetryDockWidget->toggleViewAction()->setDisabled(true); + connect(m_simulator, &SimulatorInterface::started, this, [this]() { + if (m_simulator->getCapability(SimulatorInterface::CAP_LUA)) + ui->actionReloadLua->setEnabled(true); + if (m_simulator->getCapability(SimulatorInterface::CAP_TELEM_FRSKY_SPORT)) + m_telemetryDockWidget->toggleViewAction()->setEnabled(true); + }); #ifndef USE_SDL ui->actionJoystickSettings->setDisabled(true); #endif diff --git a/companion/src/simulation/simulatorstartupdialog.cpp b/companion/src/simulation/simulatorstartupdialog.cpp index feec393cc..b41393b59 100644 --- a/companion/src/simulation/simulatorstartupdialog.cpp +++ b/companion/src/simulation/simulatorstartupdialog.cpp @@ -210,7 +210,7 @@ void SimulatorStartupDialog::loadRadioProfile(int id) *m_options = g.profile[id].simulatorOptions(); - if (m_options->firmwareId.isEmpty() && !g.profile[id].fwType().isEmpty()) + if (!g.profile[id].fwType().isEmpty()) m_options->firmwareId = g.profile[id].fwType(); m_options->firmwareId = findRadioId(m_options->firmwareId); @@ -222,11 +222,11 @@ void SimulatorStartupDialog::loadRadioProfile(int id) setGlobalFirmware(m_options->firmwareId); // always refresh as linked simulatorId as could change over time - m_options->simulatorId = getCurrentFirmware()->getSimulatorId(); + m_options->simulatorId = SimulatorLoader::findSimulatorByName(getCurrentFirmware()->getSimulatorId()); m_simProxy->setFilterFixedString(m_options->simulatorId); - i = ui->cbSimulator->findText(findRadioId(m_options->simulatorId), Qt::MatchContains); + i = ui->cbSimulator->findText(m_options->simulatorId, Qt::MatchContains); if (i > -1) ui->cbSimulator->setCurrentIndex(i); @@ -293,11 +293,11 @@ void SimulatorStartupDialog::onRadioTypeChanged(int index) setGlobalFirmware(id); // always refresh as linked simulatorId as could change over time - id = getCurrentFirmware()->getSimulatorId(); + id = SimulatorLoader::findSimulatorByName(getCurrentFirmware()->getSimulatorId()); m_simProxy->setFilterFixedString(id); - const int i = ui->cbSimulator->findText(findRadioId(id), Qt::MatchContains); + const int i = ui->cbSimulator->findText(id, Qt::MatchContains); if (i > -1) ui->cbSimulator->setCurrentIndex(i); } @@ -305,8 +305,8 @@ void SimulatorStartupDialog::onRadioTypeChanged(int index) void SimulatorStartupDialog::onDataFileSelect(bool) { QString filter = SIMU_FILES_FILTER; - QString file = QFileDialog::getSaveFileName(this, tr("Select a data file"), ui->dataFile->text(), - filter, NULL, QFileDialog::DontConfirmOverwrite); + QString file = QFileDialog::getOpenFileName(this, tr("Select a data file"), ui->dataFile->text(), + filter); if (!file.isEmpty()) { ui->dataFile->setText(file); ui->optFile->setChecked(true); diff --git a/companion/src/simulation/simulatorwidget.cpp b/companion/src/simulation/simulatorwidget.cpp index b4e3e2686..878e34c4f 100644 --- a/companion/src/simulation/simulatorwidget.cpp +++ b/companion/src/simulation/simulatorwidget.cpp @@ -104,7 +104,6 @@ SimulatorWidget::SimulatorWidget(QWidget * parent, SimulatorInterface * simulato connect(simulator, &SimulatorInterface::started, this, &SimulatorWidget::onSimulatorStarted); connect(simulator, &SimulatorInterface::heartbeat, this, &SimulatorWidget::onSimulatorHeartbeat); - connect(simulator, &SimulatorInterface::runtimeError, this, &SimulatorWidget::onSimulatorError); connect(simulator, &SimulatorInterface::phaseChanged, this, &SimulatorWidget::onPhaseChanged); connect((SimulatorMainWindow *)parent, &SimulatorMainWindow::txBatteryVoltageChanged, this, &SimulatorWidget::onTxBatteryVoltageChanged); @@ -862,8 +861,6 @@ void SimulatorWidget::onjoystickAxisValueChanged(int axis, int value) GeneralSettings radioSettings = GeneralSettings(); if (radioSettings.isInputAvailable(stick)) { if (radioSettings.isInputPot(stick)) { - if (radioSettings.inputConfig[stick].flexType == Board::FlexType::FLEX_MULTIPOS) - stickval += 1024; emit widgetValueChange(RadioWidget::RADIO_WIDGET_KNOB, stick, stickval); } else if (radioSettings.isInputSlider(stick)) { emit widgetValueChange(RadioWidget::RADIO_WIDGET_FADER, stick, stickval); diff --git a/companion/src/simulation/wasmsimulatorinterface.cpp b/companion/src/simulation/wasmsimulatorinterface.cpp new file mode 100644 index 000000000..8e6f4309b --- /dev/null +++ b/companion/src/simulation/wasmsimulatorinterface.cpp @@ -0,0 +1,1083 @@ +/* + * Copyright (C) EdgeTX + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include "wasmsimulatorinterface.h" + +#include +#include +#include + +// WAMR native callback: called by WASM module to get analog values in +// ADC range (0..4096, center=2048). The WASM ADC driver does a direct +// passthrough, so the host must provide values in ADC range. +// UI widgets emit -1024..+1024 (center=0); we convert here. +static uint32_t host_simuGetAnalog(wasm_exec_env_t exec_env, uint32_t idx) +{ + auto * inst = wasm_runtime_get_module_inst(exec_env); + auto * iface = static_cast( + wasm_runtime_get_custom_data(inst)); + if (iface) { + int16_t raw = iface->getAnalogValue((uint8_t)idx); + uint16_t adc = (uint16_t)((raw + 1024) * 2); // -1024..+1024 → 0..4096 + return (uint32_t)adc; + } + return 2048; +} + +// WAMR native callback: called by WASM module to queue audio PCM data +static void host_simuQueueAudio(wasm_exec_env_t exec_env, uint8_t * buf, + uint32_t len) +{ + auto * inst = wasm_runtime_get_module_inst(exec_env); + auto * iface = static_cast( + wasm_runtime_get_custom_data(inst)); + if (iface && buf && len > 0) + iface->queueAudio(buf, len); +} + +// WAMR native callback: called by WASM module to send trace/debug output +static void host_simuTrace(wasm_exec_env_t exec_env, const char * text) +{ + auto * inst = wasm_runtime_get_module_inst(exec_env); + auto * iface = static_cast( + wasm_runtime_get_custom_data(inst)); + if (iface && text) + iface->writeTrace(text); +} + +static bool s_wamrInitialized = false; + +static void host_simuLcdNotify(wasm_exec_env_t exec_env) +{ + auto * inst = wasm_runtime_get_module_inst(exec_env); + auto * iface = static_cast( + wasm_runtime_get_custom_data(inst)); + if (iface) + iface->notifyLcdReady(); +} + +static NativeSymbol s_nativeSymbols[] = { + {"simuGetAnalog", (void *)host_simuGetAnalog, "(i)i", nullptr}, + {"simuQueueAudio", (void *)host_simuQueueAudio, "(*~)", nullptr}, + {"simuTrace", (void *)host_simuTrace, "($)", nullptr}, + {"simuLcdNotify", (void *)host_simuLcdNotify, "()", nullptr}, +}; + +WasmSimulatorInterface::WasmSimulatorInterface(const QString & wasmPath, + const QString & boardName, + Board::Type boardType) + : SimulatorInterface(), + m_wasmPath(wasmPath), + m_boardName(boardName), + m_boardType(boardType) +{ +} + +WasmSimulatorInterface::~WasmSimulatorInterface() +{ + if (isRunning()) + stop(); + + unloadModule(); + + delete[] m_lcdBuffer; + delete m_timer10ms; +} + +QString WasmSimulatorInterface::name() +{ + return m_boardName; +} + +bool WasmSimulatorInterface::isRunning() +{ + if (!m_fnIsRunning || !m_execEnv) + return false; + + QMutexLocker lckr(&m_mutex); + uint32_t argv[1] = {0}; + if (wasm_runtime_call_wasm(m_execEnv, m_fnIsRunning, 0, argv)) + return argv[0] != 0; + return false; +} + +uint8_t * WasmSimulatorInterface::getLcd() +{ + return m_lcdBuffer; +} + +uint8_t WasmSimulatorInterface::getSensorInstance(uint16_t id, + uint8_t defaultValue) +{ + Q_UNUSED(id); + return defaultValue; +} + +uint16_t WasmSimulatorInterface::getSensorRatio(uint16_t id) +{ + Q_UNUSED(id); + return 0; +} + +const int WasmSimulatorInterface::getCapability(Capability cap) +{ + if (!m_fnGetCapability || !m_execEnv) + return 0; + QMutexLocker lckr(&m_mutex); + uint32_t argv[1] = {(uint32_t)cap}; + if (wasm_runtime_call_wasm(m_execEnv, m_fnGetCapability, 1, argv)) + return (int32_t)argv[0]; + return 0; +} + +bool WasmSimulatorInterface::loadModule() +{ + // Initialize WAMR runtime (once) + if (!s_wamrInitialized) { + if (!wasm_runtime_init()) { + qWarning() << "Failed to initialize WAMR runtime"; + return false; + } + wasm_runtime_set_log_level(WASM_LOG_LEVEL_WARNING); + wasm_runtime_register_natives("env", s_nativeSymbols, + sizeof(s_nativeSymbols) / + sizeof(NativeSymbol)); + s_wamrInitialized = true; + } + + // Read .wasm file + QFile file(m_wasmPath); + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open WASM file:" << m_wasmPath; + return false; + } + m_wasmBinary = file.readAll(); + file.close(); + + // Load module + char errorBuf[128]; + m_module = wasm_runtime_load((uint8_t *)m_wasmBinary.data(), + m_wasmBinary.size(), errorBuf, sizeof(errorBuf)); + if (!m_module) { + qWarning() << "Failed to load WASM module:" << errorBuf; + return false; + } + + // Set up WASI (filesystem access for SD card and settings). + // NOTE: wasm_runtime_set_wasi_args() only stores pointers; the actual WASI + // init happens inside wasm_runtime_instantiate(), so all buffers must remain + // alive until after instantiation completes. + QByteArray sdPathUtf8 = m_sdPath.toUtf8(); + QByteArray settingsPathUtf8 = m_settingsPath.toUtf8(); + const char * dirList[2] = {}; + int nDirs = 0; + if (!m_sdPath.isEmpty()) { + dirList[nDirs++] = sdPathUtf8.constData(); + } + if (!m_settingsPath.isEmpty() && m_settingsPath != m_sdPath) { + dirList[nDirs++] = settingsPathUtf8.constData(); + } + if (nDirs > 0) { + for (int i = 0; i < nDirs; ++i) + qDebug() << "WASI preopen[" << i << "]:" << dirList[i]; + wasm_runtime_set_wasi_args(m_module, dirList, nDirs, nullptr, 0, + nullptr, 0, nullptr, 0); + } + + // Instantiate + uint32_t stackSize = 256 * 1024; + uint32_t heapSize = 16 * 1024 * 1024; + m_moduleInst = wasm_runtime_instantiate(m_module, stackSize, heapSize, + errorBuf, sizeof(errorBuf)); + if (!m_moduleInst) { + qWarning() << "Failed to instantiate WASM module:" << errorBuf; + return false; + } + + // Store pointer to this instance for native callbacks + wasm_runtime_set_custom_data(m_moduleInst, this); + + // Create execution environment + m_execEnv = + wasm_runtime_create_exec_env(m_moduleInst, stackSize); + if (!m_execEnv) { + qWarning() << "Failed to create WASM execution environment"; + return false; + } + + return resolveExports(); +} + +void WasmSimulatorInterface::unloadModule() +{ + if (m_execEnv && m_fnFree) { + if (m_wasmLcdBuf) { + uint32_t freeArgv[1] = {m_wasmLcdBuf}; + wasm_runtime_call_wasm(m_execEnv, m_fnFree, 1, freeArgv); + m_wasmLcdBuf = 0; + } + if (m_wasmScratchBuf) { + uint32_t freeArgv[1] = {m_wasmScratchBuf}; + wasm_runtime_call_wasm(m_execEnv, m_fnFree, 1, freeArgv); + m_wasmScratchBuf = 0; + } + } + if (m_execEnv) { + wasm_runtime_destroy_exec_env(m_execEnv); + m_execEnv = nullptr; + } + if (m_moduleInst) { + wasm_runtime_deinstantiate(m_moduleInst); + m_moduleInst = nullptr; + } + if (m_module) { + wasm_runtime_unload(m_module); + m_module = nullptr; + } + m_wasmBinary.clear(); + + // Fully destroy the WAMR runtime so that its per-thread signal + // environment is re-initialised on the next loadModule() call. + // Without this, a new Qt thread (simuThread) will not have the + // thread-local signal env set up and instantiation will fail with + // "thread signal env not inited". + if (s_wamrInitialized) { + wasm_runtime_destroy(); + s_wamrInitialized = false; + } +} + +bool WasmSimulatorInterface::resolveExports() +{ + m_fnInit = wasm_runtime_lookup_function(m_moduleInst, "simuInit"); + m_fnStart = wasm_runtime_lookup_function(m_moduleInst, "simuStart"); + m_fnStop = wasm_runtime_lookup_function(m_moduleInst, "simuStop"); + m_fnIsRunning = wasm_runtime_lookup_function(m_moduleInst, "simuIsRunning"); + m_fnSetKey = wasm_runtime_lookup_function(m_moduleInst, "simuSetKey"); + m_fnSetTrim = wasm_runtime_lookup_function(m_moduleInst, "simuSetTrim"); + m_fnSetSwitch = wasm_runtime_lookup_function(m_moduleInst, "simuSetSwitch"); + m_fnLcdChanged = + wasm_runtime_lookup_function(m_moduleInst, "simuLcdChanged"); + m_fnLcdCopy = wasm_runtime_lookup_function(m_moduleInst, "simuLcdCopy"); + m_fnLcdGetWidth = + wasm_runtime_lookup_function(m_moduleInst, "simuLcdGetWidth"); + m_fnLcdGetHeight = + wasm_runtime_lookup_function(m_moduleInst, "simuLcdGetHeight"); + m_fnLcdGetDepth = + wasm_runtime_lookup_function(m_moduleInst, "simuLcdGetDepth"); + m_fnTouchDown = + wasm_runtime_lookup_function(m_moduleInst, "simuTouchDown"); + m_fnTouchUp = + wasm_runtime_lookup_function(m_moduleInst, "simuTouchUp"); + m_fnFatfsSetPaths = + wasm_runtime_lookup_function(m_moduleInst, "simuFatfsSetPaths"); + m_fnRotaryEncoderEvent = + wasm_runtime_lookup_function(m_moduleInst, "simuRotaryEncoderEvent"); + m_fnGetCapability = + wasm_runtime_lookup_function(m_moduleInst, "simuGetCapability"); + + // Output value exports (bulk copy) + m_fnGetNumChannels = + wasm_runtime_lookup_function(m_moduleInst, "simuGetNumChannels"); + m_fnCopyChannelOutputs = + wasm_runtime_lookup_function(m_moduleInst, "simuCopyChannelOutputs"); + m_fnCopyMixOutputs = + wasm_runtime_lookup_function(m_moduleInst, "simuCopyMixOutputs"); + m_fnGetNumLogicalSwitches = + wasm_runtime_lookup_function(m_moduleInst, "simuGetNumLogicalSwitches"); + m_fnCopyLogicalSwitches = + wasm_runtime_lookup_function(m_moduleInst, "simuCopyLogicalSwitches"); + m_fnGetTrimValue = + wasm_runtime_lookup_function(m_moduleInst, "simuGetTrimValue"); + m_fnGetTrimRange = + wasm_runtime_lookup_function(m_moduleInst, "simuGetTrimRange"); + m_fnGetFlightMode = + wasm_runtime_lookup_function(m_moduleInst, "simuGetFlightMode"); + m_fnGetNumGVars = + wasm_runtime_lookup_function(m_moduleInst, "simuGetNumGVars"); + m_fnGetNumFlightModes = + wasm_runtime_lookup_function(m_moduleInst, "simuGetNumFlightModes"); + m_fnGetGVar = + wasm_runtime_lookup_function(m_moduleInst, "simuGetGVar"); + + // Phase 4: telemetry, trim, lua, lcd, trainer + m_fnSetTrimValue = + wasm_runtime_lookup_function(m_moduleInst, "simuSetTrimValue"); + m_fnSendTelemetry = + wasm_runtime_lookup_function(m_moduleInst, "simuSendTelemetry"); + m_fnLuaReloadPermanentScripts = + wasm_runtime_lookup_function(m_moduleInst, "simuLuaReloadPermanentScripts"); + m_fnLcdFlushed = + wasm_runtime_lookup_function(m_moduleInst, "simuLcdFlushed"); + m_fnGetMaxTrainerChannels = + wasm_runtime_lookup_function(m_moduleInst, "simuGetMaxTrainerChannels"); + m_fnCopyTrainerInput = + wasm_runtime_lookup_function(m_moduleInst, "simuCopyTrainerInput"); + m_fnSetTrainerTimeout = + wasm_runtime_lookup_function(m_moduleInst, "simuSetTrainerTimeout"); + + // Backlight and function switch LEDs + m_fnGetBacklightState = + wasm_runtime_lookup_function(m_moduleInst, "simuGetBacklightState"); + m_fnGetNumCustomSwitches = + wasm_runtime_lookup_function(m_moduleInst, "simuGetNumCustomSwitches"); + m_fnGetCustomSwitchColor = + wasm_runtime_lookup_function(m_moduleInst, "simuGetCustomSwitchColor"); + m_fnGetCustomSwitchIndex = + wasm_runtime_lookup_function(m_moduleInst, "simuGetCustomSwitchIndex"); + + m_fnMalloc = wasm_runtime_lookup_function(m_moduleInst, "malloc"); + m_fnFree = wasm_runtime_lookup_function(m_moduleInst, "free"); + + if (!m_fnInit || !m_fnStart || !m_fnStop || !m_fnIsRunning || + !m_fnLcdCopy || !m_fnLcdGetWidth || + !m_fnLcdGetHeight || !m_fnLcdGetDepth || !m_fnMalloc || !m_fnFree) { + qWarning() << "Failed to resolve required WASM exports"; + return false; + } + + return true; +} + +void WasmSimulatorInterface::init() +{ + if (isRunning()) + return; + + if (!m_moduleInst && !loadModule()) { + qWarning() << "Failed to load WASM module for" << m_boardName; + return; + } + + if (!m_timer10ms) { + m_timer10ms = new QTimer(); + m_timer10ms->setInterval(10); + connect(m_timer10ms, &QTimer::timeout, this, + &WasmSimulatorInterface::run); + connect(this, SIGNAL(started()), m_timer10ms, SLOT(start())); + connect(this, SIGNAL(stopped()), m_timer10ms, SLOT(stop())); + } + + m_stopRequested = false; + memset(m_analogValues, 0, sizeof(m_analogValues)); + + // Call simuInit + QMutexLocker lckr(&m_mutex); + wasm_runtime_call_wasm(m_execEnv, m_fnInit, 0, nullptr); + + // Query LCD dimensions + uint32_t argv[1] = {0}; + if (wasm_runtime_call_wasm(m_execEnv, m_fnLcdGetWidth, 0, argv)) + m_lcdWidth = argv[0]; + argv[0] = 0; + if (wasm_runtime_call_wasm(m_execEnv, m_fnLcdGetHeight, 0, argv)) + m_lcdHeight = argv[0]; + argv[0] = 0; + if (wasm_runtime_call_wasm(m_execEnv, m_fnLcdGetDepth, 0, argv)) + m_lcdDepth = argv[0]; + + // Allocate host-side LCD buffer + if (m_lcdWidth > 0 && m_lcdHeight > 0) { + uint32_t bitsPerPixel = m_lcdDepth; + if (bitsPerPixel == 1) + m_lcdBufferSize = m_lcdWidth * ((m_lcdHeight + 7) / 8); + else if (bitsPerPixel == 4) + m_lcdBufferSize = m_lcdWidth * m_lcdHeight * 4 / 8; + else + m_lcdBufferSize = m_lcdWidth * m_lcdHeight * (bitsPerPixel / 8); + + delete[] m_lcdBuffer; + m_lcdBuffer = new uint8_t[m_lcdBufferSize]; + memset(m_lcdBuffer, 0, m_lcdBufferSize); + + // Allocate persistent WASM-side buffer for simuLcdCopy + if (m_fnMalloc) { + uint32_t allocArgv[1] = {m_lcdBufferSize}; + if (wasm_runtime_call_wasm(m_execEnv, m_fnMalloc, 1, allocArgv) && + allocArgv[0]) { + m_wasmLcdBuf = allocArgv[0]; + } + } + } + + // Allocate persistent scratch buffer in WASM memory for bulk copies. + // Sized for max(channels * sizeof(int16_t), logical switches). + m_wasmScratchSize = CPN_MAX_CHNOUT * sizeof(int16_t); + if (CPN_MAX_LOGICAL_SWITCHES > m_wasmScratchSize) + m_wasmScratchSize = CPN_MAX_LOGICAL_SWITCHES; + if (m_fnMalloc) { + uint32_t allocArgv[1] = {m_wasmScratchSize}; + if (wasm_runtime_call_wasm(m_execEnv, m_fnMalloc, 1, allocArgv) && + allocArgv[0]) { + m_wasmScratchBuf = allocArgv[0]; + } + } + + m_lastOutputs.clear(); + m_resetOutputsData = true; + + qDebug() << "WASM simulator" << m_boardName << "initialized: LCD" + << m_lcdWidth << "x" << m_lcdHeight << "depth" << m_lcdDepth; +} + +void WasmSimulatorInterface::start(const char * filename, bool tests) +{ + Q_UNUSED(filename); + if (isRunning()) + return; + + QMutexLocker lckr(&m_mutex); + + // Set up FATFS paths before starting (prevents STORAGE WARNING) + if (m_fnFatfsSetPaths && m_fnMalloc && m_fnFree) { + QByteArray sdPathUtf8 = m_sdPath.toUtf8(); + QByteArray settingsPathUtf8 = m_settingsPath.toUtf8(); + uint32_t sdLen = sdPathUtf8.size() + 1; + uint32_t settingsLen = settingsPathUtf8.size() + 1; + + // Allocate WASM memory for both strings + uint32_t allocArgv[1] = {sdLen}; + uint32_t wasmSdPath = 0, wasmSettingsPath = 0; + + if (wasm_runtime_call_wasm(m_execEnv, m_fnMalloc, 1, allocArgv) && + allocArgv[0]) { + wasmSdPath = allocArgv[0]; + void * nativeSd = + wasm_runtime_addr_app_to_native(m_moduleInst, wasmSdPath); + if (nativeSd) + memcpy(nativeSd, sdPathUtf8.constData(), sdLen); + } + + allocArgv[0] = settingsLen; + if (wasm_runtime_call_wasm(m_execEnv, m_fnMalloc, 1, allocArgv) && + allocArgv[0]) { + wasmSettingsPath = allocArgv[0]; + void * nativeSettings = + wasm_runtime_addr_app_to_native(m_moduleInst, wasmSettingsPath); + if (nativeSettings) + memcpy(nativeSettings, settingsPathUtf8.constData(), settingsLen); + } + + if (wasmSdPath && wasmSettingsPath) { + uint32_t pathArgv[2] = {wasmSdPath, wasmSettingsPath}; + if (!wasm_runtime_call_wasm(m_execEnv, m_fnFatfsSetPaths, 2, pathArgv)) { + qWarning() << "WASM simuFatfsSetPaths failed:" + << wasm_runtime_get_exception(m_moduleInst); + } + } + + // Free WASM string buffers + if (wasmSdPath) { + uint32_t freeArgv[1] = {wasmSdPath}; + wasm_runtime_call_wasm(m_execEnv, m_fnFree, 1, freeArgv); + } + if (wasmSettingsPath) { + uint32_t freeArgv[1] = {wasmSettingsPath}; + wasm_runtime_call_wasm(m_execEnv, m_fnFree, 1, freeArgv); + } + } + + uint32_t argv[1] = {(uint32_t)tests}; + if (!wasm_runtime_call_wasm(m_execEnv, m_fnStart, 1, argv)) { + qWarning() << "WASM simuStart failed:" + << wasm_runtime_get_exception(m_moduleInst); + return; + } + + initAudio(); + + emit started(); + QTimer::singleShot(0, this, SLOT(run())); +} + +void WasmSimulatorInterface::stop() +{ + if (!isRunning()) + return; + + m_stopRequested = true; + deinitAudio(); + + { + QMutexLocker lckr(&m_mutex); + wasm_runtime_call_wasm(m_execEnv, m_fnStop, 0, nullptr); + } + + // Fully destroy the WASM module so the next init()/start() gets a clean + // instance. WASI threads created by the firmware's RTOS layer cannot be + // reliably restarted within the same module instance. + unloadModule(); + + emit stopped(); +} + +void WasmSimulatorInterface::setSdPath(const QString & sdPath, + const QString & settingsPath) +{ + m_sdPath = sdPath; + m_settingsPath = settingsPath; +} + +void WasmSimulatorInterface::setVolumeGain(const int value) +{ + m_volumeGain = qBound(0, value * SDL_MIX_MAXVOLUME / 100, SDL_MIX_MAXVOLUME); +} + +void WasmSimulatorInterface::setAnalogValue(uint8_t index, int16_t value) +{ + if (index < MAX_ANALOGS) + m_analogValues[index] = value; +} + +int16_t WasmSimulatorInterface::getAnalogValue(uint8_t index) +{ + if (index < MAX_ANALOGS) + return m_analogValues[index]; + return 0; +} + +void WasmSimulatorInterface::setKey(uint8_t key, bool state) +{ + if (!m_fnSetKey || !m_execEnv) + return; + QMutexLocker lckr(&m_mutex); + uint32_t argv[2] = {key, (uint32_t)state}; + wasm_runtime_call_wasm(m_execEnv, m_fnSetKey, 2, argv); +} + +void WasmSimulatorInterface::setSwitch(uint8_t swtch, int8_t state) +{ + if (!m_fnSetSwitch || !m_execEnv) + return; + QMutexLocker lckr(&m_mutex); + uint32_t argv[2] = {swtch, (uint32_t)(int32_t)state}; + wasm_runtime_call_wasm(m_execEnv, m_fnSetSwitch, 2, argv); +} + +void WasmSimulatorInterface::setTrim(unsigned int idx, int value) +{ + if (!m_fnSetTrimValue || !m_execEnv) + return; + QMutexLocker lckr(&m_mutex); + uint32_t argv[2] = {(uint32_t)idx, (uint32_t)(int32_t)value}; + wasm_runtime_call_wasm(m_execEnv, m_fnSetTrimValue, 2, argv); +} + +void WasmSimulatorInterface::setTrimSwitch(uint8_t trim, bool state) +{ + if (!m_fnSetTrim || !m_execEnv) + return; + QMutexLocker lckr(&m_mutex); + uint32_t argv[2] = {trim, (uint32_t)state}; + wasm_runtime_call_wasm(m_execEnv, m_fnSetTrim, 2, argv); +} + +void WasmSimulatorInterface::setTrainerInput(unsigned int inputNumber, + int16_t value) +{ + if (inputNumber < MAX_TRAINER_CH) { + m_trainerValues[inputNumber] = value; + m_trainerDirty = true; + } +} + +void WasmSimulatorInterface::setInputValue(int type, uint8_t index, + int16_t value) +{ + switch (type) { + case INPUT_SRC_ANALOG: + case INPUT_SRC_STICK: + case INPUT_SRC_KNOB: + case INPUT_SRC_SLIDER: + setAnalogValue(index, value); + break; + case INPUT_SRC_TXVIN: + setAnalogValue(index, value); + emit txBatteryVoltageChanged((unsigned int)value); + break; + case INPUT_SRC_SWITCH: + setSwitch(index, (int8_t)value); + break; + case INPUT_SRC_TRIM_SW: + setTrimSwitch(index, (bool)value); + break; + case INPUT_SRC_TRIM: + setTrim(index, value); + break; + case INPUT_SRC_KEY: + setKey(index, (bool)value); + break; + case INPUT_SRC_TRAINER: + setTrainerInput(index, value); + break; + default: + break; + } +} + +void WasmSimulatorInterface::rotaryEncoderEvent(int steps) +{ + if (!m_fnRotaryEncoderEvent || !m_execEnv) + return; + QMutexLocker lckr(&m_mutex); + uint32_t argv[1] = {(uint32_t)(int32_t)steps}; + wasm_runtime_call_wasm(m_execEnv, m_fnRotaryEncoderEvent, 1, argv); +} + +void WasmSimulatorInterface::touchEvent(int type, int x, int y) +{ + if (!m_execEnv) + return; + QMutexLocker lckr(&m_mutex); + if ((type == 1 || type == 2) && m_fnTouchDown) { // TouchDown or TouchSlide + uint32_t argv[2] = {(uint32_t)(int16_t)x, (uint32_t)(int16_t)y}; + wasm_runtime_call_wasm(m_execEnv, m_fnTouchDown, 2, argv); + } else if (type == 0 && m_fnTouchUp) { // TouchUp + wasm_runtime_call_wasm(m_execEnv, m_fnTouchUp, 0, nullptr); + } +} + +void WasmSimulatorInterface::lcdFlushed() +{ + if (!m_fnLcdFlushed || !m_execEnv) + return; + QMutexLocker lckr(&m_mutex); + wasm_runtime_call_wasm(m_execEnv, m_fnLcdFlushed, 0, nullptr); +} + +void WasmSimulatorInterface::notifyLcdReady() +{ + // Called from WAMR thread — coalesce multiple notifications into one + // Qt event by only posting when the flag transitions from false to true. + if (!m_lcdNotified.exchange(true)) { + QMetaObject::invokeMethod(this, "onLcdNotify", Qt::QueuedConnection); + } +} + +void WasmSimulatorInterface::onLcdNotify() +{ + if (!m_lcdNotified.exchange(false)) + return; + refreshLcd(); +} + +void WasmSimulatorInterface::refreshLcd() +{ + if (!m_fnLcdCopy || !m_execEnv || !m_wasmLcdBuf || !m_lcdBuffer) + return; + + bool backlightOn = true; + { + QMutexLocker lckr(&m_mutex); + + uint32_t copyArgv[2] = {m_wasmLcdBuf, m_lcdBufferSize}; + if (wasm_runtime_call_wasm(m_execEnv, m_fnLcdCopy, 2, copyArgv)) { + uint32_t bytesWritten = copyArgv[0]; + void * nativePtr = + wasm_runtime_addr_app_to_native(m_moduleInst, m_wasmLcdBuf); + if (nativePtr && bytesWritten > 0) { + memcpy(m_lcdBuffer, nativePtr, qMin(bytesWritten, m_lcdBufferSize)); + } + } + + if (m_fnGetBacklightState) { + uint32_t blArgv[1] = {0}; + if (wasm_runtime_call_wasm(m_execEnv, m_fnGetBacklightState, 0, blArgv)) + backlightOn = blArgv[0] != 0; + } + } + + // Emit outside mutex — onLcdChange() calls lcdFlushed() which re-acquires it + emit lcdChange(backlightOn); +} + +void WasmSimulatorInterface::setTrainerTimeout(uint16_t ms) +{ + if (!m_fnSetTrainerTimeout || !m_execEnv) + return; + QMutexLocker lckr(&m_mutex); + uint32_t argv[1] = {(uint32_t)ms}; + wasm_runtime_call_wasm(m_execEnv, m_fnSetTrainerTimeout, 1, argv); +} + +void WasmSimulatorInterface::sendInternalModuleTelemetry(const quint8 protocol, + const QByteArray data) +{ + if (!m_fnSendTelemetry || !m_execEnv || !m_fnMalloc || !m_fnFree || + data.isEmpty()) + return; + + QMutexLocker lckr(&m_mutex); + uint32_t len = data.size(); + uint32_t allocArgv[1] = {len}; + if (!wasm_runtime_call_wasm(m_execEnv, m_fnMalloc, 1, allocArgv) || + !allocArgv[0]) + return; + + uint32_t wasmBuf = allocArgv[0]; + void * nativePtr = wasm_runtime_addr_app_to_native(m_moduleInst, wasmBuf); + if (nativePtr) + memcpy(nativePtr, data.constData(), len); + + uint32_t argv[4] = {0 /* INTERNAL_MODULE */, (uint32_t)protocol, wasmBuf, len}; + wasm_runtime_call_wasm(m_execEnv, m_fnSendTelemetry, 4, argv); + + uint32_t freeArgv[1] = {wasmBuf}; + wasm_runtime_call_wasm(m_execEnv, m_fnFree, 1, freeArgv); +} + +void WasmSimulatorInterface::sendExternalModuleTelemetry(const quint8 protocol, + const QByteArray data) +{ + if (!m_fnSendTelemetry || !m_execEnv || !m_fnMalloc || !m_fnFree || + data.isEmpty()) + return; + + QMutexLocker lckr(&m_mutex); + uint32_t len = data.size(); + uint32_t allocArgv[1] = {len}; + if (!wasm_runtime_call_wasm(m_execEnv, m_fnMalloc, 1, allocArgv) || + !allocArgv[0]) + return; + + uint32_t wasmBuf = allocArgv[0]; + void * nativePtr = wasm_runtime_addr_app_to_native(m_moduleInst, wasmBuf); + if (nativePtr) + memcpy(nativePtr, data.constData(), len); + + uint32_t argv[4] = {1 /* EXTERNAL_MODULE */, (uint32_t)protocol, wasmBuf, len}; + wasm_runtime_call_wasm(m_execEnv, m_fnSendTelemetry, 4, argv); + + uint32_t freeArgv[1] = {wasmBuf}; + wasm_runtime_call_wasm(m_execEnv, m_fnFree, 1, freeArgv); +} + +void WasmSimulatorInterface::setLuaStateReloadPermanentScripts() +{ + if (!m_fnLuaReloadPermanentScripts || !m_execEnv) + return; + QMutexLocker lckr(&m_mutex); + wasm_runtime_call_wasm(m_execEnv, m_fnLuaReloadPermanentScripts, 0, nullptr); +} + +void WasmSimulatorInterface::addTracebackDevice(QIODevice * device) +{ + QMutexLocker lckr(&m_mtxTbDevices); + if (device && !m_tracebackDevices.contains(device)) + m_tracebackDevices.append(device); +} + +void WasmSimulatorInterface::removeTracebackDevice(QIODevice * device) +{ + QMutexLocker lckr(&m_mtxTbDevices); + m_tracebackDevices.removeAll(device); +} + +void WasmSimulatorInterface::writeTrace(const char * text) +{ + QMutexLocker lckr(&m_mtxTbDevices); + for (QIODevice * dev : m_tracebackDevices) { + if (dev) + dev->write(text); + } +} + +void WasmSimulatorInterface::initAudio() +{ + if (SDL_WasInit(SDL_INIT_AUDIO) == 0) { + if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { + qWarning() << "Failed to init SDL audio:" << SDL_GetError(); + return; + } + } + + SDL_AudioSpec wanted = {}; + wanted.freq = 32000; // AUDIO_SAMPLE_RATE + wanted.format = AUDIO_S16SYS; + wanted.channels = 1; + wanted.samples = 1024; + + SDL_AudioSpec have = {}; + m_audioDevice = SDL_OpenAudioDevice(nullptr, 0, &wanted, &have, 0); + if (m_audioDevice > 0) { + qDebug() << "SDL audio opened: freq=" << have.freq + << "format=" << have.format << "channels=" << have.channels + << "samples=" << have.samples; + SDL_PauseAudioDevice(m_audioDevice, 0); + } else { + qWarning() << "Failed to open SDL audio:" << SDL_GetError(); + } +} + +void WasmSimulatorInterface::deinitAudio() +{ + if (m_audioDevice > 0) { + SDL_CloseAudioDevice(m_audioDevice); + m_audioDevice = 0; + } +} + +void WasmSimulatorInterface::queueAudio(const uint8_t * buf, uint32_t len) +{ + if (m_audioDevice == 0 || len == 0) + return; + + // Apply companion volume gain (m_volumeGain is 0..SDL_MIX_MAXVOLUME) + QByteArray scaled(len, 0); + SDL_MixAudioFormat((uint8_t *)scaled.data(), buf, AUDIO_S16SYS, len, + m_volumeGain); + SDL_QueueAudio(m_audioDevice, scaled.constData(), len); +} + +void WasmSimulatorInterface::receiveAuxSerialData(const quint8 port_num, + const QByteArray & data) +{ + Q_UNUSED(port_num); + Q_UNUSED(data); +} + +void WasmSimulatorInterface::run() +{ + static uint32_t loops = 0; + + if (m_stopRequested) + return; + + ++loops; + + // Emit heartbeat early so it is not delayed by WASM calls below + if (!(loops % (SIMULATOR_INTERFACE_HEARTBEAT_PERIOD / 10))) { + emit heartbeat(loops, 0); + } + + if (!isRunning()) { + emit stopped(); + return; + } + + // LCD updates are handled by onLcdNotify() via simuLcdNotify callback + + // Flush trainer input buffer (bulk copy) + if (m_trainerDirty && m_fnCopyTrainerInput && m_wasmScratchBuf) { + m_trainerDirty = false; + QMutexLocker lckr(&m_mutex); + uint8_t count = MAX_TRAINER_CH; + if (m_fnGetMaxTrainerChannels) { + uint32_t r[1] = {0}; + if (wasm_runtime_call_wasm(m_execEnv, m_fnGetMaxTrainerChannels, 0, r)) + count = qMin(count, (uint8_t)r[0]); + } + uint32_t bytes = count * sizeof(int16_t); + if (bytes <= m_wasmScratchSize) { + void * nativePtr = wasm_runtime_addr_app_to_native(m_moduleInst, + m_wasmScratchBuf); + if (nativePtr) { + memcpy(nativePtr, m_trainerValues, bytes); + uint32_t argv[2] = {m_wasmScratchBuf, (uint32_t)count}; + wasm_runtime_call_wasm(m_execEnv, m_fnCopyTrainerInput, 2, argv); + } + } + } + + // Check output values every 50ms (5 loops) + if (!(loops % 5)) { + QMutexLocker lckr(&m_mutex); + checkOutputsChanged(); + } +} + +// Helper: call a WASM function with 0 args, return int32 result +static int32_t wasmCall0(wasm_exec_env_t env, wasm_function_inst_t fn) +{ + uint32_t argv[1] = {0}; + if (wasm_runtime_call_wasm(env, fn, 0, argv)) + return (int32_t)argv[0]; + return 0; +} + +// Helper: call a WASM function with 1 uint32 arg, return int32 result +static int32_t wasmCall1(wasm_exec_env_t env, wasm_function_inst_t fn, + uint32_t arg0) +{ + uint32_t argv[1] = {arg0}; + if (wasm_runtime_call_wasm(env, fn, 1, argv)) + return (int32_t)argv[0]; + return 0; +} + +// Helper: call a WASM function with 2 uint32 args, return int32 result +static int32_t wasmCall2(wasm_exec_env_t env, wasm_function_inst_t fn, + uint32_t arg0, uint32_t arg1) +{ + uint32_t argv[2] = {arg0, arg1}; + if (wasm_runtime_call_wasm(env, fn, 2, argv)) + return (int32_t)argv[0]; + return 0; +} + +void WasmSimulatorInterface::checkOutputsChanged() +{ + if (!m_execEnv || !m_wasmScratchBuf) + return; + + const int16_t limit = 512 * 2; + int32_t tmpVal; + void * nativePtr = wasm_runtime_addr_app_to_native(m_moduleInst, + m_wasmScratchBuf); + if (!nativePtr) + return; + + // Channel outputs (bulk copy) + if (m_fnCopyChannelOutputs) { + uint32_t argv[2] = {m_wasmScratchBuf, CPN_MAX_CHNOUT}; + if (wasm_runtime_call_wasm(m_execEnv, m_fnCopyChannelOutputs, 2, argv)) { + uint8_t numCh = (uint8_t)argv[0]; + const int16_t * chans = (const int16_t *)nativePtr; + for (uint8_t i = 0; i < numCh; i++) { + if (m_lastOutputs.chans[i] != chans[i] || m_resetOutputsData) { + emit channelOutValueChange(i, chans[i], limit); + emit outputValueChange(OUTPUT_SRC_CHAN_OUT, i, chans[i]); + m_lastOutputs.chans[i] = chans[i]; + } + } + } + } + + // Mix outputs (bulk copy, reuse same buffer) + if (m_fnCopyMixOutputs) { + uint32_t argv[2] = {m_wasmScratchBuf, CPN_MAX_CHNOUT}; + if (wasm_runtime_call_wasm(m_execEnv, m_fnCopyMixOutputs, 2, argv)) { + uint8_t numCh = (uint8_t)argv[0]; + const int16_t * mix = (const int16_t *)nativePtr; + for (uint8_t i = 0; i < numCh; i++) { + if (m_lastOutputs.ex_chans[i] != mix[i] || m_resetOutputsData) { + emit channelMixValueChange(i, mix[i], limit * 2); + emit outputValueChange(OUTPUT_SRC_CHAN_MIX, i, mix[i]); + m_lastOutputs.ex_chans[i] = mix[i]; + } + } + } + } + + // Logical switches (bulk copy) + if (m_fnCopyLogicalSwitches) { + uint32_t argv[2] = {m_wasmScratchBuf, CPN_MAX_LOGICAL_SWITCHES}; + if (wasm_runtime_call_wasm(m_execEnv, m_fnCopyLogicalSwitches, 2, argv)) { + uint8_t numLsw = (uint8_t)argv[0]; + const uint8_t * lsw = (const uint8_t *)nativePtr; + for (uint8_t i = 0; i < numLsw; i++) { + bool val = lsw[i] != 0; + if (m_lastOutputs.vsw[i] != val || m_resetOutputsData) { + emit virtualSwValueChange(i, val ? 1 : 0); + emit outputValueChange(OUTPUT_SRC_VIRTUAL_SW, i, val ? 1 : 0); + m_lastOutputs.vsw[i] = val; + } + } + } + } + + // Trims + if (m_fnGetTrimValue) { + for (uint8_t i = 0; i < Board::TRIM_AXIS_COUNT; i++) { + tmpVal = wasmCall1(m_execEnv, m_fnGetTrimValue, i); + if (m_lastOutputs.trims[i] != tmpVal || m_resetOutputsData) { + emit trimValueChange(i, tmpVal); + emit outputValueChange(OUTPUT_SRC_TRIM_VALUE, i, tmpVal); + m_lastOutputs.trims[i] = tmpVal; + } + } + } + + // Trim range + if (m_fnGetTrimRange) { + tmpVal = (int16_t)wasmCall0(m_execEnv, m_fnGetTrimRange); + if (m_lastOutputs.trimRange != tmpVal || m_resetOutputsData) { + emit trimRangeChange(Board::TRIM_AXIS_COUNT, -tmpVal, tmpVal); + emit outputValueChange(OUTPUT_SRC_TRIM_RANGE, Board::TRIM_AXIS_COUNT, tmpVal); + m_lastOutputs.trimRange = tmpVal; + } + } + + // Flight mode + if (m_fnGetFlightMode) { + int8_t phase = (int8_t)wasmCall0(m_execEnv, m_fnGetFlightMode); + if (m_lastOutputs.phase != phase || m_resetOutputsData) { + emit phaseChanged(phase, QString::number(phase)); + emit outputValueChange(OUTPUT_SRC_PHASE, 0, (qint16)phase); + m_lastOutputs.phase = phase; + } + } + + // GVars + if (m_fnGetNumGVars && m_fnGetNumFlightModes && m_fnGetGVar) { + uint8_t numGv = (uint8_t)wasmCall0(m_execEnv, m_fnGetNumGVars); + uint8_t numFm = (uint8_t)wasmCall0(m_execEnv, m_fnGetNumFlightModes); + if (numGv > CPN_MAX_GVARS) numGv = CPN_MAX_GVARS; + if (numFm > CPN_MAX_FLIGHT_MODES) numFm = CPN_MAX_FLIGHT_MODES; + for (uint8_t gv = 0; gv < numGv; gv++) { + for (uint8_t fm = 0; fm < numFm; fm++) { + tmpVal = wasmCall2(m_execEnv, m_fnGetGVar, gv, fm); + if (m_lastOutputs.gvars[fm][gv] != tmpVal || m_resetOutputsData) { + m_lastOutputs.gvars[fm][gv] = tmpVal; + emit gVarValueChange(gv, tmpVal); + emit outputValueChange(OUTPUT_SRC_GVAR, gv, tmpVal); + } + } + } + } + + // Function switch LED colors + if (m_fnGetNumCustomSwitches && m_fnGetCustomSwitchColor && m_fnGetCustomSwitchIndex) { + uint8_t numFs = (uint8_t)wasmCall0(m_execEnv, m_fnGetNumCustomSwitches); + if (numFs > MAX_FS_LEDS) numFs = MAX_FS_LEDS; + for (uint8_t i = 0; i < numFs; i++) { + uint32_t color = (uint32_t)wasmCall1(m_execEnv, m_fnGetCustomSwitchColor, i); + if (m_lastFSLedColors[i] != color || m_resetOutputsData) { + m_lastFSLedColors[i] = color; + // Map custom switch index to global switch index for the UI widget + uint8_t swIdx = (uint8_t)wasmCall1(m_execEnv, m_fnGetCustomSwitchIndex, i); + emit fsColorChange(swIdx, (qint32)color); + } + } + } + + m_resetOutputsData = false; +} + +/* + * WasmSimulatorFactory + */ + +WasmSimulatorFactory::WasmSimulatorFactory(const QString & wasmPath, + const QString & boardName, + Board::Type boardType) + : m_wasmPath(wasmPath), m_boardName(boardName), m_boardType(boardType) +{ +} + +QString WasmSimulatorFactory::name() +{ + return m_boardName; +} + +Board::Type WasmSimulatorFactory::type() +{ + return m_boardType; +} + +SimulatorInterface * WasmSimulatorFactory::create() +{ + return new WasmSimulatorInterface(m_wasmPath, m_boardName, m_boardType); +} diff --git a/companion/src/simulation/wasmsimulatorinterface.h b/companion/src/simulation/wasmsimulatorinterface.h new file mode 100644 index 000000000..c45ea9b7f --- /dev/null +++ b/companion/src/simulation/wasmsimulatorinterface.h @@ -0,0 +1,217 @@ +/* + * Copyright (C) EdgeTX + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#pragma once + +#include "simulatorinterface.h" + +#include +#include +#include +#include + +#include "wasm_export.h" + +#include + +class WasmSimulatorInterface : public SimulatorInterface +{ + Q_OBJECT + + public: + WasmSimulatorInterface(const QString & wasmPath, const QString & boardName, + Board::Type boardType); + virtual ~WasmSimulatorInterface(); + + QString name() override; + bool isRunning() override; + void readRadioData(QByteArray & dest) override {} + uint8_t * getLcd() override; + uint8_t getSensorInstance(uint16_t id, uint8_t defaultValue = 0) override; + uint16_t getSensorRatio(uint16_t id) override; + const int getCapability(Capability cap) override; + + public slots: + void init() override; + void start(const char * filename = nullptr, bool tests = true) override; + void stop() override; + void setSdPath(const QString & sdPath = "", + const QString & settingsPath = "") override; + void setVolumeGain(const int value) override; + void setRadioData(const QByteArray & data) override {} + void setAnalogValue(uint8_t index, int16_t value) override; + void setKey(uint8_t key, bool state) override; + void setSwitch(uint8_t swtch, int8_t state) override; + void setTrim(unsigned int idx, int value) override; + void setTrimSwitch(uint8_t trim, bool state) override; + void setTrainerInput(unsigned int inputNumber, int16_t value) override; + void setInputValue(int type, uint8_t index, int16_t value) override; + void rotaryEncoderEvent(int steps) override; + void touchEvent(int type, int x, int y) override; + void lcdFlushed() override; + void setTrainerTimeout(uint16_t ms) override; + void sendInternalModuleTelemetry(const quint8 protocol, + const QByteArray data) override; + void sendExternalModuleTelemetry(const quint8 protocol, + const QByteArray data) override; + void setLuaStateReloadPermanentScripts() override; + void addTracebackDevice(QIODevice * device) override; + void removeTracebackDevice(QIODevice * device) override; + void receiveAuxSerialData(const quint8 port_num, + const QByteArray & data) override; + + // Called by WASM import simuGetAnalog + int16_t getAnalogValue(uint8_t index); + + // Called by WASM import simuTrace + void writeTrace(const char * text); + + // Called by WASM import simuQueueAudio + void queueAudio(const uint8_t * buf, uint32_t len); + + // Called by WASM import simuLcdNotify (from WAMR thread) + void notifyLcdReady(); + + protected slots: + void run(); + void onLcdNotify(); + + protected: + bool loadModule(); + void unloadModule(); + bool resolveExports(); + void refreshLcd(); + void checkOutputsChanged(); + void initAudio(); + void deinitAudio(); + + QString m_wasmPath; + QString m_boardName; + Board::Type m_boardType; + QString m_sdPath; + QString m_settingsPath; + + QTimer * m_timer10ms = nullptr; + std::atomic m_lcdNotified{false}; + QMutex m_mutex; + QMutex m_mtxTbDevices; + QVector m_tracebackDevices; + bool m_stopRequested = false; + + // Audio + SDL_AudioDeviceID m_audioDevice = 0; + int m_volumeGain = SDL_MIX_MAXVOLUME; + + // Host-side analog values (polled by WASM via simuGetAnalog) + static constexpr int MAX_ANALOGS = 32; + int16_t m_analogValues[MAX_ANALOGS] = {}; + + // Cached output values for change detection + TxOutputs m_lastOutputs; + bool m_resetOutputsData = true; + + // Persistent WASM buffer for bulk copies (allocated once in init) + uint32_t m_wasmScratchBuf = 0; + uint32_t m_wasmScratchSize = 0; + + // Host-side LCD buffer + persistent WASM-side copy buffer + uint8_t * m_lcdBuffer = nullptr; + uint32_t m_lcdBufferSize = 0; + uint32_t m_wasmLcdBuf = 0; + uint32_t m_lcdWidth = 0; + uint32_t m_lcdHeight = 0; + uint32_t m_lcdDepth = 0; + + // WAMR handles + wasm_module_t m_module = nullptr; + wasm_module_inst_t m_moduleInst = nullptr; + wasm_exec_env_t m_execEnv = nullptr; + QByteArray m_wasmBinary; + + // Cached WASM function references + wasm_function_inst_t m_fnInit = nullptr; + wasm_function_inst_t m_fnStart = nullptr; + wasm_function_inst_t m_fnStop = nullptr; + wasm_function_inst_t m_fnIsRunning = nullptr; + wasm_function_inst_t m_fnSetKey = nullptr; + wasm_function_inst_t m_fnSetTrim = nullptr; + wasm_function_inst_t m_fnSetSwitch = nullptr; + wasm_function_inst_t m_fnLcdChanged = nullptr; + wasm_function_inst_t m_fnLcdCopy = nullptr; + wasm_function_inst_t m_fnLcdGetWidth = nullptr; + wasm_function_inst_t m_fnLcdGetHeight = nullptr; + wasm_function_inst_t m_fnLcdGetDepth = nullptr; + wasm_function_inst_t m_fnTouchDown = nullptr; + wasm_function_inst_t m_fnTouchUp = nullptr; + wasm_function_inst_t m_fnFatfsSetPaths = nullptr; + wasm_function_inst_t m_fnRotaryEncoderEvent = nullptr; + wasm_function_inst_t m_fnGetCapability = nullptr; + + // Output value exports (bulk copy) + wasm_function_inst_t m_fnGetNumChannels = nullptr; + wasm_function_inst_t m_fnCopyChannelOutputs = nullptr; + wasm_function_inst_t m_fnCopyMixOutputs = nullptr; + wasm_function_inst_t m_fnGetNumLogicalSwitches = nullptr; + wasm_function_inst_t m_fnCopyLogicalSwitches = nullptr; + wasm_function_inst_t m_fnGetTrimValue = nullptr; + wasm_function_inst_t m_fnGetTrimRange = nullptr; + wasm_function_inst_t m_fnGetFlightMode = nullptr; + wasm_function_inst_t m_fnGetNumGVars = nullptr; + wasm_function_inst_t m_fnGetNumFlightModes = nullptr; + wasm_function_inst_t m_fnGetGVar = nullptr; + + // Phase 4: telemetry, trim, lua, lcd, trainer + wasm_function_inst_t m_fnSetTrimValue = nullptr; + wasm_function_inst_t m_fnSendTelemetry = nullptr; + wasm_function_inst_t m_fnLuaReloadPermanentScripts = nullptr; + wasm_function_inst_t m_fnLcdFlushed = nullptr; + wasm_function_inst_t m_fnGetMaxTrainerChannels = nullptr; + wasm_function_inst_t m_fnCopyTrainerInput = nullptr; + wasm_function_inst_t m_fnSetTrainerTimeout = nullptr; + + // Backlight and function switch LEDs + wasm_function_inst_t m_fnGetBacklightState = nullptr; + wasm_function_inst_t m_fnGetNumCustomSwitches = nullptr; + wasm_function_inst_t m_fnGetCustomSwitchColor = nullptr; + wasm_function_inst_t m_fnGetCustomSwitchIndex = nullptr; + + // Trainer input buffer (flushed in bulk via run()) + static constexpr int MAX_TRAINER_CH = 16; + int16_t m_trainerValues[MAX_TRAINER_CH] = {}; + bool m_trainerDirty = false; + + // Cached function switch LED colors for change detection + static constexpr int MAX_FS_LEDS = 8; + uint32_t m_lastFSLedColors[MAX_FS_LEDS] = {}; + + wasm_function_inst_t m_fnMalloc = nullptr; + wasm_function_inst_t m_fnFree = nullptr; +}; + +class WasmSimulatorFactory : public SimulatorFactory +{ + public: + WasmSimulatorFactory(const QString & wasmPath, const QString & boardName, + Board::Type boardType); + + QString name() override; + Board::Type type() override; + SimulatorInterface * create() override; + + private: + QString m_wasmPath; + QString m_boardName; + Board::Type m_boardType; +}; diff --git a/companion/src/simulation/widgets/radioknobwidget.h b/companion/src/simulation/widgets/radioknobwidget.h index c0c1f37ca..342ac4bac 100644 --- a/companion/src/simulation/widgets/radioknobwidget.h +++ b/companion/src/simulation/widgets/radioknobwidget.h @@ -71,18 +71,16 @@ class RadioKnobWidget : public RadioWidget m_dial->setFixedSize(QSize(42, 42)); m_dial->setNotchesVisible(true); + m_dial->setMinimum(-1024); + m_dial->setMaximum(1024); + if (m_stepSize > 1) { - m_dial->setMinimum(0); - m_dial->setMaximum(2048); - // this is a bit of a hack to get the notch markers to display correctly - // the actual notches/value are constrained in setValue() + // notch markers for multipos — actual values are constrained in setValueFromDial() m_dial->setSingleStep(m_stepSize / 10); m_dial->setPageStep(m_stepSize); m_dial->setNotchTarget(5.7); } else { - m_dial->setMinimum(-1024); - m_dial->setMaximum(1024); m_dial->setPageStep(128); m_dial->setNotchTarget(64); } @@ -99,14 +97,15 @@ class RadioKnobWidget : public RadioWidget { int v = value; if (m_stepSize > 1) { - v = ((v + m_stepSize / 2) / m_stepSize) * m_stepSize; - // Fix values to account for lack of precision from using integer step size - // This makes the values more symmetrical around the center point - // Note: this is specific to the 6 position switch which is currently the only use case here - if (v > 1024) v += 3; + // Round to nearest step position relative to minimum + int offset = v - m_dial->minimum(); + offset = ((offset + m_stepSize / 2) / m_stepSize) * m_stepSize; + v = m_dial->minimum() + offset; + // Fix values to account for lack of precision from integer step size + // (2048 / 5 = 409 leaves a remainder of 3). Add correction to the + // upper half so positions are symmetric around center. + if (v > 0) v += 3; if (v != value) { - // If the desired value is different then update the slider position - // Note: this will trigger another value changed event call back into this function, at which time setValue will be called below m_dial->setValue(v); return; } diff --git a/companion/src/simulator.cpp b/companion/src/simulator.cpp index c5f78a98f..c354873e9 100644 --- a/companion/src/simulator.cpp +++ b/companion/src/simulator.cpp @@ -312,14 +312,14 @@ int main(int argc, char *argv[]) if (cliResult == CommandLineExitErr) return finish(1); - // TODO : defaults should be set in Profile::init() - if (simOptions.firmwareId.isEmpty()) { + // Always use profile's firmware type and SD path as authoritative source — + // the copies in simulatorOptions can become stale. + if (!g.profile[profileId].fwType().isEmpty()) simOptions.firmwareId = g.profile[profileId].fwType(); - } if (simOptions.dataFolder.isEmpty()) simOptions.dataFolder = g.eepromDir(); - if (simOptions.sdPath.isEmpty()) + if (!g.profile[profileId].sdPath().isEmpty()) simOptions.sdPath = g.profile[profileId].sdPath(); // DO NOT use saved simulatorId as could be changed in later releases diff --git a/radio/src/CMakeLists.txt b/radio/src/CMakeLists.txt index 0b376f2ab..4cc552e0c 100644 --- a/radio/src/CMakeLists.txt +++ b/radio/src/CMakeLists.txt @@ -34,7 +34,6 @@ option(SIMU_LUA_COMPILER "Pre-compile and save Lua scripts in simulator." ON) option(FAS_PROTOTYPE "Support of old FAS prototypes (different resistors)" OFF) option(RAS "RAS (SWR) enabled" ON) option(TEMPLATES "Model templates menu" OFF) -option(TRACE_SIMPGMSPACE "Turn on traces in simpgmspace.cpp" ON) option(TRACE_LUA_INTERNALS "Turn on traces for Lua internals" OFF) option(TRACE_AUDIO "Traces audio enabled" OFF) option(DEBUG_SEGGER_RTT "Debug output to Segger RTT" OFF) @@ -575,7 +574,10 @@ if(NATIVE_BUILD) set_property(TARGET radiolib_native PROPERTY POSITION_INDEPENDENT_CODE ON) add_subdirectory(targets/simu) - add_subdirectory(tests) + + if(NOT WASI) + add_subdirectory(tests) + endif() endif() set(SRC ${SRC} ${FIRMWARE_SRC}) @@ -595,6 +597,8 @@ if(NOT USE_UNSUPPORTED_TOOLCHAIN AND NOT CMAKE_CXX_COMPILER_VERSION VERSION_EQUA message(FATAL_ERROR "Only Arm GNU Toolchain version 14.2.rel1 is supported!!") endif() +enable_language(ASM) + set(CMAKE_C_FLAGS "${FIRMWARE_C_FLAGS}") set(CMAKE_C_FLAGS_DEBUG "${FIRMWARE_C_FLAGS_DEBUG}") set(CMAKE_C_FLAGS_RELEASE "${FIRMWARE_C_FLAGS}") diff --git a/radio/src/audio.cpp b/radio/src/audio.cpp index 5a082e286..29aa2da35 100644 --- a/radio/src/audio.cpp +++ b/radio/src/audio.cpp @@ -730,13 +730,6 @@ void AudioQueue::playTone(uint16_t freq, uint16_t len, uint16_t pause, uint8_t f void AudioQueue::playFile(const char * filename, uint8_t flags, uint8_t id, int8_t fragmentVolume) { -#if defined(SIMU) - TRACE("playFile(\"%s\", flags=%x, id=%d fragmentVolume=%d ee_general=%d)", filename, flags, id, fragmentVolume, g_eeGeneral.wavVolume); - if (strlen(filename) > AUDIO_FILENAME_MAXLEN) { - TRACE("file name too long! maximum length is %d characters", AUDIO_FILENAME_MAXLEN); - } -#endif - if (!sdMounted()) return; diff --git a/radio/src/edgetx.cpp b/radio/src/edgetx.cpp index a38bab088..73a606587 100644 --- a/radio/src/edgetx.cpp +++ b/radio/src/edgetx.cpp @@ -1536,7 +1536,7 @@ void edgeTxInit() #if defined(AUDIO) currentSpeakerVolume = requiredSpeakerVolume = - g_eeGeneral.speakerVolume + VOLUME_LEVEL_DEF; + limit(0, g_eeGeneral.speakerVolume + VOLUME_LEVEL_DEF, VOLUME_LEVEL_MAX); #if !defined(SOFTWARE_VOLUME) audioSetVolume(currentSpeakerVolume); #endif diff --git a/radio/src/edgetx.h b/radio/src/edgetx.h index 6350b13f7..029cad377 100644 --- a/radio/src/edgetx.h +++ b/radio/src/edgetx.h @@ -27,11 +27,6 @@ #include "edgetx_helpers.h" #include "touch.h" #include "switches.h" - -#if defined(SIMU) -#include "targets/simu/simpgmspace.h" -#endif - #include "board.h" #if !defined(SIMU) diff --git a/radio/src/functions.cpp b/radio/src/functions.cpp index 0d71e40a6..37a6933fa 100644 --- a/radio/src/functions.cpp +++ b/radio/src/functions.cpp @@ -23,8 +23,7 @@ #include "switches.h" #include "hal/audio_driver.h" - -#include "boards/generic_stm32/rgb_leds.h" +#include "os/time.h" #if defined(COLORLCD) void setRequestedMainView(uint8_t view); @@ -380,9 +379,10 @@ void evalFunctions(CustomFunctionData * functions, CustomFunctionsContext & func case FUNC_PUSH_CUST_SWITCH: if (CFN_PARAM(cfn)) { // Duration is set if (! CFN_VAL2(cfn) ) { // Duration not started yet - CFN_VAL2(cfn) = timersGetMsTick() + CFN_PARAM(cfn) * 100; + CFN_VAL2(cfn) = time_get_ms() + CFN_PARAM(cfn) * 100; g_model.cfsSetSFState(CFN_CS_INDEX(cfn), 1); - } else if (timersGetMsTick() < (uint32_t)CFN_VAL2(cfn) ) { // Still within push duration + } + else if (time_get_ms() < (uint32_t)CFN_VAL2(cfn) ) { // Still within push duration g_model.cfsSetSFState(CFN_CS_INDEX(cfn), 1); } } else { // No duration set @@ -457,7 +457,7 @@ void evalFunctions(CustomFunctionData * functions, CustomFunctionsContext & func #if defined(FUNCTION_SWITCHES) if (CFN_FUNC(cfn) == FUNC_PUSH_CUST_SWITCH) { // Handling duration after function is active - if (timersGetMsTick() < (uint32_t)CFN_VAL2(cfn)) { + if (time_get_ms() < (uint32_t)CFN_VAL2(cfn)) { g_model.cfsSetSFState(CFN_CS_INDEX(cfn), 1); } else { diff --git a/radio/src/gps_nmea.cpp b/radio/src/gps_nmea.cpp index 00fa047b2..7f963cfe4 100644 --- a/radio/src/gps_nmea.cpp +++ b/radio/src/gps_nmea.cpp @@ -256,11 +256,9 @@ bool gpsNewFrameNMEA(char c) gpsData.numSat = gps_Msg.numSat; gpsData.hdop = gps_Msg.hdop; if (gps_Msg.fix) { - __disable_irq(); // do the atomic update of lat/lon gpsData.latitude = gps_Msg.latitude; gpsData.longitude = gps_Msg.longitude; gpsData.altitude = gps_Msg.altitude; - __enable_irq(); } break; case FRAME_RMC: diff --git a/radio/src/gps_ubx.cpp b/radio/src/gps_ubx.cpp index 90ee23c3c..4b04969e8 100644 --- a/radio/src/gps_ubx.cpp +++ b/radio/src/gps_ubx.cpp @@ -158,11 +158,9 @@ static void gpsProcessMessage(uint16_t msg_type, uint16_t msg_len, gpsData.speed = pvt->gSpeed / 100; // speed in 0.1m/s gpsData.groundCourse = pvt->headMot / 10000; // degrees * 10 if (gpsData.fix) { - __disable_irq(); // do the atomic update of lat/lon gpsData.longitude = pvt->lon / 10; // degrees * 1.000.000 gpsData.latitude = pvt->lat / 10; // degrees * 1.000.000 gpsData.altitude = pvt->hMSL / 100; // altitude in 0.1m - __enable_irq(); } #if defined(RTCLOCK) diff --git a/radio/src/gui/colorlcd/libui/bitmapbuffer.cpp b/radio/src/gui/colorlcd/libui/bitmapbuffer.cpp index 13707fce0..0d6531390 100644 --- a/radio/src/gui/colorlcd/libui/bitmapbuffer.cpp +++ b/radio/src/gui/colorlcd/libui/bitmapbuffer.cpp @@ -529,6 +529,12 @@ coord_t BitmapBuffer::drawSizedText(coord_t x, coord_t y, const char *s, return ((flags & RIGHT) ? orig_pos : pos) - offsetX; } +coord_t BitmapBuffer::drawText(coord_t x, coord_t y, const char* s, LcdFlags flags) +{ + if (!s) return x; + return drawSizedText(x, y, s, strlen(s), flags); +} + // Resize and convert to LVGL image data format with alpha blending to // background color void BitmapBuffer::resizeToLVGL(coord_t w, coord_t h) diff --git a/radio/src/gui/colorlcd/libui/bitmapbuffer.h b/radio/src/gui/colorlcd/libui/bitmapbuffer.h index 3eccce8a8..e6a9a69c6 100644 --- a/radio/src/gui/colorlcd/libui/bitmapbuffer.h +++ b/radio/src/gui/colorlcd/libui/bitmapbuffer.h @@ -132,11 +132,7 @@ class BitmapBuffer coord_t drawSizedText(coord_t x, coord_t y, const char* s, uint8_t len, LcdFlags flags = 0); - coord_t drawText(coord_t x, coord_t y, const char* s, LcdFlags flags = 0) - { - if (!s) return x; - return drawSizedText(x, y, s, strlen(s), flags); - } + coord_t drawText(coord_t x, coord_t y, const char* s, LcdFlags flags = 0); void drawBitmap(coord_t x, coord_t y, const BitmapBuffer* bmp, coord_t srcx = 0, coord_t srcy = 0, coord_t srcw = 0, diff --git a/radio/src/gui/colorlcd/libui/dialog.cpp b/radio/src/gui/colorlcd/libui/dialog.cpp index be5ec9265..7719af567 100644 --- a/radio/src/gui/colorlcd/libui/dialog.cpp +++ b/radio/src/gui/colorlcd/libui/dialog.cpp @@ -176,9 +176,7 @@ LabelDialog::LabelDialog(const char *label, int length, const char* title, std::function _saveHandler) : ModalWindow(false), saveHandler(std::move(_saveHandler)) { - assert(length <= MAX_LABEL_LENGTH); - - strncpy(this->label, label, length); + strncpy(this->label, label, std::min(length, MAX_LABEL_LENGTH)); this->label[length] = '\0'; auto form = new Window(this, rect_t{}); diff --git a/radio/src/gui/colorlcd/libui/mainwindow.cpp b/radio/src/gui/colorlcd/libui/mainwindow.cpp index ab5470b22..127a00f35 100644 --- a/radio/src/gui/colorlcd/libui/mainwindow.cpp +++ b/radio/src/gui/colorlcd/libui/mainwindow.cpp @@ -25,12 +25,11 @@ #include "layout.h" #include "LvglWrapper.h" #include "etx_lv_theme.h" -#include "os/sleep.h" -#include "sdcard.h" #include "view_main.h" -// timers_driver.h -uint32_t timersGetMsTick(); +#include "os/sleep.h" +#include "os/time.h" +#include "sdcard.h" MainWindow* MainWindow::_instance = nullptr; @@ -63,7 +62,7 @@ void MainWindow::run(bool trash) LvglWrapper::instance()->run(); #if defined(DEBUG_WINDOWS) - auto start = timersGetMsTick(); + auto start = time_get_ms(); #endif if (widgetRefreshEnable) @@ -85,7 +84,7 @@ void MainWindow::run(bool trash) emptyTrash(); #if defined(DEBUG_WINDOWS) - auto delta = timersGetMsTick() - start; + auto delta = time_get_ms() - start; if (delta > 10) { TRACE_WINDOWS("MainWindow::run took %dms", delta); } diff --git a/radio/src/gui/colorlcd/module/pxx1_settings.cpp b/radio/src/gui/colorlcd/module/pxx1_settings.cpp index eb9ad050b..4e236a675 100644 --- a/radio/src/gui/colorlcd/module/pxx1_settings.cpp +++ b/radio/src/gui/colorlcd/module/pxx1_settings.cpp @@ -27,6 +27,7 @@ #define SET_DIRTY() storageDirty(EE_MODEL) +#if defined(EXTERNAL_ANTENNA) PXX1AntennaSettings::PXX1AntennaSettings(Window* parent, const FlexGridLayout& g, uint8_t moduleIdx) : @@ -62,3 +63,4 @@ PXX1AntennaSettings::PXX1AntennaSettings(Window* parent, antennaChoice->setAvailableHandler( [=](int8_t mode) { return mode != ANTENNA_MODE_PER_MODEL; }); } +#endif diff --git a/radio/src/gui/common/stdlcd/menus_common.cpp b/radio/src/gui/common/stdlcd/menus_common.cpp index f185a902e..75630e71c 100644 --- a/radio/src/gui/common/stdlcd/menus_common.cpp +++ b/radio/src/gui/common/stdlcd/menus_common.cpp @@ -30,8 +30,8 @@ uint8_t menuLevel = 0; void popMenu() { killEvents(KEY_EXIT); + if (menuLevel == 0) return; - assert(menuLevel > 0); menuLevel = menuLevel - 1; menuEvent = EVT_ENTRY_UP; // TODO ? AUDIO_KEY_PRESS(); @@ -57,6 +57,7 @@ void chainMenu(MenuHandlerFunc newMenu) void pushMenu(MenuHandlerFunc newMenu) { killAllEvents(); + if (menuLevel == DIM(menuHandlers) - 1) return; if (menuLevel == 0) { if (newMenu == menuTabGeneral[0].menuFunc) @@ -71,8 +72,6 @@ void pushMenu(MenuHandlerFunc newMenu) menuLevel++; - assert(menuLevel < DIM(menuHandlers)); - menuHandlers[menuLevel] = newMenu; menuEvent = EVT_ENTRY; // ? AUDIO_KEY_PRESS(); diff --git a/radio/src/hal/adc_driver.cpp b/radio/src/hal/adc_driver.cpp index 7cf26e888..91118e17e 100644 --- a/radio/src/hal/adc_driver.cpp +++ b/radio/src/hal/adc_driver.cpp @@ -366,8 +366,6 @@ static uint32_t apply_low_pass_filter(uint32_t v, uint32_t v_prev, static uint32_t apply_calibration(const CalibData* calib, uint32_t v) { - // Simu uses normed inputs -#if !defined(SIMU) // Apply calibration relative to mid-point int32_t s = v - 2 * calib->mid; s = s * (int32_t)RESX / @@ -383,10 +381,7 @@ static uint32_t apply_calibration(const CalibData* calib, uint32_t v) s = 4 * RESX; } - v = s; -#endif - - return v; + return s; } static uint32_t apply_multipos(const StepsCalibData* calib, uint32_t v) @@ -407,6 +402,24 @@ static uint32_t apply_multipos(const StepsCalibData* calib, uint32_t v) return ANAFILT_MAX; } +#if defined(SIMU) +// In simulation, assume equally spaced positions across full ADC range +static uint32_t apply_multipos_simu(uint32_t v) +{ + constexpr uint32_t ALPHA_MULT = JITTER_ALPHA * ANALOG_MULTIPLIER; + constexpr uint32_t ANAFILT_MAX = 2 * RESX * ALPHA_MULT; + constexpr uint32_t N = XPOTS_MULTIPOS_COUNT; + constexpr uint32_t COUNT = N - 1; + + uint32_t pos = v * N / (ANAFILT_MAX + 1); + if (pos > COUNT) pos = COUNT; + + return (pos < COUNT) + ? (pos * (ANAFILT_MAX + ALPHA_MULT)) / COUNT + : ANAFILT_MAX; +} +#endif + void getADC() { auto max_analogs = adcGetMaxInputs(ADC_INPUT_ALL); @@ -435,14 +448,17 @@ void getADC() bool is_flex_input = (x >= pot_offset) && (x < pot_offset + max_pots); bool is_multipos = is_flex_input && IS_POT_MULTIPOS(x - pot_offset); - // 1st: apply calibration uint32_t v = getAnalogValue(x); +#if !defined(SIMU) + // Apply hardware calibration (not needed in simulation: + // the host already provides normalized ADC-range values) if (x < max_calib_analogs && !is_multipos) { v = apply_calibration(&g_eeGeneral.calib[x], v); } +#endif - // 2nd: apply inversion + // Apply inversion if (x < pot_offset && getStickInversion(inputMappingConvertMode(x))) { v = 4 * RESX - v; } @@ -451,14 +467,18 @@ void getADC() v = 4 * RESX - v; } - // 3rd: apply filtering + // Apply filtering s_anaFilt[x] = apply_low_pass_filter(v, s_anaFilt[x], x < max_mains); if (is_multipos) { +#if defined(SIMU) + s_anaFilt[x] = apply_multipos_simu(s_anaFilt[x]); +#else const auto* calib = (const StepsCalibData*)&g_eeGeneral.calib[x]; if (IS_MULTIPOS_CALIBRATED(calib)) { s_anaFilt[x] = apply_multipos(calib, s_anaFilt[x]); } +#endif } #if defined(JITTER_MEASURE) diff --git a/radio/src/mixer.cpp b/radio/src/mixer.cpp index 70661b93b..147b7a8fc 100644 --- a/radio/src/mixer.cpp +++ b/radio/src/mixer.cpp @@ -1200,7 +1200,6 @@ void evalMixes(uint8_t tick10ms) weight += fp_act[p]; } } - assert(weight); mixerCurrentFlightMode = fm; } else { @@ -1235,7 +1234,8 @@ void evalMixes(uint8_t tick10ms) if (g_eeGeneral.volumeSrc) { calcVolumeValue(g_eeGeneral.volumeSrc); } else { - requiredSpeakerVolume = g_eeGeneral.speakerVolume + VOLUME_LEVEL_DEF; + requiredSpeakerVolume = + limit(0, g_eeGeneral.speakerVolume + VOLUME_LEVEL_DEF, VOLUME_LEVEL_MAX); } } #endif diff --git a/radio/src/os/time_native.cpp b/radio/src/os/time_native.cpp index 59f0ad370..30961d679 100644 --- a/radio/src/os/time_native.cpp +++ b/radio/src/os/time_native.cpp @@ -23,11 +23,12 @@ #include -extern uint64_t simuTimerMicros(void); - uint32_t time_get_ms() { - return simuTimerMicros() / 1000; + static auto _start = std::chrono::steady_clock::now();; + auto now = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast(now - _start); + return duration.count(); } time_point_t time_point_now() diff --git a/radio/src/switches.cpp b/radio/src/switches.cpp index 69ddb9a4d..d0ac61260 100644 --- a/radio/src/switches.cpp +++ b/radio/src/switches.cpp @@ -403,8 +403,14 @@ void getSwitchesPosition(bool startup) if (IS_POT_MULTIPOS(i)) { auto analog_idx = offset + i; StepsCalibData * calib = (StepsCalibData *) &g_eeGeneral.calib[analog_idx]; +#if defined(SIMU) + { + uint8_t count = XPOTS_MULTIPOS_COUNT - 1; +#else if (IS_MULTIPOS_CALIBRATED(calib)) { - uint8_t pos = anaIn(analog_idx) / (2 * RESX / calib->count); + uint8_t count = calib->count; +#endif + uint8_t pos = anaIn(analog_idx) / (2 * RESX / count); uint8_t previousPos = potsPos[i] >> 4; uint8_t previousStoredPos = potsPos[i] & 0x0F; if (startup) { @@ -839,9 +845,15 @@ swsrc_t getMovedSwitch() for (int i = 0; i < MAX_POTS; i++) { if (IS_POT_MULTIPOS(i)) { StepsCalibData * calib = (StepsCalibData *) &g_eeGeneral.calib[MAX_STICKS + i]; +#if defined(SIMU) + { + uint8_t count = XPOTS_MULTIPOS_COUNT - 1; +#else if (IS_MULTIPOS_CALIBRATED(calib)) { + uint8_t count = calib->count; +#endif uint8_t prev = potsPos[i] & 0x0F; - uint8_t next = anaIn(MAX_STICKS + i) / (2 * RESX / calib->count); + uint8_t next = anaIn(MAX_STICKS + i) / (2 * RESX / count); if (prev != next) { result = SWSRC_FIRST_MULTIPOS_SWITCH + i * XPOTS_MULTIPOS_COUNT + next; } diff --git a/radio/src/targets/common/arm/stm32/board_common.h b/radio/src/targets/common/arm/stm32/board_common.h index f4e9067c4..ea64638dc 100644 --- a/radio/src/targets/common/arm/stm32/board_common.h +++ b/radio/src/targets/common/arm/stm32/board_common.h @@ -38,9 +38,4 @@ #endif #endif - -#if defined(SIMU) -#include "../simu/simpgmspace.h" -#endif - #include "delays_driver.h" diff --git a/radio/src/targets/simu/CMakeLists.txt b/radio/src/targets/simu/CMakeLists.txt index cd601777a..6ec880ba0 100644 --- a/radio/src/targets/simu/CMakeLists.txt +++ b/radio/src/targets/simu/CMakeLists.txt @@ -5,8 +5,27 @@ if(NOT SIMU_TARGET) return() endif() +if(MINGW) + # struct packing breaks on MinGW w/out -mno-ms-bitfields: + # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=52991 + # http://stackoverflow.com/questions/24015852/struct-packing-and-alignment-with-mingw + # + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mno-ms-bitfields") +endif() + +set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -O0") +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0") + +if(ASAN AND NOT WASI) + set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fsanitize=address") + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address") +endif() + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WARNING_FLAGS}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WARNING_FLAGS}") + set(SIMU_DRIVERS - simpgmspace.cpp + simulib.cpp simufatfs.cpp simudisk.cpp simulcd.cpp @@ -20,10 +39,9 @@ set(SIMU_DRIVERS bt_driver.cpp timers_driver.cpp abnormal_reboot.cpp - ) +) set(HW_DESC_JSON ${FLAVOUR}.json) -# AddHardwareDefTarget(${HW_DESC_JSON}) AddHWGenTarget(${HW_DESC_JSON} simu_keys simu_keys.inc) AddHWGenTarget(${HW_DESC_JSON} simu_switches simu_switches.inc) AddHWGenTarget(${HW_DESC_JSON} hal_adc_inputs hal_adc_inputs.inc) @@ -32,35 +50,21 @@ set(SIMU_DRIVERS ${SIMU_DRIVERS} ${CMAKE_CURRENT_BINARY_DIR}/simu_keys.inc ${CMAKE_CURRENT_BINARY_DIR}/simu_switches.inc ${CMAKE_CURRENT_BINARY_DIR}/hal_adc_inputs.inc - ${RADIO_SRC_DIR}/boards/hw_defs/${HW_DESC_JSON} ) + include_directories(${CMAKE_CURRENT_BINARY_DIR}) -if(SIMU_LUA_COMPILER) - add_definitions(-DLUA_COMPILER) -endif() - -if(SIMU_BOOTLOADER) - add_definitions(-DSIMU_BOOTLOADER) - set(SIMU_DRIVERS ${SIMU_DRIVERS} - ../../bootloader/bin_files.cpp - ../../bootloader/boot.cpp - # TODO - how to handle this for B&W (not currently used)? - ../../gui/colorlcd/boot_menu.cpp - ../../thirdparty/lz4/lz4.c - ) -endif() - # Pack all simu driver sources into an object lib add_library(simu_drivers OBJECT EXCLUDE_FROM_ALL - ${SIMU_DRIVERS}) + ${SIMU_DRIVERS} +) get_property(SIMU_SRC_OPTIONS TARGET radiolib_native - PROPERTY INTERFACE_COMPILE_OPTIONS) + PROPERTY INTERFACE_COMPILE_OPTIONS +) target_compile_options(simu_drivers PRIVATE ${SIMU_SRC_OPTIONS}) - set_property(TARGET simu_drivers PROPERTY POSITION_INDEPENDENT_CODE ON) if(SIMU_AUX) @@ -71,101 +75,64 @@ endif() set(SIMU_SRC $ $ - ) +) set(SIMU_SRC ${SIMU_SRC} PARENT_SCOPE) # Set the options as well in parent scope to be used by unit tests set(SIMU_SRC_OPTIONS ${SIMU_SRC_OPTIONS} PARENT_SCOPE) -if(Qt6Widgets_FOUND) - set(SIMULATOR_FLAVOUR edgetx-${FLAVOUR}) - set(SIMULATOR_TARGET ${SIMULATOR_FLAVOUR}-simulator) - - add_definitions(-DSIMULATOR_FLAVOUR="${SIMULATOR_FLAVOUR}") - include_directories(${COMPANION_SRC_DIRECTORY} ${COMPANION_SRC_DIRECTORY}/simulation) - - qt_wrap_cpp(SIMULATOR_SRC - ${COMPANION_SRC_DIRECTORY}/simulation/simulatorinterface.h - opentxsimulator.h - ) - list(APPEND SIMULATOR_SRC ${SIMU_SRC} opentxsimulator.cpp) - - add_library(${SIMULATOR_TARGET} SHARED ${SIMULATOR_SRC}) - set_target_properties(${SIMULATOR_TARGET} PROPERTIES PREFIX "lib") - - if(TARGET SDL2::SDL2) - set(SDL2_LIBRARIES SDL2::SDL2) - target_sources(${SIMULATOR_TARGET} PRIVATE simuaudio.cpp) - else() - target_sources(${SIMULATOR_TARGET} PRIVATE no_audio.cpp) - endif() - - target_compile_options(${SIMULATOR_TARGET} PRIVATE ${SIMU_SRC_OPTIONS}) - target_compile_definitions(${SIMULATOR_TARGET} PUBLIC ${APP_COMMON_DEFINES}) - target_link_libraries(${SIMULATOR_TARGET} ${SDL2_LIBRARIES} Qt::Core Qt::SerialPort) - - # Remove debug symbols on release builds - if("${CMAKE_BUILD_TYPE}" STREQUAL "Release") - add_custom_command(TARGET ${SIMULATOR_TARGET} POST_BUILD - COMMAND ${CMAKE_STRIP} -x $) - endif() - - add_custom_command( - TARGET ${SIMULATOR_TARGET} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/plugins/ - COMMAND ${CMAKE_COMMAND} -E copy - $ ${CMAKE_BINARY_DIR}/plugins/ +if(WASI) + # strip debug info in Release builds + list(APPEND CMAKE_EXE_LINKER_FLAGS_RELEASE + -Wl,--strip-debug ) - # Introduced at 2.5 - TODO replace with a code change - # When compiled shared simulators using libopenui and Companion have classes named MainWindow - # When the simulator is launched from Companion the runtime dynamic linking can result in Companion overiding the simulator - # and crashing the simulator under some OSes - # The flag forces the runtime linker to always look for references in the simulator first - if (NOT CLANG) - target_link_options(${SIMULATOR_TARGET} PRIVATE "LINKER:-Bsymbolic") + set(WASI_COMPILE_OPTIONS + -Os + -fno-exceptions + -fno-rtti + -ffunction-sections + -fdata-sections + # setjmp / longjmp handling + -mllvm -wasm-enable-sjlj + ) + + if(USE_LTO) + list(APPEND WASI_COMPILE_OPTIONS -flto=thin) endif() - # - add_custom_target(libsimulator DEPENDS ${SIMULATOR_TARGET}) - # Prepare the "all-simu-libs" target to build simulator libraries for *every* supported PCB type (PCB_TYPES list) - # (a fast build machine or corresponding amount of patience is recommended for this target). - if(${CMAKE_GENERATOR} MATCHES ".*Unix Makefiles$") - # Ensure parallel builds with GCC make (try with MAKEFLAGS but this seems to be getting clobbered somewhere). - # otherwise 'make' may complain: "warning: jobserver unavailable: using -j1..." and build single-threaded. (Ninja always rocks this.) - set(mkflags "$ENV{MAKEFLAGS}") - if (mkflags STREQUAL "") - set(mkflags "-j") - endif() + target_compile_options(radiolib_native PUBLIC ${WASI_COMPILE_OPTIONS}) + target_compile_options(simu_drivers PUBLIC ${WASI_COMPILE_OPTIONS}) + + add_executable(wasi-module EXCLUDE_FROM_ALL + ${SIMU_SRC} + ) + set_target_properties(wasi-module PROPERTIES + OUTPUT_NAME "edgetx-${FLAVOUR}-simulator" + SUFFIX ".wasm" + ) + + target_compile_options(wasi-module PUBLIC ${WASI_COMPILE_OPTIONS}) + + target_link_options(wasi-module PRIVATE + -Wl,--import-memory + -Wl,--export-memory + -Wl,--max-memory=2147483648 + -Wl,--gc-sections + # enable runtime to check stack / heap + -Wl,--export=__heap_base,--export=__data_end + -Wl,--export=malloc -Wl,--export=free + # no main() + -mexec-model=reactor + # setjmp / longjmp + -Wl,-mllvm,-wasm-enable-sjlj + -lsetjmp + ) + + if(USE_LTO) + target_link_options(wasi-module PRIVATE -Wl,--thinlto-jobs=all) endif() - set(all_libs_cmd ${CMAKE_COMMAND} -E echo "Building all simulator libraries") - foreach(pcb ${PCB_TYPES}) - set(pcbrev "") - if(pcb STREQUAL X12S) - set(pcbrev "-DPCBREV=13") - endif() - set(all_libs_cmd ${all_libs_cmd} && ${CMAKE_COMMAND} -DPCB=${pcb} ${pcbrev} -Wno-dev ${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR} && ${CMAKE_COMMAND} --build . --target libsimulator -- ${mkflags}) - endforeach() - add_custom_target(all-simu-libs COMMAND ${all_libs_cmd} USES_TERMINAL WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) -endif() - -if(MINGW) - # struct packing breaks on MinGW w/out -mno-ms-bitfields: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=52991 & http://stackoverflow.com/questions/24015852/struct-packing-and-alignment-with-mingw - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mno-ms-bitfields") -endif() - -set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -O0") -set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0") - -if(ASAN) - set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fsanitize=address") - set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address") -endif() - -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WARNING_FLAGS}") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WARNING_FLAGS}") - -if(TARGET SDL2::SDL2) +else() add_executable(simu EXCLUDE_FROM_ALL ${SIMU_SRC} @@ -177,15 +144,16 @@ if(TARGET SDL2::SDL2) sdl_simu.cpp ) - include(FetchImgui) target_compile_options(simu PUBLIC -DSIMU) add_png_target(image_assets "assets/images/*.png") add_dependencies(simu image_assets) + # STB set(STB_DIR ${RADIO_SRC_DIR}/thirdparty/stb) target_include_directories(simu PUBLIC ${STB_DIR}) - target_link_libraries(simu PUBLIC imgui) -endif() -PrintTargetReport("simu/libsimulator") + # ImGui + include(FetchImgui) + target_link_libraries(simu PRIVATE imgui) +endif() diff --git a/radio/src/targets/simu/adc_driver.cpp b/radio/src/targets/simu/adc_driver.cpp index 852967710..94fa69b57 100644 --- a/radio/src/targets/simu/adc_driver.cpp +++ b/radio/src/targets/simu/adc_driver.cpp @@ -19,20 +19,24 @@ * GNU General Public License for more details. */ +#include "simulib.h" #include "hal/adc_driver.h" -#include "definitions.h" +#include "definitions.h" #include "myeeprom.h" -#include "board.h" -#include "edgetx.h" +#include "translations/translations.h" #include "hal_adc_inputs.inc" void enableVBatBridge(){} void disableVBatBridge(){} bool isVBatBridgeEnabled(){ return false; } -uint16_t getLuxSensorValue() { return 1024; } +uint16_t getLuxSensorValue() +{ + if (adcGetMaxInputs(ADC_INPUT_LUX) < 1) return 0; + return anaIn(adcGetInputOffset(ADC_INPUT_LUX)); +} uint16_t getBatteryVoltage() { @@ -45,19 +49,22 @@ uint16_t getRTCBatteryVoltage() return 300; } -extern uint16_t simu_get_analog(uint8_t idx); +extern uint16_t simuGetAnalog(uint8_t idx); static bool simu_start_conversion() { - int max_input = adcGetInputOffset(ADC_INPUT_VBAT); + int max_input = adcGetMaxInputs(ADC_INPUT_ALL); for (int i = 0; i < max_input; i++) { - setAnalogValue(i, simu_get_analog(i)); + setAnalogValue(i, simuGetAnalog(i)); } // set batteries default voltages int i = adcGetInputOffset(ADC_INPUT_VBAT); if (i > 0) { - uint16_t volts = (uint16_t)((g_eeGeneral.vBatWarn > 0 ? g_eeGeneral.vBatWarn : BATTERY_WARN) + 5) * 10; // +0.5V and prec2 + // +0.5V and prec2 + uint16_t vBatWarn = BATTERY_WARN; + if (g_eeGeneral.vBatWarn > 0) vBatWarn = g_eeGeneral.vBatWarn; + uint16_t volts = (vBatWarn + 5) * 10; setAnalogValue(i, volts * 2); } diff --git a/radio/src/targets/simu/audio_driver.cpp b/radio/src/targets/simu/audio_driver.cpp index e014bd41d..58d901d55 100644 --- a/radio/src/targets/simu/audio_driver.cpp +++ b/radio/src/targets/simu/audio_driver.cpp @@ -20,7 +20,7 @@ */ #include "audio.h" -#include "simuaudio.h" +#include "simulib.h" #if defined(AUDIO_HP_DETECT_PIN) bool audioHeadphoneDetect() @@ -51,8 +51,6 @@ int simuAudioGetVolume() #endif } -void simuQueueAudio(const uint8_t* data, uint32_t len); - void audioConsumeCurrentBuffer() { auto& fifo = audioQueue.buffersFifo; diff --git a/radio/src/targets/simu/interface.md b/radio/src/targets/simu/interface.md new file mode 100644 index 000000000..0f7cd8278 --- /dev/null +++ b/radio/src/targets/simu/interface.md @@ -0,0 +1,47 @@ +# Simulator Plugin Interface + +## Methods + +```c + +/** + * Simu module functions + */ + +/* Simulator plugin name */ +const char* simu_get_name(); + +/* Start/stop */ +void simuInit(); +void simuStop(); // should be async but isn't +bool simuIsRunning(); + +/* Live data */ +unsigned int simu_get_num_outputs(); +const int16_t* simu_get_outputs(); + +unsigned int simu_get_num_channels(); +const int16_t* simu_get_channels(); + +unsigned int simu_get_num_logical_switches(); +uint32_t* simu_get_logical_switches(); + +uint8_t simu_get_flight_mode(); +uint8_t simu_get_trim_value(uint8_t flight_mode, uint8_t axis); + +uint8_t simu_get_stick_mode(); +uint16_t simu_get_trim_range(); + +void simu_get_flight_mode_name(uint8_t flight_mode, char* buffer, int buflen); + +// TODO: get GVar definitions + values + +/** + * Callbacks / native functions + */ +void simu_read_analogs(uint16_t* analogs, int num_analogs); +uint32_t simu_read_keys(); +uint32_t simu_read_trims(); +void simu_flush_display(bool backlight, uint8_t* buffer, int buflen); +``` + diff --git a/radio/src/targets/simu/opentxsimulator.cpp b/radio/src/targets/simu/opentxsimulator.cpp deleted file mode 100644 index 1befeaff8..000000000 --- a/radio/src/targets/simu/opentxsimulator.cpp +++ /dev/null @@ -1,945 +0,0 @@ -/* - * Copyright (C) EdgeTX - * - * Based on code named - * opentx - https://github.com/opentx/opentx - * th9x - http://code.google.com/p/th9x - * er9x - http://code.google.com/p/er9x - * gruvin9x - http://code.google.com/p/gruvin9x - * - * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - */ - -#include "opentxsimulator.h" -#include "edgetx.h" -#include "simulcd.h" -#include "simuaudio.h" -#include "switches.h" -#include "serial.h" -#include "myeeprom.h" - -#include "hal/adc_driver.h" -#include "hal/rotary_encoder.h" -#include "os/time.h" -#include "boards/generic_stm32/rgb_leds.h" - -#include -#include - -#if !defined(MAX_LOGICAL_SWITCHES) && defined(NUM_CSW) - #define MAX_LOGICAL_SWITCHES NUM_CSW -#endif - -#define GET_SWITCH_BOOL(sw__) getSwitch((sw__), 0); - -#define ETXS_DBG qDebug() << "(" << simuTimerMicros() << "us)" - -int16_t g_anas[MAX_ANALOG_INPUTS]; -QVector OpenTxSimulator::tracebackDevices; - -typedef struct { - uint8_t index; - QMutex mutex; - QQueue receiveBuffer; - OpenTxSimulator * simulator; -} simulated_serial_port_t; - -simulated_serial_port_t simulatedSerialPorts[MAX_AUX_SERIAL]; -extern etx_serial_port_t * serialPorts[MAX_AUX_SERIAL]; - -#if defined(HARDWARE_TOUCH) - tmr10ms_t downTime = 0; - tmr10ms_t tapTime = 0; - short tapCount = 0; - #define TAP_TIME 25 -#endif - -uint16_t simu_get_analog(uint8_t idx) -{ - // TODO: return raw values for ADC_INPUT_VBAT and ADC_INPUT_RTC_BAT - - // 6POS simu mechanism use a different scale, so needs specific offset - if (IS_POT_MULTIPOS(idx - adcGetInputOffset(ADC_INPUT_FLEX))) { - // Use radio calibration data to determine conversion factor - StepsCalibData * calib = (StepsCalibData *) &g_eeGeneral.calib[idx]; - int range6POS = 2048; // Default if calibration is not valid - if (calib->count != 0) { - // calculate 6POS switch range from calibration data - int c1 = calib->steps[calib->count - 1] * 32; // last calibration value - int c2 = calib->steps[calib->count - 2] * 32; // 2nd last calibration value - range6POS = c1 + (c1 - c2) / 2; - } - return (g_anas[idx] * range6POS / 2048); - } - return (g_anas[idx] * 2) + 2048; -} - -void firmwareTraceCb(const char * text) -{ - foreach (QIODevice * dev, OpenTxSimulator::tracebackDevices) { - if (dev) - dev->write(text); - } -} - -// Serial port handling needs to know about OpenTxSimulator, so we we -// need to update what's in simpgmspace.cpp when we have a simulator -// to point at. - -static void* simulator_host_drv_init(void* hw_def, const etx_serial_init* dev) -{ - if (hw_def == nullptr) - return nullptr; - - simulated_serial_port_t *port = (simulated_serial_port_t *)hw_def; - - port->simulator->drv_auxSerialInit(port->index, dev); - - // Return the port definition as the context - return (void *)port; -} - -static void simulator_host_drv_deinit(void* ctx) -{ - if (ctx == nullptr) - return; - - simulated_serial_port_t *port = (simulated_serial_port_t *)ctx; - port->simulator->drv_auxSerialDeinit(port->index); -} - -static void simulator_host_drv_send_byte(void* ctx, uint8_t b) -{ - if (ctx == nullptr) - return; - - simulated_serial_port_t *port = (simulated_serial_port_t *)ctx; - - port->simulator->drv_auxSerialSendByte(port->index, b); -} - -static void simulator_host_drv_send_buffer(void* ctx, const uint8_t* b, uint32_t l) -{ - if (ctx == nullptr) - return; - - simulated_serial_port_t *port = (simulated_serial_port_t *)ctx; - - port->simulator->drv_auxSerialSendBuffer(port->index, b, l); -} - -static int simulator_host_drv_get_byte(void* ctx, uint8_t* b) -{ - if (ctx == nullptr) - return 0; - - simulated_serial_port_t *port = (simulated_serial_port_t *)ctx; - - return port->simulator->drv_auxSerialGetByte(port->index, b); -} - -static void simulator_host_drv_set_baudrate(void* ctx, uint32_t baudrate) -{ - if (ctx == nullptr) - return; - - simulated_serial_port_t *port = (simulated_serial_port_t *)ctx; - port->simulator->drv_auxSerialSetBaudrate(port->index, baudrate); -} - -static bool simulator_host_drv_tx_completed(void* ctx) { return true; } - -static const etx_serial_driver_t simulator_host_drv = { - .init = simulator_host_drv_init, - .deinit = simulator_host_drv_deinit, - .sendByte = simulator_host_drv_send_byte, - .sendBuffer = simulator_host_drv_send_buffer, - .txCompleted = simulator_host_drv_tx_completed, - .waitForTxCompleted = nullptr, - .enableRx = nullptr, - .getByte = simulator_host_drv_get_byte, - .getLastByte = nullptr, - .getBufferedBytes = nullptr, - .copyRxBuffer = nullptr, - .clearRxBuffer = nullptr, - .getBaudrate = nullptr, - .setBaudrate = simulator_host_drv_set_baudrate, - .setPolarity = nullptr, - .setHWOption = nullptr, - .setReceiveCb = nullptr, - .setIdleCb = nullptr, - .setBaudrateCb = nullptr, -}; - - -OpenTxSimulator::OpenTxSimulator() : - SimulatorInterface(), - m_timer10ms(nullptr), - m_resetOutputsData(true), - m_stopRequested(false) -{ - tracebackDevices.clear(); - traceCallback = firmwareTraceCb; - - // When we create the simulator, we change the UART driver - for (int i = 0; i < MAX_AUX_SERIAL; i++) { - etx_serial_port_t * port = serialPorts[i]; - if (port != nullptr) { - port->uart = &simulator_host_drv; - port->hw_def = &(simulatedSerialPorts[i]); - simulatedSerialPorts[i].index = i; - simulatedSerialPorts[i].simulator = this; - } - } -} - -OpenTxSimulator::~OpenTxSimulator() -{ - traceCallback = nullptr; - tracebackDevices.clear(); - - for (int i = 0; i < MAX_AUX_SERIAL; i++) { - etx_serial_port_t * port = serialPorts[i]; - if (port != nullptr) { - port->hw_def = nullptr; - } - } - - if (m_timer10ms) - delete m_timer10ms; - - if (isRunning()) { - stop(); - QElapsedTimer tmout; - tmout.start(); - while (isRunning() && !tmout.hasExpired(1000)) - ; - } - //qDebug() << "Deleting OpenTxSimulator"; -} - -QString OpenTxSimulator::name() -{ - return QString(SIMULATOR_FLAVOUR); -} - -bool OpenTxSimulator::isRunning() -{ - QMutexLocker lckr(&m_mtxSimuMain); - return simuIsRunning(); -} - -void OpenTxSimulator::init() -{ - if (isRunning()) - return; - - ETXS_DBG; - - if (!m_timer10ms) { - // make sure we create & control the timer from current thread - m_timer10ms = new QTimer(); - m_timer10ms->setInterval(10); - connect(m_timer10ms, &QTimer::timeout, this, &OpenTxSimulator::run); - connect(this, SIGNAL(started()), m_timer10ms, SLOT(start())); - connect(this, SIGNAL(stopped()), m_timer10ms, SLOT(stop())); - } - - m_resetOutputsData = true; - setStopRequested(false); - - QMutexLocker lckr(&m_mtxSimuMain); - memset(g_anas, 0, sizeof(g_anas)); - - simuInit(); - - if (adcGetMaxInputs(ADC_INPUT_RTC_BAT) > 0) { - auto idx = adcGetInputOffset(ADC_INPUT_RTC_BAT); - setAnalogValue(idx, 800); - } -} - -void OpenTxSimulator::start(const char * filename, bool tests) -{ - if (isRunning()) return; - ETXS_DBG << "file:" << filename << "tests:" << tests; - - QMutexLocker lckr(&m_mtxSimuMain); - QMutexLocker slckr(&m_mtxSettings); - simuAudioInit(); - simuStart(tests, simuSdDirectory.toLatin1().constData(), - simuSettingsDirectory.toLatin1().constData()); - - emit started(); - QTimer::singleShot(0, this, SLOT(run())); // old style for Qt < 5.4 -} - -void OpenTxSimulator::stop() -{ - if (!isRunning()) - return; - ETXS_DBG; - - setStopRequested(true); - - QMutexLocker lckr(&m_mtxSimuMain); - simuStop(); - simuAudioDeInit(); - - emit stopped(); -} - -void OpenTxSimulator::setSdPath(const QString & sdPath, const QString & settingsPath) -{ - QMutexLocker lckr(&m_mtxSettings); - simuSdDirectory = sdPath; - simuSettingsDirectory = settingsPath; -} - -void OpenTxSimulator::setVolumeGain(const int value) -{ - QMutexLocker lckr(&m_mtxSettings); - volumeGain = value; -} - -void OpenTxSimulator::setRadioData(const QByteArray & data) -{ -// #if defined(EEPROM_SIZE) -// QMutexLocker lckr(&m_mtxRadioData); -// eeprom = (uint8_t *)malloc(qMin(EEPROM_SIZE, data.size())); -// memcpy(eeprom, data.data(), qMin(EEPROM_SIZE, data.size())); -// #endif -} - -void OpenTxSimulator::readRadioData(QByteArray & dest) -{ -// #if defined(EEPROM_SIZE) -// QMutexLocker lckr(&m_mtxRadioData); -// if (eeprom) -// memcpy(dest.data(), eeprom, qMin(EEPROM_SIZE, dest.size())); -// #endif -} - -uint8_t * OpenTxSimulator::getLcd() -{ - return (uint8_t *)simuLcdBuf; -} - -void OpenTxSimulator::setAnalogValue(uint8_t index, int16_t value) -{ - static int dim = DIM(g_anas); - if (index < dim) - g_anas[index] = value; -} - -void OpenTxSimulator::setSwitch(uint8_t swtch, int8_t state) -{ - simuSetSwitch(swtch, state); -} - -void OpenTxSimulator::setKey(uint8_t key, bool state) -{ - simuSetKey(key, state); -} - -void OpenTxSimulator::setTrimSwitch(uint8_t trim, bool state) -{ - simuSetTrim(trim, state); -} - -void OpenTxSimulator::setTrim(unsigned int idx, int value) -{ - unsigned i = inputMappingConvertMode(idx); - uint8_t phase = getTrimFlightMode(getFlightMode(), i); - setTrimValue(phase, i, value); -} - -void OpenTxSimulator::setTrainerInput(unsigned int inputNumber, int16_t value) -{ - static unsigned dim = DIM(trainerInput); - if (inputNumber < dim) - trainerInput[inputNumber] = qMin(qMax((int16_t)-512, value), (int16_t)512); -} - -void OpenTxSimulator::setInputValue(int type, uint8_t index, int16_t value) -{ - //qDebug() << type << index << value << this->thread(); - switch (type) { - case INPUT_SRC_ANALOG : - case INPUT_SRC_STICK : - case INPUT_SRC_KNOB : - case INPUT_SRC_SLIDER : - setAnalogValue(index, value); - break; - case INPUT_SRC_TXVIN : - if (adcGetMaxInputs(ADC_INPUT_VBAT) > 0) { - auto idx = adcGetInputOffset(ADC_INPUT_VBAT); - setAnalogValue(idx, value); - emit txBatteryVoltageChanged((unsigned int)value); - } - break; - case INPUT_SRC_SWITCH : - setSwitch(index, (int8_t)value); - break; - case INPUT_SRC_TRIM_SW : - setTrimSwitch(index, (bool)value); - break; - case INPUT_SRC_TRIM : - setTrim(index, value); - break; - case INPUT_SRC_KEY : - setKey(index, (bool)value); - break; - case INPUT_SRC_TRAINER : - setTrainerInput(index, value); - break; - case INPUT_SRC_ROTENC : // TODO - default: - return; - } -} - -extern volatile rotenc_t rotencValue; -extern volatile uint32_t rotencDt; - -void OpenTxSimulator::rotaryEncoderEvent(int steps) -{ - if (steps == 0) return; - -#if defined(ROTARY_ENCODER_NAVIGATION) && !defined(USE_HATS_AS_KEYS) - static uint32_t last_tick = 0; - if (steps != 0) { - if (g_eeGeneral.rotEncMode == ROTARY_ENCODER_MODE_INVERT_BOTH) - steps *= -1; - rotencValue += steps * ROTARY_ENCODER_GRANULARITY; - // TODO: set rotencDt - uint32_t now = time_get_ms(); - uint32_t dt = now - last_tick; - rotencDt += dt; - last_tick = now; - } -#else - int key; - if (keyIsSupported(KEY_UP) | keyIsSupported(KEY_DOWN)) { - key = steps > 0 ? KEY_DOWN : KEY_UP; - } else if (keyIsSupported(KEY_PLUS) | keyIsSupported(KEY_MINUS)) { - key = steps > 0 ? KEY_MINUS : KEY_PLUS; - } else { - return; // not supposed to happen??? - } - - setKey(key, 1); - QTimer::singleShot(10, [this, key]() { setKey(key, 0); }); -#endif // defined(ROTARY_ENCODER_NAVIGATION) -} - -void OpenTxSimulator::touchEvent(int type, int x, int y) -{ - #if defined(HARDWARE_TOUCH) - tmr10ms_t now = get_tmr10ms(); - simTouchState.tapCount = 0; - #endif - - switch (type) { - case TouchDown: - TRACE_WINDOWS("[Mouse Press] %d %d", x, y); - -#if defined(HARDWARE_TOUCH) - simTouchState.event = TE_DOWN; - simTouchState.startX = simTouchState.x = x; - simTouchState.startY = simTouchState.y = y; - downTime = now; -#endif - break; - - case TouchUp: - TRACE_WINDOWS("[Mouse Release] %d %d", x, y); - -#if defined(HARDWARE_TOUCH) - if (simTouchState.event == TE_DOWN) { - simTouchState.event = TE_UP; - simTouchState.x = simTouchState.startX; - simTouchState.y = simTouchState.startY; - if (now - downTime <= TAP_TIME) { - if (now - tapTime > TAP_TIME) - tapCount = 1; - else - tapCount++; - simTouchState.tapCount = tapCount; - tapTime = now; - } - } else { - simTouchState.event = TE_SLIDE_END; - } -#endif - break; - - case TouchSlide: - TRACE_WINDOWS("[Mouse Move] %d %d", x, y); - -#if defined(HARDWARE_TOUCH) - simTouchState.deltaX += x - simTouchState.x; - simTouchState.deltaY += y - simTouchState.y; - if (simTouchState.event == TE_SLIDE || - abs(simTouchState.deltaX) >= SLIDE_RANGE || - abs(simTouchState.deltaY) >= SLIDE_RANGE) { - simTouchState.event = TE_SLIDE; - simTouchState.x = x; - simTouchState.y = y; - } -#endif - break; - } -#if defined(HARDWARE_TOUCH) - simTouchOccured=true; -#endif -} - -void OpenTxSimulator::lcdFlushed() -{ - ::lcdFlushed(); -} - -void OpenTxSimulator::setTrainerTimeout(uint16_t ms) -{ - trainerSetTimer(ms); -} - -void OpenTxSimulator::sendTelemetry(const uint8_t module, const uint8_t protocol, const QByteArray data) -{ - //ETXS_DBG << data; - switch (protocol) { - case SIMU_TELEMETRY_PROTOCOL_FRSKY_SPORT: - sportProcessTelemetryPacket(module, - (uint8_t *)data.constData(), - data.size()); - break; - case SIMU_TELEMETRY_PROTOCOL_FRSKY_HUB: - frskyDProcessPacket(module, - (uint8_t *)data.constData(), - data.size()); - break; - case SIMU_TELEMETRY_PROTOCOL_FRSKY_HUB_OOB: - // FrSky D telemetry is a stream which can span multiple - // packets. The telemetry parser _could_ be in the middle of a - // user packet when we want to inject telemetry, so we can't just - // call frskyDProcessPacket() with a USRPKT (at least not safely!) - // Instead we will bypass the telemetry stream parser and inject - // out of band into processHubPacket(). This way we also don't - // have to mess with byte stuffing and variable length packets. - // - // Note this doesn't take into account which module it's from, but - // that's how it works in the radio too if you have two frsky D - // modules running at once, so ¯\_(ツ)_/¯ - { - uint8_t id = data[0]; - uint16_t value = ((uint8_t)(data[2]) << 8) + (uint8_t)(data[1]); - - processHubPacket(id, value); - } - break; - case SIMU_TELEMETRY_PROTOCOL_CROSSFIRE: - processCrossfireTelemetryFrame(module, - (uint8_t *)data.constData(), - data.size()); - break; - default: - // Do nothing - break; - } -} - -void OpenTxSimulator::sendInternalModuleTelemetry(const uint8_t protocol, const QByteArray data) -{ - sendTelemetry(INTERNAL_MODULE, protocol, data); -} - -void OpenTxSimulator::sendExternalModuleTelemetry(const uint8_t protocol, const QByteArray data) -{ - sendTelemetry(EXTERNAL_MODULE, protocol, data); -} - -uint8_t OpenTxSimulator::getSensorInstance(uint16_t id, uint8_t defaultValue) -{ - for (int i = 0; i < MAX_TELEMETRY_SENSORS; i++) { - if (isTelemetryFieldAvailable(i)) { - TelemetrySensor * sensor = &g_model.telemetrySensors[i]; - if (sensor->id == id) { - return sensor->frskyInstance.physID; - } - } - } - return defaultValue; -} - -uint16_t OpenTxSimulator::getSensorRatio(uint16_t id) -{ - for (int i = 0; i < MAX_TELEMETRY_SENSORS; i++) { - if (isTelemetryFieldAvailable(i)) { - TelemetrySensor * sensor = &g_model.telemetrySensors[i]; - if (sensor->id == id) { - return sensor->custom.ratio; - } - } - } - return 0; -} - -const int OpenTxSimulator::getCapability(Capability cap) -{ - int ret = 0; - switch(cap) { - case CAP_LUA : - #ifdef LUA - ret = 1; - #endif - break; - - case CAP_ROTARY_ENC : - break; - - case CAP_ROTARY_ENC_NAV : - #ifdef ROTARY_ENCODER_NAVIGATION - ret = 1; - #endif - break; - - case CAP_TELEM_FRSKY_SPORT : - ret = 1; - break; - - case CAP_SERIAL_AUX1: - ret = (auxSerialGetPort(SP_AUX1) != nullptr); - break; - - case CAP_SERIAL_AUX2: - ret = (auxSerialGetPort(SP_AUX2) != nullptr); - break; - } - return ret; -} - -void OpenTxSimulator::setLuaStateReloadPermanentScripts() -{ -#if defined(LUA) - luaState = INTERPRETER_RELOAD_PERMANENT_SCRIPTS; -#endif -} - -void OpenTxSimulator::addTracebackDevice(QIODevice * device) -{ - QMutexLocker lckr(&m_mtxTbDevices); - if (device && !tracebackDevices.contains(device)) - tracebackDevices.append(device); -} - -void OpenTxSimulator::removeTracebackDevice(QIODevice * device) -{ - if (device) { - QMutexLocker lckr(&m_mtxTbDevices); - // no QVector::removeAll() in Qt < 5.4 - int i = 0; - foreach (QIODevice * d, tracebackDevices) { - if (d == device) - tracebackDevices.remove(i); - ++i; - } - } -} - -void OpenTxSimulator::receiveAuxSerialData(quint8 port_nr, const QByteArray & data) -{ - if (port_nr >= MAX_AUX_SERIAL) - return; - - QMutexLocker locker(&(simulatedSerialPorts[port_nr].mutex)); - - for (uint8_t byte : data) - simulatedSerialPorts[port_nr].receiveBuffer.enqueue(byte); -} - -void OpenTxSimulator::drv_auxSerialSetBaudrate(quint8 port_nr, quint32 baudrate) -{ - emit auxSerialSetBaudrate(port_nr, baudrate); -} - -void OpenTxSimulator::drv_auxSerialInit(quint8 port_nr, const etx_serial_init* dev) -{ - switch(dev->encoding) { - case ETX_Encoding_8N1: - emit auxSerialSetEncoding(port_nr, SERIAL_ENCODING_8N1); - break; - case ETX_Encoding_8E2: - emit auxSerialSetEncoding(port_nr, SERIAL_ENCODING_8E2); - break; - default: - // Do nothing, host hardware can't do SERIAL_ENCODING_PXX1_PWM - break; - } - - if (dev->baudrate != 0) - emit auxSerialSetBaudrate(port_nr, dev->baudrate); - - emit auxSerialStart(port_nr); -} - -void OpenTxSimulator::drv_auxSerialDeinit(quint8 port_nr) -{ - emit auxSerialStop(port_nr); -} - -void OpenTxSimulator::drv_auxSerialSendByte(quint8 port_nr, uint8_t b) -{ - QByteArray data = QByteArray((const char *)&b, 1); - - emit auxSerialSendData(port_nr, data); -} - -void OpenTxSimulator::drv_auxSerialSendBuffer(quint8 port_nr, const uint8_t* b, uint32_t l) -{ - QByteArray data = QByteArray((const char *)b, l); - - emit auxSerialSendData(port_nr, data); -} - -int OpenTxSimulator::drv_auxSerialGetByte(quint8 port_nr, uint8_t *b) -{ - // Obtain the port's mutex before messing with the buffer - QMutexLocker locker(&(simulatedSerialPorts[port_nr].mutex)); - - if (simulatedSerialPorts[port_nr].receiveBuffer.isEmpty()) - return 0; - - uint8_t byte = simulatedSerialPorts[port_nr].receiveBuffer.dequeue(); - - *b = byte; - return 1; -} - -/*** Protected functions ***/ - -void OpenTxSimulator::run() -{ - static uint32_t loops = 0; - static QElapsedTimer ts; - - if (!loops) - ts.start(); - - if (isStopRequested()) { - return; - } - if (!isRunning()) { - QString err(getError()); - emit runtimeError(err); - emit stopped(); - return; - } - - ++loops; - - checkLcdChanged(); - checkFuncSwitchChanged(); - - if (!(loops % 5)) { - checkOutputsChanged(); - } - - if (!(loops % (SIMULATOR_INTERFACE_HEARTBEAT_PERIOD / 10))) { - emit heartbeat(loops, simuTimerMicros() / 1000); - } -} - -bool OpenTxSimulator::isStopRequested() -{ - QMutexLocker lckr(&m_mtxStopReq); - return m_stopRequested; -} - -void OpenTxSimulator::setStopRequested(bool stop) -{ - QMutexLocker lckr(&m_mtxStopReq); - m_stopRequested = stop; -} - -bool OpenTxSimulator::checkLcdChanged() -{ - if (simuLcdRefresh) { - simuLcdRefresh = false; - emit lcdChange(isBacklightEnabled()); - return true; - } - return false; -} - -void OpenTxSimulator::checkFuncSwitchChanged() -{ -#if defined(FUNCTION_SWITCHES) - for (int i = 0; i < CPN_MAX_SWITCHES; i += 1) { - if (switchIsCustomSwitch(i)) { - uint8_t cfs = switchGetCustomSwitchIdx(i); - uint32_t c = rgbGetLedColor(cfs); - emit fsColorChange(i, c); - } - } -#endif -} - -void OpenTxSimulator::checkOutputsChanged() -{ - static TxOutputs lastOutputs; - static size_t chansDim = DIM(channelOutputs); - const static int16_t limit = 512 * 2; - qint32 tmpVal; - uint8_t i, idx; - const uint8_t phase = getFlightMode(); // edgetx.cpp - - for (i=0; i < chansDim; i++) { - if (lastOutputs.chans[i] != channelOutputs[i] || m_resetOutputsData) { - emit channelOutValueChange(i, channelOutputs[i], (g_model.extendedLimits ? limit * LIMIT_EXT_PERCENT / 100 : limit)); - emit outputValueChange(OUTPUT_SRC_CHAN_OUT, i, channelOutputs[i]); - lastOutputs.chans[i] = channelOutputs[i]; - } - if (lastOutputs.ex_chans[i] != ex_chans[i] || m_resetOutputsData) { - emit channelMixValueChange(i, ex_chans[i], limit * 2); - emit outputValueChange(OUTPUT_SRC_CHAN_MIX, i, ex_chans[i]); - lastOutputs.ex_chans[i] = ex_chans[i]; - } - } - - for (i=0; i < MAX_LOGICAL_SWITCHES; i++) { - tmpVal = (qint32)GET_SWITCH_BOOL(SWSRC_FIRST_LOGICAL_SWITCH+i); - if (lastOutputs.vsw[i] != (bool)tmpVal || m_resetOutputsData) { - emit virtualSwValueChange(i, tmpVal); - emit outputValueChange(OUTPUT_SRC_VIRTUAL_SW, i, tmpVal); - lastOutputs.vsw[i] = tmpVal; - } - } - - for (i=0; i < Board::TRIM_AXIS_COUNT; i++) { - idx = inputMappingConvertMode(i); - tmpVal = getTrimValue(getTrimFlightMode(phase, idx), idx); - if (lastOutputs.trims[i] != tmpVal || m_resetOutputsData) { - emit trimValueChange(i, tmpVal); - emit outputValueChange(OUTPUT_SRC_TRIM_VALUE, i, tmpVal); - lastOutputs.trims[i] = tmpVal; - } - } - - tmpVal = g_model.extendedTrims ? TRIM_EXTENDED_MAX : TRIM_MAX; - if (lastOutputs.trimRange != tmpVal || m_resetOutputsData) { - emit trimRangeChange(Board::TRIM_AXIS_COUNT, -tmpVal, tmpVal); - emit outputValueChange(OUTPUT_SRC_TRIM_RANGE, Board::TRIM_AXIS_COUNT, tmpVal); - lastOutputs.trimRange = tmpVal; - } - - if (lastOutputs.phase != phase || m_resetOutputsData) { - emit phaseChanged(phase, getCurrentPhaseName()); - emit outputValueChange(OUTPUT_SRC_PHASE, 0, qint16(phase)); - lastOutputs.phase = phase; - } - -#if defined(GVAR_VALUE) && defined(GVARS) - gVarMode_t gvar; - for (uint8_t gv=0; gv < MAX_GVARS; gv++) { - gvar.prec = g_model.gvars[gv].prec; - gvar.unit = g_model.gvars[gv].unit; - for (uint8_t fm=0; fm < MAX_FLIGHT_MODES; fm++) { - gvar.mode = fm; - gvar.value = (int16_t)GVAR_VALUE(gv, getGVarFlightMode(fm, gv)); - tmpVal = gvar; - if (lastOutputs.gvars[fm][gv] != tmpVal || m_resetOutputsData) { - lastOutputs.gvars[fm][gv] = tmpVal; - emit gVarValueChange(gv, tmpVal); - emit outputValueChange(OUTPUT_SRC_GVAR, gv, tmpVal); - } - } - } -#endif - - m_resetOutputsData = false; -} - -uint8_t OpenTxSimulator::getStickMode() -{ - return limit(0, g_eeGeneral.stickMode, 3); -} - -const char * OpenTxSimulator::getPhaseName(unsigned int phase) -{ - static char buff[LEN_FLIGHT_MODE_NAME+1]; - strAppend(buff, g_model.flightModeData[phase].name, LEN_FLIGHT_MODE_NAME); - return buff; -} - -const QString OpenTxSimulator::getCurrentPhaseName() -{ - unsigned phase = getFlightMode(); - QString name(getPhaseName(phase)); - if (name.isEmpty()) - name = QString::number(phase); - return name; -} - -const char * OpenTxSimulator::getError() -{ - return main_thread_error; -} - - -/* - * OpenTxSimulatorFactory - */ - -class OpenTxSimulatorFactory: public SimulatorFactory -{ - public: - OpenTxSimulatorFactory() - { - } - - virtual SimulatorInterface * create() - { - return new OpenTxSimulator(); - } - - virtual QString name() - { - return QString(SIMULATOR_FLAVOUR); - } - - virtual Board::Type type() - { -#if defined(PCBX12S) - return Board::BOARD_HORUS_X12S; -#elif defined(PCBX10) - return Board::BOARD_X10; -#elif defined(PCBX7ACCESS) - return Board::BOARD_TARANIS_X7_ACCESS; -#elif defined(PCBX7) - return Board::BOARD_TARANIS_X7; -#elif defined(PCBX9LITES) - return Board::BOARD_TARANIS_X9LITES; -#elif defined(PCBX9LITE) - return Board::BOARD_TARANIS_X9LITE; -#elif defined(PCBPL18) - return Board::BOARD_FLYSKY_PL18; -#else - return Board::BOARD_TARANIS_X9D; -#endif - } -}; - -extern "C" DLLEXPORT SimulatorFactory * registerSimu() -{ - return new OpenTxSimulatorFactory(); -} diff --git a/radio/src/targets/simu/opentxsimulator.h b/radio/src/targets/simu/opentxsimulator.h deleted file mode 100644 index 4936d3212..000000000 --- a/radio/src/targets/simu/opentxsimulator.h +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (C) EdgeTX - * - * Based on code named - * opentx - https://github.com/opentx/opentx - * th9x - http://code.google.com/p/th9x - * er9x - http://code.google.com/p/er9x - * gruvin9x - http://code.google.com/p/gruvin9x - * - * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - */ - -#pragma once - -#include "simulatorinterface.h" -#include "dataconstants.h" - -#include -#include -#include - -#if defined __GNUC__ - #define DLLEXPORT -#else - #define DLLEXPORT __declspec(dllexport) -#endif - -class DLLEXPORT OpenTxSimulator : public SimulatorInterface -{ - Q_OBJECT - - public: - - enum TouchEvent { - TouchUp=0, - TouchDown, - TouchSlide - }; - - OpenTxSimulator(); - virtual ~OpenTxSimulator(); - - virtual QString name(); - virtual bool isRunning(); - virtual void readRadioData(QByteArray & dest); - virtual uint8_t * getLcd(); - virtual uint8_t getSensorInstance(uint16_t id, uint8_t defaultValue = 0); - virtual uint16_t getSensorRatio(uint16_t id); - virtual const int getCapability(Capability cap); - - static QVector tracebackDevices; - - void drv_auxSerialInit(quint8 port_num, const etx_serial_init* dev); - void drv_auxSerialDeinit(quint8 port_num); - void drv_auxSerialSetBaudrate(quint8 port_num, uint32_t baudrate); - void drv_auxSerialSendByte(quint8 port_num, uint8_t byte); - void drv_auxSerialSendBuffer(quint8 port_num, const uint8_t* data, uint32_t size); - int drv_auxSerialGetByte(quint8 port_num, uint8_t* data); - - public slots: - - virtual void init(); - virtual void start(const char * filename = nullptr, bool tests = true); - virtual void stop(); - virtual void setSdPath(const QString & sdPath = "", const QString & settingsPath = ""); - virtual void setVolumeGain(const int value); - virtual void setRadioData(const QByteArray & data); - virtual void setAnalogValue(uint8_t index, int16_t value); - virtual void setKey(uint8_t key, bool state); - virtual void setSwitch(uint8_t swtch, int8_t state); - virtual void setTrim(unsigned int idx, int value); - virtual void setTrimSwitch(uint8_t trim, bool state); - virtual void setTrainerInput(unsigned int inputNumber, int16_t value); - virtual void setInputValue(int type, uint8_t index, int16_t value); - virtual void rotaryEncoderEvent(int steps); - virtual void touchEvent(int type, int x, int y); - virtual void lcdFlushed(); - virtual void setTrainerTimeout(uint16_t ms); - virtual void sendTelemetry(const uint8_t module, const uint8_t protocol, const QByteArray data); - virtual void sendInternalModuleTelemetry(const uint8_t protocol, const QByteArray data); - virtual void sendExternalModuleTelemetry(const uint8_t protocol, const QByteArray data); - virtual void setLuaStateReloadPermanentScripts(); - virtual void addTracebackDevice(QIODevice * device); - virtual void removeTracebackDevice(QIODevice * device); - virtual void receiveAuxSerialData(const quint8 port_num, const QByteArray & data); - - protected slots: - void run(); - - protected: - - bool isStopRequested(); - void setStopRequested(bool stop); - bool checkLcdChanged(); - void checkOutputsChanged(); - void checkFuncSwitchChanged(); - uint8_t getStickMode(); - const char * getPhaseName(unsigned int phase); - const QString getCurrentPhaseName(); - const char * getError(); - - QString simuSdDirectory; - QString simuSettingsDirectory; - QTimer * m_timer10ms; - QMutex m_mtxStopReq; - QMutex m_mtxSimuMain; - QMutex m_mtxRadioData; - QMutex m_mtxSettings; - QMutex m_mtxTbDevices; - int volumeGain; - bool m_resetOutputsData; - bool m_stopRequested; - -}; diff --git a/radio/src/targets/simu/sdl_simu.cpp b/radio/src/targets/simu/sdl_simu.cpp index feb4fe02d..a6ab43e33 100644 --- a/radio/src/targets/simu/sdl_simu.cpp +++ b/radio/src/targets/simu/sdl_simu.cpp @@ -38,6 +38,10 @@ #include "hal/rotary_encoder.h" #include "hal/switch_driver.h" +#ifdef __EMSCRIPTEN__ +#include +#endif + #if !SDL_VERSION_ATLEAST(2,0,19) #error This backend requires SDL 2.0.19+ because of SDL_RenderGeometryRaw() function #endif @@ -62,7 +66,7 @@ #include "display.h" #include "simuaudio.h" -#include "simpgmspace.h" +#include "simulib.h" #include "hal/key_driver.h" #include "hal/switch_driver.h" @@ -83,9 +87,11 @@ static SDL_Texture* screen_frame_buffer; static GimbalState stick_left = {{0.5f, 0.5f}, false}; static GimbalState stick_right = {{0.5f, 0.5f}, false}; +#if !defined(__EMSCRIPTEN__) static const unsigned char _icon_png[] = { #include "icon.lbm" }; +#endif #if defined(ROTARY_ENCODER_NAVIGATION) extern volatile rotenc_t rotencValue; @@ -292,7 +298,6 @@ static SDL_Texture* LoadTexture(SDL_Renderer* renderer, const unsigned char* pix return texture; } - static float switches_width() { const int max_sw_line = MAX_SWITCHES / 2; @@ -731,10 +736,12 @@ int main(int argc, char* argv[]) SDL_GetRendererInfo(renderer, &info); SDL_Log("Current SDL_Renderer: %s", info.name); +#if !defined(__EMSCRIPTEN__) SDL_Surface* icon = LoadImage(_icon_png, sizeof(_icon_png)); if (window && icon) { SDL_SetWindowIcon(window, icon); } +#endif // Setup Dear ImGui context IMGUI_CHECKVERSION(); @@ -779,8 +786,9 @@ int main(int argc, char* argv[]) // Init simulation simuInit(); - simuStart(true, args.getStoragePath().c_str(), - args.getSettingsPath().c_str()); + simuFatfsSetPaths(args.getStoragePath().c_str(), + args.getSettingsPath().c_str()); + simuStart(); // Main loop SDL_SetEventFilter([](void*, SDL_Event* event){ @@ -792,6 +800,9 @@ int main(int argc, char* argv[]) return 1; }, NULL); +#if defined(__EMSCRIPTEN__) + emscripten_set_main_loop([]() { handleEvents(); }, 0, true); +#else do { Uint64 start_ts = SDL_GetPerformanceCounter(); if (!handleEvents()) break; @@ -804,6 +815,7 @@ int main(int argc, char* argv[]) SDL_Delay(std::max(0,(int32_t)floor(16.666f - elapsedMS))); } while(true); +#endif // App cleanup simuStop(); @@ -816,14 +828,16 @@ int main(int argc, char* argv[]) SDL_DestroyTexture(screen_frame_buffer); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); +#if !defined(__EMSCRIPTEN__) SDL_FreeSurface(icon); +#endif SDL_CloseAudio(); SDL_Quit(); return 0; } -uint16_t simu_get_analog(uint8_t idx) +uint16_t simuGetAnalog(uint8_t idx) { auto max_sticks = adcGetMaxInputs(ADC_INPUT_MAIN); if (idx < max_sticks) { @@ -857,3 +871,6 @@ uint16_t simu_get_analog(uint8_t idx) // probably RTC_BAT return 0; } + +void simuTrace(const char* text) {} +void simuLcdNotify() {} diff --git a/radio/src/targets/simu/simpgmspace.h b/radio/src/targets/simu/simpgmspace.h deleted file mode 100644 index ac17f9caa..000000000 --- a/radio/src/targets/simu/simpgmspace.h +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) EdgeTX - * - * Based on code named - * opentx - https://github.com/opentx/opentx - * th9x - http://code.google.com/p/th9x - * er9x - http://code.google.com/p/er9x - * gruvin9x - http://code.google.com/p/gruvin9x - * - * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - */ - -#pragma once - -#include -#include -#include -#include -#include - -#include - -#define __disable_irq() -#define __enable_irq() - -extern char * main_thread_error; - -uint64_t simuTimerMicros(void); - -void simuSetKey(uint8_t key, bool state); -void simuSetTrim(uint8_t trim, bool state); -void simuSetSwitch(uint8_t swtch, int8_t state); - -#if defined(__cplusplus) -void simuInit(); -void simuStart(bool tests = true, const char * sdPath = nullptr, const char * settingsPath = nullptr); -void simuStop(); -bool simuIsRunning(); -void startEepromThread(const char * filename = "eeprom.bin"); -void stopEepromThread(); -#endif - -void simuMain(); - -void simuFatfsSetPaths(const char * sdPath, const char * settingsPath); - -std::string simuFatfsGetCurrentPath(); -std::string simuFatfsGetRealPath(const std::string &p); - -#if defined(TRACE_SIMPGMSPACE) - #undef TRACE_SIMPGMSPACE - #define TRACE_SIMPGMSPACE TRACE -#else - #define TRACE_SIMPGMSPACE(...) -#endif - -#if defined(HARDWARE_TOUCH) - extern struct TouchState simTouchState; - extern bool simTouchOccured; -#endif diff --git a/radio/src/targets/simu/simuaudio.cpp b/radio/src/targets/simu/simuaudio.cpp index 2503697aa..6e4c84e93 100644 --- a/radio/src/targets/simu/simuaudio.cpp +++ b/radio/src/targets/simu/simuaudio.cpp @@ -21,6 +21,7 @@ #include "audio.h" #include "simuaudio.h" +#include "simulib.h" #include #include diff --git a/radio/src/targets/simu/simuaudio.h b/radio/src/targets/simu/simuaudio.h index fa0b1c4f4..a1b0408c2 100644 --- a/radio/src/targets/simu/simuaudio.h +++ b/radio/src/targets/simu/simuaudio.h @@ -21,9 +21,6 @@ #pragma once -#include - bool simuAudioInit(); void simuAudioDeInit(); -void simuQueueAudio(const uint8_t* data, uint32_t len); int simuAudioGetVolume(); diff --git a/radio/src/targets/simu/simufatfs.cpp b/radio/src/targets/simu/simufatfs.cpp index e234e3065..0050c7749 100644 --- a/radio/src/targets/simu/simufatfs.cpp +++ b/radio/src/targets/simu/simufatfs.cpp @@ -19,7 +19,7 @@ * GNU General Public License for more details. */ -#include "simpgmspace.h" +#include "simulib.h" #if !defined(SIMU_DISKIO) @@ -31,6 +31,7 @@ #include #include #include +#include #include namespace fs = std::filesystem; @@ -200,22 +201,17 @@ FRESULT file_stat(const std::string& realPath, FILINFO* fno) // Get last write time auto ftime = fs::last_write_time(fsPath, ec); if (!ec) { - try { - auto systime = ftime_to_systime(ftime); - auto time_t_val = std::chrono::system_clock::to_time_t(systime); - struct tm* ltime = localtime(&time_t_val); + auto systime = ftime_to_systime(ftime); + auto time_t_val = std::chrono::system_clock::to_time_t(systime); + struct tm* ltime = localtime(&time_t_val); - if (ltime) { - // Convert to FatFs format - fno->fdate = ((ltime->tm_year - 80) << 9) | - ((ltime->tm_mon + 1) << 5) | ltime->tm_mday; - fno->ftime = (ltime->tm_hour << 11) | (ltime->tm_min << 5) | - (ltime->tm_sec / 2); - } else { - fno->fdate = 0; - fno->ftime = 0; - } - } catch (...) { + if (ltime) { + // Convert to FatFs format + fno->fdate = ((ltime->tm_year - 80) << 9) | ((ltime->tm_mon + 1) << 5) | + ltime->tm_mday; + fno->ftime = + (ltime->tm_hour << 11) | (ltime->tm_min << 5) | (ltime->tm_sec / 2); + } else { fno->fdate = 0; fno->ftime = 0; } @@ -286,16 +282,12 @@ FRESULT f_open(FIL* fil, const TCHAR* name, BYTE flag) } } - try { - auto simuFil = new _simu_FIL(realPath, mode); - if (simuFil->stream->is_open()) { - fil->obj.fs = reinterpret_cast(simuFil); - return FR_OK; - } else { - delete simuFil; - return FR_INVALID_NAME; - } - } catch (...) { + auto simuFil = new _simu_FIL(realPath, mode); + if (simuFil->stream->is_open()) { + fil->obj.fs = reinterpret_cast(simuFil); + return FR_OK; + } else { + delete simuFil; return FR_INVALID_NAME; } } @@ -408,35 +400,20 @@ struct _simu_DIR { if (ec) { iter = end_iter; // Set to end if error } - skipDotEntries(); // TODO: check if necessary - } - - private: - void skipDotEntries() - { - while (iter != end_iter) { - std::string filename = iter->path().filename().string(); - if (filename != "." && filename != "..") { - break; - } - std::error_code ec; - ++iter; - } } public: bool hasNext() const { return iter != end_iter; } - fs::directory_entry getNext() + std::optional getNext() { if (iter == end_iter) { - throw std::runtime_error("No more entries"); + return std::nullopt; } auto current = *iter; std::error_code ec; ++iter; - skipDotEntries(); return current; } @@ -452,13 +429,8 @@ FRESULT f_opendir(DIR* rep, const TCHAR* name) return FR_NO_PATH; } - try { - rep->obj.fs = reinterpret_cast(new _simu_DIR(path)); - return FR_OK; - } catch (...) { - rep->obj.fs = nullptr; - return FR_NO_PATH; - } + rep->obj.fs = reinterpret_cast(new _simu_DIR(path)); + return FR_OK; } FRESULT f_closedir(DIR* rep) @@ -479,24 +451,24 @@ FRESULT f_readdir(DIR* rep, FILINFO* fil) return FR_NO_FILE; } - try { - auto entry = sd->getNext(); - - if (fil != nullptr) { - memset(fil->fname, 0, FF_MAX_LFN); - - std::string filename = entry.path().filename().string(); - size_t copyLen = std::min(filename.length(), size_t(FF_MAX_LFN - 1)); - strncpy(fil->fname, filename.c_str(), copyLen); - - std::string fullPath = entry.path().string(); - return file_stat(fullPath, fil); - } - - return FR_OK; - } catch (...) { + auto maybe_entry = sd->getNext(); + if (!maybe_entry.has_value()) { return FR_NO_FILE; } + + auto entry = maybe_entry.value(); + if (fil != nullptr) { + memset(fil->fname, 0, FF_MAX_LFN); + + std::string filename = entry.path().filename().string(); + size_t copyLen = std::min(filename.length(), size_t(FF_MAX_LFN - 1)); + strncpy(fil->fname, filename.c_str(), copyLen); + + std::string fullPath = entry.path().string(); + return file_stat(fullPath, fil); + } + + return FR_OK; } FRESULT f_mkfs(const TCHAR* path, BYTE opt, DWORD au, void* work, UINT len) diff --git a/radio/src/targets/simu/simulcd.cpp b/radio/src/targets/simu/simulcd.cpp index 03b738ca3..8530bae2a 100644 --- a/radio/src/targets/simu/simulcd.cpp +++ b/radio/src/targets/simu/simulcd.cpp @@ -21,6 +21,7 @@ #include "lcd.h" #include "simulcd.h" +#include "simulib.h" #include "rtos.h" #include #include @@ -41,10 +42,11 @@ void lcdInit() {} void lcdRefresh() { - // Mark screen dirty for async refresh - simuLcdRefresh = true; - memcpy(simuLcdBuf, displayBuf, DISPLAY_BUFFER_SIZE * sizeof(pixel_t)); + + // Mark screen dirty and notify host for async refresh + simuLcdRefresh = true; + simuLcdNotify(); } #else @@ -102,8 +104,9 @@ static void simuRefreshLcd(lv_disp_drv_t * disp_drv, uint16_t *buffer, const rec // simply set LVGL's buffer as our current frame buffer simuLcdBuf = buffer; - // Trigger async refresh + // Trigger async refresh and notify host simuLcdRefresh = true; + simuLcdNotify(); #else _copy_area(simuLcdBackBuf, buffer, copy_area); @@ -118,8 +121,9 @@ static void simuRefreshLcd(lv_disp_drv_t * disp_drv, uint16_t *buffer, const rec simuLcdBackBuf = _LCD_BUF2; } - // Trigger async refresh + // Trigger async refresh and notify host simuLcdRefresh = true; + simuLcdNotify(); // Copy refreshed & rotated areas into new back buffer uint16_t* src = simuLcdBuf; diff --git a/radio/src/targets/simu/simpgmspace.cpp b/radio/src/targets/simu/simulib.cpp similarity index 61% rename from radio/src/targets/simu/simpgmspace.cpp rename to radio/src/targets/simu/simulib.cpp index 035d22581..b3d6233d8 100644 --- a/radio/src/targets/simu/simpgmspace.cpp +++ b/radio/src/targets/simu/simulib.cpp @@ -19,29 +19,31 @@ * GNU General Public License for more details. */ -#include "board.h" -#define SIMPGMSPC_USE_QT 0 - -#include "edgetx.h" +#include "simulib.h" #include "simulcd.h" #include "hal/adc_driver.h" #include "hal/rotary_encoder.h" #include "hal/usb_driver.h" -#include "hal/audio_driver.h" #include "os/sleep.h" #include "os/task.h" -#include "os/timer_native_impl.h" -#include -#include -#include - -#if !defined (_MSC_VER) || defined (__GNUC__) - #include - #include +#include "edgetx.h" +#include "debug.h" +#include "switches.h" +#include "input_mapping.h" +#if defined(GVARS) +#include "gvars.h" #endif +#include "trainer.h" +#include "telemetry/frsky.h" +#include "telemetry/crossfire.h" +#if defined(LUA) +#include "lua/lua_api.h" +#endif + +#include int g_snapshot_idx = 0; @@ -61,32 +63,10 @@ rotenc_t rotaryEncoderGetValue() return rotencValue / ROTARY_ENCODER_GRANULARITY; } -// TODO: remove all STM32 defs - extern const etx_hal_adc_driver_t simu_adc_driver; void lcdCopy(void * dest, void * src); - -uint64_t simuTimerMicros(void) -{ - auto now = std::chrono::steady_clock::now(); - return (uint64_t) std::chrono::duration_cast(now.time_since_epoch()).count(); -} - -uint16_t getTmr16KHz() -{ - return simuTimerMicros() * 2 / 125; -} - -uint16_t getTmr2MHz() -{ - return simuTimerMicros() * 2; -} - -uint32_t timersGetMsTick(void) -{ - return simuTimerMicros() / 1000; -} +void lcdFlushed(); void simuInit() { @@ -94,6 +74,9 @@ void simuInit() rotencValue = 0; #endif + // Route firmware TRACE() output to host via WASM import + traceCallback = simuTrace; + // Init ADC driver callback adcInit(&simu_adc_driver); // Switches @@ -103,7 +86,6 @@ void simuInit() bool keysStates[MAX_KEYS] = { false }; void simuSetKey(uint8_t key, bool state) { - // TRACE("simuSetKey(%d, %d)", key, state); assert(key < DIM(keysStates)); keysStates[key] = state; } @@ -111,7 +93,6 @@ void simuSetKey(uint8_t key, bool state) bool trimsStates[MAX_TRIMS * 2] = { false }; void simuSetTrim(uint8_t trim, bool state) { - // TRACE("simuSetTrim(%d, %d)", trim, state); assert(trim < DIM(trimsStates)); trimsStates[trim] = state; } @@ -125,7 +106,7 @@ static void* bootloaderThread(void*) } #endif -void simuStart(bool tests, const char * sdPath, const char * settingsPath) +void simuStart(bool tests) { if (simu_running) return; @@ -137,8 +118,6 @@ void simuStart(bool tests, const char * sdPath, const char * settingsPath) startOptions = (tests ? 0 : OPENTX_START_NO_SPLASH | OPENTX_START_NO_CALIBRATION | OPENTX_START_NO_CHECKS); simu_shutdown = false; - simuFatfsSetPaths(sdPath, settingsPath); - /* g_tmr10ms must be non-zero otherwise some SF functions (that use this timer as a marker when it was last executed) will be executed twice on startup. Normal radio does not see this issue because g_tmr10ms is already a big number @@ -217,6 +196,42 @@ bool simuIsRunning() return simu_running; } +bool simuLcdChanged() +{ + bool changed = simuLcdRefresh; + simuLcdRefresh = false; + return changed; +} + +uint32_t simuLcdCopy(uint8_t* buf, uint32_t maxLen) +{ + uint32_t size = DISPLAY_BUFFER_SIZE * sizeof(pixel_t); + if (size > maxLen) size = maxLen; + memcpy(buf, simuLcdBuf, size); + return size; +} + +uint32_t simuLcdGetWidth() +{ + return LCD_W; +} + +uint32_t simuLcdGetHeight() +{ + return LCD_H; +} + +uint32_t simuLcdGetDepth() +{ +#if defined(COLORLCD) + return 16; +#elif LCD_W == 212 + return 4; +#else + return 1; +#endif +} + #if !defined(COLORLCD) void lcdSetRefVolt(uint8_t val) { @@ -278,7 +293,6 @@ uint32_t readKeys() for (int i = 0; i < MAX_KEYS; i++) { if (keysStates[i]) { - // TRACE("key pressed %d", i); result |= 1 << i; } } @@ -292,7 +306,6 @@ uint32_t readTrims() for (int i = 0; i < keysGetMaxTrims() * 2; i++) { if (trimsStates[i]) { - // TRACE("trim pressed %d", i); trims |= 1 << i; } } @@ -508,3 +521,238 @@ struct TouchState getInternalTouchState() return simTouchState; } #endif + +void simuTouchDown(int16_t x, int16_t y) +{ +#if defined(HARDWARE_TOUCH) + touchPanelDown(x, y); +#endif +} + +void simuTouchUp() +{ +#if defined(HARDWARE_TOUCH) + touchPanelUp(); +#endif +} + +void simuRotaryEncoderEvent(int32_t steps) +{ +#if defined(ROTARY_ENCODER_NAVIGATION) + rotencValue += steps * ROTARY_ENCODER_GRANULARITY; +#endif +} + +int32_t simuGetCapability(uint8_t cap) +{ + switch (cap) { + case 0: // CAP_LUA +#ifdef LUA + return 1; +#else + return 0; +#endif + case 1: // CAP_ROTARY_ENC + return 0; + case 2: // CAP_ROTARY_ENC_NAV +#ifdef ROTARY_ENCODER_NAVIGATION + return 1; +#else + return 0; +#endif + case 3: // CAP_TELEM_FRSKY_SPORT + return 1; + case 4: // CAP_SERIAL_AUX1 + return (auxSerialGetPort(SP_AUX1) != nullptr) ? 1 : 0; + case 5: // CAP_SERIAL_AUX2 + return (auxSerialGetPort(SP_AUX2) != nullptr) ? 1 : 0; + default: + return 0; + } +} + +void simuSetTrimValue(uint8_t idx, int32_t value) +{ + unsigned i = inputMappingConvertMode(idx); + uint8_t phase = getTrimFlightMode(getFlightMode(), i); + setTrimValue(phase, i, value); +} + +void simuSendTelemetry(uint8_t module, uint8_t protocol, + const uint8_t* data, uint32_t len) +{ + switch (protocol) { + case 0: // SIMU_TELEMETRY_PROTOCOL_FRSKY_SPORT + sportProcessTelemetryPacket(module, data, len); + break; + case 1: // SIMU_TELEMETRY_PROTOCOL_FRSKY_HUB + frskyDProcessPacket(module, data, len); + break; + case 2: // SIMU_TELEMETRY_PROTOCOL_CROSSFIRE + processCrossfireTelemetryFrame(module, (uint8_t*)data, len); + break; + case 3: // SIMU_TELEMETRY_PROTOCOL_FRSKY_HUB_OOB + if (len >= 3) { + uint8_t id = data[0]; + int16_t value = ((uint8_t)(data[2]) << 8) + (uint8_t)(data[1]); + processHubPacket(id, value); + } + break; + default: + break; + } +} + +void simuLuaReloadPermanentScripts() +{ +#if defined(LUA) + luaState = INTERPRETER_RELOAD_PERMANENT_SCRIPTS; +#endif +} + +void simuLcdFlushed() +{ + ::lcdFlushed(); +} + +uint8_t simuGetMaxTrainerChannels() +{ + return MAX_TRAINER_CHANNELS; +} + +void simuCopyTrainerInput(const int16_t* buf, uint8_t count) +{ + if (count > MAX_TRAINER_CHANNELS) + count = MAX_TRAINER_CHANNELS; + for (uint8_t i = 0; i < count; i++) { + int16_t v = buf[i]; + if (v < -512) v = -512; + if (v > 512) v = 512; + trainerInput[i] = v; + } +} + +void simuSetTrainerTimeout(uint16_t ms) +{ + trainerSetTimer(ms / 10); +} + +// -- Output values -- + +uint8_t simuGetNumChannels() +{ + return MAX_OUTPUT_CHANNELS; +} + +uint8_t simuCopyChannelOutputs(int16_t* buf, uint8_t maxCount) +{ + uint8_t n = MAX_OUTPUT_CHANNELS < maxCount ? MAX_OUTPUT_CHANNELS : maxCount; + memcpy(buf, channelOutputs, n * sizeof(int16_t)); + return n; +} + +uint8_t simuCopyMixOutputs(int16_t* buf, uint8_t maxCount) +{ + uint8_t n = MAX_OUTPUT_CHANNELS < maxCount ? MAX_OUTPUT_CHANNELS : maxCount; + memcpy(buf, ex_chans, n * sizeof(int16_t)); + return n; +} + +uint8_t simuGetNumLogicalSwitches() +{ + return MAX_LOGICAL_SWITCHES; +} + +uint8_t simuCopyLogicalSwitches(uint8_t* buf, uint8_t maxCount) +{ + uint8_t n = MAX_LOGICAL_SWITCHES < maxCount ? MAX_LOGICAL_SWITCHES : maxCount; + for (uint8_t i = 0; i < n; i++) + buf[i] = getSwitch(SWSRC_FIRST_LOGICAL_SWITCH + i, 0) ? 1 : 0; + return n; +} + +int32_t simuGetTrimValue(uint8_t idx) +{ + uint8_t phase = getFlightMode(); + uint8_t mapped = inputMappingConvertMode(idx); + return getTrimValue(getTrimFlightMode(phase, mapped), mapped); +} + +int16_t simuGetTrimRange() +{ + return g_model.extendedTrims ? TRIM_EXTENDED_MAX : TRIM_MAX; +} + +int32_t simuGetFlightMode() +{ + return getFlightMode(); +} + +uint8_t simuGetNumGVars() +{ +#if defined(GVARS) + return MAX_GVARS; +#else + return 0; +#endif +} + +uint8_t simuGetNumFlightModes() +{ + return MAX_FLIGHT_MODES; +} + +int32_t simuGetGVar(uint8_t gv, uint8_t fm) +{ +#if defined(GVARS) + if (gv < MAX_GVARS && fm < MAX_FLIGHT_MODES) { + uint8_t prec = g_model.gvars[gv].prec; + uint8_t unit = g_model.gvars[gv].unit; + int16_t value = (int16_t)GVAR_VALUE(gv, getGVarFlightMode(fm, gv)); + // Encode as gVarMode_t: value[15:0] | mode[23:16] | prec[25:24] | unit[27:26] + return (((unit & 0x3) << 26) | ((prec & 0x3) << 24) | + ((fm & 0xFF) << 16) | (value & 0xFFFF)); + } +#endif + return 0; +} + +bool simuGetBacklightState() +{ + return isBacklightEnabled(); +} + +// -- Custom (function) switches -- + +uint8_t simuGetNumCustomSwitches() +{ + return NUM_FUNCTIONS_SWITCHES; +} + +uint8_t simuGetCustomSwitchIndex(uint8_t cfsIdx) +{ +#if defined(FUNCTION_SWITCHES) + return switchGetSwitchFromCustomIdx(cfsIdx); +#else + (void)cfsIdx; + return 0; +#endif +} + +bool simuGetCustomSwitchState(uint8_t idx) +{ +#if defined(FUNCTION_SWITCHES) + if (idx < NUM_FUNCTIONS_SWITCHES) + return fsLedState(idx); +#endif + return false; +} + +uint32_t simuGetCustomSwitchColor(uint8_t idx) +{ +#if defined(FUNCTION_SWITCHES) + if (idx < NUM_FUNCTIONS_SWITCHES) + return fsGetLedRGB(idx); +#endif + return 0; +} diff --git a/radio/src/targets/simu/simulib.h b/radio/src/targets/simu/simulib.h new file mode 100644 index 000000000..5becb60c5 --- /dev/null +++ b/radio/src/targets/simu/simulib.h @@ -0,0 +1,168 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#pragma once + +#include +#include +#include + +#include + +#if __wasm__ +#define WASM_EXPORT_AS(name) __attribute__((export_name(name))) +#define WASM_EXPORT(symbol) WASM_EXPORT_AS(#symbol) symbol +#define WASM_IMPORT_AS(name) __attribute__((import_name(name))) +#define WASM_IMPORT(symbol) WASM_IMPORT_AS(#symbol) symbol +#else +#define WASM_EXPORT_AS(name) +#define WASM_EXPORT(symbol) symbol +#define WASM_IMPORT_AS(name) +#define WASM_IMPORT(symbol) symbol +#endif + +// -- WASM exports (called by host) -- + +// Lifecycle: call simuInit() once, then simuFatfsSetPaths() + simuStart(). +// Poll simuIsRunning() periodically. Call simuStop() to shut down. +void WASM_EXPORT(simuInit)(); +void WASM_EXPORT(simuStart)(bool tests = true); +void WASM_EXPORT(simuStop)(); +bool WASM_EXPORT(simuIsRunning)(); + +// Set SD card and settings paths before simuStart() to avoid STORAGE WARNING. +void WASM_EXPORT(simuFatfsSetPaths)(const char * sdPath, const char * settingsPath); + +// Input: keys use Board::Keys enum, switches use Board switch indices, +// trims use Board::TrimSwitches enum (momentary press, not value). +void WASM_EXPORT(simuSetKey)(uint8_t key, bool state); +void WASM_EXPORT(simuSetTrim)(uint8_t trim, bool state); +void WASM_EXPORT(simuSetSwitch)(uint8_t swtch, int8_t state); + +// Touch: call simuTouchDown(x,y) on press and repeatedly during drag +// (continuous position updates), then simuTouchUp() on release. +// The firmware detects slides from successive position changes. +void WASM_EXPORT(simuTouchDown)(int16_t x, int16_t y); +void WASM_EXPORT(simuTouchUp)(); + +// LCD: the firmware calls simuLcdNotify() (import) when a new frame is ready. +// The host waits for this notification, then calls simuLcdCopy() to read the +// framebuffer and simuLcdFlushed() to signal that the buffer can be reused. +// simuLcdChanged() is kept for backward compatibility (Companion polling). +// Depth is bits per pixel (1, 4, or 16). +bool WASM_EXPORT(simuLcdChanged)(); +uint32_t WASM_EXPORT(simuLcdCopy)(uint8_t* buf, uint32_t maxLen); +uint32_t WASM_EXPORT(simuLcdGetWidth)(); +uint32_t WASM_EXPORT(simuLcdGetHeight)(); +uint32_t WASM_EXPORT(simuLcdGetDepth)(); + +// Rotary encoder: positive steps = clockwise. The firmware handles +// mode inversion and key translation internally. +void WASM_EXPORT(simuRotaryEncoderEvent)(int32_t steps); + +// Capabilities: cap values match SimulatorInterface::Capability enum +// (0=LUA, 1=ROTARY_ENC, 2=ROTARY_ENC_NAV, 3=TELEM_FRSKY_SPORT, +// 4=SERIAL_AUX1, 5=SERIAL_AUX2). Returns 0 or 1. +int32_t WASM_EXPORT(simuGetCapability)(uint8_t cap); + +int WASM_EXPORT(simuAudioGetVolume)(); + +// Backlight state: returns true if backlight is currently enabled. +bool WASM_EXPORT(simuGetBacklightState)(); + +// Custom (function) switches: LED state and RGB color. +// idx is the custom switch index (0-based), not the global switch index. +// Use simuGetNumCustomSwitches() to get the count. +uint8_t WASM_EXPORT(simuGetNumCustomSwitches)(); +uint8_t WASM_EXPORT(simuGetCustomSwitchIndex)(uint8_t cfsIdx); +bool WASM_EXPORT(simuGetCustomSwitchState)(uint8_t idx); +uint32_t WASM_EXPORT(simuGetCustomSwitchColor)(uint8_t idx); + +// Value-based trim setting: idx is mapped through inputMappingConvertMode. +void WASM_EXPORT(simuSetTrimValue)(uint8_t idx, int32_t value); + +// Telemetry injection: protocol matches SIMU_TELEMETRY_PROTOCOL_* enum. +// module: 0=internal, 1=external. +void WASM_EXPORT(simuSendTelemetry)(uint8_t module, uint8_t protocol, + const uint8_t* data, uint32_t len); + +// Lua: trigger reload of permanent scripts. +void WASM_EXPORT(simuLuaReloadPermanentScripts)(); + +// LCD: notify firmware that host has consumed the LCD buffer. +void WASM_EXPORT(simuLcdFlushed)(); + +// Trainer input: bulk copy values (-512..512) into trainerInput[]. +// Returns the max number of channels. +uint8_t WASM_EXPORT(simuGetMaxTrainerChannels)(); +void WASM_EXPORT(simuCopyTrainerInput)(const int16_t* buf, uint8_t count); +void WASM_EXPORT(simuSetTrainerTimeout)(uint16_t ms); + +// Output values: polled periodically by host (every ~50ms). + +// Bulk copy channel outputs into buf (int16_t[]). Returns channel count. +uint8_t WASM_EXPORT(simuGetNumChannels)(); +uint8_t WASM_EXPORT(simuCopyChannelOutputs)(int16_t* buf, uint8_t maxCount); +uint8_t WASM_EXPORT(simuCopyMixOutputs)(int16_t* buf, uint8_t maxCount); + +// Bulk copy logical switch states into buf (uint8_t[], 0 or 1). Returns count. +uint8_t WASM_EXPORT(simuGetNumLogicalSwitches)(); +uint8_t WASM_EXPORT(simuCopyLogicalSwitches)(uint8_t* buf, uint8_t maxCount); + +// Trim values. idx 0..TRIM_AXIS_COUNT-1 (typically 8). +// Returns the trim value for the current flight mode. +int32_t WASM_EXPORT(simuGetTrimValue)(uint8_t idx); +int16_t WASM_EXPORT(simuGetTrimRange)(); + +// Flight mode: returns current flight mode index (0-based). +int32_t WASM_EXPORT(simuGetFlightMode)(); + +// GVars: gv 0..N-1, fm 0..M-1. Returns encoded gVarMode_t. +uint8_t WASM_EXPORT(simuGetNumGVars)(); +uint8_t WASM_EXPORT(simuGetNumFlightModes)(); +int32_t WASM_EXPORT(simuGetGVar)(uint8_t gv, uint8_t fm); + +// -- WASM imports (provided by host) -- + +// simuGetAnalog: return ADC-range value for input at index idx. +// All inputs use 0..4096 range (center=2048), same as SDL simulator. +uint16_t WASM_IMPORT(simuGetAnalog)(uint8_t idx); +void WASM_IMPORT(simuQueueAudio)(const uint8_t* buf, uint32_t len); + +// simuTrace: send debug/trace output text to the host for display. +// Called via traceCallback from debugPrintf(). +void WASM_IMPORT(simuTrace)(const char* text); + +// simuLcdNotify: signal the host that the LCD framebuffer has been updated +// and is ready to be copied. Called from lcdRefresh() (mono) and the LVGL +// flush callback (color). On the host side this wakes an Atomics.waitAsync +// listener so the frame can be rendered without polling. +void WASM_IMPORT(simuLcdNotify)(); + +// -- Internal (not exported) -- +void simuMain(); +std::string simuFatfsGetCurrentPath(); +std::string simuFatfsGetRealPath(const std::string& p); + +#if defined(HARDWARE_TOUCH) + extern struct TouchState simTouchState; + extern bool simTouchOccured; +#endif diff --git a/radio/src/targets/simu/switch_driver.cpp b/radio/src/targets/simu/switch_driver.cpp index a6900a8cd..580953016 100644 --- a/radio/src/targets/simu/switch_driver.cpp +++ b/radio/src/targets/simu/switch_driver.cpp @@ -19,6 +19,8 @@ * GNU General Public License for more details. */ +#include "simulib.h" + #include "hal/switch_driver.h" #include "definitions.h" #include "myeeprom.h" diff --git a/radio/src/tests/CMakeLists.txt b/radio/src/tests/CMakeLists.txt index 1fe933fdf..28d7d7108 100644 --- a/radio/src/tests/CMakeLists.txt +++ b/radio/src/tests/CMakeLists.txt @@ -18,7 +18,6 @@ file(GLOB TEST_SRC_FILES ${RADIO_SRC_DIR}/tests/*.cpp CONFIGURE_DEPENDS "${RADIO_SRC_DIR}/tests/*.cpp") set(TEST_SRC_FILES ${TEST_SRC_FILES} - ${CMAKE_CURRENT_SOURCE_DIR}/location.h ${SIMU_SRC} ) diff --git a/radio/src/tests/gtests.cpp b/radio/src/tests/gtests.cpp index 16031616c..46ee6f578 100644 --- a/radio/src/tests/gtests.cpp +++ b/radio/src/tests/gtests.cpp @@ -122,12 +122,10 @@ class TersePrinter : public EmptyTestEventListener { int32_t lastAct = 0; -uint16_t simu_get_analog(uint8_t idx) -{ - return 0; -} - -void simuQueueAudio(const uint8_t*, uint32_t) {} +uint16_t simuGetAnalog(uint8_t) { return 0; } +void simuQueueAudio(const uint8_t *, uint32_t) {} +void simuTrace(const char* text) {} +void simuLcdNotify() {} static char _stringResult[200]; diff --git a/radio/src/tests/gtests.h b/radio/src/tests/gtests.h index af213581b..74f3bb74d 100644 --- a/radio/src/tests/gtests.h +++ b/radio/src/tests/gtests.h @@ -25,6 +25,7 @@ #include #include "edgetx.h" +#include "simulib.h" #include "model_init.h" #include "switches.h" #include "hal/switch_driver.h" diff --git a/radio/src/tests/lcd.cpp b/radio/src/tests/lcd.cpp index 03a96092c..5bb577ad8 100644 --- a/radio/src/tests/lcd.cpp +++ b/radio/src/tests/lcd.cpp @@ -25,7 +25,7 @@ #include #if !defined(COLORLCD) -#include "simpgmspace.h" +#include "simulib.h" #include "simulcd.h" #include "common/stdlcd/draw_functions.h" diff --git a/radio/src/tests/lcd_480x272.cpp b/radio/src/tests/lcd_480x272.cpp index e59ed5c27..8cff1ed1b 100644 --- a/radio/src/tests/lcd_480x272.cpp +++ b/radio/src/tests/lcd_480x272.cpp @@ -24,7 +24,7 @@ #if defined(COLORLCD) -#include "simpgmspace.h" +#include "simulib.h" #include "bitmaps.h" #include "debug.h" diff --git a/radio/src/thirdparty/Lua/src/luaconf.h b/radio/src/thirdparty/Lua/src/luaconf.h index 158f64081..4f679b032 100644 --- a/radio/src/thirdparty/Lua/src/luaconf.h +++ b/radio/src/thirdparty/Lua/src/luaconf.h @@ -20,6 +20,10 @@ // #include "user_config.h" +#if __wasm__ + #define l_signalT long +#endif + // prevent localeconv() #define getlocaledecpoint() ('.') diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..92612f897 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +clang +jinja2 +pillow +lz4 +pydantic diff --git a/tools/build-companion.sh b/tools/build-companion.sh index d01229455..d3c17ab5f 100755 --- a/tools/build-companion.sh +++ b/tools/build-companion.sh @@ -1,7 +1,7 @@ #!/bin/bash SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -. "$SCRIPT_DIR/build-common.sh" +. "$SCRIPT_DIR/build-common.sh" SRCDIR=$1 OUTDIR=$2 @@ -14,23 +14,24 @@ if [[ -z ${OUTDIR} ]]; then OUTDIR="$(pwd)/output" fi +if [[ ! -d "${OUTDIR}" ]]; then + mkdir -p "${OUTDIR}" +fi + # Determine parallel jobs determine_max_jobs -# Create a master log file for the entire build process -MASTER_LOG="build-summary.log" - QUIET_FLAGS="" if [[ "$CMAKE_GENERATOR" == "Ninja" ]]; then - QUIET_FLAGS="-- --quiet" + QUIET_FLAGS="-- --quiet" else - # Assume Makefile generator for non-Ninja builds - COMMON_OPTIONS="${COMMON_OPTIONS} -DCMAKE_RULE_MESSAGES=OFF" + # Assume Makefile generator for non-Ninja builds + COMMON_OPTIONS="${COMMON_OPTIONS} -DCMAKE_RULE_MESSAGES=OFF" fi -COMMON_OPTIONS="${COMMON_OPTIONS} -DCMAKE_BUILD_TYPE=Release -DCMAKE_MESSAGE_LOG_LEVEL=WARNING -Wno-dev -DGVARS=YES -DHELI=YES -DLUA=YES" +COMMON_OPTIONS="${COMMON_OPTIONS} -DCMAKE_BUILD_TYPE=Release -DCMAKE_MESSAGE_LOG_LEVEL=WARNING -Wno-dev" if [ "$(uname)" = "Darwin" ]; then - COMMON_OPTIONS="${COMMON_OPTIONS} -DCMAKE_OSX_DEPLOYMENT_TARGET='10.15'" + COMMON_OPTIONS="${COMMON_OPTIONS} -DCMAKE_OSX_DEPLOYMENT_TARGET='11.0'" fi # Generate EDGETX_VERSION_SUFFIX if not already set @@ -53,103 +54,6 @@ fi rm -rf build && mkdir build && cd build -# Function to output error logs (works in both GitHub Actions and terminal) -output_error_log() { - local log_file="$1" - local context="$2" - - if [[ -f "$log_file" ]]; then - echo "------------------------------------------" - echo " Full error output from $log_file ($context):" - echo "------------------------------------------" - cat "$log_file" - echo "------------------------------------------" - - # Also append to master log for aggregation - { - echo "=== Error from $log_file ($context) ===" - cat "$log_file" - echo "=== End of $log_file ===" - } >> "$MASTER_LOG" - else - echo "⚠️ Warning: Log file $log_file not found for $context" - echo "⚠️ Warning: Log file $log_file not found for $context" >> "$MASTER_LOG" - fi -} - -# Function to show last N lines of a log file for warnings -show_log_summary() { - local log_file="$1" - local lines="${2:-50}" - local context="$3" - - if [[ -f "$log_file" ]]; then - echo "------------------------------------------" - echo "Last $lines lines from $log_file:" - echo "------------------------------------------" - tail -n "$lines" "$log_file" - fi -} - -run_pipeline() { - local pipeline_type="$1" - local log_file="${2:-/dev/null}" - local context="$3" - local show_details="${4:-false}" - local cmake_opts="${QUIET_FLAGS}" - - case "$pipeline_type" in - "plugin") - clean_build - if ! execute_with_output "⚙️ Configuring" "cmake ${BUILD_OPTIONS} ${SRCDIR}" "$log_file" "$show_details"; then - output_error_log "$log_file" "$context (Configuration)" - return 1 - fi - if ! execute_with_output "🔧 Native config" "cmake_build_parallel . --target native-configure ${cmake_opts}" "$log_file" "$show_details"; then - output_error_log "$log_file" "$context (Native Configure)" - return 1 - fi - if ! execute_with_output "📦 Building lib" "cmake_build_parallel native --target libsimulator ${cmake_opts}" "$log_file" "$show_details"; then - output_error_log "$log_file" "$context (Library Build)" - return 1 - fi - ;; - "final") - if ! execute_with_output "🔧 Final config" "cmake_build_parallel . --target native-configure ${cmake_opts}" "$log_file" "$show_details"; then - output_error_log "$log_file" "Final Configuration" - return 1 - fi - if ! execute_with_output "📦 Building companion" "cmake_build_parallel native --target companion ${cmake_opts}" "$log_file" "$show_details"; then - output_error_log "$log_file" "$context (Companion Build)" - return 1 - fi - if ! execute_with_output "📦 Packaging" "cmake_build_parallel native --target ${PACKAGE_TARGET}" "$log_file" "true"; then - output_error_log "$log_file" "Final Packaging" - return 1 - fi - ;; - esac - - return 0 -} - -execute_with_output() { - local description="$1" - local command="$2" - local log_file="$3" - local show_output="${4:-false}" - - if [[ "$show_output" == "true" && "$log_file" != "/dev/null" ]]; then - echo " $description..." - fi - - eval "$command" >> "$log_file" 2>&1 -} - -clean_build() { - rm -f CMakeCache.txt native/CMakeCache.txt -} - get_platform_config() { local platform=$(uname) case "$platform" in @@ -174,139 +78,50 @@ get_platform_config() { esac } -# Enhanced plugin builder with better error handling -build_plugin() { - local plugin="$1" - local log_file="build_${plugin}.log" - local verbose="${2:-false}" - - BUILD_OPTIONS="${COMMON_OPTIONS} " - - if ! get_target_build_options "$plugin" >> "$log_file" 2>&1; then - if [[ -n "$GITHUB_ACTIONS" ]]; then - echo "::error::Failed to get build options for $plugin" - fi - output_error_log "$log_file" "$plugin (Build Options)" - return 1 - fi - - # Only show detailed output in GitHub Actions or if verbose is requested - local show_details="false" - if [[ -n "$GITHUB_ACTIONS" || "$verbose" == "true" ]]; then - show_details="true" - fi - - if ! run_pipeline "plugin" "$log_file" "$plugin" "$show_details"; then - return 1 - fi - - # Check for warnings and show summary if found - if grep -q -i "warning" "$log_file"; then - echo " ⚠️ $plugin completed with warnings" - if [[ "$show_details" == "true" ]]; then - show_log_summary "$log_file" 50 "$plugin (Warnings)" - fi - return 0 - fi - - echo " ✅ $plugin completed successfully" - return 0 -} - -declare -a simulator_plugins=( - x9lite x9lites x9d x9dp x9dp2019 x9e - x7 x7access - t8 t12 t12max tx12 tx12mk2 t15 t15pro t16 t18 t20 t20v2 - xlite xlites - x10 x10express x12s - zorro tx16s tx16smk3 tx15 - commando8 boxer pocket mt12 gx12 - tlite tpro tprov2 tpros bumblebee lr3pro t14 - nv14 el18 pl18 pl18ev pl18u st16 pa01 - f16 v14 v16 -) - get_platform_config -TOTAL=${#simulator_plugins[@]} -FAILED_PLUGINS=() -echo "🔨 Building $TOTAL Plugins for $PACKAGE_NAME" - -for i in "${!simulator_plugins[@]}"; do - plugin="${simulator_plugins[$i]}" - current=$((i + 1)) - percent=$((current * 100 / TOTAL)) - - # For terminal output, put each plugin on its own line for cleaner display - if [[ -n "$GITHUB_ACTIONS" ]]; then - printf "::group::%s %-12s [%2d/%2d] %3d%%\n" "$PACKAGE_EMOJI" "$plugin" "$current" "$TOTAL" "$percent" - else - echo "🔨 [$current/$TOTAL] ($percent%) Building $plugin..." - fi - - error_status=0 - if ! build_plugin "$plugin"; then - FAILED_PLUGINS+=("$plugin") - error_status=1 - fi - - if [[ -n "$GITHUB_ACTIONS" ]]; then - echo "::endgroup::" - fi - - if [ $error_status -ne 0 ]; then - # TODO: if not '--build-all' used - exit 1 - fi -done - -# Show summary of any failures (if using continue-on-error approach) -if [ ${#FAILED_PLUGINS[@]} -gt 0 ]; then - if [[ -n "$GITHUB_ACTIONS" ]]; then - echo "::group::❌ Build Failures Summary" - else - echo "❌ Build Failures Summary" - echo "==========================================" - fi - - echo "The following plugins failed to build:" - for failed_plugin in "${FAILED_PLUGINS[@]}"; do - echo " ❌ $failed_plugin" - done - - if [[ -n "$GITHUB_ACTIONS" ]]; then - echo "::endgroup::" - else - echo "==========================================" - fi - exit 1 -fi +BUILD_OPTIONS="${COMMON_OPTIONS} -DEdgeTX_SUPERBUILD:BOOL=0 -DNATIVE_BUILD:BOOL=1" +LOG_FILE="build-companion.log" if [[ -n "$GITHUB_ACTIONS" ]]; then - echo "::group::📦 Final $PACKAGE_NAME Package" + echo "::group::📦 Building $PACKAGE_NAME" else - echo "📦 Final $PACKAGE_NAME Package" + echo "📦 Building $PACKAGE_NAME" echo "==========================================" fi -error_status=0 -if run_pipeline "final" "final.log" "companion" "true"; then - if cp native/$PACKAGE_FILES "${OUTDIR}" 2>/dev/null; then - echo " ✅ All builds completed successfully!" - echo " 📁 Package saved to: ${OUTDIR}" - else - echo " ❌ Failed to copy package files to output directory" - echo " 📁 Directory Contents:" - echo " ----------------------" +clean_build() { + rm -f CMakeCache.txt native/CMakeCache.txt +} - echo "Contents of native/ directory:" - ls -la native/ || echo "native/ directory not found" - echo "Looking for files matching: $PACKAGE_FILES" - find native/ -name "$PACKAGE_FILES" 2>/dev/null || echo "No matching files found" - error_status=1 - fi +clean_build && mkdir -p native/plugins + +# Copy WASM simulator modules if available +if [[ -d "${SRCDIR}/wasm-modules" ]]; then + cp "${SRCDIR}"/wasm-modules/*.wasm native/plugins/ 2>/dev/null && \ + echo " 🔌 Copied WASM modules to plugins/" || true +fi + +error_status=0 + +if ! cmake -B native -S "${SRCDIR}" --toolchain cmake/toolchain/native.cmake ${BUILD_OPTIONS} >> "$LOG_FILE" 2>&1; then + echo " ❌ CMake configuration failed" + cat "$LOG_FILE" + error_status=1 +elif ! cmake_build_parallel native --target companion ${QUIET_FLAGS} >> "$LOG_FILE" 2>&1; then + echo " ❌ Companion build failed" + cat "$LOG_FILE" + error_status=1 +elif ! cmake_build_parallel native --target ${PACKAGE_TARGET} >> "$LOG_FILE" 2>&1; then + echo " ❌ Packaging failed" + cat "$LOG_FILE" + error_status=1 +elif cp native/$PACKAGE_FILES "${OUTDIR}" 2>/dev/null; then + echo " ✅ Build completed successfully!" + echo " 📁 Package saved to: ${OUTDIR}" else - echo " ❌ Final packaging failed" + echo " ❌ Failed to copy package files to output directory" + ls -la native/ || echo "native/ directory not found" error_status=1 fi diff --git a/tools/build-wasm-modules.sh b/tools/build-wasm-modules.sh new file mode 100755 index 000000000..da5c6a762 --- /dev/null +++ b/tools/build-wasm-modules.sh @@ -0,0 +1,243 @@ +#!/bin/bash + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +. "$SCRIPT_DIR/build-common.sh" + +SRCDIR=$1 +OUTDIR=$2 + +if [[ -z ${SRCDIR} ]]; then + SRCDIR="$(pwd)" +fi + +if [[ -z ${OUTDIR} ]]; then + OUTDIR="$(pwd)/output" +fi + +if [[ ! -d "${OUTDIR}" ]]; then + mkdir -p "${OUTDIR}" +fi + +QUIET_FLAGS="" +if [[ "$CMAKE_GENERATOR" == "Ninja" ]]; then + QUIET_FLAGS="-- --quiet" +else + # Assume Makefile generator for non-Ninja builds + COMMON_OPTIONS="-DCMAKE_RULE_MESSAGES=OFF" +fi + +COMMON_OPTIONS+=" -DCMAKE_BUILD_TYPE=Release -DCMAKE_MESSAGE_LOG_LEVEL=WARNING -Wno-dev" +COMMON_OPTIONS+=" -DCMAKE_MODULE_PATH=/opt/wasi-sdk/share/cmake/" +COMMON_OPTIONS+=" -DEdgeTX_SUPERBUILD:BOOL=0 -DNATIVE_BUILD:BOOL=1" + +# Generate EDGETX_VERSION_SUFFIX if not already set +if [[ -z ${EDGETX_VERSION_SUFFIX} ]]; then + gh_type=$(echo "$GITHUB_REF" | awk -F / '{print $2}') #heads|tags|pull + if [[ $gh_type = "tags" ]]; then + # tags: refs/tags/ + gh_tag=${GITHUB_REF##*/} + export EDGETX_VERSION_TAG=$gh_tag + elif [[ $gh_type = "pull" ]]; then + # pull: refs/pull//merge + gh_pull_number=PR$(echo "$GITHUB_REF" | awk -F / '{print $3}') + export EDGETX_VERSION_SUFFIX=$gh_pull_number + elif [[ $gh_type = "heads" ]]; then + # heads: refs/heads/ + gh_branch=${GITHUB_REF##*/} + export EDGETX_VERSION_SUFFIX=$gh_branch + fi +fi + +if [[ -n "$GITHUB_ACTIONS" ]]; then + MAX_JOBS=${MAX_JOBS:-3} +fi + +BUILD_DIR="build/wasm" + +# Function to output error logs (works in both GitHub Actions and terminal) +output_error_log() { + local log_file="$1" + local context="$2" + + if [[ -f "$log_file" ]]; then + echo "------------------------------------------" + echo " Full error output from $log_file:" + echo "------------------------------------------" + cat "$log_file" + else + echo "⚠️ Warning: Log file $log_file not found for $context" + fi +} + +# Function to show last N lines of a log file for warnings +show_log_summary() { + local log_file="$1" + local lines="${2:-50}" + local context="$3" + + if [[ -f "$log_file" ]]; then + echo "------------------------------------------" + echo "Last $lines lines from $log_file:" + echo "------------------------------------------" + tail -n "$lines" "$log_file" + fi +} + +run_pipeline() { + local log_file="${1:-/dev/null}" + local context="$2" + local show_details="${3:-false}" + local cmake_opts="--parallel ${MAX_JOBS} ${QUIET_FLAGS}" + local wasi_toolchain="/opt/wasi-sdk/share/cmake/wasi-sdk-pthread.cmake" + + clean_build + if ! execute_with_output "🔧 CMake config" "cmake -S ${SRCDIR} -B ${BUILD_DIR} --toolchain ${wasi_toolchain} ${BUILD_OPTIONS}" "$log_file" "$show_details"; then + output_error_log "$log_file" "$context (Configuration)" + return 1 + fi + if ! execute_with_output "📦 Building WASM module" "cmake --build ${BUILD_DIR} --target wasi-module ${cmake_opts}" "$log_file" "$show_details"; then + output_error_log "$log_file" "$context (Library Build)" + return 1 + fi + + return 0 +} + +execute_with_output() { + local description="$1" + local command="$2" + local log_file="$3" + local show_output="${4:-false}" + + if [[ "$show_output" == "true" && "$log_file" != "/dev/null" ]]; then + echo " $description..." + fi + + eval "$command" >> "$log_file" 2>&1 +} + +clean_build() { + rm -f "${BUILD_DIR}/CMakeCache.txt" +} + +# Enhanced plugin builder with better error handling +build_plugin() { + local plugin="$1" + local log_file="${OUTDIR}/build_${plugin}.log" + local verbose="${2:-false}" + + BUILD_OPTIONS="${COMMON_OPTIONS} " + + if ! get_target_build_options "$plugin" >> "$log_file" 2>&1; then + if [[ -n "$GITHUB_ACTIONS" ]]; then + echo "::error::Failed to get build options for $plugin" + fi + output_error_log "$log_file" "$plugin (Build Options)" + return 1 + fi + + # Only show detailed output in GitHub Actions or if verbose is requested + local show_details="false" + if [[ -n "$GITHUB_ACTIONS" || "$verbose" == "true" ]]; then + show_details="true" + fi + + if ! run_pipeline "$log_file" "$plugin" "$show_details"; then + return 1 + fi + + # FLAVOUR may differ from target name (e.g. x9dp2019 -> x9d+2019), + # so copy whatever .wasm was produced. + cp ${BUILD_DIR}/edgetx-*-simulator.wasm "${OUTDIR}/" 2>/dev/null + + # Check for warnings and show summary if found + if grep -q -i "warning" "$log_file"; then + echo " ⚠️ $plugin completed with warnings" + if [[ "$show_details" == "true" ]]; then + show_log_summary "$log_file" 50 "$plugin (Warnings)" + fi + return 0 + fi + + echo " ✅ $plugin completed successfully" + return 0 +} + +if [[ -n "$FLAVOR" ]]; then + # Convert semicolon-separated string to array + IFS=';' read -ra temp_array <<< "$FLAVOR" + plugins=() + for item in "${temp_array[@]}"; do + plugins+=($(echo "$item" | tr '[:upper:]' '[:lower:]')) + done +else + declare -a plugins=( + # monochrome + boxer bumblebee commando8 + gx12 mt12 pocket t12max + t14 t20 t20v2 tpros tprov2 + tx12mk2 zorro v12 v14 + x7access x9dp2019 x9e + # colour + el18 nb4p nv14 st16 pa01 + pl18 pl18ev pl18u + t15 t15pro t16 t18 + tx15 tx16s tx16smk3 f16 v16 + x10 x10express x12s + ) +fi + +TOTAL=${#plugins[@]} +FAILED_PLUGINS=() + +echo "🔨 Building $TOTAL Plugins" + +for i in "${!plugins[@]}"; do + plugin="${plugins[$i]}" + current=$((i + 1)) + percent=$((current * 100 / TOTAL)) + + # For terminal output, put each plugin on its own line for cleaner display + if [[ -n "$GITHUB_ACTIONS" ]]; then + printf "::group::📦 %-12s [%2d/%2d] %3d%%\n" "$plugin" "$current" "$TOTAL" "$percent" + else + echo "🔨 [$current/$TOTAL] ($percent%) Building $plugin..." + fi + + error_status=0 + if ! build_plugin "$plugin"; then + FAILED_PLUGINS+=("$plugin") + error_status=1 + fi + + if [[ -n "$GITHUB_ACTIONS" ]]; then + echo "::endgroup::" + fi + + if [ $error_status -ne 0 ]; then + # TODO: if not '--build-all' used + exit 1 + fi +done + +# Show summary of any failures (if using continue-on-error approach) +if [ ${#FAILED_PLUGINS[@]} -gt 0 ]; then + if [[ -n "$GITHUB_ACTIONS" ]]; then + echo "::group::❌ Build Failures Summary" + else + echo "❌ Build Failures Summary" + echo "==========================================" + fi + + echo "The following plugins failed to build:" + for failed_plugin in "${FAILED_PLUGINS[@]}"; do + echo " ❌ $failed_plugin" + done + + if [[ -n "$GITHUB_ACTIONS" ]]; then + echo "::endgroup::" + else + echo "==========================================" + fi + exit 1 +fi diff --git a/tools/commit-tests.sh b/tools/commit-tests.sh index 0d2e7802d..be09b3f18 100755 --- a/tools/commit-tests.sh +++ b/tools/commit-tests.sh @@ -59,12 +59,8 @@ do cmake ${BUILD_OPTIONS} "${SRCDIR}" - cmake_build_parallel . --target arm-none-eabi-configure - cmake_build_parallel arm-none-eabi --target ${FIRMARE_TARGET} - cmake_build_parallel . --target native-configure - cmake_build_parallel native --target libsimulator cmake_build_parallel native --target tests-radio - rm -f CMakeCache.txt native/CMakeCache.txt arm-none-eabi/CMakeCache.txt + rm -f CMakeCache.txt native/CMakeCache.txt done