diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d4a4c31..a005d9a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,3 +1,6 @@ +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + name: CMake Build on: [push, pull_request] @@ -37,6 +40,23 @@ jobs: shell: pwsh run: curl --ssl-no-revoke "https://web.archive.org/web/20250820134143/https://ftdichip.com/wp-content/uploads/2023/09/CDM-v2.12.36.4-WHQL-Certified.zip" -o "third-party\CDM-v2.12.36.4-WHQL-Certified.zip" + fetch-ftdi-windows-arm64: + runs-on: windows-11-arm + steps: + - uses: actions/checkout@v6 + + - name: Cache FTDI archive for ARM64 + id: cache-ftdi-windows-arm64 + uses: actions/cache@v5 + with: + path: third-party\CDM-v2.12.36.20-for-ARM64-WHQL-Certified.zip + key: ftdi-windows-arm64-2.12.36.20 + + - name: Download FTDI archive for ARM64 + if: steps.cache-ftdi-windows-arm64.outputs.cache-hit != 'true' + shell: pwsh + run: curl --ssl-no-revoke "https://web.archive.org/web/20250821211500/https://ftdichip.com/wp-content/uploads/2025/03/CDM-v2.12.36.20-for-ARM64-WHQL-Certified.zip" -o "third-party\CDM-v2.12.36.20-for-ARM64-WHQL-Certified.zip" + build-linux: needs: fetch-ftdi-linux runs-on: ${{ matrix.os }} @@ -45,7 +65,6 @@ jobs: matrix: os: [ubuntu-22.04, ubuntu-latest] qt_version: ['6.9.2', '6.10.0'] - build_type: [Debug, Release] steps: - uses: actions/checkout@v6 @@ -53,7 +72,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y python3-pip wget libxkbcommon-x11-0 x11-common libxcb-xinerama0-dev build-essential libgl1-mesa-dev mesa-common-dev uuid-dev libpulse-dev + sudo apt-get install -y python3-pip wget libxkbcommon-x11-0 x11-common libxcb-xinerama0-dev build-essential libgl1-mesa-dev mesa-common-dev uuid-dev libpulse-dev ninja-build - name: Cache Qt installation on Linux uses: actions/cache@v5 @@ -66,10 +85,8 @@ jobs: - name: Install Qt ${{ matrix.qt_version }} on ${{ matrix.os }} run: | - aqt install-qt linux desktop ${{ matrix.qt_version }} linux_gcc_64 aqt install-qt linux desktop ${{ matrix.qt_version }} linux_gcc_64 -m qtmultimedia qtserialport - echo "QT_ROOT_DIR=$HOME/work/qcom-test-automation-controller/qcom-test-automation-controller/${{ matrix.qt_version }}/gcc_64" >> $GITHUB_ENV - echo "$HOME/work/qcom-test-automation-controller/qcom-test-automation-controller/${{ matrix.qt_version }}/gcc_64/bin" >> $GITHUB_PATH + echo "QTBIN=$HOME/work/qcom-test-automation-controller/qcom-test-automation-controller/${{ matrix.qt_version }}/gcc_64/bin" >> $GITHUB_ENV - name: Restore FTDI archive on Linux uses: actions/cache/restore@v5 @@ -77,18 +94,8 @@ jobs: path: third-party/libftd2xx-linux-x86_64-1.4.33.tgz key: ftdi-linux-1.4.33 - - name: Extract FTDI library on Linux - run: | - mkdir -p __Builds/Linux/Debug/lib __Builds/Linux/Release/lib - tar -xvf third-party/libftd2xx-linux-x86_64-1.4.33.tgz -C third-party/ - find third-party/ -name "libftd2xx-static.a" -exec cp {} __Builds/Linux/Debug/lib/libftd2xx.a \; - cp __Builds/Linux/Debug/lib/libftd2xx.a __Builds/Linux/Release/lib/libftd2xx.a - - - name: Configure CMake for ${{ matrix.build_type }} - run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCMAKE_PREFIX_PATH="$QT_ROOT_DIR/lib/cmake" - - - name: Build with ${{ matrix.build_type }} - run: cmake --build build --config ${{ matrix.build_type }} + - name: Build + run: ./build.sh build-windows: needs: fetch-ftdi-windows @@ -97,7 +104,6 @@ jobs: fail-fast: false matrix: qt_version: ['6.9.2', '6.10.0'] - build_type: [Debug, Release] steps: - uses: actions/checkout@v6 @@ -116,10 +122,7 @@ jobs: shell: pwsh run: | aqt install-qt windows desktop ${{ matrix.qt_version }} win64_msvc2022_64 --outputdir "C:\Qt" -m qtserialport qtmultimedia - - $qtRoot = "C:\Qt\${{ matrix.qt_version }}\msvc2022_64" - echo "QT_ROOT_DIR=$qtRoot" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - echo "$qtRoot\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + echo "QTBIN=C:\Qt\${{ matrix.qt_version }}\msvc2022_64\bin" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - name: Restore FTDI archive on Windows uses: actions/cache/restore@v5 @@ -127,34 +130,43 @@ jobs: path: third-party\CDM-v2.12.36.4-WHQL-Certified.zip key: ftdi-windows-2.12.36.4 - - name: Extract FTDI library on Windows - shell: pwsh - run: | - $debugLib = "__Builds\x64\Debug\lib" - $releaseLib = "__Builds\x64\Release\lib" - $debugBin = "__Builds\x64\Debug\bin" - $releaseBin = "__Builds\x64\Release\bin" - New-Item -ItemType Directory -Force -Path $debugLib, $releaseLib, $debugBin, $releaseBin | Out-Null - - $tmp = "third-party\ftdi_extract" - Expand-Archive -Path "third-party\CDM-v2.12.36.4-WHQL-Certified.zip" -DestinationPath $tmp -Force - - $lib = Get-ChildItem -Path $tmp -Filter "ftd2xx.lib" -Recurse | Where-Object { $_.FullName -match "amd64" } | Select-Object -First 1 - if (-not $lib) { $lib = Get-ChildItem -Path $tmp -Filter "ftd2xx.lib" -Recurse | Select-Object -First 1 } - Copy-Item $lib.FullName -Destination $debugLib - Copy-Item $lib.FullName -Destination $releaseLib - - $dll = Get-ChildItem -Path $tmp -Filter "FTD2XX64.dll" -Recurse | Select-Object -First 1 - if ($dll) { - Copy-Item $dll.FullName -Destination "$debugBin\ftd2xx.dll" - Copy-Item $dll.FullName -Destination "$releaseBin\ftd2xx.dll" - } - Remove-Item -Recurse -Force $tmp - - - name: Configure CMake for ${{ matrix.build_type }} + - name: Build + shell: cmd + run: build.bat + + build-windows-arm64: + needs: fetch-ftdi-windows-arm64 + runs-on: windows-11-arm + strategy: + fail-fast: false + matrix: + qt_version: ['6.9.2', '6.10.0'] + + steps: + - uses: actions/checkout@v6 + + - name: Install Python dependencies shell: pwsh - run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCMAKE_PREFIX_PATH="$env:QT_ROOT_DIR\lib\cmake" + run: pip install aqtinstall - - name: Build with ${{ matrix.build_type }} + - name: Cache Qt installation for ARM64 + uses: actions/cache@v5 + with: + path: C:\Qt\${{ matrix.qt_version }}\msvc2022_arm64 + key: windows-arm64-qt-${{ matrix.qt_version }} + + - name: Install Qt ${{ matrix.qt_version }} for MSVC ARM64 shell: pwsh - run: cmake --build build --config ${{ matrix.build_type }} + run: | + aqt install-qt windows_arm64 desktop ${{ matrix.qt_version }} win64_msvc2022_arm64 --outputdir "C:\Qt" --autodesktop -m qtserialport qtmultimedia + echo "QTBIN=C:\Qt\${{ matrix.qt_version }}\msvc2022_arm64\bin" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Restore FTDI archive for ARM64 + uses: actions/cache/restore@v5 + with: + path: third-party\CDM-v2.12.36.20-for-ARM64-WHQL-Certified.zip + key: ftdi-windows-arm64-2.12.36.20 + + - name: Build + shell: cmd + run: build.bat diff --git a/.gitignore b/.gitignore index f8e3c62..60e7882 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,13 @@ ui_* **/*/dist __pycache__/ .pytest_cache/ +*.egg-info/ +*.pyc +*.pyo +*.pyd + +# MCP logs +examples/MCP/**/*.log # Qt build directory __Builds/ diff --git a/CMakeLists.txt b/CMakeLists.txt index d1f3386..fdbc9d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,36 +1,5 @@ -# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted (subject to the limitations in the -# disclaimer below) provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# * Neither the name of Qualcomm Technologies, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE -# GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT -# HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED -# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE -# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER -# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN -# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -# Author: Biswajit Roy (biswroy@qti.qualcomm.com) +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause cmake_minimum_required(VERSION 3.22) project(QTAC-Workspace VERSION 1.0 LANGUAGES CXX) diff --git a/README.md b/README.md index a3d12df..f7dce00 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,11 @@ QTAC is a software suite that enables users to control Qualcomm devices remotely | **OS** | Windows / Debian | Windows 10+ / Ubuntu 22.04+ | | **Compiler** | [MSVC 2022](https://aka.ms/vs/17/release/vs_community.exe) / GCC | MSVC 2022 / GCC-11, G++-11, GLIBC-2.35 | | **Build System** | [CMake](https://cmake.org/download/) | 3.22+ | +| **Build Tool** | [Ninja](https://ninja-build.org/) (via Qt installer, system package manager, or direct download) | 1.10+ | | **UI Framework** | [Qt Open-source](https://www.qt.io/download-qt-installer-oss) | 6.9.0+ | > [!NOTE] -> Review license terms for [Visual Studio](https://visualstudio.microsoft.com/license-terms/) and [Qt](https://www.qt.io/development/download-open-source). MSVC 2022 is linked as Qt doesn't support MSVC 2026 yet. +> Review license terms for [Visual Studio](https://visualstudio.microsoft.com/license-terms/) and [Qt](https://www.qt.io/development/download-open-source). MSVC 2022 is linked as Qt does not yet support later MSVC versions. ### Drivers @@ -63,7 +64,14 @@ QTAC is a software suite that enables users to control Qualcomm devices remotely - **[Qualcomm USB Drivers](https://softwarecenter.qualcomm.com/catalog/item/Qualcomm_Userspace_Driver)**: To view device status. > [!NOTE] -> FTDI libraries are installed _automatically_ during the cmake configuration step when building from source. +> FTDI libraries are downloaded _automatically_ during the cmake configuration step when building from source. +> If the automatic download fails (e.g. due to network restrictions), download the archive manually and place it in the `third-party/` directory before re-running cmake: +> +> | Platform | Archive | +> | :-- | :-- | +> | **Windows x64** | [`CDM-v2.12.36.4-WHQL-Certified.zip`](https://web.archive.org/web/20250820134143/https://ftdichip.com/wp-content/uploads/2023/09/CDM-v2.12.36.4-WHQL-Certified.zip) | +> | **Windows ARM64** | [`CDM-v2.12.36.20-for-ARM64-WHQL-Certified.zip`](https://web.archive.org/web/20250821211500/https://ftdichip.com/wp-content/uploads/2025/03/CDM-v2.12.36.20-for-ARM64-WHQL-Certified.zip) | +> | **Linux x86_64** | [`libftd2xx-linux-x86_64-1.4.33.tgz`](https://web.archive.org/web/20250822044524/https://ftdichip.com/wp-content/uploads/2025/03/libftd2xx-linux-x86_64-1.4.33.tgz) | ### Optional Software @@ -84,29 +92,39 @@ git clone https://github.com/qualcomm/qcom-test-automation-controller.git 1. **Visual Studio**: Install **Desktop development with C++** and **.NET desktop development**. ![Desktop development with C++](./docs/resources/qtac-msvc-2022-requirements.png) 2. **Qt**: Install Qt 6.9+ for **MSVC 2022 64-bit**, **Qt Serial Port** and **Qt Multimedia** components. + For ARM64, also install the **MSVC 2022 ARM64** Qt component. > [!NOTE] > Installation using Qt Online Installer will require users to create a Qt account. 3. **Environment Variable**: + + **x64**: ```cmd setx QTBIN C:\Qt\\msvc2022_64\bin ``` + **ARM64**: + ```cmd + setx QTBIN C:\Qt\\msvc2022_arm64\bin + ``` ### Build & Usage -Execute `build.bat` to generate executables: +Execute `build.bat` to generate executables. The build targets the host architecture automatically (x64 on an x64 host, ARM64 on an ARM64 host): ```cmd build.bat ``` **Build output**: -- Debug: `__Builds\x64\Debug` -- Release: `__Builds\x64\Release` +- x64 Debug: `__Builds\x64\Debug` +- x64 Release: `__Builds\x64\Release` +- ARM64 Debug: `__Builds\ARM64\Debug` +- ARM64 Release: `__Builds\ARM64\Release` **Usage**: ```cmd __Builds\x64\Release\QTAC.exe +__Builds\ARM64\Release\QTAC.exe ``` ## Linux Guide @@ -132,7 +150,7 @@ __Builds\x64\Release\QTAC.exe sudo cp udev-rules/99-QTAC-USB.rules /etc/udev/rules.d/ sudo udevadm control --reload ``` -4. **Environment Variable**: +3. **Environment Variable**: ```bash export QTBIN=/path/to/Qt/directory//gcc_64/bin ``` @@ -149,9 +167,6 @@ Execute `build.sh` to generate executables: - Debug: `__Builds/Linux/Debug` - Release: `__Builds/Linux/Release` -> [!NOTE] -> Ensure that [make](https://www.gnu.org/software/make/) is available in your environment before building. - **Usage**: ```bash ./__Builds/Linux/Release/QTAC diff --git a/build.bat b/build.bat index a713d84..8df7846 100644 --- a/build.bat +++ b/build.bat @@ -1,48 +1,117 @@ -@REM Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. - -@REM Redistribution and use in source and binary forms, with or without -@REM modification, are permitted (subject to the limitations in the -@REM disclaimer below) provided that the following conditions are met: - -@REM * Redistributions of source code must retain the above copyright -@REM notice, this list of conditions and the following disclaimer. - -@REM * Redistributions in binary form must reproduce the above -@REM copyright notice, this list of conditions and the following -@REM disclaimer in the documentation and/or other materials provided -@REM with the distribution. - -@REM * Neither the name of Qualcomm Technologies, Inc. nor the names of its -@REM contributors may be used to endorse or promote products derived -@REM from this software without specific prior written permission. - -@REM NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE -@REM GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT -@REM HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED -@REM WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -@REM MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -@REM IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -@REM ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -@REM DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE -@REM GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -@REM INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER -@REM IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -@REM OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN -@REM IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -@REM Author: Biswajit Roy (biswroy@qti.qualcomm.com) +@REM Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +@REM SPDX-License-Identifier: BSD-3-Clause @echo off +@REM --------------------------------------------------------------------------- +@REM Detect host architecture (native build only) +@REM --------------------------------------------------------------------------- +if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( + set ARCH=ARM64 + set EXPECTED_QT_PATH=msvc2022_arm64 + set VCVARS_SCRIPT=vcvarsarm64.bat + set VS_COMPONENT=MSVC v143 - VS 2022 C++ ARM64 build tools +) else ( + set ARCH=x64 + set EXPECTED_QT_PATH=msvc2022_64 + set VCVARS_SCRIPT=vcvars64.bat + set VS_COMPONENT=Desktop development with C++ +) + +echo Architecture : %ARCH% + +@REM --------------------------------------------------------------------------- +@REM Validate QTBIN +@REM --------------------------------------------------------------------------- if "%QTBIN%"=="" ( - echo Set QTBIN to the bin directory of the Qt installed location + echo. + echo ERROR: QTBIN is not set. + echo QTBIN must point to the Qt bin directory for your target architecture ^(%ARCH%^). + echo Run the following command and then open a new command prompt: + if /i "%ARCH%"=="x64" ( + echo setx QTBIN "C:\Qt\^\msvc2022_64\bin" + ) else ( + echo setx QTBIN "C:\Qt\^\msvc2022_arm64\bin" + ) + exit /b 1 +) + +if not exist "%QTBIN%" ( + echo. + echo ERROR: QTBIN directory does not exist: %QTBIN% + echo Qt does not appear to be installed at this path. + echo Install Qt 6.9+ via the Qt Online Installer ^(https://www.qt.io/download-qt-installer-oss^) + echo and include the MSVC 2022 %ARCH% component, then update QTBIN. + exit /b 1 +) + +echo %QTBIN% | findstr /i "%EXPECTED_QT_PATH%" >nul +if errorlevel 1 ( + echo. + echo ERROR: QTBIN points to the wrong Qt architecture for a %ARCH% build. + echo QTBIN is currently: %QTBIN% + if /i "%ARCH%"=="x64" ( + echo An x64 build requires the Qt MSVC 2022 64-bit component. QTBIN must contain 'msvc2022_64', e.g.: + echo setx QTBIN "C:\Qt\^\msvc2022_64\bin" + ) else ( + echo An ARM64 build requires the Qt MSVC 2022 ARM64 component. QTBIN must contain 'msvc2022_arm64', e.g.: + echo setx QTBIN "C:\Qt\^\msvc2022_arm64\bin" + echo If you have not installed the ARM64 Qt component, open Qt Online Installer, select Modify, + echo and add 'MSVC 2022 ARM64' under Qt ^. + ) exit /b 1 ) -call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" -call "C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Auxiliary\Build\vcvars64.bat" -call "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvars64.bat" +echo QTBIN : %QTBIN% [OK] + +@REM --------------------------------------------------------------------------- +@REM Locate and call VS2022 vcvars +@REM --------------------------------------------------------------------------- +set VCVARS_FOUND=0 + +if exist "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\%VCVARS_SCRIPT%" ( + call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\%VCVARS_SCRIPT%" + set VCVARS_FOUND=1 + echo VS2022 toolchain : Enterprise [OK] + goto :vcvars_done +) +if exist "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\%VCVARS_SCRIPT%" ( + call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\%VCVARS_SCRIPT%" + set VCVARS_FOUND=1 + echo VS2022 toolchain : Community [OK] + goto :vcvars_done +) +if exist "C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Auxiliary\Build\%VCVARS_SCRIPT%" ( + call "C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Auxiliary\Build\%VCVARS_SCRIPT%" + set VCVARS_FOUND=1 + echo VS2022 toolchain : Professional [OK] + goto :vcvars_done +) +if exist "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\%VCVARS_SCRIPT%" ( + call "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\%VCVARS_SCRIPT%" + set VCVARS_FOUND=1 + echo VS2022 toolchain : BuildTools [OK] + goto :vcvars_done +) + +:vcvars_done +if "%VCVARS_FOUND%"=="0" ( + echo. + echo ERROR: Visual Studio 2022 %ARCH% build tools not found ^(%VCVARS_SCRIPT%^). + echo Open Visual Studio Installer, click Modify on your VS2022 installation, + echo go to Individual Components, and install: + echo '%VS_COMPONENT%' + echo Searched in: + echo C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build + echo C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build + echo C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Auxiliary\Build + echo C:\Program Files ^(x86^)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build + exit /b 1 +) +@REM --------------------------------------------------------------------------- +@REM Build +@REM --------------------------------------------------------------------------- set "PATH=%QTBIN%;%PATH%" if exist build rmdir /s /q build diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 index 1009d7c..351f3fa --- a/build.sh +++ b/build.sh @@ -1,43 +1,44 @@ #!/bin/bash -# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted (subject to the limitations in the -# disclaimer below) provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# * Neither the name of Qualcomm Technologies, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE -# GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT -# HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED -# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE -# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER -# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN -# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause # Author: Biswajit Roy (biswroy@qti.qualcomm.com) set -e if [ -z "$QTBIN" ]; then - echo "Set QTBIN first" + echo "" + echo "ERROR: QTBIN is not set." + echo " QTBIN must point to the Qt bin directory, e.g.:" + echo " export QTBIN=/path/to/Qt//gcc_64/bin" + echo " Then re-run this script." + exit 1 +fi + +if [ ! -d "$QTBIN" ]; then + echo "" + echo "ERROR: QTBIN directory does not exist: $QTBIN" + echo " Install Qt 6.9+ via the Qt Online Installer (https://www.qt.io/download-qt-installer-oss)" + echo " and include the GCC 64-bit component, then update QTBIN." + exit 1 +fi + +if ! echo "$QTBIN" | grep -q "gcc_64"; then + echo "" + echo "ERROR: QTBIN does not point to a GCC 64-bit Qt installation." + echo " QTBIN is currently: $QTBIN" + echo " A Linux build requires the Qt GCC 64-bit component. QTBIN must contain 'gcc_64', e.g.:" + echo " export QTBIN=/path/to/Qt//gcc_64/bin" + exit 1 +fi + +if ! command -v ninja &>/dev/null; then + echo "" + echo "ERROR: ninja not found in PATH." + echo " Install ninja via your package manager, e.g.:" + echo " sudo apt install ninja-build" + echo " Or via the Qt installer (Tools > Ninja)." exit 1 fi @@ -47,11 +48,20 @@ export PATH="$QTBIN:$PATH" rm -rf build __Builds # Debug -cmake -S . -B build/Debug -DCMAKE_PREFIX_PATH="$(dirname "$QTBIN")" -DCMAKE_BUILD_TYPE=Debug +cmake -S . -B build/Debug \ + -DCMAKE_PREFIX_PATH="$(dirname "$QTBIN")" \ + -DCMAKE_COLOR_DIAGNOSTICS=ON \ + -DCMAKE_GENERATOR=Ninja \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_CXX_FLAGS_INIT=-DQT_QML_DEBUG cmake --build build/Debug # Release -cmake -S . -B build/Release -DCMAKE_PREFIX_PATH="$(dirname "$QTBIN")" -DCMAKE_BUILD_TYPE=Release +cmake -S . -B build/Release \ + -DCMAKE_PREFIX_PATH="$(dirname "$QTBIN")" \ + -DCMAKE_COLOR_DIAGNOSTICS=ON \ + -DCMAKE_GENERATOR=Ninja \ + -DCMAKE_BUILD_TYPE=Release cmake --build build/Release echo "Check __Builds directory" diff --git a/docs/getting-started/01-Build-Using-Qt-Creator.md b/docs/getting-started/01-Build-Using-Qt-Creator.md index 77d04dc..113c8f3 100644 --- a/docs/getting-started/01-Build-Using-Qt-Creator.md +++ b/docs/getting-started/01-Build-Using-Qt-Creator.md @@ -2,11 +2,20 @@ The [README](../../README.md) outlines the steps required to build QTAC using command line interface. To understand and modify the Qt UI components, you will need to get Qt Creator. If you've already -installed Qt, you may execute the **Qt Maintainence Tool** to download the additional software Qt +installed Qt, you may execute the **Qt Maintenance Tool** to download the additional software Qt Creator. If you're performing a fresh install, use the [Qt Online Installer](https://www.qt.io/download-open-source). ## Configure Qt installation -QTAC requires Qt6 and MSVC2022 64-bit. Please review below custom install configuration in Qt to optimize download time. +QTAC requires Qt 6.9+. Install the component matching your platform and target architecture: + +**Windows** +- **x64**: `MSVC 2022 64-bit` +- **ARM64**: `MSVC 2022 ARM64` + +**Linux** +- **x86_64**: `GCC 64-bit` + +Please review below custom install configuration in Qt to optimize download time. ![Qt installation](../resources/qt-install-config.png) @@ -17,8 +26,8 @@ Required additional libraries: ![Qt additional dependencies](../resources/qtac-additional-dependencies.png) -### Setup CMake -QTAC requires CMake for building from source. You can either install CMake separately or use the Qt installer to setup CMake. +### Setup CMake and Ninja +QTAC requires CMake and Ninja for building from source. Both can be installed via the Qt installer, separately, or via your system package manager (`sudo apt install ninja-build cmake` on Linux). Review the following screenshot to setup CMake on your system. ![Qt CMake dependencies](../resources/qtac-cmake-install.png) @@ -29,20 +38,47 @@ Use the below command to clone the project source: git clone https://github.com/qualcomm/qcom-test-automation-controller.git ``` -## Configure Qt 6.9.0 +## Configure Qt 6.9+ Open the session. The **Project** tab on the left pane will be inactive. Create a [Sample Qt project](https://doc.qt.io/qtcreator/creator-project-creating.html) to see if Qt is set up properly. The **Project** tab now becomes active. Review the Qt kit configuration. Make sure no stray paths or kits are present as they can lead to erroneous libraries and applications. +For Windows x64, ensure a kit is configured with: +- **Compiler**: MSVC 2022 x64 (`cl.exe` from the x64 toolchain) +- **Qt version**: the `msvc2022_64` Qt installation + +For ARM64 (Windows), ensure a kit is configured with: +- **Compiler**: MSVC 2022 ARM64 (`cl.exe` from the ARM64 toolchain) +- **Qt version**: the `msvc2022_arm64` Qt installation + +For Linux, ensure a kit is configured with: +- **Compiler**: GCC (g++ from the system toolchain, minimum GCC 11) +- **Qt version**: the `gcc_64` Qt installation + +> [!NOTE] +> On Linux, install the udev rules before running the application: +> ```bash +> sudo cp udev-rules/99-QTAC-USB.rules /etc/udev/rules.d/ +> sudo udevadm control --reload +> ``` + The numbers on the image refers to some of the areas you need to review before building the project. ![Qt Creator Kits](../resources/qt-creator-configuration.png) ## Setup third-party libraries QTAC uses FTDI libraries to control FT4232H chip on the debug board. You can find out more about the FTDI D2XX libraries -[here](https://ftdichip.com/drivers/d2xx-drivers/). The CMake scripts take care of setting up the libraries and no manual step is required. +[here](https://ftdichip.com/drivers/d2xx-drivers/). The CMake scripts download the correct library automatically during configuration. + +If the automatic download fails (e.g. due to network restrictions), download the archive manually and place it in the `third-party/` directory before opening the project in Qt Creator: + +| Platform | Archive | +| :-- | :-- | +| **Windows x64** | [`CDM-v2.12.36.4-WHQL-Certified.zip`](https://web.archive.org/web/20250820134143/https://ftdichip.com/wp-content/uploads/2023/09/CDM-v2.12.36.4-WHQL-Certified.zip) | +| **Windows ARM64** | [`CDM-v2.12.36.20-for-ARM64-WHQL-Certified.zip`](https://web.archive.org/web/20250821211500/https://ftdichip.com/wp-content/uploads/2025/03/CDM-v2.12.36.20-for-ARM64-WHQL-Certified.zip) | +| **Linux x86_64** | [`libftd2xx-linux-x86_64-1.4.33.tgz`](https://web.archive.org/web/20250822044524/https://ftdichip.com/wp-content/uploads/2025/03/libftd2xx-linux-x86_64-1.4.33.tgz) | ## Open project Now you're set to build the project using Qt Creator. diff --git a/examples/MCP/sse/README.md b/examples/MCP/sse/README.md new file mode 100644 index 0000000..4c810e8 --- /dev/null +++ b/examples/MCP/sse/README.md @@ -0,0 +1,104 @@ +# QTAC MCP Server - SSE + +Persistent server supporting multiple concurrent clients. Each client session gets exclusive +ownership of a device. + +Use this variant when multiple automation clients need to share a device pool simultaneously. +For single-client use see [`../stdio/`](../stdio/). + +## Prerequisites + +- Python 3.10+ (architecture must match: x64 or ARM64) +- Project built via `build.bat` / `build.sh` + +## Setup + +Installs the TACDev library and all dependencies: + +```bat +setup.bat # Windows (auto-detects x64/ARM64) +``` +```bash +./setup.sh # Linux +``` + +Or manually: + +```bash +pip install interfaces/Python # from repo root +pip install -r examples/MCP/sse/requirements.txt +``` + +## Configuration + +`config.yaml` - all fields optional, defaults shown: + +| Parameter | Default | Description | +| :-- | :-- | :-- | +| `server.host` | `127.0.0.1` | Bind address | +| `server.port` | `8000` | Port | +| `logging.file` | `tacdev_mcp.log` | Log file path | +| `logging.level` | `INFO` | DEBUG / INFO / WARNING / ERROR | +| `logging.max_bytes` | `10485760` | Rotation size (10 MB) | +| `logging.backup_count` | `5` | Rotated files to keep | + +## Running + +The server must be started before any client connects: + +```bash +python examples/MCP/sse/tacdev_mcp_server.py +``` + +### Option 1 - Python client + +```bash +python examples/MCP/sse/tacdev_mcp_client.py # first available device +python examples/MCP/sse/tacdev_mcp_client.py COM41 # specific port +``` + +### Option 2 - Claude CLI + +Register once (server must already be running): + +```bash +claude mcp add --transport sse TACDev-MCP http://127.0.0.1:8000/sse +``` + +Verify: + +```bash +claude mcp list +``` + +Then ask Claude naturally - no special syntax needed: + +``` +List connected devices +Power on COM41 +Boot COM3 to fastboot +``` + +> **Note:** The server must be running before starting a Claude session. Claude connects to it +> over SSE; it does not start the server automatically. + +## Recovering a locked device + +If a client crashes without closing its handle, the device will remain locked. Restart the server +to release all devices cleanly: + +``` +Ctrl+C +python examples/MCP/sse/tacdev_mcp_server.py +``` + +## Troubleshooting + +| Error | Fix | +| :-- | :-- | +| `No module named 'TACDev'` | `pip install interfaces/Python` from repo root | +| `TACDev library not found` | Run `build.bat` / `build.sh` first | +| `Architecture mismatch` | Use Python matching your OS architecture | +| `No module named 'fastmcp'` | `pip install -r requirements.txt` (requires Python 3.10+) | +| `cryptography` build failure on ARM64 | Re-run `setup.bat` - downloads OpenSSL automatically. Manual fallback: install [Win64ARMOpenSSL-4_0_1.msi](https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi) (full, not Light) and set `OPENSSL_DIR=C:\Program Files\OpenSSL-Win64-ARM` | +| `get_device_count` returns 0 | Check board is connected. Windows: run `FTDICheck.exe` from `__Builds\x64\Release\bin`. Linux: `sudo cp udev-rules/99-QTAC-USB.rules /etc/udev/rules.d/ && sudo udevadm control --reload` | diff --git a/examples/MCP/sse/config.yaml b/examples/MCP/sse/config.yaml new file mode 100644 index 0000000..41d6bf5 --- /dev/null +++ b/examples/MCP/sse/config.yaml @@ -0,0 +1,15 @@ +server: + # Host to bind the MCP server on + host: "127.0.0.1" + # Port the MCP server listens on + port: 8000 + +logging: + # Log file path (relative to server script or absolute) + file: "tacdev_mcp.log" + # Logging level: DEBUG, INFO, WARNING, ERROR + level: "INFO" + # Maximum log file size in bytes before rotation (10 MB default) + max_bytes: 10485760 + # Number of rotated backup log files to retain + backup_count: 5 diff --git a/examples/MCP/sse/requirements.txt b/examples/MCP/sse/requirements.txt new file mode 100644 index 0000000..f2f1e38 --- /dev/null +++ b/examples/MCP/sse/requirements.txt @@ -0,0 +1,2 @@ +fastmcp>=3.0.0 +pyyaml diff --git a/examples/MCP/sse/setup.bat b/examples/MCP/sse/setup.bat new file mode 100644 index 0000000..a331f3d --- /dev/null +++ b/examples/MCP/sse/setup.bat @@ -0,0 +1,85 @@ +@REM Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +@REM SPDX-License-Identifier: BSD-3-Clause + +@echo off +setlocal enabledelayedexpansion + +set SCRIPT_DIR=%~dp0 +pushd "%SCRIPT_DIR%\..\..\..\" +set REPO_ROOT=%CD% +popd + +set ARCH=%1 +if "%ARCH%"=="" ( + if "%PROCESSOR_ARCHITECTURE%"=="ARM64" (set ARCH=ARM64) else (set ARCH=x64) +) + +if /i "%ARCH%"=="ARM64" ( + if "%OPENSSL_DIR%"=="" set OPENSSL_DIR=C:\Program Files\OpenSSL-Win64-ARM +) + +if /i "%ARCH%"=="ARM64" if not exist "!OPENSSL_DIR!\include" ( + echo OpenSSL not found. Downloading and installing... + curl -L -o "%TEMP%\Win64ARMOpenSSL.msi" https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi + if errorlevel 1 ( + echo. + echo ERROR: Download failed. Install manually from: + echo https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi + echo Then re-run setup.bat + echo. + exit /b 1 + ) + msiexec /i "%TEMP%\Win64ARMOpenSSL.msi" /quiet /norestart INSTALLDIR="!OPENSSL_DIR!" + if errorlevel 1 ( + echo. + echo ERROR: OpenSSL installation failed. Install manually from: + echo https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi + echo Then re-run setup.bat + echo. + exit /b 1 + ) + echo OpenSSL installed to !OPENSSL_DIR! +) + +if /i "%ARCH%"=="ARM64" ( + if not exist "!OPENSSL_DIR!\include" ( + echo. + echo ERROR: OpenSSL headers not found at !OPENSSL_DIR!\include + echo Set OPENSSL_DIR to the full installer path and re-run: + echo setx OPENSSL_DIR "C:\Program Files\OpenSSL-Win64-ARM" + echo. + exit /b 1 + ) + if not exist "!OPENSSL_DIR!\lib\VC\arm64\MD" ( + echo. + echo ERROR: OpenSSL libs not found at !OPENSSL_DIR!\lib\VC\arm64\MD + echo Reinstall using the full installer ^(not Light^): + echo https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi + echo. + exit /b 1 + ) + set OPENSSL_LIB_DIR=!OPENSSL_DIR!\lib\VC\arm64\MD + echo OpenSSL : !OPENSSL_DIR! [OK] + echo OpenSSL libs : !OPENSSL_LIB_DIR! [OK] +) + +set BUILD_DIR=%REPO_ROOT%\__Builds +if not exist "%BUILD_DIR%" ( + echo [1/3] Build not found. Running root build... + call "%REPO_ROOT%\build.bat" + if errorlevel 1 exit /b 1 +) else ( + echo [1/3] Build found at %BUILD_DIR%, skipping recompile. +) + +echo [2/3] Installing TACDev Python library... +pip install "%REPO_ROOT%\interfaces\Python" +if errorlevel 1 exit /b 1 + +echo [3/3] Installing MCP dependencies... +pip install -r "%SCRIPT_DIR%requirements.txt" +if errorlevel 1 exit /b 1 + +echo. +echo Setup complete. Start the MCP server: +echo python examples\MCP\sse\tacdev_mcp_server.py diff --git a/examples/MCP/sse/setup.sh b/examples/MCP/sse/setup.sh new file mode 100644 index 0000000..01c00d3 --- /dev/null +++ b/examples/MCP/sse/setup.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" + +if [ ! -d "$REPO_ROOT/__Builds" ]; then + echo "[1/3] Build not found. Running root build..." + "$REPO_ROOT/build.sh" +else + echo "[1/3] Build found at $REPO_ROOT/__Builds, skipping recompile." +fi + +echo "[2/3] Installing TACDev Python library..." +pip install "$REPO_ROOT/interfaces/Python" + +echo "[3/3] Installing MCP dependencies..." +pip install -r "$SCRIPT_DIR/requirements.txt" + +echo "" +echo "Setup complete. Start the MCP server:" +echo " python examples/MCP/sse/tacdev_mcp_server.py" diff --git a/examples/MCP/sse/tacdev_mcp_client.py b/examples/MCP/sse/tacdev_mcp_client.py new file mode 100644 index 0000000..d95039d --- /dev/null +++ b/examples/MCP/sse/tacdev_mcp_client.py @@ -0,0 +1,329 @@ +#!/usr/bin/env python3 + +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +""" +MCP client for tacdev_mcp_server.py. + +Typical usage +------------- + from tacdev_mcp_client import TACDevClient + + async def main(): + async with TACDevClient() as tac: + devices = await tac.list_devices() + handle = await tac.open_handle_by_description("COM3") + await tac.power_on_button(handle) + await tac.close_tac_handle(handle) + + asyncio.run(main()) +""" + +from __future__ import annotations + +import asyncio +import json +import sys +from pathlib import Path +from typing import Any + +import yaml +from fastmcp import Client + +_CONFIG_PATH = Path(__file__).with_name("config.yaml") + + +def _server_url() -> str: + with open(_CONFIG_PATH) as f: + cfg = yaml.safe_load(f) + s = cfg["server"] + return f"http://{s['host']}:{s['port']}/sse" + + +class TACDevClient: + def __init__(self) -> None: + self._client = Client(_server_url()) + + async def __aenter__(self) -> TACDevClient: + await self._client.__aenter__() + return self + + async def __aexit__(self, *args) -> None: + await self._client.__aexit__(*args) + + async def _call(self, tool: str, **kwargs: Any) -> Any: + result = await self._client.call_tool(tool, kwargs) + if result.content: + text = result.content[0].text + try: + return json.loads(text) + except (ValueError, TypeError): + return text + return None + + async def list_devices(self) -> dict: + return await self._call("list_devices") + + async def get_alpaca_version(self) -> str: + return (await self._call("get_alpaca_version"))["alpaca_version"] + + async def get_tac_version(self) -> str: + return (await self._call("get_tac_version"))["tac_version"] + + async def get_last_tac_error(self) -> str: + return (await self._call("get_last_tac_error"))["last_error"] + + async def get_logging_state(self) -> bool: + return (await self._call("get_logging_state"))["logging_enabled"] + + async def set_logging_state(self, enabled: bool) -> None: + await self._call("set_logging_state", enabled=enabled) + + async def get_device_count(self) -> int: + return (await self._call("get_device_count"))["device_count"] + + async def get_port_data(self, device_index: int) -> str: + return (await self._call("get_port_data", device_index=device_index))["port_data"] + + async def list_ports(self) -> list[str]: + count = await self.get_device_count() + return [await self.get_port_data(i) for i in range(count)] + + async def open_handle_by_description(self, port_name: str) -> int: + return (await self._call("open_handle_by_description", port_name=port_name))["handle"] + + async def close_tac_handle(self, handle: int) -> None: + await self._call("close_tac_handle", handle=handle) + + async def get_name(self, handle: int) -> str: + return (await self._call("get_name", handle=handle))["name"] + + async def get_firmware_version(self, handle: int) -> str: + return (await self._call("get_firmware_version", handle=handle))["firmware_version"] + + async def get_hardware(self, handle: int) -> str: + return (await self._call("get_hardware", handle=handle))["hardware"] + + async def get_hardware_version(self, handle: int) -> str: + return (await self._call("get_hardware_version", handle=handle))["hardware_version"] + + async def get_uuid(self, handle: int) -> str: + return (await self._call("get_uuid", handle=handle))["uuid"] + + async def get_device_info(self, handle: int) -> dict: + return { + "name": await self.get_name(handle), + "firmware_version": await self.get_firmware_version(handle), + "hardware": await self.get_hardware(handle), + "hardware_version": await self.get_hardware_version(handle), + "uuid": await self.get_uuid(handle), + } + + async def set_external_power_control(self, handle: int, state: bool) -> None: + await self._call("set_external_power_control", handle=handle, state=state) + + async def list_commands(self, handle: int) -> list[str]: + return (await self._call("list_commands", handle=handle))["commands"] + + async def get_command(self, handle: int, command_index: int) -> str: + return (await self._call("get_command", handle=handle, command_index=command_index))["command"] + + async def list_quick_commands(self, handle: int) -> list[str]: + return (await self._call("list_quick_commands", handle=handle))["quick_commands"] + + async def list_script_variables(self, handle: int) -> list[str]: + return (await self._call("list_script_variables", handle=handle))["script_variables"] + + async def update_script_variable(self, handle: int, variable: str, value: str) -> None: + await self._call("update_script_variable", handle=handle, variable=variable, value=value) + + async def get_command_state(self, handle: int, command: str) -> bool: + return (await self._call("get_command_state", handle=handle, command=command))["state"] + + async def send_command(self, handle: int, command: str, state: bool) -> None: + await self._call("send_command", handle=handle, command=command, state=state) + + async def get_help_text(self, handle: int) -> str: + return (await self._call("get_help_text", handle=handle))["help_text"] + + async def is_command_queue_clear(self, handle: int) -> bool: + return (await self._call("is_command_queue_clear", handle=handle))["queue_clear"] + + async def set_pin_state(self, handle: int, pin: int, state: bool) -> None: + await self._call("set_pin_state", handle=handle, pin=pin, state=state) + + async def set_battery_state(self, handle: int, state: bool) -> None: + await self._call("set_battery_state", handle=handle, state=state) + + async def get_battery_state(self, handle: int) -> bool: + return (await self._call("get_battery_state", handle=handle))["state"] + + async def set_usb0(self, handle: int, state: bool) -> None: + await self._call("set_usb0", handle=handle, state=state) + + async def get_usb0_state(self, handle: int) -> bool: + return (await self._call("get_usb0_state", handle=handle))["state"] + + async def set_usb1(self, handle: int, state: bool) -> None: + await self._call("set_usb1", handle=handle, state=state) + + async def get_usb1_state(self, handle: int) -> bool: + return (await self._call("get_usb1_state", handle=handle))["state"] + + async def set_power_key(self, handle: int, state: bool) -> None: + await self._call("set_power_key", handle=handle, state=state) + + async def get_power_key_state(self, handle: int) -> bool: + return (await self._call("get_power_key_state", handle=handle))["state"] + + async def set_volume_up(self, handle: int, state: bool) -> None: + await self._call("set_volume_up", handle=handle, state=state) + + async def get_volume_up_state(self, handle: int) -> bool: + return (await self._call("get_volume_up_state", handle=handle))["state"] + + async def set_volume_down(self, handle: int, state: bool) -> None: + await self._call("set_volume_down", handle=handle, state=state) + + async def get_volume_down_state(self, handle: int) -> bool: + return (await self._call("get_volume_down_state", handle=handle))["state"] + + async def set_disconnect_uim1(self, handle: int, state: bool) -> None: + await self._call("set_disconnect_uim1", handle=handle, state=state) + + async def get_disconnect_uim1_state(self, handle: int) -> bool: + return (await self._call("get_disconnect_uim1_state", handle=handle))["state"] + + async def set_disconnect_uim2(self, handle: int, state: bool) -> None: + await self._call("set_disconnect_uim2", handle=handle, state=state) + + async def get_disconnect_uim2_state(self, handle: int) -> bool: + return (await self._call("get_disconnect_uim2_state", handle=handle))["state"] + + async def set_disconnect_sd_card(self, handle: int, state: bool) -> None: + await self._call("set_disconnect_sd_card", handle=handle, state=state) + + async def get_disconnect_sd_card_state(self, handle: int) -> bool: + return (await self._call("get_disconnect_sd_card_state", handle=handle))["state"] + + async def set_primary_edl(self, handle: int, state: bool) -> None: + await self._call("set_primary_edl", handle=handle, state=state) + + async def get_primary_edl_state(self, handle: int) -> bool: + return (await self._call("get_primary_edl_state", handle=handle))["state"] + + async def set_secondary_edl(self, handle: int, state: bool) -> None: + await self._call("set_secondary_edl", handle=handle, state=state) + + async def get_secondary_edl_state(self, handle: int) -> bool: + return (await self._call("get_secondary_edl_state", handle=handle))["state"] + + async def set_force_ps_hold_high(self, handle: int, state: bool) -> None: + await self._call("set_force_ps_hold_high", handle=handle, state=state) + + async def get_force_ps_hold_high_state(self, handle: int) -> bool: + return (await self._call("get_force_ps_hold_high_state", handle=handle))["state"] + + async def set_secondary_pm_resin_n(self, handle: int, state: bool) -> None: + await self._call("set_secondary_pm_resin_n", handle=handle, state=state) + + async def get_secondary_pm_resin_n_state(self, handle: int) -> bool: + return (await self._call("get_secondary_pm_resin_n_state", handle=handle))["state"] + + async def set_eud(self, handle: int, state: bool) -> None: + await self._call("set_eud", handle=handle, state=state) + + async def get_eud_state(self, handle: int) -> bool: + return (await self._call("get_eud_state", handle=handle))["state"] + + async def set_headset_disconnect(self, handle: int, state: bool) -> None: + await self._call("set_headset_disconnect", handle=handle, state=state) + + async def get_headset_disconnect_state(self, handle: int) -> bool: + return (await self._call("get_headset_disconnect_state", handle=handle))["state"] + + async def set_name(self, handle: int, new_name: str) -> None: + await self._call("set_name", handle=handle, new_name=new_name) + + async def get_reset_count(self, handle: int) -> int: + return (await self._call("get_reset_count", handle=handle))["reset_count"] + + async def clear_reset_count(self, handle: int) -> None: + await self._call("clear_reset_count", handle=handle) + + async def power_on_button(self, handle: int) -> None: + await self._call("power_on_button", handle=handle) + + async def power_off_button(self, handle: int) -> None: + await self._call("power_off_button", handle=handle) + + async def boot_to_fastboot_button(self, handle: int) -> None: + await self._call("boot_to_fastboot_button", handle=handle) + + async def boot_to_uefi_menu_button(self, handle: int) -> None: + await self._call("boot_to_uefi_menu_button", handle=handle) + + async def boot_to_edl_button(self, handle: int) -> None: + await self._call("boot_to_edl_button", handle=handle) + + async def boot_to_secondary_edl_button(self, handle: int) -> None: + await self._call("boot_to_secondary_edl_button", handle=handle) + + +async def TACExample(port_name: str | None = None) -> None: + async with TACDevClient() as tac: + print(f" Alpaca version : {await tac.get_alpaca_version()}") + print(f" TAC version : {await tac.get_tac_version()}") + print(f" Logging : {await tac.get_logging_state()}") + + devices = await tac.list_devices() + print(f" Available : {devices['available']}") + print(f" In use : {devices['in_use']}") + + if not port_name and devices["available"]: + port_name = devices["available"][0]["port"] + + if port_name: + print(f"\nOpening '{port_name}'...") + handle = await tac.open_handle_by_description(port_name) + + info = await tac.get_device_info(handle) + for k, v in info.items(): + print(f" {k:<20}: {v}") + + print(f"\n Queue clear : {await tac.is_command_queue_clear(handle)}") + + print("\n--- Available commands ---") + print(await tac.get_help_text(handle)) + + commands = await tac.list_commands(handle) + if commands: + cmd_names = [c.split(";")[0].strip() for c in commands] + print(f"\nCommands : {cmd_names}") + + quick_commands = await tac.list_quick_commands(handle) + if quick_commands: + qc_names = [q.split(";")[1].strip() for q in quick_commands] + print(f"Quick commands: {qc_names}") + + if commands: + cmd = cmd_names[0] + state_before = await tac.get_command_state(handle, cmd) + print(f"\nCommand '{cmd}' state before toggle: {state_before}") + await tac.send_command(handle, cmd, not state_before) + await asyncio.sleep(2) + state_after = await tac.get_command_state(handle, cmd) + print(f"Command '{cmd}' state after toggle: {state_after}") + await tac.send_command(handle, cmd, state_before) + await asyncio.sleep(2) + print(f"Command '{cmd}' restored to : {await tac.get_command_state(handle, cmd)}") + + await tac.close_tac_handle(handle) + print("\nHandle closed.") + else: + print("No device available.") + + +if __name__ == "__main__": + asyncio.run(TACExample(sys.argv[1] if len(sys.argv) > 1 else None)) diff --git a/examples/MCP/sse/tacdev_mcp_server.py b/examples/MCP/sse/tacdev_mcp_server.py new file mode 100644 index 0000000..d665446 --- /dev/null +++ b/examples/MCP/sse/tacdev_mcp_server.py @@ -0,0 +1,620 @@ +#!/usr/bin/env python3 + +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +import asyncio +import logging +import logging.handlers +import time +from pathlib import Path + +import yaml +import TACDev +from fastmcp import FastMCP, Context +from fastmcp.server.middleware import Middleware, MiddlewareContext, CallNext + +# --------------------------------------------------------------------------- # +# Config +# --------------------------------------------------------------------------- # +_CONFIG_PATH = Path(__file__).with_name("config.yaml") + +def _load_config() -> dict: + with open(_CONFIG_PATH) as f: + return yaml.safe_load(f) + +cfg = _load_config() + +# --------------------------------------------------------------------------- # +# Logging +# --------------------------------------------------------------------------- # +def _setup_logging() -> logging.Logger: + log_cfg = cfg["logging"] + logger = logging.getLogger("tacdev_mcp") + logger.setLevel(log_cfg["level"].upper()) + + log_path = _CONFIG_PATH.with_name(log_cfg["file"]) + handler = logging.handlers.RotatingFileHandler( + log_path, + maxBytes=log_cfg["max_bytes"], + backupCount=log_cfg["backup_count"], + ) + handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) + logger.addHandler(handler) + return logger + +log = _setup_logging() + +# Suppress uvicorn SSE shutdown race: starlette sends http.response.start after +# uvicorn has already torn down the connection on Ctrl+C. +logging.getLogger("uvicorn.error").addFilter( + type("_suppress_sse_shutdown", (logging.Filter,), { + "filter": lambda self, r: "Expected ASGI message" not in r.getMessage() + })() +) + +# --------------------------------------------------------------------------- # +# Device ownership registry +# --------------------------------------------------------------------------- # +_lock = asyncio.Lock() + +device_ownership: dict[str, str] = {} +session_handles: dict[str, list[int]] = {} +handles: dict[int, tuple[TACDev.TACDevice, str, str]] = {} # handle -> (device, port, session_id) +_next_handle = 1 + + +def _register(device: TACDev.TACDevice, port: str, session_id: str) -> int: + global _next_handle + handle = _next_handle + _next_handle += 1 + handles[handle] = (device, port, session_id) + session_handles.setdefault(session_id, []).append(handle) + return handle + + +def _get_device_owned(handle: int, session_id: str) -> TACDev.TACDevice: + entry = handles.get(handle) + if entry is None: + raise RuntimeError(f"Invalid handle {handle}.") + device, port, owner = entry + if owner != session_id: + raise RuntimeError(f"Handle {handle} is owned by a different session.") + return device + + +def _release_all() -> None: + log.info("Closing all open devices before shutdown...") + for handle, entry in list(handles.items()): + device, port, session_id = entry + try: + device.Close() + log.info("session=%s handle=%d port=%s closed on shutdown", session_id, handle, port) + time.sleep(2) + except Exception as e: + log.warning("session=%s handle=%d port=%s failed to close on shutdown: %s", session_id, handle, port, e) + handles.clear() + session_handles.clear() + device_ownership.clear() + log.info("All devices released.") + + +def _device_list() -> dict: + count = TACDev.GetDeviceCount() + available = [] + in_use = [] + for i in range(count): + d = TACDev.GetDevice(i) + if not d: + continue + port = d.PortName() + entry = {"port": port, "description": d.Description(), "serial": d.SerialNumber()} + if port in device_ownership: + in_use.append({**entry, "owned_by": device_ownership[port]}) + else: + available.append(entry) + return {"available": available, "in_use": in_use} + + +# --------------------------------------------------------------------------- # +# Session lifecycle middleware +# --------------------------------------------------------------------------- # +class SessionMiddleware(Middleware): + async def on_initialize( + self, + context: MiddlewareContext, + call_next: CallNext, + ): + result = await call_next(context) + ctx = context.fastmcp_context + if ctx: + log.info("session=%s connected", ctx.session_id) + return result + + +# --------------------------------------------------------------------------- # +# MCP server +# --------------------------------------------------------------------------- # +mcp = FastMCP("TACDev-MCP", middleware=[SessionMiddleware()]) + + +# --------------------------------------------------------------------------- # +# Tools: device list +# --------------------------------------------------------------------------- # +@mcp.tool() +def list_devices() -> dict: + return _device_list() + + +# --------------------------------------------------------------------------- # +# Tools: diagnostics +# --------------------------------------------------------------------------- # +@mcp.tool() +def get_alpaca_version() -> dict: + return {"alpaca_version": TACDev.AlpacaVersion()} + + +@mcp.tool() +def get_tac_version() -> dict: + return {"tac_version": TACDev.TACVersion()} + + +@mcp.tool() +def get_last_tac_error() -> dict: + return {"last_error": TACDev.GetLastError()} + + +# --------------------------------------------------------------------------- # +# Tools: logging +# --------------------------------------------------------------------------- # +@mcp.tool() +def get_logging_state() -> dict: + return {"logging_enabled": TACDev.GetLoggingState()} + + +@mcp.tool() +def set_logging_state(enabled: bool) -> dict: + TACDev.SetLoggingState(enabled) + return {"success": True} + + +# --------------------------------------------------------------------------- # +# Tools: device enumeration +# --------------------------------------------------------------------------- # +@mcp.tool() +def get_device_count() -> dict: + return {"device_count": TACDev.GetDeviceCount()} + + +@mcp.tool() +def get_port_data(device_index: int) -> dict: + device = TACDev.GetDevice(device_index) + if device is None: + raise RuntimeError(f"No device at index {device_index}.") + return {"port_data": device.PortName(), "description": device.Description(), "serial_number": device.SerialNumber()} + + +# --------------------------------------------------------------------------- # +# Tools: handle management +# --------------------------------------------------------------------------- # +@mcp.tool() +async def open_handle_by_description(port_name: str, ctx: Context) -> dict: + session_id = ctx.session_id + async with _lock: + if port_name in device_ownership: + owner = device_ownership[port_name] + log.warning("session=%s tried to open %s already owned by %s", session_id, port_name, owner) + raise RuntimeError( + f"Device '{port_name}' is already in use by session {owner}. " + f"If that session crashed, restart the server (Ctrl+C) to release all devices." + ) + count = TACDev.GetDeviceCount() + for i in range(count): + device = TACDev.GetDevice(i) + if device and device.PortName() == port_name: + if not device.Open(): + raise RuntimeError(f"Failed to open device '{port_name}'.") + handle = _register(device, port_name, session_id) + device_ownership[port_name] = session_id + log.info("session=%s opened %s handle=%d", session_id, port_name, handle) + return {"handle": handle} + raise RuntimeError(f"No device with port name '{port_name}'.") + + +@mcp.tool() +async def close_tac_handle(handle: int, ctx: Context) -> dict: + session_id = ctx.session_id + async with _lock: + entry = handles.get(handle) + if entry is None: + raise RuntimeError(f"Invalid handle {handle}.") + device, port, owner = entry + if owner != session_id: + raise RuntimeError(f"Handle {handle} is owned by a different session.") + device.Close() + del handles[handle] + if handle in session_handles.get(session_id, []): + session_handles[session_id].remove(handle) + del device_ownership[port] + log.info("session=%s closed handle=%d port=%s", session_id, handle, port) + return {"success": True} + + +# --------------------------------------------------------------------------- # +# Tools: device info +# --------------------------------------------------------------------------- # +@mcp.tool() +async def get_name(handle: int, ctx: Context) -> dict: + return {"name": _get_device_owned(handle, ctx.session_id).Get_Name()} + + +@mcp.tool() +async def get_firmware_version(handle: int, ctx: Context) -> dict: + return {"firmware_version": _get_device_owned(handle, ctx.session_id).GetFirmwareVersion()} + + +@mcp.tool() +async def get_hardware(handle: int, ctx: Context) -> dict: + return {"hardware": _get_device_owned(handle, ctx.session_id).GetHardware()} + + +@mcp.tool() +async def get_hardware_version(handle: int, ctx: Context) -> dict: + return {"hardware_version": _get_device_owned(handle, ctx.session_id).Get_HardwareVersion()} + + +@mcp.tool() +async def get_uuid(handle: int, ctx: Context) -> dict: + return {"uuid": _get_device_owned(handle, ctx.session_id).Get_UUID()} + + +# --------------------------------------------------------------------------- # +# Tools: external power +# --------------------------------------------------------------------------- # +@mcp.tool() +async def set_external_power_control(handle: int, state: bool, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).SetExternalPowerControl(state) + return {"success": True} + + +# --------------------------------------------------------------------------- # +# Tools: dynamic commands +# --------------------------------------------------------------------------- # +@mcp.tool() +async def list_commands(handle: int, ctx: Context) -> dict: + device = _get_device_owned(handle, ctx.session_id) + return {"commands": [device.GetCommand(i) for i in range(device.GetCommandCount())]} + + +@mcp.tool() +async def get_command(handle: int, command_index: int, ctx: Context) -> dict: + return {"command": _get_device_owned(handle, ctx.session_id).GetCommand(command_index)} + + +@mcp.tool() +async def list_quick_commands(handle: int, ctx: Context) -> dict: + device = _get_device_owned(handle, ctx.session_id) + return {"quick_commands": [device.GetQuickCommand(i) for i in range(device.GetQuickCommandCount())]} + + +# --------------------------------------------------------------------------- # +# Tools: script variables +# --------------------------------------------------------------------------- # +@mcp.tool() +async def list_script_variables(handle: int, ctx: Context) -> dict: + device = _get_device_owned(handle, ctx.session_id) + return {"script_variables": [device.GetScriptVariable(i) for i in range(device.GetScriptVariableCount())]} + + +@mcp.tool() +async def update_script_variable(handle: int, variable: str, value: str, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).UpdateScriptVariableValue(variable, value) + return {"success": True} + + +# --------------------------------------------------------------------------- # +# Tools: core command interface +# --------------------------------------------------------------------------- # +@mcp.tool() +async def get_command_state(handle: int, command: str, ctx: Context) -> dict: + return {"command": command, "state": _get_device_owned(handle, ctx.session_id).GetCommandState(command)} + + +@mcp.tool() +async def send_command(handle: int, command: str, state: bool, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).SendCommand(command, state) + return {"success": True} + + +# --------------------------------------------------------------------------- # +# Tools: help / queue +# --------------------------------------------------------------------------- # +@mcp.tool() +async def get_help_text(handle: int, ctx: Context) -> dict: + return {"help_text": _get_device_owned(handle, ctx.session_id).GetHelpText()} + + +@mcp.tool() +async def is_command_queue_clear(handle: int, ctx: Context) -> dict: + return {"queue_clear": _get_device_owned(handle, ctx.session_id).IsCommandQueueClear()} + + +# --------------------------------------------------------------------------- # +# Tools: raw pin +# --------------------------------------------------------------------------- # +@mcp.tool() +async def set_pin_state(handle: int, pin: int, state: bool, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).SetPin(pin, state) + return {"success": True} + + +# --------------------------------------------------------------------------- # +# Tools: battery +# --------------------------------------------------------------------------- # +@mcp.tool() +async def set_battery_state(handle: int, state: bool, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).SetBatteryState(state) + return {"success": True} + + +@mcp.tool() +async def get_battery_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetBatteryState()} + + +# --------------------------------------------------------------------------- # +# Tools: USB +# --------------------------------------------------------------------------- # +@mcp.tool() +async def set_usb0(handle: int, state: bool, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).Usb0(state) + return {"success": True} + + +@mcp.tool() +async def get_usb0_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetUsb0State()} + + +@mcp.tool() +async def set_usb1(handle: int, state: bool, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).Usb1(state) + return {"success": True} + + +@mcp.tool() +async def get_usb1_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetUsb1State()} + + +# --------------------------------------------------------------------------- # +# Tools: power key +# --------------------------------------------------------------------------- # +@mcp.tool() +async def set_power_key(handle: int, state: bool, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).PowerKey(state) + return {"success": True} + + +@mcp.tool() +async def get_power_key_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetPowerKeyState()} + + +# --------------------------------------------------------------------------- # +# Tools: volume +# --------------------------------------------------------------------------- # +@mcp.tool() +async def set_volume_up(handle: int, state: bool, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).VolumeUp(state) + return {"success": True} + + +@mcp.tool() +async def get_volume_up_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetVolumeUpState()} + + +@mcp.tool() +async def set_volume_down(handle: int, state: bool, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).VolumeDown(state) + return {"success": True} + + +@mcp.tool() +async def get_volume_down_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetVolumeDownState()} + + +# --------------------------------------------------------------------------- # +# Tools: SIM / SD +# --------------------------------------------------------------------------- # +@mcp.tool() +async def set_disconnect_uim1(handle: int, state: bool, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).DisconnectUIM1(state) + return {"success": True} + + +@mcp.tool() +async def get_disconnect_uim1_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetDisconnectUIM1State()} + + +@mcp.tool() +async def set_disconnect_uim2(handle: int, state: bool, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).DisconnectUIM2(state) + return {"success": True} + + +@mcp.tool() +async def get_disconnect_uim2_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetDisconnectUIM2State()} + + +@mcp.tool() +async def set_disconnect_sd_card(handle: int, state: bool, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).DisconnectSDCard(state) + return {"success": True} + + +@mcp.tool() +async def get_disconnect_sd_card_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetDisconnectSDCardState()} + + +# --------------------------------------------------------------------------- # +# Tools: EDL +# --------------------------------------------------------------------------- # +@mcp.tool() +async def set_primary_edl(handle: int, state: bool, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).PrimaryEDL(state) + return {"success": True} + + +@mcp.tool() +async def get_primary_edl_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetPrimaryEDLState()} + + +@mcp.tool() +async def set_secondary_edl(handle: int, state: bool, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).SecondaryEDL(state) + return {"success": True} + + +@mcp.tool() +async def get_secondary_edl_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetSecondaryEDLState()} + + +# --------------------------------------------------------------------------- # +# Tools: PS_HOLD / RESIN_N +# --------------------------------------------------------------------------- # +@mcp.tool() +async def set_force_ps_hold_high(handle: int, state: bool, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).ForcePSHoldHigh(state) + return {"success": True} + + +@mcp.tool() +async def get_force_ps_hold_high_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetForcePSHoldHighState()} + + +@mcp.tool() +async def set_secondary_pm_resin_n(handle: int, state: bool, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).SecondaryPM_RESIN_N(state) + return {"success": True} + + +@mcp.tool() +async def get_secondary_pm_resin_n_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetSecondaryPM_RESIN_NState()} + + +# --------------------------------------------------------------------------- # +# Tools: EUD +# --------------------------------------------------------------------------- # +@mcp.tool() +async def set_eud(handle: int, state: bool, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).Eud(state) + return {"success": True} + + +@mcp.tool() +async def get_eud_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetEUDState()} + + +# --------------------------------------------------------------------------- # +# Tools: headset +# --------------------------------------------------------------------------- # +@mcp.tool() +async def set_headset_disconnect(handle: int, state: bool, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).HeadsetDisconnect(state) + return {"success": True} + + +@mcp.tool() +async def get_headset_disconnect_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetHeadsetDisconnectState()} + + +# --------------------------------------------------------------------------- # +# Tools: name / reset count +# --------------------------------------------------------------------------- # +@mcp.tool() +async def set_name(handle: int, new_name: str, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).SetName(new_name) + return {"success": True} + + +@mcp.tool() +async def get_reset_count(handle: int, ctx: Context) -> dict: + return {"reset_count": _get_device_owned(handle, ctx.session_id).GetResetCount()} + + +@mcp.tool() +async def clear_reset_count(handle: int, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).ClearResetCount() + return {"success": True} + + +# --------------------------------------------------------------------------- # +# Tools: button sequences +# --------------------------------------------------------------------------- # +@mcp.tool() +async def power_on_button(handle: int, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).PowerOnButton() + return {"success": True} + + +@mcp.tool() +async def power_off_button(handle: int, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).PowerOffButton() + return {"success": True} + + +@mcp.tool() +async def boot_to_fastboot_button(handle: int, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).BootToFastBootButton() + return {"success": True} + + +@mcp.tool() +async def boot_to_uefi_menu_button(handle: int, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).BootToUEFIMenuButton() + return {"success": True} + + +@mcp.tool() +async def boot_to_edl_button(handle: int, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).BootToEDLButton() + return {"success": True} + + +@mcp.tool() +async def boot_to_secondary_edl_button(handle: int, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).BootToSecondaryEDLButton() + return {"success": True} + + +# --------------------------------------------------------------------------- # +# Entry point +# --------------------------------------------------------------------------- # +if __name__ == "__main__": + server_cfg = cfg["server"] + log.info("Starting TACDev MCP server on %s:%d", server_cfg["host"], server_cfg["port"]) + log.info("Tip: if a client crashes and leaves a device locked, press Ctrl+C to restart the server. All devices will be released cleanly on shutdown.") + try: + mcp.run( + transport="sse", + host=server_cfg["host"], + port=server_cfg["port"], + uvicorn_config={"timeout_graceful_shutdown": 0}, + ) + except KeyboardInterrupt: + pass + finally: + _release_all() + log.info("Server stopped.") diff --git a/examples/MCP/stdio/README.md b/examples/MCP/stdio/README.md new file mode 100644 index 0000000..f860794 --- /dev/null +++ b/examples/MCP/stdio/README.md @@ -0,0 +1,83 @@ +# QTAC MCP Server - stdio + +Server is spawned automatically per-client process. No standalone server to manage. + +Use this variant for Claude CLI integration or single-client automation. +For multi-client shared access see [`../sse/`](../sse/). + +## Prerequisites + +- Python 3.10+ (architecture must match: x64 or ARM64) +- Project built via `build.bat` / `build.sh` + +## Setup + +Installs the TACDev library and all dependencies: + +```bat +setup.bat # Windows (auto-detects x64/ARM64) +``` +```bash +./setup.sh # Linux +``` + +Or manually: + +```bash +pip install interfaces/Python # from repo root +pip install -r examples/MCP/stdio/requirements.txt +``` + +## Running + +No separate server process needed - the server is spawned automatically. + +### Option 1 - Python client + +```bash +python examples/MCP/stdio/tacdev_mcp_client.py # first available device +python examples/MCP/stdio/tacdev_mcp_client.py COM41 # specific port +``` + +### Option 2 - Claude CLI + +Register once from repo root: + +```bash +claude mcp add TACDev-MCP -- python examples/MCP/stdio/tacdev_mcp_server.py +``` + +Verify: + +```bash +claude mcp list +``` + +Then ask Claude naturally - no special syntax needed: + +``` +List connected devices +Power on COM41 +Boot COM3 to fastboot +``` + +> **Note:** Claude spawns the server automatically on startup. No manual server management needed. + +**Scope options:** + +| Flag | Saved to | Visible to | +| :-- | :-- | :-- | +| *(default)* | `.claude/settings.local.json` | you, this project | +| `--scope project` | `.mcp.json` (repo root) | whole team | +| `--scope user` | `~/.claude/settings.json` | you, all projects | + +## Troubleshooting + +| Error | Fix | +| :-- | :-- | +| `No module named 'TACDev'` | `pip install interfaces/Python` from repo root | +| `TACDev library not found` | Run `build.bat` / `build.sh` first | +| `Architecture mismatch` | Use Python matching your OS architecture | +| `No module named 'fastmcp'` | `pip install -r requirements.txt` (requires Python 3.10+) | +| `cryptography` build failure on ARM64 | Re-run `setup.bat` - downloads OpenSSL automatically. Manual fallback: install [Win64ARMOpenSSL-4_0_1.msi](https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi) (full, not Light) and set `OPENSSL_DIR=C:\Program Files\OpenSSL-Win64-ARM` | +| `get_device_count` returns 0 | Check board is connected. Windows: run `FTDICheck.exe` from `__Builds\x64\Release\bin`. Linux: `sudo cp udev-rules/99-QTAC-USB.rules /etc/udev/rules.d/ && sudo udevadm control --reload` | diff --git a/examples/MCP/stdio/requirements.txt b/examples/MCP/stdio/requirements.txt new file mode 100644 index 0000000..886b069 --- /dev/null +++ b/examples/MCP/stdio/requirements.txt @@ -0,0 +1 @@ +fastmcp>=3.0.0 diff --git a/examples/MCP/stdio/setup.bat b/examples/MCP/stdio/setup.bat new file mode 100644 index 0000000..1e41035 --- /dev/null +++ b/examples/MCP/stdio/setup.bat @@ -0,0 +1,85 @@ +@REM Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +@REM SPDX-License-Identifier: BSD-3-Clause + +@echo off +setlocal enabledelayedexpansion + +set SCRIPT_DIR=%~dp0 +pushd "%SCRIPT_DIR%\..\..\..\" +set REPO_ROOT=%CD% +popd + +set ARCH=%1 +if "%ARCH%"=="" ( + if "%PROCESSOR_ARCHITECTURE%"=="ARM64" (set ARCH=ARM64) else (set ARCH=x64) +) + +if /i "%ARCH%"=="ARM64" ( + if "%OPENSSL_DIR%"=="" set OPENSSL_DIR=C:\Program Files\OpenSSL-Win64-ARM +) + +if /i "%ARCH%"=="ARM64" if not exist "!OPENSSL_DIR!\include" ( + echo OpenSSL not found. Downloading and installing... + curl -L -o "%TEMP%\Win64ARMOpenSSL.msi" https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi + if errorlevel 1 ( + echo. + echo ERROR: Download failed. Install manually from: + echo https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi + echo Then re-run setup.bat + echo. + exit /b 1 + ) + msiexec /i "%TEMP%\Win64ARMOpenSSL.msi" /quiet /norestart INSTALLDIR="!OPENSSL_DIR!" + if errorlevel 1 ( + echo. + echo ERROR: OpenSSL installation failed. Install manually from: + echo https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi + echo Then re-run setup.bat + echo. + exit /b 1 + ) + echo OpenSSL installed to !OPENSSL_DIR! +) + +if /i "%ARCH%"=="ARM64" ( + if not exist "!OPENSSL_DIR!\include" ( + echo. + echo ERROR: OpenSSL headers not found at !OPENSSL_DIR!\include + echo Set OPENSSL_DIR to the full installer path and re-run: + echo setx OPENSSL_DIR "C:\Program Files\OpenSSL-Win64-ARM" + echo. + exit /b 1 + ) + if not exist "!OPENSSL_DIR!\lib\VC\arm64\MD" ( + echo. + echo ERROR: OpenSSL libs not found at !OPENSSL_DIR!\lib\VC\arm64\MD + echo Reinstall using the full installer ^(not Light^): + echo https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi + echo. + exit /b 1 + ) + set OPENSSL_LIB_DIR=!OPENSSL_DIR!\lib\VC\arm64\MD + echo OpenSSL : !OPENSSL_DIR! [OK] + echo OpenSSL libs : !OPENSSL_LIB_DIR! [OK] +) + +set BUILD_DIR=%REPO_ROOT%\__Builds +if not exist "%BUILD_DIR%" ( + echo [1/3] Build not found. Running root build... + call "%REPO_ROOT%\build.bat" + if errorlevel 1 exit /b 1 +) else ( + echo [1/3] Build found at %BUILD_DIR%, skipping recompile. +) + +echo [2/3] Installing TACDev Python library... +pip install "%REPO_ROOT%\interfaces\Python" +if errorlevel 1 exit /b 1 + +echo [3/3] Installing MCP dependencies... +pip install -r "%SCRIPT_DIR%requirements.txt" +if errorlevel 1 exit /b 1 + +echo. +echo Setup complete. Run the client directly (no server process needed): +echo python examples\MCP\stdio\tacdev_mcp_client.py diff --git a/examples/MCP/stdio/setup.sh b/examples/MCP/stdio/setup.sh new file mode 100644 index 0000000..ae15e00 --- /dev/null +++ b/examples/MCP/stdio/setup.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" + +if [ ! -d "$REPO_ROOT/__Builds" ]; then + echo "[1/3] Build not found. Running root build..." + "$REPO_ROOT/build.sh" +else + echo "[1/3] Build found at $REPO_ROOT/__Builds, skipping recompile." +fi + +echo "[2/3] Installing TACDev Python library..." +pip install "$REPO_ROOT/interfaces/Python" + +echo "[3/3] Installing MCP dependencies..." +pip install -r "$SCRIPT_DIR/requirements.txt" + +echo "" +echo "Setup complete. Run the client directly (no server process needed):" +echo " python examples/MCP/stdio/tacdev_mcp_client.py" diff --git a/examples/MCP/stdio/tacdev_mcp_client.py b/examples/MCP/stdio/tacdev_mcp_client.py new file mode 100644 index 0000000..9fd51f7 --- /dev/null +++ b/examples/MCP/stdio/tacdev_mcp_client.py @@ -0,0 +1,323 @@ +#!/usr/bin/env python3 + +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +""" +MCP client for tacdev_mcp_server.py (stdio transport). + +The server is spawned automatically as a subprocess — no separate process to +start before running this client. + +Typical usage +------------- + from tacdev_mcp_client import TACDevClient + + async def main(): + async with TACDevClient() as tac: + devices = await tac.list_devices() + handle = await tac.open_handle_by_description("COM3") + await tac.power_on_button(handle) + await tac.close_tac_handle(handle) + + asyncio.run(main()) +""" + +from __future__ import annotations + +import asyncio +import json +import sys +from pathlib import Path +from typing import Any + +from fastmcp import Client + +_SERVER_PATH = Path(__file__).with_name("tacdev_mcp_server.py") + + +class TACDevClient: + def __init__(self) -> None: + self._client = Client({"mcpServers": {"tacdev": {"command": sys.executable, "args": [str(_SERVER_PATH)]}}}) + + async def __aenter__(self) -> TACDevClient: + await self._client.__aenter__() + return self + + async def __aexit__(self, *args) -> None: + await self._client.__aexit__(*args) + + async def _call(self, tool: str, **kwargs: Any) -> Any: + result = await self._client.call_tool(tool, kwargs) + if result.content: + text = result.content[0].text + try: + return json.loads(text) + except (ValueError, TypeError): + return text + return None + + async def list_devices(self) -> dict: + return await self._call("list_devices") + + async def get_alpaca_version(self) -> str: + return (await self._call("get_alpaca_version"))["alpaca_version"] + + async def get_tac_version(self) -> str: + return (await self._call("get_tac_version"))["tac_version"] + + async def get_last_tac_error(self) -> str: + return (await self._call("get_last_tac_error"))["last_error"] + + async def get_logging_state(self) -> bool: + return (await self._call("get_logging_state"))["logging_enabled"] + + async def set_logging_state(self, enabled: bool) -> None: + await self._call("set_logging_state", enabled=enabled) + + async def get_device_count(self) -> int: + return (await self._call("get_device_count"))["device_count"] + + async def get_port_data(self, device_index: int) -> str: + return (await self._call("get_port_data", device_index=device_index))["port_data"] + + async def list_ports(self) -> list[str]: + count = await self.get_device_count() + return [await self.get_port_data(i) for i in range(count)] + + async def open_handle_by_description(self, port_name: str) -> int: + return (await self._call("open_handle_by_description", port_name=port_name))["handle"] + + async def close_tac_handle(self, handle: int) -> None: + await self._call("close_tac_handle", handle=handle) + + async def get_name(self, handle: int) -> str: + return (await self._call("get_name", handle=handle))["name"] + + async def get_firmware_version(self, handle: int) -> str: + return (await self._call("get_firmware_version", handle=handle))["firmware_version"] + + async def get_hardware(self, handle: int) -> str: + return (await self._call("get_hardware", handle=handle))["hardware"] + + async def get_hardware_version(self, handle: int) -> str: + return (await self._call("get_hardware_version", handle=handle))["hardware_version"] + + async def get_uuid(self, handle: int) -> str: + return (await self._call("get_uuid", handle=handle))["uuid"] + + async def get_device_info(self, handle: int) -> dict: + return { + "name": await self.get_name(handle), + "firmware_version": await self.get_firmware_version(handle), + "hardware": await self.get_hardware(handle), + "hardware_version": await self.get_hardware_version(handle), + "uuid": await self.get_uuid(handle), + } + + async def set_external_power_control(self, handle: int, state: bool) -> None: + await self._call("set_external_power_control", handle=handle, state=state) + + async def list_commands(self, handle: int) -> list[str]: + return (await self._call("list_commands", handle=handle))["commands"] + + async def get_command(self, handle: int, command_index: int) -> str: + return (await self._call("get_command", handle=handle, command_index=command_index))["command"] + + async def list_quick_commands(self, handle: int) -> list[str]: + return (await self._call("list_quick_commands", handle=handle))["quick_commands"] + + async def list_script_variables(self, handle: int) -> list[str]: + return (await self._call("list_script_variables", handle=handle))["script_variables"] + + async def update_script_variable(self, handle: int, variable: str, value: str) -> None: + await self._call("update_script_variable", handle=handle, variable=variable, value=value) + + async def get_command_state(self, handle: int, command: str) -> bool: + return (await self._call("get_command_state", handle=handle, command=command))["state"] + + async def send_command(self, handle: int, command: str, state: bool) -> None: + await self._call("send_command", handle=handle, command=command, state=state) + + async def get_help_text(self, handle: int) -> str: + return (await self._call("get_help_text", handle=handle))["help_text"] + + async def is_command_queue_clear(self, handle: int) -> bool: + return (await self._call("is_command_queue_clear", handle=handle))["queue_clear"] + + async def set_pin_state(self, handle: int, pin: int, state: bool) -> None: + await self._call("set_pin_state", handle=handle, pin=pin, state=state) + + async def set_battery_state(self, handle: int, state: bool) -> None: + await self._call("set_battery_state", handle=handle, state=state) + + async def get_battery_state(self, handle: int) -> bool: + return (await self._call("get_battery_state", handle=handle))["state"] + + async def set_usb0(self, handle: int, state: bool) -> None: + await self._call("set_usb0", handle=handle, state=state) + + async def get_usb0_state(self, handle: int) -> bool: + return (await self._call("get_usb0_state", handle=handle))["state"] + + async def set_usb1(self, handle: int, state: bool) -> None: + await self._call("set_usb1", handle=handle, state=state) + + async def get_usb1_state(self, handle: int) -> bool: + return (await self._call("get_usb1_state", handle=handle))["state"] + + async def set_power_key(self, handle: int, state: bool) -> None: + await self._call("set_power_key", handle=handle, state=state) + + async def get_power_key_state(self, handle: int) -> bool: + return (await self._call("get_power_key_state", handle=handle))["state"] + + async def set_volume_up(self, handle: int, state: bool) -> None: + await self._call("set_volume_up", handle=handle, state=state) + + async def get_volume_up_state(self, handle: int) -> bool: + return (await self._call("get_volume_up_state", handle=handle))["state"] + + async def set_volume_down(self, handle: int, state: bool) -> None: + await self._call("set_volume_down", handle=handle, state=state) + + async def get_volume_down_state(self, handle: int) -> bool: + return (await self._call("get_volume_down_state", handle=handle))["state"] + + async def set_disconnect_uim1(self, handle: int, state: bool) -> None: + await self._call("set_disconnect_uim1", handle=handle, state=state) + + async def get_disconnect_uim1_state(self, handle: int) -> bool: + return (await self._call("get_disconnect_uim1_state", handle=handle))["state"] + + async def set_disconnect_uim2(self, handle: int, state: bool) -> None: + await self._call("set_disconnect_uim2", handle=handle, state=state) + + async def get_disconnect_uim2_state(self, handle: int) -> bool: + return (await self._call("get_disconnect_uim2_state", handle=handle))["state"] + + async def set_disconnect_sd_card(self, handle: int, state: bool) -> None: + await self._call("set_disconnect_sd_card", handle=handle, state=state) + + async def get_disconnect_sd_card_state(self, handle: int) -> bool: + return (await self._call("get_disconnect_sd_card_state", handle=handle))["state"] + + async def set_primary_edl(self, handle: int, state: bool) -> None: + await self._call("set_primary_edl", handle=handle, state=state) + + async def get_primary_edl_state(self, handle: int) -> bool: + return (await self._call("get_primary_edl_state", handle=handle))["state"] + + async def set_secondary_edl(self, handle: int, state: bool) -> None: + await self._call("set_secondary_edl", handle=handle, state=state) + + async def get_secondary_edl_state(self, handle: int) -> bool: + return (await self._call("get_secondary_edl_state", handle=handle))["state"] + + async def set_force_ps_hold_high(self, handle: int, state: bool) -> None: + await self._call("set_force_ps_hold_high", handle=handle, state=state) + + async def get_force_ps_hold_high_state(self, handle: int) -> bool: + return (await self._call("get_force_ps_hold_high_state", handle=handle))["state"] + + async def set_secondary_pm_resin_n(self, handle: int, state: bool) -> None: + await self._call("set_secondary_pm_resin_n", handle=handle, state=state) + + async def get_secondary_pm_resin_n_state(self, handle: int) -> bool: + return (await self._call("get_secondary_pm_resin_n_state", handle=handle))["state"] + + async def set_eud(self, handle: int, state: bool) -> None: + await self._call("set_eud", handle=handle, state=state) + + async def get_eud_state(self, handle: int) -> bool: + return (await self._call("get_eud_state", handle=handle))["state"] + + async def set_headset_disconnect(self, handle: int, state: bool) -> None: + await self._call("set_headset_disconnect", handle=handle, state=state) + + async def get_headset_disconnect_state(self, handle: int) -> bool: + return (await self._call("get_headset_disconnect_state", handle=handle))["state"] + + async def set_name(self, handle: int, new_name: str) -> None: + await self._call("set_name", handle=handle, new_name=new_name) + + async def get_reset_count(self, handle: int) -> int: + return (await self._call("get_reset_count", handle=handle))["reset_count"] + + async def clear_reset_count(self, handle: int) -> None: + await self._call("clear_reset_count", handle=handle) + + async def power_on_button(self, handle: int) -> None: + await self._call("power_on_button", handle=handle) + + async def power_off_button(self, handle: int) -> None: + await self._call("power_off_button", handle=handle) + + async def boot_to_fastboot_button(self, handle: int) -> None: + await self._call("boot_to_fastboot_button", handle=handle) + + async def boot_to_uefi_menu_button(self, handle: int) -> None: + await self._call("boot_to_uefi_menu_button", handle=handle) + + async def boot_to_edl_button(self, handle: int) -> None: + await self._call("boot_to_edl_button", handle=handle) + + async def boot_to_secondary_edl_button(self, handle: int) -> None: + await self._call("boot_to_secondary_edl_button", handle=handle) + + +async def TACExample(port_name: str | None = None) -> None: + async with TACDevClient() as tac: + print(f" Alpaca version : {await tac.get_alpaca_version()}") + print(f" TAC version : {await tac.get_tac_version()}") + print(f" Logging : {await tac.get_logging_state()}") + + devices = await tac.list_devices() + print(f" Available : {devices['available']}") + + if not port_name and devices["available"]: + port_name = devices["available"][0]["port"] + + if port_name: + print(f"\nOpening '{port_name}'...") + handle = await tac.open_handle_by_description(port_name) + + info = await tac.get_device_info(handle) + for k, v in info.items(): + print(f" {k:<20}: {v}") + + print(f"\n Queue clear : {await tac.is_command_queue_clear(handle)}") + + print("\n--- Available commands ---") + print(await tac.get_help_text(handle)) + + commands = await tac.list_commands(handle) + if commands: + cmd_names = [c.split(";")[0].strip() for c in commands] + print(f"\nCommands : {cmd_names}") + + quick_commands = await tac.list_quick_commands(handle) + if quick_commands: + qc_names = [q.split(";")[1].strip() for q in quick_commands] + print(f"Quick commands: {qc_names}") + + if commands: + cmd = cmd_names[0] + state_before = await tac.get_command_state(handle, cmd) + print(f"\nCommand '{cmd}' state before toggle: {state_before}") + await tac.send_command(handle, cmd, not state_before) + await asyncio.sleep(2) + state_after = await tac.get_command_state(handle, cmd) + print(f"Command '{cmd}' state after toggle: {state_after}") + await tac.send_command(handle, cmd, state_before) + await asyncio.sleep(2) + print(f"Command '{cmd}' restored to : {await tac.get_command_state(handle, cmd)}") + + await tac.close_tac_handle(handle) + print("\nHandle closed.") + else: + print("No device available.") + + +if __name__ == "__main__": + asyncio.run(TACExample(sys.argv[1] if len(sys.argv) > 1 else None)) diff --git a/examples/MCP/stdio/tacdev_mcp_server.py b/examples/MCP/stdio/tacdev_mcp_server.py new file mode 100644 index 0000000..5f14a97 --- /dev/null +++ b/examples/MCP/stdio/tacdev_mcp_server.py @@ -0,0 +1,500 @@ +#!/usr/bin/env python3 + +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +""" +MCP server for TACDev using stdio transport. + +The server is launched automatically by the client as a subprocess — there is +no standalone process to start. Each client gets its own server instance. +""" + +import TACDev +from fastmcp import FastMCP + +handles: dict[int, TACDev.TACDevice] = {} +_next_handle = 1 + +mcp = FastMCP("TACDev-MCP") + + +def _register(device: TACDev.TACDevice) -> int: + global _next_handle + handle = _next_handle + _next_handle += 1 + handles[handle] = device + return handle + + +def _get_device(handle: int) -> TACDev.TACDevice: + device = handles.get(handle) + if device is None: + raise RuntimeError(f"Invalid handle {handle}.") + return device + + +def _release_all() -> None: + for handle, device in list(handles.items()): + try: + device.Close() + except Exception: + pass + handles.clear() + + +# --------------------------------------------------------------------------- # +# Tools: device list +# --------------------------------------------------------------------------- # +@mcp.tool() +def list_devices() -> dict: + count = TACDev.GetDeviceCount() + available = [] + for i in range(count): + d = TACDev.GetDevice(i) + if d: + available.append({"port": d.PortName(), "description": d.Description(), "serial": d.SerialNumber()}) + return {"available": available, "in_use": []} + + +# --------------------------------------------------------------------------- # +# Tools: diagnostics +# --------------------------------------------------------------------------- # +@mcp.tool() +def get_alpaca_version() -> dict: + return {"alpaca_version": TACDev.AlpacaVersion()} + + +@mcp.tool() +def get_tac_version() -> dict: + return {"tac_version": TACDev.TACVersion()} + + +@mcp.tool() +def get_last_tac_error() -> dict: + return {"last_error": TACDev.GetLastError()} + + +# --------------------------------------------------------------------------- # +# Tools: logging +# --------------------------------------------------------------------------- # +@mcp.tool() +def get_logging_state() -> dict: + return {"logging_enabled": TACDev.GetLoggingState()} + + +@mcp.tool() +def set_logging_state(enabled: bool) -> dict: + TACDev.SetLoggingState(enabled) + return {"success": True} + + +# --------------------------------------------------------------------------- # +# Tools: device enumeration +# --------------------------------------------------------------------------- # +@mcp.tool() +def get_device_count() -> dict: + return {"device_count": TACDev.GetDeviceCount()} + + +@mcp.tool() +def get_port_data(device_index: int) -> dict: + device = TACDev.GetDevice(device_index) + if device is None: + raise RuntimeError(f"No device at index {device_index}.") + return {"port_data": device.PortName(), "description": device.Description(), "serial_number": device.SerialNumber()} + + +# --------------------------------------------------------------------------- # +# Tools: handle management +# --------------------------------------------------------------------------- # +@mcp.tool() +def open_handle_by_description(port_name: str) -> dict: + count = TACDev.GetDeviceCount() + for i in range(count): + device = TACDev.GetDevice(i) + if device and device.PortName() == port_name: + if not device.Open(): + raise RuntimeError(f"Failed to open device '{port_name}'.") + return {"handle": _register(device)} + raise RuntimeError(f"No device with port name '{port_name}'.") + + +@mcp.tool() +def close_tac_handle(handle: int) -> dict: + device = _get_device(handle) + device.Close() + del handles[handle] + return {"success": True} + + +# --------------------------------------------------------------------------- # +# Tools: device info +# --------------------------------------------------------------------------- # +@mcp.tool() +def get_name(handle: int) -> dict: + return {"name": _get_device(handle).Get_Name()} + + +@mcp.tool() +def get_firmware_version(handle: int) -> dict: + return {"firmware_version": _get_device(handle).GetFirmwareVersion()} + + +@mcp.tool() +def get_hardware(handle: int) -> dict: + return {"hardware": _get_device(handle).GetHardware()} + + +@mcp.tool() +def get_hardware_version(handle: int) -> dict: + return {"hardware_version": _get_device(handle).Get_HardwareVersion()} + + +@mcp.tool() +def get_uuid(handle: int) -> dict: + return {"uuid": _get_device(handle).Get_UUID()} + + +# --------------------------------------------------------------------------- # +# Tools: external power +# --------------------------------------------------------------------------- # +@mcp.tool() +def set_external_power_control(handle: int, state: bool) -> dict: + _get_device(handle).SetExternalPowerControl(state) + return {"success": True} + + +# --------------------------------------------------------------------------- # +# Tools: dynamic commands +# --------------------------------------------------------------------------- # +@mcp.tool() +def list_commands(handle: int) -> dict: + device = _get_device(handle) + return {"commands": [device.GetCommand(i) for i in range(device.GetCommandCount())]} + + +@mcp.tool() +def get_command(handle: int, command_index: int) -> dict: + return {"command": _get_device(handle).GetCommand(command_index)} + + +@mcp.tool() +def list_quick_commands(handle: int) -> dict: + device = _get_device(handle) + return {"quick_commands": [device.GetQuickCommand(i) for i in range(device.GetQuickCommandCount())]} + + +# --------------------------------------------------------------------------- # +# Tools: script variables +# --------------------------------------------------------------------------- # +@mcp.tool() +def list_script_variables(handle: int) -> dict: + device = _get_device(handle) + return {"script_variables": [device.GetScriptVariable(i) for i in range(device.GetScriptVariableCount())]} + + +@mcp.tool() +def update_script_variable(handle: int, variable: str, value: str) -> dict: + _get_device(handle).UpdateScriptVariableValue(variable, value) + return {"success": True} + + +# --------------------------------------------------------------------------- # +# Tools: core command interface +# --------------------------------------------------------------------------- # +@mcp.tool() +def get_command_state(handle: int, command: str) -> dict: + return {"command": command, "state": _get_device(handle).GetCommandState(command)} + + +@mcp.tool() +def send_command(handle: int, command: str, state: bool) -> dict: + _get_device(handle).SendCommand(command, state) + return {"success": True} + + +# --------------------------------------------------------------------------- # +# Tools: help / queue +# --------------------------------------------------------------------------- # +@mcp.tool() +def get_help_text(handle: int) -> dict: + return {"help_text": _get_device(handle).GetHelpText()} + + +@mcp.tool() +def is_command_queue_clear(handle: int) -> dict: + return {"queue_clear": _get_device(handle).IsCommandQueueClear()} + + +# --------------------------------------------------------------------------- # +# Tools: raw pin +# --------------------------------------------------------------------------- # +@mcp.tool() +def set_pin_state(handle: int, pin: int, state: bool) -> dict: + _get_device(handle).SetPin(pin, state) + return {"success": True} + + +# --------------------------------------------------------------------------- # +# Tools: battery +# --------------------------------------------------------------------------- # +@mcp.tool() +def set_battery_state(handle: int, state: bool) -> dict: + _get_device(handle).SetBatteryState(state) + return {"success": True} + + +@mcp.tool() +def get_battery_state(handle: int) -> dict: + return {"state": _get_device(handle).GetBatteryState()} + + +# --------------------------------------------------------------------------- # +# Tools: USB +# --------------------------------------------------------------------------- # +@mcp.tool() +def set_usb0(handle: int, state: bool) -> dict: + _get_device(handle).Usb0(state) + return {"success": True} + + +@mcp.tool() +def get_usb0_state(handle: int) -> dict: + return {"state": _get_device(handle).GetUsb0State()} + + +@mcp.tool() +def set_usb1(handle: int, state: bool) -> dict: + _get_device(handle).Usb1(state) + return {"success": True} + + +@mcp.tool() +def get_usb1_state(handle: int) -> dict: + return {"state": _get_device(handle).GetUsb1State()} + + +# --------------------------------------------------------------------------- # +# Tools: power key +# --------------------------------------------------------------------------- # +@mcp.tool() +def set_power_key(handle: int, state: bool) -> dict: + _get_device(handle).PowerKey(state) + return {"success": True} + + +@mcp.tool() +def get_power_key_state(handle: int) -> dict: + return {"state": _get_device(handle).GetPowerKeyState()} + + +# --------------------------------------------------------------------------- # +# Tools: volume +# --------------------------------------------------------------------------- # +@mcp.tool() +def set_volume_up(handle: int, state: bool) -> dict: + _get_device(handle).VolumeUp(state) + return {"success": True} + + +@mcp.tool() +def get_volume_up_state(handle: int) -> dict: + return {"state": _get_device(handle).GetVolumeUpState()} + + +@mcp.tool() +def set_volume_down(handle: int, state: bool) -> dict: + _get_device(handle).VolumeDown(state) + return {"success": True} + + +@mcp.tool() +def get_volume_down_state(handle: int) -> dict: + return {"state": _get_device(handle).GetVolumeDownState()} + + +# --------------------------------------------------------------------------- # +# Tools: SIM / SD +# --------------------------------------------------------------------------- # +@mcp.tool() +def set_disconnect_uim1(handle: int, state: bool) -> dict: + _get_device(handle).DisconnectUIM1(state) + return {"success": True} + + +@mcp.tool() +def get_disconnect_uim1_state(handle: int) -> dict: + return {"state": _get_device(handle).GetDisconnectUIM1State()} + + +@mcp.tool() +def set_disconnect_uim2(handle: int, state: bool) -> dict: + _get_device(handle).DisconnectUIM2(state) + return {"success": True} + + +@mcp.tool() +def get_disconnect_uim2_state(handle: int) -> dict: + return {"state": _get_device(handle).GetDisconnectUIM2State()} + + +@mcp.tool() +def set_disconnect_sd_card(handle: int, state: bool) -> dict: + _get_device(handle).DisconnectSDCard(state) + return {"success": True} + + +@mcp.tool() +def get_disconnect_sd_card_state(handle: int) -> dict: + return {"state": _get_device(handle).GetDisconnectSDCardState()} + + +# --------------------------------------------------------------------------- # +# Tools: EDL +# --------------------------------------------------------------------------- # +@mcp.tool() +def set_primary_edl(handle: int, state: bool) -> dict: + _get_device(handle).PrimaryEDL(state) + return {"success": True} + + +@mcp.tool() +def get_primary_edl_state(handle: int) -> dict: + return {"state": _get_device(handle).GetPrimaryEDLState()} + + +@mcp.tool() +def set_secondary_edl(handle: int, state: bool) -> dict: + _get_device(handle).SecondaryEDL(state) + return {"success": True} + + +@mcp.tool() +def get_secondary_edl_state(handle: int) -> dict: + return {"state": _get_device(handle).GetSecondaryEDLState()} + + +# --------------------------------------------------------------------------- # +# Tools: PS_HOLD / RESIN_N +# --------------------------------------------------------------------------- # +@mcp.tool() +def set_force_ps_hold_high(handle: int, state: bool) -> dict: + _get_device(handle).ForcePSHoldHigh(state) + return {"success": True} + + +@mcp.tool() +def get_force_ps_hold_high_state(handle: int) -> dict: + return {"state": _get_device(handle).GetForcePSHoldHighState()} + + +@mcp.tool() +def set_secondary_pm_resin_n(handle: int, state: bool) -> dict: + _get_device(handle).SecondaryPM_RESIN_N(state) + return {"success": True} + + +@mcp.tool() +def get_secondary_pm_resin_n_state(handle: int) -> dict: + return {"state": _get_device(handle).GetSecondaryPM_RESIN_NState()} + + +# --------------------------------------------------------------------------- # +# Tools: EUD +# --------------------------------------------------------------------------- # +@mcp.tool() +def set_eud(handle: int, state: bool) -> dict: + _get_device(handle).Eud(state) + return {"success": True} + + +@mcp.tool() +def get_eud_state(handle: int) -> dict: + return {"state": _get_device(handle).GetEUDState()} + + +# --------------------------------------------------------------------------- # +# Tools: headset +# --------------------------------------------------------------------------- # +@mcp.tool() +def set_headset_disconnect(handle: int, state: bool) -> dict: + _get_device(handle).HeadsetDisconnect(state) + return {"success": True} + + +@mcp.tool() +def get_headset_disconnect_state(handle: int) -> dict: + return {"state": _get_device(handle).GetHeadsetDisconnectState()} + + +# --------------------------------------------------------------------------- # +# Tools: name / reset count +# --------------------------------------------------------------------------- # +@mcp.tool() +def set_name(handle: int, new_name: str) -> dict: + _get_device(handle).SetName(new_name) + return {"success": True} + + +@mcp.tool() +def get_reset_count(handle: int) -> dict: + return {"reset_count": _get_device(handle).GetResetCount()} + + +@mcp.tool() +def clear_reset_count(handle: int) -> dict: + _get_device(handle).ClearResetCount() + return {"success": True} + + +# --------------------------------------------------------------------------- # +# Tools: button sequences +# --------------------------------------------------------------------------- # +@mcp.tool() +def power_on_button(handle: int) -> dict: + _get_device(handle).PowerOnButton() + return {"success": True} + + +@mcp.tool() +def power_off_button(handle: int) -> dict: + _get_device(handle).PowerOffButton() + return {"success": True} + + +@mcp.tool() +def boot_to_fastboot_button(handle: int) -> dict: + _get_device(handle).BootToFastBootButton() + return {"success": True} + + +@mcp.tool() +def boot_to_uefi_menu_button(handle: int) -> dict: + _get_device(handle).BootToUEFIMenuButton() + return {"success": True} + + +@mcp.tool() +def boot_to_edl_button(handle: int) -> dict: + _get_device(handle).BootToEDLButton() + return {"success": True} + + +@mcp.tool() +def boot_to_secondary_edl_button(handle: int) -> dict: + _get_device(handle).BootToSecondaryEDLButton() + return {"success": True} + + + +# --------------------------------------------------------------------------- # +# Entry point +# --------------------------------------------------------------------------- # +if __name__ == "__main__": + try: + mcp.run(transport="stdio") + except KeyboardInterrupt: + pass + finally: + _release_all() diff --git a/interfaces/C++/TACDev/CMakeLists.txt b/interfaces/C++/TACDev/CMakeLists.txt index e0d7d78..88bc714 100644 --- a/interfaces/C++/TACDev/CMakeLists.txt +++ b/interfaces/C++/TACDev/CMakeLists.txt @@ -1,36 +1,5 @@ -# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted (subject to the limitations in the -# disclaimer below) provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# * Neither the name of Qualcomm Technologies, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE -# GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT -# HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED -# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE -# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER -# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN -# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -# Author: Biswajit Roy (biswroy@qti.qualcomm.com) +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause include(${CMAKE_SOURCE_DIR}/src/libraries/qcommon-console/Common.cmake) diff --git a/interfaces/C++/TACDev/TACDev.cpp b/interfaces/C++/TACDev/TACDev.cpp index eba06f6..2848b4c 100644 --- a/interfaces/C++/TACDev/TACDev.cpp +++ b/interfaces/C++/TACDev/TACDev.cpp @@ -1,41 +1,5 @@ -/* - Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. - - Redistribution and use in source and binary forms, with or without - modification, are permitted (subject to the limitations in the - disclaimer below) provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - * Neither the name of Qualcomm Technologies, Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - - NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE - GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT - HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED - WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE - GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER - IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR - OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN - IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -/* - Author: Michael Simpson (msimpson@qti.qualcomm.com) - Biswajit Roy (biswroy@qti.qualcomm.com) -*/ +// Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +// SPDX-License-Identifier: BSD-3-Clause #include "TACDev.h" #include "TACDevCore.h" diff --git a/interfaces/C++/TACDev/TACDev.h b/interfaces/C++/TACDev/TACDev.h index 11a537a..ee7cd87 100644 --- a/interfaces/C++/TACDev/TACDev.h +++ b/interfaces/C++/TACDev/TACDev.h @@ -1,42 +1,8 @@ +// Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +// SPDX-License-Identifier: BSD-3-Clause + #ifndef DEVTAC_H #define DEVTAC_H -/* - Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. - - Redistribution and use in source and binary forms, with or without - modification, are permitted (subject to the limitations in the - disclaimer below) provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - * Neither the name of Qualcomm Technologies, Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - - NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE - GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT - HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED - WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE - GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER - IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR - OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN - IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -/* - Author: Michael Simpson (msimpson@qti.qualcomm.com) -*/ #if defined(TACDEV_LIBRARY) #ifdef __linux__ diff --git a/interfaces/C++/TACDev/TACDevCore.cpp b/interfaces/C++/TACDev/TACDevCore.cpp index bc93261..e363e53 100644 --- a/interfaces/C++/TACDev/TACDevCore.cpp +++ b/interfaces/C++/TACDev/TACDevCore.cpp @@ -1,40 +1,5 @@ -/* - Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. - - Redistribution and use in source and binary forms, with or without - modification, are permitted (subject to the limitations in the - disclaimer below) provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - * Neither the name of Qualcomm Technologies, Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - - NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE - GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT - HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED - WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE - GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER - IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR - OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN - IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -/* - Author: Michael Simpson (msimpson@qti.qualcomm.com) -*/ +// Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +// SPDX-License-Identifier: BSD-3-Clause #include "TACDevCore.h" #include "AlpacaDefines.h" diff --git a/interfaces/C++/TACDev/TACDevCore.h b/interfaces/C++/TACDev/TACDevCore.h index feb3dac..5fb6aae 100644 --- a/interfaces/C++/TACDev/TACDevCore.h +++ b/interfaces/C++/TACDev/TACDevCore.h @@ -1,41 +1,5 @@ -/* - Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. - - Redistribution and use in source and binary forms, with or without - modification, are permitted (subject to the limitations in the - disclaimer below) provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - * Neither the name of Qualcomm Technologies, Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - - NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE - GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT - HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED - WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE - GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER - IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR - OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN - IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -/* - Author: Michael Simpson (msimpson@qti.qualcomm.com) - Biswajit Roy (biswroy@qti.qualcomm.com) -*/ +// Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +// SPDX-License-Identifier: BSD-3-Clause #include "TACDev.h" diff --git a/interfaces/CMakeLists.txt b/interfaces/CMakeLists.txt index 189f6c1..138ef7f 100644 --- a/interfaces/CMakeLists.txt +++ b/interfaces/CMakeLists.txt @@ -1,36 +1,5 @@ -# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted (subject to the limitations in the -# disclaimer below) provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# * Neither the name of Qualcomm Technologies, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE -# GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT -# HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED -# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE -# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER -# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN -# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -# Author: Biswajit Roy (biswroy@qti.qualcomm.com) +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause cmake_minimum_required(VERSION 3.22) project(interfaces VERSION 1.0 LANGUAGES C CXX) diff --git a/interfaces/Python/TACDev/TACDev.py b/interfaces/Python/TACDev/TACDev.py index 528f714..2010b25 100644 --- a/interfaces/Python/TACDev/TACDev.py +++ b/interfaces/Python/TACDev/TACDev.py @@ -1,117 +1,61 @@ -# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted (subject to the limitations in the -# disclaimer below) provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# * Neither the name of Qualcomm Technologies, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE -# GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT -# HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED -# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE -# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER -# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN -# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause # TACDev Python Library # Written by Michael Simpson (msimpson@qti.qualcomm.com) and edited by Biswajit Roy (biswroy@qti.qualcomm.com) # Don't modify this file. If you do, you run the risk of missing out on bug fixes and product updates import logging -import os +import platform +import struct import time from ctypes import * from pathlib import Path -from sys import exit, platform +from sys import exit # Configure logging logger = logging.getLogger(__name__) -log_fmt: str = logging.BASIC_FORMAT -logging.basicConfig(format=log_fmt, level=logging.INFO) +logging.basicConfig(format=logging.BASIC_FORMAT, level=logging.INFO) +_LIBS = { + ("win32", "AMD64"): Path("__Builds/x64/Release/bin/TACDev.dll"), + ("win32", "ARM64"): Path("__Builds/ARM64/Release/bin/TACDev.dll"), + ("linux", "x86_64"): Path("__Builds/Linux/Release/lib/libTACDev.so"), +} -class _SetupTAC: - """ - Internal class containing methods to configure shared library paths to TAC. - """ - - # The path to the TAC shared library - __tacLibraryPath: Path = None - # The path to the debug python dll - __debugPythonPath: Path = Path("C:/github/AlpacaRepos/__Builds/x64/Debug/bin/PythonDebug.dll") - # Whether the current execution for debugging - __isDebugExecution: bool = False - - def __init__(self) -> None: - self.setupSharedLibraryPath() - logger.debug(f"Configured the TAC library path to be: {self.__tacLibraryPath.as_posix()}") - - if self.__isDebugExecution: - logger.info(f"Process ID: {os.getpid()}") - - def PythonIsDebugging(self) -> bool: - """ - Debug function to update the dll path to a debug dll. - """ - if self.__debugPythonPath.exists(): - try: - __pythonDebugLib = CDLL(self.__debugPythonPath.as_posix()) - __isPythonDebuggingFunc = __pythonDebugLib.IsPythonDebugging - self.__isDebugExecution = __isPythonDebuggingFunc() - return self.__isDebugExecution - - except Exception as error: - logger.error(f"Could not load the 'IsPythonDebugging()' from PythonDebug shared library. {error}") - exit(1) - return False - - def setupSharedLibraryPath(self): - """ - Configures the shared library path for TAC based on OS and QTAC installation - """ - debugLinuxLibraryPath: Path = Path("/local/mnt/workspace/github/AlpacaRepos/__Builds/Linux/Debug/lib/libTACDev.so") - debugWindowsLibraryPath: Path = Path("C:/github/AlpacaRepos/__Builds/x64/Debug/bin/TACDevd.dll") - linuxLibraryPath: Path = Path("/opt/qcom/QTAC/lib/libTACDev.so") - windowsLibraryPath: Path = Path("C:/Program Files (x86)/Qualcomm/QTAC/TACDev.dll") - - pythonIsDebugging = self.PythonIsDebugging() - currentPlatform = platform - - if currentPlatform.startswith("linux") and pythonIsDebugging: - self.__tacLibraryPath = debugLinuxLibraryPath - elif platform.startswith("win32") and pythonIsDebugging: - self.__tacLibraryPath = debugWindowsLibraryPath +def _find_lib() -> Path: + os_key = "win32" if platform.system() == "Windows" else "linux" + machine = platform.machine() - elif currentPlatform.startswith("linux") and not pythonIsDebugging: - self.__tacLibraryPath = linuxLibraryPath + rel = _LIBS.get((os_key, machine)) + if rel is None: + logger.error( + f"Unsupported platform: {platform.system()} {machine}.\n" + " QTAC supports Windows x64, Windows ARM64, and Linux x86_64." + ) + exit(1) - elif currentPlatform.startswith("win32") and not pythonIsDebugging: - self.__tacLibraryPath = windowsLibraryPath + if struct.calcsize("P") != 8: + logger.error( + f"Architecture mismatch: Python must be 64-bit on {platform.system()} {machine}.\n" + " Install the 64-bit Python build from https://www.python.org/downloads/" + ) + exit(1) + candidate = Path.cwd() + for _ in range(8): + lib = candidate / rel + if lib.exists(): + return lib.resolve() + candidate = candidate.parent - def getTACLibraryPath(self) -> str: - """ - Returns the appropriate TAC library path as string. - """ - return self.__tacLibraryPath.as_posix() + logger.error( + f"TACDev library not found.\n" + f" Expected: {Path.cwd().resolve() / rel}\n" + " Build the project first using build.bat / build.sh." + ) + exit(1) # TAC HANDLE @@ -147,32 +91,24 @@ def GetErrorString(errorCode: int) -> str: global __getLoggingFunc global __setLoggingFunc -# the path to the EPMDev shared library -tacLibraryPath = _SetupTAC().getTACLibraryPath() - -if tacLibraryPath is not None: - try: - tacLibrary = CDLL(tacLibraryPath) +try: + tacLibrary = CDLL(_find_lib().as_posix()) - if not tacLibrary: - raise RuntimeError("TACDev not found") - - initializeFunc = tacLibrary.InitializeTACDev - initResult = initializeFunc() - if initResult == TACDEV_INIT_FAILED: - logger.error("TACDev failed to initialize. Please report this issue to support.") - exit(1) + initResult = tacLibrary.InitializeTACDev() + if initResult == TACDEV_INIT_FAILED: + logger.error("TACDev failed to initialize. Please report this issue to support.") + exit(1) - __alpacaVersionFunc = tacLibrary.GetAlpacaVersion - __tacVersionFunc = tacLibrary.GetTACVersion - __getDeviceCountFunc = tacLibrary.GetDeviceCount - __getDeviceFunc = tacLibrary.GetPortData - __getLoggingFunc = tacLibrary.GetLoggingState - __setLoggingFunc = tacLibrary.SetLoggingState + __alpacaVersionFunc = tacLibrary.GetAlpacaVersion + __tacVersionFunc = tacLibrary.GetTACVersion + __getDeviceCountFunc = tacLibrary.GetDeviceCount + __getDeviceFunc = tacLibrary.GetPortData + __getLoggingFunc = tacLibrary.GetLoggingState + __setLoggingFunc = tacLibrary.SetLoggingState - except Exception as error: - logger.error(f"Error with TACDev shared Object. {error}") - exit(1) +except Exception as error: + logger.error(f"Error loading TACDev library. {error}") + exit(1) class TACDevice: diff --git a/interfaces/Python/TACDev/__init__.py b/interfaces/Python/TACDev/__init__.py index 980da43..9242a04 100644 --- a/interfaces/Python/TACDev/__init__.py +++ b/interfaces/Python/TACDev/__init__.py @@ -1,35 +1,4 @@ -# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted (subject to the limitations in the -# disclaimer below) provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# * Neither the name of Qualcomm Technologies, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE -# GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT -# HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED -# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE -# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER -# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN -# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -# Author: Biswajit Roy +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause from .TACDev import * \ No newline at end of file diff --git a/interfaces/Python/setup.bat b/interfaces/Python/setup.bat index e0f0db7..7e1487a 100644 --- a/interfaces/Python/setup.bat +++ b/interfaces/Python/setup.bat @@ -1,3 +1,6 @@ +@REM Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +@REM SPDX-License-Identifier: BSD-3-Clause + DEL /F /Q /S TACDev.egg-info > NUL RMDIR /Q /S TACDev.egg-info diff --git a/interfaces/Python/setup.py b/interfaces/Python/setup.py index a693e5f..ed4f680 100644 --- a/interfaces/Python/setup.py +++ b/interfaces/Python/setup.py @@ -1,35 +1,5 @@ -# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted (subject to the limitations in the -# disclaimer below) provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# * Neither the name of Qualcomm Technologies, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE -# GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT -# HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED -# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE -# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER -# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN -# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# Author: Biswajit Roy +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause from setuptools import find_packages, setup diff --git a/interfaces/Python/setup.sh b/interfaces/Python/setup.sh index ad36ca1..9174943 100644 --- a/interfaces/Python/setup.sh +++ b/interfaces/Python/setup.sh @@ -1,36 +1,7 @@ #!/bin/sh -# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted (subject to the limitations in the -# disclaimer below) provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# * Neither the name of Qualcomm Technologies, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE -# GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT -# HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED -# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE -# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER -# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN -# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause rm -rf TACDev.egg-info rm -rf build diff --git a/src/libraries/qcommon-console/Common.cmake b/src/libraries/qcommon-console/Common.cmake index e900a5e..e9ae615 100644 --- a/src/libraries/qcommon-console/Common.cmake +++ b/src/libraries/qcommon-console/Common.cmake @@ -65,8 +65,11 @@ if(CMAKE_SIZEOF_VOID_P EQUAL 4) list(APPEND QCOMMONCONSOLE_DEFINITIONS __i386__) endif() set(WINTARGET "Win32") +elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64") + message(STATUS "Building ARM64") + set(WINTARGET "ARM64") else() - message(STATUS "Building 64 bit") + message(STATUS "Building x86_64") if(UNIX) list(APPEND QCOMMONCONSOLE_DEFINITIONS __X86_64__) endif() diff --git a/src/libraries/qcommon/Common.cmake b/src/libraries/qcommon/Common.cmake index 3807932..be28224 100644 --- a/src/libraries/qcommon/Common.cmake +++ b/src/libraries/qcommon/Common.cmake @@ -57,8 +57,11 @@ if(CMAKE_SIZEOF_VOID_P EQUAL 4) list(APPEND QCOMMON_DEFINITIONS __i386__) endif() set(WINTARGET "Win32") +elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64") + message(STATUS "Building ARM64") + set(WINTARGET "ARM64") else() - message(STATUS "Building 64 bit") + message(STATUS "Building x86_64") if(UNIX) list(APPEND QCOMMON_DEFINITIONS __X86_64__) endif() diff --git a/third-party/CMakeLists.txt b/third-party/CMakeLists.txt index c3a70e6..d4f36d7 100644 --- a/third-party/CMakeLists.txt +++ b/third-party/CMakeLists.txt @@ -1,44 +1,25 @@ -# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted (subject to the limitations in the -# disclaimer below) provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# * Neither the name of Qualcomm Technologies, Inc. nor the names of its -# contributors may be used to endorse or promote products derived -# from this software without specific prior written permission. -# -# NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE -# GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT -# HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED -# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE -# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER -# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN -# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -# Author: Biswajit Roy (biswroy@qti.qualcomm.com) +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause cmake_minimum_required(VERSION 3.22) if(WIN32) - set(FTDI_URL "https://web.archive.org/web/20250820134143/https://ftdichip.com/wp-content/uploads/2023/09/CDM-v2.12.36.4-WHQL-Certified.zip") - set(FTDI_ARCHIVE "CDM-v2.12.36.4-WHQL-Certified.zip") - set(DEBUG_LIB "${CMAKE_SOURCE_DIR}/__Builds/x64/Debug/lib/ftd2xx.lib") - set(RELEASE_LIB "${CMAKE_SOURCE_DIR}/__Builds/x64/Release/lib/ftd2xx.lib") + if(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64" OR CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64") + set(FTDI_URL "https://web.archive.org/web/20250821211500/https://ftdichip.com/wp-content/uploads/2025/03/CDM-v2.12.36.20-for-ARM64-WHQL-Certified.zip") + set(FTDI_ARCHIVE "CDM-v2.12.36.20-for-ARM64-WHQL-Certified.zip") + set(FTDI_WIN_ARCH "ARM64") + set(FTDI_ZIP_LIB_GLOB "ARM64/Release/FTD2XXstatic.lib") + set(FTDI_ZIP_DLL_GLOB "ARM64/Release/FTD2XX.dll") + else() + set(FTDI_URL "https://web.archive.org/web/20250820134143/https://ftdichip.com/wp-content/uploads/2023/09/CDM-v2.12.36.4-WHQL-Certified.zip") + set(FTDI_ARCHIVE "CDM-v2.12.36.4-WHQL-Certified.zip") + set(FTDI_WIN_ARCH "x64") + set(FTDI_ZIP_LIB_GLOB "amd64/ftd2xx.lib") + set(FTDI_ZIP_DLL_GLOB "amd64/FTD2XX64.dll") + endif() + + set(DEBUG_LIB "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Debug/lib/ftd2xx.lib") + set(RELEASE_LIB "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Release/lib/ftd2xx.lib") else() set(FTDI_URL "https://web.archive.org/web/20250822044524/https://ftdichip.com/wp-content/uploads/2025/03/libftd2xx-linux-x86_64-1.4.33.tgz") set(FTDI_ARCHIVE "libftd2xx-linux-x86_64-1.4.33.tgz") @@ -49,23 +30,30 @@ endif() set(ARCHIVE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/${FTDI_ARCHIVE}") set(TEMP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/temp_extract") -set(NEED_DOWNLOAD FALSE) -if(NOT EXISTS "${DEBUG_LIB}" OR NOT EXISTS "${RELEASE_LIB}") - set(NEED_DOWNLOAD TRUE) +set(DOWNLOAD_SUCCESS FALSE) + +if(EXISTS "${ARCHIVE_PATH}") + execute_process( + COMMAND ${CMAKE_COMMAND} -E tar tf "${ARCHIVE_PATH}" + RESULT_VARIABLE TAR_RESULT OUTPUT_QUIET ERROR_QUIET) + if(TAR_RESULT EQUAL 0) + set(DOWNLOAD_SUCCESS TRUE) + message(STATUS "FTDI: using existing archive at ${ARCHIVE_PATH}") + else() + file(REMOVE "${ARCHIVE_PATH}") + endif() endif() -if(NEED_DOWNLOAD) +if(NOT DOWNLOAD_SUCCESS) set(RETRY_COUNT 0) while(RETRY_COUNT LESS 3) math(EXPR RETRY_COUNT "${RETRY_COUNT} + 1") - if(EXISTS "${ARCHIVE_PATH}") - file(REMOVE "${ARCHIVE_PATH}") - endif() file(DOWNLOAD "${FTDI_URL}" "${ARCHIVE_PATH}" SHOW_PROGRESS STATUS DOWNLOAD_STATUS TIMEOUT 300) list(GET DOWNLOAD_STATUS 0 DOWNLOAD_ERROR_CODE) if(DOWNLOAD_ERROR_CODE EQUAL 0 AND EXISTS "${ARCHIVE_PATH}") execute_process(COMMAND ${CMAKE_COMMAND} -E tar tf "${ARCHIVE_PATH}" RESULT_VARIABLE TAR_RESULT OUTPUT_QUIET ERROR_QUIET) if(TAR_RESULT EQUAL 0) + set(DOWNLOAD_SUCCESS TRUE) break() endif() endif() @@ -73,47 +61,72 @@ if(NEED_DOWNLOAD) execute_process(COMMAND ${CMAKE_COMMAND} -E sleep 5) endif() endwhile() - if(RETRY_COUNT EQUAL 3) - message(FATAL_ERROR "Failed to download FTDI library after 3 attempts") + if(NOT DOWNLOAD_SUCCESS) + message(FATAL_ERROR + "Failed to download the FTDI library archive after ${RETRY_COUNT} attempts.\n" + "Manually download '${FTDI_ARCHIVE}' from:\n" + " ${FTDI_URL}\n" + "Place the downloaded file at:\n" + " ${ARCHIVE_PATH}\n" + "Then re-run cmake.") endif() - - file(REMOVE_RECURSE "${TEMP_DIR}") - file(MAKE_DIRECTORY "${TEMP_DIR}") - execute_process(COMMAND ${CMAKE_COMMAND} -E tar xzf "${ARCHIVE_PATH}" WORKING_DIRECTORY "${TEMP_DIR}") - - if(WIN32) - file(MAKE_DIRECTORY "${CMAKE_SOURCE_DIR}/__Builds/x64/Debug/lib") - file(MAKE_DIRECTORY "${CMAKE_SOURCE_DIR}/__Builds/x64/Release/lib") - file(GLOB SOURCE_LIB "${TEMP_DIR}/amd64/ftd2xx.lib") +endif() - file(GLOB SOURCE_DLL "${TEMP_DIR}/amd64/FTD2XX64.dll") - if(SOURCE_DLL) - file(MAKE_DIRECTORY "${CMAKE_SOURCE_DIR}/__Builds/x64/Debug/bin") - file(MAKE_DIRECTORY "${CMAKE_SOURCE_DIR}/__Builds/x64/Release/bin") - file(COPY "${SOURCE_DLL}" DESTINATION "${CMAKE_SOURCE_DIR}/__Builds/x64/Debug/bin") - file(RENAME "${CMAKE_SOURCE_DIR}/__Builds/x64/Debug/bin/FTD2XX64.dll" "${CMAKE_SOURCE_DIR}/__Builds/x64/Debug/bin/ftd2xx.dll") - file(COPY "${SOURCE_DLL}" DESTINATION "${CMAKE_SOURCE_DIR}/__Builds/x64/Release/bin") - file(RENAME "${CMAKE_SOURCE_DIR}/__Builds/x64/Release/bin/FTD2XX64.dll" "${CMAKE_SOURCE_DIR}/__Builds/x64/Release/bin/ftd2xx.dll") - endif() +file(REMOVE_RECURSE "${TEMP_DIR}") +file(MAKE_DIRECTORY "${TEMP_DIR}") +execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf "${ARCHIVE_PATH}" WORKING_DIRECTORY "${TEMP_DIR}") - if(NOT SOURCE_LIB) - file(GLOB SOURCE_LIB "${TEMP_DIR}/Static/amd64/ftd2xx.lib") +if(WIN32) + file(MAKE_DIRECTORY "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Debug/lib") + file(MAKE_DIRECTORY "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Release/lib") + file(GLOB SOURCE_LIB "${TEMP_DIR}/${FTDI_ZIP_LIB_GLOB}") + + file(GLOB SOURCE_DLL "${TEMP_DIR}/${FTDI_ZIP_DLL_GLOB}") + if(SOURCE_DLL) + file(MAKE_DIRECTORY "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Debug/bin") + file(MAKE_DIRECTORY "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Release/bin") + file(COPY "${SOURCE_DLL}" DESTINATION "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Debug/bin") + get_filename_component(SOURCE_DLL_NAME "${SOURCE_DLL}" NAME) + if(NOT SOURCE_DLL_NAME STREQUAL "ftd2xx.dll") + file(RENAME "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Debug/bin/${SOURCE_DLL_NAME}" "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Debug/bin/ftd2xx.dll") endif() - file(COPY "${SOURCE_LIB}" DESTINATION "${CMAKE_SOURCE_DIR}/__Builds/x64/Debug/lib") - file(COPY "${SOURCE_LIB}" DESTINATION "${CMAKE_SOURCE_DIR}/__Builds/x64/Release/lib") - else() - file(MAKE_DIRECTORY "${CMAKE_SOURCE_DIR}/__Builds/Linux/Debug/lib") - file(MAKE_DIRECTORY "${CMAKE_SOURCE_DIR}/__Builds/Linux/Release/lib") - file(GLOB SOURCE_LIB "${TEMP_DIR}/*/libftd2xx-static.a") - file(COPY "${SOURCE_LIB}" DESTINATION "${CMAKE_SOURCE_DIR}/__Builds/Linux/Debug/lib") - file(COPY "${SOURCE_LIB}" DESTINATION "${CMAKE_SOURCE_DIR}/__Builds/Linux/Release/lib") - file(RENAME "${CMAKE_SOURCE_DIR}/__Builds/Linux/Debug/lib/libftd2xx-static.a" "${DEBUG_LIB}") - file(RENAME "${CMAKE_SOURCE_DIR}/__Builds/Linux/Release/lib/libftd2xx-static.a" "${RELEASE_LIB}") + file(COPY "${SOURCE_DLL}" DESTINATION "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Release/bin") + if(NOT SOURCE_DLL_NAME STREQUAL "ftd2xx.dll") + file(RENAME "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Release/bin/${SOURCE_DLL_NAME}" "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Release/bin/ftd2xx.dll") + endif() + endif() + + if(NOT SOURCE_LIB AND FTDI_WIN_ARCH STREQUAL "x64") + file(GLOB SOURCE_LIB "${TEMP_DIR}/Static/amd64/ftd2xx.lib") + endif() + if(NOT SOURCE_LIB) + message(FATAL_ERROR + "FTDI ${FTDI_WIN_ARCH} lib not found in the downloaded CDM package.\n" + "Manually download '${FTDI_ARCHIVE}' from:\n" + " ${FTDI_URL}\n" + "Place the downloaded file at:\n" + " ${ARCHIVE_PATH}\n" + "Then re-run cmake.") endif() - - file(REMOVE_RECURSE "${TEMP_DIR}") + file(COPY "${SOURCE_LIB}" DESTINATION "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Debug/lib") + file(COPY "${SOURCE_LIB}" DESTINATION "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Release/lib") + get_filename_component(SOURCE_LIB_NAME "${SOURCE_LIB}" NAME) + if(NOT SOURCE_LIB_NAME STREQUAL "ftd2xx.lib") + file(RENAME "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Debug/lib/${SOURCE_LIB_NAME}" "${DEBUG_LIB}") + file(RENAME "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Release/lib/${SOURCE_LIB_NAME}" "${RELEASE_LIB}") + endif() +else() + file(MAKE_DIRECTORY "${CMAKE_SOURCE_DIR}/__Builds/Linux/Debug/lib") + file(MAKE_DIRECTORY "${CMAKE_SOURCE_DIR}/__Builds/Linux/Release/lib") + file(GLOB SOURCE_LIB "${TEMP_DIR}/*/libftd2xx-static.a") + file(COPY "${SOURCE_LIB}" DESTINATION "${CMAKE_SOURCE_DIR}/__Builds/Linux/Debug/lib") + file(COPY "${SOURCE_LIB}" DESTINATION "${CMAKE_SOURCE_DIR}/__Builds/Linux/Release/lib") + file(RENAME "${CMAKE_SOURCE_DIR}/__Builds/Linux/Debug/lib/libftd2xx-static.a" "${DEBUG_LIB}") + file(RENAME "${CMAKE_SOURCE_DIR}/__Builds/Linux/Release/lib/libftd2xx-static.a" "${RELEASE_LIB}") endif() +file(REMOVE_RECURSE "${TEMP_DIR}") + if(WIN32) add_library(ftd2xx_debug STATIC IMPORTED GLOBAL) add_library(ftd2xx_release STATIC IMPORTED GLOBAL) @@ -128,36 +141,6 @@ else() ) endif() -if(WIN32 AND DEFINED ENV{QTDIR}) - set(QT_BIN_DIR "$ENV{QTDIR}/bin") - set(QT_PLUGINS_DIR "$ENV{QTDIR}/plugins") - set(DEBUG_BIN_DIR "${CMAKE_SOURCE_DIR}/__Builds/x64/Debug/bin") - set(RELEASE_BIN_DIR "${CMAKE_SOURCE_DIR}/__Builds/x64/Release/bin") - - set(QT_DLLS Qt6Core Qt6Gui Qt6Widgets Qt6SerialPort Qt6Concurrent Qt6Network Qt6Xml Qt6Multimedia Qt6MultimediaWidgets) - - file(MAKE_DIRECTORY "${DEBUG_BIN_DIR}") - file(MAKE_DIRECTORY "${RELEASE_BIN_DIR}") - file(MAKE_DIRECTORY "${DEBUG_BIN_DIR}/platforms") - file(MAKE_DIRECTORY "${RELEASE_BIN_DIR}/platforms") - - foreach(QT_DLL ${QT_DLLS}) - if(EXISTS "${QT_BIN_DIR}/${QT_DLL}d.dll") - file(COPY "${QT_BIN_DIR}/${QT_DLL}d.dll" DESTINATION "${DEBUG_BIN_DIR}") - endif() - if(EXISTS "${QT_BIN_DIR}/${QT_DLL}.dll") - file(COPY "${QT_BIN_DIR}/${QT_DLL}.dll" DESTINATION "${RELEASE_BIN_DIR}") - endif() - endforeach() - - if(EXISTS "${QT_PLUGINS_DIR}/platforms/qwindowsd.dll") - file(COPY "${QT_PLUGINS_DIR}/platforms/qwindowsd.dll" DESTINATION "${DEBUG_BIN_DIR}/platforms") - endif() - if(EXISTS "${QT_PLUGINS_DIR}/platforms/qwindows.dll") - file(COPY "${QT_PLUGINS_DIR}/platforms/qwindows.dll" DESTINATION "${RELEASE_BIN_DIR}/platforms") - endif() -endif() - function(link_ftd2xx target_name) if(WIN32) target_link_libraries(${target_name} PRIVATE $<$:ftd2xx_debug> $<$:ftd2xx_release>)