From beb2af75684658f439e0a45102deec99695c2faf Mon Sep 17 00:00:00 2001 From: "Roy, Biswajit" Date: Wed, 24 Jun 2026 22:53:46 +0530 Subject: [PATCH 01/17] arm64 support and initial mcp example Signed-off-by: Roy, Biswajit --- .github/workflows/build.yml | 88 +++ .gitignore | 4 + CMakeLists.txt | 35 +- README.md | 25 +- build.bat | 165 ++++-- build.sh | 33 +- .../01-Build-Using-Qt-Creator.md | 38 +- examples/MCP/README.md | 93 ++++ examples/MCP/requirements.txt | 2 + examples/MCP/setup.bat | 29 + examples/MCP/setup.sh | 25 + examples/MCP/tacdev_mcp_client.py | 433 +++++++++++++++ examples/MCP/tacdev_mcp_server.py | 509 ++++++++++++++++++ interfaces/C++/TACDev/CMakeLists.txt | 35 +- interfaces/C++/TACDev/TACDev.cpp | 40 +- interfaces/C++/TACDev/TACDev.h | 40 +- interfaces/C++/TACDev/TACDevCore.cpp | 39 +- interfaces/C++/TACDev/TACDevCore.h | 40 +- interfaces/CMakeLists.txt | 35 +- interfaces/Python/TACDev/TACDev.py | 172 ++---- interfaces/Python/TACDev/__init__.py | 35 +- interfaces/Python/setup.bat | 3 + interfaces/Python/setup.py | 34 +- interfaces/Python/setup.sh | 33 +- third-party/CMakeLists.txt | 81 ++- 25 files changed, 1506 insertions(+), 560 deletions(-) create mode 100644 examples/MCP/README.md create mode 100644 examples/MCP/requirements.txt create mode 100644 examples/MCP/setup.bat create mode 100644 examples/MCP/setup.sh create mode 100644 examples/MCP/tacdev_mcp_client.py create mode 100644 examples/MCP/tacdev_mcp_server.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d4a4c31..6007e7a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,6 +37,24 @@ 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-2022 + 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 + enableCrossOsArchive: true + + - 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 }} @@ -158,3 +176,73 @@ jobs: - name: Build with ${{ matrix.build_type }} shell: pwsh run: cmake --build build --config ${{ matrix.build_type }} + + 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'] + build_type: [Debug, Release] + + steps: + - uses: actions/checkout@v6 + + - name: Install Python dependencies + shell: pwsh + run: pip install aqtinstall + + - 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: | + aqt install-qt windows_arm64 desktop ${{ matrix.qt_version }} win64_msvc2022_arm64 --outputdir "C:\Qt" --autodesktop -m qtserialport qtmultimedia + + $qtRoot = "C:\Qt\${{ matrix.qt_version }}\msvc2022_arm64" + 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 + + - 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 + enableCrossOsArchive: true + + - name: Extract FTDI library for ARM64 + shell: pwsh + run: | + $debugLib = "__Builds\ARM64\Debug\lib" + $releaseLib = "__Builds\ARM64\Release\lib" + $debugBin = "__Builds\ARM64\Debug\bin" + $releaseBin = "__Builds\ARM64\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.20-for-ARM64-WHQL-Certified.zip" -DestinationPath $tmp -Force + + $lib = Get-ChildItem -Path $tmp -Filter "FTD2XXstatic.lib" -Recurse | Select-Object -First 1 + if (-not $lib) { $lib = Get-ChildItem -Path $tmp -Filter "*.lib" -Recurse | Select-Object -First 1 } + Copy-Item $lib.FullName -Destination "$debugLib\ftd2xx.lib" + Copy-Item $lib.FullName -Destination "$releaseLib\ftd2xx.lib" + + $dll = Get-ChildItem -Path $tmp -Filter "FTD2XX.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 }} + shell: pwsh + run: cmake -B build -A ARM64 -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCMAKE_PREFIX_PATH="$env:QT_ROOT_DIR\lib\cmake" + + - name: Build with ${{ matrix.build_type }} + shell: pwsh + run: cmake --build build --config ${{ matrix.build_type }} diff --git a/.gitignore b/.gitignore index f8e3c62..cab114c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,10 @@ ui_* **/*/dist __pycache__/ .pytest_cache/ +*.egg-info/ +*.pyc +*.pyo +*.pyd # 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..953f7e2 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,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 | URL | +> | :-- | :-- | :-- | +> | **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,25 +91,35 @@ 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: ```cmd -build.bat +build.bat (x64, default) +build.bat ARM64 (ARM64) ``` **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 diff --git a/build.bat b/build.bat index a713d84..5d46cc3 100644 --- a/build.bat +++ b/build.bat @@ -1,48 +1,135 @@ -@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 target architecture +@REM --------------------------------------------------------------------------- +set ARCH=%1 +if "%ARCH%"=="" ( + if "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( + set ARCH=ARM64 + ) else ( + set ARCH=x64 + ) +) + +if /i "%ARCH%"=="x64" ( + set CMAKE_SYSTEM_PROCESSOR=AMD64 + set EXPECTED_QT_PATH=msvc2022_64 + set VCVARS_SCRIPT=vcvars64.bat + set VS_COMPONENT=Desktop development with C++ +) else if /i "%ARCH%"=="ARM64" ( + set CMAKE_SYSTEM_PROCESSOR=ARM64 + set EXPECTED_QT_PATH=msvc2022_arm64 + if "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( + set VCVARS_SCRIPT=vcvarsarm64.bat + ) else if "%PROCESSOR_ARCHITECTURE%"=="AMD64" ( + set VCVARS_SCRIPT=vcvarsamd64_arm64.bat + ) else ( + echo ERROR: Cannot build for ARM64 - unrecognised host architecture '%PROCESSOR_ARCHITECTURE%'. + echo ARM64 builds require an x64 host ^(cross-compile^) or an ARM64 host ^(native compile^). + exit /b 1 + ) + set VS_COMPONENT=MSVC v143 - VS 2022 C++ ARM64 build tools +) else ( + echo ERROR: Unsupported architecture '%ARCH%'. + echo Usage: + echo build.bat - auto-detect from host machine ^(current: %PROCESSOR_ARCHITECTURE%^) + echo build.bat x64 - build for x64 + echo build.bat ARM64 - build for ARM64 + exit /b 1 +) + +echo Target architecture : %ARCH% +echo Host architecture : %PROCESSOR_ARCHITECTURE% + +@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\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\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 @@ -52,6 +139,7 @@ cmake -S . -B build\Debug -DCMAKE_PREFIX_PATH="%QTBIN%\.." ^ -DCMAKE_COLOR_DIAGNOSTICS=ON ^ -DCMAKE_GENERATOR=Ninja ^ -DCMAKE_BUILD_TYPE=Debug ^ + -DCMAKE_SYSTEM_PROCESSOR=%CMAKE_SYSTEM_PROCESSOR% ^ -DCMAKE_CXX_FLAGS_INIT=-DQT_QML_DEBUG cmake --build build\Debug @@ -59,7 +147,8 @@ cmake --build build\Debug cmake -S . -B build\Release -DCMAKE_PREFIX_PATH="%QTBIN%\.." ^ -DCMAKE_COLOR_DIAGNOSTICS=ON ^ -DCMAKE_GENERATOR=Ninja ^ - -DCMAKE_BUILD_TYPE=Release + -DCMAKE_BUILD_TYPE=Release ^ + -DCMAKE_SYSTEM_PROCESSOR=%CMAKE_SYSTEM_PROCESSOR% cmake --build build\Release diff --git a/build.sh b/build.sh index 1009d7c..e698d45 100644 --- a/build.sh +++ b/build.sh @@ -1,36 +1,7 @@ #!/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) diff --git a/docs/getting-started/01-Build-Using-Qt-Creator.md b/docs/getting-started/01-Build-Using-Qt-Creator.md index 77d04dc..7e1cb20 100644 --- a/docs/getting-started/01-Build-Using-Qt-Creator.md +++ b/docs/getting-started/01-Build-Using-Qt-Creator.md @@ -6,7 +6,16 @@ installed Qt, you may execute the **Qt Maintainence Tool** to download the addit 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) @@ -29,20 +38,43 @@ 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 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 | URL | +| :-- | :-- | :-- | +| **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/README.md b/examples/MCP/README.md new file mode 100644 index 0000000..08a0231 --- /dev/null +++ b/examples/MCP/README.md @@ -0,0 +1,93 @@ +# QTAC MCP Server + +An [MCP](https://modelcontextprotocol.io) server that exposes the full TACDev API as tools, +allowing AI assistants to control Qualcomm devices via a QTAC debug board. + +## Prerequisites + +- Python 3.8+ (64-bit, matching your build architecture: x64 or ARM64) +- Project built with `build.bat` / `build.sh` (the TACDev library is loaded from `__Builds`) +- MCP-capable host + +## Setup + +Run from the `examples/MCP` directory: + +```cmd +setup.bat (Windows x64, auto-detected) +setup.bat ARM64 (Windows ARM64) +``` +```bash +./setup.sh (Linux) +``` + +Each script builds the project, installs the TACDev Python library, and installs MCP dependencies. + +For full setup guidance see [Bootcamp guide](../../docs/bootcamp/01-Bootcamp.md) and +[Python API reference](../../docs/bootcamp/02-Python-API.md). + +## Usage + +`tacdev_mcp_server.py`: starts the MCP server over stdio. Run from the repo root: + +```bash +python examples/MCP/tacdev_mcp_server.py +``` + +`tacdev_mcp_client.py`: launches the server as a subprocess and runs a device demo. +Pass an optional port name to open a specific device: + +```bash +python examples/MCP/tacdev_mcp_client.py # lists devices, opens first found +python examples/MCP/tacdev_mcp_client.py TAC-Lite # opens device by port name +``` + +## Available tools + +| Category | Tools | +| :-- | :-- | +| Diagnostics | `get_alpaca_version`, `get_tac_version` | +| Logging | `get_logging_state`, `set_logging_state` | +| Device enumeration | `get_device_count`, `get_port_data` | +| Handle management | `open_handle_by_description`, `close_tac_handle` | +| Device info | `get_name`, `get_firmware_version`, `get_hardware`, `get_hardware_version`, `get_uuid` | +| External power | `set_external_power_control` | +| Dynamic commands | `list_commands`, `get_command` | +| Quick commands | `list_quick_commands` | +| Script variables | `list_script_variables`, `update_script_variable` | +| Command interface | `get_command_state`, `send_command` | +| Help | `get_help_text` | +| Raw pin | `set_pin_state` | +| Command queue | `is_command_queue_clear` | +| Battery | `set_battery_state`, `get_battery_state` | +| USB | `set_usb0`, `get_usb0_state`, `set_usb1`, `get_usb1_state` | +| Buttons | `set_power_key`, `get_power_key_state`, `set_volume_up`, `get_volume_up_state`, `set_volume_down`, `get_volume_down_state` | +| SIM / SD | `set_disconnect_uim1`, `get_disconnect_uim1_state`, `set_disconnect_uim2`, `get_disconnect_uim2_state`, `set_disconnect_sd_card`, `get_disconnect_sd_card_state` | +| EDL | `set_primary_edl`, `get_primary_edl_state`, `set_secondary_edl`, `get_secondary_edl_state` | +| PS_HOLD / RESIN | `set_force_ps_hold_high`, `get_force_ps_hold_high_state`, `set_secondary_pm_resin_n`, `get_secondary_pm_resin_n_state` | +| EUD | `set_eud`, `get_eud_state` | +| Headset | `set_headset_disconnect`, `get_headset_disconnect_state` | +| Device name / resets | `set_name`, `get_reset_count`, `clear_reset_count` | +| Button sequences | `power_on_button`, `power_off_button`, `boot_to_fastboot_button`, `boot_to_uefi_menu_button`, `boot_to_edl_button`, `boot_to_secondary_edl_button` | + +## Troubleshooting + +**`ModuleNotFoundError: No module named 'TACDev'`** +Run `pip install interfaces/Python` from the repo root, or use `setup.bat` / `setup.sh`. + +**`TACDev library not found`** +Build the project first with `build.bat` / `build.sh` and run scripts from the repo root. + +**`Architecture mismatch`** +Use a 64-bit Python build matching your target architecture (x64 or ARM64). + +**`get_device_count` returns 0** +Check the debug board is connected. On Windows, look for Unknown Device in Device Manager and +run `FTDICheck.exe` from `__Builds\x64\Release\bin` to diagnose the FTDI connection. +On Linux, ensure udev rules are installed: +```bash +sudo cp udev-rules/99-QTAC-USB.rules /etc/udev/rules.d/ +sudo udevadm control --reload +``` + +See [Bootcamp troubleshooting](../../docs/bootcamp/01-Bootcamp.md#troubleshooting) for more. diff --git a/examples/MCP/requirements.txt b/examples/MCP/requirements.txt new file mode 100644 index 0000000..dac0892 --- /dev/null +++ b/examples/MCP/requirements.txt @@ -0,0 +1,2 @@ +fastmcp>=2.0.0 +mcp>=1.0.0 diff --git a/examples/MCP/setup.bat b/examples/MCP/setup.bat new file mode 100644 index 0000000..a18feec --- /dev/null +++ b/examples/MCP/setup.bat @@ -0,0 +1,29 @@ +@REM Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +@REM SPDX-License-Identifier: BSD-3-Clause + +@echo off + +@REM Resolve repo root (two levels up from examples\MCP) +set SCRIPT_DIR=%~dp0 +pushd "%SCRIPT_DIR%\..\.." +set REPO_ROOT=%CD% +popd + +@REM Step 1: build the project +echo [1/3] Building QTAC... +call "%REPO_ROOT%\build.bat" %1 +if errorlevel 1 exit /b 1 + +@REM Step 2: install TACDev Python library +echo [2/3] Installing TACDev Python library... +pip install "%REPO_ROOT%\interfaces\Python" +if errorlevel 1 exit /b 1 + +@REM Step 3: install MCP dependencies +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 MCP server from the repo root: +echo python examples\MCP\tacdev_mcp_server.py diff --git a/examples/MCP/setup.sh b/examples/MCP/setup.sh new file mode 100644 index 0000000..1019557 --- /dev/null +++ b/examples/MCP/setup.sh @@ -0,0 +1,25 @@ +#!/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)" + +# Step 1: build the project +echo "[1/3] Building QTAC..." +"$REPO_ROOT/build.sh" + +# Step 2: install TACDev Python library +echo "[2/3] Installing TACDev Python library..." +pip install "$REPO_ROOT/interfaces/Python" + +# Step 3: install MCP dependencies +echo "[3/3] Installing MCP dependencies..." +pip install -r "$SCRIPT_DIR/requirements.txt" + +echo "" +echo "Setup complete. Run the MCP server from the repo root:" +echo " python examples/MCP/tacdev_mcp_server.py" diff --git a/examples/MCP/tacdev_mcp_client.py b/examples/MCP/tacdev_mcp_client.py new file mode 100644 index 0000000..90d4864 --- /dev/null +++ b/examples/MCP/tacdev_mcp_client.py @@ -0,0 +1,433 @@ +#!/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. + +Launches the server as a subprocess and exposes a clean +TACDevClient class whose methods map 1-to-1 to every MCP tool. + +Typical usage +------------- + from tacdev_mcp_client import TACDevClient + + with TACDevClient() as tac: + count = tac.get_device_count() + handle = tac.open_handle_by_description("TAC-Lite") + tac.power_on_button(handle) + tac.close_tac_handle(handle) + +Run as a script for a quick interactive demo: + python tacdev_mcp_client.py [port_name] +""" + +from __future__ import annotations + +import asyncio +import os +import sys +from contextlib import asynccontextmanager +from pathlib import Path +from typing import Any + +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client + +# --------------------------------------------------------------------------- # +# Server launch parameters +# --------------------------------------------------------------------------- # +_SERVER_SCRIPT = str(Path(__file__).with_name("tacdev_mcp_server.py")) + +_SERVER_PARAMS = StdioServerParameters( + command=sys.executable, + args=[_SERVER_SCRIPT], + env={ + **os.environ, + # Override library path here if needed, or set TACDEV_LIB_PATH in env. + # "TACDEV_LIB_PATH": r"C:\path\to\TACDev.dll", + }, +) + + +# --------------------------------------------------------------------------- # +# Low-level async helper +# --------------------------------------------------------------------------- # +@asynccontextmanager +async def _session(): + """Async context manager that yields a live MCP ClientSession.""" + async with stdio_client(_SERVER_PARAMS) as (read, write): + async with ClientSession(read, write) as session: + await session.initialize() + yield session + + +async def _call(session: ClientSession, tool: str, **kwargs: Any) -> Any: + """Call one MCP tool and return its unwrapped result value.""" + result = await session.call_tool(tool, arguments=kwargs) + # FastMCP returns a list of TextContent; pull out the parsed dict. + if result.content: + import json + text = result.content[0].text + try: + return json.loads(text) + except (ValueError, TypeError): + return text + return None + + +# --------------------------------------------------------------------------- # +# Synchronous TACDevClient +# --------------------------------------------------------------------------- # +class TACDevClient: + """ + Synchronous wrapper around the TACDev MCP server. + + Can be used as a context manager: + with TACDevClient() as tac: + ... + + Or manually: + tac = TACDevClient() + tac.connect() + tac.disconnect() + """ + + def __init__(self) -> None: + self._loop: asyncio.AbstractEventLoop | None = None + self._session: ClientSession | None = None + self._ctx = None + + # ── Lifecycle ──────────────────────────────────────────────────────────── + + def connect(self) -> "TACDevClient": + self._loop = asyncio.new_event_loop() + self._ctx = _session() + self._session = self._loop.run_until_complete(self._ctx.__aenter__()) + return self + + def disconnect(self) -> None: + if self._ctx and self._loop: + self._loop.run_until_complete(self._ctx.__aexit__(None, None, None)) + if self._loop: + self._loop.close() + self._session = None + self._loop = None + self._ctx = None + + def __enter__(self) -> "TACDevClient": + return self.connect() + + def __exit__(self, *_) -> None: + self.disconnect() + + # ── Internal dispatch ──────────────────────────────────────────────────── + + def _call(self, tool: str, **kwargs: Any) -> Any: + if self._session is None or self._loop is None: + raise RuntimeError("Not connected — call connect() or use as a context manager") + return self._loop.run_until_complete(_call(self._session, tool, **kwargs)) + + # ── Version / diagnostics ──────────────────────────────────────────────── + + def get_alpaca_version(self) -> str: + """Return the Alpaca firmware version string.""" + return self._call("get_alpaca_version")["alpaca_version"] + + def get_tac_version(self) -> str: + """Return the TACDev library version string.""" + return self._call("get_tac_version")["tac_version"] + + def get_last_tac_error(self) -> str: + """Return the last error message recorded by TACDev.""" + return self._call("get_last_tac_error")["last_error"] + + # ── Logging ────────────────────────────────────────────────────────────── + + def get_logging_state(self) -> bool: + """Return whether TACDev logging is currently enabled.""" + return self._call("get_logging_state")["logging_enabled"] + + def set_logging_state(self, enabled: bool) -> None: + """Enable or disable TACDev logging.""" + self._call("set_logging_state", enabled=enabled) + + # ── Device enumeration ─────────────────────────────────────────────────── + + def get_device_count(self) -> int: + """Return the number of connected TAC devices.""" + return self._call("get_device_count")["device_count"] + + def get_port_data(self, device_index: int) -> str: + """Return the port description string for the device at device_index.""" + return self._call("get_port_data", device_index=device_index)["port_data"] + + def list_ports(self) -> list[str]: + """Convenience: return port descriptions for all connected devices.""" + count = self.get_device_count() + return [self.get_port_data(i) for i in range(count)] + + # ── Handle management ──────────────────────────────────────────────────── + + def open_handle_by_description(self, port_name: str) -> int: + """Open a TAC device by port description and return its integer handle.""" + return self._call("open_handle_by_description", port_name=port_name)["handle"] + + def close_tac_handle(self, handle: int) -> None: + """Close a previously opened TAC device handle.""" + self._call("close_tac_handle", handle=handle) + + # ── Device info ────────────────────────────────────────────────────────── + + def get_name(self, handle: int) -> str: + return self._call("get_name", handle=handle)["name"] + + def get_firmware_version(self, handle: int) -> str: + return self._call("get_firmware_version", handle=handle)["firmware_version"] + + def get_hardware(self, handle: int) -> str: + return self._call("get_hardware", handle=handle)["hardware"] + + def get_hardware_version(self, handle: int) -> str: + return self._call("get_hardware_version", handle=handle)["hardware_version"] + + def get_uuid(self, handle: int) -> str: + return self._call("get_uuid", handle=handle)["uuid"] + + def get_device_info(self, handle: int) -> dict: + """Convenience: return all device identity fields in one call.""" + return { + "name": self.get_name(handle), + "firmware_version": self.get_firmware_version(handle), + "hardware": self.get_hardware(handle), + "hardware_version": self.get_hardware_version(handle), + "uuid": self.get_uuid(handle), + } + + # ── External power ─────────────────────────────────────────────────────── + + def set_external_power_control(self, handle: int, state: bool) -> None: + self._call("set_external_power_control", handle=handle, state=state) + + # ── Dynamic commands ───────────────────────────────────────────────────── + + def list_commands(self, handle: int) -> list[str]: + return self._call("list_commands", handle=handle)["commands"] + + def get_command(self, handle: int, command_index: int) -> str: + return self._call("get_command", handle=handle, command_index=command_index)["command"] + + def list_quick_commands(self, handle: int) -> list[str]: + return self._call("list_quick_commands", handle=handle)["quick_commands"] + + # ── Script variables ───────────────────────────────────────────────────── + + def list_script_variables(self, handle: int) -> list[str]: + return self._call("list_script_variables", handle=handle)["script_variables"] + + def update_script_variable(self, handle: int, variable: str, value: str) -> None: + self._call("update_script_variable", handle=handle, variable=variable, value=value) + + # ── Core command interface ──────────────────────────────────────────────── + + def get_command_state(self, handle: int, command: str) -> bool: + return self._call("get_command_state", handle=handle, command=command)["state"] + + def send_command(self, handle: int, command: str, state: bool) -> None: + self._call("send_command", handle=handle, command=command, state=state) + + # ── Help / queue ───────────────────────────────────────────────────────── + + def get_help_text(self, handle: int) -> str: + return self._call("get_help_text", handle=handle)["help_text"] + + def is_command_queue_clear(self, handle: int) -> bool: + return self._call("is_command_queue_clear", handle=handle)["queue_clear"] + + # ── Raw pin control ─────────────────────────────────────────────────────── + + def set_pin_state(self, handle: int, pin: int, state: bool) -> None: + self._call("set_pin_state", handle=handle, pin=pin, state=state) + + # ── Battery ─────────────────────────────────────────────────────────────── + + def set_battery_state(self, handle: int, state: bool) -> None: + self._call("set_battery_state", handle=handle, state=state) + + def get_battery_state(self, handle: int) -> bool: + return self._call("get_battery_state", handle=handle)["state"] + + # ── USB ─────────────────────────────────────────────────────────────────── + + def set_usb0(self, handle: int, state: bool) -> None: + self._call("set_usb0", handle=handle, state=state) + + def get_usb0_state(self, handle: int) -> bool: + return self._call("get_usb0_state", handle=handle)["state"] + + def set_usb1(self, handle: int, state: bool) -> None: + self._call("set_usb1", handle=handle, state=state) + + def get_usb1_state(self, handle: int) -> bool: + return self._call("get_usb1_state", handle=handle)["state"] + + # ── Power key ───────────────────────────────────────────────────────────── + + def set_power_key(self, handle: int, state: bool) -> None: + self._call("set_power_key", handle=handle, state=state) + + def get_power_key_state(self, handle: int) -> bool: + return self._call("get_power_key_state", handle=handle)["state"] + + # ── Volume ──────────────────────────────────────────────────────────────── + + def set_volume_up(self, handle: int, state: bool) -> None: + self._call("set_volume_up", handle=handle, state=state) + + def get_volume_up_state(self, handle: int) -> bool: + return self._call("get_volume_up_state", handle=handle)["state"] + + def set_volume_down(self, handle: int, state: bool) -> None: + self._call("set_volume_down", handle=handle, state=state) + + def get_volume_down_state(self, handle: int) -> bool: + return self._call("get_volume_down_state", handle=handle)["state"] + + # ── SIM / SD ────────────────────────────────────────────────────────────── + + def set_disconnect_uim1(self, handle: int, state: bool) -> None: + self._call("set_disconnect_uim1", handle=handle, state=state) + + def get_disconnect_uim1_state(self, handle: int) -> bool: + return self._call("get_disconnect_uim1_state", handle=handle)["state"] + + def set_disconnect_uim2(self, handle: int, state: bool) -> None: + self._call("set_disconnect_uim2", handle=handle, state=state) + + def get_disconnect_uim2_state(self, handle: int) -> bool: + return self._call("get_disconnect_uim2_state", handle=handle)["state"] + + def set_disconnect_sd_card(self, handle: int, state: bool) -> None: + self._call("set_disconnect_sd_card", handle=handle, state=state) + + def get_disconnect_sd_card_state(self, handle: int) -> bool: + return self._call("get_disconnect_sd_card_state", handle=handle)["state"] + + # ── EDL ─────────────────────────────────────────────────────────────────── + + def set_primary_edl(self, handle: int, state: bool) -> None: + self._call("set_primary_edl", handle=handle, state=state) + + def get_primary_edl_state(self, handle: int) -> bool: + return self._call("get_primary_edl_state", handle=handle)["state"] + + def set_secondary_edl(self, handle: int, state: bool) -> None: + self._call("set_secondary_edl", handle=handle, state=state) + + def get_secondary_edl_state(self, handle: int) -> bool: + return self._call("get_secondary_edl_state", handle=handle)["state"] + + # ── PS_HOLD / RESIN_N ──────────────────────────────────────────────────── + + def set_force_ps_hold_high(self, handle: int, state: bool) -> None: + self._call("set_force_ps_hold_high", handle=handle, state=state) + + def get_force_ps_hold_high_state(self, handle: int) -> bool: + return self._call("get_force_ps_hold_high_state", handle=handle)["state"] + + def set_secondary_pm_resin_n(self, handle: int, state: bool) -> None: + self._call("set_secondary_pm_resin_n", handle=handle, state=state) + + def get_secondary_pm_resin_n_state(self, handle: int) -> bool: + return self._call("get_secondary_pm_resin_n_state", handle=handle)["state"] + + # ── EUD ─────────────────────────────────────────────────────────────────── + + def set_eud(self, handle: int, state: bool) -> None: + self._call("set_eud", handle=handle, state=state) + + def get_eud_state(self, handle: int) -> bool: + return self._call("get_eud_state", handle=handle)["state"] + + # ── Headset ─────────────────────────────────────────────────────────────── + + def set_headset_disconnect(self, handle: int, state: bool) -> None: + self._call("set_headset_disconnect", handle=handle, state=state) + + def get_headset_disconnect_state(self, handle: int) -> bool: + return self._call("get_headset_disconnect_state", handle=handle)["state"] + + # ── Name / reset count ─────────────────────────────────────────────────── + + def set_name(self, handle: int, new_name: str) -> None: + self._call("set_name", handle=handle, new_name=new_name) + + def get_reset_count(self, handle: int) -> int: + return self._call("get_reset_count", handle=handle)["reset_count"] + + def clear_reset_count(self, handle: int) -> None: + self._call("clear_reset_count", handle=handle) + + # ── Button sequences ───────────────────────────────────────────────────── + + def power_on_button(self, handle: int) -> None: + """Execute the power-on button sequence.""" + self._call("power_on_button", handle=handle) + + def power_off_button(self, handle: int) -> None: + """Execute the power-off button sequence.""" + self._call("power_off_button", handle=handle) + + def boot_to_fastboot_button(self, handle: int) -> None: + """Boot the device into Fastboot mode.""" + self._call("boot_to_fastboot_button", handle=handle) + + def boot_to_uefi_menu_button(self, handle: int) -> None: + """Boot the device into the UEFI menu.""" + self._call("boot_to_uefi_menu_button", handle=handle) + + def boot_to_edl_button(self, handle: int) -> None: + """Boot the device into primary EDL mode.""" + self._call("boot_to_edl_button", handle=handle) + + def boot_to_secondary_edl_button(self, handle: int) -> None: + """Boot the device into secondary EDL mode.""" + self._call("boot_to_secondary_edl_button", handle=handle) + + +# --------------------------------------------------------------------------- # +# Quick demo / smoke-test (run as script) +# --------------------------------------------------------------------------- # +def _demo(port_name: str | None = None) -> None: + with TACDevClient() as tac: + print(f" Alpaca version : {tac.get_alpaca_version()}") + print(f" TAC version : {tac.get_tac_version()}") + print(f" Logging : {tac.get_logging_state()}") + + ports = tac.list_ports() + print(f" Devices found : {len(ports)}") + for i, p in enumerate(ports): + print(f" [{i}] {p}") + + if not port_name and ports: + port_name = ports[0] + + if port_name: + print(f"\nOpening '{port_name}'...") + handle = tac.open_handle_by_description(port_name) + info = tac.get_device_info(handle) + for k, v in info.items(): + print(f" {k:<20}: {v}") + + print(f" Commands : {tac.list_commands(handle)}") + print(f" Queue clear : {tac.is_command_queue_clear(handle)}") + + tac.close_tac_handle(handle) + print("Handle closed.") + else: + print("No device available — skipping handle demo.") + + +if __name__ == "__main__": + _demo(sys.argv[1] if len(sys.argv) > 1 else None) diff --git a/examples/MCP/tacdev_mcp_server.py b/examples/MCP/tacdev_mcp_server.py new file mode 100644 index 0000000..9966eb0 --- /dev/null +++ b/examples/MCP/tacdev_mcp_server.py @@ -0,0 +1,509 @@ +#!/usr/bin/env python3 + +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +""" +MCP server for TACDev. +Wraps the QTAC TACDev Python library and exposes every function as an MCP tool. + +Install the TACDev library first — see README.md for setup instructions. + +Usage: + python tacdev_mcp_server.py +""" + +import TACDev +from fastmcp import FastMCP + +# --------------------------------------------------------------------------- # +# Handle registry +# Bridges MCP integer handles to TACDevice objects returned by the library. +# --------------------------------------------------------------------------- # +_handles: dict[int, TACDev.TACDevice] = {} +_next_handle: int = 1 + + +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}. Call open_handle_by_description first.") + return device + + +# --------------------------------------------------------------------------- # +# MCP server +# --------------------------------------------------------------------------- # +mcp = FastMCP("TACDev") + + +# ── Diagnostics ────────────────────────────────────────────────────────────── + +@mcp.tool() +def get_alpaca_version() -> dict: + """Return the QTAC (Alpaca) version string.""" + return {"alpaca_version": TACDev.AlpacaVersion()} + + +@mcp.tool() +def get_tac_version() -> dict: + """Return the TACDev library version string.""" + return {"tac_version": TACDev.TACVersion()} + + +# ── Logging ────────────────────────────────────────────────────────────────── + +@mcp.tool() +def get_logging_state() -> dict: + """Return whether TACDev logging is currently enabled.""" + return {"logging_enabled": TACDev.GetLoggingState()} + + +@mcp.tool() +def set_logging_state(enabled: bool) -> dict: + """Enable or disable TACDev logging.""" + TACDev.SetLoggingState(enabled) + return {"success": True} + + +# ── Device enumeration ─────────────────────────────────────────────────────── + +@mcp.tool() +def get_device_count() -> dict: + """Return the number of connected TAC devices.""" + return {"device_count": TACDev.GetDeviceCount()} + + +@mcp.tool() +def get_port_data(device_index: int) -> dict: + """Return the port description string for the device at device_index.""" + device = TACDev.GetDevice(device_index) + if device is None: + raise RuntimeError(f"No device found at index {device_index}.") + return {"port_data": device.PortName(), "description": device.Description(), "serial_number": device.SerialNumber()} + + +# ── Handle management ──────────────────────────────────────────────────────── + +@mcp.tool() +def open_handle_by_description(port_name: str) -> dict: + """Open a TAC device by its port name and return an integer handle.""" + 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"Found device '{port_name}' but failed to open it.") + return {"handle": _register(device)} + raise RuntimeError(f"No device found with port name '{port_name}'. Use get_port_data to list available devices.") + + +@mcp.tool() +def close_tac_handle(handle: int) -> dict: + """Close a previously opened TAC device handle.""" + device = _get_device(handle) + device.Close() + del _handles[handle] + return {"success": True} + + +# ── Device info ────────────────────────────────────────────────────────────── + +@mcp.tool() +def get_name(handle: int) -> dict: + """Return the human-readable name of the TAC device.""" + return {"name": _get_device(handle).Get_Name()} + + +@mcp.tool() +def get_firmware_version(handle: int) -> dict: + """Return the firmware version of the TAC device.""" + return {"firmware_version": _get_device(handle).GetFirmwareVersion()} + + +@mcp.tool() +def get_hardware(handle: int) -> dict: + """Return the hardware description of the TAC device.""" + return {"hardware": _get_device(handle).GetHardware()} + + +@mcp.tool() +def get_hardware_version(handle: int) -> dict: + """Return the hardware version of the TAC device.""" + return {"hardware_version": _get_device(handle).Get_HardwareVersion()} + + +@mcp.tool() +def get_uuid(handle: int) -> dict: + """Return the UUID of the TAC device.""" + return {"uuid": _get_device(handle).Get_UUID()} + + +# ── External power ─────────────────────────────────────────────────────────── + +@mcp.tool() +def set_external_power_control(handle: int, state: bool) -> dict: + """Enable or disable external power control on the TAC device.""" + _get_device(handle).SetExternalPowerControl(state) + return {"success": True} + + +# ── Dynamic commands ───────────────────────────────────────────────────────── + +@mcp.tool() +def list_commands(handle: int) -> dict: + """Return all dynamic command names available on the device.""" + device = _get_device(handle) + count = device.GetCommandCount() + return {"commands": [device.GetCommand(i) for i in range(count)]} + + +@mcp.tool() +def get_command(handle: int, command_index: int) -> dict: + """Return the dynamic command name at command_index.""" + return {"command": _get_device(handle).GetCommand(command_index)} + + +# ── Quick commands ─────────────────────────────────────────────────────────── + +@mcp.tool() +def list_quick_commands(handle: int) -> dict: + """Return all quick command names available on the device.""" + device = _get_device(handle) + count = device.GetQuickCommandCount() + return {"quick_commands": [device.GetQuickCommand(i) for i in range(count)]} + + +# ── Script variables ───────────────────────────────────────────────────────── + +@mcp.tool() +def list_script_variables(handle: int) -> dict: + """Return all script variable names for the device.""" + device = _get_device(handle) + count = device.GetScriptVariableCount() + return {"script_variables": [device.GetScriptVariable(i) for i in range(count)]} + + +@mcp.tool() +def update_script_variable(handle: int, variable: str, value: str) -> dict: + """Set the value of a named script variable on the device.""" + _get_device(handle).UpdateScriptVariableValue(variable, value) + return {"success": True} + + +# ── Core command interface ─────────────────────────────────────────────────── + +@mcp.tool() +def get_command_state(handle: int, command: str) -> dict: + """Return the current boolean state of a named command on the device.""" + return {"command": command, "state": _get_device(handle).GetCommandState(command)} + + +@mcp.tool() +def send_command(handle: int, command: str, state: bool) -> dict: + """Send a named command with a boolean state to the device.""" + _get_device(handle).SendCommand(command, state) + return {"success": True} + + +# ── Help ───────────────────────────────────────────────────────────────────── + +@mcp.tool() +def get_help_text(handle: int) -> dict: + """Return the full help text listing all commands supported by the device.""" + return {"help_text": _get_device(handle).GetHelpText()} + + +# ── Raw pin control ────────────────────────────────────────────────────────── + +@mcp.tool() +def set_pin_state(handle: int, pin: int, state: bool) -> dict: + """Set the raw boolean state of a hardware pin on the device.""" + _get_device(handle).SetPin(pin, state) + return {"success": True} + + +# ── Command queue ──────────────────────────────────────────────────────────── + +@mcp.tool() +def is_command_queue_clear(handle: int) -> dict: + """Return True if the device's command queue is empty.""" + return {"queue_clear": _get_device(handle).IsCommandQueueClear()} + + +# ── Power / peripheral boolean controls ───────────────────────────────────── + +@mcp.tool() +def set_battery_state(handle: int, state: bool) -> dict: + """Set the battery connection state (True = connected).""" + _get_device(handle).SetBatteryState(state) + return {"success": True} + + +@mcp.tool() +def get_battery_state(handle: int) -> dict: + """Return the current battery connection state.""" + return {"state": _get_device(handle).GetBatteryState()} + + +@mcp.tool() +def set_usb0(handle: int, state: bool) -> dict: + """Set the USB0 connection state (True = connected).""" + _get_device(handle).Usb0(state) + return {"success": True} + + +@mcp.tool() +def get_usb0_state(handle: int) -> dict: + """Return the current USB0 connection state.""" + return {"state": _get_device(handle).GetUsb0State()} + + +@mcp.tool() +def set_usb1(handle: int, state: bool) -> dict: + """Set the USB1 connection state (True = connected).""" + _get_device(handle).Usb1(state) + return {"success": True} + + +@mcp.tool() +def get_usb1_state(handle: int) -> dict: + """Return the current USB1 connection state.""" + return {"state": _get_device(handle).GetUsb1State()} + + +@mcp.tool() +def set_power_key(handle: int, state: bool) -> dict: + """Set the power key state (True = pressed).""" + _get_device(handle).PowerKey(state) + return {"success": True} + + +@mcp.tool() +def get_power_key_state(handle: int) -> dict: + """Return the current power key state.""" + return {"state": _get_device(handle).GetPowerKeyState()} + + +@mcp.tool() +def set_volume_up(handle: int, state: bool) -> dict: + """Set the volume-up button state (True = pressed).""" + _get_device(handle).VolumeUp(state) + return {"success": True} + + +@mcp.tool() +def get_volume_up_state(handle: int) -> dict: + """Return the current volume-up button state.""" + return {"state": _get_device(handle).GetVolumeUpState()} + + +@mcp.tool() +def set_volume_down(handle: int, state: bool) -> dict: + """Set the volume-down button state (True = pressed).""" + _get_device(handle).VolumeDown(state) + return {"success": True} + + +@mcp.tool() +def get_volume_down_state(handle: int) -> dict: + """Return the current volume-down button state.""" + return {"state": _get_device(handle).GetVolumeDownState()} + + +@mcp.tool() +def set_disconnect_uim1(handle: int, state: bool) -> dict: + """Set the UIM1 disconnect state (True = disconnected).""" + _get_device(handle).DisconnectUIM1(state) + return {"success": True} + + +@mcp.tool() +def get_disconnect_uim1_state(handle: int) -> dict: + """Return the current UIM1 disconnect state.""" + return {"state": _get_device(handle).GetDisconnectUIM1State()} + + +@mcp.tool() +def set_disconnect_uim2(handle: int, state: bool) -> dict: + """Set the UIM2 disconnect state (True = disconnected).""" + _get_device(handle).DisconnectUIM2(state) + return {"success": True} + + +@mcp.tool() +def get_disconnect_uim2_state(handle: int) -> dict: + """Return the current UIM2 disconnect state.""" + return {"state": _get_device(handle).GetDisconnectUIM2State()} + + +@mcp.tool() +def set_disconnect_sd_card(handle: int, state: bool) -> dict: + """Set the SD card disconnect state (True = disconnected).""" + _get_device(handle).DisconnectSDCard(state) + return {"success": True} + + +@mcp.tool() +def get_disconnect_sd_card_state(handle: int) -> dict: + """Return the current SD card disconnect state.""" + return {"state": _get_device(handle).GetDisconnectSDCardState()} + + +@mcp.tool() +def set_primary_edl(handle: int, state: bool) -> dict: + """Set the primary EDL (Emergency Download Mode) state.""" + _get_device(handle).PrimaryEDL(state) + return {"success": True} + + +@mcp.tool() +def get_primary_edl_state(handle: int) -> dict: + """Return the current primary EDL state.""" + return {"state": _get_device(handle).GetPrimaryEDLState()} + + +@mcp.tool() +def set_secondary_edl(handle: int, state: bool) -> dict: + """Set the secondary EDL state.""" + _get_device(handle).SecondaryEDL(state) + return {"success": True} + + +@mcp.tool() +def get_secondary_edl_state(handle: int) -> dict: + """Return the current secondary EDL state.""" + return {"state": _get_device(handle).GetSecondaryEDLState()} + + +@mcp.tool() +def set_force_ps_hold_high(handle: int, state: bool) -> dict: + """Force PS_HOLD high (True = forced high).""" + _get_device(handle).ForcePSHoldHigh(state) + return {"success": True} + + +@mcp.tool() +def get_force_ps_hold_high_state(handle: int) -> dict: + """Return the current force PS_HOLD high state.""" + return {"state": _get_device(handle).GetForcePSHoldHighState()} + + +@mcp.tool() +def set_secondary_pm_resin_n(handle: int, state: bool) -> dict: + """Set the secondary PM RESIN_N state.""" + _get_device(handle).SecondaryPM_RESIN_N(state) + return {"success": True} + + +@mcp.tool() +def get_secondary_pm_resin_n_state(handle: int) -> dict: + """Return the current secondary PM RESIN_N state.""" + return {"state": _get_device(handle).GetSecondaryPM_RESIN_NState()} + + +@mcp.tool() +def set_eud(handle: int, state: bool) -> dict: + """Set the EUD (Enhanced USB Debugging) state.""" + _get_device(handle).Eud(state) + return {"success": True} + + +@mcp.tool() +def get_eud_state(handle: int) -> dict: + """Return the current EUD state.""" + return {"state": _get_device(handle).GetEUDState()} + + +@mcp.tool() +def set_headset_disconnect(handle: int, state: bool) -> dict: + """Set the headset disconnect state (True = disconnected).""" + _get_device(handle).HeadsetDisconnect(state) + return {"success": True} + + +@mcp.tool() +def get_headset_disconnect_state(handle: int) -> dict: + """Return the current headset disconnect state.""" + return {"state": _get_device(handle).GetHeadsetDisconnectState()} + + +# ── Name / reset count ─────────────────────────────────────────────────────── + +@mcp.tool() +def set_name(handle: int, new_name: str) -> dict: + """Set a new human-readable name on the TAC device (persisted in firmware).""" + _get_device(handle).SetName(new_name) + return {"success": True} + + +@mcp.tool() +def get_reset_count(handle: int) -> dict: + """Return the number of resets recorded by the TAC device.""" + return {"reset_count": _get_device(handle).GetResetCount()} + + +@mcp.tool() +def clear_reset_count(handle: int) -> dict: + """Clear (zero) the reset counter on the TAC device.""" + _get_device(handle).ClearResetCount() + return {"success": True} + + +# ── Button sequences ───────────────────────────────────────────────────────── + +@mcp.tool() +def power_on_button(handle: int) -> dict: + """Execute the power-on button sequence on the device.""" + _get_device(handle).PowerOnButton() + return {"success": True} + + +@mcp.tool() +def power_off_button(handle: int) -> dict: + """Execute the power-off button sequence on the device.""" + _get_device(handle).PowerOffButton() + return {"success": True} + + +@mcp.tool() +def boot_to_fastboot_button(handle: int) -> dict: + """Execute the button sequence to boot the device into Fastboot mode.""" + _get_device(handle).BootToFastBootButton() + return {"success": True} + + +@mcp.tool() +def boot_to_uefi_menu_button(handle: int) -> dict: + """Execute the button sequence to enter the UEFI menu.""" + _get_device(handle).BootToUEFIMenuButton() + return {"success": True} + + +@mcp.tool() +def boot_to_edl_button(handle: int) -> dict: + """Execute the button sequence to boot the device into primary EDL mode.""" + _get_device(handle).BootToEDLButton() + return {"success": True} + + +@mcp.tool() +def boot_to_secondary_edl_button(handle: int) -> dict: + """Execute the button sequence to boot the device into secondary EDL mode.""" + _get_device(handle).BootToSecondaryEDLButton() + return {"success": True} + + +# --------------------------------------------------------------------------- # +# Entry point +# --------------------------------------------------------------------------- # +if __name__ == "__main__": + mcp.run() 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/third-party/CMakeLists.txt b/third-party/CMakeLists.txt index c3a70e6..12ae74e 100644 --- a/third-party/CMakeLists.txt +++ b/third-party/CMakeLists.txt @@ -35,10 +35,22 @@ 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") @@ -56,6 +68,7 @@ endif() if(NEED_DOWNLOAD) set(RETRY_COUNT 0) + set(DOWNLOAD_SUCCESS FALSE) while(RETRY_COUNT LESS 3) math(EXPR RETRY_COUNT "${RETRY_COUNT} + 1") if(EXISTS "${ARCHIVE_PATH}") @@ -66,6 +79,7 @@ if(NEED_DOWNLOAD) 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,8 +87,14 @@ 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}") @@ -82,25 +102,44 @@ if(NEED_DOWNLOAD) 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") + 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}/amd64/FTD2XX64.dll") + file(GLOB SOURCE_DLL "${TEMP_DIR}/${FTDI_ZIP_DLL_GLOB}") 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") + 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_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) + if(NOT SOURCE_LIB AND FTDI_WIN_ARCH STREQUAL "x64") file(GLOB SOURCE_LIB "${TEMP_DIR}/Static/amd64/ftd2xx.lib") 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") + 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(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") @@ -131,8 +170,8 @@ 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(DEBUG_BIN_DIR "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Debug/bin") + set(RELEASE_BIN_DIR "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Release/bin") set(QT_DLLS Qt6Core Qt6Gui Qt6Widgets Qt6SerialPort Qt6Concurrent Qt6Network Qt6Xml Qt6Multimedia Qt6MultimediaWidgets) From 2b866feb698c23a704078fd0840425f50a3a4505 Mon Sep 17 00:00:00 2001 From: "Roy, Biswajit" Date: Wed, 24 Jun 2026 23:07:49 +0530 Subject: [PATCH 02/17] update arm64 cache --- .github/workflows/build.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6007e7a..cf9cc62 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ jobs: 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-2022 + runs-on: windows-11-arm steps: - uses: actions/checkout@v6 @@ -48,7 +48,6 @@ jobs: with: path: third-party\CDM-v2.12.36.20-for-ARM64-WHQL-Certified.zip key: ftdi-windows-arm64-2.12.36.20 - enableCrossOsArchive: true - name: Download FTDI archive for ARM64 if: steps.cache-ftdi-windows-arm64.outputs.cache-hit != 'true' @@ -213,7 +212,6 @@ jobs: with: path: third-party\CDM-v2.12.36.20-for-ARM64-WHQL-Certified.zip key: ftdi-windows-arm64-2.12.36.20 - enableCrossOsArchive: true - name: Extract FTDI library for ARM64 shell: pwsh From a995a12087a311556fc5bc123dceca47e49ef84e Mon Sep 17 00:00:00 2001 From: "Roy, Biswajit" Date: Thu, 25 Jun 2026 00:06:14 +0530 Subject: [PATCH 03/17] arm64 build --- .github/workflows/build.yml | 9 ++-- README.md | 9 ++-- build.bat | 19 ++------ build.sh | 45 +++++++++++++++++-- .../01-Build-Using-Qt-Creator.md | 10 +++-- third-party/CMakeLists.txt | 35 +-------------- 6 files changed, 62 insertions(+), 65 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf9cc62..71cd032 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,7 +70,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 @@ -83,7 +83,6 @@ 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 @@ -102,7 +101,7 @@ jobs: 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" + run: cmake -B build -DCMAKE_GENERATOR=Ninja -DCMAKE_COLOR_DIAGNOSTICS=ON -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ${{ matrix.build_type == 'Debug' && '-DCMAKE_CXX_FLAGS_INIT=-DQT_QML_DEBUG' || '' }} -DCMAKE_PREFIX_PATH="$QT_ROOT_DIR/lib/cmake" - name: Build with ${{ matrix.build_type }} run: cmake --build build --config ${{ matrix.build_type }} @@ -170,7 +169,7 @@ jobs: - name: Configure CMake for ${{ matrix.build_type }} shell: pwsh - run: cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCMAKE_PREFIX_PATH="$env:QT_ROOT_DIR\lib\cmake" + run: cmake -B build -DCMAKE_GENERATOR=Ninja -DCMAKE_COLOR_DIAGNOSTICS=ON -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ${{ matrix.build_type == 'Debug' && '-DCMAKE_CXX_FLAGS_INIT=-DQT_QML_DEBUG' || '' }} -DCMAKE_PREFIX_PATH="$env:QT_ROOT_DIR\lib\cmake" - name: Build with ${{ matrix.build_type }} shell: pwsh @@ -239,7 +238,7 @@ jobs: - name: Configure CMake for ${{ matrix.build_type }} shell: pwsh - run: cmake -B build -A ARM64 -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCMAKE_PREFIX_PATH="$env:QT_ROOT_DIR\lib\cmake" + run: cmake -B build -DCMAKE_GENERATOR=Ninja -DCMAKE_COLOR_DIAGNOSTICS=ON -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ${{ matrix.build_type == 'Debug' && '-DCMAKE_CXX_FLAGS_INIT=-DQT_QML_DEBUG' || '' }} -DCMAKE_PREFIX_PATH="$env:QT_ROOT_DIR\lib\cmake" - name: Build with ${{ matrix.build_type }} shell: pwsh diff --git a/README.md b/README.md index 953f7e2..a8204fa 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 @@ -124,6 +125,7 @@ build.bat ARM64 (ARM64) **Usage**: ```cmd __Builds\x64\Release\QTAC.exe +__Builds\ARM64\Release\QTAC.exe ``` ## Linux Guide @@ -149,7 +151,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 ``` @@ -166,9 +168,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 5d46cc3..32435b5 100644 --- a/build.bat +++ b/build.bat @@ -16,22 +16,12 @@ if "%ARCH%"=="" ( ) if /i "%ARCH%"=="x64" ( - set CMAKE_SYSTEM_PROCESSOR=AMD64 set EXPECTED_QT_PATH=msvc2022_64 set VCVARS_SCRIPT=vcvars64.bat set VS_COMPONENT=Desktop development with C++ ) else if /i "%ARCH%"=="ARM64" ( - set CMAKE_SYSTEM_PROCESSOR=ARM64 set EXPECTED_QT_PATH=msvc2022_arm64 - if "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( - set VCVARS_SCRIPT=vcvarsarm64.bat - ) else if "%PROCESSOR_ARCHITECTURE%"=="AMD64" ( - set VCVARS_SCRIPT=vcvarsamd64_arm64.bat - ) else ( - echo ERROR: Cannot build for ARM64 - unrecognised host architecture '%PROCESSOR_ARCHITECTURE%'. - echo ARM64 builds require an x64 host ^(cross-compile^) or an ARM64 host ^(native compile^). - exit /b 1 - ) + set VCVARS_SCRIPT=vcvarsarm64.bat set VS_COMPONENT=MSVC v143 - VS 2022 C++ ARM64 build tools ) else ( echo ERROR: Unsupported architecture '%ARCH%'. @@ -42,8 +32,7 @@ if /i "%ARCH%"=="x64" ( exit /b 1 ) -echo Target architecture : %ARCH% -echo Host architecture : %PROCESSOR_ARCHITECTURE% +echo Architecture : %ARCH% @REM --------------------------------------------------------------------------- @REM Validate QTBIN @@ -139,7 +128,6 @@ cmake -S . -B build\Debug -DCMAKE_PREFIX_PATH="%QTBIN%\.." ^ -DCMAKE_COLOR_DIAGNOSTICS=ON ^ -DCMAKE_GENERATOR=Ninja ^ -DCMAKE_BUILD_TYPE=Debug ^ - -DCMAKE_SYSTEM_PROCESSOR=%CMAKE_SYSTEM_PROCESSOR% ^ -DCMAKE_CXX_FLAGS_INIT=-DQT_QML_DEBUG cmake --build build\Debug @@ -147,8 +135,7 @@ cmake --build build\Debug cmake -S . -B build\Release -DCMAKE_PREFIX_PATH="%QTBIN%\.." ^ -DCMAKE_COLOR_DIAGNOSTICS=ON ^ -DCMAKE_GENERATOR=Ninja ^ - -DCMAKE_BUILD_TYPE=Release ^ - -DCMAKE_SYSTEM_PROCESSOR=%CMAKE_SYSTEM_PROCESSOR% + -DCMAKE_BUILD_TYPE=Release cmake --build build\Release diff --git a/build.sh b/build.sh index e698d45..351f3fa 100644 --- a/build.sh +++ b/build.sh @@ -8,7 +8,37 @@ 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 @@ -18,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 7e1cb20..51adb7e 100644 --- a/docs/getting-started/01-Build-Using-Qt-Creator.md +++ b/docs/getting-started/01-Build-Using-Qt-Creator.md @@ -2,7 +2,7 @@ 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 @@ -26,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) @@ -45,6 +45,10 @@ Open the session. The **Project** tab on the left pane will be inactive. Create 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 diff --git a/third-party/CMakeLists.txt b/third-party/CMakeLists.txt index 12ae74e..7087ff1 100644 --- a/third-party/CMakeLists.txt +++ b/third-party/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) From 39b197b9821403198ec87002fba6f90169efa8d1 Mon Sep 17 00:00:00 2001 From: "Roy, Biswajit" Date: Thu, 25 Jun 2026 00:23:17 +0530 Subject: [PATCH 04/17] minify build steps --- .github/workflows/build.yml | 101 +++--------------- README.md | 10 +- .../01-Build-Using-Qt-Creator.md | 10 +- 3 files changed, 24 insertions(+), 97 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 71cd032..a4456dc 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] @@ -62,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 @@ -84,8 +86,7 @@ jobs: - name: Install Qt ${{ matrix.qt_version }} on ${{ matrix.os }} run: | 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 @@ -93,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_GENERATOR=Ninja -DCMAKE_COLOR_DIAGNOSTICS=ON -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ${{ matrix.build_type == 'Debug' && '-DCMAKE_CXX_FLAGS_INIT=-DQT_QML_DEBUG' || '' }} -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 @@ -113,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 @@ -132,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 @@ -143,37 +130,9 @@ 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 }} - shell: pwsh - run: cmake -B build -DCMAKE_GENERATOR=Ninja -DCMAKE_COLOR_DIAGNOSTICS=ON -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ${{ matrix.build_type == 'Debug' && '-DCMAKE_CXX_FLAGS_INIT=-DQT_QML_DEBUG' || '' }} -DCMAKE_PREFIX_PATH="$env:QT_ROOT_DIR\lib\cmake" - - - name: Build with ${{ matrix.build_type }} - shell: pwsh - run: cmake --build build --config ${{ matrix.build_type }} + - name: Build + shell: cmd + run: build.bat x64 build-windows-arm64: needs: fetch-ftdi-windows-arm64 @@ -182,7 +141,6 @@ jobs: fail-fast: false matrix: qt_version: ['6.9.2', '6.10.0'] - build_type: [Debug, Release] steps: - uses: actions/checkout@v6 @@ -201,10 +159,7 @@ jobs: shell: pwsh run: | aqt install-qt windows_arm64 desktop ${{ matrix.qt_version }} win64_msvc2022_arm64 --outputdir "C:\Qt" --autodesktop -m qtserialport qtmultimedia - - $qtRoot = "C:\Qt\${{ matrix.qt_version }}\msvc2022_arm64" - 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_arm64\bin" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - name: Restore FTDI archive for ARM64 uses: actions/cache/restore@v5 @@ -212,34 +167,6 @@ jobs: path: third-party\CDM-v2.12.36.20-for-ARM64-WHQL-Certified.zip key: ftdi-windows-arm64-2.12.36.20 - - name: Extract FTDI library for ARM64 - shell: pwsh - run: | - $debugLib = "__Builds\ARM64\Debug\lib" - $releaseLib = "__Builds\ARM64\Release\lib" - $debugBin = "__Builds\ARM64\Debug\bin" - $releaseBin = "__Builds\ARM64\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.20-for-ARM64-WHQL-Certified.zip" -DestinationPath $tmp -Force - - $lib = Get-ChildItem -Path $tmp -Filter "FTD2XXstatic.lib" -Recurse | Select-Object -First 1 - if (-not $lib) { $lib = Get-ChildItem -Path $tmp -Filter "*.lib" -Recurse | Select-Object -First 1 } - Copy-Item $lib.FullName -Destination "$debugLib\ftd2xx.lib" - Copy-Item $lib.FullName -Destination "$releaseLib\ftd2xx.lib" - - $dll = Get-ChildItem -Path $tmp -Filter "FTD2XX.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 }} - shell: pwsh - run: cmake -B build -DCMAKE_GENERATOR=Ninja -DCMAKE_COLOR_DIAGNOSTICS=ON -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} ${{ matrix.build_type == 'Debug' && '-DCMAKE_CXX_FLAGS_INIT=-DQT_QML_DEBUG' || '' }} -DCMAKE_PREFIX_PATH="$env:QT_ROOT_DIR\lib\cmake" - - - name: Build with ${{ matrix.build_type }} - shell: pwsh - run: cmake --build build --config ${{ matrix.build_type }} + - name: Build + shell: cmd + run: build.bat ARM64 diff --git a/README.md b/README.md index a8204fa..0a35f27 100644 --- a/README.md +++ b/README.md @@ -67,11 +67,11 @@ QTAC is a software suite that enables users to control Qualcomm devices remotely > 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 | URL | -> | :-- | :-- | :-- | -> | **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 | +> | 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 diff --git a/docs/getting-started/01-Build-Using-Qt-Creator.md b/docs/getting-started/01-Build-Using-Qt-Creator.md index 51adb7e..113c8f3 100644 --- a/docs/getting-started/01-Build-Using-Qt-Creator.md +++ b/docs/getting-started/01-Build-Using-Qt-Creator.md @@ -74,11 +74,11 @@ QTAC uses FTDI libraries to control FT4232H chip on the debug board. You can fin 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 | URL | -| :-- | :-- | :-- | -| **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 | +| 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. From 5c874624d0c8101f89f027d4be2129d09572d533 Mon Sep 17 00:00:00 2001 From: "Roy, Biswajit" Date: Thu, 25 Jun 2026 00:29:15 +0530 Subject: [PATCH 05/17] new compact build workflow --- build.bat | 7 +++++++ build.sh | 0 2 files changed, 7 insertions(+) mode change 100644 => 100755 build.sh diff --git a/build.bat b/build.bat index 32435b5..4695b14 100644 --- a/build.bat +++ b/build.bat @@ -83,6 +83,12 @@ echo QTBIN : %QTBIN% [OK] @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 @@ -110,6 +116,7 @@ if "%VCVARS_FOUND%"=="0" ( 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 diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 From aa5fe50a59464789701e08aaba40fc3b4cd62785 Mon Sep 17 00:00:00 2001 From: "Roy, Biswajit" Date: Thu, 25 Jun 2026 01:02:03 +0530 Subject: [PATCH 06/17] try cmake arm64 --- .github/workflows/build.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a4456dc..8efb779 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -161,6 +161,12 @@ jobs: 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: Install native ARM64 CMake + shell: pwsh + run: | + winget install --id Kitware.CMake --architecture arm64 --silent --accept-package-agreements --accept-source-agreements + echo "C:\Program Files\CMake\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: Restore FTDI archive for ARM64 uses: actions/cache/restore@v5 with: From 5e392ba0d648ebbbbbb7a7deabd55bcc5bd6596c Mon Sep 17 00:00:00 2001 From: "Roy, Biswajit" Date: Thu, 25 Jun 2026 01:15:03 +0530 Subject: [PATCH 07/17] install arm64 cmake --- .github/workflows/build.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8efb779..9d230d0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -164,8 +164,10 @@ jobs: - name: Install native ARM64 CMake shell: pwsh run: | - winget install --id Kitware.CMake --architecture arm64 --silent --accept-package-agreements --accept-source-agreements - echo "C:\Program Files\CMake\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + $version = "4.3.3" + $msi = "cmake-$version-windows-arm64.msi" + curl --ssl-no-revoke -L "https://github.com/Kitware/CMake/releases/download/v$version/$msi" -o $msi + Start-Process msiexec.exe -Wait -ArgumentList "/i $msi /quiet /norestart ADD_CMAKE_TO_PATH=System" - name: Restore FTDI archive for ARM64 uses: actions/cache/restore@v5 From 9d63cae1cbb2307e003dcb59671e5d3cb41f8846 Mon Sep 17 00:00:00 2001 From: "Roy, Biswajit" Date: Thu, 25 Jun 2026 14:35:34 +0530 Subject: [PATCH 08/17] cmake is already native on ci --- .github/workflows/build.yml | 8 -------- src/libraries/qcommon-console/Common.cmake | 5 ++++- src/libraries/qcommon/Common.cmake | 5 ++++- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9d230d0..a4456dc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -161,14 +161,6 @@ jobs: 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: Install native ARM64 CMake - shell: pwsh - run: | - $version = "4.3.3" - $msi = "cmake-$version-windows-arm64.msi" - curl --ssl-no-revoke -L "https://github.com/Kitware/CMake/releases/download/v$version/$msi" -o $msi - Start-Process msiexec.exe -Wait -ArgumentList "/i $msi /quiet /norestart ADD_CMAKE_TO_PATH=System" - - name: Restore FTDI archive for ARM64 uses: actions/cache/restore@v5 with: 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() From 8db27238ba880517a8c0260197824bdc3328f9f5 Mon Sep 17 00:00:00 2001 From: "Roy, Biswajit" Date: Thu, 25 Jun 2026 18:31:25 +0530 Subject: [PATCH 09/17] mcp server works --- .gitignore | 3 + examples/MCP/README.md | 100 +++++- examples/MCP/config.yaml | 15 + examples/MCP/requirements.txt | 4 +- examples/MCP/tacdev_mcp_client.py | 513 ++++++++++++------------------ examples/MCP/tacdev_mcp_server.py | 347 ++++++++++++-------- 6 files changed, 527 insertions(+), 455 deletions(-) create mode 100644 examples/MCP/config.yaml diff --git a/.gitignore b/.gitignore index cab114c..13e35fd 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,9 @@ __pycache__/ *.pyo *.pyd +# MCP logs +examples/MCP/*.log + # Qt build directory __Builds/ diff --git a/examples/MCP/README.md b/examples/MCP/README.md index 08a0231..eabf3fb 100644 --- a/examples/MCP/README.md +++ b/examples/MCP/README.md @@ -1,13 +1,59 @@ # QTAC MCP Server An [MCP](https://modelcontextprotocol.io) server that exposes the full TACDev API as tools, -allowing AI assistants to control Qualcomm devices via a QTAC debug board. +allowing AI assistants and automation clients to control Qualcomm devices via a QTAC debug board. + +## Architecture + +A persistent SSE server. Multiple clients connect concurrently, each identified by a UUID session. +Each client can open one or more devices. A device can only be held by one session at a time. +On disconnect, the server closes any devices the session left open. + +```mermaid +sequenceDiagram + participant A as Client A + participant S as MCP Server + participant B as Client B + + A->>S: SSE connect + S-->>A: session UUID (auto) + S-->>A: device list push [COM3: free, COM5: free] + + B->>S: SSE connect + S-->>B: session UUID (auto) + S-->>B: device list push [COM3: free, COM5: free] + + A->>S: open_handle_by_description(COM3) + S->>S: TACDev.Open(COM3), register ownership + S-->>A: handle: 1 + + B->>S: open_handle_by_description(COM3) + S-->>B: ERROR - COM3 in use by session-A + + B->>S: open_handle_by_description(COM5) + S->>S: TACDev.Open(COM5), register ownership + S-->>B: handle: 2 + + par Client A uses COM3 + A->>S: tool calls (handle 1) + S-->>A: results + and Client B uses COM5 + B->>S: tool calls (handle 2) + S-->>B: results + end + + A->>S: close_tac_handle(1) + S->>S: TACDev.Close(COM3), release ownership + A->>S: SSE disconnect + + B->>S: SSE disconnect (without closing handle) + S->>S: TACDev.Close(COM5), release ownership +``` ## Prerequisites - Python 3.8+ (64-bit, matching your build architecture: x64 or ARM64) - Project built with `build.bat` / `build.sh` (the TACDev library is loaded from `__Builds`) -- MCP-capable host ## Setup @@ -26,39 +72,69 @@ Each script builds the project, installs the TACDev Python library, and installs For full setup guidance see [Bootcamp guide](../../docs/bootcamp/01-Bootcamp.md) and [Python API reference](../../docs/bootcamp/02-Python-API.md). +## Configuration + +All runtime parameters are in `config.yaml`: + +```yaml +server: + host: "127.0.0.1" + port: 8000 + +logging: + file: "tacdev_mcp.log" + level: "INFO" + max_bytes: 10485760 + backup_count: 5 +``` + ## Usage -`tacdev_mcp_server.py`: starts the MCP server over stdio. Run from the repo root: +**Start the server** (must be running before any client connects): ```bash python examples/MCP/tacdev_mcp_server.py ``` -`tacdev_mcp_client.py`: launches the server as a subprocess and runs a device demo. -Pass an optional port name to open a specific device: +**Run the client demo:** ```bash -python examples/MCP/tacdev_mcp_client.py # lists devices, opens first found -python examples/MCP/tacdev_mcp_client.py TAC-Lite # opens device by port name +python examples/MCP/tacdev_mcp_client.py # opens first available device +python examples/MCP/tacdev_mcp_client.py COM41 # opens specific port +``` + +**Use as a library:** + +```python +import asyncio +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("COM41") + await tac.power_on_button(handle) + await tac.close_tac_handle(handle) + +asyncio.run(main()) ``` ## Available tools | Category | Tools | | :-- | :-- | -| Diagnostics | `get_alpaca_version`, `get_tac_version` | +| Devices | `list_devices` | +| Diagnostics | `get_alpaca_version`, `get_tac_version`, `get_last_tac_error` | | Logging | `get_logging_state`, `set_logging_state` | | Device enumeration | `get_device_count`, `get_port_data` | | Handle management | `open_handle_by_description`, `close_tac_handle` | | Device info | `get_name`, `get_firmware_version`, `get_hardware`, `get_hardware_version`, `get_uuid` | | External power | `set_external_power_control` | -| Dynamic commands | `list_commands`, `get_command` | -| Quick commands | `list_quick_commands` | +| Dynamic commands | `list_commands`, `get_command`, `list_quick_commands` | | Script variables | `list_script_variables`, `update_script_variable` | | Command interface | `get_command_state`, `send_command` | -| Help | `get_help_text` | +| Help / queue | `get_help_text`, `is_command_queue_clear` | | Raw pin | `set_pin_state` | -| Command queue | `is_command_queue_clear` | | Battery | `set_battery_state`, `get_battery_state` | | USB | `set_usb0`, `get_usb0_state`, `set_usb1`, `get_usb1_state` | | Buttons | `set_power_key`, `get_power_key_state`, `set_volume_up`, `get_volume_up_state`, `set_volume_down`, `get_volume_down_state` | diff --git a/examples/MCP/config.yaml b/examples/MCP/config.yaml new file mode 100644 index 0000000..41d6bf5 --- /dev/null +++ b/examples/MCP/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/requirements.txt b/examples/MCP/requirements.txt index dac0892..f2f1e38 100644 --- a/examples/MCP/requirements.txt +++ b/examples/MCP/requirements.txt @@ -1,2 +1,2 @@ -fastmcp>=2.0.0 -mcp>=1.0.0 +fastmcp>=3.0.0 +pyyaml diff --git a/examples/MCP/tacdev_mcp_client.py b/examples/MCP/tacdev_mcp_client.py index 90d4864..564a90a 100644 --- a/examples/MCP/tacdev_mcp_client.py +++ b/examples/MCP/tacdev_mcp_client.py @@ -6,69 +6,44 @@ """ MCP client for tacdev_mcp_server.py. -Launches the server as a subprocess and exposes a clean -TACDevClient class whose methods map 1-to-1 to every MCP tool. - Typical usage ------------- from tacdev_mcp_client import TACDevClient - with TACDevClient() as tac: - count = tac.get_device_count() - handle = tac.open_handle_by_description("TAC-Lite") - tac.power_on_button(handle) - tac.close_tac_handle(handle) + 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) -Run as a script for a quick interactive demo: - python tacdev_mcp_client.py [port_name] + asyncio.run(main()) """ from __future__ import annotations import asyncio -import os +import json import sys -from contextlib import asynccontextmanager from pathlib import Path from typing import Any -from mcp import ClientSession, StdioServerParameters -from mcp.client.stdio import stdio_client - -# --------------------------------------------------------------------------- # -# Server launch parameters -# --------------------------------------------------------------------------- # -_SERVER_SCRIPT = str(Path(__file__).with_name("tacdev_mcp_server.py")) - -_SERVER_PARAMS = StdioServerParameters( - command=sys.executable, - args=[_SERVER_SCRIPT], - env={ - **os.environ, - # Override library path here if needed, or set TACDEV_LIB_PATH in env. - # "TACDEV_LIB_PATH": r"C:\path\to\TACDev.dll", - }, -) - - -# --------------------------------------------------------------------------- # -# Low-level async helper -# --------------------------------------------------------------------------- # -@asynccontextmanager -async def _session(): - """Async context manager that yields a live MCP ClientSession.""" - async with stdio_client(_SERVER_PARAMS) as (read, write): - async with ClientSession(read, write) as session: - await session.initialize() - yield session - - -async def _call(session: ClientSession, tool: str, **kwargs: Any) -> Any: - """Call one MCP tool and return its unwrapped result value.""" - result = await session.call_tool(tool, arguments=kwargs) - # FastMCP returns a list of TextContent; pull out the parsed dict. +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" + + +async def _call(client: Client, tool: str, **kwargs: Any) -> Any: + result = await client.call_tool(tool, kwargs) if result.content: - import json text = result.content[0].text try: return json.loads(text) @@ -77,357 +52,271 @@ async def _call(session: ClientSession, tool: str, **kwargs: Any) -> Any: return None -# --------------------------------------------------------------------------- # -# Synchronous TACDevClient -# --------------------------------------------------------------------------- # class TACDevClient: - """ - Synchronous wrapper around the TACDev MCP server. - - Can be used as a context manager: - with TACDevClient() as tac: - ... - - Or manually: - tac = TACDevClient() - tac.connect() - tac.disconnect() - """ - def __init__(self) -> None: - self._loop: asyncio.AbstractEventLoop | None = None - self._session: ClientSession | None = None - self._ctx = None - - # ── Lifecycle ──────────────────────────────────────────────────────────── + self._client = Client(_server_url()) - def connect(self) -> "TACDevClient": - self._loop = asyncio.new_event_loop() - self._ctx = _session() - self._session = self._loop.run_until_complete(self._ctx.__aenter__()) + async def __aenter__(self) -> TACDevClient: + await self._client.__aenter__() return self - def disconnect(self) -> None: - if self._ctx and self._loop: - self._loop.run_until_complete(self._ctx.__aexit__(None, None, None)) - if self._loop: - self._loop.close() - self._session = None - self._loop = None - self._ctx = None + async def __aexit__(self, *args) -> None: + await self._client.__aexit__(*args) - def __enter__(self) -> "TACDevClient": - return self.connect() + async def _call(self, tool: str, **kwargs: Any) -> Any: + return await _call(self._client, tool, **kwargs) - def __exit__(self, *_) -> None: - self.disconnect() + async def list_devices(self) -> dict: + return await self._call("list_devices") - # ── Internal dispatch ──────────────────────────────────────────────────── + async def get_alpaca_version(self) -> str: + return (await self._call("get_alpaca_version"))["alpaca_version"] - def _call(self, tool: str, **kwargs: Any) -> Any: - if self._session is None or self._loop is None: - raise RuntimeError("Not connected — call connect() or use as a context manager") - return self._loop.run_until_complete(_call(self._session, tool, **kwargs)) + async def get_tac_version(self) -> str: + return (await self._call("get_tac_version"))["tac_version"] - # ── Version / diagnostics ──────────────────────────────────────────────── + async def get_last_tac_error(self) -> str: + return (await self._call("get_last_tac_error"))["last_error"] - def get_alpaca_version(self) -> str: - """Return the Alpaca firmware version string.""" - return self._call("get_alpaca_version")["alpaca_version"] + async def get_logging_state(self) -> bool: + return (await self._call("get_logging_state"))["logging_enabled"] - def get_tac_version(self) -> str: - """Return the TACDev library version string.""" - return self._call("get_tac_version")["tac_version"] + async def set_logging_state(self, enabled: bool) -> None: + await self._call("set_logging_state", enabled=enabled) - def get_last_tac_error(self) -> str: - """Return the last error message recorded by TACDev.""" - return self._call("get_last_tac_error")["last_error"] + async def get_device_count(self) -> int: + return (await self._call("get_device_count"))["device_count"] - # ── Logging ────────────────────────────────────────────────────────────── + async def get_port_data(self, device_index: int) -> str: + return (await self._call("get_port_data", device_index=device_index))["port_data"] - def get_logging_state(self) -> bool: - """Return whether TACDev logging is currently enabled.""" - return self._call("get_logging_state")["logging_enabled"] + 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)] - def set_logging_state(self, enabled: bool) -> None: - """Enable or disable TACDev logging.""" - self._call("set_logging_state", enabled=enabled) + async def open_handle_by_description(self, port_name: str) -> int: + return (await self._call("open_handle_by_description", port_name=port_name))["handle"] - # ── Device enumeration ─────────────────────────────────────────────────── + async def close_tac_handle(self, handle: int) -> None: + await self._call("close_tac_handle", handle=handle) - def get_device_count(self) -> int: - """Return the number of connected TAC devices.""" - return self._call("get_device_count")["device_count"] + async def get_name(self, handle: int) -> str: + return (await self._call("get_name", handle=handle))["name"] - def get_port_data(self, device_index: int) -> str: - """Return the port description string for the device at device_index.""" - return self._call("get_port_data", device_index=device_index)["port_data"] + async def get_firmware_version(self, handle: int) -> str: + return (await self._call("get_firmware_version", handle=handle))["firmware_version"] - def list_ports(self) -> list[str]: - """Convenience: return port descriptions for all connected devices.""" - count = self.get_device_count() - return [self.get_port_data(i) for i in range(count)] + async def get_hardware(self, handle: int) -> str: + return (await self._call("get_hardware", handle=handle))["hardware"] - # ── Handle management ──────────────────────────────────────────────────── + async def get_hardware_version(self, handle: int) -> str: + return (await self._call("get_hardware_version", handle=handle))["hardware_version"] - def open_handle_by_description(self, port_name: str) -> int: - """Open a TAC device by port description and return its integer handle.""" - return self._call("open_handle_by_description", port_name=port_name)["handle"] + async def get_uuid(self, handle: int) -> str: + return (await self._call("get_uuid", handle=handle))["uuid"] - def close_tac_handle(self, handle: int) -> None: - """Close a previously opened TAC device handle.""" - self._call("close_tac_handle", handle=handle) - - # ── Device info ────────────────────────────────────────────────────────── - - def get_name(self, handle: int) -> str: - return self._call("get_name", handle=handle)["name"] - - def get_firmware_version(self, handle: int) -> str: - return self._call("get_firmware_version", handle=handle)["firmware_version"] - - def get_hardware(self, handle: int) -> str: - return self._call("get_hardware", handle=handle)["hardware"] - - def get_hardware_version(self, handle: int) -> str: - return self._call("get_hardware_version", handle=handle)["hardware_version"] - - def get_uuid(self, handle: int) -> str: - return self._call("get_uuid", handle=handle)["uuid"] - - def get_device_info(self, handle: int) -> dict: - """Convenience: return all device identity fields in one call.""" + async def get_device_info(self, handle: int) -> dict: return { - "name": self.get_name(handle), - "firmware_version": self.get_firmware_version(handle), - "hardware": self.get_hardware(handle), - "hardware_version": self.get_hardware_version(handle), - "uuid": self.get_uuid(handle), + "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), } - # ── External power ─────────────────────────────────────────────────────── - - def set_external_power_control(self, handle: int, state: bool) -> None: - self._call("set_external_power_control", handle=handle, state=state) - - # ── Dynamic commands ───────────────────────────────────────────────────── + async def set_external_power_control(self, handle: int, state: bool) -> None: + await self._call("set_external_power_control", handle=handle, state=state) - def list_commands(self, handle: int) -> list[str]: - return self._call("list_commands", handle=handle)["commands"] + async def list_commands(self, handle: int) -> list[str]: + return (await self._call("list_commands", handle=handle))["commands"] - def get_command(self, handle: int, command_index: int) -> str: - return self._call("get_command", handle=handle, command_index=command_index)["command"] + async def get_command(self, handle: int, command_index: int) -> str: + return (await self._call("get_command", handle=handle, command_index=command_index))["command"] - def list_quick_commands(self, handle: int) -> list[str]: - return self._call("list_quick_commands", handle=handle)["quick_commands"] + async def list_quick_commands(self, handle: int) -> list[str]: + return (await self._call("list_quick_commands", handle=handle))["quick_commands"] - # ── Script variables ───────────────────────────────────────────────────── + async def list_script_variables(self, handle: int) -> list[str]: + return (await self._call("list_script_variables", handle=handle))["script_variables"] - def list_script_variables(self, handle: int) -> list[str]: - return 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) - def update_script_variable(self, handle: int, variable: str, value: str) -> None: - 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"] - # ── Core command interface ──────────────────────────────────────────────── + async def send_command(self, handle: int, command: str, state: bool) -> None: + await self._call("send_command", handle=handle, command=command, state=state) - def get_command_state(self, handle: int, command: str) -> bool: - return self._call("get_command_state", handle=handle, command=command)["state"] + async def get_help_text(self, handle: int) -> str: + return (await self._call("get_help_text", handle=handle))["help_text"] - def send_command(self, handle: int, command: str, state: bool) -> None: - self._call("send_command", handle=handle, command=command, state=state) + async def is_command_queue_clear(self, handle: int) -> bool: + return (await self._call("is_command_queue_clear", handle=handle))["queue_clear"] - # ── Help / queue ───────────────────────────────────────────────────────── + 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) - def get_help_text(self, handle: int) -> str: - return self._call("get_help_text", handle=handle)["help_text"] + async def set_battery_state(self, handle: int, state: bool) -> None: + await self._call("set_battery_state", handle=handle, state=state) - def is_command_queue_clear(self, handle: int) -> bool: - return self._call("is_command_queue_clear", handle=handle)["queue_clear"] + async def get_battery_state(self, handle: int) -> bool: + return (await self._call("get_battery_state", handle=handle))["state"] - # ── Raw pin control ─────────────────────────────────────────────────────── + async def set_usb0(self, handle: int, state: bool) -> None: + await self._call("set_usb0", handle=handle, state=state) - def set_pin_state(self, handle: int, pin: int, state: bool) -> None: - self._call("set_pin_state", handle=handle, pin=pin, state=state) + async def get_usb0_state(self, handle: int) -> bool: + return (await self._call("get_usb0_state", handle=handle))["state"] - # ── Battery ─────────────────────────────────────────────────────────────── + async def set_usb1(self, handle: int, state: bool) -> None: + await self._call("set_usb1", handle=handle, state=state) - def set_battery_state(self, handle: int, state: bool) -> None: - self._call("set_battery_state", handle=handle, state=state) + async def get_usb1_state(self, handle: int) -> bool: + return (await self._call("get_usb1_state", handle=handle))["state"] - def get_battery_state(self, handle: int) -> bool: - return self._call("get_battery_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) - # ── USB ─────────────────────────────────────────────────────────────────── + async def get_power_key_state(self, handle: int) -> bool: + return (await self._call("get_power_key_state", handle=handle))["state"] - def set_usb0(self, handle: int, state: bool) -> None: - self._call("set_usb0", handle=handle, state=state) + async def set_volume_up(self, handle: int, state: bool) -> None: + await self._call("set_volume_up", handle=handle, state=state) - def get_usb0_state(self, handle: int) -> bool: - return self._call("get_usb0_state", handle=handle)["state"] + async def get_volume_up_state(self, handle: int) -> bool: + return (await self._call("get_volume_up_state", handle=handle))["state"] - def set_usb1(self, handle: int, state: bool) -> None: - self._call("set_usb1", handle=handle, state=state) + async def set_volume_down(self, handle: int, state: bool) -> None: + await self._call("set_volume_down", handle=handle, state=state) - def get_usb1_state(self, handle: int) -> bool: - return self._call("get_usb1_state", handle=handle)["state"] + async def get_volume_down_state(self, handle: int) -> bool: + return (await self._call("get_volume_down_state", handle=handle))["state"] - # ── Power key ───────────────────────────────────────────────────────────── + async def set_disconnect_uim1(self, handle: int, state: bool) -> None: + await self._call("set_disconnect_uim1", handle=handle, state=state) - def set_power_key(self, handle: int, state: bool) -> None: - self._call("set_power_key", 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"] - def get_power_key_state(self, handle: int) -> bool: - return self._call("get_power_key_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) - # ── Volume ──────────────────────────────────────────────────────────────── + async def get_disconnect_uim2_state(self, handle: int) -> bool: + return (await self._call("get_disconnect_uim2_state", handle=handle))["state"] - def set_volume_up(self, handle: int, state: bool) -> None: - self._call("set_volume_up", handle=handle, state=state) + async def set_disconnect_sd_card(self, handle: int, state: bool) -> None: + await self._call("set_disconnect_sd_card", handle=handle, state=state) - def get_volume_up_state(self, handle: int) -> bool: - return self._call("get_volume_up_state", handle=handle)["state"] + async def get_disconnect_sd_card_state(self, handle: int) -> bool: + return (await self._call("get_disconnect_sd_card_state", handle=handle))["state"] - def set_volume_down(self, handle: int, state: bool) -> None: - self._call("set_volume_down", handle=handle, state=state) + async def set_primary_edl(self, handle: int, state: bool) -> None: + await self._call("set_primary_edl", handle=handle, state=state) - def get_volume_down_state(self, handle: int) -> bool: - return self._call("get_volume_down_state", handle=handle)["state"] + async def get_primary_edl_state(self, handle: int) -> bool: + return (await self._call("get_primary_edl_state", handle=handle))["state"] - # ── SIM / SD ────────────────────────────────────────────────────────────── + async def set_secondary_edl(self, handle: int, state: bool) -> None: + await self._call("set_secondary_edl", handle=handle, state=state) - def set_disconnect_uim1(self, handle: int, state: bool) -> None: - self._call("set_disconnect_uim1", 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"] - def get_disconnect_uim1_state(self, handle: int) -> bool: - return self._call("get_disconnect_uim1_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) - def set_disconnect_uim2(self, handle: int, state: bool) -> None: - self._call("set_disconnect_uim2", 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"] - def get_disconnect_uim2_state(self, handle: int) -> bool: - return self._call("get_disconnect_uim2_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) - def set_disconnect_sd_card(self, handle: int, state: bool) -> None: - self._call("set_disconnect_sd_card", 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"] - def get_disconnect_sd_card_state(self, handle: int) -> bool: - return self._call("get_disconnect_sd_card_state", handle=handle)["state"] + async def set_eud(self, handle: int, state: bool) -> None: + await self._call("set_eud", handle=handle, state=state) - # ── EDL ─────────────────────────────────────────────────────────────────── + async def get_eud_state(self, handle: int) -> bool: + return (await self._call("get_eud_state", handle=handle))["state"] - def set_primary_edl(self, handle: int, state: bool) -> None: - self._call("set_primary_edl", handle=handle, state=state) + async def set_headset_disconnect(self, handle: int, state: bool) -> None: + await self._call("set_headset_disconnect", handle=handle, state=state) - def get_primary_edl_state(self, handle: int) -> bool: - return self._call("get_primary_edl_state", handle=handle)["state"] + async def get_headset_disconnect_state(self, handle: int) -> bool: + return (await self._call("get_headset_disconnect_state", handle=handle))["state"] - def set_secondary_edl(self, handle: int, state: bool) -> None: - self._call("set_secondary_edl", handle=handle, state=state) + async def set_name(self, handle: int, new_name: str) -> None: + await self._call("set_name", handle=handle, new_name=new_name) - def get_secondary_edl_state(self, handle: int) -> bool: - return self._call("get_secondary_edl_state", handle=handle)["state"] + async def get_reset_count(self, handle: int) -> int: + return (await self._call("get_reset_count", handle=handle))["reset_count"] - # ── PS_HOLD / RESIN_N ──────────────────────────────────────────────────── + async def clear_reset_count(self, handle: int) -> None: + await self._call("clear_reset_count", handle=handle) - def set_force_ps_hold_high(self, handle: int, state: bool) -> None: - self._call("set_force_ps_hold_high", handle=handle, state=state) + async def power_on_button(self, handle: int) -> None: + await self._call("power_on_button", handle=handle) - def get_force_ps_hold_high_state(self, handle: int) -> bool: - return self._call("get_force_ps_hold_high_state", handle=handle)["state"] + async def power_off_button(self, handle: int) -> None: + await self._call("power_off_button", handle=handle) - def set_secondary_pm_resin_n(self, handle: int, state: bool) -> None: - self._call("set_secondary_pm_resin_n", handle=handle, state=state) + async def boot_to_fastboot_button(self, handle: int) -> None: + await self._call("boot_to_fastboot_button", handle=handle) - def get_secondary_pm_resin_n_state(self, handle: int) -> bool: - return self._call("get_secondary_pm_resin_n_state", handle=handle)["state"] + async def boot_to_uefi_menu_button(self, handle: int) -> None: + await self._call("boot_to_uefi_menu_button", handle=handle) - # ── EUD ─────────────────────────────────────────────────────────────────── + async def boot_to_edl_button(self, handle: int) -> None: + await self._call("boot_to_edl_button", handle=handle) - def set_eud(self, handle: int, state: bool) -> None: - self._call("set_eud", handle=handle, state=state) + async def boot_to_secondary_edl_button(self, handle: int) -> None: + await self._call("boot_to_secondary_edl_button", handle=handle) - def get_eud_state(self, handle: int) -> bool: - return self._call("get_eud_state", handle=handle)["state"] - # ── Headset ─────────────────────────────────────────────────────────────── +async def _demo(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()}") - def set_headset_disconnect(self, handle: int, state: bool) -> None: - self._call("set_headset_disconnect", handle=handle, state=state) + devices = await tac.list_devices() + print(f" Available : {devices['available']}") + print(f" In use : {devices['in_use']}") - def get_headset_disconnect_state(self, handle: int) -> bool: - return self._call("get_headset_disconnect_state", handle=handle)["state"] - - # ── Name / reset count ─────────────────────────────────────────────────── - - def set_name(self, handle: int, new_name: str) -> None: - self._call("set_name", handle=handle, new_name=new_name) - - def get_reset_count(self, handle: int) -> int: - return self._call("get_reset_count", handle=handle)["reset_count"] - - def clear_reset_count(self, handle: int) -> None: - self._call("clear_reset_count", handle=handle) - - # ── Button sequences ───────────────────────────────────────────────────── - - def power_on_button(self, handle: int) -> None: - """Execute the power-on button sequence.""" - self._call("power_on_button", handle=handle) - - def power_off_button(self, handle: int) -> None: - """Execute the power-off button sequence.""" - self._call("power_off_button", handle=handle) - - def boot_to_fastboot_button(self, handle: int) -> None: - """Boot the device into Fastboot mode.""" - self._call("boot_to_fastboot_button", handle=handle) - - def boot_to_uefi_menu_button(self, handle: int) -> None: - """Boot the device into the UEFI menu.""" - self._call("boot_to_uefi_menu_button", handle=handle) - - def boot_to_edl_button(self, handle: int) -> None: - """Boot the device into primary EDL mode.""" - self._call("boot_to_edl_button", handle=handle) - - def boot_to_secondary_edl_button(self, handle: int) -> None: - """Boot the device into secondary EDL mode.""" - self._call("boot_to_secondary_edl_button", handle=handle) - - -# --------------------------------------------------------------------------- # -# Quick demo / smoke-test (run as script) -# --------------------------------------------------------------------------- # -def _demo(port_name: str | None = None) -> None: - with TACDevClient() as tac: - print(f" Alpaca version : {tac.get_alpaca_version()}") - print(f" TAC version : {tac.get_tac_version()}") - print(f" Logging : {tac.get_logging_state()}") - - ports = tac.list_ports() - print(f" Devices found : {len(ports)}") - for i, p in enumerate(ports): - print(f" [{i}] {p}") - - if not port_name and ports: - port_name = ports[0] + if not port_name and devices["available"]: + port_name = devices["available"][0]["port"] if port_name: print(f"\nOpening '{port_name}'...") - handle = tac.open_handle_by_description(port_name) - info = tac.get_device_info(handle) + 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" Commands : {tac.list_commands(handle)}") - print(f" Queue clear : {tac.is_command_queue_clear(handle)}") + 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 = commands[0].split(";")[0] + state_before = await tac.get_command_state(handle, cmd) + print(f"Command '{cmd}' state before toggle: {state_before}") + await tac.send_command(handle, cmd, not state_before) + 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) + print(f"Command '{cmd}' restored to : {await tac.get_command_state(handle, cmd)}") - tac.close_tac_handle(handle) - print("Handle closed.") + await tac.close_tac_handle(handle) + print("\nHandle closed.") else: - print("No device available — skipping handle demo.") + print("No device available.") if __name__ == "__main__": - _demo(sys.argv[1] if len(sys.argv) > 1 else None) + asyncio.run(_demo(sys.argv[1] if len(sys.argv) > 1 else None)) diff --git a/examples/MCP/tacdev_mcp_server.py b/examples/MCP/tacdev_mcp_server.py index 9966eb0..80a7889 100644 --- a/examples/MCP/tacdev_mcp_server.py +++ b/examples/MCP/tacdev_mcp_server.py @@ -3,501 +3,579 @@ # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. # SPDX-License-Identifier: BSD-3-Clause -""" -MCP server for TACDev. -Wraps the QTAC TACDev Python library and exposes every function as an MCP tool. +import logging +import logging.handlers +from pathlib import Path -Install the TACDev library first — see README.md for setup instructions. +import yaml +import TACDev +from fastmcp import FastMCP, Context +from fastmcp.server.middleware import Middleware, MiddlewareContext, CallNext -Usage: - python tacdev_mcp_server.py -""" +import mcp.types as mt -import TACDev -from fastmcp import FastMCP +# --------------------------------------------------------------------------- # +# 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() # --------------------------------------------------------------------------- # -# Handle registry -# Bridges MCP integer handles to TACDevice objects returned by the library. +# Logging # --------------------------------------------------------------------------- # -_handles: dict[int, TACDev.TACDevice] = {} -_next_handle: int = 1 +def _setup_logging() -> logging.Logger: + log_cfg = cfg["logging"] + logger = logging.getLogger("tacdev_mcp") + logger.setLevel(log_cfg["level"].upper()) + + handler = logging.handlers.RotatingFileHandler( + log_cfg["file"], + 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() + +# --------------------------------------------------------------------------- # +# Device ownership registry +# --------------------------------------------------------------------------- # +device_ownership: dict[str, str] = {} +session_handles: dict[str, list[int]] = {} +handles: dict[int, TACDev.TACDevice] = {} +_next_handle = 1 -def _register(device: TACDev.TACDevice) -> int: +def _register(device: TACDev.TACDevice, session_id: str) -> int: global _next_handle handle = _next_handle _next_handle += 1 - _handles[handle] = device + handles[handle] = device + session_handles.setdefault(session_id, []).append(handle) return handle def _get_device(handle: int) -> TACDev.TACDevice: - device = _handles.get(handle) + device = handles.get(handle) if device is None: - raise RuntimeError(f"Invalid handle {handle}. Call open_handle_by_description first.") + raise RuntimeError(f"Invalid handle {handle}.") return device +def _release_session(session_id: str) -> None: + for handle in session_handles.pop(session_id, []): + device = handles.pop(handle, None) + if device: + port = next((p for p, s in device_ownership.items() if s == session_id), None) + device.Close() + if port: + del device_ownership[port] + log.info("session=%s handle=%d auto-closed on disconnect", session_id, handle) + + +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 connect middleware +# --------------------------------------------------------------------------- # +class SessionMiddleware(Middleware): + async def on_initialize( + self, + context: MiddlewareContext[mt.InitializeRequest], + call_next: CallNext, + ): + result = await call_next(context) + ctx = context.fastmcp_context + if ctx: + session_id = ctx.session_id + log.info("session=%s connected", session_id) + devices = _device_list() + await ctx.send_notification( + mt.ResourceListChangedNotification( + method="notifications/resources/list_changed" + ) + ) + log.info("session=%s device list pushed: %s", session_id, devices) + return result + + # --------------------------------------------------------------------------- # # MCP server # --------------------------------------------------------------------------- # -mcp = FastMCP("TACDev") +mcp = FastMCP("TACDev", middleware=[SessionMiddleware()]) + +# --------------------------------------------------------------------------- # +# Tools: device list +# --------------------------------------------------------------------------- # +@mcp.tool() +def list_devices() -> dict: + return _device_list() -# ── Diagnostics ────────────────────────────────────────────────────────────── +# --------------------------------------------------------------------------- # +# Tools: diagnostics +# --------------------------------------------------------------------------- # @mcp.tool() def get_alpaca_version() -> dict: - """Return the QTAC (Alpaca) version string.""" return {"alpaca_version": TACDev.AlpacaVersion()} @mcp.tool() def get_tac_version() -> dict: - """Return the TACDev library version string.""" return {"tac_version": TACDev.TACVersion()} -# ── Logging ────────────────────────────────────────────────────────────────── +@mcp.tool() +def get_last_tac_error() -> dict: + return {"last_error": TACDev.GetLastError()} + +# --------------------------------------------------------------------------- # +# Tools: logging +# --------------------------------------------------------------------------- # @mcp.tool() def get_logging_state() -> dict: - """Return whether TACDev logging is currently enabled.""" return {"logging_enabled": TACDev.GetLoggingState()} @mcp.tool() def set_logging_state(enabled: bool) -> dict: - """Enable or disable TACDev logging.""" TACDev.SetLoggingState(enabled) return {"success": True} -# ── Device enumeration ─────────────────────────────────────────────────────── - +# --------------------------------------------------------------------------- # +# Tools: device enumeration +# --------------------------------------------------------------------------- # @mcp.tool() def get_device_count() -> dict: - """Return the number of connected TAC devices.""" return {"device_count": TACDev.GetDeviceCount()} @mcp.tool() def get_port_data(device_index: int) -> dict: - """Return the port description string for the device at device_index.""" device = TACDev.GetDevice(device_index) if device is None: - raise RuntimeError(f"No device found at index {device_index}.") + raise RuntimeError(f"No device at index {device_index}.") return {"port_data": device.PortName(), "description": device.Description(), "serial_number": device.SerialNumber()} -# ── Handle management ──────────────────────────────────────────────────────── - +# --------------------------------------------------------------------------- # +# Tools: handle management +# --------------------------------------------------------------------------- # @mcp.tool() -def open_handle_by_description(port_name: str) -> dict: - """Open a TAC device by its port name and return an integer handle.""" +async def open_handle_by_description(port_name: str, ctx: Context) -> dict: + session_id = ctx.session_id + 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}.") 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"Found device '{port_name}' but failed to open it.") - return {"handle": _register(device)} - raise RuntimeError(f"No device found with port name '{port_name}'. Use get_port_data to list available devices.") + raise RuntimeError(f"Failed to open device '{port_name}'.") + handle = _register(device, 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() -def close_tac_handle(handle: int) -> dict: - """Close a previously opened TAC device handle.""" +async def close_tac_handle(handle: int, ctx: Context) -> dict: + session_id = ctx.session_id device = _get_device(handle) + port = next((p for p, s in device_ownership.items() if s == session_id), None) device.Close() - del _handles[handle] + del handles[handle] + if handle in session_handles.get(session_id, []): + session_handles[session_id].remove(handle) + if port: + del device_ownership[port] + log.info("session=%s closed handle=%d port=%s", session_id, handle, port) return {"success": True} -# ── Device info ────────────────────────────────────────────────────────────── - +# --------------------------------------------------------------------------- # +# Tools: device info +# --------------------------------------------------------------------------- # @mcp.tool() def get_name(handle: int) -> dict: - """Return the human-readable name of the TAC device.""" return {"name": _get_device(handle).Get_Name()} @mcp.tool() def get_firmware_version(handle: int) -> dict: - """Return the firmware version of the TAC device.""" return {"firmware_version": _get_device(handle).GetFirmwareVersion()} @mcp.tool() def get_hardware(handle: int) -> dict: - """Return the hardware description of the TAC device.""" return {"hardware": _get_device(handle).GetHardware()} @mcp.tool() def get_hardware_version(handle: int) -> dict: - """Return the hardware version of the TAC device.""" return {"hardware_version": _get_device(handle).Get_HardwareVersion()} @mcp.tool() def get_uuid(handle: int) -> dict: - """Return the UUID of the TAC device.""" return {"uuid": _get_device(handle).Get_UUID()} -# ── External power ─────────────────────────────────────────────────────────── - +# --------------------------------------------------------------------------- # +# Tools: external power +# --------------------------------------------------------------------------- # @mcp.tool() def set_external_power_control(handle: int, state: bool) -> dict: - """Enable or disable external power control on the TAC device.""" _get_device(handle).SetExternalPowerControl(state) return {"success": True} -# ── Dynamic commands ───────────────────────────────────────────────────────── - +# --------------------------------------------------------------------------- # +# Tools: dynamic commands +# --------------------------------------------------------------------------- # @mcp.tool() def list_commands(handle: int) -> dict: - """Return all dynamic command names available on the device.""" device = _get_device(handle) - count = device.GetCommandCount() - return {"commands": [device.GetCommand(i) for i in range(count)]} + return {"commands": [device.GetCommand(i) for i in range(device.GetCommandCount())]} @mcp.tool() def get_command(handle: int, command_index: int) -> dict: - """Return the dynamic command name at command_index.""" return {"command": _get_device(handle).GetCommand(command_index)} -# ── Quick commands ─────────────────────────────────────────────────────────── - @mcp.tool() def list_quick_commands(handle: int) -> dict: - """Return all quick command names available on the device.""" device = _get_device(handle) - count = device.GetQuickCommandCount() - return {"quick_commands": [device.GetQuickCommand(i) for i in range(count)]} - + return {"quick_commands": [device.GetQuickCommand(i) for i in range(device.GetQuickCommandCount())]} -# ── Script variables ───────────────────────────────────────────────────────── +# --------------------------------------------------------------------------- # +# Tools: script variables +# --------------------------------------------------------------------------- # @mcp.tool() def list_script_variables(handle: int) -> dict: - """Return all script variable names for the device.""" device = _get_device(handle) - count = device.GetScriptVariableCount() - return {"script_variables": [device.GetScriptVariable(i) for i in range(count)]} + 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: - """Set the value of a named script variable on the device.""" _get_device(handle).UpdateScriptVariableValue(variable, value) return {"success": True} -# ── Core command interface ─────────────────────────────────────────────────── - +# --------------------------------------------------------------------------- # +# Tools: core command interface +# --------------------------------------------------------------------------- # @mcp.tool() def get_command_state(handle: int, command: str) -> dict: - """Return the current boolean state of a named command on the device.""" return {"command": command, "state": _get_device(handle).GetCommandState(command)} @mcp.tool() def send_command(handle: int, command: str, state: bool) -> dict: - """Send a named command with a boolean state to the device.""" _get_device(handle).SendCommand(command, state) return {"success": True} -# ── Help ───────────────────────────────────────────────────────────────────── - +# --------------------------------------------------------------------------- # +# Tools: help / queue +# --------------------------------------------------------------------------- # @mcp.tool() def get_help_text(handle: int) -> dict: - """Return the full help text listing all commands supported by the device.""" return {"help_text": _get_device(handle).GetHelpText()} -# ── Raw pin control ────────────────────────────────────────────────────────── +@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: - """Set the raw boolean state of a hardware pin on the device.""" _get_device(handle).SetPin(pin, state) return {"success": True} -# ── Command queue ──────────────────────────────────────────────────────────── - -@mcp.tool() -def is_command_queue_clear(handle: int) -> dict: - """Return True if the device's command queue is empty.""" - return {"queue_clear": _get_device(handle).IsCommandQueueClear()} - - -# ── Power / peripheral boolean controls ───────────────────────────────────── - +# --------------------------------------------------------------------------- # +# Tools: battery +# --------------------------------------------------------------------------- # @mcp.tool() def set_battery_state(handle: int, state: bool) -> dict: - """Set the battery connection state (True = connected).""" _get_device(handle).SetBatteryState(state) return {"success": True} @mcp.tool() def get_battery_state(handle: int) -> dict: - """Return the current battery connection state.""" return {"state": _get_device(handle).GetBatteryState()} +# --------------------------------------------------------------------------- # +# Tools: USB +# --------------------------------------------------------------------------- # @mcp.tool() def set_usb0(handle: int, state: bool) -> dict: - """Set the USB0 connection state (True = connected).""" _get_device(handle).Usb0(state) return {"success": True} @mcp.tool() def get_usb0_state(handle: int) -> dict: - """Return the current USB0 connection state.""" return {"state": _get_device(handle).GetUsb0State()} @mcp.tool() def set_usb1(handle: int, state: bool) -> dict: - """Set the USB1 connection state (True = connected).""" _get_device(handle).Usb1(state) return {"success": True} @mcp.tool() def get_usb1_state(handle: int) -> dict: - """Return the current USB1 connection state.""" return {"state": _get_device(handle).GetUsb1State()} +# --------------------------------------------------------------------------- # +# Tools: power key +# --------------------------------------------------------------------------- # @mcp.tool() def set_power_key(handle: int, state: bool) -> dict: - """Set the power key state (True = pressed).""" _get_device(handle).PowerKey(state) return {"success": True} @mcp.tool() def get_power_key_state(handle: int) -> dict: - """Return the current power key state.""" return {"state": _get_device(handle).GetPowerKeyState()} +# --------------------------------------------------------------------------- # +# Tools: volume +# --------------------------------------------------------------------------- # @mcp.tool() def set_volume_up(handle: int, state: bool) -> dict: - """Set the volume-up button state (True = pressed).""" _get_device(handle).VolumeUp(state) return {"success": True} @mcp.tool() def get_volume_up_state(handle: int) -> dict: - """Return the current volume-up button state.""" return {"state": _get_device(handle).GetVolumeUpState()} @mcp.tool() def set_volume_down(handle: int, state: bool) -> dict: - """Set the volume-down button state (True = pressed).""" _get_device(handle).VolumeDown(state) return {"success": True} @mcp.tool() def get_volume_down_state(handle: int) -> dict: - """Return the current volume-down button state.""" return {"state": _get_device(handle).GetVolumeDownState()} +# --------------------------------------------------------------------------- # +# Tools: SIM / SD +# --------------------------------------------------------------------------- # @mcp.tool() def set_disconnect_uim1(handle: int, state: bool) -> dict: - """Set the UIM1 disconnect state (True = disconnected).""" _get_device(handle).DisconnectUIM1(state) return {"success": True} @mcp.tool() def get_disconnect_uim1_state(handle: int) -> dict: - """Return the current UIM1 disconnect state.""" return {"state": _get_device(handle).GetDisconnectUIM1State()} @mcp.tool() def set_disconnect_uim2(handle: int, state: bool) -> dict: - """Set the UIM2 disconnect state (True = disconnected).""" _get_device(handle).DisconnectUIM2(state) return {"success": True} @mcp.tool() def get_disconnect_uim2_state(handle: int) -> dict: - """Return the current UIM2 disconnect state.""" return {"state": _get_device(handle).GetDisconnectUIM2State()} @mcp.tool() def set_disconnect_sd_card(handle: int, state: bool) -> dict: - """Set the SD card disconnect state (True = disconnected).""" _get_device(handle).DisconnectSDCard(state) return {"success": True} @mcp.tool() def get_disconnect_sd_card_state(handle: int) -> dict: - """Return the current SD card disconnect state.""" return {"state": _get_device(handle).GetDisconnectSDCardState()} +# --------------------------------------------------------------------------- # +# Tools: EDL +# --------------------------------------------------------------------------- # @mcp.tool() def set_primary_edl(handle: int, state: bool) -> dict: - """Set the primary EDL (Emergency Download Mode) state.""" _get_device(handle).PrimaryEDL(state) return {"success": True} @mcp.tool() def get_primary_edl_state(handle: int) -> dict: - """Return the current primary EDL state.""" return {"state": _get_device(handle).GetPrimaryEDLState()} @mcp.tool() def set_secondary_edl(handle: int, state: bool) -> dict: - """Set the secondary EDL state.""" _get_device(handle).SecondaryEDL(state) return {"success": True} @mcp.tool() def get_secondary_edl_state(handle: int) -> dict: - """Return the current secondary EDL state.""" return {"state": _get_device(handle).GetSecondaryEDLState()} +# --------------------------------------------------------------------------- # +# Tools: PS_HOLD / RESIN_N +# --------------------------------------------------------------------------- # @mcp.tool() def set_force_ps_hold_high(handle: int, state: bool) -> dict: - """Force PS_HOLD high (True = forced high).""" _get_device(handle).ForcePSHoldHigh(state) return {"success": True} @mcp.tool() def get_force_ps_hold_high_state(handle: int) -> dict: - """Return the current force PS_HOLD high state.""" return {"state": _get_device(handle).GetForcePSHoldHighState()} @mcp.tool() def set_secondary_pm_resin_n(handle: int, state: bool) -> dict: - """Set the secondary PM RESIN_N state.""" _get_device(handle).SecondaryPM_RESIN_N(state) return {"success": True} @mcp.tool() def get_secondary_pm_resin_n_state(handle: int) -> dict: - """Return the current secondary PM RESIN_N state.""" return {"state": _get_device(handle).GetSecondaryPM_RESIN_NState()} +# --------------------------------------------------------------------------- # +# Tools: EUD +# --------------------------------------------------------------------------- # @mcp.tool() def set_eud(handle: int, state: bool) -> dict: - """Set the EUD (Enhanced USB Debugging) state.""" _get_device(handle).Eud(state) return {"success": True} @mcp.tool() def get_eud_state(handle: int) -> dict: - """Return the current EUD state.""" return {"state": _get_device(handle).GetEUDState()} +# --------------------------------------------------------------------------- # +# Tools: headset +# --------------------------------------------------------------------------- # @mcp.tool() def set_headset_disconnect(handle: int, state: bool) -> dict: - """Set the headset disconnect state (True = disconnected).""" _get_device(handle).HeadsetDisconnect(state) return {"success": True} @mcp.tool() def get_headset_disconnect_state(handle: int) -> dict: - """Return the current headset disconnect state.""" return {"state": _get_device(handle).GetHeadsetDisconnectState()} -# ── Name / reset count ─────────────────────────────────────────────────────── - +# --------------------------------------------------------------------------- # +# Tools: name / reset count +# --------------------------------------------------------------------------- # @mcp.tool() def set_name(handle: int, new_name: str) -> dict: - """Set a new human-readable name on the TAC device (persisted in firmware).""" _get_device(handle).SetName(new_name) return {"success": True} @mcp.tool() def get_reset_count(handle: int) -> dict: - """Return the number of resets recorded by the TAC device.""" return {"reset_count": _get_device(handle).GetResetCount()} @mcp.tool() def clear_reset_count(handle: int) -> dict: - """Clear (zero) the reset counter on the TAC device.""" _get_device(handle).ClearResetCount() return {"success": True} -# ── Button sequences ───────────────────────────────────────────────────────── - +# --------------------------------------------------------------------------- # +# Tools: button sequences +# --------------------------------------------------------------------------- # @mcp.tool() def power_on_button(handle: int) -> dict: - """Execute the power-on button sequence on the device.""" _get_device(handle).PowerOnButton() return {"success": True} @mcp.tool() def power_off_button(handle: int) -> dict: - """Execute the power-off button sequence on the device.""" _get_device(handle).PowerOffButton() return {"success": True} @mcp.tool() def boot_to_fastboot_button(handle: int) -> dict: - """Execute the button sequence to boot the device into Fastboot mode.""" _get_device(handle).BootToFastBootButton() return {"success": True} @mcp.tool() def boot_to_uefi_menu_button(handle: int) -> dict: - """Execute the button sequence to enter the UEFI menu.""" _get_device(handle).BootToUEFIMenuButton() return {"success": True} @mcp.tool() def boot_to_edl_button(handle: int) -> dict: - """Execute the button sequence to boot the device into primary EDL mode.""" _get_device(handle).BootToEDLButton() return {"success": True} @mcp.tool() def boot_to_secondary_edl_button(handle: int) -> dict: - """Execute the button sequence to boot the device into secondary EDL mode.""" _get_device(handle).BootToSecondaryEDLButton() return {"success": True} @@ -506,4 +584,15 @@ def boot_to_secondary_edl_button(handle: int) -> dict: # Entry point # --------------------------------------------------------------------------- # if __name__ == "__main__": - mcp.run() + server_cfg = cfg["server"] + log.info("Starting TACDev MCP server on %s:%d", server_cfg["host"], server_cfg["port"]) + try: + mcp.run( + transport="sse", + host=server_cfg["host"], + port=server_cfg["port"], + ) + except KeyboardInterrupt: + pass + finally: + log.info("Server stopped.") From bae7a28bafa92d9fc7d5915f7f416cae63cd6636 Mon Sep 17 00:00:00 2001 From: "Roy, Biswajit" Date: Thu, 25 Jun 2026 19:13:31 +0530 Subject: [PATCH 10/17] update setup scripts --- examples/MCP/README.md | 94 ++++++++++++------------------------------ examples/MCP/setup.bat | 40 ++++++++++++++---- examples/MCP/setup.sh | 13 +++--- 3 files changed, 66 insertions(+), 81 deletions(-) diff --git a/examples/MCP/README.md b/examples/MCP/README.md index eabf3fb..6a88c12 100644 --- a/examples/MCP/README.md +++ b/examples/MCP/README.md @@ -52,22 +52,21 @@ sequenceDiagram ## Prerequisites -- Python 3.8+ (64-bit, matching your build architecture: x64 or ARM64) -- Project built with `build.bat` / `build.sh` (the TACDev library is loaded from `__Builds`) +| Requirement | Details | +| :-- | :-- | +| Python | 3.10+ matching your target architecture (x64 Python on x64, ARM64 Python on ARM64) | +| OpenSSL | Required to build the `cryptography` package (a fastmcp dependency). Install the version matching your architecture from [slproweb.com](https://slproweb.com/products/Win32OpenSSL.html) or via package manager on Linux. On ARM64 Windows, also set `OPENSSL_DIR` to the install path before running `setup.bat ARM64` - the ARM64 wheel must be compiled from source | +| QTAC build | Project must be built with `build.bat` / `build.sh` - TACDev library is loaded from `__Builds` | ## Setup -Run from the `examples/MCP` directory: - -```cmd -setup.bat (Windows x64, auto-detected) -setup.bat ARM64 (Windows ARM64) -``` -```bash -./setup.sh (Linux) -``` +| Platform | Command | +| :-- | :-- | +| Windows x64 (auto-detected) | `setup.bat` | +| Windows ARM64 | `setup.bat ARM64` | +| Linux | `./setup.sh` | -Each script builds the project, installs the TACDev Python library, and installs MCP dependencies. +Each script checks for an existing build, installs the TACDev Python library, and installs MCP dependencies. For full setup guidance see [Bootcamp guide](../../docs/bootcamp/01-Bootcamp.md) and [Python API reference](../../docs/bootcamp/02-Python-API.md). @@ -76,17 +75,14 @@ For full setup guidance see [Bootcamp guide](../../docs/bootcamp/01-Bootcamp.md) All runtime parameters are in `config.yaml`: -```yaml -server: - host: "127.0.0.1" - port: 8000 - -logging: - file: "tacdev_mcp.log" - level: "INFO" - max_bytes: 10485760 - backup_count: 5 -``` +| Parameter | Default | Description | +| :-- | :-- | :-- | +| `server.host` | `127.0.0.1` | Host to bind the server on | +| `server.port` | `8000` | Port the server listens on | +| `logging.file` | `tacdev_mcp.log` | Log file path | +| `logging.level` | `INFO` | Log level: DEBUG, INFO, WARNING, ERROR | +| `logging.max_bytes` | `10485760` | Max log file size before rotation (10 MB) | +| `logging.backup_count` | `5` | Number of rotated backup files to retain | ## Usage @@ -119,51 +115,15 @@ async def main(): asyncio.run(main()) ``` -## Available tools - -| Category | Tools | -| :-- | :-- | -| Devices | `list_devices` | -| Diagnostics | `get_alpaca_version`, `get_tac_version`, `get_last_tac_error` | -| Logging | `get_logging_state`, `set_logging_state` | -| Device enumeration | `get_device_count`, `get_port_data` | -| Handle management | `open_handle_by_description`, `close_tac_handle` | -| Device info | `get_name`, `get_firmware_version`, `get_hardware`, `get_hardware_version`, `get_uuid` | -| External power | `set_external_power_control` | -| Dynamic commands | `list_commands`, `get_command`, `list_quick_commands` | -| Script variables | `list_script_variables`, `update_script_variable` | -| Command interface | `get_command_state`, `send_command` | -| Help / queue | `get_help_text`, `is_command_queue_clear` | -| Raw pin | `set_pin_state` | -| Battery | `set_battery_state`, `get_battery_state` | -| USB | `set_usb0`, `get_usb0_state`, `set_usb1`, `get_usb1_state` | -| Buttons | `set_power_key`, `get_power_key_state`, `set_volume_up`, `get_volume_up_state`, `set_volume_down`, `get_volume_down_state` | -| SIM / SD | `set_disconnect_uim1`, `get_disconnect_uim1_state`, `set_disconnect_uim2`, `get_disconnect_uim2_state`, `set_disconnect_sd_card`, `get_disconnect_sd_card_state` | -| EDL | `set_primary_edl`, `get_primary_edl_state`, `set_secondary_edl`, `get_secondary_edl_state` | -| PS_HOLD / RESIN | `set_force_ps_hold_high`, `get_force_ps_hold_high_state`, `set_secondary_pm_resin_n`, `get_secondary_pm_resin_n_state` | -| EUD | `set_eud`, `get_eud_state` | -| Headset | `set_headset_disconnect`, `get_headset_disconnect_state` | -| Device name / resets | `set_name`, `get_reset_count`, `clear_reset_count` | -| Button sequences | `power_on_button`, `power_off_button`, `boot_to_fastboot_button`, `boot_to_uefi_menu_button`, `boot_to_edl_button`, `boot_to_secondary_edl_button` | - ## Troubleshooting -**`ModuleNotFoundError: No module named 'TACDev'`** -Run `pip install interfaces/Python` from the repo root, or use `setup.bat` / `setup.sh`. - -**`TACDev library not found`** -Build the project first with `build.bat` / `build.sh` and run scripts from the repo root. - -**`Architecture mismatch`** -Use a 64-bit Python build matching your target architecture (x64 or ARM64). - -**`get_device_count` returns 0** -Check the debug board is connected. On Windows, look for Unknown Device in Device Manager and -run `FTDICheck.exe` from `__Builds\x64\Release\bin` to diagnose the FTDI connection. -On Linux, ensure udev rules are installed: -```bash -sudo cp udev-rules/99-QTAC-USB.rules /etc/udev/rules.d/ -sudo udevadm control --reload -``` +| Error | Fix | +| :-- | :-- | +| `ModuleNotFoundError: No module named 'TACDev'` | Run `pip install interfaces/Python` from the repo root, or use `setup.bat` / `setup.sh` | +| `TACDev library not found` | Build the project first with `build.bat` / `build.sh` and run from repo root | +| `Architecture mismatch` | Use a 64-bit Python matching your target architecture (x64 or ARM64) | +| `ModuleNotFoundError: No module named 'fastmcp'` | Requires Python 3.10+. Run `pip install -r requirements.txt` | +| `cryptography` build failure on ARM64 | Set `OPENSSL_DIR` to the ARM64 OpenSSL install path before running `setup.bat ARM64`. Download the ARM64 OpenSSL installer from [slproweb.com](https://slproweb.com/products/Win32OpenSSL.html), then: `setx OPENSSL_DIR "C:\Program Files\OpenSSL-ARM64"` and reopen the prompt | +| `get_device_count` returns 0 | Check debug board is connected. On Windows run `FTDICheck.exe` from `__Builds\x64\Release\bin`. On Linux ensure udev rules are installed: `sudo cp udev-rules/99-QTAC-USB.rules /etc/udev/rules.d/ && sudo udevadm control --reload` | See [Bootcamp troubleshooting](../../docs/bootcamp/01-Bootcamp.md#troubleshooting) for more. diff --git a/examples/MCP/setup.bat b/examples/MCP/setup.bat index a18feec..b6ab74c 100644 --- a/examples/MCP/setup.bat +++ b/examples/MCP/setup.bat @@ -3,27 +3,51 @@ @echo off -@REM Resolve repo root (two levels up from examples\MCP) set SCRIPT_DIR=%~dp0 pushd "%SCRIPT_DIR%\..\.." set REPO_ROOT=%CD% popd -@REM Step 1: build the project -echo [1/3] Building QTAC... -call "%REPO_ROOT%\build.bat" %1 -if errorlevel 1 exit /b 1 +set ARCH=%1 +if "%ARCH%"=="" set ARCH=x64 + +@REM On ARM64, the cryptography package (required by fastmcp) must be compiled +@REM from source. This requires OpenSSL development headers. Check early. +if /i "%ARCH%"=="ARM64" ( + if "%OPENSSL_DIR%"=="" ( + echo. + echo ERROR: OPENSSL_DIR is not set. + echo ARM64 builds require OpenSSL development headers to compile the + echo cryptography package. Install OpenSSL ARM64 and set OPENSSL_DIR: + echo. + echo 1. Download the ARM64 OpenSSL installer from https://slproweb.com/products/Win32OpenSSL.html + echo 2. Install it, then run: + echo setx OPENSSL_DIR "C:\Program Files\OpenSSL-ARM64" + echo 3. Open a new command prompt and re-run setup.bat ARM64 + echo. + exit /b 1 + ) + echo OpenSSL : %OPENSSL_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" %ARCH% + if errorlevel 1 exit /b 1 +) else ( + echo [1/3] Build found at %BUILD_DIR%, skipping recompile. +) -@REM Step 2: install TACDev Python library echo [2/3] Installing TACDev Python library... pip install "%REPO_ROOT%\interfaces\Python" if errorlevel 1 exit /b 1 -@REM Step 3: install MCP dependencies 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 MCP server from the repo root: +echo Setup complete. Start the MCP server: echo python examples\MCP\tacdev_mcp_server.py diff --git a/examples/MCP/setup.sh b/examples/MCP/setup.sh index 1019557..0326745 100644 --- a/examples/MCP/setup.sh +++ b/examples/MCP/setup.sh @@ -8,18 +8,19 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -# Step 1: build the project -echo "[1/3] Building QTAC..." -"$REPO_ROOT/build.sh" +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 -# Step 2: install TACDev Python library echo "[2/3] Installing TACDev Python library..." pip install "$REPO_ROOT/interfaces/Python" -# Step 3: install MCP dependencies echo "[3/3] Installing MCP dependencies..." pip install -r "$SCRIPT_DIR/requirements.txt" echo "" -echo "Setup complete. Run the MCP server from the repo root:" +echo "Setup complete. Start the MCP server:" echo " python examples/MCP/tacdev_mcp_server.py" From ad616c1b494bef8ee3a45d36c9906b3cf2947d30 Mon Sep 17 00:00:00 2001 From: "Roy, Biswajit" Date: Thu, 25 Jun 2026 19:35:45 +0530 Subject: [PATCH 11/17] improve mcp automation script --- examples/MCP/README.md | 13 ++++----- examples/MCP/setup.bat | 60 +++++++++++++++++++++++++++++++++++------- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/examples/MCP/README.md b/examples/MCP/README.md index 6a88c12..3727a93 100644 --- a/examples/MCP/README.md +++ b/examples/MCP/README.md @@ -55,7 +55,6 @@ sequenceDiagram | Requirement | Details | | :-- | :-- | | Python | 3.10+ matching your target architecture (x64 Python on x64, ARM64 Python on ARM64) | -| OpenSSL | Required to build the `cryptography` package (a fastmcp dependency). Install the version matching your architecture from [slproweb.com](https://slproweb.com/products/Win32OpenSSL.html) or via package manager on Linux. On ARM64 Windows, also set `OPENSSL_DIR` to the install path before running `setup.bat ARM64` - the ARM64 wheel must be compiled from source | | QTAC build | Project must be built with `build.bat` / `build.sh` - TACDev library is loaded from `__Builds` | ## Setup @@ -63,10 +62,12 @@ sequenceDiagram | Platform | Command | | :-- | :-- | | Windows x64 (auto-detected) | `setup.bat` | -| Windows ARM64 | `setup.bat ARM64` | +| Windows ARM64 (auto-detected) | `setup.bat` | | Linux | `./setup.sh` | -Each script checks for an existing build, installs the TACDev Python library, and installs MCP dependencies. +Each script auto-detects architecture, checks for an existing build, installs the TACDev Python +library, and installs MCP dependencies. On ARM64 Windows, OpenSSL is downloaded and installed +automatically if not already present. For full setup guidance see [Bootcamp guide](../../docs/bootcamp/01-Bootcamp.md) and [Python API reference](../../docs/bootcamp/02-Python-API.md). @@ -121,9 +122,9 @@ asyncio.run(main()) | :-- | :-- | | `ModuleNotFoundError: No module named 'TACDev'` | Run `pip install interfaces/Python` from the repo root, or use `setup.bat` / `setup.sh` | | `TACDev library not found` | Build the project first with `build.bat` / `build.sh` and run from repo root | -| `Architecture mismatch` | Use a 64-bit Python matching your target architecture (x64 or ARM64) | +| `Architecture mismatch` | Use Python matching your target architecture (x64 or ARM64) | | `ModuleNotFoundError: No module named 'fastmcp'` | Requires Python 3.10+. Run `pip install -r requirements.txt` | -| `cryptography` build failure on ARM64 | Set `OPENSSL_DIR` to the ARM64 OpenSSL install path before running `setup.bat ARM64`. Download the ARM64 OpenSSL installer from [slproweb.com](https://slproweb.com/products/Win32OpenSSL.html), then: `setx OPENSSL_DIR "C:\Program Files\OpenSSL-ARM64"` and reopen the prompt | -| `get_device_count` returns 0 | Check debug board is connected. On Windows run `FTDICheck.exe` from `__Builds\x64\Release\bin`. On Linux ensure udev rules are installed: `sudo cp udev-rules/99-QTAC-USB.rules /etc/udev/rules.d/ && sudo udevadm control --reload` | +| `cryptography` build failure on ARM64 | Re-run `setup.bat` - it downloads and installs OpenSSL automatically. If it still fails, install manually from [slproweb.com](https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi) (full installer, not Light) and set `OPENSSL_DIR=C:\Program Files\OpenSSL-Win64-ARM` | +| `get_device_count` returns 0 | Check debug board is connected. On Windows run `FTDICheck.exe` from `__Builds\x64\Release\bin`. On Linux: `sudo cp udev-rules/99-QTAC-USB.rules /etc/udev/rules.d/ && sudo udevadm control --reload` | See [Bootcamp troubleshooting](../../docs/bootcamp/01-Bootcamp.md#troubleshooting) for more. diff --git a/examples/MCP/setup.bat b/examples/MCP/setup.bat index b6ab74c..64baed5 100644 --- a/examples/MCP/setup.bat +++ b/examples/MCP/setup.bat @@ -9,25 +9,67 @@ set REPO_ROOT=%CD% popd set ARCH=%1 -if "%ARCH%"=="" set ARCH=x64 +if "%ARCH%"=="" ( + if "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( + set ARCH=ARM64 + ) else ( + set ARCH=x64 + ) +) @REM On ARM64, the cryptography package (required by fastmcp) must be compiled @REM from source. This requires OpenSSL development headers. Check early. if /i "%ARCH%"=="ARM64" ( - if "%OPENSSL_DIR%"=="" ( + if "%OPENSSL_DIR%"=="" set OPENSSL_DIR=C:\Program Files\OpenSSL-Win64-ARM + + if not exist "%OPENSSL_DIR%\include" ( + echo OpenSSL not found. Downloading and installing... + set OPENSSL_MSI=%TEMP%\Win64ARMOpenSSL-4_0_1.msi + curl -L -o "%OPENSSL_MSI%" https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi + if errorlevel 1 ( + echo. + echo ERROR: Failed to download OpenSSL installer. + echo Check your internet connection and try again, or download manually: + echo https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi + echo Install it, then re-run setup.bat + echo. + exit /b 1 + ) + msiexec /i "%OPENSSL_MSI%" /quiet /norestart INSTALLDIR="%OPENSSL_DIR%" + if errorlevel 1 ( + echo. + echo ERROR: OpenSSL installation failed. + echo Try installing 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 not exist "%OPENSSL_DIR%\include" ( echo. - echo ERROR: OPENSSL_DIR is not set. - echo ARM64 builds require OpenSSL development headers to compile the - echo cryptography package. Install OpenSSL ARM64 and set OPENSSL_DIR: + echo ERROR: OpenSSL headers still not found at %OPENSSL_DIR%\include + echo Installation may have used a different path. + echo Set OPENSSL_DIR manually and re-run: + echo setx OPENSSL_DIR "C:\Program Files\OpenSSL-Win64-ARM" echo. - echo 1. Download the ARM64 OpenSSL installer from https://slproweb.com/products/Win32OpenSSL.html - echo 2. Install it, then run: - echo setx OPENSSL_DIR "C:\Program Files\OpenSSL-ARM64" - echo 3. Open a new command prompt and re-run setup.bat ARM64 + exit /b 1 + ) + + set OPENSSL_LIB_DIR=%OPENSSL_DIR%\lib\VC\arm64\MD + if not exist "%OPENSSL_LIB_DIR%" ( + echo. + echo ERROR: OpenSSL lib directory not found at %OPENSSL_LIB_DIR% + echo Expected layout from the full ARM64 installer. Try reinstalling from: + echo https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi echo. exit /b 1 ) + echo OpenSSL : %OPENSSL_DIR% [OK] + echo OpenSSL libs : %OPENSSL_LIB_DIR% [OK] ) set BUILD_DIR=%REPO_ROOT%\__Builds From 9316bfabb6b08dcb52fe47d716444b330e4115a3 Mon Sep 17 00:00:00 2001 From: "Roy, Biswajit" Date: Thu, 25 Jun 2026 19:45:38 +0530 Subject: [PATCH 12/17] update setup.bat --- examples/MCP/setup.bat | 78 ++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 44 deletions(-) diff --git a/examples/MCP/setup.bat b/examples/MCP/setup.bat index 64baed5..ce51533 100644 --- a/examples/MCP/setup.bat +++ b/examples/MCP/setup.bat @@ -2,6 +2,7 @@ @REM SPDX-License-Identifier: BSD-3-Clause @echo off +setlocal enabledelayedexpansion set SCRIPT_DIR=%~dp0 pushd "%SCRIPT_DIR%\..\.." @@ -10,70 +11,59 @@ popd set ARCH=%1 if "%ARCH%"=="" ( - if "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( - set ARCH=ARM64 - ) else ( - set ARCH=x64 - ) + if "%PROCESSOR_ARCHITECTURE%"=="ARM64" (set ARCH=ARM64) else (set ARCH=x64) ) -@REM On ARM64, the cryptography package (required by fastmcp) must be compiled -@REM from source. This requires OpenSSL development headers. Check early. if /i "%ARCH%"=="ARM64" ( if "%OPENSSL_DIR%"=="" set OPENSSL_DIR=C:\Program Files\OpenSSL-Win64-ARM +) - if not exist "%OPENSSL_DIR%\include" ( - echo OpenSSL not found. Downloading and installing... - set OPENSSL_MSI=%TEMP%\Win64ARMOpenSSL-4_0_1.msi - curl -L -o "%OPENSSL_MSI%" https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi - if errorlevel 1 ( - echo. - echo ERROR: Failed to download OpenSSL installer. - echo Check your internet connection and try again, or download manually: - echo https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi - echo Install it, then re-run setup.bat - echo. - exit /b 1 - ) - msiexec /i "%OPENSSL_MSI%" /quiet /norestart INSTALLDIR="%OPENSSL_DIR%" - if errorlevel 1 ( - echo. - echo ERROR: OpenSSL installation failed. - echo Try installing 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 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 not exist "%OPENSSL_DIR%\include" ( +if /i "%ARCH%"=="ARM64" ( + if not exist "!OPENSSL_DIR!\include" ( echo. - echo ERROR: OpenSSL headers still not found at %OPENSSL_DIR%\include - echo Installation may have used a different path. - echo Set OPENSSL_DIR manually and re-run: + 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 ) - - set OPENSSL_LIB_DIR=%OPENSSL_DIR%\lib\VC\arm64\MD - if not exist "%OPENSSL_LIB_DIR%" ( + if not exist "!OPENSSL_DIR!\lib\VC\arm64\MD" ( echo. - echo ERROR: OpenSSL lib directory not found at %OPENSSL_LIB_DIR% - echo Expected layout from the full ARM64 installer. Try reinstalling from: + 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 ) - - echo OpenSSL : %OPENSSL_DIR% [OK] - echo OpenSSL libs : %OPENSSL_LIB_DIR% [OK] + 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" %ARCH% From a7f3480a5f95ee6601c2ee1699a0fe24fe78dd88 Mon Sep 17 00:00:00 2001 From: "Roy, Biswajit" Date: Mon, 29 Jun 2026 21:21:51 +0530 Subject: [PATCH 13/17] mcp stdio example Signed-off-by: Roy, Biswajit --- examples/MCP/stdio/README.md | 68 ++++ examples/MCP/stdio/requirements.txt | 1 + examples/MCP/stdio/setup.bat | 85 +++++ examples/MCP/stdio/setup.sh | 26 ++ examples/MCP/stdio/tacdev_mcp_client.py | 316 +++++++++++++++ examples/MCP/stdio/tacdev_mcp_server.py | 485 ++++++++++++++++++++++++ 6 files changed, 981 insertions(+) create mode 100644 examples/MCP/stdio/README.md create mode 100644 examples/MCP/stdio/requirements.txt create mode 100644 examples/MCP/stdio/setup.bat create mode 100644 examples/MCP/stdio/setup.sh create mode 100644 examples/MCP/stdio/tacdev_mcp_client.py create mode 100644 examples/MCP/stdio/tacdev_mcp_server.py diff --git a/examples/MCP/stdio/README.md b/examples/MCP/stdio/README.md new file mode 100644 index 0000000..9910aca --- /dev/null +++ b/examples/MCP/stdio/README.md @@ -0,0 +1,68 @@ +# QTAC MCP Server — stdio + +Server is spawned automatically per-client. No standalone process to manage. +Use this variant for Claude CLI integration. + +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 + +```bat +setup.bat # Windows (auto-detects x64/ARM64) +``` +```bash +./setup.sh # Linux +``` + +## Claude CLI + +**Register the server** (run once from repo root): + +```bash +claude mcp add TACDev-MCP -- python examples/MCP/stdio/tacdev_mcp_server.py +``` + +**Verify:** + +```bash +claude mcp list +``` + +**Then just ask Claude naturally:** + +``` +List connected devices +Power on the device on COM41 +Boot COM3 to fastboot +``` + +**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 | + +## Client demo + +```bash +python examples/MCP/stdio/tacdev_mcp_client.py # first available device +python examples/MCP/stdio/tacdev_mcp_client.py COM41 # specific port +``` + +## 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..fdb2da4 --- /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" %ARCH% + 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..7d643b2 --- /dev/null +++ b/examples/MCP/stdio/tacdev_mcp_client.py @@ -0,0 +1,316 @@ +#!/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") + + +async def _call(client: Client, tool: str, **kwargs: Any) -> Any: + result = await 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 + + +class TACDevClient: + def __init__(self) -> None: + self._client = Client({"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: + return await _call(self._client, tool, **kwargs) + + 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 _demo(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 = commands[0].split(";")[0] + state_before = await tac.get_command_state(handle, cmd) + print(f"Command '{cmd}' state before toggle: {state_before}") + await tac.send_command(handle, cmd, not state_before) + 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) + 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(_demo(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..e594372 --- /dev/null +++ b/examples/MCP/stdio/tacdev_mcp_server.py @@ -0,0 +1,485 @@ +#!/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 + + +# --------------------------------------------------------------------------- # +# 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__": + mcp.run(transport="stdio") From ee58794f6015bb4c8602a8586b9e28a91c44439a Mon Sep 17 00:00:00 2001 From: "Roy, Biswajit" Date: Mon, 29 Jun 2026 23:05:55 +0530 Subject: [PATCH 14/17] updated mcp sse example Signed-off-by: Roy, Biswajit --- examples/MCP/README.md | 130 -------- examples/MCP/sse/README.md | 104 +++++++ examples/MCP/{ => sse}/config.yaml | 0 examples/MCP/{ => sse}/requirements.txt | 0 examples/MCP/{ => sse}/setup.bat | 4 +- examples/MCP/{ => sse}/setup.sh | 4 +- examples/MCP/{ => sse}/tacdev_mcp_client.py | 32 +- examples/MCP/{ => sse}/tacdev_mcp_server.py | 328 ++++++++++---------- examples/MCP/stdio/README.md | 43 ++- examples/MCP/stdio/tacdev_mcp_client.py | 34 +- examples/MCP/stdio/tacdev_mcp_server.py | 15 +- 11 files changed, 353 insertions(+), 341 deletions(-) delete mode 100644 examples/MCP/README.md create mode 100644 examples/MCP/sse/README.md rename examples/MCP/{ => sse}/config.yaml (100%) rename examples/MCP/{ => sse}/requirements.txt (100%) rename examples/MCP/{ => sse}/setup.bat (96%) rename examples/MCP/{ => sse}/setup.sh (86%) rename examples/MCP/{ => sse}/tacdev_mcp_client.py (94%) rename examples/MCP/{ => sse}/tacdev_mcp_server.py (50%) diff --git a/examples/MCP/README.md b/examples/MCP/README.md deleted file mode 100644 index 3727a93..0000000 --- a/examples/MCP/README.md +++ /dev/null @@ -1,130 +0,0 @@ -# QTAC MCP Server - -An [MCP](https://modelcontextprotocol.io) server that exposes the full TACDev API as tools, -allowing AI assistants and automation clients to control Qualcomm devices via a QTAC debug board. - -## Architecture - -A persistent SSE server. Multiple clients connect concurrently, each identified by a UUID session. -Each client can open one or more devices. A device can only be held by one session at a time. -On disconnect, the server closes any devices the session left open. - -```mermaid -sequenceDiagram - participant A as Client A - participant S as MCP Server - participant B as Client B - - A->>S: SSE connect - S-->>A: session UUID (auto) - S-->>A: device list push [COM3: free, COM5: free] - - B->>S: SSE connect - S-->>B: session UUID (auto) - S-->>B: device list push [COM3: free, COM5: free] - - A->>S: open_handle_by_description(COM3) - S->>S: TACDev.Open(COM3), register ownership - S-->>A: handle: 1 - - B->>S: open_handle_by_description(COM3) - S-->>B: ERROR - COM3 in use by session-A - - B->>S: open_handle_by_description(COM5) - S->>S: TACDev.Open(COM5), register ownership - S-->>B: handle: 2 - - par Client A uses COM3 - A->>S: tool calls (handle 1) - S-->>A: results - and Client B uses COM5 - B->>S: tool calls (handle 2) - S-->>B: results - end - - A->>S: close_tac_handle(1) - S->>S: TACDev.Close(COM3), release ownership - A->>S: SSE disconnect - - B->>S: SSE disconnect (without closing handle) - S->>S: TACDev.Close(COM5), release ownership -``` - -## Prerequisites - -| Requirement | Details | -| :-- | :-- | -| Python | 3.10+ matching your target architecture (x64 Python on x64, ARM64 Python on ARM64) | -| QTAC build | Project must be built with `build.bat` / `build.sh` - TACDev library is loaded from `__Builds` | - -## Setup - -| Platform | Command | -| :-- | :-- | -| Windows x64 (auto-detected) | `setup.bat` | -| Windows ARM64 (auto-detected) | `setup.bat` | -| Linux | `./setup.sh` | - -Each script auto-detects architecture, checks for an existing build, installs the TACDev Python -library, and installs MCP dependencies. On ARM64 Windows, OpenSSL is downloaded and installed -automatically if not already present. - -For full setup guidance see [Bootcamp guide](../../docs/bootcamp/01-Bootcamp.md) and -[Python API reference](../../docs/bootcamp/02-Python-API.md). - -## Configuration - -All runtime parameters are in `config.yaml`: - -| Parameter | Default | Description | -| :-- | :-- | :-- | -| `server.host` | `127.0.0.1` | Host to bind the server on | -| `server.port` | `8000` | Port the server listens on | -| `logging.file` | `tacdev_mcp.log` | Log file path | -| `logging.level` | `INFO` | Log level: DEBUG, INFO, WARNING, ERROR | -| `logging.max_bytes` | `10485760` | Max log file size before rotation (10 MB) | -| `logging.backup_count` | `5` | Number of rotated backup files to retain | - -## Usage - -**Start the server** (must be running before any client connects): - -```bash -python examples/MCP/tacdev_mcp_server.py -``` - -**Run the client demo:** - -```bash -python examples/MCP/tacdev_mcp_client.py # opens first available device -python examples/MCP/tacdev_mcp_client.py COM41 # opens specific port -``` - -**Use as a library:** - -```python -import asyncio -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("COM41") - await tac.power_on_button(handle) - await tac.close_tac_handle(handle) - -asyncio.run(main()) -``` - -## Troubleshooting - -| Error | Fix | -| :-- | :-- | -| `ModuleNotFoundError: No module named 'TACDev'` | Run `pip install interfaces/Python` from the repo root, or use `setup.bat` / `setup.sh` | -| `TACDev library not found` | Build the project first with `build.bat` / `build.sh` and run from repo root | -| `Architecture mismatch` | Use Python matching your target architecture (x64 or ARM64) | -| `ModuleNotFoundError: No module named 'fastmcp'` | Requires Python 3.10+. Run `pip install -r requirements.txt` | -| `cryptography` build failure on ARM64 | Re-run `setup.bat` - it downloads and installs OpenSSL automatically. If it still fails, install manually from [slproweb.com](https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi) (full installer, not Light) and set `OPENSSL_DIR=C:\Program Files\OpenSSL-Win64-ARM` | -| `get_device_count` returns 0 | Check debug board is connected. On Windows run `FTDICheck.exe` from `__Builds\x64\Release\bin`. On Linux: `sudo cp udev-rules/99-QTAC-USB.rules /etc/udev/rules.d/ && sudo udevadm control --reload` | - -See [Bootcamp troubleshooting](../../docs/bootcamp/01-Bootcamp.md#troubleshooting) for more. diff --git a/examples/MCP/sse/README.md b/examples/MCP/sse/README.md new file mode 100644 index 0000000..65a203a --- /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/config.yaml b/examples/MCP/sse/config.yaml similarity index 100% rename from examples/MCP/config.yaml rename to examples/MCP/sse/config.yaml diff --git a/examples/MCP/requirements.txt b/examples/MCP/sse/requirements.txt similarity index 100% rename from examples/MCP/requirements.txt rename to examples/MCP/sse/requirements.txt diff --git a/examples/MCP/setup.bat b/examples/MCP/sse/setup.bat similarity index 96% rename from examples/MCP/setup.bat rename to examples/MCP/sse/setup.bat index ce51533..6f3c6fb 100644 --- a/examples/MCP/setup.bat +++ b/examples/MCP/sse/setup.bat @@ -5,7 +5,7 @@ setlocal enabledelayedexpansion set SCRIPT_DIR=%~dp0 -pushd "%SCRIPT_DIR%\..\.." +pushd "%SCRIPT_DIR%\..\..\..\" set REPO_ROOT=%CD% popd @@ -82,4 +82,4 @@ if errorlevel 1 exit /b 1 echo. echo Setup complete. Start the MCP server: -echo python examples\MCP\tacdev_mcp_server.py +echo python examples\MCP\sse\tacdev_mcp_server.py diff --git a/examples/MCP/setup.sh b/examples/MCP/sse/setup.sh similarity index 86% rename from examples/MCP/setup.sh rename to examples/MCP/sse/setup.sh index 0326745..01c00d3 100644 --- a/examples/MCP/setup.sh +++ b/examples/MCP/sse/setup.sh @@ -6,7 +6,7 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" if [ ! -d "$REPO_ROOT/__Builds" ]; then echo "[1/3] Build not found. Running root build..." @@ -23,4 +23,4 @@ pip install -r "$SCRIPT_DIR/requirements.txt" echo "" echo "Setup complete. Start the MCP server:" -echo " python examples/MCP/tacdev_mcp_server.py" +echo " python examples/MCP/sse/tacdev_mcp_server.py" diff --git a/examples/MCP/tacdev_mcp_client.py b/examples/MCP/sse/tacdev_mcp_client.py similarity index 94% rename from examples/MCP/tacdev_mcp_client.py rename to examples/MCP/sse/tacdev_mcp_client.py index 564a90a..eda28fc 100644 --- a/examples/MCP/tacdev_mcp_client.py +++ b/examples/MCP/sse/tacdev_mcp_client.py @@ -41,17 +41,6 @@ def _server_url() -> str: return f"http://{s['host']}:{s['port']}/sse" -async def _call(client: Client, tool: str, **kwargs: Any) -> Any: - result = await 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 - - class TACDevClient: def __init__(self) -> None: self._client = Client(_server_url()) @@ -64,7 +53,14 @@ async def __aexit__(self, *args) -> None: await self._client.__aexit__(*args) async def _call(self, tool: str, **kwargs: Any) -> Any: - return await _call(self._client, tool, **kwargs) + 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") @@ -275,7 +271,7 @@ async def boot_to_secondary_edl_button(self, handle: int) -> None: await self._call("boot_to_secondary_edl_button", handle=handle) -async def _demo(port_name: str | None = None) -> None: +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()}") @@ -303,9 +299,13 @@ async def _demo(port_name: str | None = None) -> None: commands = await tac.list_commands(handle) if commands: - cmd = commands[0].split(";")[0] + # Extract short command names (e.g. "sedl" from "sedl(30), Secondary EDL") + cmd_names = [c.split("(")[0].strip() for c in commands] + print(f"\nCommand names: {cmd_names}") + + cmd = cmd_names[0] state_before = await tac.get_command_state(handle, cmd) - print(f"Command '{cmd}' state before toggle: {state_before}") + print(f"\nCommand '{cmd}' state before toggle: {state_before}") await tac.send_command(handle, cmd, not state_before) state_after = await tac.get_command_state(handle, cmd) print(f"Command '{cmd}' state after toggle: {state_after}") @@ -319,4 +319,4 @@ async def _demo(port_name: str | None = None) -> None: if __name__ == "__main__": - asyncio.run(_demo(sys.argv[1] if len(sys.argv) > 1 else None)) + asyncio.run(TACExample(sys.argv[1] if len(sys.argv) > 1 else None)) diff --git a/examples/MCP/tacdev_mcp_server.py b/examples/MCP/sse/tacdev_mcp_server.py similarity index 50% rename from examples/MCP/tacdev_mcp_server.py rename to examples/MCP/sse/tacdev_mcp_server.py index 80a7889..0112e5f 100644 --- a/examples/MCP/tacdev_mcp_server.py +++ b/examples/MCP/sse/tacdev_mcp_server.py @@ -3,6 +3,7 @@ # Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. # SPDX-License-Identifier: BSD-3-Clause +import asyncio import logging import logging.handlers from pathlib import Path @@ -12,8 +13,6 @@ from fastmcp import FastMCP, Context from fastmcp.server.middleware import Middleware, MiddlewareContext, CallNext -import mcp.types as mt - # --------------------------------------------------------------------------- # # Config # --------------------------------------------------------------------------- # @@ -47,37 +46,46 @@ def _setup_logging() -> logging.Logger: # --------------------------------------------------------------------------- # # Device ownership registry # --------------------------------------------------------------------------- # +_lock = asyncio.Lock() + device_ownership: dict[str, str] = {} session_handles: dict[str, list[int]] = {} -handles: dict[int, TACDev.TACDevice] = {} +handles: dict[int, tuple[TACDev.TACDevice, str, str]] = {} # handle -> (device, port, session_id) _next_handle = 1 -def _register(device: TACDev.TACDevice, session_id: str) -> int: +def _register(device: TACDev.TACDevice, port: str, session_id: str) -> int: global _next_handle handle = _next_handle _next_handle += 1 - handles[handle] = device + handles[handle] = (device, port, session_id) session_handles.setdefault(session_id, []).append(handle) return handle -def _get_device(handle: int) -> TACDev.TACDevice: - device = handles.get(handle) - if device is None: +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_session(session_id: str) -> None: - for handle in session_handles.pop(session_id, []): - device = handles.pop(handle, None) - if device: - port = next((p for p, s in device_ownership.items() if s == session_id), None) +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() - if port: - del device_ownership[port] - log.info("session=%s handle=%d auto-closed on disconnect", session_id, handle) + log.info("session=%s handle=%d port=%s closed on shutdown", session_id, handle, port) + 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: @@ -98,33 +106,25 @@ def _device_list() -> dict: # --------------------------------------------------------------------------- # -# Session connect middleware +# Session lifecycle middleware # --------------------------------------------------------------------------- # class SessionMiddleware(Middleware): async def on_initialize( self, - context: MiddlewareContext[mt.InitializeRequest], + context: MiddlewareContext, call_next: CallNext, ): result = await call_next(context) ctx = context.fastmcp_context if ctx: - session_id = ctx.session_id - log.info("session=%s connected", session_id) - devices = _device_list() - await ctx.send_notification( - mt.ResourceListChangedNotification( - method="notifications/resources/list_changed" - ) - ) - log.info("session=%s device list pushed: %s", session_id, devices) + log.info("session=%s connected", ctx.session_id) return result # --------------------------------------------------------------------------- # # MCP server # --------------------------------------------------------------------------- # -mcp = FastMCP("TACDev", middleware=[SessionMiddleware()]) +mcp = FastMCP("TACDev-MCP", middleware=[SessionMiddleware()]) # --------------------------------------------------------------------------- # @@ -189,33 +189,41 @@ def get_port_data(device_index: int) -> dict: @mcp.tool() async def open_handle_by_description(port_name: str, ctx: Context) -> dict: session_id = ctx.session_id - 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}.") - 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, session_id) - device_ownership[port_name] = session_id - log.info("session=%s opened %s handle=%d", session_id, port_name, handle) - return {"handle": handle} + 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 - device = _get_device(handle) - port = next((p for p, s in device_ownership.items() if s == session_id), None) - device.Close() - del handles[handle] - if handle in session_handles.get(session_id, []): - session_handles[session_id].remove(handle) - if port: + 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} @@ -225,36 +233,36 @@ async def close_tac_handle(handle: int, ctx: Context) -> dict: # Tools: device info # --------------------------------------------------------------------------- # @mcp.tool() -def get_name(handle: int) -> dict: - return {"name": _get_device(handle).Get_Name()} +async def get_name(handle: int, ctx: Context) -> dict: + return {"name": _get_device_owned(handle, ctx.session_id).Get_Name()} @mcp.tool() -def get_firmware_version(handle: int) -> dict: - return {"firmware_version": _get_device(handle).GetFirmwareVersion()} +async def get_firmware_version(handle: int, ctx: Context) -> dict: + return {"firmware_version": _get_device_owned(handle, ctx.session_id).GetFirmwareVersion()} @mcp.tool() -def get_hardware(handle: int) -> dict: - return {"hardware": _get_device(handle).GetHardware()} +async def get_hardware(handle: int, ctx: Context) -> dict: + return {"hardware": _get_device_owned(handle, ctx.session_id).GetHardware()} @mcp.tool() -def get_hardware_version(handle: int) -> dict: - return {"hardware_version": _get_device(handle).Get_HardwareVersion()} +async def get_hardware_version(handle: int, ctx: Context) -> dict: + return {"hardware_version": _get_device_owned(handle, ctx.session_id).Get_HardwareVersion()} @mcp.tool() -def get_uuid(handle: int) -> dict: - return {"uuid": _get_device(handle).Get_UUID()} +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() -def set_external_power_control(handle: int, state: bool) -> dict: - _get_device(handle).SetExternalPowerControl(state) +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} @@ -262,19 +270,19 @@ def set_external_power_control(handle: int, state: bool) -> dict: # Tools: dynamic commands # --------------------------------------------------------------------------- # @mcp.tool() -def list_commands(handle: int) -> dict: - device = _get_device(handle) +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() -def get_command(handle: int, command_index: int) -> dict: - return {"command": _get_device(handle).GetCommand(command_index)} +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() -def list_quick_commands(handle: int) -> dict: - device = _get_device(handle) +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())]} @@ -282,14 +290,14 @@ def list_quick_commands(handle: int) -> dict: # Tools: script variables # --------------------------------------------------------------------------- # @mcp.tool() -def list_script_variables(handle: int) -> dict: - device = _get_device(handle) +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() -def update_script_variable(handle: int, variable: str, value: str) -> dict: - _get_device(handle).UpdateScriptVariableValue(variable, value) +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} @@ -297,13 +305,13 @@ def update_script_variable(handle: int, variable: str, value: str) -> dict: # Tools: core command interface # --------------------------------------------------------------------------- # @mcp.tool() -def get_command_state(handle: int, command: str) -> dict: - return {"command": command, "state": _get_device(handle).GetCommandState(command)} +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() -def send_command(handle: int, command: str, state: bool) -> dict: - _get_device(handle).SendCommand(command, state) +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} @@ -311,21 +319,21 @@ def send_command(handle: int, command: str, state: bool) -> dict: # Tools: help / queue # --------------------------------------------------------------------------- # @mcp.tool() -def get_help_text(handle: int) -> dict: - return {"help_text": _get_device(handle).GetHelpText()} +async def get_help_text(handle: int, ctx: Context) -> dict: + return {"help_text": _get_device_owned(handle, ctx.session_id).GetHelpText()} @mcp.tool() -def is_command_queue_clear(handle: int) -> dict: - return {"queue_clear": _get_device(handle).IsCommandQueueClear()} +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() -def set_pin_state(handle: int, pin: int, state: bool) -> dict: - _get_device(handle).SetPin(pin, state) +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} @@ -333,211 +341,211 @@ def set_pin_state(handle: int, pin: int, state: bool) -> dict: # Tools: battery # --------------------------------------------------------------------------- # @mcp.tool() -def set_battery_state(handle: int, state: bool) -> dict: - _get_device(handle).SetBatteryState(state) +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() -def get_battery_state(handle: int) -> dict: - return {"state": _get_device(handle).GetBatteryState()} +async def get_battery_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetBatteryState()} # --------------------------------------------------------------------------- # # Tools: USB # --------------------------------------------------------------------------- # @mcp.tool() -def set_usb0(handle: int, state: bool) -> dict: - _get_device(handle).Usb0(state) +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() -def get_usb0_state(handle: int) -> dict: - return {"state": _get_device(handle).GetUsb0State()} +async def get_usb0_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetUsb0State()} @mcp.tool() -def set_usb1(handle: int, state: bool) -> dict: - _get_device(handle).Usb1(state) +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() -def get_usb1_state(handle: int) -> dict: - return {"state": _get_device(handle).GetUsb1State()} +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() -def set_power_key(handle: int, state: bool) -> dict: - _get_device(handle).PowerKey(state) +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() -def get_power_key_state(handle: int) -> dict: - return {"state": _get_device(handle).GetPowerKeyState()} +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() -def set_volume_up(handle: int, state: bool) -> dict: - _get_device(handle).VolumeUp(state) +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() -def get_volume_up_state(handle: int) -> dict: - return {"state": _get_device(handle).GetVolumeUpState()} +async def get_volume_up_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetVolumeUpState()} @mcp.tool() -def set_volume_down(handle: int, state: bool) -> dict: - _get_device(handle).VolumeDown(state) +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() -def get_volume_down_state(handle: int) -> dict: - return {"state": _get_device(handle).GetVolumeDownState()} +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() -def set_disconnect_uim1(handle: int, state: bool) -> dict: - _get_device(handle).DisconnectUIM1(state) +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() -def get_disconnect_uim1_state(handle: int) -> dict: - return {"state": _get_device(handle).GetDisconnectUIM1State()} +async def get_disconnect_uim1_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetDisconnectUIM1State()} @mcp.tool() -def set_disconnect_uim2(handle: int, state: bool) -> dict: - _get_device(handle).DisconnectUIM2(state) +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() -def get_disconnect_uim2_state(handle: int) -> dict: - return {"state": _get_device(handle).GetDisconnectUIM2State()} +async def get_disconnect_uim2_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetDisconnectUIM2State()} @mcp.tool() -def set_disconnect_sd_card(handle: int, state: bool) -> dict: - _get_device(handle).DisconnectSDCard(state) +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() -def get_disconnect_sd_card_state(handle: int) -> dict: - return {"state": _get_device(handle).GetDisconnectSDCardState()} +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() -def set_primary_edl(handle: int, state: bool) -> dict: - _get_device(handle).PrimaryEDL(state) +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() -def get_primary_edl_state(handle: int) -> dict: - return {"state": _get_device(handle).GetPrimaryEDLState()} +async def get_primary_edl_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetPrimaryEDLState()} @mcp.tool() -def set_secondary_edl(handle: int, state: bool) -> dict: - _get_device(handle).SecondaryEDL(state) +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() -def get_secondary_edl_state(handle: int) -> dict: - return {"state": _get_device(handle).GetSecondaryEDLState()} +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() -def set_force_ps_hold_high(handle: int, state: bool) -> dict: - _get_device(handle).ForcePSHoldHigh(state) +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() -def get_force_ps_hold_high_state(handle: int) -> dict: - return {"state": _get_device(handle).GetForcePSHoldHighState()} +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() -def set_secondary_pm_resin_n(handle: int, state: bool) -> dict: - _get_device(handle).SecondaryPM_RESIN_N(state) +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() -def get_secondary_pm_resin_n_state(handle: int) -> dict: - return {"state": _get_device(handle).GetSecondaryPM_RESIN_NState()} +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() -def set_eud(handle: int, state: bool) -> dict: - _get_device(handle).Eud(state) +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() -def get_eud_state(handle: int) -> dict: - return {"state": _get_device(handle).GetEUDState()} +async def get_eud_state(handle: int, ctx: Context) -> dict: + return {"state": _get_device_owned(handle, ctx.session_id).GetEUDState()} # --------------------------------------------------------------------------- # # Tools: headset # --------------------------------------------------------------------------- # @mcp.tool() -def set_headset_disconnect(handle: int, state: bool) -> dict: - _get_device(handle).HeadsetDisconnect(state) +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() -def get_headset_disconnect_state(handle: int) -> dict: - return {"state": _get_device(handle).GetHeadsetDisconnectState()} +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() -def set_name(handle: int, new_name: str) -> dict: - _get_device(handle).SetName(new_name) +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() -def get_reset_count(handle: int) -> dict: - return {"reset_count": _get_device(handle).GetResetCount()} +async def get_reset_count(handle: int, ctx: Context) -> dict: + return {"reset_count": _get_device_owned(handle, ctx.session_id).GetResetCount()} @mcp.tool() -def clear_reset_count(handle: int) -> dict: - _get_device(handle).ClearResetCount() +async def clear_reset_count(handle: int, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).ClearResetCount() return {"success": True} @@ -545,38 +553,38 @@ def clear_reset_count(handle: int) -> dict: # Tools: button sequences # --------------------------------------------------------------------------- # @mcp.tool() -def power_on_button(handle: int) -> dict: - _get_device(handle).PowerOnButton() +async def power_on_button(handle: int, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).PowerOnButton() return {"success": True} @mcp.tool() -def power_off_button(handle: int) -> dict: - _get_device(handle).PowerOffButton() +async def power_off_button(handle: int, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).PowerOffButton() return {"success": True} @mcp.tool() -def boot_to_fastboot_button(handle: int) -> dict: - _get_device(handle).BootToFastBootButton() +async def boot_to_fastboot_button(handle: int, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).BootToFastBootButton() return {"success": True} @mcp.tool() -def boot_to_uefi_menu_button(handle: int) -> dict: - _get_device(handle).BootToUEFIMenuButton() +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() -def boot_to_edl_button(handle: int) -> dict: - _get_device(handle).BootToEDLButton() +async def boot_to_edl_button(handle: int, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).BootToEDLButton() return {"success": True} @mcp.tool() -def boot_to_secondary_edl_button(handle: int) -> dict: - _get_device(handle).BootToSecondaryEDLButton() +async def boot_to_secondary_edl_button(handle: int, ctx: Context) -> dict: + _get_device_owned(handle, ctx.session_id).BootToSecondaryEDLButton() return {"success": True} @@ -586,6 +594,7 @@ def boot_to_secondary_edl_button(handle: int) -> dict: 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", @@ -595,4 +604,5 @@ def boot_to_secondary_edl_button(handle: int) -> dict: except KeyboardInterrupt: pass finally: + _release_all() log.info("Server stopped.") diff --git a/examples/MCP/stdio/README.md b/examples/MCP/stdio/README.md index 9910aca..4323b90 100644 --- a/examples/MCP/stdio/README.md +++ b/examples/MCP/stdio/README.md @@ -1,8 +1,8 @@ # QTAC MCP Server — stdio -Server is spawned automatically per-client. No standalone process to manage. -Use this variant for Claude CLI integration. +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 @@ -12,6 +12,8 @@ For multi-client shared access see [`../sse/`](../sse/). ## Setup +Installs the TACDev library and all dependencies: + ```bat setup.bat # Windows (auto-detects x64/ARM64) ``` @@ -19,28 +21,48 @@ setup.bat # Windows (auto-detects x64/ARM64) ./setup.sh # Linux ``` -## Claude CLI +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 the server** (run once from repo root): +Register once from repo root: ```bash claude mcp add TACDev-MCP -- python examples/MCP/stdio/tacdev_mcp_server.py ``` -**Verify:** +Verify: ```bash claude mcp list ``` -**Then just ask Claude naturally:** +Then ask Claude naturally — no special syntax needed: ``` List connected devices -Power on the device on COM41 +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 | @@ -49,13 +71,6 @@ Boot COM3 to fastboot | `--scope project` | `.mcp.json` (repo root) | whole team | | `--scope user` | `~/.claude/settings.json` | you, all projects | -## Client demo - -```bash -python examples/MCP/stdio/tacdev_mcp_client.py # first available device -python examples/MCP/stdio/tacdev_mcp_client.py COM41 # specific port -``` - ## Troubleshooting | Error | Fix | diff --git a/examples/MCP/stdio/tacdev_mcp_client.py b/examples/MCP/stdio/tacdev_mcp_client.py index 7d643b2..9d3ff85 100644 --- a/examples/MCP/stdio/tacdev_mcp_client.py +++ b/examples/MCP/stdio/tacdev_mcp_client.py @@ -36,20 +36,9 @@ async def main(): _SERVER_PATH = Path(__file__).with_name("tacdev_mcp_server.py") -async def _call(client: Client, tool: str, **kwargs: Any) -> Any: - result = await 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 - - class TACDevClient: def __init__(self) -> None: - self._client = Client({"command": sys.executable, "args": [str(_SERVER_PATH)]}) + self._client = Client({"mcpServers": {"tacdev": {"command": sys.executable, "args": [str(_SERVER_PATH)]}}}) async def __aenter__(self) -> TACDevClient: await self._client.__aenter__() @@ -59,7 +48,14 @@ async def __aexit__(self, *args) -> None: await self._client.__aexit__(*args) async def _call(self, tool: str, **kwargs: Any) -> Any: - return await _call(self._client, tool, **kwargs) + 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") @@ -270,7 +266,7 @@ async def boot_to_secondary_edl_button(self, handle: int) -> None: await self._call("boot_to_secondary_edl_button", handle=handle) -async def _demo(port_name: str | None = None) -> None: +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()}") @@ -297,9 +293,13 @@ async def _demo(port_name: str | None = None) -> None: commands = await tac.list_commands(handle) if commands: - cmd = commands[0].split(";")[0] + # Extract short command names (e.g. "sedl" from "sedl(30), Secondary EDL") + cmd_names = [c.split("(")[0].strip() for c in commands] + print(f"\nCommand names: {cmd_names}") + + cmd = cmd_names[0] state_before = await tac.get_command_state(handle, cmd) - print(f"Command '{cmd}' state before toggle: {state_before}") + print(f"\nCommand '{cmd}' state before toggle: {state_before}") await tac.send_command(handle, cmd, not state_before) state_after = await tac.get_command_state(handle, cmd) print(f"Command '{cmd}' state after toggle: {state_after}") @@ -313,4 +313,4 @@ async def _demo(port_name: str | None = None) -> None: if __name__ == "__main__": - asyncio.run(_demo(sys.argv[1] if len(sys.argv) > 1 else None)) + 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 index e594372..084e7fb 100644 --- a/examples/MCP/stdio/tacdev_mcp_server.py +++ b/examples/MCP/stdio/tacdev_mcp_server.py @@ -34,6 +34,15 @@ def _get_device(handle: int) -> TACDev.TACDevice: return device +def _release_all() -> None: + for handle, device in list(handles.items()): + try: + device.Close() + except Exception: + pass + handles.clear() + + # --------------------------------------------------------------------------- # # Tools: device list # --------------------------------------------------------------------------- # @@ -478,8 +487,12 @@ def boot_to_secondary_edl_button(handle: int) -> dict: return {"success": True} + # --------------------------------------------------------------------------- # # Entry point # --------------------------------------------------------------------------- # if __name__ == "__main__": - mcp.run(transport="stdio") + try: + mcp.run(transport="stdio") + finally: + _release_all() From 3cd33a1599c7283cc7158d8c5a1c8c6c92149b7b Mon Sep 17 00:00:00 2001 From: "Roy, Biswajit" Date: Tue, 30 Jun 2026 20:24:26 +0530 Subject: [PATCH 15/17] finalize mcp stdio and sse example --- .gitignore | 2 +- examples/MCP/sse/README.md | 12 ++++++------ examples/MCP/sse/tacdev_mcp_client.py | 13 ++++++++++--- examples/MCP/sse/tacdev_mcp_server.py | 16 ++++++++++++++-- examples/MCP/stdio/README.md | 12 ++++++------ examples/MCP/stdio/tacdev_mcp_client.py | 13 ++++++++++--- examples/MCP/stdio/tacdev_mcp_server.py | 2 ++ 7 files changed, 49 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 13e35fd..60e7882 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,7 @@ __pycache__/ *.pyd # MCP logs -examples/MCP/*.log +examples/MCP/**/*.log # Qt build directory __Builds/ diff --git a/examples/MCP/sse/README.md b/examples/MCP/sse/README.md index 65a203a..4c810e8 100644 --- a/examples/MCP/sse/README.md +++ b/examples/MCP/sse/README.md @@ -1,4 +1,4 @@ -# QTAC MCP Server — SSE +# QTAC MCP Server - SSE Persistent server supporting multiple concurrent clients. Each client session gets exclusive ownership of a device. @@ -31,7 +31,7 @@ pip install -r examples/MCP/sse/requirements.txt ## Configuration -`config.yaml` — all fields optional, defaults shown: +`config.yaml` - all fields optional, defaults shown: | Parameter | Default | Description | | :-- | :-- | :-- | @@ -50,14 +50,14 @@ The server must be started before any client connects: python examples/MCP/sse/tacdev_mcp_server.py ``` -### Option 1 — Python client +### 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 +### Option 2 - Claude CLI Register once (server must already be running): @@ -71,7 +71,7 @@ Verify: claude mcp list ``` -Then ask Claude naturally — no special syntax needed: +Then ask Claude naturally - no special syntax needed: ``` List connected devices @@ -100,5 +100,5 @@ python examples/MCP/sse/tacdev_mcp_server.py | `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` | +| `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/tacdev_mcp_client.py b/examples/MCP/sse/tacdev_mcp_client.py index eda28fc..d95039d 100644 --- a/examples/MCP/sse/tacdev_mcp_client.py +++ b/examples/MCP/sse/tacdev_mcp_client.py @@ -299,17 +299,24 @@ async def TACExample(port_name: str | None = None) -> None: commands = await tac.list_commands(handle) if commands: - # Extract short command names (e.g. "sedl" from "sedl(30), Secondary EDL") - cmd_names = [c.split("(")[0].strip() for c in commands] - print(f"\nCommand names: {cmd_names}") + 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) diff --git a/examples/MCP/sse/tacdev_mcp_server.py b/examples/MCP/sse/tacdev_mcp_server.py index 0112e5f..d665446 100644 --- a/examples/MCP/sse/tacdev_mcp_server.py +++ b/examples/MCP/sse/tacdev_mcp_server.py @@ -6,6 +6,7 @@ import asyncio import logging import logging.handlers +import time from pathlib import Path import yaml @@ -32,8 +33,9 @@ def _setup_logging() -> logging.Logger: 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_cfg["file"], + log_path, maxBytes=log_cfg["max_bytes"], backupCount=log_cfg["backup_count"], ) @@ -43,6 +45,14 @@ def _setup_logging() -> logging.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 # --------------------------------------------------------------------------- # @@ -80,6 +90,7 @@ def _release_all() -> None: 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() @@ -594,12 +605,13 @@ async def boot_to_secondary_edl_button(handle: int, ctx: Context) -> dict: 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.") + 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 diff --git a/examples/MCP/stdio/README.md b/examples/MCP/stdio/README.md index 4323b90..f860794 100644 --- a/examples/MCP/stdio/README.md +++ b/examples/MCP/stdio/README.md @@ -1,4 +1,4 @@ -# QTAC MCP Server — stdio +# QTAC MCP Server - stdio Server is spawned automatically per-client process. No standalone server to manage. @@ -30,16 +30,16 @@ pip install -r examples/MCP/stdio/requirements.txt ## Running -No separate server process needed — the server is spawned automatically. +No separate server process needed - the server is spawned automatically. -### Option 1 — Python client +### 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 +### Option 2 - Claude CLI Register once from repo root: @@ -53,7 +53,7 @@ Verify: claude mcp list ``` -Then ask Claude naturally — no special syntax needed: +Then ask Claude naturally - no special syntax needed: ``` List connected devices @@ -79,5 +79,5 @@ Boot COM3 to fastboot | `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` | +| `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/tacdev_mcp_client.py b/examples/MCP/stdio/tacdev_mcp_client.py index 9d3ff85..9fd51f7 100644 --- a/examples/MCP/stdio/tacdev_mcp_client.py +++ b/examples/MCP/stdio/tacdev_mcp_client.py @@ -293,17 +293,24 @@ async def TACExample(port_name: str | None = None) -> None: commands = await tac.list_commands(handle) if commands: - # Extract short command names (e.g. "sedl" from "sedl(30), Secondary EDL") - cmd_names = [c.split("(")[0].strip() for c in commands] - print(f"\nCommand names: {cmd_names}") + 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) diff --git a/examples/MCP/stdio/tacdev_mcp_server.py b/examples/MCP/stdio/tacdev_mcp_server.py index 084e7fb..5f14a97 100644 --- a/examples/MCP/stdio/tacdev_mcp_server.py +++ b/examples/MCP/stdio/tacdev_mcp_server.py @@ -494,5 +494,7 @@ def boot_to_secondary_edl_button(handle: int) -> dict: if __name__ == "__main__": try: mcp.run(transport="stdio") + except KeyboardInterrupt: + pass finally: _release_all() From 1f008a6a463ac0a653cfbd54a1d31eccb6b36511 Mon Sep 17 00:00:00 2001 From: arkosen Date: Fri, 3 Jul 2026 19:17:50 +0530 Subject: [PATCH 16/17] Fix for ftdi driver download if zip file already present --- third-party/CMakeLists.txt | 127 +++++++++++++++++++------------------ 1 file changed, 66 insertions(+), 61 deletions(-) diff --git a/third-party/CMakeLists.txt b/third-party/CMakeLists.txt index 7087ff1..7c5f6c7 100644 --- a/third-party/CMakeLists.txt +++ b/third-party/CMakeLists.txt @@ -30,19 +30,24 @@ 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) - set(DOWNLOAD_SUCCESS FALSE) 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}") @@ -65,63 +70,63 @@ if(NEED_DOWNLOAD) " ${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/${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}") +endif() - 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_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() +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(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.") +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/${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}") + 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() - 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(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(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) @@ -136,9 +141,9 @@ else() ) endif() -if(WIN32 AND DEFINED ENV{QTDIR}) - set(QT_BIN_DIR "$ENV{QTDIR}/bin") - set(QT_PLUGINS_DIR "$ENV{QTDIR}/plugins") +if(WIN32 AND DEFINED ENV{QTBIN}) + set(QT_BIN_DIR "$ENV{QTBIN}") + set(QT_PLUGINS_DIR "$ENV{QTBIN}/../plugins") set(DEBUG_BIN_DIR "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Debug/bin") set(RELEASE_BIN_DIR "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Release/bin") From 3d15c00d8145cf95d71003dcf8326a09dc575afc Mon Sep 17 00:00:00 2001 From: "Roy, Biswajit" Date: Tue, 14 Jul 2026 02:49:15 +0530 Subject: [PATCH 17/17] simplify compilation workflow Signed-off-by: Roy, Biswajit --- .github/workflows/build.yml | 4 ++-- README.md | 5 ++--- build.bat | 28 +++++++--------------------- examples/MCP/sse/setup.bat | 2 +- examples/MCP/stdio/setup.bat | 2 +- third-party/CMakeLists.txt | 32 +------------------------------- 6 files changed, 14 insertions(+), 59 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a4456dc..a005d9a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -132,7 +132,7 @@ jobs: - name: Build shell: cmd - run: build.bat x64 + run: build.bat build-windows-arm64: needs: fetch-ftdi-windows-arm64 @@ -169,4 +169,4 @@ jobs: - name: Build shell: cmd - run: build.bat ARM64 + run: build.bat diff --git a/README.md b/README.md index 0a35f27..f7dce00 100644 --- a/README.md +++ b/README.md @@ -109,11 +109,10 @@ git clone https://github.com/qualcomm/qcom-test-automation-controller.git ### 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 (x64, default) -build.bat ARM64 (ARM64) +build.bat ``` **Build output**: diff --git a/build.bat b/build.bat index 4695b14..8df7846 100644 --- a/build.bat +++ b/build.bat @@ -4,32 +4,18 @@ @echo off @REM --------------------------------------------------------------------------- -@REM Detect target architecture +@REM Detect host architecture (native build only) @REM --------------------------------------------------------------------------- -set ARCH=%1 -if "%ARCH%"=="" ( - if "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( - set ARCH=ARM64 - ) else ( - set ARCH=x64 - ) -) - -if /i "%ARCH%"=="x64" ( - set EXPECTED_QT_PATH=msvc2022_64 - set VCVARS_SCRIPT=vcvars64.bat - set VS_COMPONENT=Desktop development with C++ -) else if /i "%ARCH%"=="ARM64" ( +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 ( - echo ERROR: Unsupported architecture '%ARCH%'. - echo Usage: - echo build.bat - auto-detect from host machine ^(current: %PROCESSOR_ARCHITECTURE%^) - echo build.bat x64 - build for x64 - echo build.bat ARM64 - build for ARM64 - exit /b 1 + set ARCH=x64 + set EXPECTED_QT_PATH=msvc2022_64 + set VCVARS_SCRIPT=vcvars64.bat + set VS_COMPONENT=Desktop development with C++ ) echo Architecture : %ARCH% diff --git a/examples/MCP/sse/setup.bat b/examples/MCP/sse/setup.bat index 6f3c6fb..a331f3d 100644 --- a/examples/MCP/sse/setup.bat +++ b/examples/MCP/sse/setup.bat @@ -66,7 +66,7 @@ if /i "%ARCH%"=="ARM64" ( 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" %ARCH% + call "%REPO_ROOT%\build.bat" if errorlevel 1 exit /b 1 ) else ( echo [1/3] Build found at %BUILD_DIR%, skipping recompile. diff --git a/examples/MCP/stdio/setup.bat b/examples/MCP/stdio/setup.bat index fdb2da4..1e41035 100644 --- a/examples/MCP/stdio/setup.bat +++ b/examples/MCP/stdio/setup.bat @@ -66,7 +66,7 @@ if /i "%ARCH%"=="ARM64" ( 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" %ARCH% + call "%REPO_ROOT%\build.bat" if errorlevel 1 exit /b 1 ) else ( echo [1/3] Build found at %BUILD_DIR%, skipping recompile. diff --git a/third-party/CMakeLists.txt b/third-party/CMakeLists.txt index 7c5f6c7..d4f36d7 100644 --- a/third-party/CMakeLists.txt +++ b/third-party/CMakeLists.txt @@ -74,7 +74,7 @@ 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}") +execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf "${ARCHIVE_PATH}" WORKING_DIRECTORY "${TEMP_DIR}") if(WIN32) file(MAKE_DIRECTORY "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Debug/lib") @@ -141,36 +141,6 @@ else() ) endif() -if(WIN32 AND DEFINED ENV{QTBIN}) - set(QT_BIN_DIR "$ENV{QTBIN}") - set(QT_PLUGINS_DIR "$ENV{QTBIN}/../plugins") - set(DEBUG_BIN_DIR "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/Debug/bin") - set(RELEASE_BIN_DIR "${CMAKE_SOURCE_DIR}/__Builds/${FTDI_WIN_ARCH}/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>)