diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..901011e08 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: [jonasbark] +custom: ["https://paypal.me/boni", "https://donate.stripe.com/9B6aEX0muajY8bZ1Kl6J200"] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..11272805f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,380 @@ +name: "Build" + +on: + workflow_dispatch: + inputs: + build_mac: + description: 'Build for macOS' + required: false + default: true + type: boolean + build_github: + description: 'Build for GitHub' + required: false + default: true + type: boolean + build_windows: + description: 'Build for Windows' + required: false + default: true + type: boolean + build_android: + description: 'Build for Android' + required: false + default: true + type: boolean + build_ios: + description: 'Build for iOS' + required: false + default: true + type: boolean + build_web: + description: 'Build for Web' + required: false + default: false + type: boolean + +env: + SHOREBIRD_TOKEN: ${{ secrets.SHOREBIRD_TOKEN }} + FLUTTER_VERSION: 3.35.5 + +jobs: + build: + name: Build & Release + runs-on: macos-latest + + permissions: + id-token: write + pages: write + contents: write + + steps: + #1 Checkout Repository + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Install certificates + if: inputs.build_mac || inputs.build_ios + env: + DEVELOPER_ID_APPLICATION_P12_BASE64_MAC: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_BASE64_MAC }} + DEVELOPER_ID_INSTALLER_P12_BASE64_MAC: ${{ secrets.DEVELOPER_ID_INSTALLER_P12_BASE64_MAC }} + P12_PASSWORD: ${{ secrets.P12_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + APPSTORE_PROFILE_IOS_BASE64: ${{ secrets.APPSTORE_PROFILE_IOS_BASE64 }} + APPSTORE_PROFILE_MACOS_BASE64: ${{ secrets.APPSTORE_PROFILE_MACOS_BASE64 }} + APPSTORE_PROFILE_DEV_IOS_BASE64: ${{ secrets.APPSTORE_PROFILE_DEV_IOS_BASE64 }} + run: | + # create variables + DEVELOPER_ID_APPLICATION_CERTIFICATE_PATH=$RUNNER_TEMP/build_developerID_application_certificate.p12 + DEVELOPER_ID_INSTALLER_CERTIFICATE_PATH=$RUNNER_TEMP/build_developerID_installer_certificate.p12 + PP_PATH_IOS=$RUNNER_TEMP/build_pp_ios.mobileprovision + PP_PATH_IOS_DEV=$RUNNER_TEMP/build_pp_ios_dev.mobileprovision + PP_PATH_MACOS=$RUNNER_TEMP/build_pp_macos.provisionprofile + KEYCHAIN_PATH=$RUNNER_TEMP/pg-signing.keychain-db + + # import certificate and provisioning profile from secrets + echo -n "$DEVELOPER_ID_APPLICATION_P12_BASE64_MAC" | base64 --decode --output $DEVELOPER_ID_APPLICATION_CERTIFICATE_PATH + echo -n "$DEVELOPER_ID_INSTALLER_P12_BASE64_MAC" | base64 --decode --output $DEVELOPER_ID_INSTALLER_CERTIFICATE_PATH + echo -n "$APPSTORE_PROFILE_IOS_BASE64" | base64 --decode -o $PP_PATH_IOS + echo -n "$APPSTORE_PROFILE_DEV_IOS_BASE64" | base64 --decode -o $PP_PATH_IOS_DEV + echo -n "$APPSTORE_PROFILE_MACOS_BASE64" | base64 --decode -o $PP_PATH_MACOS + + # create temporary keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + # security default-keychain -s $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # import certificate to keychain + security import $DEVELOPER_ID_APPLICATION_CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security import $DEVELOPER_ID_INSTALLER_CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles + cp $PP_PATH_IOS ~/Library/MobileDevice/Provisioning\ Profiles + cp $PP_PATH_IOS_DEV ~/Library/MobileDevice/Provisioning\ Profiles + cp $PP_PATH_MACOS ~/Library/MobileDevice/Provisioning\ Profiles + + - name: 🐦 Setup Shorebird + if: inputs.build_mac || inputs.build_android || inputs.build_ios || inputs.build_web + uses: shorebirdtech/setup-shorebird@v1 + with: + cache: true + + - name: 🚀 Shorebird Release macOS + if: inputs.build_mac + uses: shorebirdtech/shorebird-release@v1 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + platform: macos + + - name: Decode Keystore + if: inputs.build_android + run: | + echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/android.keystore; + echo "${{ secrets.KEYSTORE_PROPERTIES }}" > android/keystore.properties; + + - name: 🚀 Shorebird Release Android + if: inputs.build_android + uses: shorebirdtech/shorebird-release@v1 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + platform: android + args: "--artifact=apk" + + - name: Set Up Flutter + if: inputs.build_web + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Build Web + if: inputs.build_web + run: flutter build web --release --base-href "/swiftcontrol/" + + - name: Upload static files as artifact + if: inputs.build_web + id: deployment + uses: actions/upload-pages-artifact@v3 + with: + path: build/web + + - name: Web Deploy + if: inputs.build_web + uses: actions/deploy-pages@v4 + + - name: Extract latest changelog + id: changelog + run: | + chmod +x scripts/get_latest_changelog.sh + mkdir -p whatsnew + ./scripts/get_latest_changelog.sh | head -c 500 > whatsnew/whatsnew-en-US + + - name: Generate release body + if: inputs.build_github + run: | + chmod +x scripts/generate_release_body.sh + ./scripts/generate_release_body.sh > /tmp/release_body.md + + - name: Set Up Flutter for Screenshots + if: inputs.build_github + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Generate screenshots for App Stores + if: inputs.build_github + run: | + flutter test test/screenshot_test.dart + zip -r BikeControl.storeassets.zip screenshots + echo "Screenshots generated successfully" + + - name: 🚀 Shorebird Release iOS + if: inputs.build_ios + uses: shorebirdtech/shorebird-release@v1 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + platform: ios + args: "--export-options-plist ios/ExportOptions.plist" + + - name: Prepare App Store authentication key + if: inputs.build_ios || inputs.build_mac + env: + API_KEY_BASE64: ${{ secrets.APPSTORE_API_KEY_FILE_BASE64 }} + APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }} + run: | + mkdir -p ./private_keys; + printf %s "$API_KEY_BASE64" | base64 -D > "./private_keys/AuthKey_${APPSTORE_API_KEY}.p8"; + + - name: Upload to Play Store + if: inputs.build_android + uses: r0adkll/upload-google-play@v1 + with: + serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }} + packageName: de.jonasbark.swiftcontrol + releaseFiles: build/app/outputs/bundle/release/app-release.aab + track: production + whatsNewDirectory: whatsnew + + - name: Upload to macOS App Store + if: inputs.build_mac + env: + APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }} + APPSTORE_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }} + run: | + productbuild --component "build/macos/Build/Products/Release/BikeControl.app" /Applications "BikeControl.pkg" --sign "3rd Party Mac Developer Installer: JONAS TASSILO BARK (UZRHKPVWN9)"; + xcrun altool --upload-app -f BikeControl.pkg -t osx --apiKey "$APPSTORE_API_KEY" --apiIssuer "$APPSTORE_API_ISSUER_ID"; + + - name: Upload to iOS App Store + if: inputs.build_ios + env: + APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }} + APPSTORE_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }} + run: | + xcrun altool --upload-app -f build/ios/ipa/swift_play.ipa -t ios --apiKey "$APPSTORE_API_KEY" --apiIssuer "$APPSTORE_API_ISSUER_ID"; + + - name: Handle Android archives + if: inputs.build_android && inputs.build_github + run: | + cp build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/BikeControl.android.apk + + - name: Code Signing of macOS app + if: inputs.build_mac && inputs.build_github + run: /usr/bin/codesign --deep --force -s "$DEVELOPER_ID_APPLICATION_SIGNING_IDENTITY" --entitlements ../../../../../macos/Runner/Release.entitlements --options runtime BikeControl.app -v + working-directory: build/macos/Build/Products/Release + env: + DEVELOPER_ID_APPLICATION_SIGNING_IDENTITY: ${{ secrets.DEVELOPER_ID_APPLICATION_SIGNING_IDENTITY }} + + - name: Handle macOS archives + if: inputs.build_mac && inputs.build_github + run: | + cd build/macos/Build/Products/Release/ + zip -r BikeControl.macos.zip BikeControl.app/ + + - name: Upload Android Artifacts + if: inputs.build_android && inputs.build_github + uses: actions/upload-artifact@v4 + with: + overwrite: true + name: Releases + path: | + build/app/outputs/flutter-apk/BikeControl.android.apk + + - name: Upload macOS Artifacts + if: inputs.build_mac && inputs.build_github + uses: actions/upload-artifact@v4 + with: + overwrite: true + name: Releases + path: | + build/macos/Build/Products/Release/BikeControl.macos.zip + + - name: Upload Screenshots Artifacts + if: inputs.build_github + uses: actions/upload-artifact@v4 + with: + overwrite: true + name: Releases + path: | + screenshots/device-GitHub-600x900.png + build/BikeControl.screenshots.zip + + #10 Extract Version + - name: Extract version from pubspec.yaml + if: inputs.build_github + id: extract_version + run: | + version=$(grep '^version: ' pubspec.yaml | cut -d ' ' -f 2 | tr -d '\r') + echo "VERSION=$version" >> $GITHUB_ENV + + #13 Create Release + - name: Create Release + if: inputs.build_github + uses: ncipollo/release-action@v1 + with: + artifacts: "build/app/outputs/flutter-apk/BikeControl.android.apk,build/macos/Build/Products/Release/BikeControl.macos.zip,build/BikeControl.screenshots.zip,screenshots/device-GitHub-600x900.png" + allowUpdates: true + prerelease: true + bodyFile: /tmp/release_body.md + tag: v${{ env.VERSION }} + token: ${{ secrets.TOKEN }} + + windows: + if: inputs.build_windows + name: Build & Release on Windows + runs-on: windows-2025 + + steps: + #1 Checkout Repository + - name: Checkout Repository + uses: actions/checkout@v3 + + + - name: Extract version from pubspec.yaml (Windows) + shell: pwsh + run: | + $version = Select-String '^version: ' pubspec.yaml | ForEach-Object { + ($_ -split ' ')[1].Trim() + } + echo "VERSION=$version" >> $env:GITHUB_ENV + + - name: 🐦 Setup Shorebird + uses: shorebirdtech/setup-shorebird@v1 + with: + cache: true + + - name: 🚀 Shorebird Release Windows + uses: shorebirdtech/shorebird-release@v1 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + platform: windows + + - name: Zip directory (Windows) + shell: pwsh + run: | + + $source = "C:\Windows\System32" + $destination = "build\windows\x64\runner\Release" + + # List of required DLLs + $dlls = @("msvcp140.dll", "vcruntime140.dll", "vcruntime140_1.dll") + + # Copy each file + foreach ($dll in $dlls) { + $srcPath = Join-Path $source $dll + $destPath = Join-Path $destination $dll + + if (Test-Path $srcPath) { + Copy-Item -Path $srcPath -Destination $destPath -Force + Write-Output "Copied $dll to $destination" + } else { + Write-Warning "$dll not found in $source" + } + } + Compress-Archive -Path "build/windows/x64/runner/Release/*" -DestinationPath "build/windows/x64/runner/Release/BikeControl.windows.zip" + + - uses: microsoft/setup-msstore-cli@v1 + if: false + + - name: Configure the Microsoft Store CLI + if: false + run: msstore reconfigure --tenantId $ --clientId $ --clientSecret $ --sellerId $ + + - name: Set Up Flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Create MSIX package + run: dart run msix:create + + - name: Publish MSIX to the Microsoft Store + if: false + run: msstore publish -v "build/windows/x64/runner/Release/" + + - name: Rename swift_control.msix to BikeControl.windows.msix + shell: pwsh + run: | + Rename-Item -Path "build/windows/x64/runner/Release/swift_control.msix" -NewName "BikeControl.windows.msix" + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + overwrite: true + name: Releases + path: | + build/windows/x64/runner/Release/BikeControl.windows.zip + build/windows/x64/runner/Release/BikeControl.windows.msix + + - name: Update Release + uses: ncipollo/release-action@v1 + with: + allowUpdates: true + artifacts: "build/windows/x64/runner/Release/BikeControl.windows.zip,build/windows/x64/runner/Release/BikeControl.windows.msix" + prerelease: true + tag: v${{ env.VERSION }} + token: ${{ secrets.TOKEN }} diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml new file mode 100644 index 000000000..b0070f926 --- /dev/null +++ b/.github/workflows/patch.yml @@ -0,0 +1,168 @@ +name: "Patch" + +on: + workflow_dispatch: + +env: + SHOREBIRD_TOKEN: ${{ secrets.SHOREBIRD_TOKEN }} + FLUTTER_VERSION: 3.35.5 + +jobs: + build: + name: Patch iOS, Android & macOS + runs-on: macos-latest + + permissions: + id-token: write + pages: write + contents: write + + steps: + #1 Checkout Repository + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: 🐦 Setup Shorebird + uses: shorebirdtech/setup-shorebird@v1 + with: + cache: true + + - name: Install certificates + env: + DEVELOPER_ID_APPLICATION_P12_BASE64_MAC: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_BASE64_MAC }} + DEVELOPER_ID_INSTALLER_P12_BASE64_MAC: ${{ secrets.DEVELOPER_ID_INSTALLER_P12_BASE64_MAC }} + P12_PASSWORD: ${{ secrets.P12_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + APPSTORE_PROFILE_IOS_BASE64: ${{ secrets.APPSTORE_PROFILE_IOS_BASE64 }} + APPSTORE_PROFILE_MACOS_BASE64: ${{ secrets.APPSTORE_PROFILE_MACOS_BASE64 }} + APPSTORE_PROFILE_DEV_IOS_BASE64: ${{ secrets.APPSTORE_PROFILE_DEV_IOS_BASE64 }} + run: | + # create variables + DEVELOPER_ID_APPLICATION_CERTIFICATE_PATH=$RUNNER_TEMP/build_developerID_application_certificate.p12 + DEVELOPER_ID_INSTALLER_CERTIFICATE_PATH=$RUNNER_TEMP/build_developerID_installer_certificate.p12 + PP_PATH_IOS=$RUNNER_TEMP/build_pp_ios.mobileprovision + PP_PATH_IOS_DEV=$RUNNER_TEMP/build_pp_ios_dev.mobileprovision + PP_PATH_MACOS=$RUNNER_TEMP/build_pp_macos.provisionprofile + KEYCHAIN_PATH=$RUNNER_TEMP/pg-signing.keychain-db + + # import certificate and provisioning profile from secrets + echo -n "$DEVELOPER_ID_APPLICATION_P12_BASE64_MAC" | base64 --decode --output $DEVELOPER_ID_APPLICATION_CERTIFICATE_PATH + echo -n "$DEVELOPER_ID_INSTALLER_P12_BASE64_MAC" | base64 --decode --output $DEVELOPER_ID_INSTALLER_CERTIFICATE_PATH + echo -n "$APPSTORE_PROFILE_IOS_BASE64" | base64 --decode -o $PP_PATH_IOS + echo -n "$APPSTORE_PROFILE_DEV_IOS_BASE64" | base64 --decode -o $PP_PATH_IOS_DEV + echo -n "$APPSTORE_PROFILE_MACOS_BASE64" | base64 --decode -o $PP_PATH_MACOS + + # create temporary keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + # security default-keychain -s $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # import certificate to keychain + security import $DEVELOPER_ID_APPLICATION_CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security import $DEVELOPER_ID_INSTALLER_CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles + cp $PP_PATH_IOS ~/Library/MobileDevice/Provisioning\ Profiles + cp $PP_PATH_IOS_DEV ~/Library/MobileDevice/Provisioning\ Profiles + cp $PP_PATH_MACOS ~/Library/MobileDevice/Provisioning\ Profiles + + - name: Decode Keystore + run: | + echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/android.keystore; + echo "${{ secrets.KEYSTORE_PROPERTIES }}" > android/keystore.properties; + + - name: 🚀 Shorebird Patch macOS + if: false # patch doesn't work: https://github.com/jonasbark/swiftcontrol/issues/143 + uses: shorebirdtech/shorebird-patch@v1 + with: + platform: macos + release-version: latest + args: '--allow-asset-diffs --allow-native-diffs' + + - name: 🚀 Shorebird Patch Android + uses: shorebirdtech/shorebird-patch@v1 + with: + platform: android + release-version: latest + args: '--allow-asset-diffs --allow-native-diffs' + + - name: 🚀 Shorebird Patch iOS + uses: shorebirdtech/shorebird-patch@v1 + with: + platform: ios + release-version: latest + args: '--allow-asset-diffs --allow-native-diffs' + + - name: Set Up Flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + flutter-version: ${{ env.FLUTTER_VERSION }} + + # shorebird struggles with the app from GitHub + - name: Build macOS + if: false + run: flutter build macos --release; + + - name: Sign macOS build + if: false + env: + DEVELOPER_ID_APPLICATION_SIGNING_IDENTITY: ${{ secrets.DEVELOPER_ID_APPLICATION_SIGNING_IDENTITY }} + run: | + version=$(grep '^version: ' pubspec.yaml | cut -d ' ' -f 2 | tr -d '\r'); + echo "VERSION=$version" >> $GITHUB_ENV; + cd build/macos/Build/Products/Release/; + /usr/bin/codesign --deep --force -s "$DEVELOPER_ID_APPLICATION_SIGNING_IDENTITY" --entitlements ../../../../../macos/Runner/Release.entitlements --options runtime BikeControl.app -v; + zip -r BikeControl.macos.zip BikeControl.app/; + + #9 Upload Artifacts + - name: Upload Artifacts + if: false + uses: actions/upload-artifact@v4 + with: + overwrite: true + name: Releases + path: | + build/macos/Build/Products/Release/BikeControl.macos.zip + + - name: Generate release body + if: false + run: | + chmod +x scripts/generate_release_body.sh + ./scripts/generate_release_body.sh > /tmp/release_body.md + + # add artifact to release + - name: Create Release + if: false + uses: ncipollo/release-action@v1 + with: + allowUpdates: true + artifacts: "build/macos/Build/Products/Release/BikeControl.macos.zip" + bodyFile: /tmp/release_body.md + prerelease: true + tag: v${{ env.VERSION }} + token: ${{ secrets.TOKEN }} + + windows: + name: Patch Windows + runs-on: windows-latest + + steps: + #1 Checkout Repository + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: 🐦 Setup Shorebird + uses: shorebirdtech/setup-shorebird@v1 + with: + cache: true + + - name: 🚀 Shorebird Patch Windows + uses: shorebirdtech/shorebird-patch@v1 + with: + platform: windows + release-version: latest + args: '--allow-asset-diffs --allow-native-diffs' diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml new file mode 100644 index 000000000..402be51f3 --- /dev/null +++ b/.github/workflows/web.yml @@ -0,0 +1,51 @@ +name: "Build Web" + +on: + push: + branches: + - web + - wahoo_kickr_bike_shift + - main + paths: + - '.github/workflows/web.yml' + - 'lib/**' + - 'accessibility/**' + - 'keypress_simulator/**' + - 'pubspec.yaml' + +jobs: + build: + name: Build Web + runs-on: macos-latest + + permissions: + id-token: write + pages: write + contents: write + + steps: + #1 Checkout Repository + - name: Checkout Repository + uses: actions/checkout@v3 + + #3 Setup Flutter + - name: Set Up Flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + + #4 Install Dependencies + - name: Install Dependencies + run: flutter pub get + + - name: Build Web + run: flutter build web --release --base-href "/swiftcontrol/" + + - name: Upload static files as artifact + id: deployment + uses: actions/upload-pages-artifact@v3 + with: + path: build/web + + - name: Web Deploy + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 79c113f9b..7ac139363 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,11 @@ .history .svn/ .swiftpm/ +debug/ migrate_working_dir/ +android/keystore.properties + # IntelliJ related *.iml *.ipr @@ -43,3 +46,7 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +service-account.json +.env +/screenshots/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..a47999ef6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + + { + "name": "swiftcontrol", + "request": "launch", + "type": "dart" + }, + { + "name": "swiftcontrol (profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "swiftcontrol (release mode)", + "request": "launch", + "type": "dart", + "flutterMode": "release" + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..f22ce12fc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,197 @@ +### 3.6.0 (23-11-2025) + +SwiftControl is now called BikeControl! + +**Features:** +- show a list of predefined keymaps for the selected trainer app when using a custom keymap +- status icons so it's clear what's missing + +**Fixes:** +- Update Rouvy keymap to support virtual shifting in their latest version + +### 3.5.0 (16-11-2025) +**New Features:** +- Dark mode support +- Cycplus BC2 support (thanks @schneewoehner) +- Ignored devices now persist across app restarts - remove them from ignored devices via the menu + +**Fixes:** +- resolve issues during app start + +### 3.4.0 (08-11-2025) +**New Features:** +- Support for Shimano Di2 +- Support Keyboard shortcuts with modifier keys (Ctrl, Alt, Shift, ...) +- Support cheap BLE HID remotes +- add Keymap for Rouvy, supporting the new keyboard shortcuts for virtual shifting + +**Fixes:** +- fix detection of Elite Square Sterzo devices +- recognize cheap Bluetooth device clicks also when BikeControl is in the background + +### 3.3.0 (31-10-2025) + +**New Features:** +- Support for Elite Sterzo (thanks @michidk) +- Support for Gamepads +- Support for cheap bluetooth remotes (such as [these](https://www.amazon.com/s?k=bluetooth+remote)) +- you can now customize the Keymap right from the Customize section +- show signal strength of connected devices (thanks @michidk) +- Android and Windows only: simulate bluetooth controllers + - enables gamepad and bluetooth remotes support for Zwift, Rouvy and Biketerra + +**Fixes:** +- fix firmware version display for Zwift Click V2 devices +- fix touch position on some Android devices +- Wahoo Kickr Bike Shift can now be connected +- update default keymap for TrainingPeaks + +### 3.2.0 (2025-10-22) +- a brand-new way of controlling MyWhoosh: + - device pairing no longer required as mouse emulation is no longer needed + - BikeControl can now stay in the background + - more devices can be controlled + - do more, such as define Emotes, Camera angles and steering + +### 3.1.0 (2025-10-17) +- new app icon +- adjusted MyWhoosh keyboard navigation mapping (thanks @bin101) +- support for Wahook Kickr Bike Shift (thanks @MattW2) +- initial support for Elite Square Smart Frame +- reconnects to your device automatically when connection is lost +- BikeControl now warns you if your device firmware is outdated +- BikeControl is now available in Microsoft Store: https://apps.microsoft.com/detail/9NP42GS03Z26 + +### 3.0.3 (2025-10-12) +- BikeControl now supports iOS! + - Note that you can't run BikeControl and your trainer app on the same iPhone due to iOS limitations but...: +- You can now use BikeControl as "remote control" for other devices, such as an iPad. Example scenario: + - your phone (Android/iOS) runs BikeControl and connects to your Click devices + - your iPad or other tablet runs e.g. MyWhoosh (does not need to have BikeControl installed) + - after pairing BikeControl to your iPad / tablet via Bluetooth your phone will send the button presses to your iPad / tablet +- Ride: analog paddles are now supported thanks to contributor @jmoro +- you can now zoom in and out in the Keymap customization screen + +### 2.6.3 (2025-10-01) +- fix a few issues with the new touch placement feature +- add a workaround for Zwift Click V2 which resets the device when button events are no longer sent +- fix issue on Android and Desktop where only a "touch down" was sent, but no "touch up" +- improve UI when handling custom keymaps around the edges of the screen + +### 2.6.0 (2025-09-30) +- refactor touch placements: show touches on screen, fix misplaced coordinates - should fix #64 +- show firmware version of connected device +- Fix crashes on some Android devices +- warn the user how to make Zwift Click V2 work properly +- many UI improvements +- add setting to enable or disable vibration on button press for Zwift Ride and Zwift Play controllers + +### 2.5.0 (2025-09-25) +- Improve usability +- BikeControl is now available via the Play Store: https://play.google.com/store/apps/details?id=de.jonasbark.swiftcontrol + - BikeControl will continue to be available to download for free on GitHub + - contact me if you already donated and I'll get a voucher for you :) + +### 2.4.0+1 (2025-09-17) +- Windows: fix mouse clicks at wrong location due to display scaling (fixes #64) + +### 2.4.0 (2025-09-16) +- Show an overview of the keymap bindings +- Allow customizing an existing keymap +- Add more donation options + +### 2.3.0 (2025-09-11) +- Add support for latest Zwift Click v2 + +### 2.2.0 (2025-09-08) +- Add Long Press Mode option for custom keymaps - buttons can now send sustained key presses instead of repeated taps, perfect for movement controls in games (fixes #61) +- Windows: adjust key sending method to improve compatibility with more apps (fixes #62) + +### 2.1.0 (2025-07-03) +- Windows: automatically focus compatible training apps (MyWhoosh, IndieVelo, Biketerra) when sending keystrokes, enabling seamless multi-window usage + +### 2.0.9 (2025-05-04) +- you can now assign Escape and arrow down key to your custom keymap (#18) + +### 2.0.8 (2025-05-02) +- only use the light theme for the app +- more troubleshooting information + +### 2.0.7 (2025-04-18) +- add Biketerra.com keymap +- some UX improvements + +### 2.0.6 (2025-04-15) +- fix MyWhoosh up / downshift button assignment (I key vs K key) + +### 2.0.5 (2025-04-13) +- fix Zwift Click button assignment (#12) + +### 2.0.4 (2025-04-10) +- vibrate Zwift Play / Zwift Ride controller on gear shift (thanks @cagnulein, closes #16) + +### 2.0.3 (2025-04-08) +- adjust TrainingPeaks Virtual key mapping (#12) +- attempt to reconnect device if connection is lost +- Android: detect freeform windows for MyWhoosh + TrainingPeaks Virtual keymaps + +### 2.0.2 (2025-04-07) +- fix bluetooth scan issues on older Android devices by asking for location permission + +### 2.0.1 (2025-04-06) +- long pressing a button will trigger the action again every 250ms + +### 2.0.0 (2025-04-06) +- You can now customize the actions (touches, mouse clicks or keyboard keys) for all buttons on all supported Zwift devices +- now shows the battery level of the connected devices +- add more troubleshooting information + +### 1.1.10 (2025-04-03) +- Add more troubleshooting during connection + +### 1.1.8 (2025-04-02) +- Android: make sure the touch reassignment page is fullscreen + +### 1.1.7 (2025-04-01) +- Zwift Ride: fix connection issues by connecting only to the left controller +- Windows: connect sequentially to fix (finally?) fix connection issues +- Windows: change the way keyboard is simulated, should fix glitches + +### 1.1.6 (2025-03-31) +- Zwift Ride: add buttonPowerDown to shift gears +- Zwift Play: Fix buttonShift assignment +- Android: fix action to go to next song +- App now checks if you run the latest available version + +### 1.1.5 (2025-03-30) +- fix bluetooth connection #6, also add missing entitlement on macOS + +### 1.1.3 (2025-03-30) +- Windows: fix custom keyboard profile recreation after restart, also warn when choosing MyWhoosh profile (may fix #7) +- Zwift Ride: button map adjustments to prevent double shifting +- potential fix for #6 + +### 1.1.1 (2025-03-30) +- potential fix for Bluetooth device detection + +### 1.1.0 (2025-03-30) +- Windows & macOS: allow setting custom keymap and store the setting +- Android: allow customizing the touch area, so it can work with any device without guesswork where the buttons are (#4) +- Zwift Ride: update Zwift Ride decoding based on Feedback from @JayyajGH (#3) + +### 1.0.6 (2025-03-29) +- Another potential keyboard fix for Windows +- Zwift Play: actually also use the dedicated shift buttons + +### 1.0.5 (2025-03-29) +- Zwift Ride: remap the shifter buttons to the correct values + +### 1.0.0+4 (2025-03-29) +- Zwift Ride: attempt to fix button parsing +- Android: fix missing permissions +- Windows: potential fix for key press issues + +### 1.0.0+3 (2025-03-29) + +- Windows: fix connection by using a different Bluetooth stack (issue #1) +- Android: fix non-working touch propagation (issue #2) diff --git a/INSTRUCTIONS_ANDROID.md b/INSTRUCTIONS_ANDROID.md new file mode 100644 index 000000000..d32788053 --- /dev/null +++ b/INSTRUCTIONS_ANDROID.md @@ -0,0 +1 @@ +Instructions will be added soon diff --git a/INSTRUCTIONS_IOS.md b/INSTRUCTIONS_IOS.md new file mode 100644 index 000000000..6ff765c46 --- /dev/null +++ b/INSTRUCTIONS_IOS.md @@ -0,0 +1,12 @@ +**Instructions for using the MyWhoosh Direct Connect method** +1) launch MyWhoosh on the device of your choice +2) launch MyWhoosh Link, check if the "Link" connection works +3) close MyWhoosh Link +4) open BikeControl, follow on screen instructions + +Once you've confirmed the connection in BikeControl you won't have to repeat step 2 and 3 again in the future. This is just to make sure the connection works in general. + +And here's a video with a few explanations: + +[![BikeControl Instruction for iOS](https://img.youtube.com/vi/p8sgQhuufeI/0.jpg)](https://www.youtube.com/watch?v=p8sgQhuufeI) +[https://www.youtube.com/watch?v=p8sgQhuufeI](https://www.youtube.com/watch?v=p8sgQhuufeI) diff --git a/INSTRUCTIONS_MACOS.md b/INSTRUCTIONS_MACOS.md new file mode 100644 index 000000000..d32788053 --- /dev/null +++ b/INSTRUCTIONS_MACOS.md @@ -0,0 +1 @@ +Instructions will be added soon diff --git a/INSTRUCTIONS_WINDOWS.md b/INSTRUCTIONS_WINDOWS.md new file mode 100644 index 000000000..d32788053 --- /dev/null +++ b/INSTRUCTIONS_WINDOWS.md @@ -0,0 +1 @@ +Instructions will be added soon diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index ad14eda0d..118931deb 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,94 @@ -# SwiftControl +# BikeControl (formerly SwiftControl) -SwiftControl - Control your virtual riding +BikeControl Logo -## TODO -- github pipelines -- test Zwift Ride -- implement more actions for Play + Ride -- write readme with logo -- shorebird? +## Description + +With BikeControl you can **control your favorite trainer app** using your Zwift Click, Zwift Ride, Zwift Play, or other similar devices. Here's what you can do with it, depending on your configuration: +- Virtual Gear shifting +- Steering/turning +- adjust workout intensity +- control music on your device +- more? If you can do it via keyboard, mouse, or touch, you can do it with BikeControl + + +https://github.com/user-attachments/assets/1f81b674-1628-4763-ad66-5f3ed7a3f159 + + + + +## Downloads +Best follow our landing page and the "Get Started" button: [bikecontrol.app](https://bikecontrol.app/) to understand on which platform you want to run BikeControl. + +GetItOnGooglePlay_Badge_Web_color_English + +App Store + +Mac App Store + +Microsoft Store + +## Supported Apps +- MyWhoosh +- TrainingPeaks Virtual / indieVelo +- Biketerra.com +- Rouvy +- Zwift + - running BikeControl on Android or Windows is required to act as a "Controllable" in Zwift - iOS and macOS are not able to do so +- any other! + - You can add custom mapping and adjust touch points or keyboard shortcuts to your liking + +## Supported Devices +- Zwift Click +- Zwift Click v2 (mostly, see issue #68) +- Zwift Ride +- Zwift Play +- Shimano Di2 + - Configure your levers to use D-Fly channels with Shimano E-Tube app +- Wahoo Kickr Bike Shift +- CYCPLUS BC2 Virtual Shifter +- Elite Sterzo Smart (for steering support) +- Elite Square Smart Frame (beta) +- Gamepads (beta) +- Cheap Bluetooth buttons such as [these](https://www.amazon.com/s?k=bluetooth+remote) (beta) + - BLE HID devices and classic Bluetooth HID devices are supported + - works on Android + - on iOS and macOS requires BikeControl to act as media player + +Support for other devices can be added; check the issues tab here on GitHub. + +## Supported Platforms + +Follow the "Get Started" button over at [bikecontrol.app](https://bikecontrol.app) to understand on which platform you want to run BikeControl. +You can even try it out in your [Browser](https://jonasbark.github.io/swiftcontrol/), if it supports Bluetooth connections. No controlling possible, though. + +## Troubleshooting +Check the troubleshooting guide [here](TROUBLESHOOTING.md). + +## How does it work? +The app connects to your Controller devices (such as Zwift ones) automatically. It does not connect to your trainer itself. + +- **Android**: BikeControl uses the AccessibilityService API to simulate touch gestures on specific parts of your screen to trigger actions in training apps. The service monitors which training app window is currently active to ensure gestures are sent to the correct app. +- **iOS**: use BikeControl as a "remote control" for other devices, such as an iPad. Example scenario: + - your phone (Android/iOS) runs BikeControl and connects to your Controller devices + - your iPad or other tablet runs e.g. MyWhoosh (does not need to have BikeControl installed) + - If you want to use MyWhoosh, you can use the Link method to connect to MyWhoosh directly + - For other trainer apps, you need to pair BikeControl to your iPad / tablet via Bluetooth, and your phone will send the button presses to your iPad / tablet +- **macOS** / **Windows** A keyboard or mouse click is used to trigger the action. + - There are predefined Keymaps for MyWhoosh, indieVelo / Training Peaks, and others + - You can also create your own Keymaps for any other app + - You can also use the mouse to click on a certain part of the screen, or use keyboard shortcuts + +## Alternatives +- [qdomyos-zwift](https://www.qzfitness.com/) directly controls the trainer (as opposed to controlling the trainer app). This can be useful if your trainer app does not support virtual shifting. + +## Accessories + +- 3D printable models from Zwift [Zwift 3D-printable mounts for Click](https://www.zwift.com/uk/news/33400-zwift-3d-printable-mounts-for-click). + +## Donate +Please consider donating to support the development of this app :) + +- [via PayPal](https://paypal.me/boni) +- [via Credit Card, Google Pay, Apple Pay, etc (USD)](https://donate.stripe.com/8x24gzc5c4ZE3VJdt36J201) +- [via Credit Card, Google Pay, Apple Pay, etc (EUR)](https://donate.stripe.com/9B6aEX0muajY8bZ1Kl6J200) diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 000000000..3e9f78946 --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,53 @@ +## Click / Ride device cannot be found +You may need to update the firmware in Zwift Companion app. + +## Click / Ride device does not send any data +You may need to update the firmware in Zwift Companion app. + +## My Click v2 disconnects after a minute +Check [this](https://github.com/OpenBikeControl/bikecontrol/issues/68) discussion. + +To make your Click V2 work best you should connect it in the Zwift app once each day. +If you don't do that BikeControl will need to reconnect every minute. + +1. Open Zwift app (not the Companion) +2. Log in (subscription not required) and open the device connection screen +3. Connect your Trainer, then connect the Click V2 +4. Close the Zwift app again and connect again in BikeControl + +## Android: Connection works, buttons work but nothing happens in MyWhoosh and similar +- especially for Redmi and other chinese Android devices please follow the instructions on [https://dontkillmyapp.com/](https://dontkillmyapp.com/): + - disable battery optimization for BikeControl + - enable auto start of BikeControl + - grant accessibility permission for BikeControl +- see [https://github.com/OpenBikeControl/bikecontrol/issues/38](https://github.com/OpenBikeControl/bikecontrol/issues/38) for more details + +## Remote control is not working - nothing happens +- Try to unpair it from your phone / computer Bluetooth settings, then re-pair it. +- Try restarting the pairing process in BikeControl +- try restarting Bluetooth on your phone and on the device you want to control +- If your other device is an iOS device, go to Settings > Accessibility > Touch > AssistiveTouch > Pointer Devices > Devices and pair your device. Make sure AssistiveTouch is enabled. +- it is very important that both devices (e.g. iPhone and iPad) receive the "pairing dialog" after initial connection. If you miss it, unpair and try again. It may take a few seconds for the dialog to appear. Afterwards you may need to click on "Reconnect" in BikeControl / restart BikeControl. + +## Remote control only clicks on a single coordinate on my iPad +iOS seems to be buggy here - try this in the iOS settings: +AssistiveTouch settings > Pointer Devices > Devices > Connected Devices > iPhone (or BikeControl iOS) > Button 1 +switch the setting to None, then back to Single-Tap and it should work again + +## BikeControl crashes on Windows when searching for the device +You're probably running into [this](https://github.com/OpenBikeControl/bikecontrol/issues/70) issue. Disconnect your controller device (e.g. Zwift Play) from Windows Bluetooth settings. + +## MyWhoosh Direct Connect never connects +The same network restrictions apply for BikeControl as it applies to MyWhoosh Link app. Please verify with the MyWhoosh Link app if connection is possible at all. +Here are some instructions that can help: + +[https://mywhoosh.com/troubleshoot/](https://mywhoosh.com/troubleshoot/) +[https://www.facebook.com/groups/mywhoosh/posts/1323791068858873/](https://www.facebook.com/groups/mywhoosh/posts/1323791068858873/) +[INSTRUCTIONS_IOS.md](INSTRUCTIONS_IOS.md) + +In essence: +- your two devices (phone, tablet) need to be on the same WiFi network +- on iOS you have to turn off "Private Wi-Fi Address" in the WiFi settings +- Limit IP Address Tracking may need to be disabled +- mesh networks may not work + diff --git a/WINDOWS_STORE_VERSION.txt b/WINDOWS_STORE_VERSION.txt new file mode 100644 index 000000000..9575d51ba --- /dev/null +++ b/WINDOWS_STORE_VERSION.txt @@ -0,0 +1 @@ +3.6.1 diff --git a/accessibility/android/src/main/kotlin/de/jonasbark/accessibility/AccessibilityApi.kt b/accessibility/android/src/main/kotlin/de/jonasbark/accessibility/AccessibilityApi.kt index 0e4ece181..b97e60550 100644 --- a/accessibility/android/src/main/kotlin/de/jonasbark/accessibility/AccessibilityApi.kt +++ b/accessibility/android/src/main/kotlin/de/jonasbark/accessibility/AccessibilityApi.kt @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v25.2.0), do not edit directly. +// Autogenerated from Pigeon (v25.5.0), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -12,25 +12,57 @@ import io.flutter.plugin.common.StandardMethodCodec import io.flutter.plugin.common.StandardMessageCodec import java.io.ByteArrayOutputStream import java.nio.ByteBuffer +private object AccessibilityApiPigeonUtils { -private fun wrapResult(result: Any?): List { - return listOf(result) -} + fun wrapResult(result: Any?): List { + return listOf(result) + } -private fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) + fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } } + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a is ByteArray && b is ByteArray) { + return a.contentEquals(b) + } + if (a is IntArray && b is IntArray) { + return a.contentEquals(b) + } + if (a is LongArray && b is LongArray) { + return a.contentEquals(b) + } + if (a is DoubleArray && b is DoubleArray) { + return a.contentEquals(b) + } + if (a is Array<*> && b is Array<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is List<*> && b is List<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is Map<*, *> && b is Map<*, *>) { + return a.size == b.size && a.all { + (b as Map).containsKey(it.key) && + deepEquals(it.value, b[it.key]) + } + } + return a == b + } + } /** @@ -45,26 +77,45 @@ class FlutterError ( val details: Any? = null ) : Throwable() +enum class MediaAction(val raw: Int) { + PLAY_PAUSE(0), + NEXT(1), + VOLUME_UP(2), + VOLUME_DOWN(3); + + companion object { + fun ofRaw(raw: Int): MediaAction? { + return values().firstOrNull { it.raw == raw } + } + } +} + /** Generated class from Pigeon that represents data sent in messages. */ data class WindowEvent ( val packageName: String, - val windowHeight: Long, - val windowWidth: Long + val top: Long, + val bottom: Long, + val right: Long, + val left: Long ) { companion object { fun fromList(pigeonVar_list: List): WindowEvent { val packageName = pigeonVar_list[0] as String - val windowHeight = pigeonVar_list[1] as Long - val windowWidth = pigeonVar_list[2] as Long - return WindowEvent(packageName, windowHeight, windowWidth) + val top = pigeonVar_list[1] as Long + val bottom = pigeonVar_list[2] as Long + val right = pigeonVar_list[3] as Long + val left = pigeonVar_list[4] as Long + return WindowEvent(packageName, top, bottom, right, left) } } fun toList(): List { return listOf( packageName, - windowHeight, - windowWidth, + top, + bottom, + right, + left, ) } override fun equals(other: Any?): Boolean { @@ -74,10 +125,7 @@ data class WindowEvent ( if (this === other) { return true } - return packageName == other.packageName - && windowHeight == other.windowHeight - && windowWidth == other.windowWidth - } + return AccessibilityApiPigeonUtils.deepEquals(toList(), other.toList()) } override fun hashCode(): Int = toList().hashCode() } @@ -85,6 +133,11 @@ private open class AccessibilityApiPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return when (type) { 129.toByte() -> { + return (readValue(buffer) as Long?)?.let { + MediaAction.ofRaw(it.toInt()) + } + } + 130.toByte() -> { return (readValue(buffer) as? List)?.let { WindowEvent.fromList(it) } @@ -94,8 +147,12 @@ private open class AccessibilityApiPigeonCodec : StandardMessageCodec() { } override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { when (value) { - is WindowEvent -> { + is MediaAction -> { stream.write(129) + writeValue(stream, value.raw) + } + is WindowEvent -> { + stream.write(130) writeValue(stream, value.toList()) } else -> super.writeValue(stream, value) @@ -109,7 +166,10 @@ val AccessibilityApiPigeonMethodCodec = StandardMethodCodec(AccessibilityApiPige interface Accessibility { fun hasPermission(): Boolean fun openPermissions() - fun performTouch(x: Double, y: Double) + fun performTouch(x: Double, y: Double, isKeyDown: Boolean, isKeyUp: Boolean) + fun controlMedia(action: MediaAction) + fun isRunning(): Boolean + fun ignoreHidDevices() companion object { /** The codec used by Accessibility. */ @@ -127,7 +187,7 @@ interface Accessibility { val wrapped: List = try { listOf(api.hasPermission()) } catch (exception: Throwable) { - wrapError(exception) + AccessibilityApiPigeonUtils.wrapError(exception) } reply.reply(wrapped) } @@ -143,7 +203,7 @@ interface Accessibility { api.openPermissions() listOf(null) } catch (exception: Throwable) { - wrapError(exception) + AccessibilityApiPigeonUtils.wrapError(exception) } reply.reply(wrapped) } @@ -158,11 +218,62 @@ interface Accessibility { val args = message as List val xArg = args[0] as Double val yArg = args[1] as Double + val isKeyDownArg = args[2] as Boolean + val isKeyUpArg = args[3] as Boolean val wrapped: List = try { - api.performTouch(xArg, yArg) + api.performTouch(xArg, yArg, isKeyDownArg, isKeyUpArg) listOf(null) } catch (exception: Throwable) { - wrapError(exception) + AccessibilityApiPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.accessibility.Accessibility.controlMedia$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val actionArg = args[0] as MediaAction + val wrapped: List = try { + api.controlMedia(actionArg) + listOf(null) + } catch (exception: Throwable) { + AccessibilityApiPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.accessibility.Accessibility.isRunning$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + listOf(api.isRunning()) + } catch (exception: Throwable) { + AccessibilityApiPigeonUtils.wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.accessibility.Accessibility.ignoreHidDevices$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = try { + api.ignoreHidDevices() + listOf(null) + } catch (exception: Throwable) { + AccessibilityApiPigeonUtils.wrapError(exception) } reply.reply(wrapped) } @@ -223,3 +334,16 @@ abstract class StreamEventsStreamHandler : AccessibilityApiPigeonEventChannelWra } } +abstract class HidKeyPressedStreamHandler : AccessibilityApiPigeonEventChannelWrapper { + companion object { + fun register(messenger: BinaryMessenger, streamHandler: HidKeyPressedStreamHandler, instanceName: String = "") { + var channelName: String = "dev.flutter.pigeon.accessibility.EventChannelMethods.hidKeyPressed" + if (instanceName.isNotEmpty()) { + channelName += ".$instanceName" + } + val internalStreamHandler = AccessibilityApiPigeonStreamHandler(streamHandler) + EventChannel(messenger, channelName, AccessibilityApiPigeonMethodCodec).setStreamHandler(internalStreamHandler) + } + } +} + diff --git a/accessibility/android/src/main/kotlin/de/jonasbark/accessibility/AccessibilityPlugin.kt b/accessibility/android/src/main/kotlin/de/jonasbark/accessibility/AccessibilityPlugin.kt index de02f0154..1d94282a3 100644 --- a/accessibility/android/src/main/kotlin/de/jonasbark/accessibility/AccessibilityPlugin.kt +++ b/accessibility/android/src/main/kotlin/de/jonasbark/accessibility/AccessibilityPlugin.kt @@ -1,48 +1,45 @@ package de.jonasbark.accessibility import Accessibility +import HidKeyPressedStreamHandler +import MediaAction import PigeonEventSink import StreamEventsStreamHandler import WindowEvent import android.content.Context import android.content.Intent +import android.graphics.Rect import android.os.Bundle import android.provider.Settings +import android.view.KeyEvent import androidx.core.content.ContextCompat.startActivity import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result /** AccessibilityPlugin */ -class AccessibilityPlugin: FlutterPlugin, MethodCallHandler, Accessibility { +class AccessibilityPlugin: FlutterPlugin, Accessibility { /// The MethodChannel that will the communication between Flutter and native Android /// /// This local reference serves to register the plugin with the Flutter Engine and unregister it /// when the Flutter Engine is detached from the Activity private lateinit var channel : MethodChannel private lateinit var context: Context - private lateinit var eventHandler: EventListener + private lateinit var windowEventHandler: WindowEventListener + private lateinit var hidEventHandler: HidEventListener override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "accessibility") - eventHandler = EventListener() + windowEventHandler = WindowEventListener() + hidEventHandler = HidEventListener() context = flutterPluginBinding.applicationContext Accessibility.setUp(flutterPluginBinding.binaryMessenger, this) - StreamEventsStreamHandler.register(flutterPluginBinding.binaryMessenger, eventHandler) - Observable.fromService = eventHandler - } - - override fun onMethodCall(call: MethodCall, result: Result) { - if (call.method == "getPlatformVersion") { - result.success("Android ${android.os.Build.VERSION.RELEASE}") - } else { - result.notImplemented() - } + StreamEventsStreamHandler.register(flutterPluginBinding.binaryMessenger, windowEventHandler) + HidKeyPressedStreamHandler.register(flutterPluginBinding.binaryMessenger, hidEventHandler) + Observable.fromServiceWindow = windowEventHandler + Observable.fromServiceKeys = hidEventHandler } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { @@ -54,32 +51,83 @@ class AccessibilityPlugin: FlutterPlugin, MethodCallHandler, Accessibility { return enabledServices != null && enabledServices.contains(context.packageName) } + override fun isRunning(): Boolean { + return Observable.toService != null + } + override fun openPermissions() { startActivity(context, Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK }, Bundle.EMPTY) } - override fun performTouch(x: Double, y: Double) { - Observable.toService?.performTouch(x = x, y = y) ?: error("Service not running") + override fun performTouch(x: Double, y: Double, isKeyDown: Boolean, isKeyUp: Boolean) { + Observable.toService?.performTouch(x = x, y = y, isKeyUp = isKeyUp, isKeyDown = isKeyDown) ?: error("Service not running") + } + + override fun controlMedia(action: MediaAction) { + val audioService = context.getSystemService(Context.AUDIO_SERVICE) as android.media.AudioManager + when (action) { + MediaAction.PLAY_PAUSE -> { + audioService.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)) + audioService.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)) + } + MediaAction.NEXT -> { + audioService.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_NEXT)) + audioService.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_NEXT)) + } + MediaAction.VOLUME_DOWN -> { + audioService.adjustVolume(android.media.AudioManager.ADJUST_LOWER, android.media.AudioManager.FLAG_SHOW_UI) + } + MediaAction.VOLUME_UP -> { + audioService.adjustVolume(android.media.AudioManager.ADJUST_RAISE, android.media.AudioManager.FLAG_SHOW_UI) + } + } + } + + override fun ignoreHidDevices() { + Observable.ignoreHidDevices = true } } -class EventListener : StreamEventsStreamHandler(), Receiver { +class WindowEventListener : StreamEventsStreamHandler(), Receiver { private var eventSink: PigeonEventSink? = null override fun onListen(p0: Any?, sink: PigeonEventSink) { eventSink = sink } - fun onEventsDone() { + override fun onCancel(p0: Any?) { eventSink?.endOfStream() eventSink = null } - override fun onChange(packageName: String, windowWidth: Int, windowHeight: Int) { - eventSink?.success(WindowEvent(packageName = packageName, windowWidth = windowWidth.toLong(), windowHeight = windowHeight.toLong())) + override fun onChange(packageName: String, window: Rect) { + eventSink?.success(WindowEvent(packageName = packageName, right = window.right.toLong(), left = window.left.toLong(), bottom = window.bottom.toLong(), top = window.top.toLong())) + } + + override fun onKeyEvent(event: KeyEvent) { + + } + +} + + +class HidEventListener : HidKeyPressedStreamHandler(), Receiver { + + private var keyEventSink: PigeonEventSink? = null + + override fun onListen(p0: Any?, sink: PigeonEventSink) { + keyEventSink = sink + } + + override fun onChange(packageName: String, window: Rect) { + } + override fun onKeyEvent(event: KeyEvent) { + val keyString = KeyEvent.keyCodeToString(event.keyCode) + keyEventSink?.success(keyString) + } } diff --git a/accessibility/android/src/main/kotlin/de/jonasbark/accessibility/AccessibilityService.kt b/accessibility/android/src/main/kotlin/de/jonasbark/accessibility/AccessibilityService.kt index c5b8632fe..cb1321825 100644 --- a/accessibility/android/src/main/kotlin/de/jonasbark/accessibility/AccessibilityService.kt +++ b/accessibility/android/src/main/kotlin/de/jonasbark/accessibility/AccessibilityService.kt @@ -3,13 +3,17 @@ package de.jonasbark.accessibility import android.accessibilityservice.AccessibilityService import android.accessibilityservice.GestureDescription import android.accessibilityservice.GestureDescription.StrokeDescription +import android.accessibilityservice.AccessibilityServiceInfo +import android.content.Context import android.graphics.Path import android.graphics.Rect +import android.media.AudioManager import android.os.Build import android.util.Log +import android.view.InputDevice +import android.view.KeyEvent import android.view.ViewConfiguration import android.view.accessibility.AccessibilityEvent -import android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_DISAPPEARED import android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED @@ -26,46 +30,87 @@ class AccessibilityService : AccessibilityService(), Listener { Observable.toService = null } + private val ignorePackages = listOf("com.android.systemui", "com.android.launcher", "com.android.settings") + override fun onAccessibilityEvent(event: AccessibilityEvent) { if (event.packageName == null || rootInActiveWindow == null) { return } - if (event.contentChangeTypes == CONTENT_CHANGE_TYPE_PANE_DISAPPEARED) { + if (event.eventType != TYPE_WINDOW_STATE_CHANGED || event.packageName in ignorePackages) { // we're not interested return } val currentPackageName = event.packageName.toString() val windowSize = getWindowSize() - Observable.fromService?.onChange(packageName = currentPackageName, windowHeight = windowSize.bottom, windowWidth = windowSize.right) + Observable.fromServiceWindow?.onChange(packageName = currentPackageName, window = windowSize) } private fun getWindowSize(): Rect { val outBounds = Rect() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - rootInActiveWindow.getBoundsInWindow(outBounds) + rootInActiveWindow?.getBoundsInScreen(outBounds) + return outBounds + } + + + override fun onInterrupt() { + Log.d("AccessibilityService", "Service Interrupted") + } + + override fun onServiceConnected() { + super.onServiceConnected() + // Request key event filtering so we receive onKeyEvent for hardware/HID media keys + try { + val info = serviceInfo ?: AccessibilityServiceInfo() + info.flags = info.flags or AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS + // keep other capabilities as defined in XML + setServiceInfo(info) + } catch (e: Exception) { + Log.w("AccessibilityService", "Failed to set service info for key events: ${e.message}") + } + } + + override fun onKeyEvent(event: KeyEvent): Boolean { + if (!Observable.ignoreHidDevices && isBleRemote(event)) { + // Handle media and volume keys from HID devices here + Log.d( + "AccessibilityService", + "onKeyEvent: keyCode=${event.keyCode} action=${event.action} scanCode=${event.scanCode} flags=${event.flags}" + ) + + // Forward key events to the plugin (Flutter) and swallow them so they don't propagate. + if (event.action == KeyEvent.ACTION_DOWN) { + Observable.fromServiceKeys?.onKeyEvent(event) + } + // Return true to indicate we've handled the event and it should be swallowed. + return true } else { - rootInActiveWindow.getBoundsInScreen(outBounds) + return false } - return outBounds } - private fun simulateTap(x: Double, y: Double) { + private fun isBleRemote(event: KeyEvent): Boolean { + val dev = InputDevice.getDevice(event.deviceId) ?: return false + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + dev.isExternal + } else { + true + } + } + + override fun performTouch(x: Double, y: Double, isKeyDown: Boolean, isKeyUp: Boolean) { val gestureBuilder = GestureDescription.Builder() val path = Path() path.moveTo(x.toFloat(), y.toFloat()) path.lineTo(x.toFloat()+1, y.toFloat()) - val stroke = StrokeDescription(path, 0, ViewConfiguration.getTapTimeout().toLong()) + val stroke = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + StrokeDescription(path, 0, ViewConfiguration.getTapTimeout().toLong(), isKeyDown && !isKeyUp) + } else { + // API 24–25: no “willContinue” support + StrokeDescription(path, 0L, ViewConfiguration.getTapTimeout().toLong()) + } gestureBuilder.addStroke(stroke) dispatchGesture(gestureBuilder.build(), null, null) } - - override fun onInterrupt() { - Log.d("AccessibilityService", "Service Interrupted") - } - - override fun performTouch(x: Double, y: Double) { - simulateTap(x, y) - } } diff --git a/accessibility/android/src/main/kotlin/de/jonasbark/accessibility/Listener.kt b/accessibility/android/src/main/kotlin/de/jonasbark/accessibility/Listener.kt index 95ce4b0f6..9cd2b82e2 100644 --- a/accessibility/android/src/main/kotlin/de/jonasbark/accessibility/Listener.kt +++ b/accessibility/android/src/main/kotlin/de/jonasbark/accessibility/Listener.kt @@ -1,14 +1,20 @@ package de.jonasbark.accessibility +import android.graphics.Rect +import android.view.KeyEvent + object Observable { var toService: Listener? = null - var fromService: Receiver? = null + var fromServiceWindow: Receiver? = null + var fromServiceKeys: Receiver? = null + var ignoreHidDevices: Boolean = false } interface Listener { - fun performTouch(x: Double, y: Double) + fun performTouch(x: Double, y: Double, isKeyDown: Boolean, isKeyUp: Boolean) } interface Receiver { - fun onChange(packageName: String, windowWidth: Int, windowHeight: Int) -} \ No newline at end of file + fun onChange(packageName: String, window: Rect) + fun onKeyEvent(event: KeyEvent) +} diff --git a/accessibility/api.dart b/accessibility/api.dart index 62e9e5518..5bffa859b 100644 --- a/accessibility/api.dart +++ b/accessibility/api.dart @@ -6,18 +6,35 @@ abstract class Accessibility { void openPermissions(); - void performTouch(double x, double y); + void performTouch(double x, double y, {bool isKeyDown = true, bool isKeyUp = false}); + + void controlMedia(MediaAction action); + + bool isRunning(); + + void ignoreHidDevices(); } +enum MediaAction { playPause, next, volumeUp, volumeDown } + class WindowEvent { final String packageName; - final int windowHeight; - final int windowWidth; + final int top; + final int bottom; + final int right; + final int left; - WindowEvent({required this.packageName, required this.windowHeight, required this.windowWidth}); + WindowEvent({ + required this.packageName, + required this.left, + required this.right, + required this.top, + required this.bottom, + }); } @EventChannelApi() abstract class EventChannelMethods { WindowEvent streamEvents(); + String hidKeyPressed(); } diff --git a/accessibility/lib/accessibility.dart b/accessibility/lib/accessibility.dart index e0ca46f86..0b4d9a36b 100644 --- a/accessibility/lib/accessibility.dart +++ b/accessibility/lib/accessibility.dart @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v25.2.0), do not edit directly. +// Autogenerated from Pigeon (v25.5.0), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -14,25 +14,54 @@ PlatformException _createConnectionError(String channelName) { message: 'Unable to establish connection on channel: "$channelName".', ); } +bool _deepEquals(Object? a, Object? b) { + if (a is List && b is List) { + return a.length == b.length && + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + } + if (a is Map && b is Map) { + return a.length == b.length && a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); + } + return a == b; +} + + +enum MediaAction { + playPause, + next, + volumeUp, + volumeDown, +} class WindowEvent { WindowEvent({ required this.packageName, - required this.windowHeight, - required this.windowWidth, + required this.top, + required this.bottom, + required this.right, + required this.left, }); String packageName; - int windowHeight; + int top; - int windowWidth; + int bottom; + + int right; + + int left; List _toList() { return [ packageName, - windowHeight, - windowWidth, + top, + bottom, + right, + left, ]; } @@ -43,8 +72,10 @@ class WindowEvent { result as List; return WindowEvent( packageName: result[0]! as String, - windowHeight: result[1]! as int, - windowWidth: result[2]! as int, + top: result[1]! as int, + bottom: result[2]! as int, + right: result[3]! as int, + left: result[4]! as int, ); } @@ -57,10 +88,7 @@ class WindowEvent { if (identical(this, other)) { return true; } - return - packageName == other.packageName - && windowHeight == other.windowHeight - && windowWidth == other.windowWidth; + return _deepEquals(encode(), other.encode()); } @override @@ -77,8 +105,11 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is WindowEvent) { + } else if (value is MediaAction) { buffer.putUint8(129); + writeValue(buffer, value.index); + } else if (value is WindowEvent) { + buffer.putUint8(130); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -89,6 +120,9 @@ class _PigeonCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 129: + final int? value = readValue(buffer) as int?; + return value == null ? null : MediaAction.values[value]; + case 130: return WindowEvent.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -162,14 +196,88 @@ class Accessibility { } } - Future performTouch(double x, double y) async { + Future performTouch(double x, double y, {bool isKeyDown = true, bool isKeyUp = false, }) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.accessibility.Accessibility.performTouch$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([x, y]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([x, y, isKeyDown, isKeyUp]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future controlMedia(MediaAction action) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.accessibility.Accessibility.controlMedia$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([action]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future isRunning() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.accessibility.Accessibility.isRunning$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } + + Future ignoreHidDevices() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.accessibility.Accessibility.ignoreHidDevices$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { @@ -197,3 +305,14 @@ Stream streamEvents( {String instanceName = ''}) { }); } +Stream hidKeyPressed( {String instanceName = ''}) { + if (instanceName.isNotEmpty) { + instanceName = '.$instanceName'; + } + final EventChannel hidKeyPressedChannel = + EventChannel('dev.flutter.pigeon.accessibility.EventChannelMethods.hidKeyPressed$instanceName', pigeonMethodCodec); + return hidKeyPressedChannel.receiveBroadcastStream().map((dynamic event) { + return event as String; + }); +} + diff --git a/analysis_options.yaml b/analysis_options.yaml index 0d2902135..017f44842 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -23,6 +23,10 @@ linter: rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + - require_trailing_commas # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options +formatter: + page_width: 120 + trailing_commas: preserve diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index ab2534c3f..af5c72f1b 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,3 +1,6 @@ +import java.io.FileInputStream +import java.util.* + plugins { id("com.android.application") id("kotlin-android") @@ -5,8 +8,13 @@ plugins { id("dev.flutter.flutter-gradle-plugin") } + +val keystorePropertiesFile = rootProject.file("keystore.properties") +val keystoreProperties = Properties() +keystoreProperties.load(FileInputStream(keystorePropertiesFile)) + android { - namespace = "de.jonasbark.swift_play" + namespace = "de.jonasbark.swiftcontrol" compileSdk = flutter.compileSdkVersion ndkVersion = "27.0.12077973" @@ -24,7 +32,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "de.jonasbark.swift_play" + applicationId = "de.jonasbark.swiftcontrol" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = 24 @@ -33,11 +41,18 @@ android { versionName = flutter.versionName } + signingConfigs { + create("config") { + keyAlias = keystoreProperties["keyAlias"] as String + keyPassword = keystoreProperties["keyPassword"] as String + storeFile = file("../${keystoreProperties["storeFile"] as String}") + storePassword = keystoreProperties["storePassword"] as String + } + } + buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig = signingConfigs.getByName("debug") + signingConfig = signingConfigs.getByName("config") } } } diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 399f6981d..000000000 --- a/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0849e5c09..9e9fefe1b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,9 @@ - + + + @@ -16,11 +18,14 @@ - + + + + Boolean)? = null + var motionListener: ((MotionEvent) -> Boolean)? = null + + override fun isGamepadsInputDevice(device: InputDevice): Boolean { + return device.sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD + || device.sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK + // Some bluetooth keyboards are identified as GamePad. Check if it is ALPHABETIC keyboard. + // && device.keyboardType != InputDevice.KEYBOARD_TYPE_ALPHABETIC + } + + override fun dispatchGenericMotionEvent(motionEvent: MotionEvent): Boolean { + return motionListener?.invoke(motionEvent) ?: false + } + + override fun dispatchKeyEvent(keyEvent: KeyEvent): Boolean { + if (keyListener?.invoke(keyEvent) == true) { + return true + } + return super.dispatchKeyEvent(keyEvent) + } + + override fun registerInputDeviceListener( + listener: InputManager.InputDeviceListener, handler: Handler?) { + val inputManager = getSystemService(INPUT_SERVICE) as InputManager + inputManager.registerInputDeviceListener(listener, null) + } + + override fun registerKeyEventHandler(handler: (KeyEvent) -> Boolean) { + keyListener = handler + } + + override fun registerMotionEventHandler(handler: (MotionEvent) -> Boolean) { + motionListener = handler + } +} diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml index f74085f3f..0944b4daa 100644 --- a/android/app/src/main/res/drawable-v21/launch_background.xml +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -4,9 +4,9 @@ - + android:src="@mipmap/ic_launcher" /> + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml index 304732f88..6a36c4f3f 100644 --- a/android/app/src/main/res/drawable/launch_background.xml +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -4,9 +4,9 @@ - + android:src="@mipmap/ic_launcher" /> + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..345888d26 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index 8c8df10b1..f0384c1c4 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png new file mode 100644 index 000000000..af72f79c7 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..60ac95757 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png new file mode 100644 index 000000000..a10c890c9 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 8bc86a0d4..d0221a8bb 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png new file mode 100644 index 000000000..9b7e478cf Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..70f013997 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png new file mode 100644 index 000000000..f5fd932f9 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 2202b760a..2cf94b05f 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png new file mode 100644 index 000000000..b8153b6a5 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..d15aa8417 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png new file mode 100644 index 000000000..67f032f38 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 9c27415d0..27a4345a4 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png new file mode 100644 index 000000000..19c325bc4 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..39f2d71fd Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png new file mode 100644 index 000000000..1d23cb711 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 1e24bd1a6..de47f51ae 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png new file mode 100644 index 000000000..4228e59dd Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..eba5a733f Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png new file mode 100644 index 000000000..1d029e23b Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/xml/accessibility_service_config.xml b/android/app/src/main/res/xml/accessibility_service_config.xml index 9afa975b0..3c58ab7f9 100644 --- a/android/app/src/main/res/xml/accessibility_service_config.xml +++ b/android/app/src/main/res/xml/accessibility_service_config.xml @@ -1,8 +1,10 @@ + diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index a439442c2..ad996d112 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -18,8 +18,8 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.7.0" apply false - id("org.jetbrains.kotlin.android") version "1.8.22" apply false + id("com.android.application") version "8.7.3" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false } include(":app") diff --git a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/bug_report.md b/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 50a4c7b8b..000000000 --- a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: Bug Report -about: Create a report to help us improve -title: "fix: " -labels: bug ---- - -**Description** - -A clear and concise description of what the bug is. - -**Steps To Reproduce** - -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected Behavior** - -A clear and concise description of what you expected to happen. - -**Screenshots** - -If applicable, add screenshots to help explain your problem. - -**Additional Context** - -Add any other context about the problem here. diff --git a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/build.md b/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/build.md deleted file mode 100644 index 0cf8e62cd..000000000 --- a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/build.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Build System -about: Changes that affect the build system or external dependencies -title: "build: " -labels: build ---- - -**Description** - -Describe what changes need to be done to the build system and why. - -**Requirements** - -- [ ] The build system is passing diff --git a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/chore.md b/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/chore.md deleted file mode 100644 index 498ebfd82..000000000 --- a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/chore.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Chore -about: Other changes that don't modify src or test files -title: "chore: " -labels: chore ---- - -**Description** - -Clearly describe what change is needed and why. If this changes code then please use another issue type. - -**Requirements** - -- [ ] No functional changes to the code diff --git a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/ci.md b/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/ci.md deleted file mode 100644 index fa2dd9e2d..000000000 --- a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/ci.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Continuous Integration -about: Changes to the CI configuration files and scripts -title: "ci: " -labels: ci ---- - -**Description** - -Describe what changes need to be done to the ci/cd system and why. - -**Requirements** - -- [ ] The ci system is passing diff --git a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/config.yml b/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index ec4bb386b..000000000 --- a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1 +0,0 @@ -blank_issues_enabled: false \ No newline at end of file diff --git a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/documentation.md b/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/documentation.md deleted file mode 100644 index f494a4d98..000000000 --- a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/documentation.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Documentation -about: Improve the documentation so all collaborators have a common understanding -title: "docs: " -labels: documentation ---- - -**Description** - -Clearly describe what documentation you are looking to add or improve. - -**Requirements** - -- [ ] Requirements go here diff --git a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/feature_request.md b/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index ddd2fcca9..000000000 --- a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: Feature Request -about: A new feature to be added to the project -title: "feat: " -labels: feature ---- - -**Description** - -Clearly describe what you are looking to add. The more context the better. - -**Requirements** - -- [ ] Checklist of requirements to be fulfilled - -**Additional Context** - -Add any other context or screenshots about the feature request go here. diff --git a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/performance.md b/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/performance.md deleted file mode 100644 index 699b8d45f..000000000 --- a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/performance.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Performance Update -about: A code change that improves performance -title: "perf: " -labels: performance ---- - -**Description** - -Clearly describe what code needs to be changed and what the performance impact is going to be. Bonus point's if you can tie this directly to user experience. - -**Requirements** - -- [ ] There is no drop in test coverage. diff --git a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/refactor.md b/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/refactor.md deleted file mode 100644 index 1626c5704..000000000 --- a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/refactor.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Refactor -about: A code change that neither fixes a bug nor adds a feature -title: "refactor: " -labels: refactor ---- - -**Description** - -Clearly describe what needs to be refactored and why. Please provide links to related issues (bugs or upcoming features) in order to help prioritize. - -**Requirements** - -- [ ] There is no drop in test coverage. diff --git a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/revert.md b/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/revert.md deleted file mode 100644 index 9d121dc56..000000000 --- a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/revert.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: Revert Commit -about: Reverts a previous commit -title: "revert: " -labels: revert ---- - -**Description** - -Provide a link to a PR/Commit that you are looking to revert and why. - -**Requirements** - -- [ ] Change has been reverted -- [ ] No change in test coverage has happened -- [ ] A new ticket is created for any follow on work that needs to happen diff --git a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/style.md b/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/style.md deleted file mode 100644 index 02244a7bd..000000000 --- a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/style.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Style Changes -about: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc) -title: "style: " -labels: style ---- - -**Description** - -Clearly describe what you are looking to change and why. - -**Requirements** - -- [ ] There is no drop in test coverage. diff --git a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/test.md b/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/test.md deleted file mode 100644 index 431a7ea76..000000000 --- a/flutter_blue_plus_windows/.github/ISSUE_TEMPLATE/test.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Test -about: Adding missing tests or correcting existing tests -title: "test: " -labels: test ---- - -**Description** - -List out the tests that need to be added or changed. Please also include any information as to why this was not covered in the past. - -**Requirements** - -- [ ] There is no drop in test coverage. diff --git a/flutter_blue_plus_windows/.github/PULL_REQUEST_TEMPLATE.md b/flutter_blue_plus_windows/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 116993637..000000000 --- a/flutter_blue_plus_windows/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,27 +0,0 @@ - - -## Status - -**READY/IN DEVELOPMENT/HOLD** - -## Description - - - -## Type of Change - - - -- [ ] ✨ New feature (non-breaking change which adds functionality) -- [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) -- [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) -- [ ] 🧹 Code refactor -- [ ] ✅ Build configuration change -- [ ] 📝 Documentation -- [ ] 🗑️ Chore diff --git a/flutter_blue_plus_windows/.github/cspell.json b/flutter_blue_plus_windows/.github/cspell.json deleted file mode 100644 index b5f42e1ca..000000000 --- a/flutter_blue_plus_windows/.github/cspell.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "version": "0.2", - "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", - "dictionaries": ["vgv_allowed", "vgv_forbidden"], - "dictionaryDefinitions": [ - { - "name": "vgv_allowed", - "path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/allowed.txt", - "description": "Allowed VGV Spellings" - }, - { - "name": "vgv_forbidden", - "path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/forbidden.txt", - "description": "Forbidden VGV Spellings" - } - ], - "useGitignore": true, - "words": [ - "flutter_blue_plus_windows" - ] -} diff --git a/flutter_blue_plus_windows/.github/dependabot.yaml b/flutter_blue_plus_windows/.github/dependabot.yaml deleted file mode 100644 index 63b035cde..000000000 --- a/flutter_blue_plus_windows/.github/dependabot.yaml +++ /dev/null @@ -1,11 +0,0 @@ -version: 2 -enable-beta-ecosystems: true -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - - package-ecosystem: "pub" - directory: "/" - schedule: - interval: "daily" diff --git a/flutter_blue_plus_windows/.gitignore b/flutter_blue_plus_windows/.gitignore deleted file mode 100644 index 6936c89c3..000000000 --- a/flutter_blue_plus_windows/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -# See https://www.dartlang.org/guides/libraries/private-files - -# Files and directories created by pub -.dart_tool/ -.packages -build/ -pubspec.lock -/.idea/ -/.flutter-plugins -/.flutter-plugins-dependencies - -interanl_example \ No newline at end of file diff --git a/flutter_blue_plus_windows/CHANGELOG.md b/flutter_blue_plus_windows/CHANGELOG.md deleted file mode 100644 index b8f020afe..000000000 --- a/flutter_blue_plus_windows/CHANGELOG.md +++ /dev/null @@ -1,106 +0,0 @@ -## 1.26.1 -* Add setOptions. - -## 1.26.0 -* Set `flutter_blue_plus` upperbound <1.35.0 (due to api changes) - -## 1.25.0 -* Ensure tracing connection when reconnection occurs after force disconnection. - -## 1.24.22 -* Upgrade FBP version to `>=1.32.4 <=1.40.0` #24. - -## 1.24.21 -* fix: startScan() doesn't return correct ScanResult #25 - -## 1.24.20 -* Downgrade FBP version to `>=1.32.4 <=1.33.6` due to the breaking changes. -* After upgrade process, the dependencies will be returned to `>=1.34.4 <1.40.0` #24. - -## 1.24.19 -* Fix a bug with `onValueReceived` of emitting write packet #22. - -## 1.24.18 -* Add implementation of `BluetoothDeviceWindow.fromId()` #21. - -## 1.24.15 -* Fix a bug w.r.t. company ID in manufacturer data. (@betto-a #18) - -## 1.24.14 -* Implement cancelOnDisconnect (@jefflongo #16) - -## 1.24.12 -* Fix minor bug w.r.t. `characteristic.isNotifying`. - -## 1.24.11 -* Fix breaking changes of FBP w.r.t. `systemDevices(List withServices)`. - -## 1.24.10 -* Add support for `cancelWhenScanComplete` - -## 1.24.9 -* Implement scan filter (including `withServices`, `withRemoteIds`, `withNames`). - -## 1.24.8 -* Keep manufacturer data when scanning. - -## 1.24.7 -* Keep service uuids when scanning. - -## 1.24.0 -* Update `README.md`. - -## 1.23.6 -* Add unimplemented notification for `read` or `write`. - -## 1.14.0 -* Remove dependencies `ffi` and `win32` to avoid compile error for web - -## 1.9.5 -* Apply `flutter blue plus` to `1.28.13`. - -## 1.9.0 -* Apply a breaking changes `Guid` in `Flutter blue plus` packages. -* Use `uuid128` instead of `toString()`. - -## 1.8.10 -* Fix `Guid` bug related with `Flutter blue plus` packages. - -## 1.8.0 -* Fix bug with Guid converted from string due to starting/ending with '{ }' in `WinBLE` - -## 1.7.0 -* Apply `flutter blue plus 1.28.5` (there is several breaking changes.). - -## 1.6.6 -* Add cache for storing characteristics. - -## 1.6.0 -* Apply `Flutter blue plus 1.26.0`, (there is a breaking change with `connect()`). - -## 1.5.7 -* Remove connection by OS when performing `startScan`. - -## 1.5.3 -* Write logs when connection state stream is started/terminated. - -## 1.5.2 -* Fix a bug of features added in `1.5.1` - -## 1.5.1 -* Remove device from connected device list when device is disconnected. - -## 1.5.0 -* Split functionality of `disconnect` / `removeBond`. - -## 1.4.0 -* Implement `Subscribe/Unsubscribe Characteristic`. - -## 1.1.0 -* Implement `Read/Write Characteristic`. - -## 1.0.5 -* Change `rxdart` version to `0.27.7`. - -## 1.0.0 -* Initial release (using Github action). \ No newline at end of file diff --git a/flutter_blue_plus_windows/LICENSE b/flutter_blue_plus_windows/LICENSE deleted file mode 100644 index 64edbab0f..000000000 --- a/flutter_blue_plus_windows/LICENSE +++ /dev/null @@ -1,7 +0,0 @@ -Copyright 2023 Himchan Park - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/flutter_blue_plus_windows/README.md b/flutter_blue_plus_windows/README.md deleted file mode 100644 index 3627c78e9..000000000 --- a/flutter_blue_plus_windows/README.md +++ /dev/null @@ -1,54 +0,0 @@ -[![pub package](https://img.shields.io/pub/v/flutter_blue_plus_windows.svg)](https://pub.dartlang.org/packages/flutter_blue_plus_windows) - -## Flutter Blue Plus Windows - -This project is a wrapper library for `Flutter Blue Plus` and `Win_ble`. -It allows `Flutter_blue_plus` to operate on Windows. - -With minimal effort, you can use Flutter Blue Plus on Windows. - -## Usage -Only you need to do is change the import statement. - -```dart -// instead of import 'package:flutter_blue_plus/flutter_blue_plus.dart'; -import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart'; - -// Alternatively, you can hide FlutterBluePlus when importing the FBP statement -import 'package:flutter_blue_plus/flutter_blue_plus.dart' hide FlutterBluePlus; -import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart'; - -``` - -### Scan devices -```dart -final scannedDevices = {}; - -const timeout = Duration(seconds: 3); -FlutterBluePlus.startScan(timeout: timeout); - -final sub = FlutterBluePlus.scanResults.expand((e)=>e).listen(scannedDevices.add); - -await Future.delayed(timeout); -sub.cancel(); -scannedDevices.forEach(print); -``` - -### Connect a device -```dart -final scannedDevice = scannedDevices - .where((scanResult) => scanResult.device.platformName == DEVICE_NAME) - .firstOrNull; -final device = scannedDevice?.device; -device?.connect(); -``` - -### Disconnect the device -```dart -device?.disconnect(); -``` - -Check out the usage of Flutter Blue Plus on [Flutter Blue Plus](https://pub.dev/packages/flutter_blue_plus) - - - diff --git a/flutter_blue_plus_windows/lib/flutter_blue_plus_windows.dart b/flutter_blue_plus_windows/lib/flutter_blue_plus_windows.dart deleted file mode 100644 index 97fd9254d..000000000 --- a/flutter_blue_plus_windows/lib/flutter_blue_plus_windows.dart +++ /dev/null @@ -1,7 +0,0 @@ -library flutter_blue_plus_windows; - -export 'package:flutter_blue_plus/flutter_blue_plus.dart' hide FlutterBluePlus; -export 'package:win_ble/win_ble.dart'; -export 'package:win_ble/win_file.dart'; - -export 'src/flutter_blue_plus_windows.dart'; diff --git a/flutter_blue_plus_windows/lib/src/extension/bluetooth_adapter_state_extension.dart b/flutter_blue_plus_windows/lib/src/extension/bluetooth_adapter_state_extension.dart deleted file mode 100644 index b15adb60a..000000000 --- a/flutter_blue_plus_windows/lib/src/extension/bluetooth_adapter_state_extension.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flutter_blue_plus/flutter_blue_plus.dart'; -import 'package:win_ble/win_ble.dart'; - -extension BluetoothAdapterStateExtension on BleState { - BluetoothAdapterState toAdapterState() { - switch(this){ - case BleState.On: - return BluetoothAdapterState.on; - case BleState.Off: - return BluetoothAdapterState.off; - case BleState.Unknown: - return BluetoothAdapterState.unknown; - case BleState.Disabled: - return BluetoothAdapterState.unavailable; - case BleState.Unsupported: - return BluetoothAdapterState.unauthorized; - } - } -} \ No newline at end of file diff --git a/flutter_blue_plus_windows/lib/src/extension/bluetooth_characteristic_extension.dart b/flutter_blue_plus_windows/lib/src/extension/bluetooth_characteristic_extension.dart deleted file mode 100644 index 3f111ae4f..000000000 --- a/flutter_blue_plus_windows/lib/src/extension/bluetooth_characteristic_extension.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart'; -import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart'; - -extension BluetoothCharacteristicExtension on BluetoothCharacteristic { - BmBluetoothCharacteristic toProto() { - return BmBluetoothCharacteristic( - remoteId: DeviceIdentifier(remoteId.str), - serviceUuid: serviceUuid, - characteristicUuid: characteristicUuid, - descriptors: [for (final d in descriptors) d.toProto()], - properties: properties.toProto(), - primaryServiceUuid: null, // TODO: API changes - ); - } -} diff --git a/flutter_blue_plus_windows/lib/src/extension/bluetooth_descriptor_extension.dart b/flutter_blue_plus_windows/lib/src/extension/bluetooth_descriptor_extension.dart deleted file mode 100644 index 6bc3e34ed..000000000 --- a/flutter_blue_plus_windows/lib/src/extension/bluetooth_descriptor_extension.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:flutter_blue_plus/flutter_blue_plus.dart'; -import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart'; - -extension BluetoothDescriptorExtension on BluetoothDescriptor { - BmBluetoothDescriptor toProto() { - return BmBluetoothDescriptor( - remoteId: DeviceIdentifier(remoteId.str), - serviceUuid: serviceUuid, - characteristicUuid: characteristicUuid, - descriptorUuid: descriptorUuid, - primaryServiceUuid: null, // TODO: API changes - ); - } -} diff --git a/flutter_blue_plus_windows/lib/src/extension/bluetooth_service_extension.dart b/flutter_blue_plus_windows/lib/src/extension/bluetooth_service_extension.dart deleted file mode 100644 index c2b0af5aa..000000000 --- a/flutter_blue_plus_windows/lib/src/extension/bluetooth_service_extension.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart'; -import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart'; - -extension BluetoothServiceExtension on BluetoothService { - BmBluetoothService toProto() { - return BmBluetoothService( - serviceUuid: serviceUuid, - remoteId: DeviceIdentifier(remoteId.str), - characteristics: [for (final c in characteristics) c.toProto()], - primaryServiceUuid: null, // TODO: API changes - ); - } -} diff --git a/flutter_blue_plus_windows/lib/src/extension/characteristic_properties_extension.dart b/flutter_blue_plus_windows/lib/src/extension/characteristic_properties_extension.dart deleted file mode 100644 index b61baaab1..000000000 --- a/flutter_blue_plus_windows/lib/src/extension/characteristic_properties_extension.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flutter_blue_plus/flutter_blue_plus.dart'; -import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart'; - -extension CharacteristicPropertiesExtension on CharacteristicProperties { - BmCharacteristicProperties toProto() { - return BmCharacteristicProperties( - broadcast: broadcast, - read: read, - writeWithoutResponse: writeWithoutResponse, - write: write, - notify: notify, - indicate: indicate, - authenticatedSignedWrites: authenticatedSignedWrites, - extendedProperties: extendedProperties, - notifyEncryptionRequired: notifyEncryptionRequired, - indicateEncryptionRequired: indicateEncryptionRequired, - ); - } -} diff --git a/flutter_blue_plus_windows/lib/src/extension/extension.dart b/flutter_blue_plus_windows/lib/src/extension/extension.dart deleted file mode 100644 index 78c24f314..000000000 --- a/flutter_blue_plus_windows/lib/src/extension/extension.dart +++ /dev/null @@ -1,5 +0,0 @@ -export 'bluetooth_adapter_state_extension.dart'; -export 'bluetooth_characteristic_extension.dart'; -export 'bluetooth_descriptor_extension.dart'; -export 'bluetooth_service_extension.dart'; -export 'characteristic_properties_extension.dart'; diff --git a/flutter_blue_plus_windows/lib/src/flutter_blue_plus_windows.dart b/flutter_blue_plus_windows/lib/src/flutter_blue_plus_windows.dart deleted file mode 100644 index cca4fddb3..000000000 --- a/flutter_blue_plus_windows/lib/src/flutter_blue_plus_windows.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'extension/extension.dart'; -export 'windows/windows.dart'; -export 'wrapper/wrapper.dart'; diff --git a/flutter_blue_plus_windows/lib/src/windows/bluetooth_characteristic_windows.dart b/flutter_blue_plus_windows/lib/src/windows/bluetooth_characteristic_windows.dart deleted file mode 100644 index e5d47efc8..000000000 --- a/flutter_blue_plus_windows/lib/src/windows/bluetooth_characteristic_windows.dart +++ /dev/null @@ -1,160 +0,0 @@ -part of 'windows.dart'; - -class BluetoothCharacteristicWindows extends BluetoothCharacteristic { - final DeviceIdentifier remoteId; - final Guid serviceUuid; - final Guid? secondaryServiceUuid; - final Guid characteristicUuid; - final List descriptors; - - final Properties propertiesWinBle; - - BluetoothCharacteristicWindows({ - required this.remoteId, - required this.serviceUuid, - required this.characteristicUuid, - required this.descriptors, - required this.propertiesWinBle, - this.secondaryServiceUuid, - }) : super.fromProto( - BmBluetoothCharacteristic( - remoteId: DeviceIdentifier(remoteId.str), - serviceUuid: serviceUuid, - characteristicUuid: characteristicUuid, - descriptors: [ - for (final descriptor in descriptors) - BmBluetoothDescriptor( - remoteId: DeviceIdentifier(descriptor.remoteId.str), - serviceUuid: descriptor.serviceUuid, - characteristicUuid: descriptor.characteristicUuid, - descriptorUuid: descriptor.uuid, - primaryServiceUuid: null, // TODO: API changes - ), - ], - properties: BmCharacteristicProperties( - broadcast: propertiesWinBle.broadcast ?? false, - read: propertiesWinBle.read ?? false, - writeWithoutResponse: propertiesWinBle.writeWithoutResponse ?? false, - write: propertiesWinBle.write ?? false, - notify: propertiesWinBle.notify ?? false, - indicate: propertiesWinBle.indicate ?? false, - authenticatedSignedWrites: propertiesWinBle.authenticatedSignedWrites ?? false, - // TODO: implementation missing - extendedProperties: false, - // TODO: implementation missing - notifyEncryptionRequired: false, - // TODO: implementation missing - indicateEncryptionRequired: false, - ), - primaryServiceUuid: null, // TODO: API changes - ), - ); - - String get _address => remoteId.str.toLowerCase(); - - String get _key => "$serviceUuid:$characteristicUuid"; - - FBP.BluetoothDevice get device => - FlutterBluePlusWindows.connectedDevices.firstWhere((device) => device.remoteId == remoteId); - - /// this variable is updated: - /// - anytime `read()` is called - /// - anytime `write()` is called - /// - anytime a notification arrives (if subscribed) - List get lastValue => FlutterBluePlusWindows._lastChrs[remoteId]?[_key] ?? []; - - /// this stream emits values: - /// - anytime `read()` is called - /// - anytime `write()` is called - /// - anytime a notification arrives (if subscribed) - /// - and when first listened to, it re-emits the last value for convenience - Stream> get lastValueStream => _mergeStreams( - [ - WinBle.characteristicValueStreamOf( - address: _address, - serviceId: serviceUuid.str128, - characteristicId: characteristicUuid.str128, - ), - FlutterBluePlusWindows._charReadWriteStream.where((e) => e.$1 == _key).map((e) => e.$2) - ], - ).map((p) => [...p]).newStreamWithInitialValue(lastValue).asBroadcastStream(); - - /// this stream emits values: - /// - anytime `read()` is called - /// - anytime a notification arrives (if subscribed) - Stream> get onValueReceived => _mergeStreams( - [ - WinBle.characteristicValueStreamOf( - address: _address, - serviceId: serviceUuid.str128, - characteristicId: characteristicUuid.str128, - ), - FlutterBluePlusWindows._charReadStream.where((e) => e.$1 == _key).map((e) => e.$2) - ], - ).map((p) => [...p]).asBroadcastStream(); - - // TODO: need to verify - bool get isNotifying => FlutterBluePlusWindows._isNotifying[remoteId]?[_key] ?? false; - - Future> read({int timeout = 15}) async { - final value = await WinBle.read( - address: _address, - serviceId: serviceUuid.str128, - characteristicId: characteristicUuid.str128, - ); - FlutterBluePlusWindows._charReadWriteStreamController.add((_key, value)); - FlutterBluePlusWindows._charReadStreamController.add((_key, value)); - FlutterBluePlusWindows._lastChrs[remoteId] ??= {}; - FlutterBluePlusWindows._lastChrs[remoteId]?[_key] = value; - return value; - } - - Future write(List value, - {bool allowLongWrite = false, bool withoutResponse = false, int timeout = 15}) async { - await WinBle.write( - address: _address, - service: serviceUuid.str128, - characteristic: characteristicUuid.str128, - data: Uint8List.fromList(value), - writeWithResponse: !withoutResponse, // propertiesWinBle.writeWithoutResponse ?? false, - ); - FlutterBluePlusWindows._charReadWriteStreamController.add((_key, value)); - FlutterBluePlusWindows._lastChrs[remoteId] ??= {}; - FlutterBluePlusWindows._lastChrs[remoteId]?[_key] = value; - } - - // TODO: need to verify - Future setNotifyValue( - bool notify, { - int timeout = 15, // TODO: missing implementation - bool forceIndications = false, // TODO: missing implementation - }) async { - /// unSubscribeFromCharacteristic - try { - await WinBle.unSubscribeFromCharacteristic( - address: _address, - serviceId: serviceUuid.str128, - characteristicId: characteristicUuid.str128, - ); - } catch (e) { - log('WinBle.unSubscribeFromCharacteristic was performed ' - 'before setNotifyValue()'); - } - - /// set notify - try { - if (notify) { - await WinBle.subscribeToCharacteristic( - address: _address, - serviceId: serviceUuid.str128, - characteristicId: characteristicUuid.str128, - ); - } - FlutterBluePlusWindows._isNotifying[remoteId] ??= {}; - FlutterBluePlusWindows._isNotifying[remoteId]?[_key] = notify; - } catch (e) { - log(e.toString()); - } - return true; - } -} diff --git a/flutter_blue_plus_windows/lib/src/windows/bluetooth_device_windows.dart b/flutter_blue_plus_windows/lib/src/windows/bluetooth_device_windows.dart deleted file mode 100644 index 16929d00b..000000000 --- a/flutter_blue_plus_windows/lib/src/windows/bluetooth_device_windows.dart +++ /dev/null @@ -1,310 +0,0 @@ -// Bluetooth Device Page: -// https://github.com/boskokg/flutter_blue_plus/blob/master/lib/src/bluetooth_device.dart - -part of 'windows.dart'; - -class BluetoothDeviceWindows extends FBP.BluetoothDevice { - BluetoothDeviceWindows({required super.remoteId}); - - // used for 'servicesStream' public api - final _services = StreamController>.broadcast(); - - // used for 'isDiscoveringServices' public api - final _isDiscoveringServices = _StreamController(initialValue: false); - - String get _address => remoteId.str.toLowerCase(); - - /// Create a device from an id - /// - to connect, this device must have been discovered by your app in a previous scan - /// - iOS uses 128-bit uuids the remoteId, e.g. e006b3a7-ef7b-4980-a668-1f8005f84383 - /// - Android uses 48-bit mac addresses as the remoteId, e.g. 06:E5:28:3B:FD:E0 - static FBP.BluetoothDevice fromId(String remoteId) { - if (Platform.isWindows) { - return BluetoothDeviceWindows(remoteId: DeviceIdentifier(remoteId.toUpperCase())); - } - return FBP.BluetoothDevice.fromId(remoteId); - } - - /// platform name - /// - this name is kept track of by the platform - /// - this name usually persist between app restarts - /// - iOS: after you connect, iOS uses the GAP name characteristic (0x2A00) - /// if it exists. Otherwise iOS use the advertised name. - /// - Android: always uses the advertised name - String get platformName => FlutterBluePlusWindows._platformNames[remoteId] ?? ""; - - /// Advertised Named - /// - this is the name advertised by the device during scanning - /// - it is only available after you scan with FlutterBluePlus - /// - it is cleared when the app restarts. - /// - not all devices advertise a name - String get advName => FlutterBluePlusWindows._advNames[remoteId] ?? ""; - - // stream return whether or not we are currently discovering services - @Deprecated("planed for removal (Jan 2024). It can be easily implemented yourself") // deprecated on Aug 2023 - Stream get isDiscoveringServices => _isDiscoveringServices.stream; - - /// Get services - /// - returns empty if discoverServices() has not been called - /// or if your device does not have any services (rare) - List get servicesList => FlutterBluePlusWindows._knownServices[remoteId] ?? []; - - /// Stream of bluetooth services offered by the remote device - /// - this stream is only updated when you call discoverServices() - @Deprecated("planed for removal (Jan 2024). It can be easily implemented yourself") // deprecated on Aug 2023 - Stream> get servicesStream { - if (FlutterBluePlusWindows._knownServices[remoteId] != null) { - return _services.stream.newStreamWithInitialValue( - FlutterBluePlusWindows._knownServices[remoteId]!, - ); - } else { - return _services.stream; - } - } - - /// Register a subscription to be canceled when the device is disconnected. - /// This function simplifies cleanup, so you can prevent creating duplicate stream subscriptions. - /// - this is an optional convenience function - /// - prevents accidentally creating duplicate subscriptions on each reconnection. - /// - [next] if true, the the stream will be canceled only on the *next* disconnection. - /// This is useful if you setup your subscriptions before you connect. - /// - [delayed] Note: This option is only meant for `connectionState` subscriptions. - /// When `true`, we cancel after a small delay. This ensures the `connectionState` - /// listener receives the `disconnected` event. - void cancelWhenDisconnected(StreamSubscription subscription, {bool next = false, bool delayed = false}) { - if (isConnected == false && next == false) { - subscription.cancel(); // cancel immediately if already disconnected. - } else if (delayed) { - FlutterBluePlusWindows._delayedSubscriptions[remoteId] ??= []; - FlutterBluePlusWindows._delayedSubscriptions[remoteId]!.add(subscription); - } else { - FlutterBluePlusWindows._deviceSubscriptions[remoteId] ??= []; - FlutterBluePlusWindows._deviceSubscriptions[remoteId]!.add(subscription); - } - } - - /// Returns true if this device is currently connected to your app - bool get isConnected { - return FlutterBluePlusWindows.connectedDevices.contains(this); - } - - /// Returns true if this device is currently disconnected from your app - bool get isDisconnected => isConnected == false; - - Future connect({ - Duration? timeout = const Duration(seconds: 35), // TODO: implementation missing - bool autoConnect = false, // TODO: implementation missing - int? mtu = 512, // TODO: implementation missing - }) async { - try { - await WinBle.connect(_address); - FlutterBluePlusWindows._deviceSet.add(this); - } catch (e) { - log(e.toString()); - } - } - - Future disconnect({ - int androidDelay = 2000, // TODO: implementation missing - int timeout = 35, // TODO: implementation missing - bool queue = true, // TODO: implementation missing - }) async { - try { - await WinBle.disconnect(_address); - } catch (e) { - log(e.toString()); - } finally { - FlutterBluePlusWindows._deviceSet.remove(this); - - FlutterBluePlusWindows._deviceSubscriptions[remoteId]?.forEach((s) => s.cancel()); - FlutterBluePlusWindows._deviceSubscriptions.remove(remoteId); - // use delayed to update the stream before we cancel it - Future.delayed(Duration.zero).then((_) { - FlutterBluePlusWindows._delayedSubscriptions[remoteId]?.forEach((s) => s.cancel()); - FlutterBluePlusWindows._delayedSubscriptions.remove(remoteId); - }); - - FlutterBluePlusWindows._lastChrs[remoteId]?.clear(); - FlutterBluePlusWindows._isNotifying[remoteId]?.clear(); - } - } - - Future> discoverServices({ - bool subscribeToServicesChanged = true, // TODO: implementation missing - int timeout = 15, // TODO: implementation missing - }) async { - List result = List.from(FlutterBluePlusWindows._knownServices[remoteId] ?? []); - - try { - _isDiscoveringServices.add(true); - - final response = await WinBle.discoverServices(_address); - FlutterBluePlusWindows._characteristicCache[remoteId] ??= >{}; - - for (final serviceId in response) { - final characteristic = await WinBle.discoverCharacteristics( - address: _address, - serviceId: serviceId, - ); - FlutterBluePlusWindows._characteristicCache[remoteId] ??= {}; - FlutterBluePlusWindows._characteristicCache[remoteId]?[serviceId] ??= [ - ...characteristic.map( - (e) => BluetoothCharacteristicWindows( - remoteId: remoteId, - serviceUuid: Guid(serviceId), - characteristicUuid: Guid(e.uuid), - descriptors: [], - // TODO: implementation missing - propertiesWinBle: e.properties, - ), - ), - ]; - } - - result = [ - ...response.map( - (p) => BluetoothServiceWindows( - remoteId: remoteId, - serviceUuid: Guid(p), - // TODO: implementation missing - isPrimary: true, - // TODO: implementation missing - characteristics: FlutterBluePlusWindows._characteristicCache[remoteId]![p]!, - // TODO: implementation missing - includedServices: [], - ), - ) - ]; - - FlutterBluePlusWindows._knownServices[remoteId] = result; - - _services.add(result); - } finally { - _isDiscoveringServices.add(false); - } - return result; - } - - DisconnectReason? get disconnectReason { - // TODO: nothing to do - return null; - } - - Stream get connectionState async* { - await FlutterBluePlusWindows._initialize(); - - final map = FlutterBluePlusWindows._connectionStream.latestValue; - - if (map[_address] != null) { - yield map[_address]!.isConnected; - } - - yield* WinBle.connectionStreamOf(_address).map((e) => e.isConnected); - } - - Stream get mtu async* { - bool isEmitted = false; - int retryCount = 0; - while (!isEmitted) { - if (retryCount > 3) throw "Device not found!"; - retryCount++; - try { - yield await WinBle.getMaxMtuSize(_address); - isEmitted = true; - } catch (e) { - await Future.delayed(const Duration(milliseconds: 500)); - log(e.toString()); - } - } - } - - Future readRssi({int timeout = 15}) async { - return FlutterBluePlusWindows._rssiMap[remoteId] ?? -100; - } - - Future requestMtu( - int desiredMtu, { - double predelay = 0.35, - int timeout = 15, - }) async { - // https://github.com/rohitsangwan01/win_ble/issues/8 - return await WinBle.getMaxMtuSize(_address); - } - - Future requestConnectionPriority({ - required ConnectionPriority connectionPriorityRequest, - }) async { - // TODO: nothing to do - return; - } - - /// Set the preferred connection (Android Only) - /// - [txPhy] bitwise OR of all allowed phys for Tx, e.g. (Phy.le2m.mask | Phy.leCoded.mask) - /// - [txPhy] bitwise OR of all allowed phys for Rx, e.g. (Phy.le2m.mask | Phy.leCoded.mask) - /// - [option] preferred coding to use when transmitting on Phy.leCoded - /// Please note that this is just a recommendation given to the system. - Future setPreferredPhy({ - required int txPhy, - required int rxPhy, - required PhyCoding option, - }) async { - // TODO: implementation missing - } - - Future createBond({ - Uint8List? pin, - int timeout = 90, // TODO: implementation missing - }) async { - try { - await WinBle.pair(_address); - } catch (e) { - log(e.toString()); - } - } - - Future removeBond({ - int timeout = 30, // TODO: implementation missing - }) async { - try { - await WinBle.unPair(_address); - } catch (e) { - log(e.toString()); - } - } - - Future clearGattCache() async { - // TODO: implementation missing - } - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is BluetoothDeviceWindows && runtimeType == other.runtimeType && remoteId == other.remoteId); - - @override - int get hashCode => remoteId.hashCode; - - @override - String toString() { - return 'BluetoothDevice{' - 'remoteId: $remoteId, ' - 'platformName: $platformName, ' - 'services: ${FlutterBluePlusWindows._knownServices[remoteId]}' - '}'; - } - - @Deprecated('Use createBond() instead') - Future pair() async => await createBond(); - - @Deprecated('Use remoteId instead') - DeviceIdentifier get id => remoteId; - - @Deprecated('Use localName instead') - String get name => localName; - - @Deprecated('Use connectionState instead') - Stream get state => connectionState; - - @Deprecated('Use servicesStream instead') - Stream> get services => servicesStream; -} diff --git a/flutter_blue_plus_windows/lib/src/windows/bluetooth_service_windows.dart b/flutter_blue_plus_windows/lib/src/windows/bluetooth_service_windows.dart deleted file mode 100644 index 6a8257488..000000000 --- a/flutter_blue_plus_windows/lib/src/windows/bluetooth_service_windows.dart +++ /dev/null @@ -1,24 +0,0 @@ -part of 'windows.dart'; - -class BluetoothServiceWindows extends BluetoothService { - final DeviceIdentifier remoteId; - final Guid serviceUuid; - final bool isPrimary; - final List characteristics; - final List includedServices; - - BluetoothServiceWindows({ - required this.remoteId, - required this.serviceUuid, - required this.isPrimary, - required this.characteristics, - required this.includedServices, - }) : super.fromProto( - BmBluetoothService( - remoteId: DeviceIdentifier(remoteId.str), - serviceUuid: serviceUuid, - characteristics: [for (final c in characteristics) c.toProto()], - primaryServiceUuid: null, - ), - ); -} diff --git a/flutter_blue_plus_windows/lib/src/windows/flutter_blue_plus_windows.dart b/flutter_blue_plus_windows/lib/src/windows/flutter_blue_plus_windows.dart deleted file mode 100644 index 901f96559..000000000 --- a/flutter_blue_plus_windows/lib/src/windows/flutter_blue_plus_windows.dart +++ /dev/null @@ -1,353 +0,0 @@ -part of 'windows.dart'; - -class FlutterBluePlusWindows { - static bool _initialized = false; - - static BluetoothAdapterState _state = BluetoothAdapterState.unknown; - - // stream used for the isScanning public api - static final _isScanning = _StreamController(initialValue: false); - - // we always keep track of these device variables - static final _platformNames = {}; - static final _advNames = {}; - static final _rssiMap = {}; - static final _knownServices = >{}; - static final Map>> _lastChrs = {}; - static final Map> _isNotifying = {}; - static final Map>> _characteristicCache = {}; - static final Map> _deviceSubscriptions = {}; - static final Map> _delayedSubscriptions = {}; - static final List _scanSubscriptions = []; - - // stream used for the scanResults public api - static final _scanResultsList = _StreamController(initialValue: []); - - // the subscription to the scan results stream - static StreamSubscription? _scanSubscription; - - // timeout for scanning that can be cancelled by stopScan - static Timer? _scanTimeout; - - static List get _devices => [..._deviceSet]; - - static final _deviceSet = {}; - static final _removedDeviceTracer = {}; - - // static final _unhandledDeviceSet = {}; - - /// Flutter blue plus windows - static final _charReadWriteStreamController = StreamController<(String, List)>(); - static final _charReadStreamController = StreamController<(String, List)>(); - - static final _charReadWriteStream = _charReadWriteStreamController.stream.asBroadcastStream(); - static final _charReadStream = _charReadStreamController.stream.asBroadcastStream(); - - /// Flutter blue plus windows - static final _connectionStream = _StreamController(initialValue: {}); - - static Future _initialize() async { - if (_initialized) return; - await WinBle.initialize( - serverPath: await WinServer.path(), - enableLog: false, - ); - - WinBle.connectionStream.listen( - (event) { - log('$event - event'); - if (event['device'] == null) return; - if (event['connected'] == null) return; - - final map = _connectionStream.latestValue; - map[event['device']] = event['connected']; - - log('$map - map'); - _connectionStream.add(map); - - if (!event['connected']) { - final removingDevices = [ - ..._deviceSet.where( - (device) => device._address == event['device'], - ), - ]; - for (final device in removingDevices) { - _deviceSet.remove(device); - if (!_removedDeviceTracer.keys.contains(device)) { - _removedDeviceTracer[device] = Stream.periodic(const Duration(seconds: 10), (_) => device).listen( - (event) { - if(event.isConnected) { - _removedDeviceTracer[device]?.cancel(); - _removedDeviceTracer.remove(device); - return; - } - event.connect(); - }, - ); - } - - _deviceSubscriptions[device.remoteId]?.forEach((s) => s.cancel()); - _deviceSubscriptions.remove(device.remoteId); - // use delayed to update the stream before we cancel it - Future.delayed(Duration.zero).then((_) { - _delayedSubscriptions[device.remoteId]?.forEach((s) => s.cancel()); - _delayedSubscriptions.remove(device.remoteId); - }); - - _lastChrs[device.remoteId]?.clear(); - _isNotifying[device.remoteId]?.clear(); - } - } - }, - ); - _initialized = true; - } - - static Future get isSupported async { - return true; - } - - static Future get adapterName async { - return 'Windows'; - } - - static Stream get isScanning => _isScanning.stream; - - static bool get isScanningNow => _isScanning.latestValue; - - static Future turnOn({int timeout = 10}) async { - await _initialize(); - await WinBle.updateBluetoothState(true); - } - - // TODO: compare with original lib - static Stream> get scanResults => _scanResultsList.stream; - - static Stream get adapterState async* { - await _initialize(); - yield _state; - yield* WinBle.bleState.asBroadcastStream().map( - (s) { - _state = s.toAdapterState(); - return _state; - }, - ); - } - - /// Start a scan, and return a stream of results - /// Note: scan filters use an "or" behavior. i.e. if you set `withServices` & `withNames` we - /// return all the advertisments that match any of the specified services *or* any of the specified names. - /// - [withServices] filter by advertised services - /// - [withRemoteIds] filter for known remoteIds (iOS: 128-bit guid, android: 48-bit mac address) - /// - [withNames] filter by advertised names (exact match) - /// - [withKeywords] filter by advertised names (matches any substring) - /// - [withMsd] filter by manfacture specific data - /// - [withServiceData] filter by service data - /// - [timeout] calls stopScan after a specified duration - /// - [removeIfGone] if true, remove devices after they've stopped advertising for X duration - /// - [continuousUpdates] If `true`, we continually update 'lastSeen' & 'rssi' by processing - /// duplicate advertisements. This takes more power. You typically should not use this option. - /// - [continuousDivisor] Useful to help performance. If divisor is 3, then two-thirds of advertisements are - /// ignored, and one-third are processed. This reduces main-thread usage caused by the platform channel. - /// The scan counting is per-device so you always get the 1st advertisement from each device. - /// If divisor is 1, all advertisements are returned. This argument only matters for `continuousUpdates` mode. - /// - [oneByOne] if `true`, we will stream every advertistment one by one, possibly including duplicates. - /// If `false`, we deduplicate the advertisements, and return a list of devices. - /// - [androidLegacy] Android only. If `true`, scan on 1M phy only. - /// If `false`, scan on all supported phys. How the radio cycles through all the supported phys is purely - /// dependent on the your Bluetooth stack implementation. - /// - [androidScanMode] choose the android scan mode to use when scanning - /// - [androidUsesFineLocation] request `ACCESS_FINE_LOCATION` permission at runtime - static Future startScan({ - List withServices = const [], - List withRemoteIds = const [], - List withNames = const [], - //TODO: implementation missing - List withKeywords = const [], - //TODO: implementation missing - List withMsd = const [], - List withServiceData = const [], - Duration? timeout, - Duration? removeIfGone, - bool continuousUpdates = false, - int continuousDivisor = 1, - bool oneByOne = false, - bool androidLegacy = false, - AndroidScanMode androidScanMode = AndroidScanMode.lowLatency, - bool androidUsesFineLocation = false, - }) async { - await _initialize(); - - // stop existing scan - if (_isScanning.latestValue == true) { - await stopScan(); - } - - // push to stream - _isScanning.add(true); - - // Start timer *after* stream is being listened to, to make sure the - // timeout does not fire before _buffer is set - if (timeout != null) { - _scanTimeout = Timer(timeout, stopScan); - } - - /// remove connection by OS. - /// The reason why we add this logic is - /// to avoid uncontrollable devices and to make consistency. - - /// add WinBle scanning - WinBle.startScanning(); - - // check every 250ms for gone devices? - late Stream outputStream; - if (removeIfGone != null) { - outputStream = _mergeStreams([WinBle.scanStream, Stream.periodic(Duration(milliseconds: 250))]); - } else { - outputStream = WinBle.scanStream; - } - - final output = []; - - // listen & push to `scanResults` stream - _scanSubscription = outputStream.listen( - (BleDevice? winBleDevice) { - // print(winBleDevice?.serviceUuids); - if (winBleDevice == null) { - // if null, this is just a periodic update for removing old results - output.removeWhere((elm) => DateTime.now().difference(elm.timeStamp) > removeIfGone!); - - // push to stream - _scanResultsList.add(List.from(output)); - } else { - final remoteId = DeviceIdentifier(winBleDevice.address.toUpperCase()); - final scanResult = output.where((sr) => sr.device.remoteId == remoteId).firstOrNull; - final deviceName = winBleDevice.name.isNotEmpty ? winBleDevice.name : scanResult?.device.platformName ?? ''; - final serviceUuids = winBleDevice.serviceUuids.isNotEmpty - ? [...winBleDevice.serviceUuids.map((e) => Guid((e as String).replaceAll(RegExp(r'[{}]'), '')))] - : scanResult?.advertisementData.serviceUuids ?? []; - - final manufacturerData = winBleDevice.manufacturerData.isNotEmpty - ? { - if (winBleDevice.manufacturerData.length >= 2) - winBleDevice.manufacturerData[0] + (winBleDevice.manufacturerData[1] << 8): - winBleDevice.manufacturerData.sublist(2), - } - : scanResult?.advertisementData.manufacturerData ?? {}; - - final rssi = int.tryParse(winBleDevice.rssi) ?? -100; - - FlutterBluePlusWindows._platformNames[remoteId] = deviceName; - FlutterBluePlusWindows._advNames[remoteId] = deviceName; - FlutterBluePlusWindows._rssiMap[remoteId] = rssi; - - final device = BluetoothDeviceWindows(remoteId: remoteId); - - String hex(int value) => value.toRadixString(16).padLeft(2, '0'); - String hexToId(Iterable values) => values.map((e) => hex(e)).join(); - - final sr = ScanResult( - device: device, - advertisementData: AdvertisementData( - advName: deviceName, - txPowerLevel: winBleDevice.adStructures?.where((e) => e.type == 10).singleOrNull?.data.firstOrNull, - //TODO: Should verify - connectable: !winBleDevice.advType.contains('Non'), - manufacturerData: manufacturerData, - serviceData: { - for (final advStructures in winBleDevice.adStructures ?? []) - if (advStructures.type == 0x16 && advStructures.data.length >= 2) - Guid(hexToId(advStructures.data.sublist(0, 2).reversed)): advStructures.data.sublist(2), - for (final advStructures in winBleDevice.adStructures ?? []) - if (advStructures.type == 0x20 && advStructures.data.length >= 4) - Guid(hexToId(advStructures.data.sublist(0, 4).reversed)): advStructures.data.sublist(4), - for (final advStructures in winBleDevice.adStructures ?? []) - if (advStructures.type == 0x21 && advStructures.data.length >= 16) - Guid(hexToId(advStructures.data.sublist(0, 16).reversed)): advStructures.data.sublist(16), - }, - serviceUuids: serviceUuids, - appearance: null, - ), - rssi: rssi, - timeStamp: DateTime.now(), - ); - - // filter with services - final isFilteredWithServices = - withServices.isNotEmpty && serviceUuids.where((service) => withServices.contains(service)).isEmpty; - - // filter with remote ids - final isFilteredWithRemoteIds = withRemoteIds.isNotEmpty && !withRemoteIds.contains(remoteId); - - // filter with names - final isFilteredWithNames = withNames.isNotEmpty && !withNames.contains(deviceName); - - if (isFilteredWithServices || isFilteredWithRemoteIds || isFilteredWithNames) { - _scanResultsList.add(List.from(output)); - return; - } - - // add result to output - if (oneByOne) { - output - ..clear() - ..add(sr); - } else { - output.addOrUpdate(sr); - } - - // push to stream - _scanResultsList.add(List.from(output)); - } - }, - ); - } - - static List get connectedDevices { - return _devices; - } - - static Future> get bondedDevices async { - return _devices; - } - - /// Stops a scan for Bluetooth Low Energy devices - static Future stopScan() async { - await _initialize(); - WinBle.stopScanning(); - _scanSubscription?.cancel(); - _scanTimeout?.cancel(); - _isScanning.add(false); - - for (var subscription in _scanSubscriptions) { - subscription.cancel(); - } - - _scanResultsList.latestValue = []; - } - - /// Register a subscription to be canceled when scanning is complete. - /// This function simplifies cleanup, so you can prevent creating duplicate stream subscriptions. - /// - this is an optional convenience function - /// - prevents accidentally creating duplicate subscriptions before each scan - static void cancelWhenScanComplete(StreamSubscription subscription) { - _scanSubscriptions.add(subscription); - } - - /// Sets the internal FlutterBlue log level - static Future setLogLevel(LogLevel level, {color = true}) async { - // Nothing to implement - return; - } - - static Future turnOff({int timeout = 10}) async { - await _initialize(); - await WinBle.updateBluetoothState(false); - } - - // TODO: need to test - static Future get isOn async { - await _initialize(); - return await WinBle.bleState.asBroadcastStream().first == BleState.On; - } -} diff --git a/flutter_blue_plus_windows/lib/src/windows/util.dart b/flutter_blue_plus_windows/lib/src/windows/util.dart deleted file mode 100644 index b6114171a..000000000 --- a/flutter_blue_plus_windows/lib/src/windows/util.dart +++ /dev/null @@ -1,433 +0,0 @@ -part of 'windows.dart'; - -String _hexEncode(List numbers) { - return numbers - .map((n) => (n & 0xFF).toRadixString(16).padLeft(2, '0')) - .join(); -} - -List _hexDecode(String hex) { - List numbers = []; - for (int i = 0; i < hex.length; i += 2) { - String hexPart = hex.substring(i, i + 2); - int num = int.parse(hexPart, radix: 16); - numbers.add(num); - } - return numbers; -} - -int _compareAsciiLowerCase(String a, String b) { - const int upperCaseA = 0x41; - const int upperCaseZ = 0x5a; - const int asciiCaseBit = 0x20; - var defaultResult = 0; - for (var i = 0; i < a.length; i++) { - if (i >= b.length) return 1; - var aChar = a.codeUnitAt(i); - var bChar = b.codeUnitAt(i); - if (aChar == bChar) continue; - var aLowerCase = aChar; - var bLowerCase = bChar; - // Upper case if ASCII letters. - if (upperCaseA <= bChar && bChar <= upperCaseZ) { - bLowerCase += asciiCaseBit; - } - if (upperCaseA <= aChar && aChar <= upperCaseZ) { - aLowerCase += asciiCaseBit; - } - if (aLowerCase != bLowerCase) return (aLowerCase - bLowerCase).sign; - if (defaultResult == 0) defaultResult = aChar - bChar; - } - if (b.length > a.length) return -1; - return defaultResult.sign; -} - -// This is a reimplementation of BehaviorSubject from RxDart library. -// It is essentially a stream but: -// 1. we cache the latestValue of the stream -// 2. the "latestValue" is re-emitted whenever the stream is listened to -class _StreamController { - T latestValue; - - final StreamController _controller = StreamController.broadcast(); - - _StreamController({required T initialValue}) - : this.latestValue = initialValue; - - Stream get stream => _controller.stream; - - T get value => latestValue; - - void add(T newValue) { - latestValue = newValue; - _controller.add(newValue); - } - - void listen(Function(T) onData, - {Function? onError, void Function()? onDone, bool? cancelOnError}) { - onData(latestValue); - _controller.stream.listen(onData, - onError: onError, onDone: onDone, cancelOnError: cancelOnError); - } - - Future close() { - return _controller.close(); - } -} - -// imediately starts listening to a broadcast stream and -// buffering it in a new single-subscription stream -class _BufferStream { - final Stream _inputStream; - late final StreamSubscription? _subscription; - late final StreamController _controller; - late bool hasReceivedValue = false; - - _BufferStream.listen(this._inputStream) { - _controller = StreamController( - onCancel: () { - _subscription?.cancel(); - }, - onPause: () { - _subscription?.pause(); - }, - onResume: () { - _subscription?.resume(); - }, - onListen: () {}, // inputStream is already listened to - ); - - // immediately start listening to the inputStream - _subscription = _inputStream.listen( - (data) { - hasReceivedValue = true; - _controller.add(data); - }, - onError: (e) { - _controller.addError(e); - }, - onDone: () { - _controller.close(); - }, - cancelOnError: false, - ); - } - - void close() { - _subscription?.cancel(); - _controller.close(); - } - - Stream get stream async* { - yield* _controller.stream; - } -} - -// helper for 'doOnDone' method for streams. -class _OnDoneTransformer extends StreamTransformerBase { - final Function onDone; - - _OnDoneTransformer({required this.onDone}); - - @override - Stream bind(Stream stream) { - if (stream.isBroadcast) { - return _bindBroadcast(stream); - } - return _bindSingleSubscription(stream); - } - - Stream _bindSingleSubscription(Stream stream) { - StreamController? controller; - StreamSubscription? subscription; - - controller = StreamController( - onListen: () { - subscription = stream.listen( - controller?.add, - onError: controller?.addError, - onDone: () { - onDone(); - controller?.close(); - }, - ); - }, - onPause: ([Future? resumeSignal]) { - subscription?.pause(resumeSignal); - }, - onResume: () { - subscription?.resume(); - }, - onCancel: () { - return subscription?.cancel(); - }, - sync: true, - ); - - return controller.stream; - } - - Stream _bindBroadcast(Stream stream) { - StreamController? controller; - StreamSubscription? subscription; - - controller = StreamController.broadcast( - onListen: () { - subscription = stream - .listen(controller?.add, onError: controller?.addError, onDone: () { - onDone(); - controller?.close(); - }); - }, - onCancel: () { - subscription?.cancel(); - }, - sync: true, - ); - - return controller.stream; - } -} - -// helper for 'doOnCancel' method for streams. -class _OnCancelTransformer extends StreamTransformerBase { - final Function onCancel; - - _OnCancelTransformer({required this.onCancel}); - - @override - Stream bind(Stream stream) { - if (stream.isBroadcast) { - return _bindBroadcast(stream); - } - - return _bindSingleSubscription(stream); - } - - Stream _bindSingleSubscription(Stream stream) { - StreamController? controller; - StreamSubscription? subscription; - - controller = StreamController( - onListen: () { - subscription = stream.listen( - controller?.add, - onError: (Object error) { - controller?.addError(error); - controller?.close(); - }, - onDone: controller?.close, - ); - }, - onPause: ([Future? resumeSignal]) { - subscription?.pause(resumeSignal); - }, - onResume: () { - subscription?.resume(); - }, - onCancel: () { - onCancel(); - return subscription?.cancel(); - }, - sync: true, - ); - - return controller.stream; - } - - Stream _bindBroadcast(Stream stream) { - StreamController? controller; - StreamSubscription? subscription; - - controller = StreamController.broadcast( - onListen: () { - subscription = stream.listen( - controller?.add, - onError: (Object error) { - controller?.addError(error); - controller?.close(); - }, - onDone: controller?.close, - ); - }, - onCancel: () { - onCancel(); - subscription?.cancel(); - }, - sync: true, - ); - - return controller.stream; - } -} - -// Helper for 'newStreamWithInitialValue' method for streams. -class _NewStreamWithInitialValueTransformer - extends StreamTransformerBase { - final T initialValue; - - _NewStreamWithInitialValueTransformer(this.initialValue); - - @override - Stream bind(Stream stream) { - return _bindSingleSubscription(stream); - } - - Stream _bindSingleSubscription(Stream stream) { - StreamController? controller; - StreamSubscription? subscription; - - controller = StreamController( - onListen: () { - // Emit the initial value - controller?.add(initialValue); - - subscription = stream.listen( - controller?.add, - onError: (Object error) { - controller?.addError(error); - controller?.close(); - }, - onDone: controller?.close, - ); - }, - onPause: ([Future? resumeSignal]) { - subscription?.pause(resumeSignal); - }, - onResume: () { - subscription?.resume(); - }, - onCancel: () { - return subscription?.cancel(); - }, - sync: true, - ); - - return controller.stream; - } -} - -extension _StreamDoOnDone on Stream { - // ignore: unused_element - Stream doOnDone(void Function() onDone) { - return transform(_OnDoneTransformer(onDone: onDone)); - } -} - -extension _StreamDoOnCancel on Stream { - // ignore: unused_element - Stream doOnCancel(void Function() onCancel) { - return transform(_OnCancelTransformer(onCancel: onCancel)); - } -} - -extension _StreamNewStreamWithInitialValue on Stream { - Stream newStreamWithInitialValue(T initialValue) { - return transform(_NewStreamWithInitialValueTransformer(initialValue)); - } -} - -// ignore: unused_element -Stream _mergeStreams(List> streams) { - StreamController controller = StreamController(); - List> subscriptions = []; - - void handleData(T data) { - if (!controller.isClosed) { - controller.add(data); - } - } - - void handleError(Object error, StackTrace stackTrace) { - if (!controller.isClosed) { - controller.addError(error, stackTrace); - } - } - - void handleDone() { - if (subscriptions.every((s) => s.isPaused)) { - controller.close(); - } - } - - void subscribeToStream(Stream stream) { - final s = - stream.listen(handleData, onError: handleError, onDone: handleDone); - subscriptions.add(s); - } - - streams.forEach(subscribeToStream); - - controller.onCancel = () async { - await Future.wait(subscriptions.map((s) => s.cancel())); - }; - - return controller.stream; -} - -// dart is single threaded, but still has task switching. -// this mutex lets a single task through at a time. -class _Mutex { - final StreamController _controller = StreamController.broadcast(); - int current = 0; - int issued = 0; - - Future take() async { - int mine = issued; - issued++; - // tasks are executed in the same order they call take() - while (mine != current) { - await _controller.stream.first; // wait - } - } - - void give() { - current++; - _controller.add(null); // release waiting tasks - } -} - -// Create mutexes in a parrallel-safe way, -class _MutexFactory { - static final _Mutex _global = _Mutex(); - static final Map _all = {}; - - static Future<_Mutex> getMutexForKey(String key) async { - _Mutex? value; - await _global.take(); - { - _all[key] ??= _Mutex(); - value = _all[key]; - } - _global.give(); - return value!; - } -} - -String _black(String s) { - // Use ANSI escape codes - return '\x1B[1;30m$s\x1B[0m'; -} - -// ignore: unused_element -String _green(String s) { - // Use ANSI escape codes - return '\x1B[1;32m$s\x1B[0m'; -} - -String _magenta(String s) { - // Use ANSI escape codes - return '\x1B[1;35m$s\x1B[0m'; -} - -String _brown(String s) { - // Use ANSI escape codes - return '\x1B[1;33m$s\x1B[0m'; -} - -extension Boolean2ConnectionState on bool { - BluetoothConnectionState get isConnected { - if (this) return BluetoothConnectionState.connected; - return BluetoothConnectionState.disconnected; - } -} diff --git a/flutter_blue_plus_windows/lib/src/windows/windows.dart b/flutter_blue_plus_windows/lib/src/windows/windows.dart deleted file mode 100644 index abeaea2bc..000000000 --- a/flutter_blue_plus_windows/lib/src/windows/windows.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'dart:async'; -import 'dart:developer'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:flutter_blue_plus/flutter_blue_plus.dart' as FBP; -import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart'; -import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart'; - -part 'bluetooth_characteristic_windows.dart'; -part 'bluetooth_device_windows.dart'; -part 'bluetooth_service_windows.dart'; -part 'flutter_blue_plus_windows.dart'; -part 'util.dart'; diff --git a/flutter_blue_plus_windows/lib/src/wrapper/flutter_blue_plus_wrapper.dart b/flutter_blue_plus_windows/lib/src/wrapper/flutter_blue_plus_wrapper.dart deleted file mode 100644 index d5685d594..000000000 --- a/flutter_blue_plus_windows/lib/src/wrapper/flutter_blue_plus_wrapper.dart +++ /dev/null @@ -1,140 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:flutter_blue_plus/flutter_blue_plus.dart' as FBP; -import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart'; - -class FlutterBluePlus { - static Future startScan({ - List withServices = const [], - List withRemoteIds = const [], - List withNames = const [], - List withKeywords = const [], - List withMsd = const [], - List withServiceData = const [], - Duration? timeout, - Duration? removeIfGone, - bool continuousUpdates = false, - int continuousDivisor = 1, - bool oneByOne = false, - bool androidLegacy = false, - AndroidScanMode androidScanMode = AndroidScanMode.lowLatency, - bool androidUsesFineLocation = false, - List webOptionalServices = const [], - }) async { - if (Platform.isWindows) { - return await FlutterBluePlusWindows.startScan( - withServices: withServices, - withRemoteIds: withRemoteIds, - withNames: withNames, - withKeywords: withKeywords, - withMsd: withMsd, - withServiceData: withServiceData, - timeout: timeout, - removeIfGone: removeIfGone, - continuousUpdates: continuousUpdates, - continuousDivisor: continuousDivisor, - oneByOne: oneByOne, - androidLegacy: androidLegacy, - androidScanMode: androidScanMode, - androidUsesFineLocation: androidUsesFineLocation, - ); - } - - return await FBP.FlutterBluePlus.startScan( - withServices: withServices, - withRemoteIds: withRemoteIds, - withNames: withNames, - withKeywords: withKeywords, - withMsd: withMsd, - withServiceData: withServiceData, - timeout: timeout, - removeIfGone: removeIfGone, - continuousUpdates: continuousUpdates, - continuousDivisor: continuousDivisor, - oneByOne: oneByOne, - androidLegacy: androidLegacy, - androidScanMode: androidScanMode, - androidUsesFineLocation: androidUsesFineLocation, - webOptionalServices: webOptionalServices, - ); - } - - static Stream get adapterState { - if (Platform.isWindows) return FlutterBluePlusWindows.adapterState; - return FBP.FlutterBluePlus.adapterState; - } - - static Stream> get scanResults { - if (Platform.isWindows) return FlutterBluePlusWindows.scanResults; - return FBP.FlutterBluePlus.scanResults; - } - - static bool get isScanningNow { - if (Platform.isWindows) return FlutterBluePlusWindows.isScanningNow; - return FBP.FlutterBluePlus.isScanningNow; - } - - static Stream get isScanning { - if (Platform.isWindows) return FlutterBluePlusWindows.isScanning; - return FBP.FlutterBluePlus.isScanning; - } - - static Future stopScan() async { - if (Platform.isWindows) return await FlutterBluePlusWindows.stopScan(); - return await FBP.FlutterBluePlus.stopScan(); - } - - static Future setLogLevel(LogLevel level, {color = true}) async { - if (Platform.isWindows) return FlutterBluePlusWindows.setLogLevel(level, color: color); - return FBP.FlutterBluePlus.setLogLevel(level, color: color); - } - - /// TODO: need to verify - static LogLevel get logLevel => FBP.FlutterBluePlus.logLevel; - - static Future setOptions({bool restoreState = false, bool showPowerAlert = true}) async { - if (Platform.isWindows) return; - FBP.FlutterBluePlus.setOptions(restoreState: restoreState, showPowerAlert: showPowerAlert); - } - - static Future get isSupported async { - if (Platform.isWindows) return await FlutterBluePlusWindows.isSupported; - return await FBP.FlutterBluePlus.isSupported; - } - - static Future get adapterName async { - if (Platform.isWindows) return await FlutterBluePlusWindows.adapterName; - return await FBP.FlutterBluePlus.adapterName; - } - - static Future turnOn({int timeout = 60}) async { - if (Platform.isWindows) return await FlutterBluePlusWindows.turnOn(timeout: timeout); - return await FBP.FlutterBluePlus.turnOn(timeout: timeout); - } - - static List get connectedDevices { - if (Platform.isWindows) return FlutterBluePlusWindows.connectedDevices; - return FBP.FlutterBluePlus.connectedDevices; - } - - static Future> systemDevices(List withServices) async { - //TODO: connected devices => system devices - if (Platform.isWindows) return FlutterBluePlusWindows.connectedDevices; - return await FBP.FlutterBluePlus.systemDevices(withServices); - } - - static Future getPhySupport() { - return FBP.FlutterBluePlus.getPhySupport(); - } - - static Future> get bondedDevices async { - if (Platform.isWindows) return FlutterBluePlusWindows.connectedDevices; - return await FBP.FlutterBluePlus.bondedDevices; - } - - static void cancelWhenScanComplete(StreamSubscription subscription) { - if (Platform.isWindows) return FlutterBluePlusWindows.cancelWhenScanComplete(subscription); - return FBP.FlutterBluePlus.cancelWhenScanComplete(subscription); - } -} diff --git a/flutter_blue_plus_windows/lib/src/wrapper/wrapper.dart b/flutter_blue_plus_windows/lib/src/wrapper/wrapper.dart deleted file mode 100644 index 5336df61d..000000000 --- a/flutter_blue_plus_windows/lib/src/wrapper/wrapper.dart +++ /dev/null @@ -1 +0,0 @@ -export 'flutter_blue_plus_wrapper.dart'; diff --git a/flutter_blue_plus_windows/pubspec.yaml b/flutter_blue_plus_windows/pubspec.yaml deleted file mode 100644 index 25fbd037c..000000000 --- a/flutter_blue_plus_windows/pubspec.yaml +++ /dev/null @@ -1,13 +0,0 @@ -name: flutter_blue_plus_windows -description: Flutter blue plus for Windows -version: 1.26.1 -repository: https://github.com/chan150/flutter_blue_plus_windows -#publish_to: none - -environment: - sdk: ">=3.0.0 <4.0.0" - -dependencies: - flutter_blue_plus: ">=1.32.4" - win_ble: ">=1.1.1" - stream_with_value: ">=0.5.0" diff --git a/flutter_launcher_icons.yaml b/flutter_launcher_icons.yaml deleted file mode 100644 index 2c1088d8f..000000000 --- a/flutter_launcher_icons.yaml +++ /dev/null @@ -1,32 +0,0 @@ -# flutter pub run flutter_launcher_icons -flutter_launcher_icons: - image_path: "icon.png" - - android: "ic_launcher" - # image_path_android: "assets/icon/icon.png" - min_sdk_android: 24 # android min sdk min:16, default 21 - # adaptive_icon_background: "assets/icon/background.png" - # adaptive_icon_foreground: "assets/icon/foreground.png" - # adaptive_icon_monochrome: "assets/icon/monochrome.png" - - ios: false - # image_path_ios: "assets/icon/icon.png" - remove_alpha_channel_ios: true - # image_path_ios_dark_transparent: "assets/icon/icon_dark.png" - # image_path_ios_tinted_grayscale: "assets/icon/icon_tinted.png" - # desaturate_tinted_to_grayscale_ios: true - - web: - generate: true - image_path: "icon.png" - background_color: "#hexcode" - theme_color: "#hexcode" - - windows: - generate: true - image_path: "icon.png" - icon_size: 48 # min:48, max:256, default: 48 - - macos: - generate: true - image_path: "icon.png" diff --git a/icon.png b/icon.png index 5736b5869..27a4345a4 100644 Binary files a/icon.png and b/icon.png differ diff --git a/ios/ExportOptions.plist b/ios/ExportOptions.plist new file mode 100644 index 000000000..bce190a58 --- /dev/null +++ b/ios/ExportOptions.plist @@ -0,0 +1,29 @@ + + + + + destination + export + generateAppStoreInformation + + manageAppVersionAndBuildNumber + + method + app-store-connect + signingStyle + manual + provisioningProfiles + + de.jonasbark.swiftcontrol.darwin + ios app store + + stripSwiftSymbols + + teamID + UZRHKPVWN9 + testFlightInternalTestingOnly + + uploadSymbols + + + diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 7c5696400..1dc6cf765 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/ios/Podfile b/ios/Podfile index e549ee22f..620e46eba 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +# platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 000000000..91351f266 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,110 @@ +PODS: + - bluetooth_low_energy_darwin (0.0.1): + - Flutter + - FlutterMacOS + - device_info_plus (0.0.1): + - Flutter + - Flutter (1.0.0) + - flutter_local_notifications (0.0.1): + - Flutter + - gamepads_ios (0.1.1): + - Flutter + - image_picker_ios (0.0.1): + - Flutter + - integration_test (0.0.1): + - Flutter + - media_key_detector_ios (0.0.1): + - Flutter + - package_info_plus (0.4.5): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - permission_handler_apple (9.3.0): + - Flutter + - restart_app (0.0.1): + - Flutter + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - universal_ble (0.0.1): + - Flutter + - FlutterMacOS + - url_launcher_ios (0.0.1): + - Flutter + - wakelock_plus (0.0.1): + - Flutter + +DEPENDENCIES: + - bluetooth_low_energy_darwin (from `.symlinks/plugins/bluetooth_low_energy_darwin/darwin`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - Flutter (from `Flutter`) + - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) + - gamepads_ios (from `.symlinks/plugins/gamepads_ios/ios`) + - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + - media_key_detector_ios (from `.symlinks/plugins/media_key_detector_ios/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - restart_app (from `.symlinks/plugins/restart_app/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - universal_ble (from `.symlinks/plugins/universal_ble/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) + +EXTERNAL SOURCES: + bluetooth_low_energy_darwin: + :path: ".symlinks/plugins/bluetooth_low_energy_darwin/darwin" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" + Flutter: + :path: Flutter + flutter_local_notifications: + :path: ".symlinks/plugins/flutter_local_notifications/ios" + gamepads_ios: + :path: ".symlinks/plugins/gamepads_ios/ios" + image_picker_ios: + :path: ".symlinks/plugins/image_picker_ios/ios" + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + media_key_detector_ios: + :path: ".symlinks/plugins/media_key_detector_ios/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + restart_app: + :path: ".symlinks/plugins/restart_app/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + universal_ble: + :path: ".symlinks/plugins/universal_ble/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + wakelock_plus: + :path: ".symlinks/plugins/wakelock_plus/ios" + +SPEC CHECKSUMS: + bluetooth_low_energy_darwin: 764d8d1ae5abefbcdb839e812b4b25c0061fcf8b + device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f + gamepads_ios: 1d2930c7a4450a9a1b57444ebf305a6a6cbeea0b + image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 + media_key_detector_ios: 7ff9aefdfea00bb7b71e184132381b7d0e7e1269 + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + restart_app: 806659942bf932f6ce51c5372f91ce5e81c8c14a + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e + +PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e + +COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index c53ad03c7..8911d04f3 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -10,10 +10,12 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 3E50CA021EFA25CF89FE46AB /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C0E42A04700D6B661C7EE82 /* Pods_RunnerTests.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 9DEFD285994D09CFCE400F36 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE7ADD07A99710C0FB974A8 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -40,14 +42,20 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0CF32F9ECDBEA4B014717FF8 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2C0E42A04700D6B661C7EE82 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 5CE7ADD07A99710C0FB974A8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7D133E5D5548E2EF2879734F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 86D436F6DAF367742EF27F51 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 8AA6D129479129F106E2298A /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -55,19 +63,43 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DFFDC4B9C4D6EF6A3BDE2E73 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + EFDECED99A47773C293F8819 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 5046C8DCA17DB268ED17F005 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3E50CA021EFA25CF89FE46AB /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 9DEFD285994D09CFCE400F36 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 31E2F9ED567016937E8AEA3B /* Pods */ = { + isa = PBXGroup; + children = ( + 86D436F6DAF367742EF27F51 /* Pods-Runner.debug.xcconfig */, + 0CF32F9ECDBEA4B014717FF8 /* Pods-Runner.release.xcconfig */, + 7D133E5D5548E2EF2879734F /* Pods-Runner.profile.xcconfig */, + DFFDC4B9C4D6EF6A3BDE2E73 /* Pods-RunnerTests.debug.xcconfig */, + 8AA6D129479129F106E2298A /* Pods-RunnerTests.release.xcconfig */, + EFDECED99A47773C293F8819 /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; 331C8082294A63A400263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -76,6 +108,15 @@ path = RunnerTests; sourceTree = ""; }; + 6A38311855DC1CB8C0E2FD04 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5CE7ADD07A99710C0FB974A8 /* Pods_Runner.framework */, + 2C0E42A04700D6B661C7EE82 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -94,6 +135,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + 31E2F9ED567016937E8AEA3B /* Pods */, + 6A38311855DC1CB8C0E2FD04 /* Frameworks */, ); sourceTree = ""; }; @@ -128,8 +171,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 5E1D2B1ED00966C758CA2289 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, + 5046C8DCA17DB268ED17F005 /* Frameworks */, ); buildRules = ( ); @@ -145,12 +190,15 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + AF2FDC69578083D4D16AB4D6 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + EEF1FBDEE98BA93C4FBDB3AE /* [CP] Embed Pods Frameworks */, + 1F0C44A79AE73641A1C3FF47 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -222,6 +270,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 1F0C44A79AE73641A1C3FF47 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -238,6 +303,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 5E1D2B1ED00966C758CA2289 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -253,6 +340,45 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + AF2FDC69578083D4D16AB4D6 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + EEF1FBDEE98BA93C4FBDB3AE /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -346,7 +472,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -361,16 +487,19 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 7BL8RUV2K6; + DEVELOPMENT_TEAM = UZRHKPVWN9; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = de.jonasbark.swiftPlay; + PRODUCT_BUNDLE_IDENTIFIER = de.jonasbark.swiftcontrol.darwin; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -379,6 +508,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = DFFDC4B9C4D6EF6A3BDE2E73 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -396,6 +526,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 8AA6D129479129F106E2298A /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -411,6 +542,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = EFDECED99A47773C293F8819 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -428,7 +560,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -473,7 +605,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -485,7 +617,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -524,7 +656,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -541,16 +673,19 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 7BL8RUV2K6; + DEVELOPMENT_TEAM = UZRHKPVWN9; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = de.jonasbark.swiftPlay; + PRODUCT_BUNDLE_IDENTIFIER = de.jonasbark.swiftcontrol.darwin; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -564,16 +699,19 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 7BL8RUV2K6; + DEVELOPMENT_TEAM = UZRHKPVWN9; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = de.jonasbark.swiftPlay; + PRODUCT_BUNDLE_IDENTIFIER = de.jonasbark.swiftcontrol.darwin; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 15cada483..e3773d42e 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> + + diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png new file mode 100644 index 000000000..4737aae71 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png new file mode 100644 index 000000000..4737aae71 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png new file mode 100644 index 000000000..89ed8aade Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png new file mode 100644 index 000000000..25d2c553b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png new file mode 100644 index 000000000..a87c340d0 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png new file mode 100644 index 000000000..6d1a7f487 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png new file mode 100644 index 000000000..6d1a7f487 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png new file mode 100644 index 000000000..33e596628 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png new file mode 100644 index 000000000..a87c340d0 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png new file mode 100644 index 000000000..315020da5 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png new file mode 100644 index 000000000..315020da5 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png new file mode 100644 index 000000000..c9af31bbf Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png new file mode 100644 index 000000000..4737aae71 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png new file mode 100644 index 000000000..c9af31bbf Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png new file mode 100644 index 000000000..d062cdb5d Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png new file mode 100644 index 000000000..b4ba144d3 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png new file mode 100644 index 000000000..c9af31bbf Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png new file mode 100644 index 000000000..75e42ea51 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png new file mode 100644 index 000000000..d062cdb5d Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png new file mode 100644 index 000000000..450a704b4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png new file mode 100644 index 000000000..91eb91663 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index d36b1fab2..bd04914ae 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,122 +1,134 @@ { - "images" : [ + "images": [ { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" + "filename": "AppIcon@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "60x60" }, { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" + "filename": "AppIcon@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "60x60" }, { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" + "filename": "AppIcon~ipad.png", + "idiom": "ipad", + "scale": "1x", + "size": "76x76" }, { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" + "filename": "AppIcon@2x~ipad.png", + "idiom": "ipad", + "scale": "2x", + "size": "76x76" }, { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" + "filename": "AppIcon-83.5@2x~ipad.png", + "idiom": "ipad", + "scale": "2x", + "size": "83.5x83.5" }, { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" + "filename": "AppIcon-40@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "40x40" }, { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" + "filename": "AppIcon-40@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "40x40" }, { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" + "filename": "AppIcon-40~ipad.png", + "idiom": "ipad", + "scale": "1x", + "size": "40x40" }, { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" + "filename": "AppIcon-40@2x~ipad.png", + "idiom": "ipad", + "scale": "2x", + "size": "40x40" }, { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" + "filename": "AppIcon-20@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "20x20" }, { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" + "filename": "AppIcon-20@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "20x20" }, { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" + "filename": "AppIcon-20~ipad.png", + "idiom": "ipad", + "scale": "1x", + "size": "20x20" }, { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" + "filename": "AppIcon-20@2x~ipad.png", + "idiom": "ipad", + "scale": "2x", + "size": "20x20" }, { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" + "filename": "AppIcon-29.png", + "idiom": "iphone", + "scale": "1x", + "size": "29x29" }, { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" + "filename": "AppIcon-29@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "29x29" }, { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" + "filename": "AppIcon-29@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "29x29" }, { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" + "filename": "AppIcon-29~ipad.png", + "idiom": "ipad", + "scale": "1x", + "size": "29x29" }, { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" + "filename": "AppIcon-29@2x~ipad.png", + "idiom": "ipad", + "scale": "2x", + "size": "29x29" }, { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" + "filename": "AppIcon-60@2x~car.png", + "idiom": "car", + "scale": "2x", + "size": "60x60" + }, + { + "filename": "AppIcon-60@3x~car.png", + "idiom": "car", + "scale": "3x", + "size": "60x60" + }, + { + "filename": "AppIcon~ios-marketing.png", + "idiom": "ios-marketing", + "scale": "1x", + "size": "1024x1024" } ], - "info" : { - "version" : 1, - "author" : "xcode" + "info": { + "author": "iconkitchen", + "version": 1 } -} +} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada472..000000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 7353c41ec..000000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 797d452e4..000000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index 6ed2d933e..000000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cd7b0099..000000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index fe730945a..000000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index 321773cd8..000000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 797d452e4..000000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index 502f463a9..000000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index 0ec303439..000000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index 0ec303439..000000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index e9f5fea27..000000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index 84ac32ae7..000000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 8953cba09..000000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index 0467bf12a..000000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/Contents.json b/ios/Runner/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/ios/Runner/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/AppIcon-min 1.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/AppIcon-min 1.png new file mode 100644 index 000000000..997ed2276 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/AppIcon-min 1.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/AppIcon-min 2.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/AppIcon-min 2.png new file mode 100644 index 000000000..997ed2276 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/AppIcon-min 2.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/AppIcon-min.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/AppIcon-min.png new file mode 100644 index 000000000..997ed2276 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/AppIcon-min.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json index 0bedcf2fd..bdf6e1752 100644 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -1,23 +1,23 @@ { "images" : [ { + "filename" : "AppIcon-min 2.png", "idiom" : "universal", - "filename" : "LaunchImage.png", "scale" : "1x" }, { + "filename" : "AppIcon-min 1.png", "idiom" : "universal", - "filename" : "LaunchImage@2x.png", "scale" : "2x" }, { + "filename" : "AppIcon-min.png", "idiom" : "universal", - "filename" : "LaunchImage@3x.png", "scale" : "3x" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } } diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100644 index 9da19eaca..000000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19eaca..000000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19eaca..000000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725b7..000000000 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard index f2e259c7c..9f6e7d694 100644 --- a/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -1,8 +1,10 @@ - - + + + - + + @@ -14,9 +16,11 @@ + + @@ -28,10 +32,10 @@ - + - + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard index f3c28516f..ff3ee2178 100644 --- a/ios/Runner/Base.lproj/Main.storyboard +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -1,8 +1,10 @@ - - + + + - + + @@ -14,13 +16,14 @@ - + - + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 83e3c0f1a..98b71fc4d 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,10 +2,12 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - SwiftControl + BikeControl CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -22,8 +24,21 @@ ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) + ITSAppUsesNonExemptEncryption + LSRequiresIPhoneOS + NSBluetoothAlwaysUsageDescription + BikeControl uses Bluetooth to connect to accessories. + NSLocalNetworkUsageDescription + This app connects to your trainer app on your local network. + UIApplicationSupportsIndirectInputEvents + + UIBackgroundModes + + bluetooth-peripheral + bluetooth-central + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,9 +56,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - diff --git a/keypress_simulator/.clang-format b/keypress_simulator/.clang-format new file mode 100644 index 000000000..3c73f32a3 --- /dev/null +++ b/keypress_simulator/.clang-format @@ -0,0 +1,12 @@ +# Defines the Chromium style for automatic reformatting. +# http://clang.llvm.org/docs/ClangFormatStyleOptions.html +BasedOnStyle: Chromium +# This defaults to 'Auto'. Explicitly set it for a while, so that +# 'vector >' in existing files gets formatted to +# 'vector>'. ('Auto' means that clang-format will only use +# 'int>>' if the file already contains at least one such instance.) +Standard: Cpp11 +SortIncludes: true +--- +Language: ObjC +ColumnLimit: 100 diff --git a/keypress_simulator/.github/FUNDING.yml b/keypress_simulator/.github/FUNDING.yml new file mode 100644 index 000000000..cb2bfb178 --- /dev/null +++ b/keypress_simulator/.github/FUNDING.yml @@ -0,0 +1 @@ +liberapay: lijy91 diff --git a/keypress_simulator/.github/workflows/build.yml b/keypress_simulator/.github/workflows/build.yml new file mode 100644 index 000000000..1ee5a4aa5 --- /dev/null +++ b/keypress_simulator/.github/workflows/build.yml @@ -0,0 +1,50 @@ +name: build + +on: + push: + branches: [main, dev] + pull_request: + branches: [main] + +jobs: + build-macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.16.8" + channel: "stable" + - uses: bluefireteam/melos-action@v2 + - working-directory: ./packages/keypress_simulator/example + run: | + melos bs + flutter build macos --release + + build-web: + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.16.8" + channel: "stable" + - uses: bluefireteam/melos-action@v2 + - working-directory: ./packages/keypress_simulator/example + run: | + melos bs + flutter build web --release + + build-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.16.8" + channel: "stable" + - uses: bluefireteam/melos-action@v2 + - working-directory: ./packages/keypress_simulator/example + run: | + melos bs + flutter build windows --release diff --git a/keypress_simulator/.github/workflows/lint.yml b/keypress_simulator/.github/workflows/lint.yml new file mode 100644 index 000000000..8d68ba7c9 --- /dev/null +++ b/keypress_simulator/.github/workflows/lint.yml @@ -0,0 +1,31 @@ +name: lint + +on: + push: + branches: [main, dev] + pull_request: + branches: [main] + +jobs: + analyze: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.16.8" + channel: "stable" + - uses: bluefireteam/melos-action@v2 + - run: melos run analyze + + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.16.8" + channel: "stable" + cache: true + - uses: bluefireteam/melos-action@v2 + - run: melos run format-check diff --git a/keypress_simulator/.github/workflows/test.yml b/keypress_simulator/.github/workflows/test.yml new file mode 100644 index 000000000..617f89a88 --- /dev/null +++ b/keypress_simulator/.github/workflows/test.yml @@ -0,0 +1,20 @@ +name: test + +on: + push: + branches: [main, dev] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.16.8" + channel: "stable" + cache: true + - uses: bluefireteam/melos-action@v2 + - run: melos run test --no-select diff --git a/keypress_simulator/.gitignore b/keypress_simulator/.gitignore new file mode 100644 index 000000000..fa08f0546 --- /dev/null +++ b/keypress_simulator/.gitignore @@ -0,0 +1,6 @@ +.dart_tool/ +.idea/ + +*.iml +pubspec_overrides.yaml +pubspec.lock diff --git a/keypress_simulator/LICENSE b/keypress_simulator/LICENSE new file mode 100644 index 000000000..eea05ab78 --- /dev/null +++ b/keypress_simulator/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2024 LiJianying + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/keypress_simulator/README-ZH.md b/keypress_simulator/README-ZH.md new file mode 100644 index 000000000..91223a21a --- /dev/null +++ b/keypress_simulator/README-ZH.md @@ -0,0 +1,93 @@ +> **🚀 快速发布您的应用**: 试试 [Fastforge](https://fastforge.dev) - 构建、打包和分发您的 Flutter 应用最简单的方式。 + +# keypress_simulator + +[![pub version][pub-image]][pub-url] [![][discord-image]][discord-url] + +[pub-image]: https://img.shields.io/pub/v/keypress_simulator.svg +[pub-url]: https://pub.dev/packages/keypress_simulator +[discord-image]: https://img.shields.io/discord/884679008049037342.svg +[discord-url]: https://discord.gg/zPa6EZ2jqb + +这个插件允许 Flutter 桌面应用模拟按键操作。 + +--- + +[English](./README.md) | 简体中文 + +--- + + + + +- [平台支持](#%E5%B9%B3%E5%8F%B0%E6%94%AF%E6%8C%81) +- [快速开始](#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B) + - [安装](#%E5%AE%89%E8%A3%85) + - [用法](#%E7%94%A8%E6%B3%95) +- [谁在用使用它?](#%E8%B0%81%E5%9C%A8%E7%94%A8%E4%BD%BF%E7%94%A8%E5%AE%83) +- [许可证](#%E8%AE%B8%E5%8F%AF%E8%AF%81) + + + +## 平台支持 + +| Linux | macOS | Windows | +| :---: | :---: | :-----: | +| ➖ | ✔️ | ✔️ | + +## 快速开始 + +### 安装 + +将此添加到你的软件包的 pubspec.yaml 文件: + +```yaml +dependencies: + keypress_simulator: ^0.2.0 +``` + +### 用法 + +```dart +import 'package:keypress_simulator/keypress_simulator.dart'; + +// 1. Simulate pressing ⌘ + C + +// 1.1 Simulate key down +await keyPressSimulator.simulateKeyDown( + PhysicalKeyboardKey.keyC, + [ModifierKey.metaModifier], +); + +// 1.2 Simulate key up +await keyPressSimulator.simulateKeyUp( + PhysicalKeyboardKey.keyC, + [ModifierKey.metaModifier], +); + +// 2. Simulate long pressing ⌘ + space + +// 2.1. Simulate key down +await keyPressSimulator.simulateKeyDown( + PhysicalKeyboardKey.space, + [ModifierKey.metaModifier], +); + +await Future.delayed(const Duration(seconds: 5)); + +// 2.2. Simulate key up +await keyPressSimulator.simulateKeyUp( + PhysicalKeyboardKey.space, + [ModifierKey.metaModifier], +); +``` + +> 请看这个插件的示例应用,以了解完整的例子。 + +## 谁在用使用它? + +- [Biyi (比译)](https://biyidev.com/) - 一个便捷的翻译和词典应用程序。 + +## 许可证 + +[MIT](./LICENSE) diff --git a/keypress_simulator/README.md b/keypress_simulator/README.md new file mode 100644 index 000000000..95ee8f4e1 --- /dev/null +++ b/keypress_simulator/README.md @@ -0,0 +1,93 @@ +> **🚀 Ship Your App Faster**: Try [Fastforge](https://fastforge.dev) - The simplest way to build, package and distribute your Flutter apps. + +# keypress_simulator + +[![pub version][pub-image]][pub-url] [![][discord-image]][discord-url] + +[pub-image]: https://img.shields.io/pub/v/keypress_simulator.svg +[pub-url]: https://pub.dev/packages/keypress_simulator +[discord-image]: https://img.shields.io/discord/884679008049037342.svg +[discord-url]: https://discord.gg/zPa6EZ2jqb + +This plugin allows Flutter desktop apps to simulate key presses. + +--- + +English | [简体中文](./README-ZH.md) + +--- + + + + +- [Platform Support](#platform-support) +- [Quick Start](#quick-start) + - [Installation](#installation) + - [Usage](#usage) +- [Who's using it?](#whos-using-it) +- [License](#license) + + + +## Platform Support + +| Linux | macOS | Windows | +| :---: | :---: | :-----: | +| ➖ | ✔️ | ✔️ | + +## Quick Start + +### Installation + +Add this to your package's pubspec.yaml file: + +```yaml +dependencies: + keypress_simulator: ^0.2.0 +``` + +### Usage + +```dart +import 'package:keypress_simulator/keypress_simulator.dart'; + +// 1. Simulate pressing ⌘ + C + +// 1.1 Simulate key down +await keyPressSimulator.simulateKeyDown( + PhysicalKeyboardKey.keyC, + [ModifierKey.metaModifier], +); + +// 1.2 Simulate key up +await keyPressSimulator.simulateKeyUp( + PhysicalKeyboardKey.keyC, + [ModifierKey.metaModifier], +); + +// 2. Simulate long pressing ⌘ + space + +// 2.1. Simulate key down +await keyPressSimulator.simulateKeyDown( + PhysicalKeyboardKey.space, + [ModifierKey.metaModifier], +); + +await Future.delayed(const Duration(seconds: 5)); + +// 2.2. Simulate key up +await keyPressSimulator.simulateKeyUp( + PhysicalKeyboardKey.space, + [ModifierKey.metaModifier], +); +``` + +> Please see the example app of this plugin for a full example. + +## Who's using it? + +- [Biyi (比译)](https://biyidev.com/) - A convenient translation and dictionary app. + +## License + +[MIT](./LICENSE) diff --git a/keypress_simulator/melos.yaml b/keypress_simulator/melos.yaml new file mode 100644 index 000000000..fc9445c17 --- /dev/null +++ b/keypress_simulator/melos.yaml @@ -0,0 +1,35 @@ +name: keypress_simulator_workspace +repository: https://github.com/leanflutter/keypress_simulator + +packages: + - examples/** + - packages/** + +command: + bootstrap: + # Uses the pubspec_overrides.yaml instead of having Melos modifying the lock file. + usePubspecOverrides: true + +scripts: + analyze: + exec: flutter analyze --fatal-infos + description: Run `flutter analyze` for all packages. + + test: + exec: flutter test + description: Run `flutter test` for a specific package. + packageFilters: + dirExists: + - test + + format: + exec: dart format . --fix + description: Run `dart format` for all packages. + + format-check: + exec: dart format . --fix --set-exit-if-changed + description: Run `dart format` checks for all packages. + + fix: + exec: dart fix . --apply + description: Run `dart fix` for all packages. diff --git a/keypress_simulator/packages/keypress_simulator/.gitignore b/keypress_simulator/packages/keypress_simulator/.gitignore new file mode 100644 index 000000000..01c49d55f --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/keypress_simulator/packages/keypress_simulator/.metadata b/keypress_simulator/packages/keypress_simulator/.metadata new file mode 100644 index 000000000..0b5ddf05b --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: db747aa1331bd95bc9b3874c842261ca2d302cd5 + channel: stable + +project_type: plugin diff --git a/keypress_simulator/packages/keypress_simulator/CHANGELOG.md b/keypress_simulator/packages/keypress_simulator/CHANGELOG.md new file mode 100644 index 000000000..a25790099 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator/CHANGELOG.md @@ -0,0 +1,7 @@ +## 0.2.0 + +* feat: Convert to federated plugin + +## 0.1.0 + +* First release. diff --git a/keypress_simulator/packages/keypress_simulator/LICENSE b/keypress_simulator/packages/keypress_simulator/LICENSE new file mode 100644 index 000000000..eea05ab78 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2024 LiJianying + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/keypress_simulator/packages/keypress_simulator/README-ZH.md b/keypress_simulator/packages/keypress_simulator/README-ZH.md new file mode 120000 index 000000000..7da944f1c --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator/README-ZH.md @@ -0,0 +1 @@ +../../README-ZH.md \ No newline at end of file diff --git a/keypress_simulator/packages/keypress_simulator/README.md b/keypress_simulator/packages/keypress_simulator/README.md new file mode 120000 index 000000000..fe8400541 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator/README.md @@ -0,0 +1 @@ +../../README.md \ No newline at end of file diff --git a/keypress_simulator/packages/keypress_simulator/analysis_options.yaml b/keypress_simulator/packages/keypress_simulator/analysis_options.yaml new file mode 100644 index 000000000..095b1d67f --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator/analysis_options.yaml @@ -0,0 +1 @@ +include: package:mostly_reasonable_lints/flutter.yaml diff --git a/keypress_simulator/packages/keypress_simulator/lib/keypress_simulator.dart b/keypress_simulator/packages/keypress_simulator/lib/keypress_simulator.dart new file mode 100644 index 000000000..4c8a7e899 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator/lib/keypress_simulator.dart @@ -0,0 +1 @@ +export 'src/keypress_simulator.dart'; diff --git a/keypress_simulator/packages/keypress_simulator/lib/src/keypress_simulator.dart b/keypress_simulator/packages/keypress_simulator/lib/src/keypress_simulator.dart new file mode 100644 index 000000000..f7538712c --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator/lib/src/keypress_simulator.dart @@ -0,0 +1,58 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:keypress_simulator_platform_interface/keypress_simulator_platform_interface.dart'; + +class KeyPressSimulator { + KeyPressSimulator._(); + + /// The shared instance of [KeyPressSimulator]. + static final KeyPressSimulator instance = KeyPressSimulator._(); + + KeyPressSimulatorPlatform get _platform => KeyPressSimulatorPlatform.instance; + + Future isAccessAllowed() { + return _platform.isAccessAllowed(); + } + + Future requestAccess({bool onlyOpenPrefPane = false}) { + return _platform.requestAccess(onlyOpenPrefPane: onlyOpenPrefPane); + } + + Future simulateMouseClickDown(Offset position) { + return _platform.simulateMouseClick(position, keyDown: true); + } + + Future simulateMouseClickUp(Offset position) { + return _platform.simulateMouseClick(position, keyDown: false); + } + + /// Simulate key down. + Future simulateKeyDown(PhysicalKeyboardKey? key, [List modifiers = const []]) { + return _platform.simulateKeyPress(key: key, modifiers: modifiers, keyDown: true); + } + + /// Simulate key up. + Future simulateKeyUp(PhysicalKeyboardKey? key, [List modifiers = const []]) { + return _platform.simulateKeyPress(key: key, modifiers: modifiers, keyDown: false); + } + + @Deprecated('Please use simulateKeyDown & simulateKeyUp methods.') + Future simulateCtrlCKeyPress() async { + const key = PhysicalKeyboardKey.keyC; + final modifiers = Platform.isMacOS ? [ModifierKey.metaModifier] : [ModifierKey.controlModifier]; + await simulateKeyDown(key, modifiers); + await simulateKeyUp(key, modifiers); + } + + @Deprecated('Please use simulateKeyDown & simulateKeyUp methods.') + Future simulateCtrlVKeyPress() async { + const key = PhysicalKeyboardKey.keyV; + final modifiers = Platform.isMacOS ? [ModifierKey.metaModifier] : [ModifierKey.controlModifier]; + await simulateKeyDown(key, modifiers); + await simulateKeyUp(key, modifiers); + } +} + +final keyPressSimulator = KeyPressSimulator.instance; diff --git a/keypress_simulator/packages/keypress_simulator/pubspec.yaml b/keypress_simulator/packages/keypress_simulator/pubspec.yaml new file mode 100644 index 000000000..8cd7cccdf --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator/pubspec.yaml @@ -0,0 +1,36 @@ +name: keypress_simulator +description: This plugin allows Flutter desktop apps to simulate key presses. +version: 0.2.0 +homepage: https://github.com/leanflutter/keypress_simulator + +platforms: + macos: + windows: + +environment: + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + keypress_simulator_macos: + path: ../keypress_simulator_macos + keypress_simulator_platform_interface: + path: ../keypress_simulator_platform_interface + keypress_simulator_windows: + path: ../keypress_simulator_windows + +dev_dependencies: + flutter_test: + sdk: flutter + mostly_reasonable_lints: ^0.1.1 + +flutter: + plugin: + platforms: + macos: + default_package: keypress_simulator_macos + windows: + default_package: keypress_simulator_windows + diff --git a/keypress_simulator/packages/keypress_simulator_macos/.gitignore b/keypress_simulator/packages/keypress_simulator_macos/.gitignore new file mode 100644 index 000000000..ac5aa9893 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_macos/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/keypress_simulator/packages/keypress_simulator_macos/.metadata b/keypress_simulator/packages/keypress_simulator_macos/.metadata new file mode 100644 index 000000000..6422a9994 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_macos/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "67457e669f79e9f8d13d7a68fe09775fefbb79f4" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 + base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 + - platform: macos + create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 + base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/keypress_simulator/packages/keypress_simulator_macos/CHANGELOG.md b/keypress_simulator/packages/keypress_simulator_macos/CHANGELOG.md new file mode 100644 index 000000000..2f145cea2 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_macos/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.2.0 + +* First release. diff --git a/keypress_simulator/packages/keypress_simulator_macos/LICENSE b/keypress_simulator/packages/keypress_simulator_macos/LICENSE new file mode 100644 index 000000000..eea05ab78 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_macos/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2024 LiJianying + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/keypress_simulator/packages/keypress_simulator_macos/README.md b/keypress_simulator/packages/keypress_simulator_macos/README.md new file mode 100644 index 000000000..87a076f02 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_macos/README.md @@ -0,0 +1,12 @@ +# keypress_simulator_macos + +[![pub version][pub-image]][pub-url] + +[pub-image]: https://img.shields.io/pub/v/keypress_simulator_macos.svg +[pub-url]: https://pub.dev/packages/keypress_simulator_macos + +The macOS implementation of [keypress_simulator](https://pub.dev/packages/keypress_simulator). + +## License + +[MIT](./LICENSE) diff --git a/keypress_simulator/packages/keypress_simulator_macos/analysis_options.yaml b/keypress_simulator/packages/keypress_simulator_macos/analysis_options.yaml new file mode 100644 index 000000000..095b1d67f --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_macos/analysis_options.yaml @@ -0,0 +1 @@ +include: package:mostly_reasonable_lints/flutter.yaml diff --git a/keypress_simulator/packages/keypress_simulator_macos/macos/Classes/KeypressSimulatorMacosPlugin.swift b/keypress_simulator/packages/keypress_simulator_macos/macos/Classes/KeypressSimulatorMacosPlugin.swift new file mode 100644 index 000000000..b6c30798a --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_macos/macos/Classes/KeypressSimulatorMacosPlugin.swift @@ -0,0 +1,117 @@ +import Cocoa +import FlutterMacOS + +public class KeypressSimulatorMacosPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "dev.leanflutter.plugins/keypress_simulator", binaryMessenger: registrar.messenger) + let instance = KeypressSimulatorMacosPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "isAccessAllowed": + isAccessAllowed(call, result: result) + break + case "requestAccess": + requestAccess(call, result: result) + break + case "simulateKeyPress": + simulateKeyPress(call, result: result) + break + case "simulateMouseClick": + simulateMouseClick(call, result: result) + break + default: + result(FlutterMethodNotImplemented) + } + } + public func isAccessAllowed(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + result(AXIsProcessTrusted()) + } + + public func requestAccess(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let args:[String: Any] = call.arguments as! [String: Any] + let onlyOpenPrefPane: Bool = args["onlyOpenPrefPane"] as! Bool + + if (!onlyOpenPrefPane) { + let options = [kAXTrustedCheckOptionPrompt.takeRetainedValue(): true] as CFDictionary + AXIsProcessTrustedWithOptions(options) + } else { + let prefpaneUrl = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")! + NSWorkspace.shared.open(prefpaneUrl) + } + result(true) + } + + public func simulateKeyPress(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let args:[String: Any] = call.arguments as! [String: Any] + + let keyCode: Int? = args["keyCode"] as? Int + let modifiers: Array = args["modifiers"] as! Array + let keyDown: Bool = args["keyDown"] as! Bool + + let event = _createKeyPressEvent(keyCode, modifiers, keyDown); + event.post(tap: .cghidEventTap); + result(true) + } + + + public func simulateMouseClick(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let args:[String: Any] = call.arguments as! [String: Any] + + let x: Double = args["x"] as! Double + let y: Double = args["y"] as! Double + let keyDown: Bool = args["keyDown"] as! Bool + + let point = CGPoint(x: x, y: y) + + // Move mouse to the point + let move = CGEvent(mouseEventSource: nil, + mouseType: .mouseMoved, + mouseCursorPosition: point, + mouseButton: .left) + move?.post(tap: .cghidEventTap) + + if (keyDown) { + // Mouse down + let mouseDown = CGEvent(mouseEventSource: nil, + mouseType: .leftMouseDown, + mouseCursorPosition: point, + mouseButton: .left) + mouseDown?.post(tap: .cghidEventTap) + } else { + // Mouse up + let mouseUp = CGEvent(mouseEventSource: nil, + mouseType: .leftMouseUp, + mouseCursorPosition: point, + mouseButton: .left) + mouseUp?.post(tap: .cghidEventTap) + } + result(true) + } + + public func _createKeyPressEvent(_ keyCode: Int?, _ modifiers: Array, _ keyDown: Bool) -> CGEvent { + let virtualKey: CGKeyCode = CGKeyCode(UInt32(keyCode ?? 0)) + var flags: CGEventFlags = CGEventFlags() + + if (modifiers.contains("shiftModifier")) { + flags.insert(CGEventFlags.maskShift) + } + if (modifiers.contains("controlModifier")) { + flags.insert(CGEventFlags.maskControl) + } + if (modifiers.contains("altModifier")) { + flags.insert(CGEventFlags.maskAlternate) + } + if (modifiers.contains("metaModifier")) { + flags.insert(CGEventFlags.maskCommand) + } + if (modifiers.contains("functionModifier")) { + flags.insert(CGEventFlags.maskSecondaryFn) + } + let eventKeyPress = CGEvent(keyboardEventSource: nil, virtualKey: virtualKey, keyDown: keyDown); + eventKeyPress!.flags = flags + return eventKeyPress! + } +} diff --git a/keypress_simulator/packages/keypress_simulator_macos/macos/keypress_simulator_macos.podspec b/keypress_simulator/packages/keypress_simulator_macos/macos/keypress_simulator_macos.podspec new file mode 100644 index 000000000..81ed85326 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_macos/macos/keypress_simulator_macos.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint keypress_simulator_macos.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'keypress_simulator_macos' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin project.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' +end diff --git a/keypress_simulator/packages/keypress_simulator_macos/pubspec.yaml b/keypress_simulator/packages/keypress_simulator_macos/pubspec.yaml new file mode 100644 index 000000000..c414c0b11 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_macos/pubspec.yaml @@ -0,0 +1,27 @@ +name: keypress_simulator_macos +description: macOS implementation of the keypress_simulator plugin. +version: 0.2.0 +repository: https://github.com/leanflutter/keypress_simulator/tree/main/packages/keypress_simulator_macos + +environment: + sdk: '>=3.0.0 <4.0.0' + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + keypress_simulator_platform_interface: + path: ../keypress_simulator_platform_interface + +dev_dependencies: + flutter_test: + sdk: flutter + mostly_reasonable_lints: ^0.1.1 + +flutter: + plugin: + implements: keypress_simulator + platforms: + macos: + pluginClass: KeypressSimulatorMacosPlugin + diff --git a/keypress_simulator/packages/keypress_simulator_platform_interface/.gitignore b/keypress_simulator/packages/keypress_simulator_platform_interface/.gitignore new file mode 100644 index 000000000..ac5aa9893 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_platform_interface/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/keypress_simulator/packages/keypress_simulator_platform_interface/.metadata b/keypress_simulator/packages/keypress_simulator_platform_interface/.metadata new file mode 100644 index 000000000..9e91d2fd4 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_platform_interface/.metadata @@ -0,0 +1,27 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "67457e669f79e9f8d13d7a68fe09775fefbb79f4" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 + base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/keypress_simulator/packages/keypress_simulator_platform_interface/CHANGELOG.md b/keypress_simulator/packages/keypress_simulator_platform_interface/CHANGELOG.md new file mode 100644 index 000000000..2f145cea2 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_platform_interface/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.2.0 + +* First release. diff --git a/keypress_simulator/packages/keypress_simulator_platform_interface/LICENSE b/keypress_simulator/packages/keypress_simulator_platform_interface/LICENSE new file mode 100644 index 000000000..eea05ab78 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_platform_interface/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2024 LiJianying + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/keypress_simulator/packages/keypress_simulator_platform_interface/README.md b/keypress_simulator/packages/keypress_simulator_platform_interface/README.md new file mode 100644 index 000000000..907200d10 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_platform_interface/README.md @@ -0,0 +1,16 @@ +# keypress_simulator_platform_interface + +[![pub version][pub-image]][pub-url] + +[pub-image]: https://img.shields.io/pub/v/keypress_simulator_platform_interface.svg +[pub-url]: https://pub.dev/packages/keypress_simulator_platform_interface + +A common platform interface for the [keypress_simulator](https://pub.dev/packages/keypress_simulator) plugin. + +## Usage + +To implement a new platform-specific implementation of keypress_simulator, extend `KeyPressSimulatorPlatform` with an implementation that performs the platform-specific behavior, and when you register your plugin, set the default `KeyPressSimulatorPlatform` by calling `KeyPressSimulatorPlatform.instance = MyPlatformKeyPressSimulator()`. + +## License + +[MIT](./LICENSE) diff --git a/keypress_simulator/packages/keypress_simulator_platform_interface/analysis_options.yaml b/keypress_simulator/packages/keypress_simulator_platform_interface/analysis_options.yaml new file mode 100644 index 000000000..095b1d67f --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_platform_interface/analysis_options.yaml @@ -0,0 +1 @@ +include: package:mostly_reasonable_lints/flutter.yaml diff --git a/keypress_simulator/packages/keypress_simulator_platform_interface/lib/keypress_simulator_platform_interface.dart b/keypress_simulator/packages/keypress_simulator_platform_interface/lib/keypress_simulator_platform_interface.dart new file mode 100644 index 000000000..fdd2ecad6 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_platform_interface/lib/keypress_simulator_platform_interface.dart @@ -0,0 +1,4 @@ +library keypress_simulator_platform_interface; + +export 'src/keypress_simulator_method_channel.dart'; +export 'src/keypress_simulator_platform_interface.dart'; diff --git a/keypress_simulator/packages/keypress_simulator_platform_interface/lib/src/keypress_simulator_method_channel.dart b/keypress_simulator/packages/keypress_simulator_platform_interface/lib/src/keypress_simulator_method_channel.dart new file mode 100644 index 000000000..1d444b5c7 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_platform_interface/lib/src/keypress_simulator_method_channel.dart @@ -0,0 +1,64 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:keypress_simulator_platform_interface/src/keypress_simulator_platform_interface.dart'; +import 'package:uni_platform/uni_platform.dart'; + +/// An implementation of [KeyPressSimulatorPlatform] that uses method channels. +class MethodChannelKeyPressSimulator extends KeyPressSimulatorPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel( + 'dev.leanflutter.plugins/keypress_simulator', + ); + + @override + Future isAccessAllowed() async { + if (UniPlatform.isMacOS) { + return await methodChannel.invokeMethod('isAccessAllowed'); + } + return true; + } + + @override + Future requestAccess({ + bool onlyOpenPrefPane = false, + }) async { + if (UniPlatform.isMacOS) { + final Map arguments = { + 'onlyOpenPrefPane': onlyOpenPrefPane, + }; + await methodChannel.invokeMethod('requestAccess', arguments); + } + } + + @override + Future simulateKeyPress({ + KeyboardKey? key, + List modifiers = const [], + bool keyDown = true, + }) async { + PhysicalKeyboardKey? physicalKey = key is PhysicalKeyboardKey ? key : null; + if (key is LogicalKeyboardKey) { + physicalKey = key.physicalKey; + } + if (key != null && physicalKey == null) { + throw UnsupportedError('Unsupported key: $key.'); + } + final Map arguments = { + 'keyCode': physicalKey?.keyCode, + 'modifiers': modifiers.map((e) => e.name).toList(), + 'keyDown': keyDown, + }..removeWhere((key, value) => value == null); + await methodChannel.invokeMethod('simulateKeyPress', arguments); + } + + @override + Future simulateMouseClick(Offset position, {required bool keyDown}) async { + final Map arguments = { + 'x': position.dx, + 'y': position.dy, + 'keyDown': keyDown, + }; + await methodChannel.invokeMethod('simulateMouseClick', arguments); + } +} diff --git a/keypress_simulator/packages/keypress_simulator_platform_interface/lib/src/keypress_simulator_platform_interface.dart b/keypress_simulator/packages/keypress_simulator_platform_interface/lib/src/keypress_simulator_platform_interface.dart new file mode 100644 index 000000000..b2c67a8f2 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_platform_interface/lib/src/keypress_simulator_platform_interface.dart @@ -0,0 +1,47 @@ +import 'package:flutter/services.dart'; +import 'package:keypress_simulator_platform_interface/src/keypress_simulator_method_channel.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +abstract class KeyPressSimulatorPlatform extends PlatformInterface { + /// Constructs a KeyPressSimulatorPlatform. + KeyPressSimulatorPlatform() : super(token: _token); + + static final Object _token = Object(); + + static KeyPressSimulatorPlatform _instance = MethodChannelKeyPressSimulator(); + + /// The default instance of [KeyPressSimulatorPlatform] to use. + /// + /// Defaults to [MethodChannelKeyPressSimulator]. + static KeyPressSimulatorPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [KeyPressSimulatorPlatform] when + /// they register themselves. + static set instance(KeyPressSimulatorPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future isAccessAllowed() { + throw UnimplementedError('isAccessAllowed() has not been implemented.'); + } + + Future requestAccess({ + bool onlyOpenPrefPane = false, + }) { + throw UnimplementedError('requestAccess() has not been implemented.'); + } + + Future simulateKeyPress({ + KeyboardKey? key, + List modifiers = const [], + bool keyDown = true, + }) { + throw UnimplementedError('simulateKeyPress() has not been implemented.'); + } + + Future simulateMouseClick(Offset position, {required bool keyDown}) { + throw UnimplementedError('simulateKeyPress() has not been implemented.'); + } +} diff --git a/keypress_simulator/packages/keypress_simulator_platform_interface/pubspec.yaml b/keypress_simulator/packages/keypress_simulator_platform_interface/pubspec.yaml new file mode 100644 index 000000000..4b85fcf11 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_platform_interface/pubspec.yaml @@ -0,0 +1,20 @@ +name: keypress_simulator_platform_interface +description: A common platform interface for the keypress_simulator plugin. +version: 0.2.0 +homepage: https://github.com/leanflutter/keypress_simulator/blob/main/packages/keypress_simulator_platform_interface + +environment: + sdk: '>=3.0.0 <4.0.0' + flutter: '>=3.3.0' + +dependencies: + collection: ^1.18.0 + flutter: + sdk: flutter + plugin_platform_interface: ^2.1.8 + uni_platform: ^0.1.2 + +dev_dependencies: + flutter_test: + sdk: flutter + mostly_reasonable_lints: ^0.1.1 diff --git a/keypress_simulator/packages/keypress_simulator_platform_interface/test/src/keypress_simulator_method_channel_test.dart b/keypress_simulator/packages/keypress_simulator_platform_interface/test/src/keypress_simulator_method_channel_test.dart new file mode 100644 index 000000000..af2c3b90e --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_platform_interface/test/src/keypress_simulator_method_channel_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:keypress_simulator_platform_interface/src/keypress_simulator_method_channel.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + MethodChannelKeyPressSimulator platform = MethodChannelKeyPressSimulator(); + const MethodChannel channel = MethodChannel( + 'dev.leanflutter.plugins/keypress_simulator', + ); + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + channel, + (MethodCall methodCall) async { + if (methodCall.method == 'isAccessAllowed') return true; + return '42'; + }, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + + test('isAccessAllowed', () async { + expect(await platform.isAccessAllowed(), true); + }); +} diff --git a/keypress_simulator/packages/keypress_simulator_windows/.gitignore b/keypress_simulator/packages/keypress_simulator_windows/.gitignore new file mode 100644 index 000000000..ac5aa9893 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_windows/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/keypress_simulator/packages/keypress_simulator_windows/.metadata b/keypress_simulator/packages/keypress_simulator_windows/.metadata new file mode 100644 index 000000000..bcaf04fcc --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_windows/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "67457e669f79e9f8d13d7a68fe09775fefbb79f4" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 + base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 + - platform: windows + create_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 + base_revision: 67457e669f79e9f8d13d7a68fe09775fefbb79f4 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/keypress_simulator/packages/keypress_simulator_windows/CHANGELOG.md b/keypress_simulator/packages/keypress_simulator_windows/CHANGELOG.md new file mode 100644 index 000000000..2f145cea2 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_windows/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.2.0 + +* First release. diff --git a/keypress_simulator/packages/keypress_simulator_windows/LICENSE b/keypress_simulator/packages/keypress_simulator_windows/LICENSE new file mode 100644 index 000000000..eea05ab78 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_windows/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2024 LiJianying + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/keypress_simulator/packages/keypress_simulator_windows/README.md b/keypress_simulator/packages/keypress_simulator_windows/README.md new file mode 100644 index 000000000..a7ee3cad9 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_windows/README.md @@ -0,0 +1,12 @@ +# keypress_simulator_windows + +[![pub version][pub-image]][pub-url] + +[pub-image]: https://img.shields.io/pub/v/keypress_simulator_windows.svg +[pub-url]: https://pub.dev/packages/keypress_simulator_windows + +The Windows implementation of [keypress_simulator](https://pub.dev/packages/keypress_simulator). + +## License + +[MIT](./LICENSE) diff --git a/keypress_simulator/packages/keypress_simulator_windows/analysis_options.yaml b/keypress_simulator/packages/keypress_simulator_windows/analysis_options.yaml new file mode 100644 index 000000000..095b1d67f --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_windows/analysis_options.yaml @@ -0,0 +1 @@ +include: package:mostly_reasonable_lints/flutter.yaml diff --git a/keypress_simulator/packages/keypress_simulator_windows/pubspec.yaml b/keypress_simulator/packages/keypress_simulator_windows/pubspec.yaml new file mode 100644 index 000000000..1f94f85aa --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_windows/pubspec.yaml @@ -0,0 +1,26 @@ +name: keypress_simulator_windows +description: Windows implementation of the keypress_simulator plugin. +version: 0.2.0 +repository: https://github.com/leanflutter/keypress_simulator/tree/main/packages/keypress_simulator_windows + +environment: + sdk: '>=3.0.0 <4.0.0' + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + keypress_simulator_platform_interface: + path: ../keypress_simulator_platform_interface + +dev_dependencies: + flutter_test: + sdk: flutter + mostly_reasonable_lints: ^0.1.1 + +flutter: + plugin: + implements: keypress_simulator + platforms: + windows: + pluginClass: KeypressSimulatorWindowsPluginCApi diff --git a/keypress_simulator/packages/keypress_simulator_windows/windows/.gitignore b/keypress_simulator/packages/keypress_simulator_windows/windows/.gitignore new file mode 100644 index 000000000..b3eb2be16 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_windows/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/keypress_simulator/packages/keypress_simulator_windows/windows/CMakeLists.txt b/keypress_simulator/packages/keypress_simulator_windows/windows/CMakeLists.txt new file mode 100644 index 000000000..a516e24b8 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_windows/windows/CMakeLists.txt @@ -0,0 +1,100 @@ +# The Flutter tooling requires that developers have a version of Visual Studio +# installed that includes CMake 3.14 or later. You should not increase this +# version, as doing so will cause the plugin to fail to compile for some +# customers of the plugin. +cmake_minimum_required(VERSION 3.14) + +# Project-level configuration. +set(PROJECT_NAME "keypress_simulator_windows") +project(${PROJECT_NAME} LANGUAGES CXX) + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# This value is used when generating builds using this plugin, so it must +# not be changed +set(PLUGIN_NAME "keypress_simulator_windows_plugin") + +# Any new source files that you add to the plugin should be added here. +list(APPEND PLUGIN_SOURCES + "keypress_simulator_windows_plugin.cpp" + "keypress_simulator_windows_plugin.h" +) + +# Define the plugin library target. Its name must not be changed (see comment +# on PLUGIN_NAME above). +add_library(${PLUGIN_NAME} SHARED + "include/keypress_simulator_windows/keypress_simulator_windows_plugin_c_api.h" + "keypress_simulator_windows_plugin_c_api.cpp" + ${PLUGIN_SOURCES} +) + +# Apply a standard set of build settings that are configured in the +# application-level CMakeLists.txt. This can be removed for plugins that want +# full control over build settings. +apply_standard_settings(${PLUGIN_NAME}) + +# Symbols are hidden by default to reduce the chance of accidental conflicts +# between plugins. This should not be removed; any symbols that should be +# exported should be explicitly exported with the FLUTTER_PLUGIN_EXPORT macro. +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) + +# Source include directories and library dependencies. Add any plugin-specific +# dependencies here. +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(keypress_simulator_windows_bundled_libraries + "" + PARENT_SCOPE +) + +# === Tests === +# These unit tests can be run from a terminal after building the example, or +# from Visual Studio after opening the generated solution file. + +# Only enable test builds when building the example (which sets this variable) +# so that plugin clients aren't building the tests. +if (${include_${PROJECT_NAME}_tests}) +set(TEST_RUNNER "${PROJECT_NAME}_test") +enable_testing() + +# Add the Google Test dependency. +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/release-1.11.0.zip +) +# Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +# Disable install commands for gtest so it doesn't end up in the bundle. +set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE) +FetchContent_MakeAvailable(googletest) + +# The plugin's C API is not very useful for unit testing, so build the sources +# directly into the test binary rather than using the DLL. +add_executable(${TEST_RUNNER} + test/keypress_simulator_windows_plugin_test.cpp + ${PLUGIN_SOURCES} +) +apply_standard_settings(${TEST_RUNNER}) +target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin) +target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock) +# flutter_wrapper_plugin has link dependencies on the Flutter DLL. +add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${FLUTTER_LIBRARY}" $ +) + +# Enable automatic test discovery. +include(GoogleTest) +gtest_discover_tests(${TEST_RUNNER}) +endif() diff --git a/keypress_simulator/packages/keypress_simulator_windows/windows/include/keypress_simulator_windows/keypress_simulator_windows_plugin_c_api.h b/keypress_simulator/packages/keypress_simulator_windows/windows/include/keypress_simulator_windows/keypress_simulator_windows_plugin_c_api.h new file mode 100644 index 000000000..f69a1c287 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_windows/windows/include/keypress_simulator_windows/keypress_simulator_windows_plugin_c_api.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_C_API_H_ +#define FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_C_API_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void KeypressSimulatorWindowsPluginCApiRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_C_API_H_ diff --git a/keypress_simulator/packages/keypress_simulator_windows/windows/keypress_simulator_windows_plugin.cpp b/keypress_simulator/packages/keypress_simulator_windows/windows/keypress_simulator_windows_plugin.cpp new file mode 100644 index 000000000..0ac1f4b9a --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_windows/windows/keypress_simulator_windows_plugin.cpp @@ -0,0 +1,270 @@ +#include "keypress_simulator_windows_plugin.h" + +// This must be included before many other Windows headers. +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +using flutter::EncodableList; +using flutter::EncodableMap; +using flutter::EncodableValue; + +namespace keypress_simulator_windows { + +// Forward declarations +struct FindWindowData { + std::string targetProcessName; + std::string targetWindowTitle; + HWND foundWindow; +}; + +BOOL CALLBACK EnumWindowsCallback(HWND hwnd, LPARAM lParam); +HWND FindTargetWindow(const std::string& processName, const std::string& windowTitle); + +// static +void KeypressSimulatorWindowsPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarWindows* registrar) { + auto channel = + std::make_unique>( + registrar->messenger(), "dev.leanflutter.plugins/keypress_simulator", + &flutter::StandardMethodCodec::GetInstance()); + + auto plugin = std::make_unique(); + + channel->SetMethodCallHandler( + [plugin_pointer = plugin.get()](const auto& call, auto result) { + plugin_pointer->HandleMethodCall(call, std::move(result)); + }); + + registrar->AddPlugin(std::move(plugin)); +} + +KeypressSimulatorWindowsPlugin::KeypressSimulatorWindowsPlugin() {} + +KeypressSimulatorWindowsPlugin::~KeypressSimulatorWindowsPlugin() {} + +void KeypressSimulatorWindowsPlugin::SimulateKeyPress( + const flutter::MethodCall& method_call, + std::unique_ptr> result) { + const EncodableMap& args = std::get(*method_call.arguments()); + + UINT keyCode = std::get(args.at(EncodableValue("keyCode"))); + std::vector modifiers; + bool keyDown = std::get(args.at(EncodableValue("keyDown"))); + + EncodableList key_modifier_list = + std::get(args.at(EncodableValue("modifiers"))); + for (flutter::EncodableValue key_modifier_value : key_modifier_list) { + std::string key_modifier = std::get(key_modifier_value); + modifiers.push_back(key_modifier); + } + + // List of compatible training apps to look for + std::vector compatibleApps = { + "MyWhooshHD.exe", + "indieVelo.exe", + "biketerra.exe" + }; + + // Try to find and focus a compatible app + HWND targetWindow = NULL; + for (const std::string& processName : compatibleApps) { + targetWindow = FindTargetWindow(processName, ""); + if (targetWindow != NULL) { + // Only focus the window if it's not already in the foreground + if (GetForegroundWindow() != targetWindow) { + SetForegroundWindow(targetWindow); + Sleep(50); // Brief delay to ensure window is focused + } + break; + } + } + + // Helper function to send modifier key events + auto sendModifierKey = [](UINT vkCode, bool down) { + WORD sc = (WORD)MapVirtualKey(vkCode, MAPVK_VK_TO_VSC); + INPUT in = {0}; + in.type = INPUT_KEYBOARD; + in.ki.wVk = 0; + in.ki.wScan = sc; + in.ki.dwFlags = KEYEVENTF_SCANCODE | (down ? 0 : KEYEVENTF_KEYUP); + SendInput(1, &in, sizeof(INPUT)); + }; + + // Helper function to process modifiers + auto processModifiers = [&sendModifierKey](const std::vector& mods, bool down) { + for (const std::string& modifier : mods) { + if (modifier == "shiftModifier") { + sendModifierKey(VK_SHIFT, down); + } else if (modifier == "controlModifier") { + sendModifierKey(VK_CONTROL, down); + } else if (modifier == "altModifier") { + sendModifierKey(VK_MENU, down); + } else if (modifier == "metaModifier") { + sendModifierKey(VK_LWIN, down); + } + } + }; + + // Press modifier keys first (if keyDown) + if (keyDown) { + processModifiers(modifiers, true); + } + + // Send the main key + WORD sc = (WORD)MapVirtualKey(keyCode, MAPVK_VK_TO_VSC); + + INPUT in = {0}; + in.type = INPUT_KEYBOARD; + in.ki.wVk = 0; // when using SCANCODE, set VK=0 + in.ki.wScan = sc; + in.ki.dwFlags = KEYEVENTF_SCANCODE | (keyDown ? 0 : KEYEVENTF_KEYUP); + if (keyCode == VK_LEFT || keyCode == VK_RIGHT || keyCode == VK_UP || keyCode == VK_DOWN || + keyCode == VK_INSERT || keyCode == VK_DELETE || keyCode == VK_HOME || keyCode == VK_END || + keyCode == VK_PRIOR || keyCode == VK_NEXT) { + in.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY; + } + SendInput(1, &in, sizeof(INPUT)); + + // Release modifier keys (if keyUp) + if (!keyDown) { + processModifiers(modifiers, false); + } + + /*BYTE byteValue = static_cast(keyCode); + keybd_event(byteValue, 0x45, keyDown ? 0 : KEYEVENTF_KEYUP, 0);*/ + + result->Success(flutter::EncodableValue(true)); +} + +void KeypressSimulatorWindowsPlugin::SimulateMouseClick( + const flutter::MethodCall& method_call, + std::unique_ptr> result) { + + const EncodableMap& args = std::get(*method_call.arguments()); + double x = 0; + double y = 0; + + bool keyDown = std::get(args.at(EncodableValue("keyDown"))); + auto it_x = args.find(EncodableValue("x")); + if (it_x != args.end() && std::holds_alternative(it_x->second)) { + x = std::get(it_x->second); + } + + auto it_y = args.find(EncodableValue("y")); + if (it_y != args.end() && std::holds_alternative(it_y->second)) { + y = std::get(it_y->second); + } + + // Get the monitor containing the target point and its DPI + const POINT target_point = {static_cast(x), static_cast(y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + // Scale the coordinates according to the DPI scaling + int scaled_x = static_cast(x * scale_factor); + int scaled_y = static_cast(y * scale_factor); + + // Move the mouse to the specified coordinates + SetCursorPos(scaled_x, scaled_y); + + // Prepare input for mouse down and up + INPUT input = {0}; + input.type = INPUT_MOUSE; + + if (keyDown) { + // Mouse left button down + input.mi.dwFlags = MOUSEEVENTF_LEFTDOWN; + SendInput(1, &input, sizeof(INPUT)); + + } else { + // Mouse left button up + input.mi.dwFlags = MOUSEEVENTF_LEFTUP; + SendInput(1, &input, sizeof(INPUT)); + } + + result->Success(flutter::EncodableValue(true)); +} + +BOOL CALLBACK EnumWindowsCallback(HWND hwnd, LPARAM lParam) { + FindWindowData* data = reinterpret_cast(lParam); + + // Check if window is visible and not minimized + if (!IsWindowVisible(hwnd) || IsIconic(hwnd)) { + return TRUE; // Continue enumeration + } + + // Get window title + char windowTitle[256]; + GetWindowTextA(hwnd, windowTitle, sizeof(windowTitle)); + + // Get process name + DWORD processId; + GetWindowThreadProcessId(hwnd, &processId); + HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId); + char processName[MAX_PATH]; + if (hProcess) { + DWORD size = sizeof(processName); + if (QueryFullProcessImageNameA(hProcess, 0, processName, &size)) { + // Extract just the filename from the full path + char* filename = strrchr(processName, '\\'); + if (filename) { + filename++; // Skip the backslash + } else { + filename = processName; + } + + // Check if this matches our target + if (!data->targetProcessName.empty() && + _stricmp(filename, data->targetProcessName.c_str()) == 0) { + data->foundWindow = hwnd; + return FALSE; // Stop enumeration + } + } + CloseHandle(hProcess); + } + + // Check window title if process name didn't match + if (!data->targetWindowTitle.empty() && + _stricmp(windowTitle, data->targetWindowTitle.c_str()) == 0) { + data->foundWindow = hwnd; + return FALSE; // Stop enumeration + } + + return TRUE; // Continue enumeration +} + +HWND FindTargetWindow(const std::string& processName, const std::string& windowTitle) { + FindWindowData data; + data.targetProcessName = processName; + data.targetWindowTitle = windowTitle; + data.foundWindow = NULL; + + EnumWindows(EnumWindowsCallback, reinterpret_cast(&data)); + return data.foundWindow; +} + + + +void KeypressSimulatorWindowsPlugin::HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result) { + if (method_call.method_name().compare("simulateKeyPress") == 0) { + SimulateKeyPress(method_call, std::move(result)); + } else if (method_call.method_name().compare("simulateMouseClick") == 0) { + SimulateMouseClick(method_call, std::move(result)); + } else { + result->NotImplemented(); + } +} + +} // namespace keypress_simulator_windows diff --git a/keypress_simulator/packages/keypress_simulator_windows/windows/keypress_simulator_windows_plugin.h b/keypress_simulator/packages/keypress_simulator_windows/windows/keypress_simulator_windows_plugin.h new file mode 100644 index 000000000..57d5a2801 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_windows/windows/keypress_simulator_windows_plugin.h @@ -0,0 +1,43 @@ +#ifndef FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_H_ +#define FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_H_ + +#include +#include + +#include + +namespace keypress_simulator_windows { + +class KeypressSimulatorWindowsPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); + + KeypressSimulatorWindowsPlugin(); + + virtual ~KeypressSimulatorWindowsPlugin(); + + // Disallow copy and assign. + KeypressSimulatorWindowsPlugin(const KeypressSimulatorWindowsPlugin&) = + delete; + KeypressSimulatorWindowsPlugin& operator=( + const KeypressSimulatorWindowsPlugin&) = delete; + + void KeypressSimulatorWindowsPlugin::SimulateKeyPress( + const flutter::MethodCall& method_call, + std::unique_ptr> result); + + void KeypressSimulatorWindowsPlugin::SimulateMouseClick( + const flutter::MethodCall& method_call, + std::unique_ptr> result); + + + + // Called when a method is called on this plugin's channel from Dart. + void HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result); +}; + +} // namespace keypress_simulator_windows + +#endif // FLUTTER_PLUGIN_KEYPRESS_SIMULATOR_WINDOWS_PLUGIN_H_ diff --git a/keypress_simulator/packages/keypress_simulator_windows/windows/keypress_simulator_windows_plugin_c_api.cpp b/keypress_simulator/packages/keypress_simulator_windows/windows/keypress_simulator_windows_plugin_c_api.cpp new file mode 100644 index 000000000..5c6fb9bad --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_windows/windows/keypress_simulator_windows_plugin_c_api.cpp @@ -0,0 +1,12 @@ +#include "include/keypress_simulator_windows/keypress_simulator_windows_plugin_c_api.h" + +#include + +#include "keypress_simulator_windows_plugin.h" + +void KeypressSimulatorWindowsPluginCApiRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + keypress_simulator_windows::KeypressSimulatorWindowsPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/keypress_simulator/packages/keypress_simulator_windows/windows/test/keypress_simulator_windows_plugin_test.cpp b/keypress_simulator/packages/keypress_simulator_windows/windows/test/keypress_simulator_windows_plugin_test.cpp new file mode 100644 index 000000000..8e0518994 --- /dev/null +++ b/keypress_simulator/packages/keypress_simulator_windows/windows/test/keypress_simulator_windows_plugin_test.cpp @@ -0,0 +1,43 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "keypress_simulator_windows_plugin.h" + +namespace keypress_simulator_windows { +namespace test { + +namespace { + +using flutter::EncodableMap; +using flutter::EncodableValue; +using flutter::MethodCall; +using flutter::MethodResultFunctions; + +} // namespace + +TEST(KeypressSimulatorWindowsPlugin, GetPlatformVersion) { + KeypressSimulatorWindowsPlugin plugin; + // Save the reply value from the success callback. + std::string result_string; + plugin.HandleMethodCall( + MethodCall("getPlatformVersion", std::make_unique()), + std::make_unique>( + [&result_string](const EncodableValue* result) { + result_string = std::get(*result); + }, + nullptr, nullptr)); + + // Since the exact string varies by host, just ensure that it's a string + // with the expected format. + EXPECT_TRUE(result_string.rfind("Windows ", 0) == 0); +} + +} // namespace test +} // namespace keypress_simulator_windows diff --git a/keypress_simulator/pubspec.yaml b/keypress_simulator/pubspec.yaml new file mode 100644 index 000000000..9d261665d --- /dev/null +++ b/keypress_simulator/pubspec.yaml @@ -0,0 +1,9 @@ +name: keypress_simulator_workspace +homepage: https://github.com/leanflutter/keypress_simulator +publish_to: none + +environment: + sdk: ">=3.0.0 <4.0.0" + +dev_dependencies: + melos: ^3.1.0 diff --git a/lib/bluetooth/ble.dart b/lib/bluetooth/ble.dart new file mode 100644 index 000000000..6e5e8808e --- /dev/null +++ b/lib/bluetooth/ble.dart @@ -0,0 +1,7 @@ +class BleUuid { + static const DEVICE_INFORMATION_SERVICE_UUID = "0000180a-0000-1000-8000-00805f9b34fb"; + static const DEVICE_INFORMATION_CHARACTERISTIC_FIRMWARE_REVISION = "00002a26-0000-1000-8000-00805f9b34fb"; + + static const DEVICE_BATTERY_SERVICE_UUID = "0000180f-0000-1000-8000-00805f9b34fb"; + static const DEVICE_INFORMATION_CHARACTERISTIC_BATTERY_LEVEL = "00002A19-0000-1000-8000-00805F9B34FB"; +} diff --git a/lib/bluetooth/connection.dart b/lib/bluetooth/connection.dart new file mode 100644 index 000000000..ea359fddf --- /dev/null +++ b/lib/bluetooth/connection.dart @@ -0,0 +1,391 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:gamepads/gamepads.dart'; +import 'package:media_key_detector/media_key_detector.dart'; +import 'package:swift_control/bluetooth/devices/bluetooth_device.dart'; +import 'package:swift_control/bluetooth/devices/gamepad/gamepad_device.dart'; +import 'package:swift_control/bluetooth/devices/hid/hid_device.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/actions/android.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/utils/keymap/keymap.dart'; +import 'package:swift_control/utils/requirements/android.dart'; +import 'package:universal_ble/universal_ble.dart'; + +import '../utils/keymap/apps/my_whoosh.dart'; +import 'devices/base_device.dart'; +import 'devices/zwift/constants.dart'; +import 'messages/notification.dart'; + +class Connection { + final devices = []; + + List get bluetoothDevices => devices.whereType().toList(); + List get gamepadDevices => devices.whereType().toList(); + List get controllerDevices => [...bluetoothDevices, ...gamepadDevices, ...devices.whereType()]; + + var _androidNotificationsSetup = false; + + final _connectionQueue = []; + var _handlingConnectionQueue = false; + + final Map> _streamSubscriptions = {}; + final StreamController _actionStreams = StreamController.broadcast(); + Stream get actionStream => _actionStreams.stream; + List<({DateTime date, String entry})> lastLogEntries = []; + + final Map> _connectionSubscriptions = {}; + final StreamController _connectionStreams = StreamController.broadcast(); + Stream get connectionStream => _connectionStreams.stream; + + final _lastScanResult = []; + final ValueNotifier hasDevices = ValueNotifier(false); + final ValueNotifier isScanning = ValueNotifier(false); + final ValueNotifier isMediaKeyDetectionEnabled = ValueNotifier(false); + + Timer? _gamePadSearchTimer; + + void initialize() { + actionStream.listen((log) { + lastLogEntries.add((date: DateTime.now(), entry: log.toString())); + lastLogEntries = lastLogEntries.takeLast(20).toList(); + }); + + isMediaKeyDetectionEnabled.addListener(() { + if (!isMediaKeyDetectionEnabled.value) { + mediaKeyDetector.setIsPlaying(isPlaying: false); + mediaKeyDetector.removeListener(_onMediaKeyDetectedListener); + } else { + mediaKeyDetector.addListener(_onMediaKeyDetectedListener); + mediaKeyDetector.setIsPlaying(isPlaying: true); + } + }); + + UniversalBle.onAvailabilityChange = (available) { + _actionStreams.add(BluetoothAvailabilityNotification(available == AvailabilityState.poweredOn)); + if (available == AvailabilityState.poweredOn && !kIsWeb) { + performScanning(); + } else if (available == AvailabilityState.poweredOff) { + reset(); + } + }; + UniversalBle.onScanResult = (result) { + // Update RSSI for already connected devices + final existingDevice = bluetoothDevices.firstOrNullWhere( + (e) => e.device.deviceId == result.deviceId, + ); + if (existingDevice != null && existingDevice.rssi != result.rssi) { + existingDevice.rssi = result.rssi; + _connectionStreams.add(existingDevice); // Notify UI of update + } + + if (_lastScanResult.none((e) => e.deviceId == result.deviceId && e.services.contentEquals(result.services))) { + _lastScanResult.add(result); + + if (kDebugMode) { + print('Scan result: ${result.name} - ${result.deviceId}'); + } + + final scanResult = BluetoothDevice.fromScanResult(result); + + if (scanResult != null) { + _actionStreams.add(LogNotification('Found new device: ${scanResult.runtimeType}')); + addDevices([scanResult]); + } else { + final manufacturerData = result.manufacturerDataList; + final data = manufacturerData + .firstOrNullWhere((e) => e.companyId == ZwiftConstants.ZWIFT_MANUFACTURER_ID) + ?.payload; + if (data != null && kDebugMode) { + _actionStreams.add(LogNotification('Found unknown device with identifier: ${data.firstOrNull}')); + } + } + } + }; + + UniversalBle.onValueChange = (deviceId, characteristicUuid, value) async { + final device = bluetoothDevices.firstOrNullWhere((e) => e.device.deviceId == deviceId); + if (device == null) { + _actionStreams.add(LogNotification('Device not found: $deviceId')); + UniversalBle.disconnect(deviceId); + return; + } else { + try { + await device.processCharacteristic(characteristicUuid, value); + } catch (e, backtrace) { + _actionStreams.add( + LogNotification( + "Error processing characteristic for device ${device.name} and char: $characteristicUuid: $e\n$backtrace", + ), + ); + if (kDebugMode) { + print(e); + print("backtrace: $backtrace"); + } + } + } + }; + + UniversalBle.onConnectionChange = (String deviceId, bool isConnected, String? error) { + final device = bluetoothDevices.firstOrNullWhere((e) => e.device.deviceId == deviceId); + if (device != null && !isConnected) { + // allow reconnection + _lastScanResult.removeWhere((d) => d.deviceId == deviceId); + } + }; + } + + Future performScanning() async { + if (isScanning.value) { + return; + } + isScanning.value = true; + _actionStreams.add(LogNotification('Scanning for devices...')); + + // does not work on web, may not work on Windows + if (!kIsWeb && !Platform.isWindows) { + UniversalBle.getSystemDevices( + withServices: BluetoothDevice.servicesToScan, + ).then((devices) async { + final baseDevices = devices.mapNotNull(BluetoothDevice.fromScanResult).toList(); + if (baseDevices.isNotEmpty) { + addDevices(baseDevices); + } + }); + } + + await UniversalBle.startScan( + // allow all to enable Wahoo Kickr Bike Shift detection + //scanFilter: kIsWeb ? ScanFilter(withServices: BluetoothDevice.servicesToScan) : null, + platformConfig: PlatformConfig(web: WebOptions(optionalServices: BluetoothDevice.servicesToScan)), + ); + + if (!kIsWeb) { + _gamePadSearchTimer = Timer.periodic(Duration(seconds: 3), (_) { + Gamepads.list().then((list) { + final pads = list.map((pad) => GamepadDevice(pad.name, id: pad.id)).toList(); + addDevices(pads); + + final removedDevices = gamepadDevices.where((device) => list.none((pad) => pad.id == device.id)).toList(); + for (var device in removedDevices) { + devices.remove(device); + _streamSubscriptions[device]?.cancel(); + _streamSubscriptions.remove(device); + _connectionSubscriptions[device]?.cancel(); + _connectionSubscriptions.remove(device); + signalChange(device); + } + }); + }); + + Gamepads.list().then((list) { + final pads = list.map((pad) => GamepadDevice(pad.name, id: pad.id)).toList(); + addDevices(pads); + }); + } + + if (settings.getMyWhooshLinkEnabled() && + settings.getTrainerApp() is MyWhoosh && + !whooshLink.isStarted.value && + whooshLink.isCompatible(settings.getLastTarget()!)) { + startMyWhooshServer().catchError((e) { + _actionStreams.add( + LogNotification( + 'Error starting MyWhoosh Direct Connect server. Please make sure the "MyWhoosh Link" app is not already running on this device.\n$e', + ), + ); + }); + } + + if (devices.isNotEmpty && !_androidNotificationsSetup && !kIsWeb && Platform.isAndroid) { + _androidNotificationsSetup = true; + // start foreground service only when app is in foreground + NotificationRequirement.setup().catchError((e) { + _actionStreams.add(LogNotification(e.toString())); + }); + } + } + + Future startMyWhooshServer() { + return whooshLink.startServer( + onConnected: (socket) {}, + onDisconnected: (socket) {}, + ); + } + + void addDevices(List dev) { + final ignoredDevices = settings.getIgnoredDevices(); + final ignoredDeviceIds = ignoredDevices.map((d) => d.id).toSet(); + final newDevices = dev.where((device) { + if (devices.contains(device)) return false; + + // Check if device is in the ignored list + if (device is BluetoothDevice) { + if (ignoredDeviceIds.contains(device.device.deviceId)) { + return false; + } + } + + return true; + }).toList(); + devices.addAll(newDevices); + _connectionQueue.addAll(newDevices); + + _handleConnectionQueue(); + + hasDevices.value = devices.isNotEmpty; + } + + void _handleConnectionQueue() { + // windows apparently has issues when connecting to multiple devices at once, so don't + if (_connectionQueue.isNotEmpty && !_handlingConnectionQueue && !screenshotMode) { + _handlingConnectionQueue = true; + final device = _connectionQueue.removeAt(0); + _actionStreams.add(LogNotification('Connecting to: ${device.name}')); + _connect(device) + .then((_) { + _handlingConnectionQueue = false; + _actionStreams.add(LogNotification('Connection finished: ${device.name}')); + if (_connectionQueue.isNotEmpty) { + _handleConnectionQueue(); + } + }) + .catchError((e) { + _handlingConnectionQueue = false; + _actionStreams.add( + LogNotification('Connection failed: ${device.name} - $e'), + ); + if (_connectionQueue.isNotEmpty) { + _handleConnectionQueue(); + } + }); + } + } + + Future _connect(BaseDevice device) async { + try { + final actionSubscription = device.actionStream.listen((data) { + _actionStreams.add(data); + }); + if (device is BluetoothDevice) { + final connectionStateSubscription = UniversalBle.connectionStream(device.device.deviceId).listen((state) { + device.isConnected = state; + _connectionStreams.add(device); + if (!device.isConnected) { + disconnect(device, forget: false); + // try reconnect + performScanning(); + } + }); + _connectionSubscriptions[device] = connectionStateSubscription; + } + + await device.connect(); + signalChange(device); + + final newButtons = device.availableButtons.filter( + (button) => actionHandler.supportedApp?.keymap.getKeyPair(button) == null, + ); + for (final button in newButtons) { + actionHandler.supportedApp?.keymap.addKeyPair( + KeyPair( + touchPosition: Offset.zero, + buttons: [button], + physicalKey: null, + logicalKey: null, + isLongPress: false, + ), + ); + } + + _streamSubscriptions[device] = actionSubscription; + } catch (e, backtrace) { + _actionStreams.add(LogNotification("$e\n$backtrace")); + if (kDebugMode) { + print(e); + print("backtrace: $backtrace"); + } + rethrow; + } + } + + Future reset() async { + _actionStreams.add(LogNotification('Disconnecting all devices')); + if (actionHandler is AndroidActions) { + AndroidFlutterLocalNotificationsPlugin().stopForegroundService(); + _androidNotificationsSetup = false; + } + final isBtEnabled = (await UniversalBle.getBluetoothAvailabilityState()) == AvailabilityState.poweredOn; + if (isBtEnabled) { + UniversalBle.stopScan(); + } + isScanning.value = false; + for (var device in bluetoothDevices) { + _streamSubscriptions[device]?.cancel(); + _streamSubscriptions.remove(device); + _connectionSubscriptions[device]?.cancel(); + _connectionSubscriptions.remove(device); + UniversalBle.disconnect(device.device.deviceId); + signalChange(device); + } + _gamePadSearchTimer?.cancel(); + _lastScanResult.clear(); + hasDevices.value = false; + devices.clear(); + } + + void signalNotification(BaseNotification notification) { + _actionStreams.add(notification); + } + + void signalChange(BaseDevice baseDevice) { + _connectionStreams.add(baseDevice); + } + + Future disconnect(BaseDevice device, {required bool forget}) async { + if (device.isConnected) { + await device.disconnect(); + } + + if (device is BluetoothDevice) { + if (forget) { + // Add device to ignored list when forgetting + await settings.addIgnoredDevice(device.device.deviceId, device.name); + _actionStreams.add(LogNotification('Device ignored: ${device.name}')); + } + + // Clean up subscriptions and scan results for reconnection + _lastScanResult.removeWhere((b) => b.deviceId == device.device.deviceId); + _streamSubscriptions[device]?.cancel(); + _streamSubscriptions.remove(device); + _connectionSubscriptions[device]?.cancel(); + _connectionSubscriptions.remove(device); + + // Remove device from the list + devices.remove(device); + hasDevices.value = devices.isNotEmpty; + } + + signalChange(device); + } + + void _onMediaKeyDetectedListener(MediaKey mediaKey) { + final hidDevice = HidDevice('HID Device'); + final keyPressed = mediaKey.name; + + final button = actionHandler.supportedApp!.keymap.getOrAddButton(keyPressed, () => ControllerButton(keyPressed)); + + var availableDevice = connection.controllerDevices.firstOrNullWhere((e) => e.name == hidDevice.name); + if (availableDevice == null) { + connection.addDevices([hidDevice]); + availableDevice = hidDevice; + } + availableDevice.handleButtonsClicked([button]); + availableDevice.handleButtonsClicked([]); + } +} diff --git a/lib/bluetooth/devices/base_device.dart b/lib/bluetooth/devices/base_device.dart new file mode 100644 index 000000000..55d15ab2a --- /dev/null +++ b/lib/bluetooth/devices/base_device.dart @@ -0,0 +1,143 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:swift_control/bluetooth/devices/zwift/constants.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/actions/base_actions.dart'; +import 'package:swift_control/utils/actions/desktop.dart'; + +import '../../utils/keymap/buttons.dart'; +import '../messages/notification.dart'; + +abstract class BaseDevice { + final String name; + final bool isBeta; + final List availableButtons; + + BaseDevice(this.name, {required this.availableButtons, this.isBeta = false}); + + bool isConnected = false; + + Timer? _longPressTimer; + Set _previouslyPressedButtons = {}; + + @override + bool operator ==(Object other) => + identical(this, other) || other is BaseDevice && runtimeType == other.runtimeType && name == other.name; + + @override + int get hashCode => name.hashCode; + + @override + String toString() { + return name; + } + + final StreamController actionStreamInternal = StreamController.broadcast(); + + Stream get actionStream => actionStreamInternal.stream; + + Future connect(); + + Future handleButtonsClicked(List? buttonsClicked) async { + try { + await _handleButtonsClickedInternal(buttonsClicked); + } catch (e, st) { + actionStreamInternal.add( + LogNotification('Error handling button clicks: $e\n$st'), + ); + } + } + + Future _handleButtonsClickedInternal(List? buttonsClicked) async { + if (buttonsClicked == null) { + // ignore, no changes + } else if (buttonsClicked.isEmpty) { + actionStreamInternal.add(LogNotification('Buttons released')); + _longPressTimer?.cancel(); + + // Handle release events for long press keys + final buttonsReleased = _previouslyPressedButtons.toList(); + final isLongPress = + buttonsReleased.singleOrNull != null && + actionHandler.supportedApp?.keymap.getKeyPair(buttonsReleased.single)?.isLongPress == true; + if (buttonsReleased.isNotEmpty && isLongPress) { + await performRelease(buttonsReleased); + } + _previouslyPressedButtons.clear(); + } else { + actionStreamInternal.add(ButtonNotification(buttonsClicked: buttonsClicked)); + + // Handle release events for buttons that are no longer pressed + final buttonsReleased = _previouslyPressedButtons.difference(buttonsClicked.toSet()).toList(); + final wasLongPress = + buttonsReleased.singleOrNull != null && + actionHandler.supportedApp?.keymap.getKeyPair(buttonsReleased.single)?.isLongPress == true; + if (buttonsReleased.isNotEmpty && wasLongPress) { + await performRelease(buttonsReleased); + } + + final isLongPress = + buttonsClicked.singleOrNull != null && + actionHandler.supportedApp?.keymap.getKeyPair(buttonsClicked.single)?.isLongPress == true; + + if (!isLongPress && + !(buttonsClicked.singleOrNull == ZwiftButtons.onOffLeft || + buttonsClicked.singleOrNull == ZwiftButtons.onOffRight)) { + // we don't want to trigger the long press timer for the on/off buttons, also not when it's a long press key + _longPressTimer?.cancel(); + _longPressTimer = Timer.periodic(const Duration(milliseconds: 350), (timer) async { + performClick(buttonsClicked); + }); + } + // Update currently pressed buttons + _previouslyPressedButtons = buttonsClicked.toSet(); + + if (isLongPress) { + return performDown(buttonsClicked); + } else { + return performClick(buttonsClicked); + } + } + } + + Future performDown(List buttonsClicked) async { + for (final action in buttonsClicked) { + // For repeated actions, don't trigger key down/up events (useful for long press) + final result = await actionHandler.performAction(action, isKeyDown: true, isKeyUp: false); + actionStreamInternal.add( + ActionNotification(result), + ); + } + } + + Future performClick(List buttonsClicked) async { + for (final action in buttonsClicked) { + final result = await actionHandler.performAction(action, isKeyDown: true, isKeyUp: true); + actionStreamInternal.add( + ActionNotification(result), + ); + } + } + + Future performRelease(List buttonsReleased) async { + for (final action in buttonsReleased) { + final result = await actionHandler.performAction(action, isKeyDown: false, isKeyUp: true); + actionStreamInternal.add( + ActionNotification(result), + ); + } + } + + Future disconnect() async { + _longPressTimer?.cancel(); + // Release any held keys in long press mode + if (actionHandler is DesktopActions) { + await (actionHandler as DesktopActions).releaseAllHeldKeys(_previouslyPressedButtons.toList()); + } + _previouslyPressedButtons.clear(); + isConnected = false; + } + + Widget showInformation(BuildContext context); +} diff --git a/lib/bluetooth/devices/bluetooth_device.dart b/lib/bluetooth/devices/bluetooth_device.dart new file mode 100644 index 000000000..11e563359 --- /dev/null +++ b/lib/bluetooth/devices/bluetooth_device.dart @@ -0,0 +1,254 @@ +import 'dart:async'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:swift_control/bluetooth/ble.dart'; +import 'package:swift_control/bluetooth/devices/base_device.dart'; +import 'package:swift_control/bluetooth/devices/shimano/shimano_di2.dart'; +import 'package:swift_control/bluetooth/devices/wahoo/wahoo_kickr_bike_pro.dart'; +import 'package:swift_control/bluetooth/devices/wahoo/wahoo_kickr_bike_shift.dart'; +import 'package:swift_control/bluetooth/devices/zwift/constants.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_click.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_clickv2.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_device.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_play.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_ride.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/pages/device.dart'; +import 'package:swift_control/widgets/beta_pill.dart'; +import 'package:universal_ble/universal_ble.dart'; + +import 'cycplus/cycplus_bc2.dart'; +import 'elite/elite_square.dart'; +import 'elite/elite_sterzo.dart'; + +abstract class BluetoothDevice extends BaseDevice { + final BleDevice scanResult; + + BluetoothDevice(this.scanResult, {required super.availableButtons, super.isBeta = false}) + : super(scanResult.name ?? 'Unknown Device') { + rssi = scanResult.rssi; + } + + int? batteryLevel; + String? firmwareVersion; + int? rssi; + + static List servicesToScan = [ + ZwiftConstants.ZWIFT_CUSTOM_SERVICE_UUID, + ZwiftConstants.ZWIFT_RIDE_CUSTOM_SERVICE_UUID, + SquareConstants.SERVICE_UUID, + WahooKickrBikeShiftConstants.SERVICE_UUID, + SterzoConstants.SERVICE_UUID, + CycplusBc2Constants.SERVICE_UUID, + ShimanoDi2Constants.SERVICE_UUID, + ]; + + static BluetoothDevice? fromScanResult(BleDevice scanResult) { + // Use the name first as the "System Devices" and Web (android sometimes Windows) don't have manufacturer data + BluetoothDevice? device; + if (kIsWeb) { + device = switch (scanResult.name) { + 'Zwift Ride' => ZwiftRide(scanResult), + 'Zwift Play' => ZwiftPlay(scanResult), + 'Zwift Click' => ZwiftClickV2(scanResult), + 'SQUARE' => EliteSquare(scanResult), + null => null, + _ when scanResult.name!.toUpperCase().startsWith('STERZO') => EliteSterzo(scanResult), + _ when scanResult.name!.toUpperCase().startsWith('KICKR BIKE SHIFT') => WahooKickrBikeShift(scanResult), + _ when scanResult.name!.toUpperCase().startsWith('KICKR BIKE PRO') => WahooKickrBikePro(scanResult), + _ when scanResult.name!.toUpperCase().startsWith('CYCPLUS') && scanResult.name!.toUpperCase().contains('BC2') => + CycplusBc2(scanResult), + _ when scanResult.name!.toUpperCase().startsWith('RDR') => ShimanoDi2(scanResult), + _ => null, + }; + } else { + device = switch (scanResult.name) { + null => null, + //'Zwift Ride' => ZwiftRide(scanResult), special case for Zwift Ride: we must only connect to the left controller + // https://www.makinolo.com/blog/2024/07/26/zwift-ride-protocol/ + 'Zwift Play' => ZwiftPlay(scanResult), + //'Zwift Click' => ZwiftClick(scanResult), special case for Zwift Click v2: we must only connect to the left controller + _ when scanResult.name!.toUpperCase().startsWith('SQUARE') => EliteSquare(scanResult), + _ when scanResult.name!.toUpperCase().startsWith('STERZO') => EliteSterzo(scanResult), + _ when scanResult.name!.toUpperCase().contains('KICKR BIKE SHIFT') => WahooKickrBikeShift(scanResult), + _ when scanResult.name!.toUpperCase().startsWith('KICKR BIKE PRO') => WahooKickrBikePro(scanResult), + _ when scanResult.name!.toUpperCase().startsWith('CYCPLUS') && scanResult.name!.toUpperCase().contains('BC2') => + CycplusBc2(scanResult), + _ when scanResult.services.contains(CycplusBc2Constants.SERVICE_UUID.toLowerCase()) => CycplusBc2(scanResult), + _ when scanResult.services.contains(ShimanoDi2Constants.SERVICE_UUID.toLowerCase()) => ShimanoDi2(scanResult), + // otherwise the service UUIDs will be used + _ => null, + }; + } + + if (device != null) { + return device; + } else if (scanResult.services.containsAny([ + ZwiftConstants.ZWIFT_CUSTOM_SERVICE_UUID.toLowerCase(), + ZwiftConstants.ZWIFT_RIDE_CUSTOM_SERVICE_UUID.toLowerCase(), + ])) { + // otherwise use the manufacturer data to identify the device + final manufacturerData = scanResult.manufacturerDataList; + final data = manufacturerData + .firstOrNullWhere((e) => e.companyId == ZwiftConstants.ZWIFT_MANUFACTURER_ID) + ?.payload; + + if (data == null || data.isEmpty) { + return null; + } + + final type = ZwiftDeviceType.fromManufacturerData(data.first); + return switch (type) { + ZwiftDeviceType.click => ZwiftClick(scanResult), + ZwiftDeviceType.playRight => ZwiftPlay(scanResult), + ZwiftDeviceType.playLeft => ZwiftPlay(scanResult), + ZwiftDeviceType.rideLeft => ZwiftRide(scanResult), + //DeviceType.rideRight => ZwiftRide(scanResult), // see comment above + ZwiftDeviceType.clickV2Left => ZwiftClickV2(scanResult), + //DeviceType.clickV2Right => ZwiftClickV2(scanResult), // see comment above + _ => null, + }; + } else { + return null; + } + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is BluetoothDevice && runtimeType == other.runtimeType && scanResult.deviceId == other.scanResult.deviceId; + + @override + int get hashCode => scanResult.deviceId.hashCode; + + @override + String toString() { + return name + (firmwareVersion != null ? ' v$firmwareVersion' : ''); + } + + BleDevice get device => scanResult; + + @override + Future connect() async { + actionStream.listen((message) { + print("Received message: $message"); + }); + + await UniversalBle.connect(device.deviceId); + + if (!kIsWeb) { + await UniversalBle.requestMtu(device.deviceId, 517); + } + + final services = await UniversalBle.discoverServices(device.deviceId); + final deviceInformationService = services.firstOrNullWhere( + (service) => service.uuid == BleUuid.DEVICE_INFORMATION_SERVICE_UUID.toLowerCase(), + ); + final firmwareCharacteristic = deviceInformationService?.characteristics.firstOrNullWhere( + (c) => c.uuid == BleUuid.DEVICE_INFORMATION_CHARACTERISTIC_FIRMWARE_REVISION.toLowerCase(), + ); + if (firmwareCharacteristic != null) { + final firmwareData = await UniversalBle.read( + device.deviceId, + deviceInformationService!.uuid, + firmwareCharacteristic.uuid, + ); + firmwareVersion = String.fromCharCodes(firmwareData); + connection.signalChange(this); + } + + final batteryService = services.firstOrNullWhere( + (service) => service.uuid == BleUuid.DEVICE_BATTERY_SERVICE_UUID.toLowerCase(), + ); + + final batteryCharacteristic = batteryService?.characteristics.firstOrNullWhere( + (c) => c.uuid == BleUuid.DEVICE_INFORMATION_CHARACTERISTIC_BATTERY_LEVEL.toLowerCase(), + ); + if (batteryCharacteristic != null) { + final batteryData = await UniversalBle.read( + device.deviceId, + batteryService!.uuid, + batteryCharacteristic.uuid, + ); + if (batteryData.isNotEmpty) { + batteryLevel = batteryData.first; + connection.signalChange(this); + } + } + + await handleServices(services); + } + + Future handleServices(List services); + Future processCharacteristic(String characteristic, Uint8List bytes); + + @override + Future disconnect() async { + await UniversalBle.disconnect(device.deviceId); + super.disconnect(); + } + + @override + Widget showInformation(BuildContext context) { + return Row( + children: [ + Text( + device.name?.screenshot ?? runtimeType.toString(), + style: TextStyle(fontWeight: FontWeight.bold), + ), + if (isBeta) BetaPill(), + if (batteryLevel != null) ...[ + Icon(switch (batteryLevel!) { + >= 80 => Icons.battery_full, + >= 60 => Icons.battery_6_bar, + >= 50 => Icons.battery_5_bar, + >= 25 => Icons.battery_4_bar, + >= 10 => Icons.battery_2_bar, + _ => Icons.battery_alert, + }), + Text('$batteryLevel%'), + ], + if (firmwareVersion != null) Text(' - v$firmwareVersion'), + if (firmwareVersion != null && + this is ZwiftDevice && + firmwareVersion != (this as ZwiftDevice).latestFirmwareVersion) ...[ + SizedBox(width: 8), + Icon(Icons.warning, color: Theme.of(context).colorScheme.error), + Text( + ' (latest: ${(this as ZwiftDevice).latestFirmwareVersion})', + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + ], + if (rssi != null) + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Tooltip( + message: 'Signal Strength: $rssi dBm', + child: Icon( + switch (rssi!) { + >= -50 => Icons.signal_cellular_4_bar, + >= -60 => Icons.signal_cellular_alt_2_bar, + >= -70 => Icons.signal_cellular_alt_1_bar, + _ => Icons.signal_cellular_alt, + }, + size: 18, + ), + ), + ), + Expanded(child: SizedBox()), + PopupMenuButton( + itemBuilder: (c) => [ + PopupMenuItem( + child: Text('Disconnect and Forget'), + onTap: () { + connection.disconnect(this, forget: true); + }, + ), + ], + ), + ], + ); + } +} diff --git a/lib/bluetooth/devices/cycplus/cycplus_bc2.dart b/lib/bluetooth/devices/cycplus/cycplus_bc2.dart new file mode 100644 index 000000000..ceb67a358 --- /dev/null +++ b/lib/bluetooth/devices/cycplus/cycplus_bc2.dart @@ -0,0 +1,128 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:swift_control/bluetooth/messages/notification.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:universal_ble/universal_ble.dart'; + +import '../bluetooth_device.dart'; + +class CycplusBc2 extends BluetoothDevice { + CycplusBc2(super.scanResult) + : super( + availableButtons: CycplusBc2Buttons.values, + ); + + @override + Future handleServices(List services) async { + final service = services.firstWhere( + (e) => e.uuid.toLowerCase() == CycplusBc2Constants.SERVICE_UUID.toLowerCase(), + orElse: () => throw Exception('Service not found: ${CycplusBc2Constants.SERVICE_UUID}'), + ); + final characteristic = service.characteristics.firstWhere( + (e) => e.uuid.toLowerCase() == CycplusBc2Constants.TX_CHARACTERISTIC_UUID.toLowerCase(), + orElse: () => throw Exception('Characteristic not found: ${CycplusBc2Constants.TX_CHARACTERISTIC_UUID}'), + ); + + await UniversalBle.subscribeNotifications(device.deviceId, service.uuid, characteristic.uuid); + } + + // Track last state for index 6 and 7 + int _lastStateIndex6 = 0x00; + int _lastStateIndex7 = 0x00; + + @override + Future processCharacteristic(String characteristic, Uint8List bytes) { + if (characteristic.toLowerCase() == CycplusBc2Constants.TX_CHARACTERISTIC_UUID.toLowerCase()) { + if (bytes.length > 7) { + final buttonsToPress = []; + + // Process index 6 (shift up) + final currentByte6 = bytes[6]; + if (_shouldTriggerShift(currentByte6, _lastStateIndex6)) { + buttonsToPress.add(CycplusBc2Buttons.shiftUp); + _lastStateIndex6 = 0x00; // Reset after successful press + } else { + _updateState(currentByte6, (val) => _lastStateIndex6 = val); + } + + // Process index 7 (shift down) + final currentByte7 = bytes[7]; + if (_shouldTriggerShift(currentByte7, _lastStateIndex7)) { + buttonsToPress.add(CycplusBc2Buttons.shiftDown); + _lastStateIndex7 = 0x00; // Reset after successful press + } else { + _updateState(currentByte7, (val) => _lastStateIndex7 = val); + } + + handleButtonsClicked(buttonsToPress); + } else { + actionStreamInternal.add( + LogNotification( + 'CYCPLUS BC2 received unexpected packet: ${bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join()}', + ), + ); + handleButtonsClicked([]); + } + } + return Future.value(); + } + + // Check if we should trigger a shift based on current and last state + bool _shouldTriggerShift(int currentByte, int lastByte) { + const pressedValues = {0x01, 0x02, 0x03}; + + // State change from one pressed value to another different pressed value + // This is the ONLY time we trigger a shift + if (pressedValues.contains(currentByte) && pressedValues.contains(lastByte) && currentByte != lastByte) { + return true; + } + + return false; + } + + // Update state tracking + void _updateState(int currentByte, void Function(int) setState) { + const pressedValues = {0x01, 0x02, 0x03}; + const releaseValue = 0x00; + + // Button released: current is 0x00 and last was pressed + if (currentByte == releaseValue) { + setState(releaseValue); + } + // Lock the new pressed state + else if (pressedValues.contains(currentByte)) { + setState(currentByte); + } + } +} + +class CycplusBc2Constants { + // Nordic UART Service (NUS) - commonly used by CYCPLUS BC2 + static const String SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"; + + // TX Characteristic - device sends data to app + static const String TX_CHARACTERISTIC_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; + + // RX Characteristic - app sends data to device (not used for button reading) + static const String RX_CHARACTERISTIC_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; +} + +class CycplusBc2Buttons { + static const ControllerButton shiftUp = ControllerButton( + 'shiftUp', + action: InGameAction.shiftUp, + icon: Icons.add, + ); + + static const ControllerButton shiftDown = ControllerButton( + 'shiftDown', + action: InGameAction.shiftDown, + icon: Icons.remove, + ); + + static const List values = [ + shiftUp, + shiftDown, + ]; +} diff --git a/lib/bluetooth/devices/elite/elite_square.dart b/lib/bluetooth/devices/elite/elite_square.dart new file mode 100644 index 000000000..0e6d9bfd6 --- /dev/null +++ b/lib/bluetooth/devices/elite/elite_square.dart @@ -0,0 +1,156 @@ +import 'dart:typed_data'; + +import 'package:dartx/dartx.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:universal_ble/universal_ble.dart'; + +import '../../messages/notification.dart'; +import '../bluetooth_device.dart'; + +class EliteSquare extends BluetoothDevice { + EliteSquare(super.scanResult) + : super( + availableButtons: EliteSquareButtons.values.toList(), + isBeta: true, + ); + + String? _lastValue; + + @override + Future handleServices(List services) async { + final service = services.firstOrNullWhere((e) => e.uuid == SquareConstants.SERVICE_UUID); + if (service == null) { + throw Exception('Service not found: ${SquareConstants.SERVICE_UUID}'); + } + final characteristic = service.characteristics.firstOrNullWhere( + (e) => e.uuid == SquareConstants.CHARACTERISTIC_UUID, + ); + if (characteristic == null) { + throw Exception('Characteristic not found: ${SquareConstants.CHARACTERISTIC_UUID}'); + } + + await UniversalBle.subscribeNotifications(device.deviceId, service.uuid, characteristic.uuid); + } + + @override + Future processCharacteristic(String characteristic, Uint8List bytes) async { + if (characteristic == SquareConstants.CHARACTERISTIC_UUID) { + final fullValue = _bytesToHex(bytes); + final currentValue = _extractButtonCode(fullValue); + actionStreamInternal.add(LogNotification('Received $fullValue - vs $currentValue (last: $_lastValue)')); + + if (_lastValue != null) { + final currentRelevantPart = fullValue.length >= 14 + ? fullValue.substring(6, 14) + : fullValue.substring(6); + final lastRelevantPart = _lastValue!.length >= 14 + ? _lastValue!.substring(6, 14) + : _lastValue!.substring(6); + + if (currentRelevantPart != lastRelevantPart) { + final buttonClicked = SquareConstants.BUTTON_MAPPING[currentValue]; + actionStreamInternal.add(LogNotification('Button pressed: $buttonClicked')); + handleButtonsClicked([ + if (buttonClicked != null) buttonClicked, + ]); + } + } + + _lastValue = fullValue; + } + } + + String _extractButtonCode(String hexValue) { + if (hexValue.length >= 14) { + final buttonCode = hexValue.substring(6, 14); + if (SquareConstants.BUTTON_MAPPING.containsKey(buttonCode)) { + return buttonCode; + } + } + return hexValue; + } + + String _bytesToHex(List bytes) { + return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); + } +} + +class SquareConstants { + static const String DEVICE_NAME = "SQUARE"; + static const String CHARACTERISTIC_UUID = "347b0043-7635-408b-8918-8ff3949ce592"; + static const String SERVICE_UUID = "347b0001-7635-408b-8918-8ff3949ce592"; + static const int RECONNECT_DELAY = 5; // seconds between reconnection attempts + + // Button mapping https://images.bike24.com/i/mb/c7/36/d9/elite-square-smart-frame-indoor-bike-3-1724305.jpg + static const Map BUTTON_MAPPING = { + "00000200": EliteSquareButtons.up, //"Up", + "00000100": EliteSquareButtons.left, //"Left", + "00000800": EliteSquareButtons.down, // "Down", + "00000400": EliteSquareButtons.right, //"Right", + "00002000": EliteSquareButtons.x, //"X", + "00001000": EliteSquareButtons.square, // "Square", + "00008000": EliteSquareButtons.campagnoloLeft, // "Left Campagnolo", + "00004000": EliteSquareButtons.leftBrake, //"Left brake", + "00000002": EliteSquareButtons.leftShift1, //"Left shift 1", + "00000001": EliteSquareButtons.leftShift2, // "Left shift 2", + "02000000": EliteSquareButtons.y, // "Y", + "01000000": EliteSquareButtons.a, //"A", + "08000000": EliteSquareButtons.b, // "B", + "04000000": EliteSquareButtons.z, // "Z", + "20000000": EliteSquareButtons.circle, // "Circle", + "10000000": EliteSquareButtons.triangle, //"Triangle", + "80000000": EliteSquareButtons.campagnoloRight, // "Right Campagnolo", + "40000000": EliteSquareButtons.rightBrake, //"Right brake", + "00020000": EliteSquareButtons.rightShift1, //"Right shift 1", + "00010000": EliteSquareButtons.rightShift2, //"Right shift 2", + }; +} + +class EliteSquareButtons { + static const ControllerButton up = ControllerButton('eliteSquareUp', action: null); + static const ControllerButton left = ControllerButton('eliteSquareLeft', action: InGameAction.navigateLeft); + static const ControllerButton down = ControllerButton('eliteSquareDown', action: null); + static const ControllerButton right = ControllerButton('eliteSquareRight', action: InGameAction.navigateRight); + static const ControllerButton x = ControllerButton('eliteSquareX', action: null); + static const ControllerButton square = ControllerButton('eliteSquareSquare', action: null); + static const ControllerButton campagnoloLeft = ControllerButton('eliteSquareCampagnoloLeft', action: null); + static const ControllerButton leftBrake = ControllerButton('eliteSquareLeftBrake', action: null); + static const ControllerButton leftShift1 = ControllerButton('eliteSquareLeftShift1', action: InGameAction.shiftUp); + static const ControllerButton leftShift2 = ControllerButton('eliteSquareLeftShift2', action: InGameAction.shiftDown); + static const ControllerButton y = ControllerButton('y', action: null); + static const ControllerButton a = ControllerButton('a', action: null); + static const ControllerButton b = ControllerButton('b', action: null); + static const ControllerButton z = ControllerButton('z', action: null); + static const ControllerButton circle = ControllerButton('eliteSquareCircle', action: null); + static const ControllerButton triangle = ControllerButton('eliteSquareTriangle', action: null); + static const ControllerButton campagnoloRight = ControllerButton('eliteSquareCampagnoloRight', action: null); + static const ControllerButton rightBrake = ControllerButton('eliteSquareRightBrake', action: null); + static const ControllerButton rightShift1 = ControllerButton('eliteSquareRightShift1', action: InGameAction.shiftUp); + static const ControllerButton rightShift2 = ControllerButton( + 'eliteSquareRightShift2', + action: InGameAction.shiftDown, + ); + + static const List values = [ + up, + left, + down, + right, + x, + square, + campagnoloLeft, + leftBrake, + leftShift1, + leftShift2, + y, + a, + b, + z, + circle, + triangle, + campagnoloRight, + rightBrake, + rightShift1, + rightShift2, + ]; +} diff --git a/lib/bluetooth/devices/elite/elite_sterzo.dart b/lib/bluetooth/devices/elite/elite_sterzo.dart new file mode 100644 index 000000000..8d738aa39 --- /dev/null +++ b/lib/bluetooth/devices/elite/elite_sterzo.dart @@ -0,0 +1,390 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:swift_control/bluetooth/devices/bluetooth_device.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:universal_ble/universal_ble.dart'; + +import '../../messages/notification.dart'; + +class EliteSterzo extends BluetoothDevice { + EliteSterzo(super.scanResult) : super(availableButtons: SterzoButtons.values); + + double _lastAngle = 0.0; + int? _latestChallenge; + String? _serviceUuid; + static Uint8List? _challengeCodesData; + static bool _isLoadingChallenges = false; + + // Calibration state + final List _calibrationSamples = []; + double _calibrationOffset = 0.0; + bool _isCalibrated = false; + + // Last rounded angle for logging optimization + int? _lastRoundedAngle; + + // Debounce timer for PWM-like keypress behavior + Timer? _keypressTimer; + bool _isProcessingKeypresses = false; + + @override + Future handleServices(List services) async { + final service = services.firstOrNullWhere( + (e) => e.uuid.toLowerCase().startsWith('347b0'), + ); + if (service == null) { + throw Exception('Elite Sterzo service not found'); + } + + _serviceUuid = service.uuid; + + // Find characteristics + final challengeChar = service.characteristics.firstOrNullWhere( + (e) => e.uuid == SterzoConstants.CHALLENGE_CODE_CHARACTERISTIC_UUID, + ); + final measurementChar = service.characteristics.firstOrNullWhere( + (e) => e.uuid == SterzoConstants.MEASUREMENT_CHARACTERISTIC_UUID, + ); + final controlChar = service.characteristics.firstOrNullWhere( + (e) => e.uuid == SterzoConstants.CONTROL_POINT_CHARACTERISTIC_UUID, + ); + + if (challengeChar == null || measurementChar == null || controlChar == null) { + throw Exception('Required Sterzo characteristics not found'); + } + + // Subscribe to challenge code indications + await UniversalBle.subscribeIndications( + device.deviceId, + service.uuid, + challengeChar.uuid, + ); + + // Subscribe to measurement notifications + await UniversalBle.subscribeNotifications( + device.deviceId, + service.uuid, + measurementChar.uuid, + ); + + // Request to start challenge + await UniversalBle.write( + device.deviceId, + service.uuid, + controlChar.uuid, + Uint8List.fromList([0x03, 0x10]), + withoutResponse: false, + ); + + actionStreamInternal.add(LogNotification('Elite Sterzo: Initialization started')); + } + + @override + Future processCharacteristic(String characteristic, Uint8List bytes) async { + if (characteristic == SterzoConstants.CHALLENGE_CODE_CHARACTERISTIC_UUID) { + _handleChallengeCode(bytes); + } else if (characteristic == SterzoConstants.MEASUREMENT_CHARACTERISTIC_UUID) { + _handleSteeringMeasurement(bytes); + } + } + + Future _handleChallengeCode(Uint8List bytes) async { + if (bytes.length >= 4) { + // Challenge is in bytes 2-3 (big-endian) + final challenge = (bytes[2] << 8) | bytes[3]; + _latestChallenge = challenge; + + actionStreamInternal.add(LogNotification('Elite Sterzo: Received challenge code: $challenge')); + + // Respond to challenge + await _activateSteeringMeasurements(); + } + } + + Future _activateSteeringMeasurements() async { + if (_latestChallenge == null || _serviceUuid == null) { + return; + } + + // Ensure challenge codes are loaded + await _ensureChallengeCodesLoaded(); + + // Get response codes for the challenge + final challengeCodes = _getChallengeResponse(_latestChallenge!); + + // Send challenge response + await UniversalBle.write( + device.deviceId, + _serviceUuid!, + SterzoConstants.CONTROL_POINT_CHARACTERISTIC_UUID, + Uint8List.fromList([0x03, 0x11, challengeCodes[0], challengeCodes[1]]), + withoutResponse: false, + ); + + await Future.delayed(const Duration(seconds: 1)); + + // Activate measurements + await UniversalBle.write( + device.deviceId, + _serviceUuid!, + SterzoConstants.CONTROL_POINT_CHARACTERISTIC_UUID, + Uint8List.fromList([0x02, 0x02]), + withoutResponse: false, + ); + + actionStreamInternal.add(LogNotification('Elite Sterzo: Steering measurements activated')); + } + + static Future _ensureChallengeCodesLoaded() async { + if (_challengeCodesData != null) { + return; // Already loaded + } + + // Wait if already loading + while (_isLoadingChallenges) { + await Future.delayed(const Duration(milliseconds: 100)); + } + + // Check again after waiting + if (_challengeCodesData != null) { + return; + } + + _isLoadingChallenges = true; + + try { + if (kIsWeb) { + // On web, always fetch from HTTP + _challengeCodesData = await _fetchChallengeCodes(); + } else { + // On native platforms, try to load from cache first + _challengeCodesData = await _loadCachedChallengeCodes(); + + if (_challengeCodesData == null) { + // Cache miss - fetch from HTTP and cache it + _challengeCodesData = await _fetchChallengeCodes(); + if (_challengeCodesData != null) { + await _cacheChallengeCodes(_challengeCodesData!); + } + } + } + } finally { + _isLoadingChallenges = false; + } + } + + static Future _fetchChallengeCodes() async { + final url = kIsWeb + ? 'https://corsproxy.io/${SterzoConstants.CHALLENGE_CODES_URL}' + : SterzoConstants.CHALLENGE_CODES_URL; + try { + final response = await http.get(Uri.parse(url)); + + if (response.statusCode == 200) { + return response.bodyBytes; + } + } catch (e) { + if (kDebugMode) { + print('Failed to fetch challenge codes for URL $url: $e'); + } + } + return null; + } + + static Future _loadCachedChallengeCodes() async { + try { + final prefs = await SharedPreferences.getInstance(); + final cached = prefs.getString(SterzoConstants.CACHE_KEY); + + if (cached != null) { + // Decode from base64 + return base64Decode(cached); + } + } catch (e) { + if (kDebugMode) { + print('Failed to load cached challenge codes: $e'); + } + } + return null; + } + + static Future _cacheChallengeCodes(Uint8List data) async { + try { + final prefs = await SharedPreferences.getInstance(); + // Encode to base64 for storage + await prefs.setString(SterzoConstants.CACHE_KEY, base64Encode(data)); + } catch (e) { + if (kDebugMode) { + print('Failed to cache challenge codes: $e'); + } + } + } + + void _handleSteeringMeasurement(Uint8List bytes) { + if (bytes.length >= 4) { + // Steering angle is a 32-bit float (little-endian) + final rawAngle = ByteData.sublistView(bytes).getFloat32(0, Endian.little); + + // Ignore NaN readings during initial connection + if (rawAngle.isNaN) { + return; + } + + // Handle calibration: collect initial samples to compute offset + if (!_isCalibrated) { + _calibrationSamples.add(rawAngle); + if (_calibrationSamples.length >= SterzoConstants.CALIBRATION_SAMPLE_COUNT) { + // Compute average offset from collected samples + _calibrationOffset = _calibrationSamples.reduce((a, b) => a + b) / _calibrationSamples.length; + _isCalibrated = true; + actionStreamInternal.add( + LogNotification('Elite Sterzo: Calibration complete, offset: ${_calibrationOffset.toStringAsFixed(2)}°'), + ); + } + return; // Don't process steering during calibration + } + + // Apply calibration offset + final calibratedAngle = rawAngle - _calibrationOffset; + + // Round to whole degrees to reduce noise + final roundedAngle = calibratedAngle.round(); + + // Only log and process steering when rounded value changes to reduce verbosity + if (_lastRoundedAngle != roundedAngle) { + actionStreamInternal.add(LogNotification('Steering angle: $roundedAngle°')); + _lastRoundedAngle = roundedAngle; + + // Apply PWM-like steering behavior only when angle changes + _applyPWMSteering(roundedAngle); + } + + _lastAngle = calibratedAngle; + } + } + + /// Applies PWM-like steering behavior with repeated keypresses proportional to angle magnitude + void _applyPWMSteering(int roundedAngle) { + // Cancel any pending keypress timer + _keypressTimer?.cancel(); + + // Determine if we're steering + if (roundedAngle.abs() > SterzoConstants.STEERING_THRESHOLD) { + // Determine direction + final button = roundedAngle > 0 ? SterzoButtons.rightSteer : SterzoButtons.leftSteer; + + // Calculate number of keypress levels based on angle magnitude + final levels = _calculateKeypressLevels(roundedAngle.abs()); + + // Only trigger new keypresses when rounded angle changes to avoid overlap + // The check for _lastRoundedAngle change is already done in _handleSteeringMeasurement + // so we know this is a new angle value + _scheduleRepeatedKeypresses(button, levels); + } else { + // Center position - release any held buttons + handleButtonsClicked([]); + } + } + + /// Calculates the number of keypress levels based on angle magnitude + int _calculateKeypressLevels(int absAngle) { + final levels = (absAngle / SterzoConstants.LEVEL_DEGREE_STEP).floor(); + return levels.clamp(1, SterzoConstants.MAX_LEVELS); + } + + /// Schedules repeated keypresses to simulate PWM behavior + Future _scheduleRepeatedKeypresses(ControllerButton button, int levels) async { + // Don't overlap keypress sequences + if (_isProcessingKeypresses) { + return; + } + + _isProcessingKeypresses = true; + + // Send keypresses in sequence with delays between them + for (int i = 0; i < levels; i++) { + await Future.delayed(Duration(milliseconds: SterzoConstants.KEY_REPEAT_INTERVAL_MS)); + handleButtonsClicked([button]); + } + + _isProcessingKeypresses = false; + } + + List _getChallengeResponse(int challenge) { + if (_challengeCodesData == null) { + // Fallback if data not loaded + return [0x96, 0x96]; + } + + final index = challenge * 2; + if (index >= 0 && index < _challengeCodesData!.length - 1) { + return [_challengeCodesData![index], _challengeCodesData![index + 1]]; + } + + // Fallback for out of range challenges + return [0x96, 0x96]; + } + + @override + Future disconnect() async { + _keypressTimer?.cancel(); + await super.disconnect(); + } +} + +class SterzoConstants { + static const String DEVICE_NAME = "STERZO"; + + // Elite Sterzo Smart characteristic UUIDs + static const String MEASUREMENT_CHARACTERISTIC_UUID = "347b0030-7635-408b-8918-8ff3949ce592"; + static const String CONTROL_POINT_CHARACTERISTIC_UUID = "347b0031-7635-408b-8918-8ff3949ce592"; + static const String CHALLENGE_CODE_CHARACTERISTIC_UUID = "347b0032-7635-408b-8918-8ff3949ce592"; + + // Service UUID pattern (matches Elite devices) + static const String SERVICE_UUID = "347b0001-7635-408b-8918-8ff3949ce592"; + + // Steering angle threshold in degrees to trigger steering action + static const double STEERING_THRESHOLD = 10.0; + + static const int RECONNECT_DELAY = 5; // seconds between reconnection attempts + + // URL to fetch challenge codes + static const String CHALLENGE_CODES_URL = + 'https://github.com/zacharyedwardbull/pycycling/raw/refs/heads/master/pycycling/data/sterzo-challenge-codes.dat'; + + // Cache key for SharedPreferences + static const String CACHE_KEY = 'elite_sterzo_challenge_codes'; + + // Calibration settings + // Number of initial valid samples to collect for calibration offset + static const int CALIBRATION_SAMPLE_COUNT = 10; + + // PWM-like steering behavior constants + // Degrees per level for repeated keypress behavior + static const double LEVEL_DEGREE_STEP = 10.0; + // Maximum number of keypress levels + static const int MAX_LEVELS = 5; + // Interval between repeated keypresses in milliseconds + static const int KEY_REPEAT_INTERVAL_MS = 40; +} + +class SterzoButtons { + static final ControllerButton leftSteer = ControllerButton( + 'leftSteer', + action: InGameAction.steerLeft, + ); + static final ControllerButton rightSteer = ControllerButton( + 'rightSteer', + action: InGameAction.steerRight, + ); + + static List get values => [ + leftSteer, + rightSteer, + ]; +} diff --git a/lib/bluetooth/devices/gamepad/gamepad_device.dart b/lib/bluetooth/devices/gamepad/gamepad_device.dart new file mode 100644 index 000000000..a9d4f9a15 --- /dev/null +++ b/lib/bluetooth/devices/gamepad/gamepad_device.dart @@ -0,0 +1,66 @@ +import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; +import 'package:gamepads/gamepads.dart'; +import 'package:swift_control/bluetooth/devices/base_device.dart'; +import 'package:swift_control/bluetooth/messages/notification.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/pages/device.dart'; +import 'package:swift_control/utils/keymap/apps/custom_app.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/widgets/beta_pill.dart'; + +import '../../../widgets/warning.dart'; + +class GamepadDevice extends BaseDevice { + final String id; + + GamepadDevice(super.name, {required this.id}) : super(availableButtons: [], isBeta: true); + + List _lastButtonsClicked = []; + + @override + Future connect() async { + Gamepads.eventsByGamepad(id).listen((event) { + actionStreamInternal.add(LogNotification('Gamepad event: $event')); + + ControllerButton? button = actionHandler.supportedApp?.keymap.getOrAddButton( + event.key, + () => ControllerButton(event.key), + ); + + final buttonsClicked = event.value == 0.0 && button != null ? [button] : []; + if (_lastButtonsClicked.contentEquals(buttonsClicked) == false) { + handleButtonsClicked(buttonsClicked); + } + _lastButtonsClicked = buttonsClicked; + }); + } + + @override + Widget showInformation(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Column( + spacing: 8, + children: [ + Row( + children: [ + Text( + name.screenshot, + style: TextStyle(fontWeight: FontWeight.bold), + ), + if (isBeta) BetaPill(), + ], + ), + + if (actionHandler.supportedApp is! CustomApp) + Warning( + children: [ + Text('Use a custom keymap to use the buttons on $name.'), + ], + ), + ], + ), + ); + } +} diff --git a/lib/bluetooth/devices/hid/hid_device.dart b/lib/bluetooth/devices/hid/hid_device.dart new file mode 100644 index 000000000..8174b1bb2 --- /dev/null +++ b/lib/bluetooth/devices/hid/hid_device.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:swift_control/bluetooth/devices/base_device.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/actions/android.dart'; + +class HidDevice extends BaseDevice { + HidDevice(super.name, {super.availableButtons = const []}); + + @override + Future connect() { + return Future.value(null); + } + + @override + Widget showInformation(BuildContext context) { + return Row( + children: [ + Expanded(child: Text(name)), + PopupMenuButton( + itemBuilder: (c) => [ + PopupMenuItem( + child: Text('Ignore'), + onTap: () { + connection.disconnect(this, forget: true); + if (actionHandler is AndroidActions) { + (actionHandler as AndroidActions).ignoreHidDevices(); + } else if (connection.isMediaKeyDetectionEnabled.value) { + connection.isMediaKeyDetectionEnabled.value = false; + } + }, + ), + ], + ), + ], + ); + } +} diff --git a/lib/bluetooth/devices/link/link.dart b/lib/bluetooth/devices/link/link.dart new file mode 100644 index 000000000..bbfaff5b0 --- /dev/null +++ b/lib/bluetooth/devices/link/link.dart @@ -0,0 +1,163 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:swift_control/utils/actions/base_actions.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/utils/requirements/multi.dart'; + +class WhooshLink { + Socket? _socket; + ServerSocket? _server; + + static final List supportedActions = [ + InGameAction.shiftUp, + InGameAction.shiftDown, + InGameAction.cameraAngle, + InGameAction.emote, + InGameAction.uturn, + InGameAction.steerLeft, + InGameAction.steerRight, + ]; + + final ValueNotifier isStarted = ValueNotifier(false); + final ValueNotifier isConnected = ValueNotifier(false); + + void stopServer() async { + if (isStarted.value) { + await _socket?.close(); + await _server?.close(); + isConnected.value = false; + isStarted.value = false; + if (kDebugMode) { + print('Server stopped.'); + } + } + } + + Future startServer({ + required void Function(Socket socket) onConnected, + required void Function(Socket socket) onDisconnected, + }) async { + try { + // Create and bind server socket + _server = await ServerSocket.bind( + InternetAddress.anyIPv6, + 21587, + shared: true, + v6Only: false, + ); + } catch (e) { + if (kDebugMode) { + print('Failed to start server: $e'); + } + isConnected.value = false; + isStarted.value = false; + rethrow; + } + isStarted.value = true; + if (kDebugMode) { + print('Server started on port ${_server!.port}'); + } + + // Accept connection + _server!.listen( + (Socket socket) { + _socket = socket; + onConnected(socket); + isConnected.value = true; + if (kDebugMode) { + print('Client connected: ${socket.remoteAddress.address}:${socket.remotePort}'); + } + + // Listen for data from the client + socket.listen( + (List data) { + try { + if (kDebugMode) { + // TODO we could check if virtual shifting is enabled + final message = utf8.decode(data); + print('Received message: $message'); + } + } catch (_) {} + }, + onDone: () { + print('Client disconnected: $socket'); + onDisconnected(socket); + isConnected.value = false; + }, + ); + }, + ); + } + + ActionResult sendAction(InGameAction action, int? value) { + final jsonObject = switch (action) { + InGameAction.shiftUp => { + 'MessageType': 'Controls', + 'InGameControls': { + 'GearShifting': '1', + }, + }, + InGameAction.shiftDown => { + 'MessageType': 'Controls', + 'InGameControls': { + 'GearShifting': '-1', + }, + }, + InGameAction.cameraAngle => { + 'MessageType': 'Controls', + 'InGameControls': { + 'CameraAngle': '$value', + }, + }, + InGameAction.emote => { + 'MessageType': 'Controls', + 'InGameControls': { + 'Emote': '$value', + }, + }, + InGameAction.uturn => { + 'MessageType': 'Controls', + 'InGameControls': { + 'UTurn': 'true', + }, + }, + InGameAction.steerLeft => { + 'MessageType': 'Controls', + 'InGameControls': { + 'Steering': '-1', + }, + }, + InGameAction.steerRight => { + 'MessageType': 'Controls', + 'InGameControls': { + 'Steering': '1', + }, + }, + InGameAction.increaseResistance => null, + InGameAction.decreaseResistance => null, + InGameAction.navigateLeft => null, + InGameAction.navigateRight => null, + InGameAction.toggleUi => null, + _ => null, + }; + + if (jsonObject != null) { + final jsonString = jsonEncode(jsonObject); + _socket?.writeln(jsonString); + return Success('Sent action to MyWhoosh: $action ${value ?? ''}'); + } else { + return Error('No action available for button: $action'); + } + } + + bool isCompatible(Target target) { + return kIsWeb + ? false + : switch (target) { + Target.thisDevice => Platform.isAndroid || Platform.isWindows, + _ => true, + }; + } +} diff --git a/lib/bluetooth/devices/shimano/shimano_di2.dart b/lib/bluetooth/devices/shimano/shimano_di2.dart new file mode 100644 index 000000000..b12ab85fb --- /dev/null +++ b/lib/bluetooth/devices/shimano/shimano_di2.dart @@ -0,0 +1,101 @@ +import 'dart:typed_data'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/keymap/apps/custom_app.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:universal_ble/universal_ble.dart'; + +import '../bluetooth_device.dart'; + +class ShimanoDi2 extends BluetoothDevice { + ShimanoDi2(super.scanResult) : super(availableButtons: []); + + @override + Future handleServices(List services) async { + final service = services.firstWhere( + (e) => e.uuid.toLowerCase() == ShimanoDi2Constants.SERVICE_UUID.toLowerCase(), + orElse: () => throw Exception('Service not found: ${ShimanoDi2Constants.SERVICE_UUID}'), + ); + final characteristic = service.characteristics.firstWhere( + (e) => e.uuid.toLowerCase() == ShimanoDi2Constants.D_FLY_CHANNEL_UUID.toLowerCase(), + orElse: () => throw Exception('Characteristic not found: ${ShimanoDi2Constants.D_FLY_CHANNEL_UUID}'), + ); + + await UniversalBle.subscribeIndications(device.deviceId, service.uuid, characteristic.uuid); + } + + final _lastButtons = {}; + bool _isInitialized = false; + + @override + Future processCharacteristic(String characteristic, Uint8List bytes) { + if (characteristic.toLowerCase() == ShimanoDi2Constants.D_FLY_CHANNEL_UUID) { + final channels = bytes.sublist(1); + + // On first data reception, just initialize the state without triggering buttons + if (!_isInitialized) { + channels.forEachIndexed((int value, int index) { + final readableIndex = index + 1; + _lastButtons[index] = value; + + actionHandler.supportedApp?.keymap.getOrAddButton( + 'D-Fly Channel $readableIndex', + () => ControllerButton('D-Fly Channel $readableIndex'), + ); + }); + _isInitialized = true; + return Future.value(); + } + + final clickedButtons = []; + + channels.forEachIndexed((int value, int index) { + final didChange = _lastButtons[index] != value; + _lastButtons[index] = value; + + final readableIndex = index + 1; + + final button = actionHandler.supportedApp?.keymap.getOrAddButton( + 'D-Fly Channel $readableIndex', + () => ControllerButton('D-Fly Channel $readableIndex'), + ); + if (didChange && button != null) { + clickedButtons.add(button); + } + }); + + if (clickedButtons.isNotEmpty) { + handleButtonsClicked(clickedButtons); + handleButtonsClicked([]); + } + } + return Future.value(); + } + + @override + Widget showInformation(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + super.showInformation(context), + Text( + 'Make sure to set your Di2 buttons to D-Fly channels in the Shimano E-TUBE app.', + style: TextStyle(fontSize: 12, color: Colors.grey), + ), + if (actionHandler.supportedApp is! CustomApp) + Text( + 'Use a custom keymap to support ${scanResult.name}', + style: TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ); + } +} + +class ShimanoDi2Constants { + static const String SERVICE_UUID = "000018ef-5348-494d-414e-4f5f424c4500"; + + static const String D_FLY_CHANNEL_UUID = "00002ac2-5348-494d-414e-4f5f424c4500"; +} diff --git a/lib/bluetooth/devices/wahoo/wahoo_kickr_bike_pro.dart b/lib/bluetooth/devices/wahoo/wahoo_kickr_bike_pro.dart new file mode 100644 index 000000000..21f5f5a55 --- /dev/null +++ b/lib/bluetooth/devices/wahoo/wahoo_kickr_bike_pro.dart @@ -0,0 +1,10 @@ +import 'package:swift_control/bluetooth/devices/zwift/zwift_ride.dart'; + +import '../zwift/constants.dart'; + +class WahooKickrBikePro extends ZwiftRide { + WahooKickrBikePro(super.scanResult) : super(); + + @override + String get customServiceId => ZwiftConstants.ZWIFT_CUSTOM_SERVICE_UUID; +} diff --git a/lib/bluetooth/devices/wahoo/wahoo_kickr_bike_shift.dart b/lib/bluetooth/devices/wahoo/wahoo_kickr_bike_shift.dart new file mode 100644 index 000000000..2c9121bf6 --- /dev/null +++ b/lib/bluetooth/devices/wahoo/wahoo_kickr_bike_shift.dart @@ -0,0 +1,187 @@ +import 'dart:collection'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:universal_ble/universal_ble.dart'; + +import '../bluetooth_device.dart'; + +class WahooKickrBikeShift extends BluetoothDevice { + WahooKickrBikeShift(super.scanResult) + : super( + availableButtons: WahooKickrBikeShiftConstants.prefixToButton.values.toList(), + ); + + @override + Future handleServices(List services) async { + final service = services.firstWhere( + (e) => e.uuid == WahooKickrBikeShiftConstants.SERVICE_UUID, + orElse: () => throw Exception('Service not found: ${WahooKickrBikeShiftConstants.SERVICE_UUID}'), + ); + final characteristic = service.characteristics.firstWhere( + (e) => e.uuid == WahooKickrBikeShiftConstants.CHARACTERISTIC_UUID, + orElse: () => throw Exception('Characteristic not found: ${WahooKickrBikeShiftConstants.CHARACTERISTIC_UUID}'), + ); + + await UniversalBle.subscribeNotifications(device.deviceId, service.uuid, characteristic.uuid); + } + + @override + Future processCharacteristic(String characteristic, Uint8List bytes) { + if (characteristic == WahooKickrBikeShiftConstants.CHARACTERISTIC_UUID) { + final hex = toHex(bytes); + + // Short-frame detection (hard-coded families) + final s = parseShortFrame(hex); + if (s != null) { + if (s.pressed) { + handleButtonsClicked([s.button]); + } else { + handleButtonsClicked([]); + } + return Future.value(); + } + } + return Future.value(); + } + + // Deduplicate per (prefix, type) using the 7-bit rolling sequence + final Map lastSeqByPrefix = HashMap(); + + String toHex(Uint8List bytes) => bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join().toUpperCase(); + + // Parse short frames like "PPQQRR" (e.g., "0001E6", "80005E", "40008F", "010004") + ShortFrame? parseShortFrame(String hex) { + final re = RegExp(r'^[0-9A-F]{6}$', caseSensitive: false); + if (!re.hasMatch(hex)) return null; + + final prefix = hex.substring(0, 4); // PPQQ + final rrHex = hex.substring(4, 6); // RR + if (!WahooKickrBikeShiftConstants.prefixToButton.containsKey(prefix)) return null; + + final idx = int.parse(rrHex, radix: 16); + final type = (idx & 0x80) != 0 ? true : false; // MSB of RR + final seq = idx & 0x7F; // rolling counter for dedupe + + return ShortFrame( + prefix: prefix, + rrHex: rrHex, + idx: idx, + pressed: type, + seq: seq, + button: WahooKickrBikeShiftConstants.prefixToButton[prefix]!, + ); + } + + bool isLongFrame(String hex) { + final re = RegExp(r'^FF0F01', caseSensitive: false); + return re.hasMatch(hex); + } + + // Returns true if this (prefix,type,seq) has not been handled yet + bool shouldHandleOnce(String prefix, String type, int seq) { + final key = '$prefix:$type'; + final last = lastSeqByPrefix[key]; + if (last == seq) return false; + lastSeqByPrefix[key] = seq; + return true; + } +} + +class ShortFrame { + final String prefix; // PPQQ + final String rrHex; // RR + final int idx; + final bool pressed; + final int seq; + final ControllerButton button; + + ShortFrame({ + required this.prefix, + required this.rrHex, + required this.idx, + required this.pressed, + required this.seq, + required this.button, + }); +} + +class WahooKickrBikeShiftConstants { + static const String SERVICE_UUID = "a026ee0d-0a7d-4ab3-97fa-f1500f9feb8b"; + static const String CHARACTERISTIC_UUID = "a026e03c-0a7d-4ab3-97fa-f1500f9feb8b"; + + // https://support.wahoofitness.com/hc/en-us/articles/22259367275410-Shifter-and-button-configuration-for-KICKR-BIKE-1-2 + static const Map prefixToButton = { + '0001': WahooKickrShiftButtons.rightUp, //'Right Up', + '8000': WahooKickrShiftButtons.rightDown, //'Right Down', + '0008': WahooKickrShiftButtons.rightSteer, //'Right Steer', + '0200': WahooKickrShiftButtons.leftUp, // 'Left Up', + '0400': WahooKickrShiftButtons.leftDown, //'Left Down', + '2000': WahooKickrShiftButtons.leftSteer, //'Left Steer', + '0004': WahooKickrShiftButtons.shiftUpRight, // 'Right Shift Up', + '0002': WahooKickrShiftButtons.shiftDownRight, // 'Right Shift Down', + '1000': WahooKickrShiftButtons.shiftUpLeft, //'Left Shift Up', + '0800': WahooKickrShiftButtons.shiftDownLeft, //'Left Shift Down', + '4000': WahooKickrShiftButtons.rightBrake, //'Right Brake', + '0100': WahooKickrShiftButtons.leftBrake, //'Left Brake', + }; +} + +class WahooKickrShiftButtons { + static const ControllerButton leftSteer = ControllerButton( + 'leftSteer', + action: InGameAction.navigateLeft, + icon: Icons.keyboard_arrow_left, + color: Colors.black, + ); + static const ControllerButton rightSteer = ControllerButton( + 'rightSteer', + action: InGameAction.navigateRight, + icon: Icons.keyboard_arrow_right, + color: Colors.black, + ); + static const ControllerButton leftDown = ControllerButton('leftDown', action: InGameAction.shiftDown); + static const ControllerButton leftBrake = ControllerButton('leftBrake', action: InGameAction.shiftDown); + + static const ControllerButton shiftUpLeft = ControllerButton( + 'shiftUpLeft', + action: InGameAction.shiftDown, + icon: Icons.remove, + color: Colors.black, + ); + static const ControllerButton shiftDownLeft = ControllerButton( + 'shiftDownLeft', + action: InGameAction.shiftDown, + icon: Icons.remove, + color: Colors.black, + ); + static const ControllerButton leftUp = ControllerButton('leftUp', action: InGameAction.shiftDown); + + static const ControllerButton rightDown = ControllerButton('rightDown', action: InGameAction.shiftUp); + static const ControllerButton rightBrake = ControllerButton('rightBrake', action: InGameAction.shiftUp); + + static const ControllerButton shiftUpRight = ControllerButton( + 'shiftUpRight', + action: InGameAction.shiftUp, + icon: Icons.add, + color: Colors.black, + ); + static const ControllerButton shiftDownRight = ControllerButton('shiftDownRight', action: InGameAction.shiftUp); + static const ControllerButton rightUp = ControllerButton('rightUp', action: InGameAction.shiftUp); + + static const List values = [ + leftSteer, + rightSteer, + leftDown, + leftBrake, + shiftUpLeft, + shiftDownLeft, + leftUp, + rightDown, + rightBrake, + shiftUpRight, + shiftDownRight, + rightUp, + ]; +} diff --git a/lib/bluetooth/devices/zwift/constants.dart b/lib/bluetooth/devices/zwift/constants.dart new file mode 100644 index 000000000..d487c7008 --- /dev/null +++ b/lib/bluetooth/devices/zwift/constants.dart @@ -0,0 +1,180 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; + +class ZwiftConstants { + static const ZWIFT_CUSTOM_SERVICE_UUID = "00000001-19CA-4651-86E5-FA29DCDD09D1"; + static const ZWIFT_RIDE_CUSTOM_SERVICE_UUID = "0000fc82-0000-1000-8000-00805f9b34fb"; + static const ZWIFT_RIDE_CUSTOM_SERVICE_UUID_SHORT = "fc82"; + static const ZWIFT_ASYNC_CHARACTERISTIC_UUID = "00000002-19CA-4651-86E5-FA29DCDD09D1"; + static const ZWIFT_SYNC_RX_CHARACTERISTIC_UUID = "00000003-19CA-4651-86E5-FA29DCDD09D1"; + static const ZWIFT_SYNC_TX_CHARACTERISTIC_UUID = "00000004-19CA-4651-86E5-FA29DCDD09D1"; + + static const ZWIFT_MANUFACTURER_ID = 2378; // Zwift, Inc => 0x094A + + // Zwift Play = RC1 + static const RC1_LEFT_SIDE = 0x03; + static const RC1_RIGHT_SIDE = 0x02; + + // Zwift Ride + static const RIDE_RIGHT_SIDE = 0x07; + static const RIDE_LEFT_SIDE = 0x08; + + // Zwift Click = BC1 + static const BC1 = 0x09; + + // Zwift Click v2 Right (unconfirmed) + static const CLICK_V2_RIGHT_SIDE = 0x0A; + // Zwift Click v2 Right (unconfirmed) + static const CLICK_V2_LEFT_SIDE = 0x0B; + + static final RIDE_ON = Uint8List.fromList([0x52, 0x69, 0x64, 0x65, 0x4f, 0x6e]); + static final VIBRATE_PATTERN = Uint8List.fromList([0x12, 0x12, 0x08, 0x0A, 0x06, 0x08, 0x02, 0x10, 0x00, 0x18]); + + // these don't actually seem to matter, its just the header has to be 7 bytes RIDEON + 2 + static final REQUEST_START = Uint8List.fromList([0x00, 0x09]); //byteArrayOf(1, 2) + static final RESPONSE_START_CLICK = Uint8List.fromList([0x01, 0x03]); // from device + static final RESPONSE_START_PLAY = Uint8List.fromList([0x01, 0x04]); // from device + static final RESPONSE_START_CLICK_V2 = Uint8List.fromList([0x02, 0x03]); // from device + static final RESPONSE_STOPPED_CLICK_V2_VARIANT_1 = Uint8List.fromList([0xff, 0x05, 0x00, 0xea, 0x05]); // from device + static final RESPONSE_STOPPED_CLICK_V2_VARIANT_2 = Uint8List.fromList([0xff, 0x05, 0x00, 0xfa, 0x05]); // from device + + // Message types received from device + static const CONTROLLER_NOTIFICATION_MESSAGE_TYPE = 07; + static const EMPTY_MESSAGE_TYPE = 21; // 0x15 + static const BATTERY_LEVEL_TYPE = 25; + static const UNKNOWN_CLICKV2_TYPE = 0x3C; + + // not figured out the protobuf type this really is, the content is just two varints. + static const int CLICK_NOTIFICATION_MESSAGE_TYPE = 55; // 0x37 + static const int PLAY_NOTIFICATION_MESSAGE_TYPE = 7; + static const int RIDE_NOTIFICATION_MESSAGE_TYPE = 35; // 0x23 + + // see this if connected to Core then Zwift connects to it. just one byte + static const DISCONNECT_MESSAGE_TYPE = 0xFE; +} + +class ZwiftButtons { + // left controller + static const ControllerButton navigationUp = ControllerButton( + 'navigationUp', + action: InGameAction.toggleUi, + icon: Icons.keyboard_arrow_up, + color: Colors.black, + ); + static const ControllerButton navigationDown = ControllerButton( + 'navigationDown', + action: InGameAction.uturn, + icon: Icons.keyboard_arrow_down, + color: Colors.black, + ); + static const ControllerButton navigationLeft = ControllerButton( + 'navigationLeft', + action: InGameAction.navigateLeft, + icon: Icons.keyboard_arrow_left, + color: Colors.black, + ); + static const ControllerButton navigationRight = ControllerButton( + 'navigationRight', + action: InGameAction.navigateRight, + icon: Icons.keyboard_arrow_right, + color: Colors.black, + ); + static const ControllerButton onOffLeft = ControllerButton('onOffLeft', action: InGameAction.toggleUi); + static const ControllerButton sideButtonLeft = ControllerButton('sideButtonLeft', action: InGameAction.shiftDown); + static const ControllerButton paddleLeft = ControllerButton('paddleLeft', action: InGameAction.shiftDown); + + // zwift ride only + static const ControllerButton shiftUpLeft = ControllerButton( + 'shiftUpLeft', + action: InGameAction.shiftDown, + icon: Icons.remove, + color: Colors.black, + ); + static const ControllerButton shiftDownLeft = ControllerButton( + 'shiftDownLeft', + action: InGameAction.shiftDown, + ); + static const ControllerButton powerUpLeft = ControllerButton('powerUpLeft', action: InGameAction.shiftDown); + + // right controller + static const ControllerButton a = ControllerButton('a', action: null, color: Colors.lightGreen); + static const ControllerButton b = ControllerButton('b', action: null, color: Colors.pinkAccent); + static const ControllerButton z = ControllerButton('z', action: null, color: Colors.deepOrangeAccent); + static const ControllerButton y = ControllerButton('y', action: null, color: Colors.lightBlue); + static const ControllerButton onOffRight = ControllerButton('onOffRight', action: InGameAction.toggleUi); + static const ControllerButton sideButtonRight = ControllerButton('sideButtonRight', action: InGameAction.shiftUp); + static const ControllerButton paddleRight = ControllerButton('paddleRight', action: InGameAction.shiftUp); + + // zwift ride only + static const ControllerButton shiftUpRight = ControllerButton( + 'shiftUpRight', + action: InGameAction.shiftUp, + icon: Icons.add, + color: Colors.black, + ); + static const ControllerButton shiftDownRight = ControllerButton('shiftDownRight', action: InGameAction.shiftUp); + static const ControllerButton powerUpRight = ControllerButton('powerUpRight', action: InGameAction.shiftUp); + + static List get values => [ + // left + navigationUp, + navigationDown, + navigationLeft, + navigationRight, + onOffLeft, + sideButtonLeft, + paddleLeft, + shiftUpLeft, + shiftDownLeft, + powerUpLeft, + // right + a, + b, + z, + y, + onOffRight, + sideButtonRight, + paddleRight, + shiftUpRight, + shiftDownRight, + powerUpRight, + ]; +} + +enum ZwiftDeviceType { + click, + clickV2Right, + clickV2Left, + playLeft, + playRight, + rideRight, + rideLeft; + + @override + String toString() { + return super.toString().split('.').last; + } + + // add constructor + static ZwiftDeviceType? fromManufacturerData(int data) { + switch (data) { + case ZwiftConstants.BC1: + return ZwiftDeviceType.click; + case ZwiftConstants.CLICK_V2_RIGHT_SIDE: + return ZwiftDeviceType.clickV2Right; + case ZwiftConstants.CLICK_V2_LEFT_SIDE: + return ZwiftDeviceType.clickV2Left; + case ZwiftConstants.RC1_LEFT_SIDE: + return ZwiftDeviceType.playLeft; + case ZwiftConstants.RC1_RIGHT_SIDE: + return ZwiftDeviceType.playRight; + case ZwiftConstants.RIDE_RIGHT_SIDE: + return ZwiftDeviceType.rideRight; + case ZwiftConstants.RIDE_LEFT_SIDE: + return ZwiftDeviceType.rideLeft; + } + return null; + } +} diff --git a/lib/bluetooth/devices/zwift/protocol/zp.pb.dart b/lib/bluetooth/devices/zwift/protocol/zp.pb.dart new file mode 100644 index 000000000..5396da800 --- /dev/null +++ b/lib/bluetooth/devices/zwift/protocol/zp.pb.dart @@ -0,0 +1,6146 @@ +// +// Generated code. Do not modify. +// source: zp.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'zp.pbenum.dart'; + +export 'zp.pbenum.dart'; + +class TrainerEnvSim extends $pb.GeneratedMessage { + factory TrainerEnvSim({ + $core.int? simulatedWind, + $core.int? simulatedGrade, + $core.int? simulatedCW, + $core.int? simulatedCRR, + }) { + final $result = create(); + if (simulatedWind != null) { + $result.simulatedWind = simulatedWind; + } + if (simulatedGrade != null) { + $result.simulatedGrade = simulatedGrade; + } + if (simulatedCW != null) { + $result.simulatedCW = simulatedCW; + } + if (simulatedCRR != null) { + $result.simulatedCRR = simulatedCRR; + } + return $result; + } + TrainerEnvSim._() : super(); + factory TrainerEnvSim.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory TrainerEnvSim.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'TrainerEnvSim', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'simulatedWind', $pb.PbFieldType.OS3, protoName: 'simulatedWind') + ..a<$core.int>(2, _omitFieldNames ? '' : 'simulatedGrade', $pb.PbFieldType.OS3, protoName: 'simulatedGrade') + ..a<$core.int>(3, _omitFieldNames ? '' : 'simulatedCW', $pb.PbFieldType.OU3, protoName: 'simulatedCW') + ..a<$core.int>(4, _omitFieldNames ? '' : 'simulatedCRR', $pb.PbFieldType.OU3, protoName: 'simulatedCRR') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + TrainerEnvSim clone() => TrainerEnvSim()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + TrainerEnvSim copyWith(void Function(TrainerEnvSim) updates) => super.copyWith((message) => updates(message as TrainerEnvSim)) as TrainerEnvSim; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static TrainerEnvSim create() => TrainerEnvSim._(); + TrainerEnvSim createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static TrainerEnvSim getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static TrainerEnvSim? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get simulatedWind => $_getIZ(0); + @$pb.TagNumber(1) + set simulatedWind($core.int v) { $_setSignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasSimulatedWind() => $_has(0); + @$pb.TagNumber(1) + void clearSimulatedWind() => clearField(1); + + @$pb.TagNumber(2) + $core.int get simulatedGrade => $_getIZ(1); + @$pb.TagNumber(2) + set simulatedGrade($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasSimulatedGrade() => $_has(1); + @$pb.TagNumber(2) + void clearSimulatedGrade() => clearField(2); + + @$pb.TagNumber(3) + $core.int get simulatedCW => $_getIZ(2); + @$pb.TagNumber(3) + set simulatedCW($core.int v) { $_setUnsignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasSimulatedCW() => $_has(2); + @$pb.TagNumber(3) + void clearSimulatedCW() => clearField(3); + + @$pb.TagNumber(4) + $core.int get simulatedCRR => $_getIZ(3); + @$pb.TagNumber(4) + set simulatedCRR($core.int v) { $_setUnsignedInt32(3, v); } + @$pb.TagNumber(4) + $core.bool hasSimulatedCRR() => $_has(3); + @$pb.TagNumber(4) + void clearSimulatedCRR() => clearField(4); +} + +class TrainerBikeSim extends $pb.GeneratedMessage { + factory TrainerBikeSim({ + $core.int? simulatedRealGearRatio, + $core.int? simulatedVirtualGearRatio, + $core.int? simulatedWheelDiameter, + $core.int? simulatedBikeMass, + $core.int? simulatedRiderMass, + $core.int? simulatedFrontalArea, + $core.int? eBrake, + }) { + final $result = create(); + if (simulatedRealGearRatio != null) { + $result.simulatedRealGearRatio = simulatedRealGearRatio; + } + if (simulatedVirtualGearRatio != null) { + $result.simulatedVirtualGearRatio = simulatedVirtualGearRatio; + } + if (simulatedWheelDiameter != null) { + $result.simulatedWheelDiameter = simulatedWheelDiameter; + } + if (simulatedBikeMass != null) { + $result.simulatedBikeMass = simulatedBikeMass; + } + if (simulatedRiderMass != null) { + $result.simulatedRiderMass = simulatedRiderMass; + } + if (simulatedFrontalArea != null) { + $result.simulatedFrontalArea = simulatedFrontalArea; + } + if (eBrake != null) { + $result.eBrake = eBrake; + } + return $result; + } + TrainerBikeSim._() : super(); + factory TrainerBikeSim.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory TrainerBikeSim.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'TrainerBikeSim', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'simulatedRealGearRatio', $pb.PbFieldType.OU3, protoName: 'simulatedRealGearRatio') + ..a<$core.int>(2, _omitFieldNames ? '' : 'simulatedVirtualGearRatio', $pb.PbFieldType.OU3, protoName: 'simulatedVirtualGearRatio') + ..a<$core.int>(3, _omitFieldNames ? '' : 'simulatedWheelDiameter', $pb.PbFieldType.OU3, protoName: 'simulatedWheelDiameter') + ..a<$core.int>(4, _omitFieldNames ? '' : 'simulatedBikeMass', $pb.PbFieldType.OU3, protoName: 'simulatedBikeMass') + ..a<$core.int>(5, _omitFieldNames ? '' : 'simulatedRiderMass', $pb.PbFieldType.OU3, protoName: 'simulatedRiderMass') + ..a<$core.int>(6, _omitFieldNames ? '' : 'simulatedFrontalArea', $pb.PbFieldType.OU3, protoName: 'simulatedFrontalArea') + ..a<$core.int>(7, _omitFieldNames ? '' : 'eBrake', $pb.PbFieldType.OU3, protoName: 'eBrake') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + TrainerBikeSim clone() => TrainerBikeSim()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + TrainerBikeSim copyWith(void Function(TrainerBikeSim) updates) => super.copyWith((message) => updates(message as TrainerBikeSim)) as TrainerBikeSim; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static TrainerBikeSim create() => TrainerBikeSim._(); + TrainerBikeSim createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static TrainerBikeSim getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static TrainerBikeSim? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get simulatedRealGearRatio => $_getIZ(0); + @$pb.TagNumber(1) + set simulatedRealGearRatio($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasSimulatedRealGearRatio() => $_has(0); + @$pb.TagNumber(1) + void clearSimulatedRealGearRatio() => clearField(1); + + @$pb.TagNumber(2) + $core.int get simulatedVirtualGearRatio => $_getIZ(1); + @$pb.TagNumber(2) + set simulatedVirtualGearRatio($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasSimulatedVirtualGearRatio() => $_has(1); + @$pb.TagNumber(2) + void clearSimulatedVirtualGearRatio() => clearField(2); + + @$pb.TagNumber(3) + $core.int get simulatedWheelDiameter => $_getIZ(2); + @$pb.TagNumber(3) + set simulatedWheelDiameter($core.int v) { $_setUnsignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasSimulatedWheelDiameter() => $_has(2); + @$pb.TagNumber(3) + void clearSimulatedWheelDiameter() => clearField(3); + + @$pb.TagNumber(4) + $core.int get simulatedBikeMass => $_getIZ(3); + @$pb.TagNumber(4) + set simulatedBikeMass($core.int v) { $_setUnsignedInt32(3, v); } + @$pb.TagNumber(4) + $core.bool hasSimulatedBikeMass() => $_has(3); + @$pb.TagNumber(4) + void clearSimulatedBikeMass() => clearField(4); + + @$pb.TagNumber(5) + $core.int get simulatedRiderMass => $_getIZ(4); + @$pb.TagNumber(5) + set simulatedRiderMass($core.int v) { $_setUnsignedInt32(4, v); } + @$pb.TagNumber(5) + $core.bool hasSimulatedRiderMass() => $_has(4); + @$pb.TagNumber(5) + void clearSimulatedRiderMass() => clearField(5); + + @$pb.TagNumber(6) + $core.int get simulatedFrontalArea => $_getIZ(5); + @$pb.TagNumber(6) + set simulatedFrontalArea($core.int v) { $_setUnsignedInt32(5, v); } + @$pb.TagNumber(6) + $core.bool hasSimulatedFrontalArea() => $_has(5); + @$pb.TagNumber(6) + void clearSimulatedFrontalArea() => clearField(6); + + @$pb.TagNumber(7) + $core.int get eBrake => $_getIZ(6); + @$pb.TagNumber(7) + set eBrake($core.int v) { $_setUnsignedInt32(6, v); } + @$pb.TagNumber(7) + $core.bool hasEBrake() => $_has(6); + @$pb.TagNumber(7) + void clearEBrake() => clearField(7); +} + +class ControllerAnalogEvent extends $pb.GeneratedMessage { + factory ControllerAnalogEvent({ + $core.int? sensorId, + $core.int? value, + }) { + final $result = create(); + if (sensorId != null) { + $result.sensorId = sensorId; + } + if (value != null) { + $result.value = value; + } + return $result; + } + ControllerAnalogEvent._() : super(); + factory ControllerAnalogEvent.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ControllerAnalogEvent.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ControllerAnalogEvent', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'sensorId', $pb.PbFieldType.OU3, protoName: 'sensorId') + ..a<$core.int>(2, _omitFieldNames ? '' : 'value', $pb.PbFieldType.OS3) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ControllerAnalogEvent clone() => ControllerAnalogEvent()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ControllerAnalogEvent copyWith(void Function(ControllerAnalogEvent) updates) => super.copyWith((message) => updates(message as ControllerAnalogEvent)) as ControllerAnalogEvent; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ControllerAnalogEvent create() => ControllerAnalogEvent._(); + ControllerAnalogEvent createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ControllerAnalogEvent getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ControllerAnalogEvent? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get sensorId => $_getIZ(0); + @$pb.TagNumber(1) + set sensorId($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasSensorId() => $_has(0); + @$pb.TagNumber(1) + void clearSensorId() => clearField(1); + + @$pb.TagNumber(2) + $core.int get value => $_getIZ(1); + @$pb.TagNumber(2) + set value($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasValue() => $_has(1); + @$pb.TagNumber(2) + void clearValue() => clearField(2); +} + +class InputAnalogRange extends $pb.GeneratedMessage { + factory InputAnalogRange({ + $core.int? sensorId, + $core.int? minAnalogValue, + $core.int? maxAnalogValue, + }) { + final $result = create(); + if (sensorId != null) { + $result.sensorId = sensorId; + } + if (minAnalogValue != null) { + $result.minAnalogValue = minAnalogValue; + } + if (maxAnalogValue != null) { + $result.maxAnalogValue = maxAnalogValue; + } + return $result; + } + InputAnalogRange._() : super(); + factory InputAnalogRange.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory InputAnalogRange.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'InputAnalogRange', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'sensorId', $pb.PbFieldType.OU3, protoName: 'sensorId') + ..a<$core.int>(2, _omitFieldNames ? '' : 'minAnalogValue', $pb.PbFieldType.OS3, protoName: 'minAnalogValue') + ..a<$core.int>(3, _omitFieldNames ? '' : 'maxAnalogValue', $pb.PbFieldType.OS3, protoName: 'maxAnalogValue') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + InputAnalogRange clone() => InputAnalogRange()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + InputAnalogRange copyWith(void Function(InputAnalogRange) updates) => super.copyWith((message) => updates(message as InputAnalogRange)) as InputAnalogRange; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static InputAnalogRange create() => InputAnalogRange._(); + InputAnalogRange createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static InputAnalogRange getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static InputAnalogRange? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get sensorId => $_getIZ(0); + @$pb.TagNumber(1) + set sensorId($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasSensorId() => $_has(0); + @$pb.TagNumber(1) + void clearSensorId() => clearField(1); + + @$pb.TagNumber(2) + $core.int get minAnalogValue => $_getIZ(1); + @$pb.TagNumber(2) + set minAnalogValue($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasMinAnalogValue() => $_has(1); + @$pb.TagNumber(2) + void clearMinAnalogValue() => clearField(2); + + @$pb.TagNumber(3) + $core.int get maxAnalogValue => $_getIZ(2); + @$pb.TagNumber(3) + set maxAnalogValue($core.int v) { $_setSignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasMaxAnalogValue() => $_has(2); + @$pb.TagNumber(3) + void clearMaxAnalogValue() => clearField(3); +} + +class InputAnalogDeadzone extends $pb.GeneratedMessage { + factory InputAnalogDeadzone({ + $core.int? sensorId, + $core.int? negDeadzoneValue, + $core.int? posDeadzoneValue, + }) { + final $result = create(); + if (sensorId != null) { + $result.sensorId = sensorId; + } + if (negDeadzoneValue != null) { + $result.negDeadzoneValue = negDeadzoneValue; + } + if (posDeadzoneValue != null) { + $result.posDeadzoneValue = posDeadzoneValue; + } + return $result; + } + InputAnalogDeadzone._() : super(); + factory InputAnalogDeadzone.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory InputAnalogDeadzone.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'InputAnalogDeadzone', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'sensorId', $pb.PbFieldType.OU3, protoName: 'sensorId') + ..a<$core.int>(2, _omitFieldNames ? '' : 'negDeadzoneValue', $pb.PbFieldType.OS3, protoName: 'negDeadzoneValue') + ..a<$core.int>(3, _omitFieldNames ? '' : 'posDeadzoneValue', $pb.PbFieldType.OS3, protoName: 'posDeadzoneValue') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + InputAnalogDeadzone clone() => InputAnalogDeadzone()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + InputAnalogDeadzone copyWith(void Function(InputAnalogDeadzone) updates) => super.copyWith((message) => updates(message as InputAnalogDeadzone)) as InputAnalogDeadzone; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static InputAnalogDeadzone create() => InputAnalogDeadzone._(); + InputAnalogDeadzone createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static InputAnalogDeadzone getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static InputAnalogDeadzone? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get sensorId => $_getIZ(0); + @$pb.TagNumber(1) + set sensorId($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasSensorId() => $_has(0); + @$pb.TagNumber(1) + void clearSensorId() => clearField(1); + + @$pb.TagNumber(2) + $core.int get negDeadzoneValue => $_getIZ(1); + @$pb.TagNumber(2) + set negDeadzoneValue($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasNegDeadzoneValue() => $_has(1); + @$pb.TagNumber(2) + void clearNegDeadzoneValue() => clearField(2); + + @$pb.TagNumber(3) + $core.int get posDeadzoneValue => $_getIZ(2); + @$pb.TagNumber(3) + set posDeadzoneValue($core.int v) { $_setSignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasPosDeadzoneValue() => $_has(2); + @$pb.TagNumber(3) + void clearPosDeadzoneValue() => clearField(3); +} + +class WifiNetwork extends $pb.GeneratedMessage { + factory WifiNetwork({ + $core.int? networkId, + $core.List<$core.int>? ssid, + $core.List<$core.int>? password, + }) { + final $result = create(); + if (networkId != null) { + $result.networkId = networkId; + } + if (ssid != null) { + $result.ssid = ssid; + } + if (password != null) { + $result.password = password; + } + return $result; + } + WifiNetwork._() : super(); + factory WifiNetwork.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory WifiNetwork.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'WifiNetwork', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'networkId', $pb.PbFieldType.OU3, protoName: 'networkId') + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'ssid', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'password', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + WifiNetwork clone() => WifiNetwork()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + WifiNetwork copyWith(void Function(WifiNetwork) updates) => super.copyWith((message) => updates(message as WifiNetwork)) as WifiNetwork; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static WifiNetwork create() => WifiNetwork._(); + WifiNetwork createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static WifiNetwork getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static WifiNetwork? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get networkId => $_getIZ(0); + @$pb.TagNumber(1) + set networkId($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasNetworkId() => $_has(0); + @$pb.TagNumber(1) + void clearNetworkId() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get ssid => $_getN(1); + @$pb.TagNumber(2) + set ssid($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasSsid() => $_has(1); + @$pb.TagNumber(2) + void clearSsid() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get password => $_getN(2); + @$pb.TagNumber(3) + set password($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasPassword() => $_has(2); + @$pb.TagNumber(3) + void clearPassword() => clearField(3); +} + +class WifiRegionCode extends $pb.GeneratedMessage { + factory WifiRegionCode({ + WifiRegionCode_RegionCodeType? regionCodeType, + $core.List<$core.int>? regionCode, + }) { + final $result = create(); + if (regionCodeType != null) { + $result.regionCodeType = regionCodeType; + } + if (regionCode != null) { + $result.regionCode = regionCode; + } + return $result; + } + WifiRegionCode._() : super(); + factory WifiRegionCode.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory WifiRegionCode.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'WifiRegionCode', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'regionCodeType', $pb.PbFieldType.OE, protoName: 'regionCodeType', defaultOrMaker: WifiRegionCode_RegionCodeType.ALPHA_2, valueOf: WifiRegionCode_RegionCodeType.valueOf, enumValues: WifiRegionCode_RegionCodeType.values) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'regionCode', $pb.PbFieldType.OY, protoName: 'regionCode') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + WifiRegionCode clone() => WifiRegionCode()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + WifiRegionCode copyWith(void Function(WifiRegionCode) updates) => super.copyWith((message) => updates(message as WifiRegionCode)) as WifiRegionCode; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static WifiRegionCode create() => WifiRegionCode._(); + WifiRegionCode createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static WifiRegionCode getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static WifiRegionCode? _defaultInstance; + + @$pb.TagNumber(1) + WifiRegionCode_RegionCodeType get regionCodeType => $_getN(0); + @$pb.TagNumber(1) + set regionCodeType(WifiRegionCode_RegionCodeType v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasRegionCodeType() => $_has(0); + @$pb.TagNumber(1) + void clearRegionCodeType() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get regionCode => $_getN(1); + @$pb.TagNumber(2) + set regionCode($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasRegionCode() => $_has(1); + @$pb.TagNumber(2) + void clearRegionCode() => clearField(2); +} + +class WifiNetworkDetails extends $pb.GeneratedMessage { + factory WifiNetworkDetails({ + $core.int? networkId, + $core.List<$core.int>? bssid, + $core.List<$core.int>? ssid, + $core.int? securityType, + $core.int? band, + $core.int? rssi, + }) { + final $result = create(); + if (networkId != null) { + $result.networkId = networkId; + } + if (bssid != null) { + $result.bssid = bssid; + } + if (ssid != null) { + $result.ssid = ssid; + } + if (securityType != null) { + $result.securityType = securityType; + } + if (band != null) { + $result.band = band; + } + if (rssi != null) { + $result.rssi = rssi; + } + return $result; + } + WifiNetworkDetails._() : super(); + factory WifiNetworkDetails.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory WifiNetworkDetails.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'WifiNetworkDetails', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'networkId', $pb.PbFieldType.OU3, protoName: 'networkId') + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'bssid', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'ssid', $pb.PbFieldType.OY) + ..a<$core.int>(4, _omitFieldNames ? '' : 'securityType', $pb.PbFieldType.OU3, protoName: 'securityType') + ..a<$core.int>(5, _omitFieldNames ? '' : 'band', $pb.PbFieldType.OU3) + ..a<$core.int>(6, _omitFieldNames ? '' : 'rssi', $pb.PbFieldType.OS3) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + WifiNetworkDetails clone() => WifiNetworkDetails()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + WifiNetworkDetails copyWith(void Function(WifiNetworkDetails) updates) => super.copyWith((message) => updates(message as WifiNetworkDetails)) as WifiNetworkDetails; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static WifiNetworkDetails create() => WifiNetworkDetails._(); + WifiNetworkDetails createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static WifiNetworkDetails getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static WifiNetworkDetails? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get networkId => $_getIZ(0); + @$pb.TagNumber(1) + set networkId($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasNetworkId() => $_has(0); + @$pb.TagNumber(1) + void clearNetworkId() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get bssid => $_getN(1); + @$pb.TagNumber(2) + set bssid($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasBssid() => $_has(1); + @$pb.TagNumber(2) + void clearBssid() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get ssid => $_getN(2); + @$pb.TagNumber(3) + set ssid($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasSsid() => $_has(2); + @$pb.TagNumber(3) + void clearSsid() => clearField(3); + + @$pb.TagNumber(4) + $core.int get securityType => $_getIZ(3); + @$pb.TagNumber(4) + set securityType($core.int v) { $_setUnsignedInt32(3, v); } + @$pb.TagNumber(4) + $core.bool hasSecurityType() => $_has(3); + @$pb.TagNumber(4) + void clearSecurityType() => clearField(4); + + @$pb.TagNumber(5) + $core.int get band => $_getIZ(4); + @$pb.TagNumber(5) + set band($core.int v) { $_setUnsignedInt32(4, v); } + @$pb.TagNumber(5) + $core.bool hasBand() => $_has(4); + @$pb.TagNumber(5) + void clearBand() => clearField(5); + + @$pb.TagNumber(6) + $core.int get rssi => $_getIZ(5); + @$pb.TagNumber(6) + set rssi($core.int v) { $_setSignedInt32(5, v); } + @$pb.TagNumber(6) + $core.bool hasRssi() => $_has(5); + @$pb.TagNumber(6) + void clearRssi() => clearField(6); +} + +class SensorInfo extends $pb.GeneratedMessage { + factory SensorInfo({ + $core.int? relayAssignedId, + $core.List<$core.int>? sensorName, + $core.List<$core.int>? sensorAddress, + InterfaceType? interfaceType, + SensorConnectionStatus? connectionStatus, + $core.Iterable? deviceTypes, + $core.bool? supportsZp, + }) { + final $result = create(); + if (relayAssignedId != null) { + $result.relayAssignedId = relayAssignedId; + } + if (sensorName != null) { + $result.sensorName = sensorName; + } + if (sensorAddress != null) { + $result.sensorAddress = sensorAddress; + } + if (interfaceType != null) { + $result.interfaceType = interfaceType; + } + if (connectionStatus != null) { + $result.connectionStatus = connectionStatus; + } + if (deviceTypes != null) { + $result.deviceTypes.addAll(deviceTypes); + } + if (supportsZp != null) { + $result.supportsZp = supportsZp; + } + return $result; + } + SensorInfo._() : super(); + factory SensorInfo.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SensorInfo.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SensorInfo', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'relayAssignedId', $pb.PbFieldType.OU3, protoName: 'relayAssignedId') + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'sensorName', $pb.PbFieldType.OY, protoName: 'sensorName') + ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'sensorAddress', $pb.PbFieldType.OY, protoName: 'sensorAddress') + ..e(5, _omitFieldNames ? '' : 'interfaceType', $pb.PbFieldType.OE, protoName: 'interfaceType', defaultOrMaker: InterfaceType.INTERFACE_BLE, valueOf: InterfaceType.valueOf, enumValues: InterfaceType.values) + ..e(6, _omitFieldNames ? '' : 'connectionStatus', $pb.PbFieldType.OE, protoName: 'connectionStatus', defaultOrMaker: SensorConnectionStatus.SENSOR_STATUS_DISCOVERED, valueOf: SensorConnectionStatus.valueOf, enumValues: SensorConnectionStatus.values) + ..pc(7, _omitFieldNames ? '' : 'deviceTypes', $pb.PbFieldType.PE, protoName: 'deviceTypes', valueOf: DeviceType.valueOf, enumValues: DeviceType.values, defaultEnumValue: DeviceType.UNDEFINED) + ..aOB(8, _omitFieldNames ? '' : 'supportsZp', protoName: 'supportsZp') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SensorInfo clone() => SensorInfo()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SensorInfo copyWith(void Function(SensorInfo) updates) => super.copyWith((message) => updates(message as SensorInfo)) as SensorInfo; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SensorInfo create() => SensorInfo._(); + SensorInfo createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SensorInfo getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SensorInfo? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get relayAssignedId => $_getIZ(0); + @$pb.TagNumber(1) + set relayAssignedId($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasRelayAssignedId() => $_has(0); + @$pb.TagNumber(1) + void clearRelayAssignedId() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get sensorName => $_getN(1); + @$pb.TagNumber(2) + set sensorName($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasSensorName() => $_has(1); + @$pb.TagNumber(2) + void clearSensorName() => clearField(2); + + @$pb.TagNumber(4) + $core.List<$core.int> get sensorAddress => $_getN(2); + @$pb.TagNumber(4) + set sensorAddress($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(4) + $core.bool hasSensorAddress() => $_has(2); + @$pb.TagNumber(4) + void clearSensorAddress() => clearField(4); + + @$pb.TagNumber(5) + InterfaceType get interfaceType => $_getN(3); + @$pb.TagNumber(5) + set interfaceType(InterfaceType v) { setField(5, v); } + @$pb.TagNumber(5) + $core.bool hasInterfaceType() => $_has(3); + @$pb.TagNumber(5) + void clearInterfaceType() => clearField(5); + + @$pb.TagNumber(6) + SensorConnectionStatus get connectionStatus => $_getN(4); + @$pb.TagNumber(6) + set connectionStatus(SensorConnectionStatus v) { setField(6, v); } + @$pb.TagNumber(6) + $core.bool hasConnectionStatus() => $_has(4); + @$pb.TagNumber(6) + void clearConnectionStatus() => clearField(6); + + @$pb.TagNumber(7) + $core.List get deviceTypes => $_getList(5); + + @$pb.TagNumber(8) + $core.bool get supportsZp => $_getBF(6); + @$pb.TagNumber(8) + set supportsZp($core.bool v) { $_setBool(6, v); } + @$pb.TagNumber(8) + $core.bool hasSupportsZp() => $_has(6); + @$pb.TagNumber(8) + void clearSupportsZp() => clearField(8); +} + +class SensorInfoList extends $pb.GeneratedMessage { + factory SensorInfoList({ + $core.Iterable? sensorInfo, + }) { + final $result = create(); + if (sensorInfo != null) { + $result.sensorInfo.addAll(sensorInfo); + } + return $result; + } + SensorInfoList._() : super(); + factory SensorInfoList.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SensorInfoList.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SensorInfoList', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..pc(1, _omitFieldNames ? '' : 'sensorInfo', $pb.PbFieldType.PM, protoName: 'sensorInfo', subBuilder: SensorInfo.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SensorInfoList clone() => SensorInfoList()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SensorInfoList copyWith(void Function(SensorInfoList) updates) => super.copyWith((message) => updates(message as SensorInfoList)) as SensorInfoList; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SensorInfoList create() => SensorInfoList._(); + SensorInfoList createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SensorInfoList getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SensorInfoList? _defaultInstance; + + @$pb.TagNumber(1) + $core.List get sensorInfo => $_getList(0); +} + +class DeviceUpdatePage extends $pb.GeneratedMessage { + factory DeviceUpdatePage({ + $core.int? updateStatus, + $core.int? newVersion, + }) { + final $result = create(); + if (updateStatus != null) { + $result.updateStatus = updateStatus; + } + if (newVersion != null) { + $result.newVersion = newVersion; + } + return $result; + } + DeviceUpdatePage._() : super(); + factory DeviceUpdatePage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DeviceUpdatePage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DeviceUpdatePage', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'updateStatus', $pb.PbFieldType.OU3, protoName: 'updateStatus') + ..a<$core.int>(2, _omitFieldNames ? '' : 'newVersion', $pb.PbFieldType.OU3, protoName: 'newVersion') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + DeviceUpdatePage clone() => DeviceUpdatePage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DeviceUpdatePage copyWith(void Function(DeviceUpdatePage) updates) => super.copyWith((message) => updates(message as DeviceUpdatePage)) as DeviceUpdatePage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DeviceUpdatePage create() => DeviceUpdatePage._(); + DeviceUpdatePage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DeviceUpdatePage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DeviceUpdatePage? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get updateStatus => $_getIZ(0); + @$pb.TagNumber(1) + set updateStatus($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasUpdateStatus() => $_has(0); + @$pb.TagNumber(1) + void clearUpdateStatus() => clearField(1); + + @$pb.TagNumber(2) + $core.int get newVersion => $_getIZ(1); + @$pb.TagNumber(2) + set newVersion($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasNewVersion() => $_has(1); + @$pb.TagNumber(2) + void clearNewVersion() => clearField(2); +} + +class DateTimePage extends $pb.GeneratedMessage { + factory DateTimePage({ + $core.int? utcDateTime, + }) { + final $result = create(); + if (utcDateTime != null) { + $result.utcDateTime = utcDateTime; + } + return $result; + } + DateTimePage._() : super(); + factory DateTimePage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DateTimePage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DateTimePage', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'utcDateTime', $pb.PbFieldType.OU3, protoName: 'utcDateTime') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + DateTimePage clone() => DateTimePage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DateTimePage copyWith(void Function(DateTimePage) updates) => super.copyWith((message) => updates(message as DateTimePage)) as DateTimePage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DateTimePage create() => DateTimePage._(); + DateTimePage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DateTimePage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DateTimePage? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get utcDateTime => $_getIZ(0); + @$pb.TagNumber(1) + set utcDateTime($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasUtcDateTime() => $_has(0); + @$pb.TagNumber(1) + void clearUtcDateTime() => clearField(1); +} + +class BleSecurityPage extends $pb.GeneratedMessage { + factory BleSecurityPage({ + BleSecureConnectionStatus? secureConnectionStatus, + BleSecureConnectionWindowStatus? secureConnectionWindowStatus, + }) { + final $result = create(); + if (secureConnectionStatus != null) { + $result.secureConnectionStatus = secureConnectionStatus; + } + if (secureConnectionWindowStatus != null) { + $result.secureConnectionWindowStatus = secureConnectionWindowStatus; + } + return $result; + } + BleSecurityPage._() : super(); + factory BleSecurityPage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory BleSecurityPage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BleSecurityPage', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'secureConnectionStatus', $pb.PbFieldType.OE, protoName: 'secureConnectionStatus', defaultOrMaker: BleSecureConnectionStatus.BLE_CONNECTION_SECURITY_STATUS_NONE, valueOf: BleSecureConnectionStatus.valueOf, enumValues: BleSecureConnectionStatus.values) + ..e(2, _omitFieldNames ? '' : 'secureConnectionWindowStatus', $pb.PbFieldType.OE, protoName: 'secureConnectionWindowStatus', defaultOrMaker: BleSecureConnectionWindowStatus.BLE_SECURE_CONNECTION_WINDOW_STATUS_CLOSED, valueOf: BleSecureConnectionWindowStatus.valueOf, enumValues: BleSecureConnectionWindowStatus.values) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + BleSecurityPage clone() => BleSecurityPage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + BleSecurityPage copyWith(void Function(BleSecurityPage) updates) => super.copyWith((message) => updates(message as BleSecurityPage)) as BleSecurityPage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static BleSecurityPage create() => BleSecurityPage._(); + BleSecurityPage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static BleSecurityPage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static BleSecurityPage? _defaultInstance; + + @$pb.TagNumber(1) + BleSecureConnectionStatus get secureConnectionStatus => $_getN(0); + @$pb.TagNumber(1) + set secureConnectionStatus(BleSecureConnectionStatus v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasSecureConnectionStatus() => $_has(0); + @$pb.TagNumber(1) + void clearSecureConnectionStatus() => clearField(1); + + @$pb.TagNumber(2) + BleSecureConnectionWindowStatus get secureConnectionWindowStatus => $_getN(1); + @$pb.TagNumber(2) + set secureConnectionWindowStatus(BleSecureConnectionWindowStatus v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasSecureConnectionWindowStatus() => $_has(1); + @$pb.TagNumber(2) + void clearSecureConnectionWindowStatus() => clearField(2); +} + +class DevInfoPage_DeviceCapabilities extends $pb.GeneratedMessage { + factory DevInfoPage_DeviceCapabilities({ + $core.int? deviceType, + $core.int? capabilities, + }) { + final $result = create(); + if (deviceType != null) { + $result.deviceType = deviceType; + } + if (capabilities != null) { + $result.capabilities = capabilities; + } + return $result; + } + DevInfoPage_DeviceCapabilities._() : super(); + factory DevInfoPage_DeviceCapabilities.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DevInfoPage_DeviceCapabilities.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DevInfoPage.DeviceCapabilities', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'deviceType', $pb.PbFieldType.QU3, protoName: 'deviceType') + ..a<$core.int>(2, _omitFieldNames ? '' : 'capabilities', $pb.PbFieldType.QU3) + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + DevInfoPage_DeviceCapabilities clone() => DevInfoPage_DeviceCapabilities()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DevInfoPage_DeviceCapabilities copyWith(void Function(DevInfoPage_DeviceCapabilities) updates) => super.copyWith((message) => updates(message as DevInfoPage_DeviceCapabilities)) as DevInfoPage_DeviceCapabilities; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DevInfoPage_DeviceCapabilities create() => DevInfoPage_DeviceCapabilities._(); + DevInfoPage_DeviceCapabilities createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DevInfoPage_DeviceCapabilities getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DevInfoPage_DeviceCapabilities? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get deviceType => $_getIZ(0); + @$pb.TagNumber(1) + set deviceType($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasDeviceType() => $_has(0); + @$pb.TagNumber(1) + void clearDeviceType() => clearField(1); + + @$pb.TagNumber(2) + $core.int get capabilities => $_getIZ(1); + @$pb.TagNumber(2) + set capabilities($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasCapabilities() => $_has(1); + @$pb.TagNumber(2) + void clearCapabilities() => clearField(2); +} + +/// Page ID 0x000 +class DevInfoPage extends $pb.GeneratedMessage { + factory DevInfoPage({ + $core.int? protocolVersion, + $core.List<$core.int>? systemFwVersion, + $core.List<$core.int>? deviceName, + $core.List<$core.int>? serialNumber, + $core.List<$core.int>? systemHwRevision, + $core.Iterable? deviceCapabilities, + $core.int? manufacturerId, + $core.int? productId, + $core.List<$core.int>? deviceUid, + }) { + final $result = create(); + if (protocolVersion != null) { + $result.protocolVersion = protocolVersion; + } + if (systemFwVersion != null) { + $result.systemFwVersion = systemFwVersion; + } + if (deviceName != null) { + $result.deviceName = deviceName; + } + if (serialNumber != null) { + $result.serialNumber = serialNumber; + } + if (systemHwRevision != null) { + $result.systemHwRevision = systemHwRevision; + } + if (deviceCapabilities != null) { + $result.deviceCapabilities.addAll(deviceCapabilities); + } + if (manufacturerId != null) { + $result.manufacturerId = manufacturerId; + } + if (productId != null) { + $result.productId = productId; + } + if (deviceUid != null) { + $result.deviceUid = deviceUid; + } + return $result; + } + DevInfoPage._() : super(); + factory DevInfoPage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DevInfoPage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DevInfoPage', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'protocolVersion', $pb.PbFieldType.OU3, protoName: 'protocolVersion') + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'systemFwVersion', $pb.PbFieldType.OY, protoName: 'systemFwVersion') + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'deviceName', $pb.PbFieldType.OY, protoName: 'deviceName') + ..a<$core.List<$core.int>>(6, _omitFieldNames ? '' : 'serialNumber', $pb.PbFieldType.OY, protoName: 'serialNumber') + ..a<$core.List<$core.int>>(7, _omitFieldNames ? '' : 'systemHwRevision', $pb.PbFieldType.OY, protoName: 'systemHwRevision') + ..pc(8, _omitFieldNames ? '' : 'deviceCapabilities', $pb.PbFieldType.PM, protoName: 'deviceCapabilities', subBuilder: DevInfoPage_DeviceCapabilities.create) + ..a<$core.int>(9, _omitFieldNames ? '' : 'manufacturerId', $pb.PbFieldType.OU3, protoName: 'manufacturerId') + ..a<$core.int>(10, _omitFieldNames ? '' : 'productId', $pb.PbFieldType.OU3, protoName: 'productId') + ..a<$core.List<$core.int>>(11, _omitFieldNames ? '' : 'deviceUid', $pb.PbFieldType.OY, protoName: 'deviceUid') + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + DevInfoPage clone() => DevInfoPage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DevInfoPage copyWith(void Function(DevInfoPage) updates) => super.copyWith((message) => updates(message as DevInfoPage)) as DevInfoPage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DevInfoPage create() => DevInfoPage._(); + DevInfoPage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DevInfoPage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DevInfoPage? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get protocolVersion => $_getIZ(0); + @$pb.TagNumber(1) + set protocolVersion($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasProtocolVersion() => $_has(0); + @$pb.TagNumber(1) + void clearProtocolVersion() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get systemFwVersion => $_getN(1); + @$pb.TagNumber(2) + set systemFwVersion($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasSystemFwVersion() => $_has(1); + @$pb.TagNumber(2) + void clearSystemFwVersion() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get deviceName => $_getN(2); + @$pb.TagNumber(3) + set deviceName($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasDeviceName() => $_has(2); + @$pb.TagNumber(3) + void clearDeviceName() => clearField(3); + + @$pb.TagNumber(6) + $core.List<$core.int> get serialNumber => $_getN(3); + @$pb.TagNumber(6) + set serialNumber($core.List<$core.int> v) { $_setBytes(3, v); } + @$pb.TagNumber(6) + $core.bool hasSerialNumber() => $_has(3); + @$pb.TagNumber(6) + void clearSerialNumber() => clearField(6); + + @$pb.TagNumber(7) + $core.List<$core.int> get systemHwRevision => $_getN(4); + @$pb.TagNumber(7) + set systemHwRevision($core.List<$core.int> v) { $_setBytes(4, v); } + @$pb.TagNumber(7) + $core.bool hasSystemHwRevision() => $_has(4); + @$pb.TagNumber(7) + void clearSystemHwRevision() => clearField(7); + + @$pb.TagNumber(8) + $core.List get deviceCapabilities => $_getList(5); + + @$pb.TagNumber(9) + $core.int get manufacturerId => $_getIZ(6); + @$pb.TagNumber(9) + set manufacturerId($core.int v) { $_setUnsignedInt32(6, v); } + @$pb.TagNumber(9) + $core.bool hasManufacturerId() => $_has(6); + @$pb.TagNumber(9) + void clearManufacturerId() => clearField(9); + + @$pb.TagNumber(10) + $core.int get productId => $_getIZ(7); + @$pb.TagNumber(10) + set productId($core.int v) { $_setUnsignedInt32(7, v); } + @$pb.TagNumber(10) + $core.bool hasProductId() => $_has(7); + @$pb.TagNumber(10) + void clearProductId() => clearField(10); + + @$pb.TagNumber(11) + $core.List<$core.int> get deviceUid => $_getN(8); + @$pb.TagNumber(11) + set deviceUid($core.List<$core.int> v) { $_setBytes(8, v); } + @$pb.TagNumber(11) + $core.bool hasDeviceUid() => $_has(8); + @$pb.TagNumber(11) + void clearDeviceUid() => clearField(11); +} + +/// Page ID 0x010 +class ClientServerCfgPage extends $pb.GeneratedMessage { + factory ClientServerCfgPage({ + $core.int? notifications, + }) { + final $result = create(); + if (notifications != null) { + $result.notifications = notifications; + } + return $result; + } + ClientServerCfgPage._() : super(); + factory ClientServerCfgPage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ClientServerCfgPage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ClientServerCfgPage', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'notifications', $pb.PbFieldType.OU3) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ClientServerCfgPage clone() => ClientServerCfgPage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ClientServerCfgPage copyWith(void Function(ClientServerCfgPage) updates) => super.copyWith((message) => updates(message as ClientServerCfgPage)) as ClientServerCfgPage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ClientServerCfgPage create() => ClientServerCfgPage._(); + ClientServerCfgPage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ClientServerCfgPage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ClientServerCfgPage? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get notifications => $_getIZ(0); + @$pb.TagNumber(1) + set notifications($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasNotifications() => $_has(0); + @$pb.TagNumber(1) + void clearNotifications() => clearField(1); +} + +/// Page ID 0x020 +class TrainerSimulationParam extends $pb.GeneratedMessage { + factory TrainerSimulationParam({ + $core.int? configuredResistance, + $core.int? ergPower, + $core.int? averagingWindow, + $core.int? simulatedWind, + $core.int? simulatedGrade, + $core.int? simulatedRealGearRatio, + $core.int? simulatedVirtualGearRatio, + $core.int? simulatedCW, + $core.int? simulatedWheelDiameter, + $core.int? simulatedBikeMass, + $core.int? simulatedRiderMass, + $core.int? simulatedCRR, + $core.int? simulatedFrontalArea, + $core.int? simulatedEBrake, + }) { + final $result = create(); + if (configuredResistance != null) { + $result.configuredResistance = configuredResistance; + } + if (ergPower != null) { + $result.ergPower = ergPower; + } + if (averagingWindow != null) { + $result.averagingWindow = averagingWindow; + } + if (simulatedWind != null) { + $result.simulatedWind = simulatedWind; + } + if (simulatedGrade != null) { + $result.simulatedGrade = simulatedGrade; + } + if (simulatedRealGearRatio != null) { + $result.simulatedRealGearRatio = simulatedRealGearRatio; + } + if (simulatedVirtualGearRatio != null) { + $result.simulatedVirtualGearRatio = simulatedVirtualGearRatio; + } + if (simulatedCW != null) { + $result.simulatedCW = simulatedCW; + } + if (simulatedWheelDiameter != null) { + $result.simulatedWheelDiameter = simulatedWheelDiameter; + } + if (simulatedBikeMass != null) { + $result.simulatedBikeMass = simulatedBikeMass; + } + if (simulatedRiderMass != null) { + $result.simulatedRiderMass = simulatedRiderMass; + } + if (simulatedCRR != null) { + $result.simulatedCRR = simulatedCRR; + } + if (simulatedFrontalArea != null) { + $result.simulatedFrontalArea = simulatedFrontalArea; + } + if (simulatedEBrake != null) { + $result.simulatedEBrake = simulatedEBrake; + } + return $result; + } + TrainerSimulationParam._() : super(); + factory TrainerSimulationParam.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory TrainerSimulationParam.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'TrainerSimulationParam', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'configuredResistance', $pb.PbFieldType.OU3, protoName: 'configuredResistance') + ..a<$core.int>(2, _omitFieldNames ? '' : 'ergPower', $pb.PbFieldType.OU3, protoName: 'ergPower') + ..a<$core.int>(3, _omitFieldNames ? '' : 'averagingWindow', $pb.PbFieldType.OU3, protoName: 'averagingWindow') + ..a<$core.int>(4, _omitFieldNames ? '' : 'simulatedWind', $pb.PbFieldType.OS3, protoName: 'simulatedWind') + ..a<$core.int>(5, _omitFieldNames ? '' : 'simulatedGrade', $pb.PbFieldType.OS3, protoName: 'simulatedGrade') + ..a<$core.int>(6, _omitFieldNames ? '' : 'simulatedRealGearRatio', $pb.PbFieldType.OU3, protoName: 'simulatedRealGearRatio') + ..a<$core.int>(7, _omitFieldNames ? '' : 'simulatedVirtualGearRatio', $pb.PbFieldType.OU3, protoName: 'simulatedVirtualGearRatio') + ..a<$core.int>(8, _omitFieldNames ? '' : 'simulatedCW', $pb.PbFieldType.OU3, protoName: 'simulatedCW') + ..a<$core.int>(9, _omitFieldNames ? '' : 'simulatedWheelDiameter', $pb.PbFieldType.OU3, protoName: 'simulatedWheelDiameter') + ..a<$core.int>(10, _omitFieldNames ? '' : 'simulatedBikeMass', $pb.PbFieldType.OU3, protoName: 'simulatedBikeMass') + ..a<$core.int>(11, _omitFieldNames ? '' : 'simulatedRiderMass', $pb.PbFieldType.OU3, protoName: 'simulatedRiderMass') + ..a<$core.int>(12, _omitFieldNames ? '' : 'simulatedCRR', $pb.PbFieldType.OU3, protoName: 'simulatedCRR') + ..a<$core.int>(13, _omitFieldNames ? '' : 'simulatedFrontalArea', $pb.PbFieldType.OU3, protoName: 'simulatedFrontalArea') + ..a<$core.int>(14, _omitFieldNames ? '' : 'simulatedEBrake', $pb.PbFieldType.OU3, protoName: 'simulatedEBrake') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + TrainerSimulationParam clone() => TrainerSimulationParam()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + TrainerSimulationParam copyWith(void Function(TrainerSimulationParam) updates) => super.copyWith((message) => updates(message as TrainerSimulationParam)) as TrainerSimulationParam; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static TrainerSimulationParam create() => TrainerSimulationParam._(); + TrainerSimulationParam createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static TrainerSimulationParam getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static TrainerSimulationParam? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get configuredResistance => $_getIZ(0); + @$pb.TagNumber(1) + set configuredResistance($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasConfiguredResistance() => $_has(0); + @$pb.TagNumber(1) + void clearConfiguredResistance() => clearField(1); + + @$pb.TagNumber(2) + $core.int get ergPower => $_getIZ(1); + @$pb.TagNumber(2) + set ergPower($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasErgPower() => $_has(1); + @$pb.TagNumber(2) + void clearErgPower() => clearField(2); + + @$pb.TagNumber(3) + $core.int get averagingWindow => $_getIZ(2); + @$pb.TagNumber(3) + set averagingWindow($core.int v) { $_setUnsignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasAveragingWindow() => $_has(2); + @$pb.TagNumber(3) + void clearAveragingWindow() => clearField(3); + + @$pb.TagNumber(4) + $core.int get simulatedWind => $_getIZ(3); + @$pb.TagNumber(4) + set simulatedWind($core.int v) { $_setSignedInt32(3, v); } + @$pb.TagNumber(4) + $core.bool hasSimulatedWind() => $_has(3); + @$pb.TagNumber(4) + void clearSimulatedWind() => clearField(4); + + @$pb.TagNumber(5) + $core.int get simulatedGrade => $_getIZ(4); + @$pb.TagNumber(5) + set simulatedGrade($core.int v) { $_setSignedInt32(4, v); } + @$pb.TagNumber(5) + $core.bool hasSimulatedGrade() => $_has(4); + @$pb.TagNumber(5) + void clearSimulatedGrade() => clearField(5); + + @$pb.TagNumber(6) + $core.int get simulatedRealGearRatio => $_getIZ(5); + @$pb.TagNumber(6) + set simulatedRealGearRatio($core.int v) { $_setUnsignedInt32(5, v); } + @$pb.TagNumber(6) + $core.bool hasSimulatedRealGearRatio() => $_has(5); + @$pb.TagNumber(6) + void clearSimulatedRealGearRatio() => clearField(6); + + @$pb.TagNumber(7) + $core.int get simulatedVirtualGearRatio => $_getIZ(6); + @$pb.TagNumber(7) + set simulatedVirtualGearRatio($core.int v) { $_setUnsignedInt32(6, v); } + @$pb.TagNumber(7) + $core.bool hasSimulatedVirtualGearRatio() => $_has(6); + @$pb.TagNumber(7) + void clearSimulatedVirtualGearRatio() => clearField(7); + + @$pb.TagNumber(8) + $core.int get simulatedCW => $_getIZ(7); + @$pb.TagNumber(8) + set simulatedCW($core.int v) { $_setUnsignedInt32(7, v); } + @$pb.TagNumber(8) + $core.bool hasSimulatedCW() => $_has(7); + @$pb.TagNumber(8) + void clearSimulatedCW() => clearField(8); + + @$pb.TagNumber(9) + $core.int get simulatedWheelDiameter => $_getIZ(8); + @$pb.TagNumber(9) + set simulatedWheelDiameter($core.int v) { $_setUnsignedInt32(8, v); } + @$pb.TagNumber(9) + $core.bool hasSimulatedWheelDiameter() => $_has(8); + @$pb.TagNumber(9) + void clearSimulatedWheelDiameter() => clearField(9); + + @$pb.TagNumber(10) + $core.int get simulatedBikeMass => $_getIZ(9); + @$pb.TagNumber(10) + set simulatedBikeMass($core.int v) { $_setUnsignedInt32(9, v); } + @$pb.TagNumber(10) + $core.bool hasSimulatedBikeMass() => $_has(9); + @$pb.TagNumber(10) + void clearSimulatedBikeMass() => clearField(10); + + @$pb.TagNumber(11) + $core.int get simulatedRiderMass => $_getIZ(10); + @$pb.TagNumber(11) + set simulatedRiderMass($core.int v) { $_setUnsignedInt32(10, v); } + @$pb.TagNumber(11) + $core.bool hasSimulatedRiderMass() => $_has(10); + @$pb.TagNumber(11) + void clearSimulatedRiderMass() => clearField(11); + + @$pb.TagNumber(12) + $core.int get simulatedCRR => $_getIZ(11); + @$pb.TagNumber(12) + set simulatedCRR($core.int v) { $_setUnsignedInt32(11, v); } + @$pb.TagNumber(12) + $core.bool hasSimulatedCRR() => $_has(11); + @$pb.TagNumber(12) + void clearSimulatedCRR() => clearField(12); + + @$pb.TagNumber(13) + $core.int get simulatedFrontalArea => $_getIZ(12); + @$pb.TagNumber(13) + set simulatedFrontalArea($core.int v) { $_setUnsignedInt32(12, v); } + @$pb.TagNumber(13) + $core.bool hasSimulatedFrontalArea() => $_has(12); + @$pb.TagNumber(13) + void clearSimulatedFrontalArea() => clearField(13); + + @$pb.TagNumber(14) + $core.int get simulatedEBrake => $_getIZ(13); + @$pb.TagNumber(14) + set simulatedEBrake($core.int v) { $_setUnsignedInt32(13, v); } + @$pb.TagNumber(14) + $core.bool hasSimulatedEBrake() => $_has(13); + @$pb.TagNumber(14) + void clearSimulatedEBrake() => clearField(14); +} + +/// Page ID 0x022 +class TrainerOptions extends $pb.GeneratedMessage { + factory TrainerOptions({ + $core.bool? highSpeedDataEnabled, + $core.bool? ergPowerSmoothingEnabled, + $core.int? virtualShiftingMode, + }) { + final $result = create(); + if (highSpeedDataEnabled != null) { + $result.highSpeedDataEnabled = highSpeedDataEnabled; + } + if (ergPowerSmoothingEnabled != null) { + $result.ergPowerSmoothingEnabled = ergPowerSmoothingEnabled; + } + if (virtualShiftingMode != null) { + $result.virtualShiftingMode = virtualShiftingMode; + } + return $result; + } + TrainerOptions._() : super(); + factory TrainerOptions.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory TrainerOptions.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'TrainerOptions', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'highSpeedDataEnabled', protoName: 'highSpeedDataEnabled') + ..aOB(2, _omitFieldNames ? '' : 'ergPowerSmoothingEnabled', protoName: 'ergPowerSmoothingEnabled') + ..a<$core.int>(3, _omitFieldNames ? '' : 'virtualShiftingMode', $pb.PbFieldType.OU3, protoName: 'virtualShiftingMode') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + TrainerOptions clone() => TrainerOptions()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + TrainerOptions copyWith(void Function(TrainerOptions) updates) => super.copyWith((message) => updates(message as TrainerOptions)) as TrainerOptions; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static TrainerOptions create() => TrainerOptions._(); + TrainerOptions createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static TrainerOptions getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static TrainerOptions? _defaultInstance; + + @$pb.TagNumber(1) + $core.bool get highSpeedDataEnabled => $_getBF(0); + @$pb.TagNumber(1) + set highSpeedDataEnabled($core.bool v) { $_setBool(0, v); } + @$pb.TagNumber(1) + $core.bool hasHighSpeedDataEnabled() => $_has(0); + @$pb.TagNumber(1) + void clearHighSpeedDataEnabled() => clearField(1); + + @$pb.TagNumber(2) + $core.bool get ergPowerSmoothingEnabled => $_getBF(1); + @$pb.TagNumber(2) + set ergPowerSmoothingEnabled($core.bool v) { $_setBool(1, v); } + @$pb.TagNumber(2) + $core.bool hasErgPowerSmoothingEnabled() => $_has(1); + @$pb.TagNumber(2) + void clearErgPowerSmoothingEnabled() => clearField(2); + + @$pb.TagNumber(3) + $core.int get virtualShiftingMode => $_getIZ(2); + @$pb.TagNumber(3) + set virtualShiftingMode($core.int v) { $_setUnsignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasVirtualShiftingMode() => $_has(2); + @$pb.TagNumber(3) + void clearVirtualShiftingMode() => clearField(3); +} + +/// Page ID 0x020 and 0x022 (Get Response) +class TrainerCfgPage extends $pb.GeneratedMessage { + factory TrainerCfgPage({ + TrainerMode? trainerMode, + $core.int? configuredResistance, + $core.int? ergPower, + $core.int? averagingWindow, + $core.int? simulatedWind, + $core.int? simulatedGrade, + $core.int? simulatedRealGearRatio, + $core.int? simulatedVirtualGearRatio, + $core.int? simulatedCW, + $core.int? simulatedWheelDiameter, + $core.int? simulatedBikeMass, + $core.int? simulatedRiderMass, + $core.int? simulatedCRR, + $core.int? simulatedFrontalArea, + $core.int? simulatedEBrake, + $core.bool? highSpeedDataEnabled, + $core.bool? ergPowerSmoothingEnabled, + $core.int? virtualShiftingMode, + }) { + final $result = create(); + if (trainerMode != null) { + $result.trainerMode = trainerMode; + } + if (configuredResistance != null) { + $result.configuredResistance = configuredResistance; + } + if (ergPower != null) { + $result.ergPower = ergPower; + } + if (averagingWindow != null) { + $result.averagingWindow = averagingWindow; + } + if (simulatedWind != null) { + $result.simulatedWind = simulatedWind; + } + if (simulatedGrade != null) { + $result.simulatedGrade = simulatedGrade; + } + if (simulatedRealGearRatio != null) { + $result.simulatedRealGearRatio = simulatedRealGearRatio; + } + if (simulatedVirtualGearRatio != null) { + $result.simulatedVirtualGearRatio = simulatedVirtualGearRatio; + } + if (simulatedCW != null) { + $result.simulatedCW = simulatedCW; + } + if (simulatedWheelDiameter != null) { + $result.simulatedWheelDiameter = simulatedWheelDiameter; + } + if (simulatedBikeMass != null) { + $result.simulatedBikeMass = simulatedBikeMass; + } + if (simulatedRiderMass != null) { + $result.simulatedRiderMass = simulatedRiderMass; + } + if (simulatedCRR != null) { + $result.simulatedCRR = simulatedCRR; + } + if (simulatedFrontalArea != null) { + $result.simulatedFrontalArea = simulatedFrontalArea; + } + if (simulatedEBrake != null) { + $result.simulatedEBrake = simulatedEBrake; + } + if (highSpeedDataEnabled != null) { + $result.highSpeedDataEnabled = highSpeedDataEnabled; + } + if (ergPowerSmoothingEnabled != null) { + $result.ergPowerSmoothingEnabled = ergPowerSmoothingEnabled; + } + if (virtualShiftingMode != null) { + $result.virtualShiftingMode = virtualShiftingMode; + } + return $result; + } + TrainerCfgPage._() : super(); + factory TrainerCfgPage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory TrainerCfgPage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'TrainerCfgPage', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'trainerMode', $pb.PbFieldType.OE, protoName: 'trainerMode', defaultOrMaker: TrainerMode.MODE_UNKNOWN, valueOf: TrainerMode.valueOf, enumValues: TrainerMode.values) + ..a<$core.int>(2, _omitFieldNames ? '' : 'configuredResistance', $pb.PbFieldType.OU3, protoName: 'configuredResistance') + ..a<$core.int>(3, _omitFieldNames ? '' : 'ergPower', $pb.PbFieldType.OU3, protoName: 'ergPower') + ..a<$core.int>(4, _omitFieldNames ? '' : 'averagingWindow', $pb.PbFieldType.OU3, protoName: 'averagingWindow') + ..a<$core.int>(5, _omitFieldNames ? '' : 'simulatedWind', $pb.PbFieldType.OS3, protoName: 'simulatedWind') + ..a<$core.int>(6, _omitFieldNames ? '' : 'simulatedGrade', $pb.PbFieldType.OS3, protoName: 'simulatedGrade') + ..a<$core.int>(7, _omitFieldNames ? '' : 'simulatedRealGearRatio', $pb.PbFieldType.OU3, protoName: 'simulatedRealGearRatio') + ..a<$core.int>(8, _omitFieldNames ? '' : 'simulatedVirtualGearRatio', $pb.PbFieldType.OU3, protoName: 'simulatedVirtualGearRatio') + ..a<$core.int>(9, _omitFieldNames ? '' : 'simulatedCW', $pb.PbFieldType.OU3, protoName: 'simulatedCW') + ..a<$core.int>(10, _omitFieldNames ? '' : 'simulatedWheelDiameter', $pb.PbFieldType.OU3, protoName: 'simulatedWheelDiameter') + ..a<$core.int>(11, _omitFieldNames ? '' : 'simulatedBikeMass', $pb.PbFieldType.OU3, protoName: 'simulatedBikeMass') + ..a<$core.int>(12, _omitFieldNames ? '' : 'simulatedRiderMass', $pb.PbFieldType.OU3, protoName: 'simulatedRiderMass') + ..a<$core.int>(13, _omitFieldNames ? '' : 'simulatedCRR', $pb.PbFieldType.OU3, protoName: 'simulatedCRR') + ..a<$core.int>(14, _omitFieldNames ? '' : 'simulatedFrontalArea', $pb.PbFieldType.OU3, protoName: 'simulatedFrontalArea') + ..a<$core.int>(15, _omitFieldNames ? '' : 'simulatedEBrake', $pb.PbFieldType.OU3, protoName: 'simulatedEBrake') + ..aOB(16, _omitFieldNames ? '' : 'highSpeedDataEnabled', protoName: 'highSpeedDataEnabled') + ..aOB(17, _omitFieldNames ? '' : 'ergPowerSmoothingEnabled', protoName: 'ergPowerSmoothingEnabled') + ..a<$core.int>(18, _omitFieldNames ? '' : 'virtualShiftingMode', $pb.PbFieldType.OU3, protoName: 'virtualShiftingMode') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + TrainerCfgPage clone() => TrainerCfgPage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + TrainerCfgPage copyWith(void Function(TrainerCfgPage) updates) => super.copyWith((message) => updates(message as TrainerCfgPage)) as TrainerCfgPage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static TrainerCfgPage create() => TrainerCfgPage._(); + TrainerCfgPage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static TrainerCfgPage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static TrainerCfgPage? _defaultInstance; + + @$pb.TagNumber(1) + TrainerMode get trainerMode => $_getN(0); + @$pb.TagNumber(1) + set trainerMode(TrainerMode v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasTrainerMode() => $_has(0); + @$pb.TagNumber(1) + void clearTrainerMode() => clearField(1); + + @$pb.TagNumber(2) + $core.int get configuredResistance => $_getIZ(1); + @$pb.TagNumber(2) + set configuredResistance($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasConfiguredResistance() => $_has(1); + @$pb.TagNumber(2) + void clearConfiguredResistance() => clearField(2); + + @$pb.TagNumber(3) + $core.int get ergPower => $_getIZ(2); + @$pb.TagNumber(3) + set ergPower($core.int v) { $_setUnsignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasErgPower() => $_has(2); + @$pb.TagNumber(3) + void clearErgPower() => clearField(3); + + @$pb.TagNumber(4) + $core.int get averagingWindow => $_getIZ(3); + @$pb.TagNumber(4) + set averagingWindow($core.int v) { $_setUnsignedInt32(3, v); } + @$pb.TagNumber(4) + $core.bool hasAveragingWindow() => $_has(3); + @$pb.TagNumber(4) + void clearAveragingWindow() => clearField(4); + + @$pb.TagNumber(5) + $core.int get simulatedWind => $_getIZ(4); + @$pb.TagNumber(5) + set simulatedWind($core.int v) { $_setSignedInt32(4, v); } + @$pb.TagNumber(5) + $core.bool hasSimulatedWind() => $_has(4); + @$pb.TagNumber(5) + void clearSimulatedWind() => clearField(5); + + @$pb.TagNumber(6) + $core.int get simulatedGrade => $_getIZ(5); + @$pb.TagNumber(6) + set simulatedGrade($core.int v) { $_setSignedInt32(5, v); } + @$pb.TagNumber(6) + $core.bool hasSimulatedGrade() => $_has(5); + @$pb.TagNumber(6) + void clearSimulatedGrade() => clearField(6); + + @$pb.TagNumber(7) + $core.int get simulatedRealGearRatio => $_getIZ(6); + @$pb.TagNumber(7) + set simulatedRealGearRatio($core.int v) { $_setUnsignedInt32(6, v); } + @$pb.TagNumber(7) + $core.bool hasSimulatedRealGearRatio() => $_has(6); + @$pb.TagNumber(7) + void clearSimulatedRealGearRatio() => clearField(7); + + @$pb.TagNumber(8) + $core.int get simulatedVirtualGearRatio => $_getIZ(7); + @$pb.TagNumber(8) + set simulatedVirtualGearRatio($core.int v) { $_setUnsignedInt32(7, v); } + @$pb.TagNumber(8) + $core.bool hasSimulatedVirtualGearRatio() => $_has(7); + @$pb.TagNumber(8) + void clearSimulatedVirtualGearRatio() => clearField(8); + + @$pb.TagNumber(9) + $core.int get simulatedCW => $_getIZ(8); + @$pb.TagNumber(9) + set simulatedCW($core.int v) { $_setUnsignedInt32(8, v); } + @$pb.TagNumber(9) + $core.bool hasSimulatedCW() => $_has(8); + @$pb.TagNumber(9) + void clearSimulatedCW() => clearField(9); + + @$pb.TagNumber(10) + $core.int get simulatedWheelDiameter => $_getIZ(9); + @$pb.TagNumber(10) + set simulatedWheelDiameter($core.int v) { $_setUnsignedInt32(9, v); } + @$pb.TagNumber(10) + $core.bool hasSimulatedWheelDiameter() => $_has(9); + @$pb.TagNumber(10) + void clearSimulatedWheelDiameter() => clearField(10); + + @$pb.TagNumber(11) + $core.int get simulatedBikeMass => $_getIZ(10); + @$pb.TagNumber(11) + set simulatedBikeMass($core.int v) { $_setUnsignedInt32(10, v); } + @$pb.TagNumber(11) + $core.bool hasSimulatedBikeMass() => $_has(10); + @$pb.TagNumber(11) + void clearSimulatedBikeMass() => clearField(11); + + @$pb.TagNumber(12) + $core.int get simulatedRiderMass => $_getIZ(11); + @$pb.TagNumber(12) + set simulatedRiderMass($core.int v) { $_setUnsignedInt32(11, v); } + @$pb.TagNumber(12) + $core.bool hasSimulatedRiderMass() => $_has(11); + @$pb.TagNumber(12) + void clearSimulatedRiderMass() => clearField(12); + + @$pb.TagNumber(13) + $core.int get simulatedCRR => $_getIZ(12); + @$pb.TagNumber(13) + set simulatedCRR($core.int v) { $_setUnsignedInt32(12, v); } + @$pb.TagNumber(13) + $core.bool hasSimulatedCRR() => $_has(12); + @$pb.TagNumber(13) + void clearSimulatedCRR() => clearField(13); + + @$pb.TagNumber(14) + $core.int get simulatedFrontalArea => $_getIZ(13); + @$pb.TagNumber(14) + set simulatedFrontalArea($core.int v) { $_setUnsignedInt32(13, v); } + @$pb.TagNumber(14) + $core.bool hasSimulatedFrontalArea() => $_has(13); + @$pb.TagNumber(14) + void clearSimulatedFrontalArea() => clearField(14); + + @$pb.TagNumber(15) + $core.int get simulatedEBrake => $_getIZ(14); + @$pb.TagNumber(15) + set simulatedEBrake($core.int v) { $_setUnsignedInt32(14, v); } + @$pb.TagNumber(15) + $core.bool hasSimulatedEBrake() => $_has(14); + @$pb.TagNumber(15) + void clearSimulatedEBrake() => clearField(15); + + @$pb.TagNumber(16) + $core.bool get highSpeedDataEnabled => $_getBF(15); + @$pb.TagNumber(16) + set highSpeedDataEnabled($core.bool v) { $_setBool(15, v); } + @$pb.TagNumber(16) + $core.bool hasHighSpeedDataEnabled() => $_has(15); + @$pb.TagNumber(16) + void clearHighSpeedDataEnabled() => clearField(16); + + @$pb.TagNumber(17) + $core.bool get ergPowerSmoothingEnabled => $_getBF(16); + @$pb.TagNumber(17) + set ergPowerSmoothingEnabled($core.bool v) { $_setBool(16, v); } + @$pb.TagNumber(17) + $core.bool hasErgPowerSmoothingEnabled() => $_has(16); + @$pb.TagNumber(17) + void clearErgPowerSmoothingEnabled() => clearField(17); + + @$pb.TagNumber(18) + $core.int get virtualShiftingMode => $_getIZ(17); + @$pb.TagNumber(18) + set virtualShiftingMode($core.int v) { $_setUnsignedInt32(17, v); } + @$pb.TagNumber(18) + $core.bool hasVirtualShiftingMode() => $_has(17); + @$pb.TagNumber(18) + void clearVirtualShiftingMode() => clearField(18); +} + +/// Page ID 0x021 +class TrainerGearIndexConfigPage extends $pb.GeneratedMessage { + factory TrainerGearIndexConfigPage({ + $core.int? frontGearIdx, + $core.int? frontGearIdxMax, + $core.int? frontGearIdxMin, + $core.int? rearGearIdx, + $core.int? rearGearIdxMax, + $core.int? rearGearIdxMin, + }) { + final $result = create(); + if (frontGearIdx != null) { + $result.frontGearIdx = frontGearIdx; + } + if (frontGearIdxMax != null) { + $result.frontGearIdxMax = frontGearIdxMax; + } + if (frontGearIdxMin != null) { + $result.frontGearIdxMin = frontGearIdxMin; + } + if (rearGearIdx != null) { + $result.rearGearIdx = rearGearIdx; + } + if (rearGearIdxMax != null) { + $result.rearGearIdxMax = rearGearIdxMax; + } + if (rearGearIdxMin != null) { + $result.rearGearIdxMin = rearGearIdxMin; + } + return $result; + } + TrainerGearIndexConfigPage._() : super(); + factory TrainerGearIndexConfigPage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory TrainerGearIndexConfigPage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'TrainerGearIndexConfigPage', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'frontGearIdx', $pb.PbFieldType.OU3, protoName: 'frontGearIdx') + ..a<$core.int>(2, _omitFieldNames ? '' : 'frontGearIdxMax', $pb.PbFieldType.OU3, protoName: 'frontGearIdxMax') + ..a<$core.int>(3, _omitFieldNames ? '' : 'frontGearIdxMin', $pb.PbFieldType.OU3, protoName: 'frontGearIdxMin') + ..a<$core.int>(4, _omitFieldNames ? '' : 'rearGearIdx', $pb.PbFieldType.OU3, protoName: 'rearGearIdx') + ..a<$core.int>(5, _omitFieldNames ? '' : 'rearGearIdxMax', $pb.PbFieldType.OU3, protoName: 'rearGearIdxMax') + ..a<$core.int>(6, _omitFieldNames ? '' : 'rearGearIdxMin', $pb.PbFieldType.OU3, protoName: 'rearGearIdxMin') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + TrainerGearIndexConfigPage clone() => TrainerGearIndexConfigPage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + TrainerGearIndexConfigPage copyWith(void Function(TrainerGearIndexConfigPage) updates) => super.copyWith((message) => updates(message as TrainerGearIndexConfigPage)) as TrainerGearIndexConfigPage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static TrainerGearIndexConfigPage create() => TrainerGearIndexConfigPage._(); + TrainerGearIndexConfigPage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static TrainerGearIndexConfigPage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static TrainerGearIndexConfigPage? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get frontGearIdx => $_getIZ(0); + @$pb.TagNumber(1) + set frontGearIdx($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasFrontGearIdx() => $_has(0); + @$pb.TagNumber(1) + void clearFrontGearIdx() => clearField(1); + + @$pb.TagNumber(2) + $core.int get frontGearIdxMax => $_getIZ(1); + @$pb.TagNumber(2) + set frontGearIdxMax($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasFrontGearIdxMax() => $_has(1); + @$pb.TagNumber(2) + void clearFrontGearIdxMax() => clearField(2); + + @$pb.TagNumber(3) + $core.int get frontGearIdxMin => $_getIZ(2); + @$pb.TagNumber(3) + set frontGearIdxMin($core.int v) { $_setUnsignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasFrontGearIdxMin() => $_has(2); + @$pb.TagNumber(3) + void clearFrontGearIdxMin() => clearField(3); + + @$pb.TagNumber(4) + $core.int get rearGearIdx => $_getIZ(3); + @$pb.TagNumber(4) + set rearGearIdx($core.int v) { $_setUnsignedInt32(3, v); } + @$pb.TagNumber(4) + $core.bool hasRearGearIdx() => $_has(3); + @$pb.TagNumber(4) + void clearRearGearIdx() => clearField(4); + + @$pb.TagNumber(5) + $core.int get rearGearIdxMax => $_getIZ(4); + @$pb.TagNumber(5) + set rearGearIdxMax($core.int v) { $_setUnsignedInt32(4, v); } + @$pb.TagNumber(5) + $core.bool hasRearGearIdxMax() => $_has(4); + @$pb.TagNumber(5) + void clearRearGearIdxMax() => clearField(5); + + @$pb.TagNumber(6) + $core.int get rearGearIdxMin => $_getIZ(5); + @$pb.TagNumber(6) + set rearGearIdxMin($core.int v) { $_setUnsignedInt32(5, v); } + @$pb.TagNumber(6) + $core.bool hasRearGearIdxMin() => $_has(5); + @$pb.TagNumber(6) + void clearRearGearIdxMin() => clearField(6); +} + +/// Page ID 0x023 +class DeviceTiltConfigPage extends $pb.GeneratedMessage { + factory DeviceTiltConfigPage({ + $core.bool? tiltEnabled, + $core.int? tiltGradientMin, + $core.int? tiltGradientMax, + $core.int? tiltGradient, + }) { + final $result = create(); + if (tiltEnabled != null) { + $result.tiltEnabled = tiltEnabled; + } + if (tiltGradientMin != null) { + $result.tiltGradientMin = tiltGradientMin; + } + if (tiltGradientMax != null) { + $result.tiltGradientMax = tiltGradientMax; + } + if (tiltGradient != null) { + $result.tiltGradient = tiltGradient; + } + return $result; + } + DeviceTiltConfigPage._() : super(); + factory DeviceTiltConfigPage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DeviceTiltConfigPage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DeviceTiltConfigPage', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'tiltEnabled', protoName: 'tiltEnabled') + ..a<$core.int>(2, _omitFieldNames ? '' : 'tiltGradientMin', $pb.PbFieldType.OS3, protoName: 'tiltGradientMin') + ..a<$core.int>(3, _omitFieldNames ? '' : 'tiltGradientMax', $pb.PbFieldType.OS3, protoName: 'tiltGradientMax') + ..a<$core.int>(4, _omitFieldNames ? '' : 'tiltGradient', $pb.PbFieldType.OS3, protoName: 'tiltGradient') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + DeviceTiltConfigPage clone() => DeviceTiltConfigPage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DeviceTiltConfigPage copyWith(void Function(DeviceTiltConfigPage) updates) => super.copyWith((message) => updates(message as DeviceTiltConfigPage)) as DeviceTiltConfigPage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DeviceTiltConfigPage create() => DeviceTiltConfigPage._(); + DeviceTiltConfigPage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DeviceTiltConfigPage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DeviceTiltConfigPage? _defaultInstance; + + @$pb.TagNumber(1) + $core.bool get tiltEnabled => $_getBF(0); + @$pb.TagNumber(1) + set tiltEnabled($core.bool v) { $_setBool(0, v); } + @$pb.TagNumber(1) + $core.bool hasTiltEnabled() => $_has(0); + @$pb.TagNumber(1) + void clearTiltEnabled() => clearField(1); + + @$pb.TagNumber(2) + $core.int get tiltGradientMin => $_getIZ(1); + @$pb.TagNumber(2) + set tiltGradientMin($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasTiltGradientMin() => $_has(1); + @$pb.TagNumber(2) + void clearTiltGradientMin() => clearField(2); + + @$pb.TagNumber(3) + $core.int get tiltGradientMax => $_getIZ(2); + @$pb.TagNumber(3) + set tiltGradientMax($core.int v) { $_setSignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasTiltGradientMax() => $_has(2); + @$pb.TagNumber(3) + void clearTiltGradientMax() => clearField(3); + + @$pb.TagNumber(4) + $core.int get tiltGradient => $_getIZ(3); + @$pb.TagNumber(4) + set tiltGradient($core.int v) { $_setSignedInt32(3, v); } + @$pb.TagNumber(4) + $core.bool hasTiltGradient() => $_has(3); + @$pb.TagNumber(4) + void clearTiltGradient() => clearField(4); +} + +/// Page ID 0x040 +class ControllerInputConfigPage extends $pb.GeneratedMessage { + factory ControllerInputConfigPage({ + $core.int? supportedDigitalInputs, + $core.int? supportedAnalogInputs, + $core.Iterable? analogInputRange, + $core.Iterable? analogDeadZone, + }) { + final $result = create(); + if (supportedDigitalInputs != null) { + $result.supportedDigitalInputs = supportedDigitalInputs; + } + if (supportedAnalogInputs != null) { + $result.supportedAnalogInputs = supportedAnalogInputs; + } + if (analogInputRange != null) { + $result.analogInputRange.addAll(analogInputRange); + } + if (analogDeadZone != null) { + $result.analogDeadZone.addAll(analogDeadZone); + } + return $result; + } + ControllerInputConfigPage._() : super(); + factory ControllerInputConfigPage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ControllerInputConfigPage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ControllerInputConfigPage', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'supportedDigitalInputs', $pb.PbFieldType.OU3, protoName: 'supportedDigitalInputs') + ..a<$core.int>(2, _omitFieldNames ? '' : 'supportedAnalogInputs', $pb.PbFieldType.OU3, protoName: 'supportedAnalogInputs') + ..pc(3, _omitFieldNames ? '' : 'analogInputRange', $pb.PbFieldType.PM, protoName: 'analogInputRange', subBuilder: InputAnalogRange.create) + ..pc(4, _omitFieldNames ? '' : 'analogDeadZone', $pb.PbFieldType.PM, protoName: 'analogDeadZone', subBuilder: InputAnalogDeadzone.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ControllerInputConfigPage clone() => ControllerInputConfigPage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ControllerInputConfigPage copyWith(void Function(ControllerInputConfigPage) updates) => super.copyWith((message) => updates(message as ControllerInputConfigPage)) as ControllerInputConfigPage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ControllerInputConfigPage create() => ControllerInputConfigPage._(); + ControllerInputConfigPage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ControllerInputConfigPage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ControllerInputConfigPage? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get supportedDigitalInputs => $_getIZ(0); + @$pb.TagNumber(1) + set supportedDigitalInputs($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasSupportedDigitalInputs() => $_has(0); + @$pb.TagNumber(1) + void clearSupportedDigitalInputs() => clearField(1); + + @$pb.TagNumber(2) + $core.int get supportedAnalogInputs => $_getIZ(1); + @$pb.TagNumber(2) + set supportedAnalogInputs($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasSupportedAnalogInputs() => $_has(1); + @$pb.TagNumber(2) + void clearSupportedAnalogInputs() => clearField(2); + + @$pb.TagNumber(3) + $core.List get analogInputRange => $_getList(2); + + @$pb.TagNumber(4) + $core.List get analogDeadZone => $_getList(3); +} + +/// Page 0x041 +class IpTransportPage extends $pb.GeneratedMessage { + factory IpTransportPage({ + $core.int? ipv4Address, + $core.int? tcpPort, + }) { + final $result = create(); + if (ipv4Address != null) { + $result.ipv4Address = ipv4Address; + } + if (tcpPort != null) { + $result.tcpPort = tcpPort; + } + return $result; + } + IpTransportPage._() : super(); + factory IpTransportPage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory IpTransportPage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'IpTransportPage', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'ipv4Address', $pb.PbFieldType.OU3, protoName: 'ipv4Address') + ..a<$core.int>(2, _omitFieldNames ? '' : 'tcpPort', $pb.PbFieldType.OU3, protoName: 'tcpPort') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + IpTransportPage clone() => IpTransportPage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + IpTransportPage copyWith(void Function(IpTransportPage) updates) => super.copyWith((message) => updates(message as IpTransportPage)) as IpTransportPage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static IpTransportPage create() => IpTransportPage._(); + IpTransportPage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static IpTransportPage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static IpTransportPage? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get ipv4Address => $_getIZ(0); + @$pb.TagNumber(1) + set ipv4Address($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasIpv4Address() => $_has(0); + @$pb.TagNumber(1) + void clearIpv4Address() => clearField(1); + + @$pb.TagNumber(2) + $core.int get tcpPort => $_getIZ(1); + @$pb.TagNumber(2) + set tcpPort($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasTcpPort() => $_has(1); + @$pb.TagNumber(2) + void clearTcpPort() => clearField(2); +} + +/// Page 0x042 +class WifiConfigurationPage extends $pb.GeneratedMessage { + factory WifiConfigurationPage({ + $core.bool? wifiEnabled, + $core.int? wifiStatus, + $core.List<$core.int>? wifiSsid, + $core.int? wifiBand, + $core.int? wifiRssi, + $core.List<$core.int>? wifiRegionCode, + }) { + final $result = create(); + if (wifiEnabled != null) { + $result.wifiEnabled = wifiEnabled; + } + if (wifiStatus != null) { + $result.wifiStatus = wifiStatus; + } + if (wifiSsid != null) { + $result.wifiSsid = wifiSsid; + } + if (wifiBand != null) { + $result.wifiBand = wifiBand; + } + if (wifiRssi != null) { + $result.wifiRssi = wifiRssi; + } + if (wifiRegionCode != null) { + $result.wifiRegionCode = wifiRegionCode; + } + return $result; + } + WifiConfigurationPage._() : super(); + factory WifiConfigurationPage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory WifiConfigurationPage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'WifiConfigurationPage', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'wifiEnabled', protoName: 'wifiEnabled') + ..a<$core.int>(2, _omitFieldNames ? '' : 'wifiStatus', $pb.PbFieldType.OU3, protoName: 'wifiStatus') + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'wifiSsid', $pb.PbFieldType.OY, protoName: 'wifiSsid') + ..a<$core.int>(4, _omitFieldNames ? '' : 'wifiBand', $pb.PbFieldType.OU3, protoName: 'wifiBand') + ..a<$core.int>(5, _omitFieldNames ? '' : 'wifiRssi', $pb.PbFieldType.OS3, protoName: 'wifiRssi') + ..a<$core.List<$core.int>>(6, _omitFieldNames ? '' : 'wifiRegionCode', $pb.PbFieldType.OY, protoName: 'wifiRegionCode') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + WifiConfigurationPage clone() => WifiConfigurationPage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + WifiConfigurationPage copyWith(void Function(WifiConfigurationPage) updates) => super.copyWith((message) => updates(message as WifiConfigurationPage)) as WifiConfigurationPage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static WifiConfigurationPage create() => WifiConfigurationPage._(); + WifiConfigurationPage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static WifiConfigurationPage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static WifiConfigurationPage? _defaultInstance; + + @$pb.TagNumber(1) + $core.bool get wifiEnabled => $_getBF(0); + @$pb.TagNumber(1) + set wifiEnabled($core.bool v) { $_setBool(0, v); } + @$pb.TagNumber(1) + $core.bool hasWifiEnabled() => $_has(0); + @$pb.TagNumber(1) + void clearWifiEnabled() => clearField(1); + + @$pb.TagNumber(2) + $core.int get wifiStatus => $_getIZ(1); + @$pb.TagNumber(2) + set wifiStatus($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasWifiStatus() => $_has(1); + @$pb.TagNumber(2) + void clearWifiStatus() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get wifiSsid => $_getN(2); + @$pb.TagNumber(3) + set wifiSsid($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasWifiSsid() => $_has(2); + @$pb.TagNumber(3) + void clearWifiSsid() => clearField(3); + + @$pb.TagNumber(4) + $core.int get wifiBand => $_getIZ(3); + @$pb.TagNumber(4) + set wifiBand($core.int v) { $_setUnsignedInt32(3, v); } + @$pb.TagNumber(4) + $core.bool hasWifiBand() => $_has(3); + @$pb.TagNumber(4) + void clearWifiBand() => clearField(4); + + @$pb.TagNumber(5) + $core.int get wifiRssi => $_getIZ(4); + @$pb.TagNumber(5) + set wifiRssi($core.int v) { $_setSignedInt32(4, v); } + @$pb.TagNumber(5) + $core.bool hasWifiRssi() => $_has(4); + @$pb.TagNumber(5) + void clearWifiRssi() => clearField(5); + + @$pb.TagNumber(6) + $core.List<$core.int> get wifiRegionCode => $_getN(5); + @$pb.TagNumber(6) + set wifiRegionCode($core.List<$core.int> v) { $_setBytes(5, v); } + @$pb.TagNumber(6) + $core.bool hasWifiRegionCode() => $_has(5); + @$pb.TagNumber(6) + void clearWifiRegionCode() => clearField(6); +} + +/// Page ID 0x050 +class SensorRelayDataPage extends $pb.GeneratedMessage { + factory SensorRelayDataPage({ + $core.Iterable<$core.int>? supportedSensors, + $core.Iterable<$core.int>? pairedSensors, + }) { + final $result = create(); + if (supportedSensors != null) { + $result.supportedSensors.addAll(supportedSensors); + } + if (pairedSensors != null) { + $result.pairedSensors.addAll(pairedSensors); + } + return $result; + } + SensorRelayDataPage._() : super(); + factory SensorRelayDataPage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SensorRelayDataPage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SensorRelayDataPage', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..p<$core.int>(1, _omitFieldNames ? '' : 'supportedSensors', $pb.PbFieldType.PU3, protoName: 'supportedSensors') + ..p<$core.int>(2, _omitFieldNames ? '' : 'pairedSensors', $pb.PbFieldType.PU3, protoName: 'pairedSensors') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SensorRelayDataPage clone() => SensorRelayDataPage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SensorRelayDataPage copyWith(void Function(SensorRelayDataPage) updates) => super.copyWith((message) => updates(message as SensorRelayDataPage)) as SensorRelayDataPage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SensorRelayDataPage create() => SensorRelayDataPage._(); + SensorRelayDataPage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SensorRelayDataPage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SensorRelayDataPage? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get supportedSensors => $_getList(0); + + @$pb.TagNumber(2) + $core.List<$core.int> get pairedSensors => $_getList(1); +} + +/// OpCode 0x00 +class Get extends $pb.GeneratedMessage { + factory Get({ + $core.int? dataObjectId, + }) { + final $result = create(); + if (dataObjectId != null) { + $result.dataObjectId = dataObjectId; + } + return $result; + } + Get._() : super(); + factory Get.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Get.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Get', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'dataObjectId', $pb.PbFieldType.QU3, protoName: 'dataObjectId') + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Get clone() => Get()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Get copyWith(void Function(Get) updates) => super.copyWith((message) => updates(message as Get)) as Get; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Get create() => Get._(); + Get createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Get getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Get? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get dataObjectId => $_getIZ(0); + @$pb.TagNumber(1) + set dataObjectId($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasDataObjectId() => $_has(0); + @$pb.TagNumber(1) + void clearDataObjectId() => clearField(1); +} + +/// OpCode 0x01 +class DevInfo extends $pb.GeneratedMessage { + factory DevInfo({ + DevInfoPage? devInfo, + }) { + final $result = create(); + if (devInfo != null) { + $result.devInfo = devInfo; + } + return $result; + } + DevInfo._() : super(); + factory DevInfo.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DevInfo.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DevInfo', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..aQM(1, _omitFieldNames ? '' : 'devInfo', protoName: 'devInfo', subBuilder: DevInfoPage.create) + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + DevInfo clone() => DevInfo()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DevInfo copyWith(void Function(DevInfo) updates) => super.copyWith((message) => updates(message as DevInfo)) as DevInfo; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DevInfo create() => DevInfo._(); + DevInfo createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DevInfo getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DevInfo? _defaultInstance; + + @$pb.TagNumber(1) + DevInfoPage get devInfo => $_getN(0); + @$pb.TagNumber(1) + set devInfo(DevInfoPage v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasDevInfo() => $_has(0); + @$pb.TagNumber(1) + void clearDevInfo() => clearField(1); + @$pb.TagNumber(1) + DevInfoPage ensureDevInfo() => $_ensure(0); +} + +enum BleSecurityRequest_Request { + startSecureConnection, + notSet +} + +/// OpCode 0x02 +class BleSecurityRequest extends $pb.GeneratedMessage { + factory BleSecurityRequest({ + $core.bool? startSecureConnection, + }) { + final $result = create(); + if (startSecureConnection != null) { + $result.startSecureConnection = startSecureConnection; + } + return $result; + } + BleSecurityRequest._() : super(); + factory BleSecurityRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory BleSecurityRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, BleSecurityRequest_Request> _BleSecurityRequest_RequestByTag = { + 1 : BleSecurityRequest_Request.startSecureConnection, + 0 : BleSecurityRequest_Request.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BleSecurityRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..oo(0, [1]) + ..aOB(1, _omitFieldNames ? '' : 'startSecureConnection', protoName: 'startSecureConnection') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + BleSecurityRequest clone() => BleSecurityRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + BleSecurityRequest copyWith(void Function(BleSecurityRequest) updates) => super.copyWith((message) => updates(message as BleSecurityRequest)) as BleSecurityRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static BleSecurityRequest create() => BleSecurityRequest._(); + BleSecurityRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static BleSecurityRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static BleSecurityRequest? _defaultInstance; + + BleSecurityRequest_Request whichRequest() => _BleSecurityRequest_RequestByTag[$_whichOneof(0)]!; + void clearRequest() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + $core.bool get startSecureConnection => $_getBF(0); + @$pb.TagNumber(1) + set startSecureConnection($core.bool v) { $_setBool(0, v); } + @$pb.TagNumber(1) + $core.bool hasStartSecureConnection() => $_has(0); + @$pb.TagNumber(1) + void clearStartSecureConnection() => clearField(1); +} + +/// OpCode 0x03 +class TrainerNotification extends $pb.GeneratedMessage { + factory TrainerNotification({ + $core.int? power, + $core.int? cadence, + $core.int? bikeSpeed, + $core.int? averagedPower, + $core.int? wheelSpeed, + $core.int? calculatedRealGearRatio, + }) { + final $result = create(); + if (power != null) { + $result.power = power; + } + if (cadence != null) { + $result.cadence = cadence; + } + if (bikeSpeed != null) { + $result.bikeSpeed = bikeSpeed; + } + if (averagedPower != null) { + $result.averagedPower = averagedPower; + } + if (wheelSpeed != null) { + $result.wheelSpeed = wheelSpeed; + } + if (calculatedRealGearRatio != null) { + $result.calculatedRealGearRatio = calculatedRealGearRatio; + } + return $result; + } + TrainerNotification._() : super(); + factory TrainerNotification.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory TrainerNotification.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'TrainerNotification', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'power', $pb.PbFieldType.QU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'cadence', $pb.PbFieldType.QU3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'bikeSpeed', $pb.PbFieldType.QU3, protoName: 'bikeSpeed') + ..a<$core.int>(4, _omitFieldNames ? '' : 'averagedPower', $pb.PbFieldType.QU3, protoName: 'averagedPower') + ..a<$core.int>(5, _omitFieldNames ? '' : 'wheelSpeed', $pb.PbFieldType.QU3, protoName: 'wheelSpeed') + ..a<$core.int>(6, _omitFieldNames ? '' : 'calculatedRealGearRatio', $pb.PbFieldType.OU3, protoName: 'calculatedRealGearRatio') + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + TrainerNotification clone() => TrainerNotification()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + TrainerNotification copyWith(void Function(TrainerNotification) updates) => super.copyWith((message) => updates(message as TrainerNotification)) as TrainerNotification; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static TrainerNotification create() => TrainerNotification._(); + TrainerNotification createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static TrainerNotification getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static TrainerNotification? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get power => $_getIZ(0); + @$pb.TagNumber(1) + set power($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasPower() => $_has(0); + @$pb.TagNumber(1) + void clearPower() => clearField(1); + + @$pb.TagNumber(2) + $core.int get cadence => $_getIZ(1); + @$pb.TagNumber(2) + set cadence($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasCadence() => $_has(1); + @$pb.TagNumber(2) + void clearCadence() => clearField(2); + + @$pb.TagNumber(3) + $core.int get bikeSpeed => $_getIZ(2); + @$pb.TagNumber(3) + set bikeSpeed($core.int v) { $_setUnsignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasBikeSpeed() => $_has(2); + @$pb.TagNumber(3) + void clearBikeSpeed() => clearField(3); + + @$pb.TagNumber(4) + $core.int get averagedPower => $_getIZ(3); + @$pb.TagNumber(4) + set averagedPower($core.int v) { $_setUnsignedInt32(3, v); } + @$pb.TagNumber(4) + $core.bool hasAveragedPower() => $_has(3); + @$pb.TagNumber(4) + void clearAveragedPower() => clearField(4); + + @$pb.TagNumber(5) + $core.int get wheelSpeed => $_getIZ(4); + @$pb.TagNumber(5) + set wheelSpeed($core.int v) { $_setUnsignedInt32(4, v); } + @$pb.TagNumber(5) + $core.bool hasWheelSpeed() => $_has(4); + @$pb.TagNumber(5) + void clearWheelSpeed() => clearField(5); + + @$pb.TagNumber(6) + $core.int get calculatedRealGearRatio => $_getIZ(5); + @$pb.TagNumber(6) + set calculatedRealGearRatio($core.int v) { $_setUnsignedInt32(5, v); } + @$pb.TagNumber(6) + $core.bool hasCalculatedRealGearRatio() => $_has(5); + @$pb.TagNumber(6) + void clearCalculatedRealGearRatio() => clearField(6); +} + +enum TrainerConfigSet_Config { + configuredResistance, + ergPower, + envSim, + bikeSim, + averagingWindow, + trainerOptions, + notSet +} + +/// OpCode 0x04 +class TrainerConfigSet extends $pb.GeneratedMessage { + factory TrainerConfigSet({ + $core.int? configuredResistance, + $core.int? ergPower, + TrainerEnvSim? envSim, + TrainerBikeSim? bikeSim, + $core.int? averagingWindow, + TrainerOptions? trainerOptions, + }) { + final $result = create(); + if (configuredResistance != null) { + $result.configuredResistance = configuredResistance; + } + if (ergPower != null) { + $result.ergPower = ergPower; + } + if (envSim != null) { + $result.envSim = envSim; + } + if (bikeSim != null) { + $result.bikeSim = bikeSim; + } + if (averagingWindow != null) { + $result.averagingWindow = averagingWindow; + } + if (trainerOptions != null) { + $result.trainerOptions = trainerOptions; + } + return $result; + } + TrainerConfigSet._() : super(); + factory TrainerConfigSet.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory TrainerConfigSet.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, TrainerConfigSet_Config> _TrainerConfigSet_ConfigByTag = { + 2 : TrainerConfigSet_Config.configuredResistance, + 3 : TrainerConfigSet_Config.ergPower, + 4 : TrainerConfigSet_Config.envSim, + 5 : TrainerConfigSet_Config.bikeSim, + 6 : TrainerConfigSet_Config.averagingWindow, + 7 : TrainerConfigSet_Config.trainerOptions, + 0 : TrainerConfigSet_Config.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'TrainerConfigSet', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..oo(0, [2, 3, 4, 5, 6, 7]) + ..a<$core.int>(2, _omitFieldNames ? '' : 'configuredResistance', $pb.PbFieldType.OU3, protoName: 'configuredResistance') + ..a<$core.int>(3, _omitFieldNames ? '' : 'ergPower', $pb.PbFieldType.OU3, protoName: 'ergPower') + ..aOM(4, _omitFieldNames ? '' : 'envSim', protoName: 'envSim', subBuilder: TrainerEnvSim.create) + ..aOM(5, _omitFieldNames ? '' : 'bikeSim', protoName: 'bikeSim', subBuilder: TrainerBikeSim.create) + ..a<$core.int>(6, _omitFieldNames ? '' : 'averagingWindow', $pb.PbFieldType.OU3, protoName: 'averagingWindow') + ..aOM(7, _omitFieldNames ? '' : 'trainerOptions', protoName: 'trainerOptions', subBuilder: TrainerOptions.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + TrainerConfigSet clone() => TrainerConfigSet()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + TrainerConfigSet copyWith(void Function(TrainerConfigSet) updates) => super.copyWith((message) => updates(message as TrainerConfigSet)) as TrainerConfigSet; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static TrainerConfigSet create() => TrainerConfigSet._(); + TrainerConfigSet createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static TrainerConfigSet getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static TrainerConfigSet? _defaultInstance; + + TrainerConfigSet_Config whichConfig() => _TrainerConfigSet_ConfigByTag[$_whichOneof(0)]!; + void clearConfig() => clearField($_whichOneof(0)); + + @$pb.TagNumber(2) + $core.int get configuredResistance => $_getIZ(0); + @$pb.TagNumber(2) + set configuredResistance($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(2) + $core.bool hasConfiguredResistance() => $_has(0); + @$pb.TagNumber(2) + void clearConfiguredResistance() => clearField(2); + + @$pb.TagNumber(3) + $core.int get ergPower => $_getIZ(1); + @$pb.TagNumber(3) + set ergPower($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(3) + $core.bool hasErgPower() => $_has(1); + @$pb.TagNumber(3) + void clearErgPower() => clearField(3); + + @$pb.TagNumber(4) + TrainerEnvSim get envSim => $_getN(2); + @$pb.TagNumber(4) + set envSim(TrainerEnvSim v) { setField(4, v); } + @$pb.TagNumber(4) + $core.bool hasEnvSim() => $_has(2); + @$pb.TagNumber(4) + void clearEnvSim() => clearField(4); + @$pb.TagNumber(4) + TrainerEnvSim ensureEnvSim() => $_ensure(2); + + @$pb.TagNumber(5) + TrainerBikeSim get bikeSim => $_getN(3); + @$pb.TagNumber(5) + set bikeSim(TrainerBikeSim v) { setField(5, v); } + @$pb.TagNumber(5) + $core.bool hasBikeSim() => $_has(3); + @$pb.TagNumber(5) + void clearBikeSim() => clearField(5); + @$pb.TagNumber(5) + TrainerBikeSim ensureBikeSim() => $_ensure(3); + + @$pb.TagNumber(6) + $core.int get averagingWindow => $_getIZ(4); + @$pb.TagNumber(6) + set averagingWindow($core.int v) { $_setUnsignedInt32(4, v); } + @$pb.TagNumber(6) + $core.bool hasAveragingWindow() => $_has(4); + @$pb.TagNumber(6) + void clearAveragingWindow() => clearField(6); + + @$pb.TagNumber(7) + TrainerOptions get trainerOptions => $_getN(5); + @$pb.TagNumber(7) + set trainerOptions(TrainerOptions v) { setField(7, v); } + @$pb.TagNumber(7) + $core.bool hasTrainerOptions() => $_has(5); + @$pb.TagNumber(7) + void clearTrainerOptions() => clearField(7); + @$pb.TagNumber(7) + TrainerOptions ensureTrainerOptions() => $_ensure(5); +} + +/// OpCode 0x05 +class TrainerConfigStatus extends $pb.GeneratedMessage { + factory TrainerConfigStatus({ + TrainerCfgPage? trainerCfg, + }) { + final $result = create(); + if (trainerCfg != null) { + $result.trainerCfg = trainerCfg; + } + return $result; + } + TrainerConfigStatus._() : super(); + factory TrainerConfigStatus.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory TrainerConfigStatus.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'TrainerConfigStatus', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..aQM(1, _omitFieldNames ? '' : 'trainerCfg', protoName: 'trainerCfg', subBuilder: TrainerCfgPage.create) + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + TrainerConfigStatus clone() => TrainerConfigStatus()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + TrainerConfigStatus copyWith(void Function(TrainerConfigStatus) updates) => super.copyWith((message) => updates(message as TrainerConfigStatus)) as TrainerConfigStatus; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static TrainerConfigStatus create() => TrainerConfigStatus._(); + TrainerConfigStatus createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static TrainerConfigStatus getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static TrainerConfigStatus? _defaultInstance; + + @$pb.TagNumber(1) + TrainerCfgPage get trainerCfg => $_getN(0); + @$pb.TagNumber(1) + set trainerCfg(TrainerCfgPage v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasTrainerCfg() => $_has(0); + @$pb.TagNumber(1) + void clearTrainerCfg() => clearField(1); + @$pb.TagNumber(1) + TrainerCfgPage ensureTrainerCfg() => $_ensure(0); +} + +enum DevInfoSet_DevInfo { + deviceName, + notSet +} + +/// OpCode 0x0C +class DevInfoSet extends $pb.GeneratedMessage { + factory DevInfoSet({ + $core.List<$core.int>? deviceName, + }) { + final $result = create(); + if (deviceName != null) { + $result.deviceName = deviceName; + } + return $result; + } + DevInfoSet._() : super(); + factory DevInfoSet.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DevInfoSet.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, DevInfoSet_DevInfo> _DevInfoSet_DevInfoByTag = { + 1 : DevInfoSet_DevInfo.deviceName, + 0 : DevInfoSet_DevInfo.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DevInfoSet', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..oo(0, [1]) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'deviceName', $pb.PbFieldType.OY, protoName: 'deviceName') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + DevInfoSet clone() => DevInfoSet()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DevInfoSet copyWith(void Function(DevInfoSet) updates) => super.copyWith((message) => updates(message as DevInfoSet)) as DevInfoSet; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DevInfoSet create() => DevInfoSet._(); + DevInfoSet createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DevInfoSet getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DevInfoSet? _defaultInstance; + + DevInfoSet_DevInfo whichDevInfo() => _DevInfoSet_DevInfoByTag[$_whichOneof(0)]!; + void clearDevInfo() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + $core.List<$core.int> get deviceName => $_getN(0); + @$pb.TagNumber(1) + set deviceName($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasDeviceName() => $_has(0); + @$pb.TagNumber(1) + void clearDeviceName() => clearField(1); +} + +/// OpCode 0x18 +class Reset extends $pb.GeneratedMessage { + factory Reset({ + $core.int? resetProcedure, + }) { + final $result = create(); + if (resetProcedure != null) { + $result.resetProcedure = resetProcedure; + } + return $result; + } + Reset._() : super(); + factory Reset.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Reset.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Reset', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'resetProcedure', $pb.PbFieldType.OU3, protoName: 'resetProcedure') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Reset clone() => Reset()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Reset copyWith(void Function(Reset) updates) => super.copyWith((message) => updates(message as Reset)) as Reset; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Reset create() => Reset._(); + Reset createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Reset getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Reset? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get resetProcedure => $_getIZ(0); + @$pb.TagNumber(1) + set resetProcedure($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasResetProcedure() => $_has(0); + @$pb.TagNumber(1) + void clearResetProcedure() => clearField(1); +} + +enum BatteryNotification_Alert { + newChgState, + newPercLevel, + notSet +} + +/// OpCode 0x19 +class BatteryNotification extends $pb.GeneratedMessage { + factory BatteryNotification({ + ChargingState? newChgState, + $core.int? newPercLevel, + $core.int? source, + }) { + final $result = create(); + if (newChgState != null) { + $result.newChgState = newChgState; + } + if (newPercLevel != null) { + $result.newPercLevel = newPercLevel; + } + if (source != null) { + $result.source = source; + } + return $result; + } + BatteryNotification._() : super(); + factory BatteryNotification.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory BatteryNotification.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, BatteryNotification_Alert> _BatteryNotification_AlertByTag = { + 1 : BatteryNotification_Alert.newChgState, + 2 : BatteryNotification_Alert.newPercLevel, + 0 : BatteryNotification_Alert.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BatteryNotification', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..oo(0, [1, 2]) + ..e(1, _omitFieldNames ? '' : 'newChgState', $pb.PbFieldType.OE, protoName: 'newChgState', defaultOrMaker: ChargingState.CHARGING_IDLE, valueOf: ChargingState.valueOf, enumValues: ChargingState.values) + ..a<$core.int>(2, _omitFieldNames ? '' : 'newPercLevel', $pb.PbFieldType.OU3, protoName: 'newPercLevel') + ..a<$core.int>(3, _omitFieldNames ? '' : 'source', $pb.PbFieldType.OU3) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + BatteryNotification clone() => BatteryNotification()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + BatteryNotification copyWith(void Function(BatteryNotification) updates) => super.copyWith((message) => updates(message as BatteryNotification)) as BatteryNotification; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static BatteryNotification create() => BatteryNotification._(); + BatteryNotification createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static BatteryNotification getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static BatteryNotification? _defaultInstance; + + BatteryNotification_Alert whichAlert() => _BatteryNotification_AlertByTag[$_whichOneof(0)]!; + void clearAlert() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + ChargingState get newChgState => $_getN(0); + @$pb.TagNumber(1) + set newChgState(ChargingState v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasNewChgState() => $_has(0); + @$pb.TagNumber(1) + void clearNewChgState() => clearField(1); + + @$pb.TagNumber(2) + $core.int get newPercLevel => $_getIZ(1); + @$pb.TagNumber(2) + set newPercLevel($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasNewPercLevel() => $_has(1); + @$pb.TagNumber(2) + void clearNewPercLevel() => clearField(2); + + @$pb.TagNumber(3) + $core.int get source => $_getIZ(2); + @$pb.TagNumber(3) + set source($core.int v) { $_setUnsignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasSource() => $_has(2); + @$pb.TagNumber(3) + void clearSource() => clearField(3); +} + +/// OpCode 0x1A +class BatteryStatus extends $pb.GeneratedMessage { + factory BatteryStatus({ + ChargingState? chgState, + $core.int? percLevel, + $core.int? timeToEmpty, + $core.int? timeToFull, + }) { + final $result = create(); + if (chgState != null) { + $result.chgState = chgState; + } + if (percLevel != null) { + $result.percLevel = percLevel; + } + if (timeToEmpty != null) { + $result.timeToEmpty = timeToEmpty; + } + if (timeToFull != null) { + $result.timeToFull = timeToFull; + } + return $result; + } + BatteryStatus._() : super(); + factory BatteryStatus.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory BatteryStatus.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'BatteryStatus', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'chgState', $pb.PbFieldType.QE, protoName: 'chgState', defaultOrMaker: ChargingState.CHARGING_IDLE, valueOf: ChargingState.valueOf, enumValues: ChargingState.values) + ..a<$core.int>(2, _omitFieldNames ? '' : 'percLevel', $pb.PbFieldType.QU3, protoName: 'percLevel') + ..a<$core.int>(3, _omitFieldNames ? '' : 'timeToEmpty', $pb.PbFieldType.QU3, protoName: 'timeToEmpty') + ..a<$core.int>(4, _omitFieldNames ? '' : 'timeToFull', $pb.PbFieldType.QU3, protoName: 'timeToFull') + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + BatteryStatus clone() => BatteryStatus()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + BatteryStatus copyWith(void Function(BatteryStatus) updates) => super.copyWith((message) => updates(message as BatteryStatus)) as BatteryStatus; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static BatteryStatus create() => BatteryStatus._(); + BatteryStatus createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static BatteryStatus getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static BatteryStatus? _defaultInstance; + + @$pb.TagNumber(1) + ChargingState get chgState => $_getN(0); + @$pb.TagNumber(1) + set chgState(ChargingState v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasChgState() => $_has(0); + @$pb.TagNumber(1) + void clearChgState() => clearField(1); + + @$pb.TagNumber(2) + $core.int get percLevel => $_getIZ(1); + @$pb.TagNumber(2) + set percLevel($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasPercLevel() => $_has(1); + @$pb.TagNumber(2) + void clearPercLevel() => clearField(2); + + @$pb.TagNumber(3) + $core.int get timeToEmpty => $_getIZ(2); + @$pb.TagNumber(3) + set timeToEmpty($core.int v) { $_setUnsignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasTimeToEmpty() => $_has(2); + @$pb.TagNumber(3) + void clearTimeToEmpty() => clearField(3); + + @$pb.TagNumber(4) + $core.int get timeToFull => $_getIZ(3); + @$pb.TagNumber(4) + set timeToFull($core.int v) { $_setUnsignedInt32(3, v); } + @$pb.TagNumber(4) + $core.bool hasTimeToFull() => $_has(3); + @$pb.TagNumber(4) + void clearTimeToFull() => clearField(4); +} + +/// OpCode 0x23 +class ControllerNotification extends $pb.GeneratedMessage { + factory ControllerNotification({ + $core.int? buttonEvent, + $core.Iterable? analogEventList, + }) { + final $result = create(); + if (buttonEvent != null) { + $result.buttonEvent = buttonEvent; + } + if (analogEventList != null) { + $result.analogEventList.addAll(analogEventList); + } + return $result; + } + ControllerNotification._() : super(); + factory ControllerNotification.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ControllerNotification.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ControllerNotification', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'buttonEvent', $pb.PbFieldType.OU3, protoName: 'buttonEvent') + ..pc(3, _omitFieldNames ? '' : 'analogEventList', $pb.PbFieldType.PM, protoName: 'analogEventList', subBuilder: ControllerAnalogEvent.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ControllerNotification clone() => ControllerNotification()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ControllerNotification copyWith(void Function(ControllerNotification) updates) => super.copyWith((message) => updates(message as ControllerNotification)) as ControllerNotification; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ControllerNotification create() => ControllerNotification._(); + ControllerNotification createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ControllerNotification getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ControllerNotification? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get buttonEvent => $_getIZ(0); + @$pb.TagNumber(1) + set buttonEvent($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasButtonEvent() => $_has(0); + @$pb.TagNumber(1) + void clearButtonEvent() => clearField(1); + + @$pb.TagNumber(3) + $core.List get analogEventList => $_getList(1); +} + +class LogDataNotification_ConnectionParameters extends $pb.GeneratedMessage { + factory LogDataNotification_ConnectionParameters({ + $core.int? interval, + $core.int? latency, + $core.int? supervisorTimeout, + $core.List<$core.int>? bleChannelMap, + $core.int? mtuSize, + }) { + final $result = create(); + if (interval != null) { + $result.interval = interval; + } + if (latency != null) { + $result.latency = latency; + } + if (supervisorTimeout != null) { + $result.supervisorTimeout = supervisorTimeout; + } + if (bleChannelMap != null) { + $result.bleChannelMap = bleChannelMap; + } + if (mtuSize != null) { + $result.mtuSize = mtuSize; + } + return $result; + } + LogDataNotification_ConnectionParameters._() : super(); + factory LogDataNotification_ConnectionParameters.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory LogDataNotification_ConnectionParameters.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'LogDataNotification.ConnectionParameters', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'interval', $pb.PbFieldType.QU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'latency', $pb.PbFieldType.QU3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'supervisorTimeout', $pb.PbFieldType.QU3, protoName: 'supervisorTimeout') + ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'bleChannelMap', $pb.PbFieldType.QY, protoName: 'bleChannelMap') + ..a<$core.int>(5, _omitFieldNames ? '' : 'mtuSize', $pb.PbFieldType.OU3, protoName: 'mtuSize') + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + LogDataNotification_ConnectionParameters clone() => LogDataNotification_ConnectionParameters()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + LogDataNotification_ConnectionParameters copyWith(void Function(LogDataNotification_ConnectionParameters) updates) => super.copyWith((message) => updates(message as LogDataNotification_ConnectionParameters)) as LogDataNotification_ConnectionParameters; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static LogDataNotification_ConnectionParameters create() => LogDataNotification_ConnectionParameters._(); + LogDataNotification_ConnectionParameters createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static LogDataNotification_ConnectionParameters getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static LogDataNotification_ConnectionParameters? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get interval => $_getIZ(0); + @$pb.TagNumber(1) + set interval($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasInterval() => $_has(0); + @$pb.TagNumber(1) + void clearInterval() => clearField(1); + + @$pb.TagNumber(2) + $core.int get latency => $_getIZ(1); + @$pb.TagNumber(2) + set latency($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasLatency() => $_has(1); + @$pb.TagNumber(2) + void clearLatency() => clearField(2); + + @$pb.TagNumber(3) + $core.int get supervisorTimeout => $_getIZ(2); + @$pb.TagNumber(3) + set supervisorTimeout($core.int v) { $_setUnsignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasSupervisorTimeout() => $_has(2); + @$pb.TagNumber(3) + void clearSupervisorTimeout() => clearField(3); + + @$pb.TagNumber(4) + $core.List<$core.int> get bleChannelMap => $_getN(3); + @$pb.TagNumber(4) + set bleChannelMap($core.List<$core.int> v) { $_setBytes(3, v); } + @$pb.TagNumber(4) + $core.bool hasBleChannelMap() => $_has(3); + @$pb.TagNumber(4) + void clearBleChannelMap() => clearField(4); + + @$pb.TagNumber(5) + $core.int get mtuSize => $_getIZ(4); + @$pb.TagNumber(5) + set mtuSize($core.int v) { $_setUnsignedInt32(4, v); } + @$pb.TagNumber(5) + $core.bool hasMtuSize() => $_has(4); + @$pb.TagNumber(5) + void clearMtuSize() => clearField(5); +} + +class LogDataNotification_EnergySurveySummary extends $pb.GeneratedMessage { + factory LogDataNotification_EnergySurveySummary({ + $core.int? samples, + $core.List<$core.int>? averageEnergy, + }) { + final $result = create(); + if (samples != null) { + $result.samples = samples; + } + if (averageEnergy != null) { + $result.averageEnergy = averageEnergy; + } + return $result; + } + LogDataNotification_EnergySurveySummary._() : super(); + factory LogDataNotification_EnergySurveySummary.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory LogDataNotification_EnergySurveySummary.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'LogDataNotification.EnergySurveySummary', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'samples', $pb.PbFieldType.QU3) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'averageEnergy', $pb.PbFieldType.QY, protoName: 'averageEnergy') + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + LogDataNotification_EnergySurveySummary clone() => LogDataNotification_EnergySurveySummary()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + LogDataNotification_EnergySurveySummary copyWith(void Function(LogDataNotification_EnergySurveySummary) updates) => super.copyWith((message) => updates(message as LogDataNotification_EnergySurveySummary)) as LogDataNotification_EnergySurveySummary; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static LogDataNotification_EnergySurveySummary create() => LogDataNotification_EnergySurveySummary._(); + LogDataNotification_EnergySurveySummary createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static LogDataNotification_EnergySurveySummary getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static LogDataNotification_EnergySurveySummary? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get samples => $_getIZ(0); + @$pb.TagNumber(1) + set samples($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasSamples() => $_has(0); + @$pb.TagNumber(1) + void clearSamples() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get averageEnergy => $_getN(1); + @$pb.TagNumber(2) + set averageEnergy($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasAverageEnergy() => $_has(1); + @$pb.TagNumber(2) + void clearAverageEnergy() => clearField(2); +} + +enum LogDataNotification_DataLogObject_Event { + logData, + connectionParam, + energySurvey, + logString, + notSet +} + +class LogDataNotification_DataLogObject extends $pb.GeneratedMessage { + factory LogDataNotification_DataLogObject({ + $core.List<$core.int>? logData, + LogDataNotification_ConnectionParameters? connectionParam, + LogDataNotification_EnergySurveySummary? energySurvey, + $core.List<$core.int>? logString, + }) { + final $result = create(); + if (logData != null) { + $result.logData = logData; + } + if (connectionParam != null) { + $result.connectionParam = connectionParam; + } + if (energySurvey != null) { + $result.energySurvey = energySurvey; + } + if (logString != null) { + $result.logString = logString; + } + return $result; + } + LogDataNotification_DataLogObject._() : super(); + factory LogDataNotification_DataLogObject.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory LogDataNotification_DataLogObject.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, LogDataNotification_DataLogObject_Event> _LogDataNotification_DataLogObject_EventByTag = { + 1 : LogDataNotification_DataLogObject_Event.logData, + 2 : LogDataNotification_DataLogObject_Event.connectionParam, + 3 : LogDataNotification_DataLogObject_Event.energySurvey, + 4 : LogDataNotification_DataLogObject_Event.logString, + 0 : LogDataNotification_DataLogObject_Event.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'LogDataNotification.DataLogObject', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..oo(0, [1, 2, 3, 4]) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'logData', $pb.PbFieldType.OY, protoName: 'logData') + ..aOM(2, _omitFieldNames ? '' : 'connectionParam', protoName: 'connectionParam', subBuilder: LogDataNotification_ConnectionParameters.create) + ..aOM(3, _omitFieldNames ? '' : 'energySurvey', protoName: 'energySurvey', subBuilder: LogDataNotification_EnergySurveySummary.create) + ..a<$core.List<$core.int>>(4, _omitFieldNames ? '' : 'logString', $pb.PbFieldType.OY, protoName: 'logString') + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + LogDataNotification_DataLogObject clone() => LogDataNotification_DataLogObject()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + LogDataNotification_DataLogObject copyWith(void Function(LogDataNotification_DataLogObject) updates) => super.copyWith((message) => updates(message as LogDataNotification_DataLogObject)) as LogDataNotification_DataLogObject; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static LogDataNotification_DataLogObject create() => LogDataNotification_DataLogObject._(); + LogDataNotification_DataLogObject createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static LogDataNotification_DataLogObject getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static LogDataNotification_DataLogObject? _defaultInstance; + + LogDataNotification_DataLogObject_Event whichEvent() => _LogDataNotification_DataLogObject_EventByTag[$_whichOneof(0)]!; + void clearEvent() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + $core.List<$core.int> get logData => $_getN(0); + @$pb.TagNumber(1) + set logData($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasLogData() => $_has(0); + @$pb.TagNumber(1) + void clearLogData() => clearField(1); + + @$pb.TagNumber(2) + LogDataNotification_ConnectionParameters get connectionParam => $_getN(1); + @$pb.TagNumber(2) + set connectionParam(LogDataNotification_ConnectionParameters v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasConnectionParam() => $_has(1); + @$pb.TagNumber(2) + void clearConnectionParam() => clearField(2); + @$pb.TagNumber(2) + LogDataNotification_ConnectionParameters ensureConnectionParam() => $_ensure(1); + + @$pb.TagNumber(3) + LogDataNotification_EnergySurveySummary get energySurvey => $_getN(2); + @$pb.TagNumber(3) + set energySurvey(LogDataNotification_EnergySurveySummary v) { setField(3, v); } + @$pb.TagNumber(3) + $core.bool hasEnergySurvey() => $_has(2); + @$pb.TagNumber(3) + void clearEnergySurvey() => clearField(3); + @$pb.TagNumber(3) + LogDataNotification_EnergySurveySummary ensureEnergySurvey() => $_ensure(2); + + @$pb.TagNumber(4) + $core.List<$core.int> get logString => $_getN(3); + @$pb.TagNumber(4) + set logString($core.List<$core.int> v) { $_setBytes(3, v); } + @$pb.TagNumber(4) + $core.bool hasLogString() => $_has(3); + @$pb.TagNumber(4) + void clearLogString() => clearField(4); +} + +class LogDataNotification extends $pb.GeneratedMessage { + factory LogDataNotification({ + LogLevel? logLevel, + LogDataNotification_DataLogObject? dataLogObject, + }) { + final $result = create(); + if (logLevel != null) { + $result.logLevel = logLevel; + } + if (dataLogObject != null) { + $result.dataLogObject = dataLogObject; + } + return $result; + } + LogDataNotification._() : super(); + factory LogDataNotification.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory LogDataNotification.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'LogDataNotification', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'logLevel', $pb.PbFieldType.QE, protoName: 'logLevel', defaultOrMaker: LogLevel.LOGLEVEL_OFF, valueOf: LogLevel.valueOf, enumValues: LogLevel.values) + ..aQM(2, _omitFieldNames ? '' : 'dataLogObject', protoName: 'dataLogObject', subBuilder: LogDataNotification_DataLogObject.create) + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + LogDataNotification clone() => LogDataNotification()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + LogDataNotification copyWith(void Function(LogDataNotification) updates) => super.copyWith((message) => updates(message as LogDataNotification)) as LogDataNotification; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static LogDataNotification create() => LogDataNotification._(); + LogDataNotification createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static LogDataNotification getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static LogDataNotification? _defaultInstance; + + @$pb.TagNumber(1) + LogLevel get logLevel => $_getN(0); + @$pb.TagNumber(1) + set logLevel(LogLevel v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasLogLevel() => $_has(0); + @$pb.TagNumber(1) + void clearLogLevel() => clearField(1); + + @$pb.TagNumber(2) + LogDataNotification_DataLogObject get dataLogObject => $_getN(1); + @$pb.TagNumber(2) + set dataLogObject(LogDataNotification_DataLogObject v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasDataLogObject() => $_has(1); + @$pb.TagNumber(2) + void clearDataLogObject() => clearField(2); + @$pb.TagNumber(2) + LogDataNotification_DataLogObject ensureDataLogObject() => $_ensure(1); +} + +class SpindownRequest_StartSpindown extends $pb.GeneratedMessage { + factory SpindownRequest_StartSpindown({ + $core.bool? enable, + }) { + final $result = create(); + if (enable != null) { + $result.enable = enable; + } + return $result; + } + SpindownRequest_StartSpindown._() : super(); + factory SpindownRequest_StartSpindown.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SpindownRequest_StartSpindown.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SpindownRequest.StartSpindown', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.bool>(1, _omitFieldNames ? '' : 'enable', $pb.PbFieldType.QB) + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SpindownRequest_StartSpindown clone() => SpindownRequest_StartSpindown()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SpindownRequest_StartSpindown copyWith(void Function(SpindownRequest_StartSpindown) updates) => super.copyWith((message) => updates(message as SpindownRequest_StartSpindown)) as SpindownRequest_StartSpindown; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SpindownRequest_StartSpindown create() => SpindownRequest_StartSpindown._(); + SpindownRequest_StartSpindown createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SpindownRequest_StartSpindown getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SpindownRequest_StartSpindown? _defaultInstance; + + @$pb.TagNumber(1) + $core.bool get enable => $_getBF(0); + @$pb.TagNumber(1) + set enable($core.bool v) { $_setBool(0, v); } + @$pb.TagNumber(1) + $core.bool hasEnable() => $_has(0); + @$pb.TagNumber(1) + void clearEnable() => clearField(1); +} + +class SpindownRequest_IgnoreSpindown extends $pb.GeneratedMessage { + factory SpindownRequest_IgnoreSpindown({ + $core.bool? enable, + }) { + final $result = create(); + if (enable != null) { + $result.enable = enable; + } + return $result; + } + SpindownRequest_IgnoreSpindown._() : super(); + factory SpindownRequest_IgnoreSpindown.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SpindownRequest_IgnoreSpindown.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SpindownRequest.IgnoreSpindown', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.bool>(1, _omitFieldNames ? '' : 'enable', $pb.PbFieldType.QB) + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SpindownRequest_IgnoreSpindown clone() => SpindownRequest_IgnoreSpindown()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SpindownRequest_IgnoreSpindown copyWith(void Function(SpindownRequest_IgnoreSpindown) updates) => super.copyWith((message) => updates(message as SpindownRequest_IgnoreSpindown)) as SpindownRequest_IgnoreSpindown; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SpindownRequest_IgnoreSpindown create() => SpindownRequest_IgnoreSpindown._(); + SpindownRequest_IgnoreSpindown createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SpindownRequest_IgnoreSpindown getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SpindownRequest_IgnoreSpindown? _defaultInstance; + + @$pb.TagNumber(1) + $core.bool get enable => $_getBF(0); + @$pb.TagNumber(1) + set enable($core.bool v) { $_setBool(0, v); } + @$pb.TagNumber(1) + $core.bool hasEnable() => $_has(0); + @$pb.TagNumber(1) + void clearEnable() => clearField(1); +} + +enum SpindownRequest_Request { + startSpindown, + ignoreSpindown, + notSet +} + +/// OpCode 0x3A +class SpindownRequest extends $pb.GeneratedMessage { + factory SpindownRequest({ + SpindownRequest_StartSpindown? startSpindown, + SpindownRequest_IgnoreSpindown? ignoreSpindown, + }) { + final $result = create(); + if (startSpindown != null) { + $result.startSpindown = startSpindown; + } + if (ignoreSpindown != null) { + $result.ignoreSpindown = ignoreSpindown; + } + return $result; + } + SpindownRequest._() : super(); + factory SpindownRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SpindownRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, SpindownRequest_Request> _SpindownRequest_RequestByTag = { + 1 : SpindownRequest_Request.startSpindown, + 2 : SpindownRequest_Request.ignoreSpindown, + 0 : SpindownRequest_Request.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SpindownRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..oo(0, [1, 2]) + ..aOM(1, _omitFieldNames ? '' : 'startSpindown', protoName: 'startSpindown', subBuilder: SpindownRequest_StartSpindown.create) + ..aOM(2, _omitFieldNames ? '' : 'ignoreSpindown', protoName: 'ignoreSpindown', subBuilder: SpindownRequest_IgnoreSpindown.create) + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SpindownRequest clone() => SpindownRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SpindownRequest copyWith(void Function(SpindownRequest) updates) => super.copyWith((message) => updates(message as SpindownRequest)) as SpindownRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SpindownRequest create() => SpindownRequest._(); + SpindownRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SpindownRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SpindownRequest? _defaultInstance; + + SpindownRequest_Request whichRequest() => _SpindownRequest_RequestByTag[$_whichOneof(0)]!; + void clearRequest() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + SpindownRequest_StartSpindown get startSpindown => $_getN(0); + @$pb.TagNumber(1) + set startSpindown(SpindownRequest_StartSpindown v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasStartSpindown() => $_has(0); + @$pb.TagNumber(1) + void clearStartSpindown() => clearField(1); + @$pb.TagNumber(1) + SpindownRequest_StartSpindown ensureStartSpindown() => $_ensure(0); + + @$pb.TagNumber(2) + SpindownRequest_IgnoreSpindown get ignoreSpindown => $_getN(1); + @$pb.TagNumber(2) + set ignoreSpindown(SpindownRequest_IgnoreSpindown v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasIgnoreSpindown() => $_has(1); + @$pb.TagNumber(2) + void clearIgnoreSpindown() => clearField(2); + @$pb.TagNumber(2) + SpindownRequest_IgnoreSpindown ensureIgnoreSpindown() => $_ensure(1); +} + +class SpindownNotification_ManualSpindownStatus extends $pb.GeneratedMessage { + factory SpindownNotification_ManualSpindownStatus({ + SpindownStatus? spindownStatus, + $core.int? targetSpeedLow, + $core.int? targetSpeedHigh, + }) { + final $result = create(); + if (spindownStatus != null) { + $result.spindownStatus = spindownStatus; + } + if (targetSpeedLow != null) { + $result.targetSpeedLow = targetSpeedLow; + } + if (targetSpeedHigh != null) { + $result.targetSpeedHigh = targetSpeedHigh; + } + return $result; + } + SpindownNotification_ManualSpindownStatus._() : super(); + factory SpindownNotification_ManualSpindownStatus.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SpindownNotification_ManualSpindownStatus.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SpindownNotification.ManualSpindownStatus', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'spindownStatus', $pb.PbFieldType.QE, protoName: 'spindownStatus', defaultOrMaker: SpindownStatus.SPINDOWN_IDLE, valueOf: SpindownStatus.valueOf, enumValues: SpindownStatus.values) + ..a<$core.int>(2, _omitFieldNames ? '' : 'targetSpeedLow', $pb.PbFieldType.OU3, protoName: 'targetSpeedLow') + ..a<$core.int>(3, _omitFieldNames ? '' : 'targetSpeedHigh', $pb.PbFieldType.OU3, protoName: 'targetSpeedHigh') + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SpindownNotification_ManualSpindownStatus clone() => SpindownNotification_ManualSpindownStatus()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SpindownNotification_ManualSpindownStatus copyWith(void Function(SpindownNotification_ManualSpindownStatus) updates) => super.copyWith((message) => updates(message as SpindownNotification_ManualSpindownStatus)) as SpindownNotification_ManualSpindownStatus; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SpindownNotification_ManualSpindownStatus create() => SpindownNotification_ManualSpindownStatus._(); + SpindownNotification_ManualSpindownStatus createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SpindownNotification_ManualSpindownStatus getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SpindownNotification_ManualSpindownStatus? _defaultInstance; + + @$pb.TagNumber(1) + SpindownStatus get spindownStatus => $_getN(0); + @$pb.TagNumber(1) + set spindownStatus(SpindownStatus v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasSpindownStatus() => $_has(0); + @$pb.TagNumber(1) + void clearSpindownStatus() => clearField(1); + + @$pb.TagNumber(2) + $core.int get targetSpeedLow => $_getIZ(1); + @$pb.TagNumber(2) + set targetSpeedLow($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasTargetSpeedLow() => $_has(1); + @$pb.TagNumber(2) + void clearTargetSpeedLow() => clearField(2); + + @$pb.TagNumber(3) + $core.int get targetSpeedHigh => $_getIZ(2); + @$pb.TagNumber(3) + set targetSpeedHigh($core.int v) { $_setUnsignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasTargetSpeedHigh() => $_has(2); + @$pb.TagNumber(3) + void clearTargetSpeedHigh() => clearField(3); +} + +class SpindownNotification_AutoSpindownStatus extends $pb.GeneratedMessage { + factory SpindownNotification_AutoSpindownStatus({ + $core.bool? completed, + }) { + final $result = create(); + if (completed != null) { + $result.completed = completed; + } + return $result; + } + SpindownNotification_AutoSpindownStatus._() : super(); + factory SpindownNotification_AutoSpindownStatus.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SpindownNotification_AutoSpindownStatus.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SpindownNotification.AutoSpindownStatus', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.bool>(1, _omitFieldNames ? '' : 'completed', $pb.PbFieldType.QB) + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SpindownNotification_AutoSpindownStatus clone() => SpindownNotification_AutoSpindownStatus()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SpindownNotification_AutoSpindownStatus copyWith(void Function(SpindownNotification_AutoSpindownStatus) updates) => super.copyWith((message) => updates(message as SpindownNotification_AutoSpindownStatus)) as SpindownNotification_AutoSpindownStatus; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SpindownNotification_AutoSpindownStatus create() => SpindownNotification_AutoSpindownStatus._(); + SpindownNotification_AutoSpindownStatus createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SpindownNotification_AutoSpindownStatus getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SpindownNotification_AutoSpindownStatus? _defaultInstance; + + @$pb.TagNumber(1) + $core.bool get completed => $_getBF(0); + @$pb.TagNumber(1) + set completed($core.bool v) { $_setBool(0, v); } + @$pb.TagNumber(1) + $core.bool hasCompleted() => $_has(0); + @$pb.TagNumber(1) + void clearCompleted() => clearField(1); +} + +enum SpindownNotification_Event { + manualSpindownStatus, + autoSpindownStatus, + notSet +} + +/// OpCode 0x3B +class SpindownNotification extends $pb.GeneratedMessage { + factory SpindownNotification({ + SpindownNotification_ManualSpindownStatus? manualSpindownStatus, + SpindownNotification_AutoSpindownStatus? autoSpindownStatus, + }) { + final $result = create(); + if (manualSpindownStatus != null) { + $result.manualSpindownStatus = manualSpindownStatus; + } + if (autoSpindownStatus != null) { + $result.autoSpindownStatus = autoSpindownStatus; + } + return $result; + } + SpindownNotification._() : super(); + factory SpindownNotification.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SpindownNotification.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, SpindownNotification_Event> _SpindownNotification_EventByTag = { + 1 : SpindownNotification_Event.manualSpindownStatus, + 2 : SpindownNotification_Event.autoSpindownStatus, + 0 : SpindownNotification_Event.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SpindownNotification', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..oo(0, [1, 2]) + ..aOM(1, _omitFieldNames ? '' : 'manualSpindownStatus', protoName: 'manualSpindownStatus', subBuilder: SpindownNotification_ManualSpindownStatus.create) + ..aOM(2, _omitFieldNames ? '' : 'autoSpindownStatus', protoName: 'autoSpindownStatus', subBuilder: SpindownNotification_AutoSpindownStatus.create) + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SpindownNotification clone() => SpindownNotification()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SpindownNotification copyWith(void Function(SpindownNotification) updates) => super.copyWith((message) => updates(message as SpindownNotification)) as SpindownNotification; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SpindownNotification create() => SpindownNotification._(); + SpindownNotification createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SpindownNotification getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SpindownNotification? _defaultInstance; + + SpindownNotification_Event whichEvent() => _SpindownNotification_EventByTag[$_whichOneof(0)]!; + void clearEvent() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + SpindownNotification_ManualSpindownStatus get manualSpindownStatus => $_getN(0); + @$pb.TagNumber(1) + set manualSpindownStatus(SpindownNotification_ManualSpindownStatus v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasManualSpindownStatus() => $_has(0); + @$pb.TagNumber(1) + void clearManualSpindownStatus() => clearField(1); + @$pb.TagNumber(1) + SpindownNotification_ManualSpindownStatus ensureManualSpindownStatus() => $_ensure(0); + + @$pb.TagNumber(2) + SpindownNotification_AutoSpindownStatus get autoSpindownStatus => $_getN(1); + @$pb.TagNumber(2) + set autoSpindownStatus(SpindownNotification_AutoSpindownStatus v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasAutoSpindownStatus() => $_has(1); + @$pb.TagNumber(2) + void clearAutoSpindownStatus() => clearField(2); + @$pb.TagNumber(2) + SpindownNotification_AutoSpindownStatus ensureAutoSpindownStatus() => $_ensure(1); +} + +/// OpCode 0x3C +class GetResponse extends $pb.GeneratedMessage { + factory GetResponse({ + $core.int? dataObjectId, + $core.List<$core.int>? dataObjectData, + }) { + final $result = create(); + if (dataObjectId != null) { + $result.dataObjectId = dataObjectId; + } + if (dataObjectData != null) { + $result.dataObjectData = dataObjectData; + } + return $result; + } + GetResponse._() : super(); + factory GetResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory GetResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GetResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'dataObjectId', $pb.PbFieldType.QU3, protoName: 'dataObjectId') + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'dataObjectData', $pb.PbFieldType.OY, protoName: 'dataObjectData') + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + GetResponse clone() => GetResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + GetResponse copyWith(void Function(GetResponse) updates) => super.copyWith((message) => updates(message as GetResponse)) as GetResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GetResponse create() => GetResponse._(); + GetResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static GetResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static GetResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get dataObjectId => $_getIZ(0); + @$pb.TagNumber(1) + set dataObjectId($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasDataObjectId() => $_has(0); + @$pb.TagNumber(1) + void clearDataObjectId() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get dataObjectData => $_getN(1); + @$pb.TagNumber(2) + set dataObjectData($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasDataObjectData() => $_has(1); + @$pb.TagNumber(2) + void clearDataObjectData() => clearField(2); +} + +/// OpCode 0x3E +class StatusResponse extends $pb.GeneratedMessage { + factory StatusResponse({ + $core.int? command, + $core.int? status, + }) { + final $result = create(); + if (command != null) { + $result.command = command; + } + if (status != null) { + $result.status = status; + } + return $result; + } + StatusResponse._() : super(); + factory StatusResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory StatusResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'StatusResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'command', $pb.PbFieldType.QU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'status', $pb.PbFieldType.QU3) + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + StatusResponse clone() => StatusResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + StatusResponse copyWith(void Function(StatusResponse) updates) => super.copyWith((message) => updates(message as StatusResponse)) as StatusResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static StatusResponse create() => StatusResponse._(); + StatusResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static StatusResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static StatusResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get command => $_getIZ(0); + @$pb.TagNumber(1) + set command($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasCommand() => $_has(0); + @$pb.TagNumber(1) + void clearCommand() => clearField(1); + + @$pb.TagNumber(2) + $core.int get status => $_getIZ(1); + @$pb.TagNumber(2) + set status($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasStatus() => $_has(1); + @$pb.TagNumber(2) + void clearStatus() => clearField(2); +} + +enum Set_Set { + devInfo, + trainerSimParam, + trainerOptions, + deviceTiltConfig, + controllerInputConfig, + trainerGearIndexConfig, + wifiConfig, + dateTimeConfig, + notSet +} + +/// OpCode 0x3F +class Set extends $pb.GeneratedMessage { + factory Set({ + $core.int? options, + $core.int? msgId, + DevInfoSet? devInfo, + TrainerSimulationParam? trainerSimParam, + TrainerOptions? trainerOptions, + DeviceTiltConfigPage? deviceTiltConfig, + ControllerInputConfigPage? controllerInputConfig, + TrainerGearIndexConfigPage? trainerGearIndexConfig, + WifiConfigurationPage? wifiConfig, + DateTimePage? dateTimeConfig, + }) { + final $result = create(); + if (options != null) { + $result.options = options; + } + if (msgId != null) { + $result.msgId = msgId; + } + if (devInfo != null) { + $result.devInfo = devInfo; + } + if (trainerSimParam != null) { + $result.trainerSimParam = trainerSimParam; + } + if (trainerOptions != null) { + $result.trainerOptions = trainerOptions; + } + if (deviceTiltConfig != null) { + $result.deviceTiltConfig = deviceTiltConfig; + } + if (controllerInputConfig != null) { + $result.controllerInputConfig = controllerInputConfig; + } + if (trainerGearIndexConfig != null) { + $result.trainerGearIndexConfig = trainerGearIndexConfig; + } + if (wifiConfig != null) { + $result.wifiConfig = wifiConfig; + } + if (dateTimeConfig != null) { + $result.dateTimeConfig = dateTimeConfig; + } + return $result; + } + Set._() : super(); + factory Set.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Set.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, Set_Set> _Set_SetByTag = { + 3 : Set_Set.devInfo, + 4 : Set_Set.trainerSimParam, + 5 : Set_Set.trainerOptions, + 6 : Set_Set.deviceTiltConfig, + 7 : Set_Set.controllerInputConfig, + 8 : Set_Set.trainerGearIndexConfig, + 10 : Set_Set.wifiConfig, + 11 : Set_Set.dateTimeConfig, + 0 : Set_Set.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Set', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..oo(0, [3, 4, 5, 6, 7, 8, 10, 11]) + ..a<$core.int>(1, _omitFieldNames ? '' : 'options', $pb.PbFieldType.OU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'msgId', $pb.PbFieldType.OU3, protoName: 'msgId') + ..aOM(3, _omitFieldNames ? '' : 'devInfo', protoName: 'devInfo', subBuilder: DevInfoSet.create) + ..aOM(4, _omitFieldNames ? '' : 'trainerSimParam', protoName: 'trainerSimParam', subBuilder: TrainerSimulationParam.create) + ..aOM(5, _omitFieldNames ? '' : 'trainerOptions', protoName: 'trainerOptions', subBuilder: TrainerOptions.create) + ..aOM(6, _omitFieldNames ? '' : 'deviceTiltConfig', protoName: 'deviceTiltConfig', subBuilder: DeviceTiltConfigPage.create) + ..aOM(7, _omitFieldNames ? '' : 'controllerInputConfig', protoName: 'controllerInputConfig', subBuilder: ControllerInputConfigPage.create) + ..aOM(8, _omitFieldNames ? '' : 'trainerGearIndexConfig', protoName: 'trainerGearIndexConfig', subBuilder: TrainerGearIndexConfigPage.create) + ..aOM(10, _omitFieldNames ? '' : 'wifiConfig', protoName: 'wifiConfig', subBuilder: WifiConfigurationPage.create) + ..aOM(11, _omitFieldNames ? '' : 'dateTimeConfig', protoName: 'dateTimeConfig', subBuilder: DateTimePage.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Set clone() => Set()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Set copyWith(void Function(Set) updates) => super.copyWith((message) => updates(message as Set)) as Set; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Set create() => Set._(); + Set createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Set getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Set? _defaultInstance; + + Set_Set whichSet() => _Set_SetByTag[$_whichOneof(0)]!; + void clearSet() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + $core.int get options => $_getIZ(0); + @$pb.TagNumber(1) + set options($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasOptions() => $_has(0); + @$pb.TagNumber(1) + void clearOptions() => clearField(1); + + @$pb.TagNumber(2) + $core.int get msgId => $_getIZ(1); + @$pb.TagNumber(2) + set msgId($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasMsgId() => $_has(1); + @$pb.TagNumber(2) + void clearMsgId() => clearField(2); + + @$pb.TagNumber(3) + DevInfoSet get devInfo => $_getN(2); + @$pb.TagNumber(3) + set devInfo(DevInfoSet v) { setField(3, v); } + @$pb.TagNumber(3) + $core.bool hasDevInfo() => $_has(2); + @$pb.TagNumber(3) + void clearDevInfo() => clearField(3); + @$pb.TagNumber(3) + DevInfoSet ensureDevInfo() => $_ensure(2); + + @$pb.TagNumber(4) + TrainerSimulationParam get trainerSimParam => $_getN(3); + @$pb.TagNumber(4) + set trainerSimParam(TrainerSimulationParam v) { setField(4, v); } + @$pb.TagNumber(4) + $core.bool hasTrainerSimParam() => $_has(3); + @$pb.TagNumber(4) + void clearTrainerSimParam() => clearField(4); + @$pb.TagNumber(4) + TrainerSimulationParam ensureTrainerSimParam() => $_ensure(3); + + @$pb.TagNumber(5) + TrainerOptions get trainerOptions => $_getN(4); + @$pb.TagNumber(5) + set trainerOptions(TrainerOptions v) { setField(5, v); } + @$pb.TagNumber(5) + $core.bool hasTrainerOptions() => $_has(4); + @$pb.TagNumber(5) + void clearTrainerOptions() => clearField(5); + @$pb.TagNumber(5) + TrainerOptions ensureTrainerOptions() => $_ensure(4); + + @$pb.TagNumber(6) + DeviceTiltConfigPage get deviceTiltConfig => $_getN(5); + @$pb.TagNumber(6) + set deviceTiltConfig(DeviceTiltConfigPage v) { setField(6, v); } + @$pb.TagNumber(6) + $core.bool hasDeviceTiltConfig() => $_has(5); + @$pb.TagNumber(6) + void clearDeviceTiltConfig() => clearField(6); + @$pb.TagNumber(6) + DeviceTiltConfigPage ensureDeviceTiltConfig() => $_ensure(5); + + @$pb.TagNumber(7) + ControllerInputConfigPage get controllerInputConfig => $_getN(6); + @$pb.TagNumber(7) + set controllerInputConfig(ControllerInputConfigPage v) { setField(7, v); } + @$pb.TagNumber(7) + $core.bool hasControllerInputConfig() => $_has(6); + @$pb.TagNumber(7) + void clearControllerInputConfig() => clearField(7); + @$pb.TagNumber(7) + ControllerInputConfigPage ensureControllerInputConfig() => $_ensure(6); + + @$pb.TagNumber(8) + TrainerGearIndexConfigPage get trainerGearIndexConfig => $_getN(7); + @$pb.TagNumber(8) + set trainerGearIndexConfig(TrainerGearIndexConfigPage v) { setField(8, v); } + @$pb.TagNumber(8) + $core.bool hasTrainerGearIndexConfig() => $_has(7); + @$pb.TagNumber(8) + void clearTrainerGearIndexConfig() => clearField(8); + @$pb.TagNumber(8) + TrainerGearIndexConfigPage ensureTrainerGearIndexConfig() => $_ensure(7); + + @$pb.TagNumber(10) + WifiConfigurationPage get wifiConfig => $_getN(8); + @$pb.TagNumber(10) + set wifiConfig(WifiConfigurationPage v) { setField(10, v); } + @$pb.TagNumber(10) + $core.bool hasWifiConfig() => $_has(8); + @$pb.TagNumber(10) + void clearWifiConfig() => clearField(10); + @$pb.TagNumber(10) + WifiConfigurationPage ensureWifiConfig() => $_ensure(8); + + @$pb.TagNumber(11) + DateTimePage get dateTimeConfig => $_getN(9); + @$pb.TagNumber(11) + set dateTimeConfig(DateTimePage v) { setField(11, v); } + @$pb.TagNumber(11) + $core.bool hasDateTimeConfig() => $_has(9); + @$pb.TagNumber(11) + void clearDateTimeConfig() => clearField(11); + @$pb.TagNumber(11) + DateTimePage ensureDateTimeConfig() => $_ensure(9); +} + +enum SetResponse_Response_Page { + devInfo, + trainerSimParam, + trainerOptions, + deviceTiltConfig, + controllerInputConfig, + trainerGearIndexConfig, + wifiConfig, + dateTimeConfig, + notSet +} + +class SetResponse_Response extends $pb.GeneratedMessage { + factory SetResponse_Response({ + DevInfoPage? devInfo, + TrainerSimulationParam? trainerSimParam, + TrainerOptions? trainerOptions, + DeviceTiltConfigPage? deviceTiltConfig, + ControllerInputConfigPage? controllerInputConfig, + TrainerGearIndexConfigPage? trainerGearIndexConfig, + WifiConfigurationPage? wifiConfig, + DateTimePage? dateTimeConfig, + }) { + final $result = create(); + if (devInfo != null) { + $result.devInfo = devInfo; + } + if (trainerSimParam != null) { + $result.trainerSimParam = trainerSimParam; + } + if (trainerOptions != null) { + $result.trainerOptions = trainerOptions; + } + if (deviceTiltConfig != null) { + $result.deviceTiltConfig = deviceTiltConfig; + } + if (controllerInputConfig != null) { + $result.controllerInputConfig = controllerInputConfig; + } + if (trainerGearIndexConfig != null) { + $result.trainerGearIndexConfig = trainerGearIndexConfig; + } + if (wifiConfig != null) { + $result.wifiConfig = wifiConfig; + } + if (dateTimeConfig != null) { + $result.dateTimeConfig = dateTimeConfig; + } + return $result; + } + SetResponse_Response._() : super(); + factory SetResponse_Response.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SetResponse_Response.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, SetResponse_Response_Page> _SetResponse_Response_PageByTag = { + 1 : SetResponse_Response_Page.devInfo, + 2 : SetResponse_Response_Page.trainerSimParam, + 3 : SetResponse_Response_Page.trainerOptions, + 4 : SetResponse_Response_Page.deviceTiltConfig, + 5 : SetResponse_Response_Page.controllerInputConfig, + 6 : SetResponse_Response_Page.trainerGearIndexConfig, + 8 : SetResponse_Response_Page.wifiConfig, + 9 : SetResponse_Response_Page.dateTimeConfig, + 0 : SetResponse_Response_Page.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SetResponse.Response', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..oo(0, [1, 2, 3, 4, 5, 6, 8, 9]) + ..aOM(1, _omitFieldNames ? '' : 'devInfo', protoName: 'devInfo', subBuilder: DevInfoPage.create) + ..aOM(2, _omitFieldNames ? '' : 'trainerSimParam', protoName: 'trainerSimParam', subBuilder: TrainerSimulationParam.create) + ..aOM(3, _omitFieldNames ? '' : 'trainerOptions', protoName: 'trainerOptions', subBuilder: TrainerOptions.create) + ..aOM(4, _omitFieldNames ? '' : 'deviceTiltConfig', protoName: 'deviceTiltConfig', subBuilder: DeviceTiltConfigPage.create) + ..aOM(5, _omitFieldNames ? '' : 'controllerInputConfig', protoName: 'controllerInputConfig', subBuilder: ControllerInputConfigPage.create) + ..aOM(6, _omitFieldNames ? '' : 'trainerGearIndexConfig', protoName: 'trainerGearIndexConfig', subBuilder: TrainerGearIndexConfigPage.create) + ..aOM(8, _omitFieldNames ? '' : 'wifiConfig', protoName: 'wifiConfig', subBuilder: WifiConfigurationPage.create) + ..aOM(9, _omitFieldNames ? '' : 'dateTimeConfig', protoName: 'dateTimeConfig', subBuilder: DateTimePage.create) + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SetResponse_Response clone() => SetResponse_Response()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SetResponse_Response copyWith(void Function(SetResponse_Response) updates) => super.copyWith((message) => updates(message as SetResponse_Response)) as SetResponse_Response; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SetResponse_Response create() => SetResponse_Response._(); + SetResponse_Response createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SetResponse_Response getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SetResponse_Response? _defaultInstance; + + SetResponse_Response_Page whichPage() => _SetResponse_Response_PageByTag[$_whichOneof(0)]!; + void clearPage() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + DevInfoPage get devInfo => $_getN(0); + @$pb.TagNumber(1) + set devInfo(DevInfoPage v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasDevInfo() => $_has(0); + @$pb.TagNumber(1) + void clearDevInfo() => clearField(1); + @$pb.TagNumber(1) + DevInfoPage ensureDevInfo() => $_ensure(0); + + @$pb.TagNumber(2) + TrainerSimulationParam get trainerSimParam => $_getN(1); + @$pb.TagNumber(2) + set trainerSimParam(TrainerSimulationParam v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasTrainerSimParam() => $_has(1); + @$pb.TagNumber(2) + void clearTrainerSimParam() => clearField(2); + @$pb.TagNumber(2) + TrainerSimulationParam ensureTrainerSimParam() => $_ensure(1); + + @$pb.TagNumber(3) + TrainerOptions get trainerOptions => $_getN(2); + @$pb.TagNumber(3) + set trainerOptions(TrainerOptions v) { setField(3, v); } + @$pb.TagNumber(3) + $core.bool hasTrainerOptions() => $_has(2); + @$pb.TagNumber(3) + void clearTrainerOptions() => clearField(3); + @$pb.TagNumber(3) + TrainerOptions ensureTrainerOptions() => $_ensure(2); + + @$pb.TagNumber(4) + DeviceTiltConfigPage get deviceTiltConfig => $_getN(3); + @$pb.TagNumber(4) + set deviceTiltConfig(DeviceTiltConfigPage v) { setField(4, v); } + @$pb.TagNumber(4) + $core.bool hasDeviceTiltConfig() => $_has(3); + @$pb.TagNumber(4) + void clearDeviceTiltConfig() => clearField(4); + @$pb.TagNumber(4) + DeviceTiltConfigPage ensureDeviceTiltConfig() => $_ensure(3); + + @$pb.TagNumber(5) + ControllerInputConfigPage get controllerInputConfig => $_getN(4); + @$pb.TagNumber(5) + set controllerInputConfig(ControllerInputConfigPage v) { setField(5, v); } + @$pb.TagNumber(5) + $core.bool hasControllerInputConfig() => $_has(4); + @$pb.TagNumber(5) + void clearControllerInputConfig() => clearField(5); + @$pb.TagNumber(5) + ControllerInputConfigPage ensureControllerInputConfig() => $_ensure(4); + + @$pb.TagNumber(6) + TrainerGearIndexConfigPage get trainerGearIndexConfig => $_getN(5); + @$pb.TagNumber(6) + set trainerGearIndexConfig(TrainerGearIndexConfigPage v) { setField(6, v); } + @$pb.TagNumber(6) + $core.bool hasTrainerGearIndexConfig() => $_has(5); + @$pb.TagNumber(6) + void clearTrainerGearIndexConfig() => clearField(6); + @$pb.TagNumber(6) + TrainerGearIndexConfigPage ensureTrainerGearIndexConfig() => $_ensure(5); + + @$pb.TagNumber(8) + WifiConfigurationPage get wifiConfig => $_getN(6); + @$pb.TagNumber(8) + set wifiConfig(WifiConfigurationPage v) { setField(8, v); } + @$pb.TagNumber(8) + $core.bool hasWifiConfig() => $_has(6); + @$pb.TagNumber(8) + void clearWifiConfig() => clearField(8); + @$pb.TagNumber(8) + WifiConfigurationPage ensureWifiConfig() => $_ensure(6); + + @$pb.TagNumber(9) + DateTimePage get dateTimeConfig => $_getN(7); + @$pb.TagNumber(9) + set dateTimeConfig(DateTimePage v) { setField(9, v); } + @$pb.TagNumber(9) + $core.bool hasDateTimeConfig() => $_has(7); + @$pb.TagNumber(9) + void clearDateTimeConfig() => clearField(9); + @$pb.TagNumber(9) + DateTimePage ensureDateTimeConfig() => $_ensure(7); +} + +/// OpCode 0x40 +class SetResponse extends $pb.GeneratedMessage { + factory SetResponse({ + $core.int? status, + $core.int? msgId, + SetResponse_Response? response, + }) { + final $result = create(); + if (status != null) { + $result.status = status; + } + if (msgId != null) { + $result.msgId = msgId; + } + if (response != null) { + $result.response = response; + } + return $result; + } + SetResponse._() : super(); + factory SetResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SetResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SetResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'status', $pb.PbFieldType.OU3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'msgId', $pb.PbFieldType.OU3, protoName: 'msgId') + ..aOM(3, _omitFieldNames ? '' : 'response', subBuilder: SetResponse_Response.create) + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SetResponse clone() => SetResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SetResponse copyWith(void Function(SetResponse) updates) => super.copyWith((message) => updates(message as SetResponse)) as SetResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SetResponse create() => SetResponse._(); + SetResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SetResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SetResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get status => $_getIZ(0); + @$pb.TagNumber(1) + set status($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasStatus() => $_has(0); + @$pb.TagNumber(1) + void clearStatus() => clearField(1); + + @$pb.TagNumber(2) + $core.int get msgId => $_getIZ(1); + @$pb.TagNumber(2) + set msgId($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasMsgId() => $_has(1); + @$pb.TagNumber(2) + void clearMsgId() => clearField(2); + + @$pb.TagNumber(3) + SetResponse_Response get response => $_getN(2); + @$pb.TagNumber(3) + set response(SetResponse_Response v) { setField(3, v); } + @$pb.TagNumber(3) + $core.bool hasResponse() => $_has(2); + @$pb.TagNumber(3) + void clearResponse() => clearField(3); + @$pb.TagNumber(3) + SetResponse_Response ensureResponse() => $_ensure(2); +} + +/// OpCode 0x41 +class LogLevelSet extends $pb.GeneratedMessage { + factory LogLevelSet({ + LogLevel? logLevel, + }) { + final $result = create(); + if (logLevel != null) { + $result.logLevel = logLevel; + } + return $result; + } + LogLevelSet._() : super(); + factory LogLevelSet.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory LogLevelSet.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'LogLevelSet', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'logLevel', $pb.PbFieldType.OE, protoName: 'logLevel', defaultOrMaker: LogLevel.LOGLEVEL_OFF, valueOf: LogLevel.valueOf, enumValues: LogLevel.values) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + LogLevelSet clone() => LogLevelSet()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + LogLevelSet copyWith(void Function(LogLevelSet) updates) => super.copyWith((message) => updates(message as LogLevelSet)) as LogLevelSet; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static LogLevelSet create() => LogLevelSet._(); + LogLevelSet createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static LogLevelSet getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static LogLevelSet? _defaultInstance; + + @$pb.TagNumber(1) + LogLevel get logLevel => $_getN(0); + @$pb.TagNumber(1) + set logLevel(LogLevel v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasLogLevel() => $_has(0); + @$pb.TagNumber(1) + void clearLogLevel() => clearField(1); +} + +enum DataChangeNotification_Notification { + devInfo, + trainerSimParam, + trainerOptions, + deviceTiltConfig, + controllerInputConfig, + trainerGearIndexConfig, + wifiConfig, + deviceUpdatePage, + dateTimePage, + bleSecurityPage, + notSet +} + +/// Opcode 0x42 +class DataChangeNotification extends $pb.GeneratedMessage { + factory DataChangeNotification({ + DevInfoPage? devInfo, + TrainerSimulationParam? trainerSimParam, + TrainerOptions? trainerOptions, + DeviceTiltConfigPage? deviceTiltConfig, + ControllerInputConfigPage? controllerInputConfig, + TrainerGearIndexConfigPage? trainerGearIndexConfig, + WifiConfigurationPage? wifiConfig, + DeviceUpdatePage? deviceUpdatePage, + DateTimePage? dateTimePage, + BleSecurityPage? bleSecurityPage, + }) { + final $result = create(); + if (devInfo != null) { + $result.devInfo = devInfo; + } + if (trainerSimParam != null) { + $result.trainerSimParam = trainerSimParam; + } + if (trainerOptions != null) { + $result.trainerOptions = trainerOptions; + } + if (deviceTiltConfig != null) { + $result.deviceTiltConfig = deviceTiltConfig; + } + if (controllerInputConfig != null) { + $result.controllerInputConfig = controllerInputConfig; + } + if (trainerGearIndexConfig != null) { + $result.trainerGearIndexConfig = trainerGearIndexConfig; + } + if (wifiConfig != null) { + $result.wifiConfig = wifiConfig; + } + if (deviceUpdatePage != null) { + $result.deviceUpdatePage = deviceUpdatePage; + } + if (dateTimePage != null) { + $result.dateTimePage = dateTimePage; + } + if (bleSecurityPage != null) { + $result.bleSecurityPage = bleSecurityPage; + } + return $result; + } + DataChangeNotification._() : super(); + factory DataChangeNotification.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DataChangeNotification.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, DataChangeNotification_Notification> _DataChangeNotification_NotificationByTag = { + 1 : DataChangeNotification_Notification.devInfo, + 2 : DataChangeNotification_Notification.trainerSimParam, + 3 : DataChangeNotification_Notification.trainerOptions, + 4 : DataChangeNotification_Notification.deviceTiltConfig, + 5 : DataChangeNotification_Notification.controllerInputConfig, + 6 : DataChangeNotification_Notification.trainerGearIndexConfig, + 8 : DataChangeNotification_Notification.wifiConfig, + 9 : DataChangeNotification_Notification.deviceUpdatePage, + 10 : DataChangeNotification_Notification.dateTimePage, + 11 : DataChangeNotification_Notification.bleSecurityPage, + 0 : DataChangeNotification_Notification.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DataChangeNotification', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..oo(0, [1, 2, 3, 4, 5, 6, 8, 9, 10, 11]) + ..aOM(1, _omitFieldNames ? '' : 'devInfo', protoName: 'devInfo', subBuilder: DevInfoPage.create) + ..aOM(2, _omitFieldNames ? '' : 'trainerSimParam', protoName: 'trainerSimParam', subBuilder: TrainerSimulationParam.create) + ..aOM(3, _omitFieldNames ? '' : 'trainerOptions', protoName: 'trainerOptions', subBuilder: TrainerOptions.create) + ..aOM(4, _omitFieldNames ? '' : 'deviceTiltConfig', protoName: 'deviceTiltConfig', subBuilder: DeviceTiltConfigPage.create) + ..aOM(5, _omitFieldNames ? '' : 'controllerInputConfig', protoName: 'controllerInputConfig', subBuilder: ControllerInputConfigPage.create) + ..aOM(6, _omitFieldNames ? '' : 'trainerGearIndexConfig', protoName: 'trainerGearIndexConfig', subBuilder: TrainerGearIndexConfigPage.create) + ..aOM(8, _omitFieldNames ? '' : 'wifiConfig', protoName: 'wifiConfig', subBuilder: WifiConfigurationPage.create) + ..aOM(9, _omitFieldNames ? '' : 'deviceUpdatePage', protoName: 'deviceUpdatePage', subBuilder: DeviceUpdatePage.create) + ..aOM(10, _omitFieldNames ? '' : 'dateTimePage', protoName: 'dateTimePage', subBuilder: DateTimePage.create) + ..aOM(11, _omitFieldNames ? '' : 'bleSecurityPage', protoName: 'bleSecurityPage', subBuilder: BleSecurityPage.create) + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + DataChangeNotification clone() => DataChangeNotification()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DataChangeNotification copyWith(void Function(DataChangeNotification) updates) => super.copyWith((message) => updates(message as DataChangeNotification)) as DataChangeNotification; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DataChangeNotification create() => DataChangeNotification._(); + DataChangeNotification createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DataChangeNotification getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DataChangeNotification? _defaultInstance; + + DataChangeNotification_Notification whichNotification() => _DataChangeNotification_NotificationByTag[$_whichOneof(0)]!; + void clearNotification() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + DevInfoPage get devInfo => $_getN(0); + @$pb.TagNumber(1) + set devInfo(DevInfoPage v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasDevInfo() => $_has(0); + @$pb.TagNumber(1) + void clearDevInfo() => clearField(1); + @$pb.TagNumber(1) + DevInfoPage ensureDevInfo() => $_ensure(0); + + @$pb.TagNumber(2) + TrainerSimulationParam get trainerSimParam => $_getN(1); + @$pb.TagNumber(2) + set trainerSimParam(TrainerSimulationParam v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasTrainerSimParam() => $_has(1); + @$pb.TagNumber(2) + void clearTrainerSimParam() => clearField(2); + @$pb.TagNumber(2) + TrainerSimulationParam ensureTrainerSimParam() => $_ensure(1); + + @$pb.TagNumber(3) + TrainerOptions get trainerOptions => $_getN(2); + @$pb.TagNumber(3) + set trainerOptions(TrainerOptions v) { setField(3, v); } + @$pb.TagNumber(3) + $core.bool hasTrainerOptions() => $_has(2); + @$pb.TagNumber(3) + void clearTrainerOptions() => clearField(3); + @$pb.TagNumber(3) + TrainerOptions ensureTrainerOptions() => $_ensure(2); + + @$pb.TagNumber(4) + DeviceTiltConfigPage get deviceTiltConfig => $_getN(3); + @$pb.TagNumber(4) + set deviceTiltConfig(DeviceTiltConfigPage v) { setField(4, v); } + @$pb.TagNumber(4) + $core.bool hasDeviceTiltConfig() => $_has(3); + @$pb.TagNumber(4) + void clearDeviceTiltConfig() => clearField(4); + @$pb.TagNumber(4) + DeviceTiltConfigPage ensureDeviceTiltConfig() => $_ensure(3); + + @$pb.TagNumber(5) + ControllerInputConfigPage get controllerInputConfig => $_getN(4); + @$pb.TagNumber(5) + set controllerInputConfig(ControllerInputConfigPage v) { setField(5, v); } + @$pb.TagNumber(5) + $core.bool hasControllerInputConfig() => $_has(4); + @$pb.TagNumber(5) + void clearControllerInputConfig() => clearField(5); + @$pb.TagNumber(5) + ControllerInputConfigPage ensureControllerInputConfig() => $_ensure(4); + + @$pb.TagNumber(6) + TrainerGearIndexConfigPage get trainerGearIndexConfig => $_getN(5); + @$pb.TagNumber(6) + set trainerGearIndexConfig(TrainerGearIndexConfigPage v) { setField(6, v); } + @$pb.TagNumber(6) + $core.bool hasTrainerGearIndexConfig() => $_has(5); + @$pb.TagNumber(6) + void clearTrainerGearIndexConfig() => clearField(6); + @$pb.TagNumber(6) + TrainerGearIndexConfigPage ensureTrainerGearIndexConfig() => $_ensure(5); + + @$pb.TagNumber(8) + WifiConfigurationPage get wifiConfig => $_getN(6); + @$pb.TagNumber(8) + set wifiConfig(WifiConfigurationPage v) { setField(8, v); } + @$pb.TagNumber(8) + $core.bool hasWifiConfig() => $_has(6); + @$pb.TagNumber(8) + void clearWifiConfig() => clearField(8); + @$pb.TagNumber(8) + WifiConfigurationPage ensureWifiConfig() => $_ensure(6); + + @$pb.TagNumber(9) + DeviceUpdatePage get deviceUpdatePage => $_getN(7); + @$pb.TagNumber(9) + set deviceUpdatePage(DeviceUpdatePage v) { setField(9, v); } + @$pb.TagNumber(9) + $core.bool hasDeviceUpdatePage() => $_has(7); + @$pb.TagNumber(9) + void clearDeviceUpdatePage() => clearField(9); + @$pb.TagNumber(9) + DeviceUpdatePage ensureDeviceUpdatePage() => $_ensure(7); + + @$pb.TagNumber(10) + DateTimePage get dateTimePage => $_getN(8); + @$pb.TagNumber(10) + set dateTimePage(DateTimePage v) { setField(10, v); } + @$pb.TagNumber(10) + $core.bool hasDateTimePage() => $_has(8); + @$pb.TagNumber(10) + void clearDateTimePage() => clearField(10); + @$pb.TagNumber(10) + DateTimePage ensureDateTimePage() => $_ensure(8); + + @$pb.TagNumber(11) + BleSecurityPage get bleSecurityPage => $_getN(9); + @$pb.TagNumber(11) + set bleSecurityPage(BleSecurityPage v) { setField(11, v); } + @$pb.TagNumber(11) + $core.bool hasBleSecurityPage() => $_has(9); + @$pb.TagNumber(11) + void clearBleSecurityPage() => clearField(11); + @$pb.TagNumber(11) + BleSecurityPage ensureBleSecurityPage() => $_ensure(9); +} + +class GameStateNotification_GameSpeed extends $pb.GeneratedMessage { + factory GameStateNotification_GameSpeed({ + $core.int? normalisedSpeed, + $core.int? speed, + }) { + final $result = create(); + if (normalisedSpeed != null) { + $result.normalisedSpeed = normalisedSpeed; + } + if (speed != null) { + $result.speed = speed; + } + return $result; + } + GameStateNotification_GameSpeed._() : super(); + factory GameStateNotification_GameSpeed.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory GameStateNotification_GameSpeed.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GameStateNotification.GameSpeed', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'normalisedSpeed', $pb.PbFieldType.OS3, protoName: 'normalisedSpeed') + ..a<$core.int>(2, _omitFieldNames ? '' : 'speed', $pb.PbFieldType.OS3) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + GameStateNotification_GameSpeed clone() => GameStateNotification_GameSpeed()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + GameStateNotification_GameSpeed copyWith(void Function(GameStateNotification_GameSpeed) updates) => super.copyWith((message) => updates(message as GameStateNotification_GameSpeed)) as GameStateNotification_GameSpeed; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GameStateNotification_GameSpeed create() => GameStateNotification_GameSpeed._(); + GameStateNotification_GameSpeed createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static GameStateNotification_GameSpeed getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static GameStateNotification_GameSpeed? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get normalisedSpeed => $_getIZ(0); + @$pb.TagNumber(1) + set normalisedSpeed($core.int v) { $_setSignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasNormalisedSpeed() => $_has(0); + @$pb.TagNumber(1) + void clearNormalisedSpeed() => clearField(1); + + @$pb.TagNumber(2) + $core.int get speed => $_getIZ(1); + @$pb.TagNumber(2) + set speed($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasSpeed() => $_has(1); + @$pb.TagNumber(2) + void clearSpeed() => clearField(2); +} + +class GameStateNotification_RoadSurface extends $pb.GeneratedMessage { + factory GameStateNotification_RoadSurface({ + RoadSurfaceType? roadSurfaceType, + $core.int? roughness, + }) { + final $result = create(); + if (roadSurfaceType != null) { + $result.roadSurfaceType = roadSurfaceType; + } + if (roughness != null) { + $result.roughness = roughness; + } + return $result; + } + GameStateNotification_RoadSurface._() : super(); + factory GameStateNotification_RoadSurface.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory GameStateNotification_RoadSurface.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GameStateNotification.RoadSurface', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'roadSurfaceType', $pb.PbFieldType.OE, protoName: 'roadSurfaceType', defaultOrMaker: RoadSurfaceType.ROAD_SURFACE_SMOOTH_TARMAC, valueOf: RoadSurfaceType.valueOf, enumValues: RoadSurfaceType.values) + ..a<$core.int>(2, _omitFieldNames ? '' : 'roughness', $pb.PbFieldType.OU3) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + GameStateNotification_RoadSurface clone() => GameStateNotification_RoadSurface()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + GameStateNotification_RoadSurface copyWith(void Function(GameStateNotification_RoadSurface) updates) => super.copyWith((message) => updates(message as GameStateNotification_RoadSurface)) as GameStateNotification_RoadSurface; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GameStateNotification_RoadSurface create() => GameStateNotification_RoadSurface._(); + GameStateNotification_RoadSurface createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static GameStateNotification_RoadSurface getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static GameStateNotification_RoadSurface? _defaultInstance; + + @$pb.TagNumber(1) + RoadSurfaceType get roadSurfaceType => $_getN(0); + @$pb.TagNumber(1) + set roadSurfaceType(RoadSurfaceType v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasRoadSurfaceType() => $_has(0); + @$pb.TagNumber(1) + void clearRoadSurfaceType() => clearField(1); + + @$pb.TagNumber(2) + $core.int get roughness => $_getIZ(1); + @$pb.TagNumber(2) + set roughness($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasRoughness() => $_has(1); + @$pb.TagNumber(2) + void clearRoughness() => clearField(2); +} + +enum GameStateNotification_Notification { + gameSpeed, + roadSurface, + notSet +} + +/// Opcode 0x43 GAME_STATE_NOTIFICATION +class GameStateNotification extends $pb.GeneratedMessage { + factory GameStateNotification({ + GameStateNotification_GameSpeed? gameSpeed, + GameStateNotification_RoadSurface? roadSurface, + }) { + final $result = create(); + if (gameSpeed != null) { + $result.gameSpeed = gameSpeed; + } + if (roadSurface != null) { + $result.roadSurface = roadSurface; + } + return $result; + } + GameStateNotification._() : super(); + factory GameStateNotification.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory GameStateNotification.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, GameStateNotification_Notification> _GameStateNotification_NotificationByTag = { + 1 : GameStateNotification_Notification.gameSpeed, + 2 : GameStateNotification_Notification.roadSurface, + 0 : GameStateNotification_Notification.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'GameStateNotification', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..oo(0, [1, 2]) + ..aOM(1, _omitFieldNames ? '' : 'gameSpeed', protoName: 'gameSpeed', subBuilder: GameStateNotification_GameSpeed.create) + ..aOM(2, _omitFieldNames ? '' : 'roadSurface', protoName: 'roadSurface', subBuilder: GameStateNotification_RoadSurface.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + GameStateNotification clone() => GameStateNotification()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + GameStateNotification copyWith(void Function(GameStateNotification) updates) => super.copyWith((message) => updates(message as GameStateNotification)) as GameStateNotification; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static GameStateNotification create() => GameStateNotification._(); + GameStateNotification createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static GameStateNotification getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static GameStateNotification? _defaultInstance; + + GameStateNotification_Notification whichNotification() => _GameStateNotification_NotificationByTag[$_whichOneof(0)]!; + void clearNotification() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + GameStateNotification_GameSpeed get gameSpeed => $_getN(0); + @$pb.TagNumber(1) + set gameSpeed(GameStateNotification_GameSpeed v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasGameSpeed() => $_has(0); + @$pb.TagNumber(1) + void clearGameSpeed() => clearField(1); + @$pb.TagNumber(1) + GameStateNotification_GameSpeed ensureGameSpeed() => $_ensure(0); + + @$pb.TagNumber(2) + GameStateNotification_RoadSurface get roadSurface => $_getN(1); + @$pb.TagNumber(2) + set roadSurface(GameStateNotification_RoadSurface v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasRoadSurface() => $_has(1); + @$pb.TagNumber(2) + void clearRoadSurface() => clearField(2); + @$pb.TagNumber(2) + GameStateNotification_RoadSurface ensureRoadSurface() => $_ensure(1); +} + +enum SensorRelayConfig_Procedure { + search, + connect, + disconnect, + clearAll, + disconnectAll, + notSet +} + +/// Opcode 0x44 (SENSOR_RELAY_CONFIG) +class SensorRelayConfig extends $pb.GeneratedMessage { + factory SensorRelayConfig({ + $core.bool? search, + $core.int? connect, + $core.int? disconnect, + $core.bool? clearAll, + $core.bool? disconnectAll, + }) { + final $result = create(); + if (search != null) { + $result.search = search; + } + if (connect != null) { + $result.connect = connect; + } + if (disconnect != null) { + $result.disconnect = disconnect; + } + if (clearAll != null) { + $result.clearAll = clearAll; + } + if (disconnectAll != null) { + $result.disconnectAll = disconnectAll; + } + return $result; + } + SensorRelayConfig._() : super(); + factory SensorRelayConfig.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SensorRelayConfig.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, SensorRelayConfig_Procedure> _SensorRelayConfig_ProcedureByTag = { + 1 : SensorRelayConfig_Procedure.search, + 2 : SensorRelayConfig_Procedure.connect, + 3 : SensorRelayConfig_Procedure.disconnect, + 4 : SensorRelayConfig_Procedure.clearAll, + 5 : SensorRelayConfig_Procedure.disconnectAll, + 0 : SensorRelayConfig_Procedure.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SensorRelayConfig', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..oo(0, [1, 2, 3, 4, 5]) + ..aOB(1, _omitFieldNames ? '' : 'search') + ..a<$core.int>(2, _omitFieldNames ? '' : 'connect', $pb.PbFieldType.OU3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'disconnect', $pb.PbFieldType.OU3) + ..aOB(4, _omitFieldNames ? '' : 'clearAll', protoName: 'clearAll') + ..aOB(5, _omitFieldNames ? '' : 'disconnectAll', protoName: 'disconnectAll') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SensorRelayConfig clone() => SensorRelayConfig()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SensorRelayConfig copyWith(void Function(SensorRelayConfig) updates) => super.copyWith((message) => updates(message as SensorRelayConfig)) as SensorRelayConfig; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SensorRelayConfig create() => SensorRelayConfig._(); + SensorRelayConfig createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SensorRelayConfig getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SensorRelayConfig? _defaultInstance; + + SensorRelayConfig_Procedure whichProcedure() => _SensorRelayConfig_ProcedureByTag[$_whichOneof(0)]!; + void clearProcedure() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + $core.bool get search => $_getBF(0); + @$pb.TagNumber(1) + set search($core.bool v) { $_setBool(0, v); } + @$pb.TagNumber(1) + $core.bool hasSearch() => $_has(0); + @$pb.TagNumber(1) + void clearSearch() => clearField(1); + + @$pb.TagNumber(2) + $core.int get connect => $_getIZ(1); + @$pb.TagNumber(2) + set connect($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasConnect() => $_has(1); + @$pb.TagNumber(2) + void clearConnect() => clearField(2); + + @$pb.TagNumber(3) + $core.int get disconnect => $_getIZ(2); + @$pb.TagNumber(3) + set disconnect($core.int v) { $_setUnsignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasDisconnect() => $_has(2); + @$pb.TagNumber(3) + void clearDisconnect() => clearField(3); + + @$pb.TagNumber(4) + $core.bool get clearAll => $_getBF(3); + @$pb.TagNumber(4) + set clearAll($core.bool v) { $_setBool(3, v); } + @$pb.TagNumber(4) + $core.bool hasClearAll() => $_has(3); + @$pb.TagNumber(4) + void clearClearAll() => clearField(4); + + @$pb.TagNumber(5) + $core.bool get disconnectAll => $_getBF(4); + @$pb.TagNumber(5) + set disconnectAll($core.bool v) { $_setBool(4, v); } + @$pb.TagNumber(5) + $core.bool hasDisconnectAll() => $_has(4); + @$pb.TagNumber(5) + void clearDisconnectAll() => clearField(5); +} + +enum SensorRelayGet_Request { + pairedSensorInfo, + notSet +} + +/// Opcode 0x45 (SENSOR_RELAY_GET) +class SensorRelayGet extends $pb.GeneratedMessage { + factory SensorRelayGet({ + $core.int? pairedSensorInfo, + }) { + final $result = create(); + if (pairedSensorInfo != null) { + $result.pairedSensorInfo = pairedSensorInfo; + } + return $result; + } + SensorRelayGet._() : super(); + factory SensorRelayGet.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SensorRelayGet.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, SensorRelayGet_Request> _SensorRelayGet_RequestByTag = { + 1 : SensorRelayGet_Request.pairedSensorInfo, + 0 : SensorRelayGet_Request.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SensorRelayGet', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..oo(0, [1]) + ..a<$core.int>(1, _omitFieldNames ? '' : 'pairedSensorInfo', $pb.PbFieldType.OU3, protoName: 'pairedSensorInfo') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SensorRelayGet clone() => SensorRelayGet()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SensorRelayGet copyWith(void Function(SensorRelayGet) updates) => super.copyWith((message) => updates(message as SensorRelayGet)) as SensorRelayGet; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SensorRelayGet create() => SensorRelayGet._(); + SensorRelayGet createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SensorRelayGet getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SensorRelayGet? _defaultInstance; + + SensorRelayGet_Request whichRequest() => _SensorRelayGet_RequestByTag[$_whichOneof(0)]!; + void clearRequest() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + $core.int get pairedSensorInfo => $_getIZ(0); + @$pb.TagNumber(1) + set pairedSensorInfo($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasPairedSensorInfo() => $_has(0); + @$pb.TagNumber(1) + void clearPairedSensorInfo() => clearField(1); +} + +enum SensorRelayResponse_Response { + pairedSensorInfoList, + notSet +} + +/// Opcode 0x46 (SENSOR_RELAY_RESPONSE) +class SensorRelayResponse extends $pb.GeneratedMessage { + factory SensorRelayResponse({ + SensorInfoList? pairedSensorInfoList, + }) { + final $result = create(); + if (pairedSensorInfoList != null) { + $result.pairedSensorInfoList = pairedSensorInfoList; + } + return $result; + } + SensorRelayResponse._() : super(); + factory SensorRelayResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SensorRelayResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, SensorRelayResponse_Response> _SensorRelayResponse_ResponseByTag = { + 1 : SensorRelayResponse_Response.pairedSensorInfoList, + 0 : SensorRelayResponse_Response.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SensorRelayResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..oo(0, [1]) + ..aOM(1, _omitFieldNames ? '' : 'pairedSensorInfoList', protoName: 'pairedSensorInfoList', subBuilder: SensorInfoList.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SensorRelayResponse clone() => SensorRelayResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SensorRelayResponse copyWith(void Function(SensorRelayResponse) updates) => super.copyWith((message) => updates(message as SensorRelayResponse)) as SensorRelayResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SensorRelayResponse create() => SensorRelayResponse._(); + SensorRelayResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SensorRelayResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SensorRelayResponse? _defaultInstance; + + SensorRelayResponse_Response whichResponse() => _SensorRelayResponse_ResponseByTag[$_whichOneof(0)]!; + void clearResponse() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + SensorInfoList get pairedSensorInfoList => $_getN(0); + @$pb.TagNumber(1) + set pairedSensorInfoList(SensorInfoList v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasPairedSensorInfoList() => $_has(0); + @$pb.TagNumber(1) + void clearPairedSensorInfoList() => clearField(1); + @$pb.TagNumber(1) + SensorInfoList ensurePairedSensorInfoList() => $_ensure(0); +} + +enum SensorRelayNotification_Notification { + newSensor, + sensorStatus, + notSet +} + +/// Opcode 0x47 (SENSOR_RELAY_NOTIFIATION) +class SensorRelayNotification extends $pb.GeneratedMessage { + factory SensorRelayNotification({ + SensorInfo? newSensor, + SensorInfo? sensorStatus, + }) { + final $result = create(); + if (newSensor != null) { + $result.newSensor = newSensor; + } + if (sensorStatus != null) { + $result.sensorStatus = sensorStatus; + } + return $result; + } + SensorRelayNotification._() : super(); + factory SensorRelayNotification.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SensorRelayNotification.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, SensorRelayNotification_Notification> _SensorRelayNotification_NotificationByTag = { + 1 : SensorRelayNotification_Notification.newSensor, + 2 : SensorRelayNotification_Notification.sensorStatus, + 0 : SensorRelayNotification_Notification.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SensorRelayNotification', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..oo(0, [1, 2]) + ..aOM(1, _omitFieldNames ? '' : 'newSensor', protoName: 'newSensor', subBuilder: SensorInfo.create) + ..aOM(2, _omitFieldNames ? '' : 'sensorStatus', protoName: 'sensorStatus', subBuilder: SensorInfo.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SensorRelayNotification clone() => SensorRelayNotification()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SensorRelayNotification copyWith(void Function(SensorRelayNotification) updates) => super.copyWith((message) => updates(message as SensorRelayNotification)) as SensorRelayNotification; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SensorRelayNotification create() => SensorRelayNotification._(); + SensorRelayNotification createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SensorRelayNotification getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SensorRelayNotification? _defaultInstance; + + SensorRelayNotification_Notification whichNotification() => _SensorRelayNotification_NotificationByTag[$_whichOneof(0)]!; + void clearNotification() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + SensorInfo get newSensor => $_getN(0); + @$pb.TagNumber(1) + set newSensor(SensorInfo v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasNewSensor() => $_has(0); + @$pb.TagNumber(1) + void clearNewSensor() => clearField(1); + @$pb.TagNumber(1) + SensorInfo ensureNewSensor() => $_ensure(0); + + @$pb.TagNumber(2) + SensorInfo get sensorStatus => $_getN(1); + @$pb.TagNumber(2) + set sensorStatus(SensorInfo v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasSensorStatus() => $_has(1); + @$pb.TagNumber(2) + void clearSensorStatus() => clearField(2); + @$pb.TagNumber(2) + SensorInfo ensureSensorStatus() => $_ensure(1); +} + +/// Opcode 0x48 (HRM_DATA_NOTIFICATION) +class HrmDataNotification extends $pb.GeneratedMessage { + factory HrmDataNotification({ + $core.int? sensorId, + $core.int? hrm, + $core.int? energyExpended, + $core.Iterable<$core.int>? rrInterval, + }) { + final $result = create(); + if (sensorId != null) { + $result.sensorId = sensorId; + } + if (hrm != null) { + $result.hrm = hrm; + } + if (energyExpended != null) { + $result.energyExpended = energyExpended; + } + if (rrInterval != null) { + $result.rrInterval.addAll(rrInterval); + } + return $result; + } + HrmDataNotification._() : super(); + factory HrmDataNotification.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory HrmDataNotification.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'HrmDataNotification', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'sensorId', $pb.PbFieldType.OU3, protoName: 'sensorId') + ..a<$core.int>(2, _omitFieldNames ? '' : 'hrm', $pb.PbFieldType.OU3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'energyExpended', $pb.PbFieldType.OU3, protoName: 'energyExpended') + ..p<$core.int>(4, _omitFieldNames ? '' : 'rrInterval', $pb.PbFieldType.PU3, protoName: 'rrInterval') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + HrmDataNotification clone() => HrmDataNotification()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + HrmDataNotification copyWith(void Function(HrmDataNotification) updates) => super.copyWith((message) => updates(message as HrmDataNotification)) as HrmDataNotification; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static HrmDataNotification create() => HrmDataNotification._(); + HrmDataNotification createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static HrmDataNotification getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static HrmDataNotification? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get sensorId => $_getIZ(0); + @$pb.TagNumber(1) + set sensorId($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasSensorId() => $_has(0); + @$pb.TagNumber(1) + void clearSensorId() => clearField(1); + + @$pb.TagNumber(2) + $core.int get hrm => $_getIZ(1); + @$pb.TagNumber(2) + set hrm($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasHrm() => $_has(1); + @$pb.TagNumber(2) + void clearHrm() => clearField(2); + + @$pb.TagNumber(3) + $core.int get energyExpended => $_getIZ(2); + @$pb.TagNumber(3) + set energyExpended($core.int v) { $_setUnsignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasEnergyExpended() => $_has(2); + @$pb.TagNumber(3) + void clearEnergyExpended() => clearField(3); + + @$pb.TagNumber(4) + $core.List<$core.int> get rrInterval => $_getList(3); +} + +enum WifiConfigurationRequest_Request { + startScan, + connect, + forget, + setRegionCode, + notSet +} + +/// OpCode 0x49 +class WifiConfigurationRequest extends $pb.GeneratedMessage { + factory WifiConfigurationRequest({ + $core.bool? startScan, + WifiNetwork? connect, + $core.bool? forget, + WifiRegionCode? setRegionCode, + }) { + final $result = create(); + if (startScan != null) { + $result.startScan = startScan; + } + if (connect != null) { + $result.connect = connect; + } + if (forget != null) { + $result.forget = forget; + } + if (setRegionCode != null) { + $result.setRegionCode = setRegionCode; + } + return $result; + } + WifiConfigurationRequest._() : super(); + factory WifiConfigurationRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory WifiConfigurationRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, WifiConfigurationRequest_Request> _WifiConfigurationRequest_RequestByTag = { + 1 : WifiConfigurationRequest_Request.startScan, + 2 : WifiConfigurationRequest_Request.connect, + 3 : WifiConfigurationRequest_Request.forget, + 4 : WifiConfigurationRequest_Request.setRegionCode, + 0 : WifiConfigurationRequest_Request.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'WifiConfigurationRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..oo(0, [1, 2, 3, 4]) + ..aOB(1, _omitFieldNames ? '' : 'startScan', protoName: 'startScan') + ..aOM(2, _omitFieldNames ? '' : 'connect', subBuilder: WifiNetwork.create) + ..aOB(3, _omitFieldNames ? '' : 'forget') + ..aOM(4, _omitFieldNames ? '' : 'setRegionCode', protoName: 'setRegionCode', subBuilder: WifiRegionCode.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + WifiConfigurationRequest clone() => WifiConfigurationRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + WifiConfigurationRequest copyWith(void Function(WifiConfigurationRequest) updates) => super.copyWith((message) => updates(message as WifiConfigurationRequest)) as WifiConfigurationRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static WifiConfigurationRequest create() => WifiConfigurationRequest._(); + WifiConfigurationRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static WifiConfigurationRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static WifiConfigurationRequest? _defaultInstance; + + WifiConfigurationRequest_Request whichRequest() => _WifiConfigurationRequest_RequestByTag[$_whichOneof(0)]!; + void clearRequest() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + $core.bool get startScan => $_getBF(0); + @$pb.TagNumber(1) + set startScan($core.bool v) { $_setBool(0, v); } + @$pb.TagNumber(1) + $core.bool hasStartScan() => $_has(0); + @$pb.TagNumber(1) + void clearStartScan() => clearField(1); + + @$pb.TagNumber(2) + WifiNetwork get connect => $_getN(1); + @$pb.TagNumber(2) + set connect(WifiNetwork v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasConnect() => $_has(1); + @$pb.TagNumber(2) + void clearConnect() => clearField(2); + @$pb.TagNumber(2) + WifiNetwork ensureConnect() => $_ensure(1); + + @$pb.TagNumber(3) + $core.bool get forget => $_getBF(2); + @$pb.TagNumber(3) + set forget($core.bool v) { $_setBool(2, v); } + @$pb.TagNumber(3) + $core.bool hasForget() => $_has(2); + @$pb.TagNumber(3) + void clearForget() => clearField(3); + + @$pb.TagNumber(4) + WifiRegionCode get setRegionCode => $_getN(3); + @$pb.TagNumber(4) + set setRegionCode(WifiRegionCode v) { setField(4, v); } + @$pb.TagNumber(4) + $core.bool hasSetRegionCode() => $_has(3); + @$pb.TagNumber(4) + void clearSetRegionCode() => clearField(4); + @$pb.TagNumber(4) + WifiRegionCode ensureSetRegionCode() => $_ensure(3); +} + +class WifiNotification_WifiStatus extends $pb.GeneratedMessage { + factory WifiNotification_WifiStatus({ + WifiStatusCode? wifiStatusCode, + WifiErrorCode? wifiErrorCode, + }) { + final $result = create(); + if (wifiStatusCode != null) { + $result.wifiStatusCode = wifiStatusCode; + } + if (wifiErrorCode != null) { + $result.wifiErrorCode = wifiErrorCode; + } + return $result; + } + WifiNotification_WifiStatus._() : super(); + factory WifiNotification_WifiStatus.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory WifiNotification_WifiStatus.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'WifiNotification.WifiStatus', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'wifiStatusCode', $pb.PbFieldType.OE, protoName: 'wifiStatusCode', defaultOrMaker: WifiStatusCode.WIFI_STATUS_DISABLED, valueOf: WifiStatusCode.valueOf, enumValues: WifiStatusCode.values) + ..e(2, _omitFieldNames ? '' : 'wifiErrorCode', $pb.PbFieldType.OE, protoName: 'wifiErrorCode', defaultOrMaker: WifiErrorCode.WIFI_ERROR_UNKNOWN, valueOf: WifiErrorCode.valueOf, enumValues: WifiErrorCode.values) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + WifiNotification_WifiStatus clone() => WifiNotification_WifiStatus()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + WifiNotification_WifiStatus copyWith(void Function(WifiNotification_WifiStatus) updates) => super.copyWith((message) => updates(message as WifiNotification_WifiStatus)) as WifiNotification_WifiStatus; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static WifiNotification_WifiStatus create() => WifiNotification_WifiStatus._(); + WifiNotification_WifiStatus createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static WifiNotification_WifiStatus getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static WifiNotification_WifiStatus? _defaultInstance; + + @$pb.TagNumber(1) + WifiStatusCode get wifiStatusCode => $_getN(0); + @$pb.TagNumber(1) + set wifiStatusCode(WifiStatusCode v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasWifiStatusCode() => $_has(0); + @$pb.TagNumber(1) + void clearWifiStatusCode() => clearField(1); + + @$pb.TagNumber(2) + WifiErrorCode get wifiErrorCode => $_getN(1); + @$pb.TagNumber(2) + set wifiErrorCode(WifiErrorCode v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasWifiErrorCode() => $_has(1); + @$pb.TagNumber(2) + void clearWifiErrorCode() => clearField(2); +} + +enum WifiNotification_Notification { + discoveredNetwork, + wifiStatus, + notSet +} + +/// OpCode 0x4A +class WifiNotification extends $pb.GeneratedMessage { + factory WifiNotification({ + WifiNetworkDetails? discoveredNetwork, + WifiNotification_WifiStatus? wifiStatus, + }) { + final $result = create(); + if (discoveredNetwork != null) { + $result.discoveredNetwork = discoveredNetwork; + } + if (wifiStatus != null) { + $result.wifiStatus = wifiStatus; + } + return $result; + } + WifiNotification._() : super(); + factory WifiNotification.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory WifiNotification.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, WifiNotification_Notification> _WifiNotification_NotificationByTag = { + 1 : WifiNotification_Notification.discoveredNetwork, + 2 : WifiNotification_Notification.wifiStatus, + 0 : WifiNotification_Notification.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'WifiNotification', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..oo(0, [1, 2]) + ..aOM(1, _omitFieldNames ? '' : 'discoveredNetwork', protoName: 'discoveredNetwork', subBuilder: WifiNetworkDetails.create) + ..aOM(2, _omitFieldNames ? '' : 'wifiStatus', protoName: 'wifiStatus', subBuilder: WifiNotification_WifiStatus.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + WifiNotification clone() => WifiNotification()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + WifiNotification copyWith(void Function(WifiNotification) updates) => super.copyWith((message) => updates(message as WifiNotification)) as WifiNotification; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static WifiNotification create() => WifiNotification._(); + WifiNotification createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static WifiNotification getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static WifiNotification? _defaultInstance; + + WifiNotification_Notification whichNotification() => _WifiNotification_NotificationByTag[$_whichOneof(0)]!; + void clearNotification() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + WifiNetworkDetails get discoveredNetwork => $_getN(0); + @$pb.TagNumber(1) + set discoveredNetwork(WifiNetworkDetails v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasDiscoveredNetwork() => $_has(0); + @$pb.TagNumber(1) + void clearDiscoveredNetwork() => clearField(1); + @$pb.TagNumber(1) + WifiNetworkDetails ensureDiscoveredNetwork() => $_ensure(0); + + @$pb.TagNumber(2) + WifiNotification_WifiStatus get wifiStatus => $_getN(1); + @$pb.TagNumber(2) + set wifiStatus(WifiNotification_WifiStatus v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasWifiStatus() => $_has(1); + @$pb.TagNumber(2) + void clearWifiStatus() => clearField(2); + @$pb.TagNumber(2) + WifiNotification_WifiStatus ensureWifiStatus() => $_ensure(1); +} + +/// OpCode 0x4B +class PowerDataNotification extends $pb.GeneratedMessage { + factory PowerDataNotification({ + $core.int? sensorId, + $core.int? power, + }) { + final $result = create(); + if (sensorId != null) { + $result.sensorId = sensorId; + } + if (power != null) { + $result.power = power; + } + return $result; + } + PowerDataNotification._() : super(); + factory PowerDataNotification.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory PowerDataNotification.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PowerDataNotification', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'sensorId', $pb.PbFieldType.OU3, protoName: 'sensorId') + ..a<$core.int>(2, _omitFieldNames ? '' : 'power', $pb.PbFieldType.OU3) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + PowerDataNotification clone() => PowerDataNotification()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + PowerDataNotification copyWith(void Function(PowerDataNotification) updates) => super.copyWith((message) => updates(message as PowerDataNotification)) as PowerDataNotification; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static PowerDataNotification create() => PowerDataNotification._(); + PowerDataNotification createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static PowerDataNotification getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static PowerDataNotification? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get sensorId => $_getIZ(0); + @$pb.TagNumber(1) + set sensorId($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasSensorId() => $_has(0); + @$pb.TagNumber(1) + void clearSensorId() => clearField(1); + + @$pb.TagNumber(2) + $core.int get power => $_getIZ(1); + @$pb.TagNumber(2) + set power($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasPower() => $_has(1); + @$pb.TagNumber(2) + void clearPower() => clearField(2); +} + +/// OpCode 0x4C +class CadenceDataNotification extends $pb.GeneratedMessage { + factory CadenceDataNotification({ + $core.int? sensorId, + $core.int? cadence, + }) { + final $result = create(); + if (sensorId != null) { + $result.sensorId = sensorId; + } + if (cadence != null) { + $result.cadence = cadence; + } + return $result; + } + CadenceDataNotification._() : super(); + factory CadenceDataNotification.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory CadenceDataNotification.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'CadenceDataNotification', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'sensorId', $pb.PbFieldType.OU3, protoName: 'sensorId') + ..a<$core.int>(2, _omitFieldNames ? '' : 'cadence', $pb.PbFieldType.OU3) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + CadenceDataNotification clone() => CadenceDataNotification()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + CadenceDataNotification copyWith(void Function(CadenceDataNotification) updates) => super.copyWith((message) => updates(message as CadenceDataNotification)) as CadenceDataNotification; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static CadenceDataNotification create() => CadenceDataNotification._(); + CadenceDataNotification createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static CadenceDataNotification getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static CadenceDataNotification? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get sensorId => $_getIZ(0); + @$pb.TagNumber(1) + set sensorId($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasSensorId() => $_has(0); + @$pb.TagNumber(1) + void clearSensorId() => clearField(1); + + @$pb.TagNumber(2) + $core.int get cadence => $_getIZ(1); + @$pb.TagNumber(2) + set cadence($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasCadence() => $_has(1); + @$pb.TagNumber(2) + void clearCadence() => clearField(2); +} + +enum DeviceUpdateRequest_Procedure { + checkForUpdates, + activateUpdates, + notSet +} + +/// OpCode 0x4D +class DeviceUpdateRequest extends $pb.GeneratedMessage { + factory DeviceUpdateRequest({ + $core.bool? checkForUpdates, + $core.bool? activateUpdates, + }) { + final $result = create(); + if (checkForUpdates != null) { + $result.checkForUpdates = checkForUpdates; + } + if (activateUpdates != null) { + $result.activateUpdates = activateUpdates; + } + return $result; + } + DeviceUpdateRequest._() : super(); + factory DeviceUpdateRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DeviceUpdateRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, DeviceUpdateRequest_Procedure> _DeviceUpdateRequest_ProcedureByTag = { + 1 : DeviceUpdateRequest_Procedure.checkForUpdates, + 2 : DeviceUpdateRequest_Procedure.activateUpdates, + 0 : DeviceUpdateRequest_Procedure.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DeviceUpdateRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..oo(0, [1, 2]) + ..aOB(1, _omitFieldNames ? '' : 'checkForUpdates', protoName: 'checkForUpdates') + ..aOB(2, _omitFieldNames ? '' : 'activateUpdates', protoName: 'activateUpdates') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + DeviceUpdateRequest clone() => DeviceUpdateRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DeviceUpdateRequest copyWith(void Function(DeviceUpdateRequest) updates) => super.copyWith((message) => updates(message as DeviceUpdateRequest)) as DeviceUpdateRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DeviceUpdateRequest create() => DeviceUpdateRequest._(); + DeviceUpdateRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DeviceUpdateRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DeviceUpdateRequest? _defaultInstance; + + DeviceUpdateRequest_Procedure whichProcedure() => _DeviceUpdateRequest_ProcedureByTag[$_whichOneof(0)]!; + void clearProcedure() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + $core.bool get checkForUpdates => $_getBF(0); + @$pb.TagNumber(1) + set checkForUpdates($core.bool v) { $_setBool(0, v); } + @$pb.TagNumber(1) + $core.bool hasCheckForUpdates() => $_has(0); + @$pb.TagNumber(1) + void clearCheckForUpdates() => clearField(1); + + @$pb.TagNumber(2) + $core.bool get activateUpdates => $_getBF(1); + @$pb.TagNumber(2) + set activateUpdates($core.bool v) { $_setBool(1, v); } + @$pb.TagNumber(2) + $core.bool hasActivateUpdates() => $_has(1); + @$pb.TagNumber(2) + void clearActivateUpdates() => clearField(2); +} + +/// Opcode 0x4E +class RelayZpMessage extends $pb.GeneratedMessage { + factory RelayZpMessage({ + $core.int? relayAssignedId, + $core.List<$core.int>? payload, + }) { + final $result = create(); + if (relayAssignedId != null) { + $result.relayAssignedId = relayAssignedId; + } + if (payload != null) { + $result.payload = payload; + } + return $result; + } + RelayZpMessage._() : super(); + factory RelayZpMessage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory RelayZpMessage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'RelayZpMessage', package: const $pb.PackageName(_omitMessageNames ? '' : 'Zp'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'relayAssignedId', $pb.PbFieldType.OU3, protoName: 'relayAssignedId') + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'payload', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + RelayZpMessage clone() => RelayZpMessage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + RelayZpMessage copyWith(void Function(RelayZpMessage) updates) => super.copyWith((message) => updates(message as RelayZpMessage)) as RelayZpMessage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static RelayZpMessage create() => RelayZpMessage._(); + RelayZpMessage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static RelayZpMessage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static RelayZpMessage? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get relayAssignedId => $_getIZ(0); + @$pb.TagNumber(1) + set relayAssignedId($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasRelayAssignedId() => $_has(0); + @$pb.TagNumber(1) + void clearRelayAssignedId() => clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get payload => $_getN(1); + @$pb.TagNumber(2) + set payload($core.List<$core.int> v) { $_setBytes(1, v); } + @$pb.TagNumber(2) + $core.bool hasPayload() => $_has(1); + @$pb.TagNumber(2) + void clearPayload() => clearField(2); +} + + +const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); +const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/lib/bluetooth/devices/zwift/protocol/zp.pbenum.dart b/lib/bluetooth/devices/zwift/protocol/zp.pbenum.dart new file mode 100644 index 000000000..9350d9e9e --- /dev/null +++ b/lib/bluetooth/devices/zwift/protocol/zp.pbenum.dart @@ -0,0 +1,583 @@ +// +// Generated code. Do not modify. +// source: zp.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +/// /////////////////////////////////////////////////////////////// +/// Enumerations +/// /////////////////////////////////////////////////////////////// +class Opcode extends $pb.ProtobufEnum { + static const Opcode GET = Opcode._(0, _omitEnumNames ? '' : 'GET'); + static const Opcode DEV_INFO_STATUS = Opcode._(1, _omitEnumNames ? '' : 'DEV_INFO_STATUS'); + static const Opcode BLE_SECURITY_REQUEST = Opcode._(2, _omitEnumNames ? '' : 'BLE_SECURITY_REQUEST'); + static const Opcode TRAINER_NOTIF = Opcode._(3, _omitEnumNames ? '' : 'TRAINER_NOTIF'); + static const Opcode TRAINER_CONFIG_SET = Opcode._(4, _omitEnumNames ? '' : 'TRAINER_CONFIG_SET'); + static const Opcode TRAINER_CONFIG_STATUS = Opcode._(5, _omitEnumNames ? '' : 'TRAINER_CONFIG_STATUS'); + static const Opcode DEV_INFO_SET = Opcode._(12, _omitEnumNames ? '' : 'DEV_INFO_SET'); + static const Opcode POWER_OFF = Opcode._(15, _omitEnumNames ? '' : 'POWER_OFF'); + static const Opcode RESET = Opcode._(24, _omitEnumNames ? '' : 'RESET'); + static const Opcode BATTERY_NOTIF = Opcode._(25, _omitEnumNames ? '' : 'BATTERY_NOTIF'); + static const Opcode CONTROLLER_NOTIFICATION = Opcode._(35, _omitEnumNames ? '' : 'CONTROLLER_NOTIFICATION'); + static const Opcode LOG_DATA = Opcode._(42, _omitEnumNames ? '' : 'LOG_DATA'); + static const Opcode SPINDOWN_REQUEST = Opcode._(58, _omitEnumNames ? '' : 'SPINDOWN_REQUEST'); + static const Opcode SPINDOWN_NOTIFICATION = Opcode._(59, _omitEnumNames ? '' : 'SPINDOWN_NOTIFICATION'); + static const Opcode GET_RESPONSE = Opcode._(60, _omitEnumNames ? '' : 'GET_RESPONSE'); + static const Opcode STATUS_RESPONSE = Opcode._(62, _omitEnumNames ? '' : 'STATUS_RESPONSE'); + static const Opcode SET = Opcode._(63, _omitEnumNames ? '' : 'SET'); + static const Opcode SET_RESPONSE = Opcode._(64, _omitEnumNames ? '' : 'SET_RESPONSE'); + static const Opcode LOG_LEVEL_SET = Opcode._(65, _omitEnumNames ? '' : 'LOG_LEVEL_SET'); + static const Opcode DATA_CHANGE_NOTIFICATION = Opcode._(66, _omitEnumNames ? '' : 'DATA_CHANGE_NOTIFICATION'); + static const Opcode GAME_STATE_NOTIFICATION = Opcode._(67, _omitEnumNames ? '' : 'GAME_STATE_NOTIFICATION'); + static const Opcode SENSOR_RELAY_CONFIG = Opcode._(68, _omitEnumNames ? '' : 'SENSOR_RELAY_CONFIG'); + static const Opcode SENSOR_RELAY_GET = Opcode._(69, _omitEnumNames ? '' : 'SENSOR_RELAY_GET'); + static const Opcode SENSOR_RELAY_RESPONSE = Opcode._(70, _omitEnumNames ? '' : 'SENSOR_RELAY_RESPONSE'); + static const Opcode SENSOR_RELAY_NOTIFICATION = Opcode._(71, _omitEnumNames ? '' : 'SENSOR_RELAY_NOTIFICATION'); + static const Opcode HRM_DATA_NOTIFICATION = Opcode._(72, _omitEnumNames ? '' : 'HRM_DATA_NOTIFICATION'); + static const Opcode WIFI_CONFIG_REQUEST = Opcode._(73, _omitEnumNames ? '' : 'WIFI_CONFIG_REQUEST'); + static const Opcode WIFI_NOTIFICATION = Opcode._(74, _omitEnumNames ? '' : 'WIFI_NOTIFICATION'); + static const Opcode POWER_METER_NOTIFICATION = Opcode._(75, _omitEnumNames ? '' : 'POWER_METER_NOTIFICATION'); + static const Opcode CADENCE_SENSOR_NOTIFICATION = Opcode._(76, _omitEnumNames ? '' : 'CADENCE_SENSOR_NOTIFICATION'); + static const Opcode DEVICE_UPDATE_REQUEST = Opcode._(77, _omitEnumNames ? '' : 'DEVICE_UPDATE_REQUEST'); + static const Opcode RELAY_ZP_MESSAGE = Opcode._(78, _omitEnumNames ? '' : 'RELAY_ZP_MESSAGE'); + static const Opcode RIDE_ON = Opcode._(82, _omitEnumNames ? '' : 'RIDE_ON'); + static const Opcode RESERVED = Opcode._(253, _omitEnumNames ? '' : 'RESERVED'); + static const Opcode LOST_CONTROL = Opcode._(254, _omitEnumNames ? '' : 'LOST_CONTROL'); + static const Opcode VENDOR_MESSAGE = Opcode._(255, _omitEnumNames ? '' : 'VENDOR_MESSAGE'); + + static const $core.List values = [ + GET, + DEV_INFO_STATUS, + BLE_SECURITY_REQUEST, + TRAINER_NOTIF, + TRAINER_CONFIG_SET, + TRAINER_CONFIG_STATUS, + DEV_INFO_SET, + POWER_OFF, + RESET, + BATTERY_NOTIF, + CONTROLLER_NOTIFICATION, + LOG_DATA, + SPINDOWN_REQUEST, + SPINDOWN_NOTIFICATION, + GET_RESPONSE, + STATUS_RESPONSE, + SET, + SET_RESPONSE, + LOG_LEVEL_SET, + DATA_CHANGE_NOTIFICATION, + GAME_STATE_NOTIFICATION, + SENSOR_RELAY_CONFIG, + SENSOR_RELAY_GET, + SENSOR_RELAY_RESPONSE, + SENSOR_RELAY_NOTIFICATION, + HRM_DATA_NOTIFICATION, + WIFI_CONFIG_REQUEST, + WIFI_NOTIFICATION, + POWER_METER_NOTIFICATION, + CADENCE_SENSOR_NOTIFICATION, + DEVICE_UPDATE_REQUEST, + RELAY_ZP_MESSAGE, + RIDE_ON, + RESERVED, + LOST_CONTROL, + VENDOR_MESSAGE, + ]; + + static final $core.Map<$core.int, Opcode> _byValue = $pb.ProtobufEnum.initByValue(values); + static Opcode? valueOf($core.int value) => _byValue[value]; + + const Opcode._($core.int v, $core.String n) : super(v, n); +} + +/// Data Objects +class DO extends $pb.ProtobufEnum { + static const DO PAGE_DEV_INFO = DO._(0, _omitEnumNames ? '' : 'PAGE_DEV_INFO'); + static const DO PROTOCOL_VERSION = DO._(1, _omitEnumNames ? '' : 'PROTOCOL_VERSION'); + static const DO SYSTEM_FW_VERSION = DO._(2, _omitEnumNames ? '' : 'SYSTEM_FW_VERSION'); + static const DO DEVICE_NAME = DO._(3, _omitEnumNames ? '' : 'DEVICE_NAME'); + static const DO SERIAL_NUMBER = DO._(5, _omitEnumNames ? '' : 'SERIAL_NUMBER'); + static const DO SYSTEM_HW_REVISION = DO._(6, _omitEnumNames ? '' : 'SYSTEM_HW_REVISION'); + static const DO DEVICE_CAPABILITIES = DO._(7, _omitEnumNames ? '' : 'DEVICE_CAPABILITIES'); + static const DO MANUFACTURER_ID = DO._(8, _omitEnumNames ? '' : 'MANUFACTURER_ID'); + static const DO PRODUCT_ID = DO._(9, _omitEnumNames ? '' : 'PRODUCT_ID'); + static const DO DEVICE_UID = DO._(10, _omitEnumNames ? '' : 'DEVICE_UID'); + static const DO PAGE_CLIENT_SERVER_CONFIGURATION = DO._(16, _omitEnumNames ? '' : 'PAGE_CLIENT_SERVER_CONFIGURATION'); + static const DO CLIENT_SERVER_NOTIFICATIONS = DO._(17, _omitEnumNames ? '' : 'CLIENT_SERVER_NOTIFICATIONS'); + static const DO PAGE_DEVICE_UPDATE_INFO = DO._(32, _omitEnumNames ? '' : 'PAGE_DEVICE_UPDATE_INFO'); + static const DO DEVICE_UPDATE_STATUS = DO._(33, _omitEnumNames ? '' : 'DEVICE_UPDATE_STATUS'); + static const DO DEVICE_UPDATE_NEW_VERSION = DO._(34, _omitEnumNames ? '' : 'DEVICE_UPDATE_NEW_VERSION'); + static const DO PAGE_DATE_TIME = DO._(48, _omitEnumNames ? '' : 'PAGE_DATE_TIME'); + static const DO UTC_DATE_TIME = DO._(49, _omitEnumNames ? '' : 'UTC_DATE_TIME'); + static const DO PAGE_BLE_SECURITY = DO._(64, _omitEnumNames ? '' : 'PAGE_BLE_SECURITY'); + static const DO BLE_SECURE_CONNECTION_STATUS = DO._(65, _omitEnumNames ? '' : 'BLE_SECURE_CONNECTION_STATUS'); + static const DO BLE_SECURE_CONNECTION_WINDOW_STATUS = DO._(66, _omitEnumNames ? '' : 'BLE_SECURE_CONNECTION_WINDOW_STATUS'); + static const DO PAGE_TRAINER_CONFIG = DO._(512, _omitEnumNames ? '' : 'PAGE_TRAINER_CONFIG'); + static const DO TRAINER_MODE = DO._(513, _omitEnumNames ? '' : 'TRAINER_MODE'); + static const DO CFG_RESISTANCE = DO._(514, _omitEnumNames ? '' : 'CFG_RESISTANCE'); + static const DO ERG_POWER = DO._(515, _omitEnumNames ? '' : 'ERG_POWER'); + static const DO AVERAGING_WINDOW = DO._(516, _omitEnumNames ? '' : 'AVERAGING_WINDOW'); + static const DO SIM_WIND = DO._(517, _omitEnumNames ? '' : 'SIM_WIND'); + static const DO SIM_GRADE = DO._(518, _omitEnumNames ? '' : 'SIM_GRADE'); + static const DO SIM_REAL_GEAR_RATIO = DO._(519, _omitEnumNames ? '' : 'SIM_REAL_GEAR_RATIO'); + static const DO SIM_VIRT_GEAR_RATIO = DO._(520, _omitEnumNames ? '' : 'SIM_VIRT_GEAR_RATIO'); + static const DO SIM_CW = DO._(521, _omitEnumNames ? '' : 'SIM_CW'); + static const DO SIM_WHEEL_DIAMETER = DO._(522, _omitEnumNames ? '' : 'SIM_WHEEL_DIAMETER'); + static const DO SIM_BIKE_MASS = DO._(523, _omitEnumNames ? '' : 'SIM_BIKE_MASS'); + static const DO SIM_RIDER_MASS = DO._(524, _omitEnumNames ? '' : 'SIM_RIDER_MASS'); + static const DO SIM_CRR = DO._(525, _omitEnumNames ? '' : 'SIM_CRR'); + static const DO SIM_RESERVED_FRONTAL_AREA = DO._(526, _omitEnumNames ? '' : 'SIM_RESERVED_FRONTAL_AREA'); + static const DO SIM_EBRAKE = DO._(527, _omitEnumNames ? '' : 'SIM_EBRAKE'); + static const DO PAGE_TRAINER_GEAR_INDEX_CONFIG = DO._(528, _omitEnumNames ? '' : 'PAGE_TRAINER_GEAR_INDEX_CONFIG'); + static const DO FRONT_GEAR_INDEX = DO._(529, _omitEnumNames ? '' : 'FRONT_GEAR_INDEX'); + static const DO FRONT_GEAR_INDEX_MAX = DO._(530, _omitEnumNames ? '' : 'FRONT_GEAR_INDEX_MAX'); + static const DO FRONT_GEAR_INDEX_MIN = DO._(531, _omitEnumNames ? '' : 'FRONT_GEAR_INDEX_MIN'); + static const DO REAR_GEAR_INDEX = DO._(532, _omitEnumNames ? '' : 'REAR_GEAR_INDEX'); + static const DO REAR_GEAR_INDEX_MAX = DO._(533, _omitEnumNames ? '' : 'REAR_GEAR_INDEX_MAX'); + static const DO REAR_GEAR_INDEX_MIN = DO._(534, _omitEnumNames ? '' : 'REAR_GEAR_INDEX_MIN'); + static const DO PAGE_TRAINER_CONFIG2 = DO._(544, _omitEnumNames ? '' : 'PAGE_TRAINER_CONFIG2'); + static const DO HIGH_SPEED_DATA = DO._(545, _omitEnumNames ? '' : 'HIGH_SPEED_DATA'); + static const DO ERG_POWER_SMOOTHING = DO._(546, _omitEnumNames ? '' : 'ERG_POWER_SMOOTHING'); + static const DO VIRTUAL_SHIFTING_MODE = DO._(547, _omitEnumNames ? '' : 'VIRTUAL_SHIFTING_MODE'); + static const DO PAGE_DEVICE_TILT_CONFIG = DO._(560, _omitEnumNames ? '' : 'PAGE_DEVICE_TILT_CONFIG'); + static const DO DEVICE_TILT_ENABLED = DO._(561, _omitEnumNames ? '' : 'DEVICE_TILT_ENABLED'); + static const DO DEVICE_TILT_GRADIENT_MIN = DO._(562, _omitEnumNames ? '' : 'DEVICE_TILT_GRADIENT_MIN'); + static const DO DEVICE_TILT_GRADIENT_MAX = DO._(563, _omitEnumNames ? '' : 'DEVICE_TILT_GRADIENT_MAX'); + static const DO DEVICE_TILT_GRADIENT = DO._(564, _omitEnumNames ? '' : 'DEVICE_TILT_GRADIENT'); + static const DO BATTERY_STATE = DO._(771, _omitEnumNames ? '' : 'BATTERY_STATE'); + static const DO PAGE_CONTROLLER_INPUT_CONFIG = DO._(1024, _omitEnumNames ? '' : 'PAGE_CONTROLLER_INPUT_CONFIG'); + static const DO INPUT_SUPPORTED_DIGITAL_INPUTS = DO._(1025, _omitEnumNames ? '' : 'INPUT_SUPPORTED_DIGITAL_INPUTS'); + static const DO INPUT_SUPPORTED_ANALOG_INPUTS = DO._(1026, _omitEnumNames ? '' : 'INPUT_SUPPORTED_ANALOG_INPUTS'); + static const DO INPUT_ANALOG_INPUT_RANGE = DO._(1027, _omitEnumNames ? '' : 'INPUT_ANALOG_INPUT_RANGE'); + static const DO INPUT_ANALOG_INPUT_DEADZONE = DO._(1028, _omitEnumNames ? '' : 'INPUT_ANALOG_INPUT_DEADZONE'); + static const DO PAGE_WIFI_CONFIGURATION = DO._(1056, _omitEnumNames ? '' : 'PAGE_WIFI_CONFIGURATION'); + static const DO WIFI_ENABLED = DO._(1057, _omitEnumNames ? '' : 'WIFI_ENABLED'); + static const DO WIFI_STATUS = DO._(1058, _omitEnumNames ? '' : 'WIFI_STATUS'); + static const DO WIFI_SSID = DO._(1059, _omitEnumNames ? '' : 'WIFI_SSID'); + static const DO WIFI_BAND = DO._(1060, _omitEnumNames ? '' : 'WIFI_BAND'); + static const DO WIFI_RSSI = DO._(1061, _omitEnumNames ? '' : 'WIFI_RSSI'); + static const DO WIFI_REGION_CODE = DO._(1062, _omitEnumNames ? '' : 'WIFI_REGION_CODE'); + static const DO SENSOR_RELAY_DATA_PAGE = DO._(1280, _omitEnumNames ? '' : 'SENSOR_RELAY_DATA_PAGE'); + static const DO SENSOR_RELAY_SUPPORTED_SENSORS = DO._(1281, _omitEnumNames ? '' : 'SENSOR_RELAY_SUPPORTED_SENSORS'); + static const DO SENSOR_RELAY_PAIRED_SENSORS = DO._(1282, _omitEnumNames ? '' : 'SENSOR_RELAY_PAIRED_SENSORS'); + + static const $core.List values = [ + PAGE_DEV_INFO, + PROTOCOL_VERSION, + SYSTEM_FW_VERSION, + DEVICE_NAME, + SERIAL_NUMBER, + SYSTEM_HW_REVISION, + DEVICE_CAPABILITIES, + MANUFACTURER_ID, + PRODUCT_ID, + DEVICE_UID, + PAGE_CLIENT_SERVER_CONFIGURATION, + CLIENT_SERVER_NOTIFICATIONS, + PAGE_DEVICE_UPDATE_INFO, + DEVICE_UPDATE_STATUS, + DEVICE_UPDATE_NEW_VERSION, + PAGE_DATE_TIME, + UTC_DATE_TIME, + PAGE_BLE_SECURITY, + BLE_SECURE_CONNECTION_STATUS, + BLE_SECURE_CONNECTION_WINDOW_STATUS, + PAGE_TRAINER_CONFIG, + TRAINER_MODE, + CFG_RESISTANCE, + ERG_POWER, + AVERAGING_WINDOW, + SIM_WIND, + SIM_GRADE, + SIM_REAL_GEAR_RATIO, + SIM_VIRT_GEAR_RATIO, + SIM_CW, + SIM_WHEEL_DIAMETER, + SIM_BIKE_MASS, + SIM_RIDER_MASS, + SIM_CRR, + SIM_RESERVED_FRONTAL_AREA, + SIM_EBRAKE, + PAGE_TRAINER_GEAR_INDEX_CONFIG, + FRONT_GEAR_INDEX, + FRONT_GEAR_INDEX_MAX, + FRONT_GEAR_INDEX_MIN, + REAR_GEAR_INDEX, + REAR_GEAR_INDEX_MAX, + REAR_GEAR_INDEX_MIN, + PAGE_TRAINER_CONFIG2, + HIGH_SPEED_DATA, + ERG_POWER_SMOOTHING, + VIRTUAL_SHIFTING_MODE, + PAGE_DEVICE_TILT_CONFIG, + DEVICE_TILT_ENABLED, + DEVICE_TILT_GRADIENT_MIN, + DEVICE_TILT_GRADIENT_MAX, + DEVICE_TILT_GRADIENT, + BATTERY_STATE, + PAGE_CONTROLLER_INPUT_CONFIG, + INPUT_SUPPORTED_DIGITAL_INPUTS, + INPUT_SUPPORTED_ANALOG_INPUTS, + INPUT_ANALOG_INPUT_RANGE, + INPUT_ANALOG_INPUT_DEADZONE, + PAGE_WIFI_CONFIGURATION, + WIFI_ENABLED, + WIFI_STATUS, + WIFI_SSID, + WIFI_BAND, + WIFI_RSSI, + WIFI_REGION_CODE, + SENSOR_RELAY_DATA_PAGE, + SENSOR_RELAY_SUPPORTED_SENSORS, + SENSOR_RELAY_PAIRED_SENSORS, + ]; + + static final $core.Map<$core.int, DO> _byValue = $pb.ProtobufEnum.initByValue(values); + static DO? valueOf($core.int value) => _byValue[value]; + + const DO._($core.int v, $core.String n) : super(v, n); +} + +class Status extends $pb.ProtobufEnum { + static const Status SUCCESS = Status._(0, _omitEnumNames ? '' : 'SUCCESS'); + static const Status FAILURE = Status._(1, _omitEnumNames ? '' : 'FAILURE'); + static const Status BUSY = Status._(2, _omitEnumNames ? '' : 'BUSY'); + static const Status INVALID_PARAM = Status._(3, _omitEnumNames ? '' : 'INVALID_PARAM'); + static const Status NOT_PERMITTED = Status._(4, _omitEnumNames ? '' : 'NOT_PERMITTED'); + static const Status NOT_SUPPORTED = Status._(5, _omitEnumNames ? '' : 'NOT_SUPPORTED'); + static const Status INVALID_MODE = Status._(6, _omitEnumNames ? '' : 'INVALID_MODE'); + + static const $core.List values = [ + SUCCESS, + FAILURE, + BUSY, + INVALID_PARAM, + NOT_PERMITTED, + NOT_SUPPORTED, + INVALID_MODE, + ]; + + static final $core.Map<$core.int, Status> _byValue = $pb.ProtobufEnum.initByValue(values); + static Status? valueOf($core.int value) => _byValue[value]; + + const Status._($core.int v, $core.String n) : super(v, n); +} + +class DeviceType extends $pb.ProtobufEnum { + static const DeviceType UNDEFINED = DeviceType._(0, _omitEnumNames ? '' : 'UNDEFINED'); + static const DeviceType CYCLING_TURBO_TRAINER = DeviceType._(1, _omitEnumNames ? '' : 'CYCLING_TURBO_TRAINER'); + static const DeviceType USER_INPUT_DEVICE = DeviceType._(2, _omitEnumNames ? '' : 'USER_INPUT_DEVICE'); + static const DeviceType TREADMILL = DeviceType._(3, _omitEnumNames ? '' : 'TREADMILL'); + static const DeviceType SENSOR_RELAY = DeviceType._(4, _omitEnumNames ? '' : 'SENSOR_RELAY'); + static const DeviceType HEART_RATE_MONITOR = DeviceType._(5, _omitEnumNames ? '' : 'HEART_RATE_MONITOR'); + static const DeviceType POWER_METER = DeviceType._(6, _omitEnumNames ? '' : 'POWER_METER'); + static const DeviceType CADENCE_SENSOR = DeviceType._(7, _omitEnumNames ? '' : 'CADENCE_SENSOR'); + static const DeviceType WIFI = DeviceType._(8, _omitEnumNames ? '' : 'WIFI'); + + static const $core.List values = [ + UNDEFINED, + CYCLING_TURBO_TRAINER, + USER_INPUT_DEVICE, + TREADMILL, + SENSOR_RELAY, + HEART_RATE_MONITOR, + POWER_METER, + CADENCE_SENSOR, + WIFI, + ]; + + static final $core.Map<$core.int, DeviceType> _byValue = $pb.ProtobufEnum.initByValue(values); + static DeviceType? valueOf($core.int value) => _byValue[value]; + + const DeviceType._($core.int v, $core.String n) : super(v, n); +} + +class TrainerMode extends $pb.ProtobufEnum { + static const TrainerMode MODE_UNKNOWN = TrainerMode._(0, _omitEnumNames ? '' : 'MODE_UNKNOWN'); + static const TrainerMode MODE_ERG = TrainerMode._(1, _omitEnumNames ? '' : 'MODE_ERG'); + static const TrainerMode MODE_RESISTANCE = TrainerMode._(2, _omitEnumNames ? '' : 'MODE_RESISTANCE'); + static const TrainerMode MODE_SIM = TrainerMode._(3, _omitEnumNames ? '' : 'MODE_SIM'); + + static const $core.List values = [ + MODE_UNKNOWN, + MODE_ERG, + MODE_RESISTANCE, + MODE_SIM, + ]; + + static final $core.Map<$core.int, TrainerMode> _byValue = $pb.ProtobufEnum.initByValue(values); + static TrainerMode? valueOf($core.int value) => _byValue[value]; + + const TrainerMode._($core.int v, $core.String n) : super(v, n); +} + +class ChargingState extends $pb.ProtobufEnum { + static const ChargingState CHARGING_IDLE = ChargingState._(0, _omitEnumNames ? '' : 'CHARGING_IDLE'); + static const ChargingState CHARGING_PROGRESS = ChargingState._(1, _omitEnumNames ? '' : 'CHARGING_PROGRESS'); + static const ChargingState CHARGING_DONE = ChargingState._(2, _omitEnumNames ? '' : 'CHARGING_DONE'); + + static const $core.List values = [ + CHARGING_IDLE, + CHARGING_PROGRESS, + CHARGING_DONE, + ]; + + static final $core.Map<$core.int, ChargingState> _byValue = $pb.ProtobufEnum.initByValue(values); + static ChargingState? valueOf($core.int value) => _byValue[value]; + + const ChargingState._($core.int v, $core.String n) : super(v, n); +} + +class SpindownStatus extends $pb.ProtobufEnum { + static const SpindownStatus SPINDOWN_IDLE = SpindownStatus._(0, _omitEnumNames ? '' : 'SPINDOWN_IDLE'); + static const SpindownStatus SPINDOWN_REQUESTED = SpindownStatus._(1, _omitEnumNames ? '' : 'SPINDOWN_REQUESTED'); + static const SpindownStatus SPINDOWN_SUCCESS = SpindownStatus._(2, _omitEnumNames ? '' : 'SPINDOWN_SUCCESS'); + static const SpindownStatus SPINDOWN_ERROR = SpindownStatus._(3, _omitEnumNames ? '' : 'SPINDOWN_ERROR'); + static const SpindownStatus SPINDOWN_STOP_PEDALLING = SpindownStatus._(4, _omitEnumNames ? '' : 'SPINDOWN_STOP_PEDALLING'); + static const SpindownStatus SPINDOWN_ERROR_TIMEOUT = SpindownStatus._(5, _omitEnumNames ? '' : 'SPINDOWN_ERROR_TIMEOUT'); + static const SpindownStatus SPINDOWN_ERROR_TOSHORT = SpindownStatus._(6, _omitEnumNames ? '' : 'SPINDOWN_ERROR_TOSHORT'); + static const SpindownStatus SPINDOWN_ERROR_TOSLOW = SpindownStatus._(7, _omitEnumNames ? '' : 'SPINDOWN_ERROR_TOSLOW'); + static const SpindownStatus SPINDOWN_ERROR_TOFAST = SpindownStatus._(8, _omitEnumNames ? '' : 'SPINDOWN_ERROR_TOFAST'); + static const SpindownStatus SPINDOWN_ERROR_SAMPLEERROR = SpindownStatus._(9, _omitEnumNames ? '' : 'SPINDOWN_ERROR_SAMPLEERROR'); + static const SpindownStatus SPINDOWN_ERROR_ABORT = SpindownStatus._(10, _omitEnumNames ? '' : 'SPINDOWN_ERROR_ABORT'); + + static const $core.List values = [ + SPINDOWN_IDLE, + SPINDOWN_REQUESTED, + SPINDOWN_SUCCESS, + SPINDOWN_ERROR, + SPINDOWN_STOP_PEDALLING, + SPINDOWN_ERROR_TIMEOUT, + SPINDOWN_ERROR_TOSHORT, + SPINDOWN_ERROR_TOSLOW, + SPINDOWN_ERROR_TOFAST, + SPINDOWN_ERROR_SAMPLEERROR, + SPINDOWN_ERROR_ABORT, + ]; + + static final $core.Map<$core.int, SpindownStatus> _byValue = $pb.ProtobufEnum.initByValue(values); + static SpindownStatus? valueOf($core.int value) => _byValue[value]; + + const SpindownStatus._($core.int v, $core.String n) : super(v, n); +} + +class LogLevel extends $pb.ProtobufEnum { + static const LogLevel LOGLEVEL_OFF = LogLevel._(0, _omitEnumNames ? '' : 'LOGLEVEL_OFF'); + static const LogLevel LOGLEVEL_ERROR = LogLevel._(1, _omitEnumNames ? '' : 'LOGLEVEL_ERROR'); + static const LogLevel LOGLEVEL_WARNING = LogLevel._(2, _omitEnumNames ? '' : 'LOGLEVEL_WARNING'); + static const LogLevel LOGLEVEL_INFO = LogLevel._(3, _omitEnumNames ? '' : 'LOGLEVEL_INFO'); + static const LogLevel LOGLEVEL_DEBUG = LogLevel._(4, _omitEnumNames ? '' : 'LOGLEVEL_DEBUG'); + static const LogLevel LOGLEVEL_TRACE = LogLevel._(5, _omitEnumNames ? '' : 'LOGLEVEL_TRACE'); + + static const $core.List values = [ + LOGLEVEL_OFF, + LOGLEVEL_ERROR, + LOGLEVEL_WARNING, + LOGLEVEL_INFO, + LOGLEVEL_DEBUG, + LOGLEVEL_TRACE, + ]; + + static final $core.Map<$core.int, LogLevel> _byValue = $pb.ProtobufEnum.initByValue(values); + static LogLevel? valueOf($core.int value) => _byValue[value]; + + const LogLevel._($core.int v, $core.String n) : super(v, n); +} + +class RoadSurfaceType extends $pb.ProtobufEnum { + static const RoadSurfaceType ROAD_SURFACE_SMOOTH_TARMAC = RoadSurfaceType._(0, _omitEnumNames ? '' : 'ROAD_SURFACE_SMOOTH_TARMAC'); + static const RoadSurfaceType ROAD_SURFACE_BRICK_ROAD = RoadSurfaceType._(1, _omitEnumNames ? '' : 'ROAD_SURFACE_BRICK_ROAD'); + static const RoadSurfaceType ROAD_SURFACE_HARD_COBBLES = RoadSurfaceType._(2, _omitEnumNames ? '' : 'ROAD_SURFACE_HARD_COBBLES'); + static const RoadSurfaceType ROAD_SURFACE_SOFT_COBBLES = RoadSurfaceType._(3, _omitEnumNames ? '' : 'ROAD_SURFACE_SOFT_COBBLES'); + static const RoadSurfaceType ROAD_SURFACE_NARROW_WOODEN_PLANKS = RoadSurfaceType._(4, _omitEnumNames ? '' : 'ROAD_SURFACE_NARROW_WOODEN_PLANKS'); + static const RoadSurfaceType ROAD_SURFACE_WIDE_WOODEN_PLANKS = RoadSurfaceType._(5, _omitEnumNames ? '' : 'ROAD_SURFACE_WIDE_WOODEN_PLANKS'); + static const RoadSurfaceType ROAD_SURFACE_DIRT = RoadSurfaceType._(6, _omitEnumNames ? '' : 'ROAD_SURFACE_DIRT'); + static const RoadSurfaceType ROAD_SURFACE_GRAVEL = RoadSurfaceType._(7, _omitEnumNames ? '' : 'ROAD_SURFACE_GRAVEL'); + static const RoadSurfaceType ROAD_SURFACE_CATTLE_GRID = RoadSurfaceType._(8, _omitEnumNames ? '' : 'ROAD_SURFACE_CATTLE_GRID'); + static const RoadSurfaceType ROAD_SURFACE_CONCRETE_FLAG_STONES = RoadSurfaceType._(9, _omitEnumNames ? '' : 'ROAD_SURFACE_CONCRETE_FLAG_STONES'); + static const RoadSurfaceType ROAD_SURFACE_ICE = RoadSurfaceType._(10, _omitEnumNames ? '' : 'ROAD_SURFACE_ICE'); + + static const $core.List values = [ + ROAD_SURFACE_SMOOTH_TARMAC, + ROAD_SURFACE_BRICK_ROAD, + ROAD_SURFACE_HARD_COBBLES, + ROAD_SURFACE_SOFT_COBBLES, + ROAD_SURFACE_NARROW_WOODEN_PLANKS, + ROAD_SURFACE_WIDE_WOODEN_PLANKS, + ROAD_SURFACE_DIRT, + ROAD_SURFACE_GRAVEL, + ROAD_SURFACE_CATTLE_GRID, + ROAD_SURFACE_CONCRETE_FLAG_STONES, + ROAD_SURFACE_ICE, + ]; + + static final $core.Map<$core.int, RoadSurfaceType> _byValue = $pb.ProtobufEnum.initByValue(values); + static RoadSurfaceType? valueOf($core.int value) => _byValue[value]; + + const RoadSurfaceType._($core.int v, $core.String n) : super(v, n); +} + +class WifiStatusCode extends $pb.ProtobufEnum { + static const WifiStatusCode WIFI_STATUS_DISABLED = WifiStatusCode._(0, _omitEnumNames ? '' : 'WIFI_STATUS_DISABLED'); + static const WifiStatusCode WIFI_STATUS_NOT_PROVISIONED = WifiStatusCode._(1, _omitEnumNames ? '' : 'WIFI_STATUS_NOT_PROVISIONED'); + static const WifiStatusCode WIFI_STATUS_SCANNING = WifiStatusCode._(2, _omitEnumNames ? '' : 'WIFI_STATUS_SCANNING'); + static const WifiStatusCode WIFI_STATUS_DISCONNECTED = WifiStatusCode._(3, _omitEnumNames ? '' : 'WIFI_STATUS_DISCONNECTED'); + static const WifiStatusCode WIFI_STATUS_CONNECTING = WifiStatusCode._(4, _omitEnumNames ? '' : 'WIFI_STATUS_CONNECTING'); + static const WifiStatusCode WIFI_STATUS_CONNECTED = WifiStatusCode._(5, _omitEnumNames ? '' : 'WIFI_STATUS_CONNECTED'); + static const WifiStatusCode WIFI_STATUS_ERROR = WifiStatusCode._(6, _omitEnumNames ? '' : 'WIFI_STATUS_ERROR'); + + static const $core.List values = [ + WIFI_STATUS_DISABLED, + WIFI_STATUS_NOT_PROVISIONED, + WIFI_STATUS_SCANNING, + WIFI_STATUS_DISCONNECTED, + WIFI_STATUS_CONNECTING, + WIFI_STATUS_CONNECTED, + WIFI_STATUS_ERROR, + ]; + + static final $core.Map<$core.int, WifiStatusCode> _byValue = $pb.ProtobufEnum.initByValue(values); + static WifiStatusCode? valueOf($core.int value) => _byValue[value]; + + const WifiStatusCode._($core.int v, $core.String n) : super(v, n); +} + +class WifiErrorCode extends $pb.ProtobufEnum { + static const WifiErrorCode WIFI_ERROR_UNKNOWN = WifiErrorCode._(0, _omitEnumNames ? '' : 'WIFI_ERROR_UNKNOWN'); + static const WifiErrorCode WIFI_ERROR_NO_MEMORY = WifiErrorCode._(1, _omitEnumNames ? '' : 'WIFI_ERROR_NO_MEMORY'); + static const WifiErrorCode WIFI_ERROR_INVALID_PARAMETERS = WifiErrorCode._(2, _omitEnumNames ? '' : 'WIFI_ERROR_INVALID_PARAMETERS'); + static const WifiErrorCode WIFI_ERROR_INVALID_STATE = WifiErrorCode._(3, _omitEnumNames ? '' : 'WIFI_ERROR_INVALID_STATE'); + static const WifiErrorCode WIFI_ERROR_NOT_FOUND = WifiErrorCode._(4, _omitEnumNames ? '' : 'WIFI_ERROR_NOT_FOUND'); + static const WifiErrorCode WIFI_ERROR_NOT_SUPPORTED = WifiErrorCode._(5, _omitEnumNames ? '' : 'WIFI_ERROR_NOT_SUPPORTED'); + static const WifiErrorCode WIFI_ERROR_NOT_ALLOWED = WifiErrorCode._(6, _omitEnumNames ? '' : 'WIFI_ERROR_NOT_ALLOWED'); + static const WifiErrorCode WIFI_ERROR_NOT_INITIALISED = WifiErrorCode._(7, _omitEnumNames ? '' : 'WIFI_ERROR_NOT_INITIALISED'); + static const WifiErrorCode WIFI_ERROR_NOT_STARTED = WifiErrorCode._(8, _omitEnumNames ? '' : 'WIFI_ERROR_NOT_STARTED'); + static const WifiErrorCode WIFI_ERROR_TIMEOUT = WifiErrorCode._(9, _omitEnumNames ? '' : 'WIFI_ERROR_TIMEOUT'); + static const WifiErrorCode WIFI_ERROR_MODE = WifiErrorCode._(10, _omitEnumNames ? '' : 'WIFI_ERROR_MODE'); + static const WifiErrorCode WIFI_ERROR_SSID_INVALID = WifiErrorCode._(11, _omitEnumNames ? '' : 'WIFI_ERROR_SSID_INVALID'); + + static const $core.List values = [ + WIFI_ERROR_UNKNOWN, + WIFI_ERROR_NO_MEMORY, + WIFI_ERROR_INVALID_PARAMETERS, + WIFI_ERROR_INVALID_STATE, + WIFI_ERROR_NOT_FOUND, + WIFI_ERROR_NOT_SUPPORTED, + WIFI_ERROR_NOT_ALLOWED, + WIFI_ERROR_NOT_INITIALISED, + WIFI_ERROR_NOT_STARTED, + WIFI_ERROR_TIMEOUT, + WIFI_ERROR_MODE, + WIFI_ERROR_SSID_INVALID, + ]; + + static final $core.Map<$core.int, WifiErrorCode> _byValue = $pb.ProtobufEnum.initByValue(values); + static WifiErrorCode? valueOf($core.int value) => _byValue[value]; + + const WifiErrorCode._($core.int v, $core.String n) : super(v, n); +} + +class InterfaceType extends $pb.ProtobufEnum { + static const InterfaceType INTERFACE_BLE = InterfaceType._(1, _omitEnumNames ? '' : 'INTERFACE_BLE'); + static const InterfaceType INTERFACE_ANT = InterfaceType._(2, _omitEnumNames ? '' : 'INTERFACE_ANT'); + static const InterfaceType INTERFACE_USB = InterfaceType._(3, _omitEnumNames ? '' : 'INTERFACE_USB'); + static const InterfaceType INTERFACE_ETH = InterfaceType._(4, _omitEnumNames ? '' : 'INTERFACE_ETH'); + static const InterfaceType INTERFACE_WIFI = InterfaceType._(5, _omitEnumNames ? '' : 'INTERFACE_WIFI'); + + static const $core.List values = [ + INTERFACE_BLE, + INTERFACE_ANT, + INTERFACE_USB, + INTERFACE_ETH, + INTERFACE_WIFI, + ]; + + static final $core.Map<$core.int, InterfaceType> _byValue = $pb.ProtobufEnum.initByValue(values); + static InterfaceType? valueOf($core.int value) => _byValue[value]; + + const InterfaceType._($core.int v, $core.String n) : super(v, n); +} + +class SensorConnectionStatus extends $pb.ProtobufEnum { + static const SensorConnectionStatus SENSOR_STATUS_DISCOVERED = SensorConnectionStatus._(1, _omitEnumNames ? '' : 'SENSOR_STATUS_DISCOVERED'); + static const SensorConnectionStatus SENSOR_STATUS_DISCONNECTED = SensorConnectionStatus._(2, _omitEnumNames ? '' : 'SENSOR_STATUS_DISCONNECTED'); + static const SensorConnectionStatus SENSOR_STATUS_PAIRING = SensorConnectionStatus._(3, _omitEnumNames ? '' : 'SENSOR_STATUS_PAIRING'); + static const SensorConnectionStatus SENSOR_STATUS_CONNECTED = SensorConnectionStatus._(4, _omitEnumNames ? '' : 'SENSOR_STATUS_CONNECTED'); + + static const $core.List values = [ + SENSOR_STATUS_DISCOVERED, + SENSOR_STATUS_DISCONNECTED, + SENSOR_STATUS_PAIRING, + SENSOR_STATUS_CONNECTED, + ]; + + static final $core.Map<$core.int, SensorConnectionStatus> _byValue = $pb.ProtobufEnum.initByValue(values); + static SensorConnectionStatus? valueOf($core.int value) => _byValue[value]; + + const SensorConnectionStatus._($core.int v, $core.String n) : super(v, n); +} + +class BleSecureConnectionStatus extends $pb.ProtobufEnum { + static const BleSecureConnectionStatus BLE_CONNECTION_SECURITY_STATUS_NONE = BleSecureConnectionStatus._(0, _omitEnumNames ? '' : 'BLE_CONNECTION_SECURITY_STATUS_NONE'); + static const BleSecureConnectionStatus BLE_CONNECTION_SECURITY_STATUS_INPROGRESS = BleSecureConnectionStatus._(1, _omitEnumNames ? '' : 'BLE_CONNECTION_SECURITY_STATUS_INPROGRESS'); + static const BleSecureConnectionStatus BLE_CONNECTION_SECURITY_STATUS_ACTIVE = BleSecureConnectionStatus._(2, _omitEnumNames ? '' : 'BLE_CONNECTION_SECURITY_STATUS_ACTIVE'); + static const BleSecureConnectionStatus BLE_CONNECTION_SECURITY_STATUS_REJECTED = BleSecureConnectionStatus._(3, _omitEnumNames ? '' : 'BLE_CONNECTION_SECURITY_STATUS_REJECTED'); + + static const $core.List values = [ + BLE_CONNECTION_SECURITY_STATUS_NONE, + BLE_CONNECTION_SECURITY_STATUS_INPROGRESS, + BLE_CONNECTION_SECURITY_STATUS_ACTIVE, + BLE_CONNECTION_SECURITY_STATUS_REJECTED, + ]; + + static final $core.Map<$core.int, BleSecureConnectionStatus> _byValue = $pb.ProtobufEnum.initByValue(values); + static BleSecureConnectionStatus? valueOf($core.int value) => _byValue[value]; + + const BleSecureConnectionStatus._($core.int v, $core.String n) : super(v, n); +} + +class BleSecureConnectionWindowStatus extends $pb.ProtobufEnum { + static const BleSecureConnectionWindowStatus BLE_SECURE_CONNECTION_WINDOW_STATUS_CLOSED = BleSecureConnectionWindowStatus._(0, _omitEnumNames ? '' : 'BLE_SECURE_CONNECTION_WINDOW_STATUS_CLOSED'); + static const BleSecureConnectionWindowStatus BLE_SECURE_CONNECTION_WINDOW_STATUS_OPEN = BleSecureConnectionWindowStatus._(1, _omitEnumNames ? '' : 'BLE_SECURE_CONNECTION_WINDOW_STATUS_OPEN'); + + static const $core.List values = [ + BLE_SECURE_CONNECTION_WINDOW_STATUS_CLOSED, + BLE_SECURE_CONNECTION_WINDOW_STATUS_OPEN, + ]; + + static final $core.Map<$core.int, BleSecureConnectionWindowStatus> _byValue = $pb.ProtobufEnum.initByValue(values); + static BleSecureConnectionWindowStatus? valueOf($core.int value) => _byValue[value]; + + const BleSecureConnectionWindowStatus._($core.int v, $core.String n) : super(v, n); +} + +class WifiRegionCode_RegionCodeType extends $pb.ProtobufEnum { + static const WifiRegionCode_RegionCodeType ALPHA_2 = WifiRegionCode_RegionCodeType._(0, _omitEnumNames ? '' : 'ALPHA_2'); + static const WifiRegionCode_RegionCodeType ALPHA_3 = WifiRegionCode_RegionCodeType._(1, _omitEnumNames ? '' : 'ALPHA_3'); + static const WifiRegionCode_RegionCodeType NUMERIC = WifiRegionCode_RegionCodeType._(2, _omitEnumNames ? '' : 'NUMERIC'); + static const WifiRegionCode_RegionCodeType UNKNOWN = WifiRegionCode_RegionCodeType._(3, _omitEnumNames ? '' : 'UNKNOWN'); + + static const $core.List values = [ + ALPHA_2, + ALPHA_3, + NUMERIC, + UNKNOWN, + ]; + + static final $core.Map<$core.int, WifiRegionCode_RegionCodeType> _byValue = $pb.ProtobufEnum.initByValue(values); + static WifiRegionCode_RegionCodeType? valueOf($core.int value) => _byValue[value]; + + const WifiRegionCode_RegionCodeType._($core.int v, $core.String n) : super(v, n); +} + + +const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/lib/bluetooth/devices/zwift/protocol/zp.pbjson.dart b/lib/bluetooth/devices/zwift/protocol/zp.pbjson.dart new file mode 100644 index 000000000..d918b5874 --- /dev/null +++ b/lib/bluetooth/devices/zwift/protocol/zp.pbjson.dart @@ -0,0 +1,1692 @@ +// +// Generated code. Do not modify. +// source: zp.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use opcodeDescriptor instead') +const Opcode$json = { + '1': 'Opcode', + '2': [ + {'1': 'GET', '2': 0}, + {'1': 'DEV_INFO_STATUS', '2': 1}, + {'1': 'BLE_SECURITY_REQUEST', '2': 2}, + {'1': 'TRAINER_NOTIF', '2': 3}, + {'1': 'TRAINER_CONFIG_SET', '2': 4}, + {'1': 'TRAINER_CONFIG_STATUS', '2': 5}, + {'1': 'DEV_INFO_SET', '2': 12}, + {'1': 'POWER_OFF', '2': 15}, + {'1': 'RESET', '2': 24}, + {'1': 'BATTERY_NOTIF', '2': 25}, + {'1': 'CONTROLLER_NOTIFICATION', '2': 35}, + {'1': 'LOG_DATA', '2': 42}, + {'1': 'SPINDOWN_REQUEST', '2': 58}, + {'1': 'SPINDOWN_NOTIFICATION', '2': 59}, + {'1': 'GET_RESPONSE', '2': 60}, + {'1': 'STATUS_RESPONSE', '2': 62}, + {'1': 'SET', '2': 63}, + {'1': 'SET_RESPONSE', '2': 64}, + {'1': 'LOG_LEVEL_SET', '2': 65}, + {'1': 'DATA_CHANGE_NOTIFICATION', '2': 66}, + {'1': 'GAME_STATE_NOTIFICATION', '2': 67}, + {'1': 'SENSOR_RELAY_CONFIG', '2': 68}, + {'1': 'SENSOR_RELAY_GET', '2': 69}, + {'1': 'SENSOR_RELAY_RESPONSE', '2': 70}, + {'1': 'SENSOR_RELAY_NOTIFICATION', '2': 71}, + {'1': 'HRM_DATA_NOTIFICATION', '2': 72}, + {'1': 'WIFI_CONFIG_REQUEST', '2': 73}, + {'1': 'WIFI_NOTIFICATION', '2': 74}, + {'1': 'POWER_METER_NOTIFICATION', '2': 75}, + {'1': 'CADENCE_SENSOR_NOTIFICATION', '2': 76}, + {'1': 'DEVICE_UPDATE_REQUEST', '2': 77}, + {'1': 'RELAY_ZP_MESSAGE', '2': 78}, + {'1': 'RIDE_ON', '2': 82}, + {'1': 'RESERVED', '2': 253}, + {'1': 'LOST_CONTROL', '2': 254}, + {'1': 'VENDOR_MESSAGE', '2': 255}, + ], +}; + +/// Descriptor for `Opcode`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List opcodeDescriptor = $convert.base64Decode( + 'CgZPcGNvZGUSBwoDR0VUEAASEwoPREVWX0lORk9fU1RBVFVTEAESGAoUQkxFX1NFQ1VSSVRZX1' + 'JFUVVFU1QQAhIRCg1UUkFJTkVSX05PVElGEAMSFgoSVFJBSU5FUl9DT05GSUdfU0VUEAQSGQoV' + 'VFJBSU5FUl9DT05GSUdfU1RBVFVTEAUSEAoMREVWX0lORk9fU0VUEAwSDQoJUE9XRVJfT0ZGEA' + '8SCQoFUkVTRVQQGBIRCg1CQVRURVJZX05PVElGEBkSGwoXQ09OVFJPTExFUl9OT1RJRklDQVRJ' + 'T04QIxIMCghMT0dfREFUQRAqEhQKEFNQSU5ET1dOX1JFUVVFU1QQOhIZChVTUElORE9XTl9OT1' + 'RJRklDQVRJT04QOxIQCgxHRVRfUkVTUE9OU0UQPBITCg9TVEFUVVNfUkVTUE9OU0UQPhIHCgNT' + 'RVQQPxIQCgxTRVRfUkVTUE9OU0UQQBIRCg1MT0dfTEVWRUxfU0VUEEESHAoYREFUQV9DSEFOR0' + 'VfTk9USUZJQ0FUSU9OEEISGwoXR0FNRV9TVEFURV9OT1RJRklDQVRJT04QQxIXChNTRU5TT1Jf' + 'UkVMQVlfQ09ORklHEEQSFAoQU0VOU09SX1JFTEFZX0dFVBBFEhkKFVNFTlNPUl9SRUxBWV9SRV' + 'NQT05TRRBGEh0KGVNFTlNPUl9SRUxBWV9OT1RJRklDQVRJT04QRxIZChVIUk1fREFUQV9OT1RJ' + 'RklDQVRJT04QSBIXChNXSUZJX0NPTkZJR19SRVFVRVNUEEkSFQoRV0lGSV9OT1RJRklDQVRJT0' + '4QShIcChhQT1dFUl9NRVRFUl9OT1RJRklDQVRJT04QSxIfChtDQURFTkNFX1NFTlNPUl9OT1RJ' + 'RklDQVRJT04QTBIZChVERVZJQ0VfVVBEQVRFX1JFUVVFU1QQTRIUChBSRUxBWV9aUF9NRVNTQU' + 'dFEE4SCwoHUklERV9PThBSEg0KCFJFU0VSVkVEEP0BEhEKDExPU1RfQ09OVFJPTBD+ARITCg5W' + 'RU5ET1JfTUVTU0FHRRD/AQ=='); + +@$core.Deprecated('Use dODescriptor instead') +const DO$json = { + '1': 'DO', + '2': [ + {'1': 'PAGE_DEV_INFO', '2': 0}, + {'1': 'PROTOCOL_VERSION', '2': 1}, + {'1': 'SYSTEM_FW_VERSION', '2': 2}, + {'1': 'DEVICE_NAME', '2': 3}, + {'1': 'SERIAL_NUMBER', '2': 5}, + {'1': 'SYSTEM_HW_REVISION', '2': 6}, + {'1': 'DEVICE_CAPABILITIES', '2': 7}, + {'1': 'MANUFACTURER_ID', '2': 8}, + {'1': 'PRODUCT_ID', '2': 9}, + {'1': 'DEVICE_UID', '2': 10}, + {'1': 'PAGE_CLIENT_SERVER_CONFIGURATION', '2': 16}, + {'1': 'CLIENT_SERVER_NOTIFICATIONS', '2': 17}, + {'1': 'PAGE_DEVICE_UPDATE_INFO', '2': 32}, + {'1': 'DEVICE_UPDATE_STATUS', '2': 33}, + {'1': 'DEVICE_UPDATE_NEW_VERSION', '2': 34}, + {'1': 'PAGE_DATE_TIME', '2': 48}, + {'1': 'UTC_DATE_TIME', '2': 49}, + {'1': 'PAGE_BLE_SECURITY', '2': 64}, + {'1': 'BLE_SECURE_CONNECTION_STATUS', '2': 65}, + {'1': 'BLE_SECURE_CONNECTION_WINDOW_STATUS', '2': 66}, + {'1': 'PAGE_TRAINER_CONFIG', '2': 512}, + {'1': 'TRAINER_MODE', '2': 513}, + {'1': 'CFG_RESISTANCE', '2': 514}, + {'1': 'ERG_POWER', '2': 515}, + {'1': 'AVERAGING_WINDOW', '2': 516}, + {'1': 'SIM_WIND', '2': 517}, + {'1': 'SIM_GRADE', '2': 518}, + {'1': 'SIM_REAL_GEAR_RATIO', '2': 519}, + {'1': 'SIM_VIRT_GEAR_RATIO', '2': 520}, + {'1': 'SIM_CW', '2': 521}, + {'1': 'SIM_WHEEL_DIAMETER', '2': 522}, + {'1': 'SIM_BIKE_MASS', '2': 523}, + {'1': 'SIM_RIDER_MASS', '2': 524}, + {'1': 'SIM_CRR', '2': 525}, + {'1': 'SIM_RESERVED_FRONTAL_AREA', '2': 526}, + {'1': 'SIM_EBRAKE', '2': 527}, + {'1': 'PAGE_TRAINER_GEAR_INDEX_CONFIG', '2': 528}, + {'1': 'FRONT_GEAR_INDEX', '2': 529}, + {'1': 'FRONT_GEAR_INDEX_MAX', '2': 530}, + {'1': 'FRONT_GEAR_INDEX_MIN', '2': 531}, + {'1': 'REAR_GEAR_INDEX', '2': 532}, + {'1': 'REAR_GEAR_INDEX_MAX', '2': 533}, + {'1': 'REAR_GEAR_INDEX_MIN', '2': 534}, + {'1': 'PAGE_TRAINER_CONFIG2', '2': 544}, + {'1': 'HIGH_SPEED_DATA', '2': 545}, + {'1': 'ERG_POWER_SMOOTHING', '2': 546}, + {'1': 'VIRTUAL_SHIFTING_MODE', '2': 547}, + {'1': 'PAGE_DEVICE_TILT_CONFIG', '2': 560}, + {'1': 'DEVICE_TILT_ENABLED', '2': 561}, + {'1': 'DEVICE_TILT_GRADIENT_MIN', '2': 562}, + {'1': 'DEVICE_TILT_GRADIENT_MAX', '2': 563}, + {'1': 'DEVICE_TILT_GRADIENT', '2': 564}, + {'1': 'BATTERY_STATE', '2': 771}, + {'1': 'PAGE_CONTROLLER_INPUT_CONFIG', '2': 1024}, + {'1': 'INPUT_SUPPORTED_DIGITAL_INPUTS', '2': 1025}, + {'1': 'INPUT_SUPPORTED_ANALOG_INPUTS', '2': 1026}, + {'1': 'INPUT_ANALOG_INPUT_RANGE', '2': 1027}, + {'1': 'INPUT_ANALOG_INPUT_DEADZONE', '2': 1028}, + {'1': 'PAGE_WIFI_CONFIGURATION', '2': 1056}, + {'1': 'WIFI_ENABLED', '2': 1057}, + {'1': 'WIFI_STATUS', '2': 1058}, + {'1': 'WIFI_SSID', '2': 1059}, + {'1': 'WIFI_BAND', '2': 1060}, + {'1': 'WIFI_RSSI', '2': 1061}, + {'1': 'WIFI_REGION_CODE', '2': 1062}, + {'1': 'SENSOR_RELAY_DATA_PAGE', '2': 1280}, + {'1': 'SENSOR_RELAY_SUPPORTED_SENSORS', '2': 1281}, + {'1': 'SENSOR_RELAY_PAIRED_SENSORS', '2': 1282}, + ], +}; + +/// Descriptor for `DO`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List dODescriptor = $convert.base64Decode( + 'CgJETxIRCg1QQUdFX0RFVl9JTkZPEAASFAoQUFJPVE9DT0xfVkVSU0lPThABEhUKEVNZU1RFTV' + '9GV19WRVJTSU9OEAISDwoLREVWSUNFX05BTUUQAxIRCg1TRVJJQUxfTlVNQkVSEAUSFgoSU1lT' + 'VEVNX0hXX1JFVklTSU9OEAYSFwoTREVWSUNFX0NBUEFCSUxJVElFUxAHEhMKD01BTlVGQUNUVV' + 'JFUl9JRBAIEg4KClBST0RVQ1RfSUQQCRIOCgpERVZJQ0VfVUlEEAoSJAogUEFHRV9DTElFTlRf' + 'U0VSVkVSX0NPTkZJR1VSQVRJT04QEBIfChtDTElFTlRfU0VSVkVSX05PVElGSUNBVElPTlMQER' + 'IbChdQQUdFX0RFVklDRV9VUERBVEVfSU5GTxAgEhgKFERFVklDRV9VUERBVEVfU1RBVFVTECES' + 'HQoZREVWSUNFX1VQREFURV9ORVdfVkVSU0lPThAiEhIKDlBBR0VfREFURV9USU1FEDASEQoNVV' + 'RDX0RBVEVfVElNRRAxEhUKEVBBR0VfQkxFX1NFQ1VSSVRZEEASIAocQkxFX1NFQ1VSRV9DT05O' + 'RUNUSU9OX1NUQVRVUxBBEicKI0JMRV9TRUNVUkVfQ09OTkVDVElPTl9XSU5ET1dfU1RBVFVTEE' + 'ISGAoTUEFHRV9UUkFJTkVSX0NPTkZJRxCABBIRCgxUUkFJTkVSX01PREUQgQQSEwoOQ0ZHX1JF' + 'U0lTVEFOQ0UQggQSDgoJRVJHX1BPV0VSEIMEEhUKEEFWRVJBR0lOR19XSU5ET1cQhAQSDQoIU0' + 'lNX1dJTkQQhQQSDgoJU0lNX0dSQURFEIYEEhgKE1NJTV9SRUFMX0dFQVJfUkFUSU8QhwQSGAoT' + 'U0lNX1ZJUlRfR0VBUl9SQVRJTxCIBBILCgZTSU1fQ1cQiQQSFwoSU0lNX1dIRUVMX0RJQU1FVE' + 'VSEIoEEhIKDVNJTV9CSUtFX01BU1MQiwQSEwoOU0lNX1JJREVSX01BU1MQjAQSDAoHU0lNX0NS' + 'UhCNBBIeChlTSU1fUkVTRVJWRURfRlJPTlRBTF9BUkVBEI4EEg8KClNJTV9FQlJBS0UQjwQSIw' + 'oeUEFHRV9UUkFJTkVSX0dFQVJfSU5ERVhfQ09ORklHEJAEEhUKEEZST05UX0dFQVJfSU5ERVgQ' + 'kQQSGQoURlJPTlRfR0VBUl9JTkRFWF9NQVgQkgQSGQoURlJPTlRfR0VBUl9JTkRFWF9NSU4Qkw' + 'QSFAoPUkVBUl9HRUFSX0lOREVYEJQEEhgKE1JFQVJfR0VBUl9JTkRFWF9NQVgQlQQSGAoTUkVB' + 'Ul9HRUFSX0lOREVYX01JThCWBBIZChRQQUdFX1RSQUlORVJfQ09ORklHMhCgBBIUCg9ISUdIX1' + 'NQRUVEX0RBVEEQoQQSGAoTRVJHX1BPV0VSX1NNT09USElORxCiBBIaChVWSVJUVUFMX1NISUZU' + 'SU5HX01PREUQowQSHAoXUEFHRV9ERVZJQ0VfVElMVF9DT05GSUcQsAQSGAoTREVWSUNFX1RJTF' + 'RfRU5BQkxFRBCxBBIdChhERVZJQ0VfVElMVF9HUkFESUVOVF9NSU4QsgQSHQoYREVWSUNFX1RJ' + 'TFRfR1JBRElFTlRfTUFYELMEEhkKFERFVklDRV9USUxUX0dSQURJRU5UELQEEhIKDUJBVFRFUl' + 'lfU1RBVEUQgwYSIQocUEFHRV9DT05UUk9MTEVSX0lOUFVUX0NPTkZJRxCACBIjCh5JTlBVVF9T' + 'VVBQT1JURURfRElHSVRBTF9JTlBVVFMQgQgSIgodSU5QVVRfU1VQUE9SVEVEX0FOQUxPR19JTl' + 'BVVFMQgggSHQoYSU5QVVRfQU5BTE9HX0lOUFVUX1JBTkdFEIMIEiAKG0lOUFVUX0FOQUxPR19J' + 'TlBVVF9ERUFEWk9ORRCECBIcChdQQUdFX1dJRklfQ09ORklHVVJBVElPThCgCBIRCgxXSUZJX0' + 'VOQUJMRUQQoQgSEAoLV0lGSV9TVEFUVVMQoggSDgoJV0lGSV9TU0lEEKMIEg4KCVdJRklfQkFO' + 'RBCkCBIOCglXSUZJX1JTU0kQpQgSFQoQV0lGSV9SRUdJT05fQ09ERRCmCBIbChZTRU5TT1JfUk' + 'VMQVlfREFUQV9QQUdFEIAKEiMKHlNFTlNPUl9SRUxBWV9TVVBQT1JURURfU0VOU09SUxCBChIg' + 'ChtTRU5TT1JfUkVMQVlfUEFJUkVEX1NFTlNPUlMQggo='); + +@$core.Deprecated('Use statusDescriptor instead') +const Status$json = { + '1': 'Status', + '2': [ + {'1': 'SUCCESS', '2': 0}, + {'1': 'FAILURE', '2': 1}, + {'1': 'BUSY', '2': 2}, + {'1': 'INVALID_PARAM', '2': 3}, + {'1': 'NOT_PERMITTED', '2': 4}, + {'1': 'NOT_SUPPORTED', '2': 5}, + {'1': 'INVALID_MODE', '2': 6}, + ], +}; + +/// Descriptor for `Status`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List statusDescriptor = $convert.base64Decode( + 'CgZTdGF0dXMSCwoHU1VDQ0VTUxAAEgsKB0ZBSUxVUkUQARIICgRCVVNZEAISEQoNSU5WQUxJRF' + '9QQVJBTRADEhEKDU5PVF9QRVJNSVRURUQQBBIRCg1OT1RfU1VQUE9SVEVEEAUSEAoMSU5WQUxJ' + 'RF9NT0RFEAY='); + +@$core.Deprecated('Use deviceTypeDescriptor instead') +const DeviceType$json = { + '1': 'DeviceType', + '2': [ + {'1': 'UNDEFINED', '2': 0}, + {'1': 'CYCLING_TURBO_TRAINER', '2': 1}, + {'1': 'USER_INPUT_DEVICE', '2': 2}, + {'1': 'TREADMILL', '2': 3}, + {'1': 'SENSOR_RELAY', '2': 4}, + {'1': 'HEART_RATE_MONITOR', '2': 5}, + {'1': 'POWER_METER', '2': 6}, + {'1': 'CADENCE_SENSOR', '2': 7}, + {'1': 'WIFI', '2': 8}, + ], +}; + +/// Descriptor for `DeviceType`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List deviceTypeDescriptor = $convert.base64Decode( + 'CgpEZXZpY2VUeXBlEg0KCVVOREVGSU5FRBAAEhkKFUNZQ0xJTkdfVFVSQk9fVFJBSU5FUhABEh' + 'UKEVVTRVJfSU5QVVRfREVWSUNFEAISDQoJVFJFQURNSUxMEAMSEAoMU0VOU09SX1JFTEFZEAQS' + 'FgoSSEVBUlRfUkFURV9NT05JVE9SEAUSDwoLUE9XRVJfTUVURVIQBhISCg5DQURFTkNFX1NFTl' + 'NPUhAHEggKBFdJRkkQCA=='); + +@$core.Deprecated('Use trainerModeDescriptor instead') +const TrainerMode$json = { + '1': 'TrainerMode', + '2': [ + {'1': 'MODE_UNKNOWN', '2': 0}, + {'1': 'MODE_ERG', '2': 1}, + {'1': 'MODE_RESISTANCE', '2': 2}, + {'1': 'MODE_SIM', '2': 3}, + ], +}; + +/// Descriptor for `TrainerMode`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List trainerModeDescriptor = $convert.base64Decode( + 'CgtUcmFpbmVyTW9kZRIQCgxNT0RFX1VOS05PV04QABIMCghNT0RFX0VSRxABEhMKD01PREVfUk' + 'VTSVNUQU5DRRACEgwKCE1PREVfU0lNEAM='); + +@$core.Deprecated('Use chargingStateDescriptor instead') +const ChargingState$json = { + '1': 'ChargingState', + '2': [ + {'1': 'CHARGING_IDLE', '2': 0}, + {'1': 'CHARGING_PROGRESS', '2': 1}, + {'1': 'CHARGING_DONE', '2': 2}, + ], +}; + +/// Descriptor for `ChargingState`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List chargingStateDescriptor = $convert.base64Decode( + 'Cg1DaGFyZ2luZ1N0YXRlEhEKDUNIQVJHSU5HX0lETEUQABIVChFDSEFSR0lOR19QUk9HUkVTUx' + 'ABEhEKDUNIQVJHSU5HX0RPTkUQAg=='); + +@$core.Deprecated('Use spindownStatusDescriptor instead') +const SpindownStatus$json = { + '1': 'SpindownStatus', + '2': [ + {'1': 'SPINDOWN_IDLE', '2': 0}, + {'1': 'SPINDOWN_REQUESTED', '2': 1}, + {'1': 'SPINDOWN_SUCCESS', '2': 2}, + {'1': 'SPINDOWN_ERROR', '2': 3}, + {'1': 'SPINDOWN_STOP_PEDALLING', '2': 4}, + {'1': 'SPINDOWN_ERROR_TIMEOUT', '2': 5}, + {'1': 'SPINDOWN_ERROR_TOSHORT', '2': 6}, + {'1': 'SPINDOWN_ERROR_TOSLOW', '2': 7}, + {'1': 'SPINDOWN_ERROR_TOFAST', '2': 8}, + {'1': 'SPINDOWN_ERROR_SAMPLEERROR', '2': 9}, + {'1': 'SPINDOWN_ERROR_ABORT', '2': 10}, + ], +}; + +/// Descriptor for `SpindownStatus`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List spindownStatusDescriptor = $convert.base64Decode( + 'Cg5TcGluZG93blN0YXR1cxIRCg1TUElORE9XTl9JRExFEAASFgoSU1BJTkRPV05fUkVRVUVTVE' + 'VEEAESFAoQU1BJTkRPV05fU1VDQ0VTUxACEhIKDlNQSU5ET1dOX0VSUk9SEAMSGwoXU1BJTkRP' + 'V05fU1RPUF9QRURBTExJTkcQBBIaChZTUElORE9XTl9FUlJPUl9USU1FT1VUEAUSGgoWU1BJTk' + 'RPV05fRVJST1JfVE9TSE9SVBAGEhkKFVNQSU5ET1dOX0VSUk9SX1RPU0xPVxAHEhkKFVNQSU5E' + 'T1dOX0VSUk9SX1RPRkFTVBAIEh4KGlNQSU5ET1dOX0VSUk9SX1NBTVBMRUVSUk9SEAkSGAoUU1' + 'BJTkRPV05fRVJST1JfQUJPUlQQCg=='); + +@$core.Deprecated('Use logLevelDescriptor instead') +const LogLevel$json = { + '1': 'LogLevel', + '2': [ + {'1': 'LOGLEVEL_OFF', '2': 0}, + {'1': 'LOGLEVEL_ERROR', '2': 1}, + {'1': 'LOGLEVEL_WARNING', '2': 2}, + {'1': 'LOGLEVEL_INFO', '2': 3}, + {'1': 'LOGLEVEL_DEBUG', '2': 4}, + {'1': 'LOGLEVEL_TRACE', '2': 5}, + ], +}; + +/// Descriptor for `LogLevel`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List logLevelDescriptor = $convert.base64Decode( + 'CghMb2dMZXZlbBIQCgxMT0dMRVZFTF9PRkYQABISCg5MT0dMRVZFTF9FUlJPUhABEhQKEExPR0' + 'xFVkVMX1dBUk5JTkcQAhIRCg1MT0dMRVZFTF9JTkZPEAMSEgoOTE9HTEVWRUxfREVCVUcQBBIS' + 'Cg5MT0dMRVZFTF9UUkFDRRAF'); + +@$core.Deprecated('Use roadSurfaceTypeDescriptor instead') +const RoadSurfaceType$json = { + '1': 'RoadSurfaceType', + '2': [ + {'1': 'ROAD_SURFACE_SMOOTH_TARMAC', '2': 0}, + {'1': 'ROAD_SURFACE_BRICK_ROAD', '2': 1}, + {'1': 'ROAD_SURFACE_HARD_COBBLES', '2': 2}, + {'1': 'ROAD_SURFACE_SOFT_COBBLES', '2': 3}, + {'1': 'ROAD_SURFACE_NARROW_WOODEN_PLANKS', '2': 4}, + {'1': 'ROAD_SURFACE_WIDE_WOODEN_PLANKS', '2': 5}, + {'1': 'ROAD_SURFACE_DIRT', '2': 6}, + {'1': 'ROAD_SURFACE_GRAVEL', '2': 7}, + {'1': 'ROAD_SURFACE_CATTLE_GRID', '2': 8}, + {'1': 'ROAD_SURFACE_CONCRETE_FLAG_STONES', '2': 9}, + {'1': 'ROAD_SURFACE_ICE', '2': 10}, + ], +}; + +/// Descriptor for `RoadSurfaceType`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List roadSurfaceTypeDescriptor = $convert.base64Decode( + 'Cg9Sb2FkU3VyZmFjZVR5cGUSHgoaUk9BRF9TVVJGQUNFX1NNT09USF9UQVJNQUMQABIbChdST0' + 'FEX1NVUkZBQ0VfQlJJQ0tfUk9BRBABEh0KGVJPQURfU1VSRkFDRV9IQVJEX0NPQkJMRVMQAhId' + 'ChlST0FEX1NVUkZBQ0VfU09GVF9DT0JCTEVTEAMSJQohUk9BRF9TVVJGQUNFX05BUlJPV19XT0' + '9ERU5fUExBTktTEAQSIwofUk9BRF9TVVJGQUNFX1dJREVfV09PREVOX1BMQU5LUxAFEhUKEVJP' + 'QURfU1VSRkFDRV9ESVJUEAYSFwoTUk9BRF9TVVJGQUNFX0dSQVZFTBAHEhwKGFJPQURfU1VSRk' + 'FDRV9DQVRUTEVfR1JJRBAIEiUKIVJPQURfU1VSRkFDRV9DT05DUkVURV9GTEFHX1NUT05FUxAJ' + 'EhQKEFJPQURfU1VSRkFDRV9JQ0UQCg=='); + +@$core.Deprecated('Use wifiStatusCodeDescriptor instead') +const WifiStatusCode$json = { + '1': 'WifiStatusCode', + '2': [ + {'1': 'WIFI_STATUS_DISABLED', '2': 0}, + {'1': 'WIFI_STATUS_NOT_PROVISIONED', '2': 1}, + {'1': 'WIFI_STATUS_SCANNING', '2': 2}, + {'1': 'WIFI_STATUS_DISCONNECTED', '2': 3}, + {'1': 'WIFI_STATUS_CONNECTING', '2': 4}, + {'1': 'WIFI_STATUS_CONNECTED', '2': 5}, + {'1': 'WIFI_STATUS_ERROR', '2': 6}, + ], +}; + +/// Descriptor for `WifiStatusCode`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List wifiStatusCodeDescriptor = $convert.base64Decode( + 'Cg5XaWZpU3RhdHVzQ29kZRIYChRXSUZJX1NUQVRVU19ESVNBQkxFRBAAEh8KG1dJRklfU1RBVF' + 'VTX05PVF9QUk9WSVNJT05FRBABEhgKFFdJRklfU1RBVFVTX1NDQU5OSU5HEAISHAoYV0lGSV9T' + 'VEFUVVNfRElTQ09OTkVDVEVEEAMSGgoWV0lGSV9TVEFUVVNfQ09OTkVDVElORxAEEhkKFVdJRk' + 'lfU1RBVFVTX0NPTk5FQ1RFRBAFEhUKEVdJRklfU1RBVFVTX0VSUk9SEAY='); + +@$core.Deprecated('Use wifiErrorCodeDescriptor instead') +const WifiErrorCode$json = { + '1': 'WifiErrorCode', + '2': [ + {'1': 'WIFI_ERROR_UNKNOWN', '2': 0}, + {'1': 'WIFI_ERROR_NO_MEMORY', '2': 1}, + {'1': 'WIFI_ERROR_INVALID_PARAMETERS', '2': 2}, + {'1': 'WIFI_ERROR_INVALID_STATE', '2': 3}, + {'1': 'WIFI_ERROR_NOT_FOUND', '2': 4}, + {'1': 'WIFI_ERROR_NOT_SUPPORTED', '2': 5}, + {'1': 'WIFI_ERROR_NOT_ALLOWED', '2': 6}, + {'1': 'WIFI_ERROR_NOT_INITIALISED', '2': 7}, + {'1': 'WIFI_ERROR_NOT_STARTED', '2': 8}, + {'1': 'WIFI_ERROR_TIMEOUT', '2': 9}, + {'1': 'WIFI_ERROR_MODE', '2': 10}, + {'1': 'WIFI_ERROR_SSID_INVALID', '2': 11}, + ], +}; + +/// Descriptor for `WifiErrorCode`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List wifiErrorCodeDescriptor = $convert.base64Decode( + 'Cg1XaWZpRXJyb3JDb2RlEhYKEldJRklfRVJST1JfVU5LTk9XThAAEhgKFFdJRklfRVJST1JfTk' + '9fTUVNT1JZEAESIQodV0lGSV9FUlJPUl9JTlZBTElEX1BBUkFNRVRFUlMQAhIcChhXSUZJX0VS' + 'Uk9SX0lOVkFMSURfU1RBVEUQAxIYChRXSUZJX0VSUk9SX05PVF9GT1VORBAEEhwKGFdJRklfRV' + 'JST1JfTk9UX1NVUFBPUlRFRBAFEhoKFldJRklfRVJST1JfTk9UX0FMTE9XRUQQBhIeChpXSUZJ' + 'X0VSUk9SX05PVF9JTklUSUFMSVNFRBAHEhoKFldJRklfRVJST1JfTk9UX1NUQVJURUQQCBIWCh' + 'JXSUZJX0VSUk9SX1RJTUVPVVQQCRITCg9XSUZJX0VSUk9SX01PREUQChIbChdXSUZJX0VSUk9S' + 'X1NTSURfSU5WQUxJRBAL'); + +@$core.Deprecated('Use interfaceTypeDescriptor instead') +const InterfaceType$json = { + '1': 'InterfaceType', + '2': [ + {'1': 'INTERFACE_BLE', '2': 1}, + {'1': 'INTERFACE_ANT', '2': 2}, + {'1': 'INTERFACE_USB', '2': 3}, + {'1': 'INTERFACE_ETH', '2': 4}, + {'1': 'INTERFACE_WIFI', '2': 5}, + ], +}; + +/// Descriptor for `InterfaceType`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List interfaceTypeDescriptor = $convert.base64Decode( + 'Cg1JbnRlcmZhY2VUeXBlEhEKDUlOVEVSRkFDRV9CTEUQARIRCg1JTlRFUkZBQ0VfQU5UEAISEQ' + 'oNSU5URVJGQUNFX1VTQhADEhEKDUlOVEVSRkFDRV9FVEgQBBISCg5JTlRFUkZBQ0VfV0lGSRAF'); + +@$core.Deprecated('Use sensorConnectionStatusDescriptor instead') +const SensorConnectionStatus$json = { + '1': 'SensorConnectionStatus', + '2': [ + {'1': 'SENSOR_STATUS_DISCOVERED', '2': 1}, + {'1': 'SENSOR_STATUS_DISCONNECTED', '2': 2}, + {'1': 'SENSOR_STATUS_PAIRING', '2': 3}, + {'1': 'SENSOR_STATUS_CONNECTED', '2': 4}, + ], +}; + +/// Descriptor for `SensorConnectionStatus`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List sensorConnectionStatusDescriptor = $convert.base64Decode( + 'ChZTZW5zb3JDb25uZWN0aW9uU3RhdHVzEhwKGFNFTlNPUl9TVEFUVVNfRElTQ09WRVJFRBABEh' + '4KGlNFTlNPUl9TVEFUVVNfRElTQ09OTkVDVEVEEAISGQoVU0VOU09SX1NUQVRVU19QQUlSSU5H' + 'EAMSGwoXU0VOU09SX1NUQVRVU19DT05ORUNURUQQBA=='); + +@$core.Deprecated('Use bleSecureConnectionStatusDescriptor instead') +const BleSecureConnectionStatus$json = { + '1': 'BleSecureConnectionStatus', + '2': [ + {'1': 'BLE_CONNECTION_SECURITY_STATUS_NONE', '2': 0}, + {'1': 'BLE_CONNECTION_SECURITY_STATUS_INPROGRESS', '2': 1}, + {'1': 'BLE_CONNECTION_SECURITY_STATUS_ACTIVE', '2': 2}, + {'1': 'BLE_CONNECTION_SECURITY_STATUS_REJECTED', '2': 3}, + ], +}; + +/// Descriptor for `BleSecureConnectionStatus`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List bleSecureConnectionStatusDescriptor = $convert.base64Decode( + 'ChlCbGVTZWN1cmVDb25uZWN0aW9uU3RhdHVzEicKI0JMRV9DT05ORUNUSU9OX1NFQ1VSSVRZX1' + 'NUQVRVU19OT05FEAASLQopQkxFX0NPTk5FQ1RJT05fU0VDVVJJVFlfU1RBVFVTX0lOUFJPR1JF' + 'U1MQARIpCiVCTEVfQ09OTkVDVElPTl9TRUNVUklUWV9TVEFUVVNfQUNUSVZFEAISKwonQkxFX0' + 'NPTk5FQ1RJT05fU0VDVVJJVFlfU1RBVFVTX1JFSkVDVEVEEAM='); + +@$core.Deprecated('Use bleSecureConnectionWindowStatusDescriptor instead') +const BleSecureConnectionWindowStatus$json = { + '1': 'BleSecureConnectionWindowStatus', + '2': [ + {'1': 'BLE_SECURE_CONNECTION_WINDOW_STATUS_CLOSED', '2': 0}, + {'1': 'BLE_SECURE_CONNECTION_WINDOW_STATUS_OPEN', '2': 1}, + ], +}; + +/// Descriptor for `BleSecureConnectionWindowStatus`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List bleSecureConnectionWindowStatusDescriptor = $convert.base64Decode( + 'Ch9CbGVTZWN1cmVDb25uZWN0aW9uV2luZG93U3RhdHVzEi4KKkJMRV9TRUNVUkVfQ09OTkVDVE' + 'lPTl9XSU5ET1dfU1RBVFVTX0NMT1NFRBAAEiwKKEJMRV9TRUNVUkVfQ09OTkVDVElPTl9XSU5E' + 'T1dfU1RBVFVTX09QRU4QAQ=='); + +@$core.Deprecated('Use trainerEnvSimDescriptor instead') +const TrainerEnvSim$json = { + '1': 'TrainerEnvSim', + '2': [ + {'1': 'simulatedWind', '3': 1, '4': 1, '5': 17, '8': {}, '10': 'simulatedWind'}, + {'1': 'simulatedGrade', '3': 2, '4': 1, '5': 17, '8': {}, '10': 'simulatedGrade'}, + {'1': 'simulatedCW', '3': 3, '4': 1, '5': 13, '8': {}, '10': 'simulatedCW'}, + {'1': 'simulatedCRR', '3': 4, '4': 1, '5': 13, '8': {}, '10': 'simulatedCRR'}, + ], +}; + +/// Descriptor for `TrainerEnvSim`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List trainerEnvSimDescriptor = $convert.base64Decode( + 'Cg1UcmFpbmVyRW52U2ltEisKDXNpbXVsYXRlZFdpbmQYASABKBFCBZI/AjgQUg1zaW11bGF0ZW' + 'RXaW5kEi0KDnNpbXVsYXRlZEdyYWRlGAIgASgRQgWSPwI4EFIOc2ltdWxhdGVkR3JhZGUSJwoL' + 'c2ltdWxhdGVkQ1cYAyABKA1CBZI/AjgQUgtzaW11bGF0ZWRDVxIpCgxzaW11bGF0ZWRDUlIYBC' + 'ABKA1CBZI/AjgQUgxzaW11bGF0ZWRDUlI='); + +@$core.Deprecated('Use trainerBikeSimDescriptor instead') +const TrainerBikeSim$json = { + '1': 'TrainerBikeSim', + '2': [ + {'1': 'simulatedRealGearRatio', '3': 1, '4': 1, '5': 13, '8': {}, '10': 'simulatedRealGearRatio'}, + {'1': 'simulatedVirtualGearRatio', '3': 2, '4': 1, '5': 13, '8': {}, '10': 'simulatedVirtualGearRatio'}, + {'1': 'simulatedWheelDiameter', '3': 3, '4': 1, '5': 13, '8': {}, '10': 'simulatedWheelDiameter'}, + {'1': 'simulatedBikeMass', '3': 4, '4': 1, '5': 13, '8': {}, '10': 'simulatedBikeMass'}, + {'1': 'simulatedRiderMass', '3': 5, '4': 1, '5': 13, '8': {}, '10': 'simulatedRiderMass'}, + {'1': 'simulatedFrontalArea', '3': 6, '4': 1, '5': 13, '8': {}, '10': 'simulatedFrontalArea'}, + {'1': 'eBrake', '3': 7, '4': 1, '5': 13, '8': {}, '10': 'eBrake'}, + ], +}; + +/// Descriptor for `TrainerBikeSim`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List trainerBikeSimDescriptor = $convert.base64Decode( + 'Cg5UcmFpbmVyQmlrZVNpbRI9ChZzaW11bGF0ZWRSZWFsR2VhclJhdGlvGAEgASgNQgWSPwI4EF' + 'IWc2ltdWxhdGVkUmVhbEdlYXJSYXRpbxJDChlzaW11bGF0ZWRWaXJ0dWFsR2VhclJhdGlvGAIg' + 'ASgNQgWSPwI4EFIZc2ltdWxhdGVkVmlydHVhbEdlYXJSYXRpbxI9ChZzaW11bGF0ZWRXaGVlbE' + 'RpYW1ldGVyGAMgASgNQgWSPwI4EFIWc2ltdWxhdGVkV2hlZWxEaWFtZXRlchIzChFzaW11bGF0' + 'ZWRCaWtlTWFzcxgEIAEoDUIFkj8COBBSEXNpbXVsYXRlZEJpa2VNYXNzEjUKEnNpbXVsYXRlZF' + 'JpZGVyTWFzcxgFIAEoDUIFkj8COBBSEnNpbXVsYXRlZFJpZGVyTWFzcxI5ChRzaW11bGF0ZWRG' + 'cm9udGFsQXJlYRgGIAEoDUIFkj8COBBSFHNpbXVsYXRlZEZyb250YWxBcmVhEh0KBmVCcmFrZR' + 'gHIAEoDUIFkj8COBBSBmVCcmFrZQ=='); + +@$core.Deprecated('Use controllerAnalogEventDescriptor instead') +const ControllerAnalogEvent$json = { + '1': 'ControllerAnalogEvent', + '2': [ + {'1': 'sensorId', '3': 1, '4': 1, '5': 13, '8': {}, '10': 'sensorId'}, + {'1': 'value', '3': 2, '4': 1, '5': 17, '8': {}, '10': 'value'}, + ], +}; + +/// Descriptor for `ControllerAnalogEvent`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List controllerAnalogEventDescriptor = $convert.base64Decode( + 'ChVDb250cm9sbGVyQW5hbG9nRXZlbnQSIQoIc2Vuc29ySWQYASABKA1CBZI/AjgIUghzZW5zb3' + 'JJZBIbCgV2YWx1ZRgCIAEoEUIFkj8COBBSBXZhbHVl'); + +@$core.Deprecated('Use inputAnalogRangeDescriptor instead') +const InputAnalogRange$json = { + '1': 'InputAnalogRange', + '2': [ + {'1': 'sensorId', '3': 1, '4': 1, '5': 13, '8': {}, '10': 'sensorId'}, + {'1': 'minAnalogValue', '3': 2, '4': 1, '5': 17, '8': {}, '10': 'minAnalogValue'}, + {'1': 'maxAnalogValue', '3': 3, '4': 1, '5': 17, '8': {}, '10': 'maxAnalogValue'}, + ], +}; + +/// Descriptor for `InputAnalogRange`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List inputAnalogRangeDescriptor = $convert.base64Decode( + 'ChBJbnB1dEFuYWxvZ1JhbmdlEiEKCHNlbnNvcklkGAEgASgNQgWSPwI4CFIIc2Vuc29ySWQSLQ' + 'oObWluQW5hbG9nVmFsdWUYAiABKBFCBZI/AjgQUg5taW5BbmFsb2dWYWx1ZRItCg5tYXhBbmFs' + 'b2dWYWx1ZRgDIAEoEUIFkj8COBBSDm1heEFuYWxvZ1ZhbHVl'); + +@$core.Deprecated('Use inputAnalogDeadzoneDescriptor instead') +const InputAnalogDeadzone$json = { + '1': 'InputAnalogDeadzone', + '2': [ + {'1': 'sensorId', '3': 1, '4': 1, '5': 13, '8': {}, '10': 'sensorId'}, + {'1': 'negDeadzoneValue', '3': 2, '4': 1, '5': 17, '8': {}, '10': 'negDeadzoneValue'}, + {'1': 'posDeadzoneValue', '3': 3, '4': 1, '5': 17, '8': {}, '10': 'posDeadzoneValue'}, + ], +}; + +/// Descriptor for `InputAnalogDeadzone`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List inputAnalogDeadzoneDescriptor = $convert.base64Decode( + 'ChNJbnB1dEFuYWxvZ0RlYWR6b25lEiEKCHNlbnNvcklkGAEgASgNQgWSPwI4CFIIc2Vuc29ySW' + 'QSMQoQbmVnRGVhZHpvbmVWYWx1ZRgCIAEoEUIFkj8COBBSEG5lZ0RlYWR6b25lVmFsdWUSMQoQ' + 'cG9zRGVhZHpvbmVWYWx1ZRgDIAEoEUIFkj8COBBSEHBvc0RlYWR6b25lVmFsdWU='); + +@$core.Deprecated('Use wifiNetworkDescriptor instead') +const WifiNetwork$json = { + '1': 'WifiNetwork', + '2': [ + {'1': 'networkId', '3': 1, '4': 1, '5': 13, '8': {}, '10': 'networkId'}, + {'1': 'ssid', '3': 2, '4': 1, '5': 12, '8': {}, '10': 'ssid'}, + {'1': 'password', '3': 3, '4': 1, '5': 12, '8': {}, '10': 'password'}, + ], +}; + +/// Descriptor for `WifiNetwork`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List wifiNetworkDescriptor = $convert.base64Decode( + 'CgtXaWZpTmV0d29yaxIjCgluZXR3b3JrSWQYASABKA1CBZI/AjgIUgluZXR3b3JrSWQSGQoEc3' + 'NpZBgCIAEoDEIFkj8CCCBSBHNzaWQSIQoIcGFzc3dvcmQYAyABKAxCBZI/AghAUghwYXNzd29y' + 'ZA=='); + +@$core.Deprecated('Use wifiRegionCodeDescriptor instead') +const WifiRegionCode$json = { + '1': 'WifiRegionCode', + '2': [ + {'1': 'regionCodeType', '3': 1, '4': 1, '5': 14, '6': '.Zp.WifiRegionCode.RegionCodeType', '10': 'regionCodeType'}, + {'1': 'regionCode', '3': 2, '4': 1, '5': 12, '8': {}, '10': 'regionCode'}, + ], + '4': [WifiRegionCode_RegionCodeType$json], +}; + +@$core.Deprecated('Use wifiRegionCodeDescriptor instead') +const WifiRegionCode_RegionCodeType$json = { + '1': 'RegionCodeType', + '2': [ + {'1': 'ALPHA_2', '2': 0}, + {'1': 'ALPHA_3', '2': 1}, + {'1': 'NUMERIC', '2': 2}, + {'1': 'UNKNOWN', '2': 3}, + ], +}; + +/// Descriptor for `WifiRegionCode`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List wifiRegionCodeDescriptor = $convert.base64Decode( + 'Cg5XaWZpUmVnaW9uQ29kZRJJCg5yZWdpb25Db2RlVHlwZRgBIAEoDjIhLlpwLldpZmlSZWdpb2' + '5Db2RlLlJlZ2lvbkNvZGVUeXBlUg5yZWdpb25Db2RlVHlwZRIlCgpyZWdpb25Db2RlGAIgASgM' + 'QgWSPwIIA1IKcmVnaW9uQ29kZSJECg5SZWdpb25Db2RlVHlwZRILCgdBTFBIQV8yEAASCwoHQU' + 'xQSEFfMxABEgsKB05VTUVSSUMQAhILCgdVTktOT1dOEAM='); + +@$core.Deprecated('Use wifiNetworkDetailsDescriptor instead') +const WifiNetworkDetails$json = { + '1': 'WifiNetworkDetails', + '2': [ + {'1': 'networkId', '3': 1, '4': 1, '5': 13, '8': {}, '10': 'networkId'}, + {'1': 'bssid', '3': 2, '4': 1, '5': 12, '8': {}, '10': 'bssid'}, + {'1': 'ssid', '3': 3, '4': 1, '5': 12, '8': {}, '10': 'ssid'}, + {'1': 'securityType', '3': 4, '4': 1, '5': 13, '8': {}, '10': 'securityType'}, + {'1': 'band', '3': 5, '4': 1, '5': 13, '8': {}, '10': 'band'}, + {'1': 'rssi', '3': 6, '4': 1, '5': 17, '8': {}, '10': 'rssi'}, + ], +}; + +/// Descriptor for `WifiNetworkDetails`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List wifiNetworkDetailsDescriptor = $convert.base64Decode( + 'ChJXaWZpTmV0d29ya0RldGFpbHMSIwoJbmV0d29ya0lkGAEgASgNQgWSPwI4CFIJbmV0d29ya0' + 'lkEh0KBWJzc2lkGAIgASgMQgeSPwQIBngBUgVic3NpZBIZCgRzc2lkGAMgASgMQgWSPwIIIFIE' + 'c3NpZBIpCgxzZWN1cml0eVR5cGUYBCABKA1CBZI/AjgIUgxzZWN1cml0eVR5cGUSGQoEYmFuZB' + 'gFIAEoDUIFkj8COAhSBGJhbmQSGQoEcnNzaRgGIAEoEUIFkj8COAhSBHJzc2k='); + +@$core.Deprecated('Use sensorInfoDescriptor instead') +const SensorInfo$json = { + '1': 'SensorInfo', + '2': [ + {'1': 'relayAssignedId', '3': 1, '4': 1, '5': 13, '8': {}, '10': 'relayAssignedId'}, + {'1': 'sensorName', '3': 2, '4': 1, '5': 12, '8': {}, '10': 'sensorName'}, + {'1': 'sensorAddress', '3': 4, '4': 1, '5': 12, '8': {}, '10': 'sensorAddress'}, + {'1': 'interfaceType', '3': 5, '4': 1, '5': 14, '6': '.Zp.InterfaceType', '10': 'interfaceType'}, + {'1': 'connectionStatus', '3': 6, '4': 1, '5': 14, '6': '.Zp.SensorConnectionStatus', '10': 'connectionStatus'}, + {'1': 'deviceTypes', '3': 7, '4': 3, '5': 14, '6': '.Zp.DeviceType', '8': {}, '10': 'deviceTypes'}, + {'1': 'supportsZp', '3': 8, '4': 1, '5': 8, '10': 'supportsZp'}, + ], + '9': [ + {'1': 3, '2': 4}, + ], +}; + +/// Descriptor for `SensorInfo`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List sensorInfoDescriptor = $convert.base64Decode( + 'CgpTZW5zb3JJbmZvEi8KD3JlbGF5QXNzaWduZWRJZBgBIAEoDUIFkj8COAhSD3JlbGF5QXNzaW' + 'duZWRJZBIlCgpzZW5zb3JOYW1lGAIgASgMQgWSPwIIIFIKc2Vuc29yTmFtZRItCg1zZW5zb3JB' + 'ZGRyZXNzGAQgASgMQgeSPwQIBngBUg1zZW5zb3JBZGRyZXNzEjcKDWludGVyZmFjZVR5cGUYBS' + 'ABKA4yES5acC5JbnRlcmZhY2VUeXBlUg1pbnRlcmZhY2VUeXBlEkYKEGNvbm5lY3Rpb25TdGF0' + 'dXMYBiABKA4yGi5acC5TZW5zb3JDb25uZWN0aW9uU3RhdHVzUhBjb25uZWN0aW9uU3RhdHVzEj' + 'cKC2RldmljZVR5cGVzGAcgAygOMg4uWnAuRGV2aWNlVHlwZUIFkj8CEAhSC2RldmljZVR5cGVz' + 'Eh4KCnN1cHBvcnRzWnAYCCABKAhSCnN1cHBvcnRzWnBKBAgDEAQ='); + +@$core.Deprecated('Use sensorInfoListDescriptor instead') +const SensorInfoList$json = { + '1': 'SensorInfoList', + '2': [ + {'1': 'sensorInfo', '3': 1, '4': 3, '5': 11, '6': '.Zp.SensorInfo', '8': {}, '10': 'sensorInfo'}, + ], +}; + +/// Descriptor for `SensorInfoList`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List sensorInfoListDescriptor = $convert.base64Decode( + 'Cg5TZW5zb3JJbmZvTGlzdBI1CgpzZW5zb3JJbmZvGAEgAygLMg4uWnAuU2Vuc29ySW5mb0IFkj' + '8CEA9SCnNlbnNvckluZm8='); + +@$core.Deprecated('Use deviceUpdatePageDescriptor instead') +const DeviceUpdatePage$json = { + '1': 'DeviceUpdatePage', + '2': [ + {'1': 'updateStatus', '3': 1, '4': 1, '5': 13, '8': {}, '10': 'updateStatus'}, + {'1': 'newVersion', '3': 2, '4': 1, '5': 13, '10': 'newVersion'}, + ], +}; + +/// Descriptor for `DeviceUpdatePage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List deviceUpdatePageDescriptor = $convert.base64Decode( + 'ChBEZXZpY2VVcGRhdGVQYWdlEikKDHVwZGF0ZVN0YXR1cxgBIAEoDUIFkj8COAhSDHVwZGF0ZV' + 'N0YXR1cxIeCgpuZXdWZXJzaW9uGAIgASgNUgpuZXdWZXJzaW9u'); + +@$core.Deprecated('Use dateTimePageDescriptor instead') +const DateTimePage$json = { + '1': 'DateTimePage', + '2': [ + {'1': 'utcDateTime', '3': 1, '4': 1, '5': 13, '10': 'utcDateTime'}, + ], +}; + +/// Descriptor for `DateTimePage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List dateTimePageDescriptor = $convert.base64Decode( + 'CgxEYXRlVGltZVBhZ2USIAoLdXRjRGF0ZVRpbWUYASABKA1SC3V0Y0RhdGVUaW1l'); + +@$core.Deprecated('Use bleSecurityPageDescriptor instead') +const BleSecurityPage$json = { + '1': 'BleSecurityPage', + '2': [ + {'1': 'secureConnectionStatus', '3': 1, '4': 1, '5': 14, '6': '.Zp.BleSecureConnectionStatus', '10': 'secureConnectionStatus'}, + {'1': 'secureConnectionWindowStatus', '3': 2, '4': 1, '5': 14, '6': '.Zp.BleSecureConnectionWindowStatus', '10': 'secureConnectionWindowStatus'}, + ], +}; + +/// Descriptor for `BleSecurityPage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List bleSecurityPageDescriptor = $convert.base64Decode( + 'Cg9CbGVTZWN1cml0eVBhZ2USVQoWc2VjdXJlQ29ubmVjdGlvblN0YXR1cxgBIAEoDjIdLlpwLk' + 'JsZVNlY3VyZUNvbm5lY3Rpb25TdGF0dXNSFnNlY3VyZUNvbm5lY3Rpb25TdGF0dXMSZwocc2Vj' + 'dXJlQ29ubmVjdGlvbldpbmRvd1N0YXR1cxgCIAEoDjIjLlpwLkJsZVNlY3VyZUNvbm5lY3Rpb2' + '5XaW5kb3dTdGF0dXNSHHNlY3VyZUNvbm5lY3Rpb25XaW5kb3dTdGF0dXM='); + +@$core.Deprecated('Use devInfoPageDescriptor instead') +const DevInfoPage$json = { + '1': 'DevInfoPage', + '2': [ + {'1': 'protocolVersion', '3': 1, '4': 1, '5': 13, '8': {}, '10': 'protocolVersion'}, + {'1': 'systemFwVersion', '3': 2, '4': 1, '5': 12, '8': {}, '10': 'systemFwVersion'}, + {'1': 'deviceName', '3': 3, '4': 1, '5': 12, '8': {}, '10': 'deviceName'}, + {'1': 'serialNumber', '3': 6, '4': 1, '5': 12, '8': {}, '10': 'serialNumber'}, + {'1': 'systemHwRevision', '3': 7, '4': 1, '5': 12, '8': {}, '10': 'systemHwRevision'}, + {'1': 'deviceCapabilities', '3': 8, '4': 3, '5': 11, '6': '.Zp.DevInfoPage.DeviceCapabilities', '8': {}, '10': 'deviceCapabilities'}, + {'1': 'manufacturerId', '3': 9, '4': 1, '5': 13, '8': {}, '10': 'manufacturerId'}, + {'1': 'productId', '3': 10, '4': 1, '5': 13, '8': {}, '10': 'productId'}, + {'1': 'deviceUid', '3': 11, '4': 1, '5': 12, '8': {}, '10': 'deviceUid'}, + ], + '3': [DevInfoPage_DeviceCapabilities$json], + '9': [ + {'1': 4, '2': 5}, + {'1': 5, '2': 6}, + ], +}; + +@$core.Deprecated('Use devInfoPageDescriptor instead') +const DevInfoPage_DeviceCapabilities$json = { + '1': 'DeviceCapabilities', + '2': [ + {'1': 'deviceType', '3': 1, '4': 2, '5': 13, '8': {}, '10': 'deviceType'}, + {'1': 'capabilities', '3': 2, '4': 2, '5': 13, '10': 'capabilities'}, + ], +}; + +/// Descriptor for `DevInfoPage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List devInfoPageDescriptor = $convert.base64Decode( + 'CgtEZXZJbmZvUGFnZRIvCg9wcm90b2NvbFZlcnNpb24YASABKA1CBZI/AjgQUg9wcm90b2NvbF' + 'ZlcnNpb24SMQoPc3lzdGVtRndWZXJzaW9uGAIgASgMQgeSPwQIBHgBUg9zeXN0ZW1Gd1ZlcnNp' + 'b24SJQoKZGV2aWNlTmFtZRgDIAEoDEIFkj8CCAxSCmRldmljZU5hbWUSKwoMc2VyaWFsTnVtYm' + 'VyGAYgASgMQgeSPwQID3gBUgxzZXJpYWxOdW1iZXISMQoQc3lzdGVtSHdSZXZpc2lvbhgHIAEo' + 'DEIFkj8CCAVSEHN5c3RlbUh3UmV2aXNpb24SWQoSZGV2aWNlQ2FwYWJpbGl0aWVzGAggAygLMi' + 'IuWnAuRGV2SW5mb1BhZ2UuRGV2aWNlQ2FwYWJpbGl0aWVzQgWSPwIQD1ISZGV2aWNlQ2FwYWJp' + 'bGl0aWVzEi0KDm1hbnVmYWN0dXJlcklkGAkgASgNQgWSPwI4EFIObWFudWZhY3R1cmVySWQSIw' + 'oJcHJvZHVjdElkGAogASgNQgWSPwI4EFIJcHJvZHVjdElkEiUKCWRldmljZVVpZBgLIAEoDEIH' + 'kj8ECAx4AVIJZGV2aWNlVWlkGl8KEkRldmljZUNhcGFiaWxpdGllcxIlCgpkZXZpY2VUeXBlGA' + 'EgAigNQgWSPwI4CFIKZGV2aWNlVHlwZRIiCgxjYXBhYmlsaXRpZXMYAiACKA1SDGNhcGFiaWxp' + 'dGllc0oECAQQBUoECAUQBg=='); + +@$core.Deprecated('Use clientServerCfgPageDescriptor instead') +const ClientServerCfgPage$json = { + '1': 'ClientServerCfgPage', + '2': [ + {'1': 'notifications', '3': 1, '4': 1, '5': 13, '10': 'notifications'}, + ], +}; + +/// Descriptor for `ClientServerCfgPage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List clientServerCfgPageDescriptor = $convert.base64Decode( + 'ChNDbGllbnRTZXJ2ZXJDZmdQYWdlEiQKDW5vdGlmaWNhdGlvbnMYASABKA1SDW5vdGlmaWNhdG' + 'lvbnM='); + +@$core.Deprecated('Use trainerSimulationParamDescriptor instead') +const TrainerSimulationParam$json = { + '1': 'TrainerSimulationParam', + '2': [ + {'1': 'configuredResistance', '3': 1, '4': 1, '5': 13, '8': {}, '10': 'configuredResistance'}, + {'1': 'ergPower', '3': 2, '4': 1, '5': 13, '8': {}, '10': 'ergPower'}, + {'1': 'averagingWindow', '3': 3, '4': 1, '5': 13, '8': {}, '10': 'averagingWindow'}, + {'1': 'simulatedWind', '3': 4, '4': 1, '5': 17, '8': {}, '10': 'simulatedWind'}, + {'1': 'simulatedGrade', '3': 5, '4': 1, '5': 17, '8': {}, '10': 'simulatedGrade'}, + {'1': 'simulatedRealGearRatio', '3': 6, '4': 1, '5': 13, '8': {}, '10': 'simulatedRealGearRatio'}, + {'1': 'simulatedVirtualGearRatio', '3': 7, '4': 1, '5': 13, '8': {}, '10': 'simulatedVirtualGearRatio'}, + {'1': 'simulatedCW', '3': 8, '4': 1, '5': 13, '8': {}, '10': 'simulatedCW'}, + {'1': 'simulatedWheelDiameter', '3': 9, '4': 1, '5': 13, '8': {}, '10': 'simulatedWheelDiameter'}, + {'1': 'simulatedBikeMass', '3': 10, '4': 1, '5': 13, '8': {}, '10': 'simulatedBikeMass'}, + {'1': 'simulatedRiderMass', '3': 11, '4': 1, '5': 13, '8': {}, '10': 'simulatedRiderMass'}, + {'1': 'simulatedCRR', '3': 12, '4': 1, '5': 13, '8': {}, '10': 'simulatedCRR'}, + {'1': 'simulatedFrontalArea', '3': 13, '4': 1, '5': 13, '8': {}, '10': 'simulatedFrontalArea'}, + {'1': 'simulatedEBrake', '3': 14, '4': 1, '5': 13, '8': {}, '10': 'simulatedEBrake'}, + ], +}; + +/// Descriptor for `TrainerSimulationParam`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List trainerSimulationParamDescriptor = $convert.base64Decode( + 'ChZUcmFpbmVyU2ltdWxhdGlvblBhcmFtEjkKFGNvbmZpZ3VyZWRSZXNpc3RhbmNlGAEgASgNQg' + 'WSPwI4EFIUY29uZmlndXJlZFJlc2lzdGFuY2USIQoIZXJnUG93ZXIYAiABKA1CBZI/AjgQUghl' + 'cmdQb3dlchIvCg9hdmVyYWdpbmdXaW5kb3cYAyABKA1CBZI/AjgQUg9hdmVyYWdpbmdXaW5kb3' + 'cSKwoNc2ltdWxhdGVkV2luZBgEIAEoEUIFkj8COBBSDXNpbXVsYXRlZFdpbmQSLQoOc2ltdWxh' + 'dGVkR3JhZGUYBSABKBFCBZI/AjgQUg5zaW11bGF0ZWRHcmFkZRI9ChZzaW11bGF0ZWRSZWFsR2' + 'VhclJhdGlvGAYgASgNQgWSPwI4EFIWc2ltdWxhdGVkUmVhbEdlYXJSYXRpbxJDChlzaW11bGF0' + 'ZWRWaXJ0dWFsR2VhclJhdGlvGAcgASgNQgWSPwI4EFIZc2ltdWxhdGVkVmlydHVhbEdlYXJSYX' + 'RpbxInCgtzaW11bGF0ZWRDVxgIIAEoDUIFkj8COBBSC3NpbXVsYXRlZENXEj0KFnNpbXVsYXRl' + 'ZFdoZWVsRGlhbWV0ZXIYCSABKA1CBZI/AjgQUhZzaW11bGF0ZWRXaGVlbERpYW1ldGVyEjMKEX' + 'NpbXVsYXRlZEJpa2VNYXNzGAogASgNQgWSPwI4EFIRc2ltdWxhdGVkQmlrZU1hc3MSNQoSc2lt' + 'dWxhdGVkUmlkZXJNYXNzGAsgASgNQgWSPwI4EFISc2ltdWxhdGVkUmlkZXJNYXNzEikKDHNpbX' + 'VsYXRlZENSUhgMIAEoDUIFkj8COBBSDHNpbXVsYXRlZENSUhI5ChRzaW11bGF0ZWRGcm9udGFs' + 'QXJlYRgNIAEoDUIFkj8COBBSFHNpbXVsYXRlZEZyb250YWxBcmVhEi8KD3NpbXVsYXRlZEVCcm' + 'FrZRgOIAEoDUIFkj8COBBSD3NpbXVsYXRlZEVCcmFrZQ=='); + +@$core.Deprecated('Use trainerOptionsDescriptor instead') +const TrainerOptions$json = { + '1': 'TrainerOptions', + '2': [ + {'1': 'highSpeedDataEnabled', '3': 1, '4': 1, '5': 8, '10': 'highSpeedDataEnabled'}, + {'1': 'ergPowerSmoothingEnabled', '3': 2, '4': 1, '5': 8, '10': 'ergPowerSmoothingEnabled'}, + {'1': 'virtualShiftingMode', '3': 3, '4': 1, '5': 13, '8': {}, '10': 'virtualShiftingMode'}, + ], +}; + +/// Descriptor for `TrainerOptions`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List trainerOptionsDescriptor = $convert.base64Decode( + 'Cg5UcmFpbmVyT3B0aW9ucxIyChRoaWdoU3BlZWREYXRhRW5hYmxlZBgBIAEoCFIUaGlnaFNwZW' + 'VkRGF0YUVuYWJsZWQSOgoYZXJnUG93ZXJTbW9vdGhpbmdFbmFibGVkGAIgASgIUhhlcmdQb3dl' + 'clNtb290aGluZ0VuYWJsZWQSNwoTdmlydHVhbFNoaWZ0aW5nTW9kZRgDIAEoDUIFkj8COAhSE3' + 'ZpcnR1YWxTaGlmdGluZ01vZGU='); + +@$core.Deprecated('Use trainerCfgPageDescriptor instead') +const TrainerCfgPage$json = { + '1': 'TrainerCfgPage', + '2': [ + {'1': 'trainerMode', '3': 1, '4': 1, '5': 14, '6': '.Zp.TrainerMode', '10': 'trainerMode'}, + {'1': 'configuredResistance', '3': 2, '4': 1, '5': 13, '8': {}, '10': 'configuredResistance'}, + {'1': 'ergPower', '3': 3, '4': 1, '5': 13, '8': {}, '10': 'ergPower'}, + {'1': 'averagingWindow', '3': 4, '4': 1, '5': 13, '8': {}, '10': 'averagingWindow'}, + {'1': 'simulatedWind', '3': 5, '4': 1, '5': 17, '8': {}, '10': 'simulatedWind'}, + {'1': 'simulatedGrade', '3': 6, '4': 1, '5': 17, '8': {}, '10': 'simulatedGrade'}, + {'1': 'simulatedRealGearRatio', '3': 7, '4': 1, '5': 13, '8': {}, '10': 'simulatedRealGearRatio'}, + {'1': 'simulatedVirtualGearRatio', '3': 8, '4': 1, '5': 13, '8': {}, '10': 'simulatedVirtualGearRatio'}, + {'1': 'simulatedCW', '3': 9, '4': 1, '5': 13, '8': {}, '10': 'simulatedCW'}, + {'1': 'simulatedWheelDiameter', '3': 10, '4': 1, '5': 13, '8': {}, '10': 'simulatedWheelDiameter'}, + {'1': 'simulatedBikeMass', '3': 11, '4': 1, '5': 13, '8': {}, '10': 'simulatedBikeMass'}, + {'1': 'simulatedRiderMass', '3': 12, '4': 1, '5': 13, '8': {}, '10': 'simulatedRiderMass'}, + {'1': 'simulatedCRR', '3': 13, '4': 1, '5': 13, '8': {}, '10': 'simulatedCRR'}, + {'1': 'simulatedFrontalArea', '3': 14, '4': 1, '5': 13, '8': {}, '10': 'simulatedFrontalArea'}, + {'1': 'simulatedEBrake', '3': 15, '4': 1, '5': 13, '8': {}, '10': 'simulatedEBrake'}, + {'1': 'highSpeedDataEnabled', '3': 16, '4': 1, '5': 8, '10': 'highSpeedDataEnabled'}, + {'1': 'ergPowerSmoothingEnabled', '3': 17, '4': 1, '5': 8, '10': 'ergPowerSmoothingEnabled'}, + {'1': 'virtualShiftingMode', '3': 18, '4': 1, '5': 13, '8': {}, '10': 'virtualShiftingMode'}, + ], +}; + +/// Descriptor for `TrainerCfgPage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List trainerCfgPageDescriptor = $convert.base64Decode( + 'Cg5UcmFpbmVyQ2ZnUGFnZRIxCgt0cmFpbmVyTW9kZRgBIAEoDjIPLlpwLlRyYWluZXJNb2RlUg' + 't0cmFpbmVyTW9kZRI5ChRjb25maWd1cmVkUmVzaXN0YW5jZRgCIAEoDUIFkj8COBBSFGNvbmZp' + 'Z3VyZWRSZXNpc3RhbmNlEiEKCGVyZ1Bvd2VyGAMgASgNQgWSPwI4EFIIZXJnUG93ZXISLwoPYX' + 'ZlcmFnaW5nV2luZG93GAQgASgNQgWSPwI4EFIPYXZlcmFnaW5nV2luZG93EisKDXNpbXVsYXRl' + 'ZFdpbmQYBSABKBFCBZI/AjgQUg1zaW11bGF0ZWRXaW5kEi0KDnNpbXVsYXRlZEdyYWRlGAYgAS' + 'gRQgWSPwI4EFIOc2ltdWxhdGVkR3JhZGUSPQoWc2ltdWxhdGVkUmVhbEdlYXJSYXRpbxgHIAEo' + 'DUIFkj8COBBSFnNpbXVsYXRlZFJlYWxHZWFyUmF0aW8SQwoZc2ltdWxhdGVkVmlydHVhbEdlYX' + 'JSYXRpbxgIIAEoDUIFkj8COBBSGXNpbXVsYXRlZFZpcnR1YWxHZWFyUmF0aW8SJwoLc2ltdWxh' + 'dGVkQ1cYCSABKA1CBZI/AjgQUgtzaW11bGF0ZWRDVxI9ChZzaW11bGF0ZWRXaGVlbERpYW1ldG' + 'VyGAogASgNQgWSPwI4EFIWc2ltdWxhdGVkV2hlZWxEaWFtZXRlchIzChFzaW11bGF0ZWRCaWtl' + 'TWFzcxgLIAEoDUIFkj8COBBSEXNpbXVsYXRlZEJpa2VNYXNzEjUKEnNpbXVsYXRlZFJpZGVyTW' + 'FzcxgMIAEoDUIFkj8COBBSEnNpbXVsYXRlZFJpZGVyTWFzcxIpCgxzaW11bGF0ZWRDUlIYDSAB' + 'KA1CBZI/AjgQUgxzaW11bGF0ZWRDUlISOQoUc2ltdWxhdGVkRnJvbnRhbEFyZWEYDiABKA1CBZ' + 'I/AjgQUhRzaW11bGF0ZWRGcm9udGFsQXJlYRIvCg9zaW11bGF0ZWRFQnJha2UYDyABKA1CBZI/' + 'AjgQUg9zaW11bGF0ZWRFQnJha2USMgoUaGlnaFNwZWVkRGF0YUVuYWJsZWQYECABKAhSFGhpZ2' + 'hTcGVlZERhdGFFbmFibGVkEjoKGGVyZ1Bvd2VyU21vb3RoaW5nRW5hYmxlZBgRIAEoCFIYZXJn' + 'UG93ZXJTbW9vdGhpbmdFbmFibGVkEjcKE3ZpcnR1YWxTaGlmdGluZ01vZGUYEiABKA1CBZI/Aj' + 'gIUhN2aXJ0dWFsU2hpZnRpbmdNb2Rl'); + +@$core.Deprecated('Use trainerGearIndexConfigPageDescriptor instead') +const TrainerGearIndexConfigPage$json = { + '1': 'TrainerGearIndexConfigPage', + '2': [ + {'1': 'frontGearIdx', '3': 1, '4': 1, '5': 13, '8': {}, '10': 'frontGearIdx'}, + {'1': 'frontGearIdxMax', '3': 2, '4': 1, '5': 13, '8': {}, '10': 'frontGearIdxMax'}, + {'1': 'frontGearIdxMin', '3': 3, '4': 1, '5': 13, '8': {}, '10': 'frontGearIdxMin'}, + {'1': 'rearGearIdx', '3': 4, '4': 1, '5': 13, '8': {}, '10': 'rearGearIdx'}, + {'1': 'rearGearIdxMax', '3': 5, '4': 1, '5': 13, '8': {}, '10': 'rearGearIdxMax'}, + {'1': 'rearGearIdxMin', '3': 6, '4': 1, '5': 13, '8': {}, '10': 'rearGearIdxMin'}, + ], +}; + +/// Descriptor for `TrainerGearIndexConfigPage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List trainerGearIndexConfigPageDescriptor = $convert.base64Decode( + 'ChpUcmFpbmVyR2VhckluZGV4Q29uZmlnUGFnZRIpCgxmcm9udEdlYXJJZHgYASABKA1CBZI/Aj' + 'gIUgxmcm9udEdlYXJJZHgSLwoPZnJvbnRHZWFySWR4TWF4GAIgASgNQgWSPwI4CFIPZnJvbnRH' + 'ZWFySWR4TWF4Ei8KD2Zyb250R2VhcklkeE1pbhgDIAEoDUIFkj8COAhSD2Zyb250R2VhcklkeE' + '1pbhInCgtyZWFyR2VhcklkeBgEIAEoDUIFkj8COAhSC3JlYXJHZWFySWR4Ei0KDnJlYXJHZWFy' + 'SWR4TWF4GAUgASgNQgWSPwI4CFIOcmVhckdlYXJJZHhNYXgSLQoOcmVhckdlYXJJZHhNaW4YBi' + 'ABKA1CBZI/AjgIUg5yZWFyR2VhcklkeE1pbg=='); + +@$core.Deprecated('Use deviceTiltConfigPageDescriptor instead') +const DeviceTiltConfigPage$json = { + '1': 'DeviceTiltConfigPage', + '2': [ + {'1': 'tiltEnabled', '3': 1, '4': 1, '5': 8, '10': 'tiltEnabled'}, + {'1': 'tiltGradientMin', '3': 2, '4': 1, '5': 17, '8': {}, '10': 'tiltGradientMin'}, + {'1': 'tiltGradientMax', '3': 3, '4': 1, '5': 17, '8': {}, '10': 'tiltGradientMax'}, + {'1': 'tiltGradient', '3': 4, '4': 1, '5': 17, '8': {}, '10': 'tiltGradient'}, + ], +}; + +/// Descriptor for `DeviceTiltConfigPage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List deviceTiltConfigPageDescriptor = $convert.base64Decode( + 'ChREZXZpY2VUaWx0Q29uZmlnUGFnZRIgCgt0aWx0RW5hYmxlZBgBIAEoCFILdGlsdEVuYWJsZW' + 'QSLwoPdGlsdEdyYWRpZW50TWluGAIgASgRQgWSPwI4EFIPdGlsdEdyYWRpZW50TWluEi8KD3Rp' + 'bHRHcmFkaWVudE1heBgDIAEoEUIFkj8COBBSD3RpbHRHcmFkaWVudE1heBIpCgx0aWx0R3JhZG' + 'llbnQYBCABKBFCBZI/AjgQUgx0aWx0R3JhZGllbnQ='); + +@$core.Deprecated('Use controllerInputConfigPageDescriptor instead') +const ControllerInputConfigPage$json = { + '1': 'ControllerInputConfigPage', + '2': [ + {'1': 'supportedDigitalInputs', '3': 1, '4': 1, '5': 13, '10': 'supportedDigitalInputs'}, + {'1': 'supportedAnalogInputs', '3': 2, '4': 1, '5': 13, '8': {}, '10': 'supportedAnalogInputs'}, + {'1': 'analogInputRange', '3': 3, '4': 3, '5': 11, '6': '.Zp.InputAnalogRange', '8': {}, '10': 'analogInputRange'}, + {'1': 'analogDeadZone', '3': 4, '4': 3, '5': 11, '6': '.Zp.InputAnalogDeadzone', '8': {}, '10': 'analogDeadZone'}, + ], +}; + +/// Descriptor for `ControllerInputConfigPage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List controllerInputConfigPageDescriptor = $convert.base64Decode( + 'ChlDb250cm9sbGVySW5wdXRDb25maWdQYWdlEjYKFnN1cHBvcnRlZERpZ2l0YWxJbnB1dHMYAS' + 'ABKA1SFnN1cHBvcnRlZERpZ2l0YWxJbnB1dHMSOwoVc3VwcG9ydGVkQW5hbG9nSW5wdXRzGAIg' + 'ASgNQgWSPwI4EFIVc3VwcG9ydGVkQW5hbG9nSW5wdXRzEkcKEGFuYWxvZ0lucHV0UmFuZ2UYAy' + 'ADKAsyFC5acC5JbnB1dEFuYWxvZ1JhbmdlQgWSPwIQCFIQYW5hbG9nSW5wdXRSYW5nZRJGCg5h' + 'bmFsb2dEZWFkWm9uZRgEIAMoCzIXLlpwLklucHV0QW5hbG9nRGVhZHpvbmVCBZI/AhAIUg5hbm' + 'Fsb2dEZWFkWm9uZQ=='); + +@$core.Deprecated('Use ipTransportPageDescriptor instead') +const IpTransportPage$json = { + '1': 'IpTransportPage', + '2': [ + {'1': 'ipv4Address', '3': 1, '4': 1, '5': 13, '10': 'ipv4Address'}, + {'1': 'tcpPort', '3': 2, '4': 1, '5': 13, '8': {}, '10': 'tcpPort'}, + ], +}; + +/// Descriptor for `IpTransportPage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List ipTransportPageDescriptor = $convert.base64Decode( + 'Cg9JcFRyYW5zcG9ydFBhZ2USIAoLaXB2NEFkZHJlc3MYASABKA1SC2lwdjRBZGRyZXNzEh8KB3' + 'RjcFBvcnQYAiABKA1CBZI/AjgQUgd0Y3BQb3J0'); + +@$core.Deprecated('Use wifiConfigurationPageDescriptor instead') +const WifiConfigurationPage$json = { + '1': 'WifiConfigurationPage', + '2': [ + {'1': 'wifiEnabled', '3': 1, '4': 1, '5': 8, '10': 'wifiEnabled'}, + {'1': 'wifiStatus', '3': 2, '4': 1, '5': 13, '8': {}, '10': 'wifiStatus'}, + {'1': 'wifiSsid', '3': 3, '4': 1, '5': 12, '8': {}, '10': 'wifiSsid'}, + {'1': 'wifiBand', '3': 4, '4': 1, '5': 13, '8': {}, '10': 'wifiBand'}, + {'1': 'wifiRssi', '3': 5, '4': 1, '5': 17, '8': {}, '10': 'wifiRssi'}, + {'1': 'wifiRegionCode', '3': 6, '4': 1, '5': 12, '8': {}, '10': 'wifiRegionCode'}, + ], +}; + +/// Descriptor for `WifiConfigurationPage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List wifiConfigurationPageDescriptor = $convert.base64Decode( + 'ChVXaWZpQ29uZmlndXJhdGlvblBhZ2USIAoLd2lmaUVuYWJsZWQYASABKAhSC3dpZmlFbmFibG' + 'VkEiUKCndpZmlTdGF0dXMYAiABKA1CBZI/AjgIUgp3aWZpU3RhdHVzEiEKCHdpZmlTc2lkGAMg' + 'ASgMQgWSPwIIIFIId2lmaVNzaWQSIQoId2lmaUJhbmQYBCABKA1CBZI/AjgIUgh3aWZpQmFuZB' + 'IhCgh3aWZpUnNzaRgFIAEoEUIFkj8COAhSCHdpZmlSc3NpEi0KDndpZmlSZWdpb25Db2RlGAYg' + 'ASgMQgWSPwIIA1IOd2lmaVJlZ2lvbkNvZGU='); + +@$core.Deprecated('Use sensorRelayDataPageDescriptor instead') +const SensorRelayDataPage$json = { + '1': 'SensorRelayDataPage', + '2': [ + {'1': 'supportedSensors', '3': 1, '4': 3, '5': 13, '8': {}, '10': 'supportedSensors'}, + {'1': 'pairedSensors', '3': 2, '4': 3, '5': 13, '8': {}, '10': 'pairedSensors'}, + ], +}; + +/// Descriptor for `SensorRelayDataPage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List sensorRelayDataPageDescriptor = $convert.base64Decode( + 'ChNTZW5zb3JSZWxheURhdGFQYWdlEjMKEHN1cHBvcnRlZFNlbnNvcnMYASADKA1CB5I/BBAPOA' + 'hSEHN1cHBvcnRlZFNlbnNvcnMSLQoNcGFpcmVkU2Vuc29ycxgCIAMoDUIHkj8EEA84CFINcGFp' + 'cmVkU2Vuc29ycw=='); + +@$core.Deprecated('Use getDescriptor instead') +const Get$json = { + '1': 'Get', + '2': [ + {'1': 'dataObjectId', '3': 1, '4': 2, '5': 13, '8': {}, '10': 'dataObjectId'}, + ], +}; + +/// Descriptor for `Get`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List getDescriptor = $convert.base64Decode( + 'CgNHZXQSKQoMZGF0YU9iamVjdElkGAEgAigNQgWSPwI4EFIMZGF0YU9iamVjdElk'); + +@$core.Deprecated('Use devInfoDescriptor instead') +const DevInfo$json = { + '1': 'DevInfo', + '2': [ + {'1': 'devInfo', '3': 1, '4': 2, '5': 11, '6': '.Zp.DevInfoPage', '10': 'devInfo'}, + ], +}; + +/// Descriptor for `DevInfo`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List devInfoDescriptor = $convert.base64Decode( + 'CgdEZXZJbmZvEikKB2RldkluZm8YASACKAsyDy5acC5EZXZJbmZvUGFnZVIHZGV2SW5mbw=='); + +@$core.Deprecated('Use bleSecurityRequestDescriptor instead') +const BleSecurityRequest$json = { + '1': 'BleSecurityRequest', + '2': [ + {'1': 'startSecureConnection', '3': 1, '4': 1, '5': 8, '9': 0, '10': 'startSecureConnection'}, + ], + '8': [ + {'1': 'request'}, + ], +}; + +/// Descriptor for `BleSecurityRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List bleSecurityRequestDescriptor = $convert.base64Decode( + 'ChJCbGVTZWN1cml0eVJlcXVlc3QSNgoVc3RhcnRTZWN1cmVDb25uZWN0aW9uGAEgASgISABSFX' + 'N0YXJ0U2VjdXJlQ29ubmVjdGlvbkIJCgdyZXF1ZXN0'); + +@$core.Deprecated('Use trainerNotificationDescriptor instead') +const TrainerNotification$json = { + '1': 'TrainerNotification', + '2': [ + {'1': 'power', '3': 1, '4': 2, '5': 13, '8': {}, '10': 'power'}, + {'1': 'cadence', '3': 2, '4': 2, '5': 13, '8': {}, '10': 'cadence'}, + {'1': 'bikeSpeed', '3': 3, '4': 2, '5': 13, '8': {}, '10': 'bikeSpeed'}, + {'1': 'averagedPower', '3': 4, '4': 2, '5': 13, '8': {}, '10': 'averagedPower'}, + {'1': 'wheelSpeed', '3': 5, '4': 2, '5': 13, '8': {}, '10': 'wheelSpeed'}, + {'1': 'calculatedRealGearRatio', '3': 6, '4': 1, '5': 13, '8': {}, '10': 'calculatedRealGearRatio'}, + ], +}; + +/// Descriptor for `TrainerNotification`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List trainerNotificationDescriptor = $convert.base64Decode( + 'ChNUcmFpbmVyTm90aWZpY2F0aW9uEhsKBXBvd2VyGAEgAigNQgWSPwI4EFIFcG93ZXISHwoHY2' + 'FkZW5jZRgCIAIoDUIFkj8COBBSB2NhZGVuY2USIwoJYmlrZVNwZWVkGAMgAigNQgWSPwI4EFIJ' + 'YmlrZVNwZWVkEisKDWF2ZXJhZ2VkUG93ZXIYBCACKA1CBZI/AjgQUg1hdmVyYWdlZFBvd2VyEi' + 'UKCndoZWVsU3BlZWQYBSACKA1CBZI/AjgQUgp3aGVlbFNwZWVkEj8KF2NhbGN1bGF0ZWRSZWFs' + 'R2VhclJhdGlvGAYgASgNQgWSPwI4EFIXY2FsY3VsYXRlZFJlYWxHZWFyUmF0aW8='); + +@$core.Deprecated('Use trainerConfigSetDescriptor instead') +const TrainerConfigSet$json = { + '1': 'TrainerConfigSet', + '2': [ + {'1': 'configuredResistance', '3': 2, '4': 1, '5': 13, '8': {}, '9': 0, '10': 'configuredResistance'}, + {'1': 'ergPower', '3': 3, '4': 1, '5': 13, '8': {}, '9': 0, '10': 'ergPower'}, + {'1': 'envSim', '3': 4, '4': 1, '5': 11, '6': '.Zp.TrainerEnvSim', '9': 0, '10': 'envSim'}, + {'1': 'bikeSim', '3': 5, '4': 1, '5': 11, '6': '.Zp.TrainerBikeSim', '9': 0, '10': 'bikeSim'}, + {'1': 'averagingWindow', '3': 6, '4': 1, '5': 13, '8': {}, '9': 0, '10': 'averagingWindow'}, + {'1': 'trainerOptions', '3': 7, '4': 1, '5': 11, '6': '.Zp.TrainerOptions', '9': 0, '10': 'trainerOptions'}, + ], + '8': [ + {'1': 'config'}, + ], + '9': [ + {'1': 1, '2': 2}, + ], +}; + +/// Descriptor for `TrainerConfigSet`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List trainerConfigSetDescriptor = $convert.base64Decode( + 'ChBUcmFpbmVyQ29uZmlnU2V0EjsKFGNvbmZpZ3VyZWRSZXNpc3RhbmNlGAIgASgNQgWSPwI4EE' + 'gAUhRjb25maWd1cmVkUmVzaXN0YW5jZRIjCghlcmdQb3dlchgDIAEoDUIFkj8COBBIAFIIZXJn' + 'UG93ZXISKwoGZW52U2ltGAQgASgLMhEuWnAuVHJhaW5lckVudlNpbUgAUgZlbnZTaW0SLgoHYm' + 'lrZVNpbRgFIAEoCzISLlpwLlRyYWluZXJCaWtlU2ltSABSB2Jpa2VTaW0SMQoPYXZlcmFnaW5n' + 'V2luZG93GAYgASgNQgWSPwI4EEgAUg9hdmVyYWdpbmdXaW5kb3cSPAoOdHJhaW5lck9wdGlvbn' + 'MYByABKAsyEi5acC5UcmFpbmVyT3B0aW9uc0gAUg50cmFpbmVyT3B0aW9uc0IICgZjb25maWdK' + 'BAgBEAI='); + +@$core.Deprecated('Use trainerConfigStatusDescriptor instead') +const TrainerConfigStatus$json = { + '1': 'TrainerConfigStatus', + '2': [ + {'1': 'trainerCfg', '3': 1, '4': 2, '5': 11, '6': '.Zp.TrainerCfgPage', '10': 'trainerCfg'}, + ], +}; + +/// Descriptor for `TrainerConfigStatus`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List trainerConfigStatusDescriptor = $convert.base64Decode( + 'ChNUcmFpbmVyQ29uZmlnU3RhdHVzEjIKCnRyYWluZXJDZmcYASACKAsyEi5acC5UcmFpbmVyQ2' + 'ZnUGFnZVIKdHJhaW5lckNmZw=='); + +@$core.Deprecated('Use devInfoSetDescriptor instead') +const DevInfoSet$json = { + '1': 'DevInfoSet', + '2': [ + {'1': 'deviceName', '3': 1, '4': 1, '5': 12, '8': {}, '9': 0, '10': 'deviceName'}, + ], + '8': [ + {'1': 'devInfo'}, + ], +}; + +/// Descriptor for `DevInfoSet`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List devInfoSetDescriptor = $convert.base64Decode( + 'CgpEZXZJbmZvU2V0EicKCmRldmljZU5hbWUYASABKAxCBZI/AggMSABSCmRldmljZU5hbWVCCQ' + 'oHZGV2SW5mbw=='); + +@$core.Deprecated('Use resetDescriptor instead') +const Reset$json = { + '1': 'Reset', + '2': [ + {'1': 'resetProcedure', '3': 1, '4': 1, '5': 13, '8': {}, '10': 'resetProcedure'}, + ], +}; + +/// Descriptor for `Reset`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List resetDescriptor = $convert.base64Decode( + 'CgVSZXNldBItCg5yZXNldFByb2NlZHVyZRgBIAEoDUIFkj8COAhSDnJlc2V0UHJvY2VkdXJl'); + +@$core.Deprecated('Use batteryNotificationDescriptor instead') +const BatteryNotification$json = { + '1': 'BatteryNotification', + '2': [ + {'1': 'newChgState', '3': 1, '4': 1, '5': 14, '6': '.Zp.ChargingState', '9': 0, '10': 'newChgState'}, + {'1': 'newPercLevel', '3': 2, '4': 1, '5': 13, '8': {}, '9': 0, '10': 'newPercLevel'}, + {'1': 'source', '3': 3, '4': 1, '5': 13, '8': {}, '10': 'source'}, + ], + '8': [ + {'1': 'alert'}, + ], +}; + +/// Descriptor for `BatteryNotification`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List batteryNotificationDescriptor = $convert.base64Decode( + 'ChNCYXR0ZXJ5Tm90aWZpY2F0aW9uEjUKC25ld0NoZ1N0YXRlGAEgASgOMhEuWnAuQ2hhcmdpbm' + 'dTdGF0ZUgAUgtuZXdDaGdTdGF0ZRIrCgxuZXdQZXJjTGV2ZWwYAiABKA1CBZI/AjgISABSDG5l' + 'd1BlcmNMZXZlbBIdCgZzb3VyY2UYAyABKA1CBZI/AjgIUgZzb3VyY2VCBwoFYWxlcnQ='); + +@$core.Deprecated('Use batteryStatusDescriptor instead') +const BatteryStatus$json = { + '1': 'BatteryStatus', + '2': [ + {'1': 'chgState', '3': 1, '4': 2, '5': 14, '6': '.Zp.ChargingState', '10': 'chgState'}, + {'1': 'percLevel', '3': 2, '4': 2, '5': 13, '8': {}, '10': 'percLevel'}, + {'1': 'timeToEmpty', '3': 3, '4': 2, '5': 13, '8': {}, '10': 'timeToEmpty'}, + {'1': 'timeToFull', '3': 4, '4': 2, '5': 13, '8': {}, '10': 'timeToFull'}, + ], +}; + +/// Descriptor for `BatteryStatus`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List batteryStatusDescriptor = $convert.base64Decode( + 'Cg1CYXR0ZXJ5U3RhdHVzEi0KCGNoZ1N0YXRlGAEgAigOMhEuWnAuQ2hhcmdpbmdTdGF0ZVIIY2' + 'hnU3RhdGUSIwoJcGVyY0xldmVsGAIgAigNQgWSPwI4CFIJcGVyY0xldmVsEicKC3RpbWVUb0Vt' + 'cHR5GAMgAigNQgWSPwI4EFILdGltZVRvRW1wdHkSJQoKdGltZVRvRnVsbBgEIAIoDUIFkj8COB' + 'BSCnRpbWVUb0Z1bGw='); + +@$core.Deprecated('Use controllerNotificationDescriptor instead') +const ControllerNotification$json = { + '1': 'ControllerNotification', + '2': [ + {'1': 'buttonEvent', '3': 1, '4': 1, '5': 13, '10': 'buttonEvent'}, + {'1': 'analogEventList', '3': 3, '4': 3, '5': 11, '6': '.Zp.ControllerAnalogEvent', '8': {}, '10': 'analogEventList'}, + ], + '9': [ + {'1': 2, '2': 3}, + ], +}; + +/// Descriptor for `ControllerNotification`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List controllerNotificationDescriptor = $convert.base64Decode( + 'ChZDb250cm9sbGVyTm90aWZpY2F0aW9uEiAKC2J1dHRvbkV2ZW50GAEgASgNUgtidXR0b25Fdm' + 'VudBJKCg9hbmFsb2dFdmVudExpc3QYAyADKAsyGS5acC5Db250cm9sbGVyQW5hbG9nRXZlbnRC' + 'BZI/AhAIUg9hbmFsb2dFdmVudExpc3RKBAgCEAM='); + +@$core.Deprecated('Use logDataNotificationDescriptor instead') +const LogDataNotification$json = { + '1': 'LogDataNotification', + '2': [ + {'1': 'logLevel', '3': 1, '4': 2, '5': 14, '6': '.Zp.LogLevel', '10': 'logLevel'}, + {'1': 'dataLogObject', '3': 2, '4': 2, '5': 11, '6': '.Zp.LogDataNotification.DataLogObject', '10': 'dataLogObject'}, + ], + '3': [LogDataNotification_ConnectionParameters$json, LogDataNotification_EnergySurveySummary$json, LogDataNotification_DataLogObject$json], +}; + +@$core.Deprecated('Use logDataNotificationDescriptor instead') +const LogDataNotification_ConnectionParameters$json = { + '1': 'ConnectionParameters', + '2': [ + {'1': 'interval', '3': 1, '4': 2, '5': 13, '8': {}, '10': 'interval'}, + {'1': 'latency', '3': 2, '4': 2, '5': 13, '8': {}, '10': 'latency'}, + {'1': 'supervisorTimeout', '3': 3, '4': 2, '5': 13, '8': {}, '10': 'supervisorTimeout'}, + {'1': 'bleChannelMap', '3': 4, '4': 2, '5': 12, '8': {}, '10': 'bleChannelMap'}, + {'1': 'mtuSize', '3': 5, '4': 1, '5': 13, '8': {}, '10': 'mtuSize'}, + ], +}; + +@$core.Deprecated('Use logDataNotificationDescriptor instead') +const LogDataNotification_EnergySurveySummary$json = { + '1': 'EnergySurveySummary', + '2': [ + {'1': 'samples', '3': 1, '4': 2, '5': 13, '8': {}, '10': 'samples'}, + {'1': 'averageEnergy', '3': 2, '4': 2, '5': 12, '8': {}, '10': 'averageEnergy'}, + ], +}; + +@$core.Deprecated('Use logDataNotificationDescriptor instead') +const LogDataNotification_DataLogObject$json = { + '1': 'DataLogObject', + '2': [ + {'1': 'logData', '3': 1, '4': 1, '5': 12, '8': {}, '9': 0, '10': 'logData'}, + {'1': 'connectionParam', '3': 2, '4': 1, '5': 11, '6': '.Zp.LogDataNotification.ConnectionParameters', '9': 0, '10': 'connectionParam'}, + {'1': 'energySurvey', '3': 3, '4': 1, '5': 11, '6': '.Zp.LogDataNotification.EnergySurveySummary', '9': 0, '10': 'energySurvey'}, + {'1': 'logString', '3': 4, '4': 1, '5': 12, '8': {}, '9': 0, '10': 'logString'}, + ], + '8': [ + {'1': 'event'}, + ], +}; + +/// Descriptor for `LogDataNotification`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List logDataNotificationDescriptor = $convert.base64Decode( + 'ChNMb2dEYXRhTm90aWZpY2F0aW9uEigKCGxvZ0xldmVsGAEgAigOMgwuWnAuTG9nTGV2ZWxSCG' + 'xvZ0xldmVsEksKDWRhdGFMb2dPYmplY3QYAiACKAsyJS5acC5Mb2dEYXRhTm90aWZpY2F0aW9u' + 'LkRhdGFMb2dPYmplY3RSDWRhdGFMb2dPYmplY3Qa3wEKFENvbm5lY3Rpb25QYXJhbWV0ZXJzEi' + 'EKCGludGVydmFsGAEgAigNQgWSPwI4EFIIaW50ZXJ2YWwSHwoHbGF0ZW5jeRgCIAIoDUIFkj8C' + 'OBBSB2xhdGVuY3kSMwoRc3VwZXJ2aXNvclRpbWVvdXQYAyACKA1CBZI/AjgQUhFzdXBlcnZpc2' + '9yVGltZW91dBItCg1ibGVDaGFubmVsTWFwGAQgAigMQgeSPwQIBXgBUg1ibGVDaGFubmVsTWFw' + 'Eh8KB210dVNpemUYBSABKA1CBZI/AjgQUgdtdHVTaXplGmUKE0VuZXJneVN1cnZleVN1bW1hcn' + 'kSHwoHc2FtcGxlcxgBIAIoDUIFkj8COBBSB3NhbXBsZXMSLQoNYXZlcmFnZUVuZXJneRgCIAIo' + 'DEIHkj8ECCh4AVINYXZlcmFnZUVuZXJneRqRAgoNRGF0YUxvZ09iamVjdBIiCgdsb2dEYXRhGA' + 'EgASgMQgaSPwMIgAFIAFIHbG9nRGF0YRJYCg9jb25uZWN0aW9uUGFyYW0YAiABKAsyLC5acC5M' + 'b2dEYXRhTm90aWZpY2F0aW9uLkNvbm5lY3Rpb25QYXJhbWV0ZXJzSABSD2Nvbm5lY3Rpb25QYX' + 'JhbRJRCgxlbmVyZ3lTdXJ2ZXkYAyABKAsyKy5acC5Mb2dEYXRhTm90aWZpY2F0aW9uLkVuZXJn' + 'eVN1cnZleVN1bW1hcnlIAFIMZW5lcmd5U3VydmV5EiYKCWxvZ1N0cmluZxgEIAEoDEIGkj8DCO' + '0BSABSCWxvZ1N0cmluZ0IHCgVldmVudA=='); + +@$core.Deprecated('Use spindownRequestDescriptor instead') +const SpindownRequest$json = { + '1': 'SpindownRequest', + '2': [ + {'1': 'startSpindown', '3': 1, '4': 1, '5': 11, '6': '.Zp.SpindownRequest.StartSpindown', '9': 0, '10': 'startSpindown'}, + {'1': 'ignoreSpindown', '3': 2, '4': 1, '5': 11, '6': '.Zp.SpindownRequest.IgnoreSpindown', '9': 0, '10': 'ignoreSpindown'}, + ], + '3': [SpindownRequest_StartSpindown$json, SpindownRequest_IgnoreSpindown$json], + '8': [ + {'1': 'request'}, + ], +}; + +@$core.Deprecated('Use spindownRequestDescriptor instead') +const SpindownRequest_StartSpindown$json = { + '1': 'StartSpindown', + '2': [ + {'1': 'enable', '3': 1, '4': 2, '5': 8, '10': 'enable'}, + ], +}; + +@$core.Deprecated('Use spindownRequestDescriptor instead') +const SpindownRequest_IgnoreSpindown$json = { + '1': 'IgnoreSpindown', + '2': [ + {'1': 'enable', '3': 1, '4': 2, '5': 8, '10': 'enable'}, + ], +}; + +/// Descriptor for `SpindownRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List spindownRequestDescriptor = $convert.base64Decode( + 'Cg9TcGluZG93blJlcXVlc3QSSQoNc3RhcnRTcGluZG93bhgBIAEoCzIhLlpwLlNwaW5kb3duUm' + 'VxdWVzdC5TdGFydFNwaW5kb3duSABSDXN0YXJ0U3BpbmRvd24STAoOaWdub3JlU3BpbmRvd24Y' + 'AiABKAsyIi5acC5TcGluZG93blJlcXVlc3QuSWdub3JlU3BpbmRvd25IAFIOaWdub3JlU3Bpbm' + 'Rvd24aJwoNU3RhcnRTcGluZG93bhIWCgZlbmFibGUYASACKAhSBmVuYWJsZRooCg5JZ25vcmVT' + 'cGluZG93bhIWCgZlbmFibGUYASACKAhSBmVuYWJsZUIJCgdyZXF1ZXN0'); + +@$core.Deprecated('Use spindownNotificationDescriptor instead') +const SpindownNotification$json = { + '1': 'SpindownNotification', + '2': [ + {'1': 'manualSpindownStatus', '3': 1, '4': 1, '5': 11, '6': '.Zp.SpindownNotification.ManualSpindownStatus', '9': 0, '10': 'manualSpindownStatus'}, + {'1': 'autoSpindownStatus', '3': 2, '4': 1, '5': 11, '6': '.Zp.SpindownNotification.AutoSpindownStatus', '9': 0, '10': 'autoSpindownStatus'}, + ], + '3': [SpindownNotification_ManualSpindownStatus$json, SpindownNotification_AutoSpindownStatus$json], + '8': [ + {'1': 'event'}, + ], +}; + +@$core.Deprecated('Use spindownNotificationDescriptor instead') +const SpindownNotification_ManualSpindownStatus$json = { + '1': 'ManualSpindownStatus', + '2': [ + {'1': 'spindownStatus', '3': 1, '4': 2, '5': 14, '6': '.Zp.SpindownStatus', '10': 'spindownStatus'}, + {'1': 'targetSpeedLow', '3': 2, '4': 1, '5': 13, '8': {}, '10': 'targetSpeedLow'}, + {'1': 'targetSpeedHigh', '3': 3, '4': 1, '5': 13, '8': {}, '10': 'targetSpeedHigh'}, + ], +}; + +@$core.Deprecated('Use spindownNotificationDescriptor instead') +const SpindownNotification_AutoSpindownStatus$json = { + '1': 'AutoSpindownStatus', + '2': [ + {'1': 'completed', '3': 1, '4': 2, '5': 8, '10': 'completed'}, + ], +}; + +/// Descriptor for `SpindownNotification`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List spindownNotificationDescriptor = $convert.base64Decode( + 'ChRTcGluZG93bk5vdGlmaWNhdGlvbhJjChRtYW51YWxTcGluZG93blN0YXR1cxgBIAEoCzItLl' + 'pwLlNwaW5kb3duTm90aWZpY2F0aW9uLk1hbnVhbFNwaW5kb3duU3RhdHVzSABSFG1hbnVhbFNw' + 'aW5kb3duU3RhdHVzEl0KEmF1dG9TcGluZG93blN0YXR1cxgCIAEoCzIrLlpwLlNwaW5kb3duTm' + '90aWZpY2F0aW9uLkF1dG9TcGluZG93blN0YXR1c0gAUhJhdXRvU3BpbmRvd25TdGF0dXMasgEK' + 'FE1hbnVhbFNwaW5kb3duU3RhdHVzEjoKDnNwaW5kb3duU3RhdHVzGAEgAigOMhIuWnAuU3Bpbm' + 'Rvd25TdGF0dXNSDnNwaW5kb3duU3RhdHVzEi0KDnRhcmdldFNwZWVkTG93GAIgASgNQgWSPwI4' + 'EFIOdGFyZ2V0U3BlZWRMb3cSLwoPdGFyZ2V0U3BlZWRIaWdoGAMgASgNQgWSPwI4EFIPdGFyZ2' + 'V0U3BlZWRIaWdoGjIKEkF1dG9TcGluZG93blN0YXR1cxIcCgljb21wbGV0ZWQYASACKAhSCWNv' + 'bXBsZXRlZEIHCgVldmVudA=='); + +@$core.Deprecated('Use getResponseDescriptor instead') +const GetResponse$json = { + '1': 'GetResponse', + '2': [ + {'1': 'dataObjectId', '3': 1, '4': 2, '5': 13, '8': {}, '10': 'dataObjectId'}, + {'1': 'dataObjectData', '3': 2, '4': 1, '5': 12, '8': {}, '10': 'dataObjectData'}, + ], +}; + +/// Descriptor for `GetResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List getResponseDescriptor = $convert.base64Decode( + 'CgtHZXRSZXNwb25zZRIpCgxkYXRhT2JqZWN0SWQYASACKA1CBZI/AjgQUgxkYXRhT2JqZWN0SW' + 'QSLgoOZGF0YU9iamVjdERhdGEYAiABKAxCBpI/AwjtAVIOZGF0YU9iamVjdERhdGE='); + +@$core.Deprecated('Use statusResponseDescriptor instead') +const StatusResponse$json = { + '1': 'StatusResponse', + '2': [ + {'1': 'command', '3': 1, '4': 2, '5': 13, '8': {}, '10': 'command'}, + {'1': 'status', '3': 2, '4': 2, '5': 13, '8': {}, '10': 'status'}, + ], +}; + +/// Descriptor for `StatusResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List statusResponseDescriptor = $convert.base64Decode( + 'Cg5TdGF0dXNSZXNwb25zZRIfCgdjb21tYW5kGAEgAigNQgWSPwI4CFIHY29tbWFuZBIdCgZzdG' + 'F0dXMYAiACKA1CBZI/AjgIUgZzdGF0dXM='); + +@$core.Deprecated('Use setDescriptor instead') +const Set$json = { + '1': 'Set', + '2': [ + {'1': 'options', '3': 1, '4': 1, '5': 13, '8': {}, '10': 'options'}, + {'1': 'msgId', '3': 2, '4': 1, '5': 13, '8': {}, '10': 'msgId'}, + {'1': 'devInfo', '3': 3, '4': 1, '5': 11, '6': '.Zp.DevInfoSet', '9': 0, '10': 'devInfo'}, + {'1': 'trainerSimParam', '3': 4, '4': 1, '5': 11, '6': '.Zp.TrainerSimulationParam', '9': 0, '10': 'trainerSimParam'}, + {'1': 'trainerOptions', '3': 5, '4': 1, '5': 11, '6': '.Zp.TrainerOptions', '9': 0, '10': 'trainerOptions'}, + {'1': 'deviceTiltConfig', '3': 6, '4': 1, '5': 11, '6': '.Zp.DeviceTiltConfigPage', '9': 0, '10': 'deviceTiltConfig'}, + {'1': 'controllerInputConfig', '3': 7, '4': 1, '5': 11, '6': '.Zp.ControllerInputConfigPage', '9': 0, '10': 'controllerInputConfig'}, + {'1': 'trainerGearIndexConfig', '3': 8, '4': 1, '5': 11, '6': '.Zp.TrainerGearIndexConfigPage', '9': 0, '10': 'trainerGearIndexConfig'}, + {'1': 'wifiConfig', '3': 10, '4': 1, '5': 11, '6': '.Zp.WifiConfigurationPage', '9': 0, '10': 'wifiConfig'}, + {'1': 'dateTimeConfig', '3': 11, '4': 1, '5': 11, '6': '.Zp.DateTimePage', '9': 0, '10': 'dateTimeConfig'}, + ], + '8': [ + {'1': 'set'}, + ], + '9': [ + {'1': 9, '2': 10}, + ], +}; + +/// Descriptor for `Set`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List setDescriptor = $convert.base64Decode( + 'CgNTZXQSHwoHb3B0aW9ucxgBIAEoDUIFkj8COAhSB29wdGlvbnMSGwoFbXNnSWQYAiABKA1CBZ' + 'I/AjgIUgVtc2dJZBIqCgdkZXZJbmZvGAMgASgLMg4uWnAuRGV2SW5mb1NldEgAUgdkZXZJbmZv' + 'EkYKD3RyYWluZXJTaW1QYXJhbRgEIAEoCzIaLlpwLlRyYWluZXJTaW11bGF0aW9uUGFyYW1IAF' + 'IPdHJhaW5lclNpbVBhcmFtEjwKDnRyYWluZXJPcHRpb25zGAUgASgLMhIuWnAuVHJhaW5lck9w' + 'dGlvbnNIAFIOdHJhaW5lck9wdGlvbnMSRgoQZGV2aWNlVGlsdENvbmZpZxgGIAEoCzIYLlpwLk' + 'RldmljZVRpbHRDb25maWdQYWdlSABSEGRldmljZVRpbHRDb25maWcSVQoVY29udHJvbGxlcklu' + 'cHV0Q29uZmlnGAcgASgLMh0uWnAuQ29udHJvbGxlcklucHV0Q29uZmlnUGFnZUgAUhVjb250cm' + '9sbGVySW5wdXRDb25maWcSWAoWdHJhaW5lckdlYXJJbmRleENvbmZpZxgIIAEoCzIeLlpwLlRy' + 'YWluZXJHZWFySW5kZXhDb25maWdQYWdlSABSFnRyYWluZXJHZWFySW5kZXhDb25maWcSOwoKd2' + 'lmaUNvbmZpZxgKIAEoCzIZLlpwLldpZmlDb25maWd1cmF0aW9uUGFnZUgAUgp3aWZpQ29uZmln' + 'EjoKDmRhdGVUaW1lQ29uZmlnGAsgASgLMhAuWnAuRGF0ZVRpbWVQYWdlSABSDmRhdGVUaW1lQ2' + '9uZmlnQgUKA3NldEoECAkQCg=='); + +@$core.Deprecated('Use setResponseDescriptor instead') +const SetResponse$json = { + '1': 'SetResponse', + '2': [ + {'1': 'status', '3': 1, '4': 1, '5': 13, '8': {}, '10': 'status'}, + {'1': 'msgId', '3': 2, '4': 1, '5': 13, '8': {}, '10': 'msgId'}, + {'1': 'response', '3': 3, '4': 1, '5': 11, '6': '.Zp.SetResponse.Response', '10': 'response'}, + ], + '3': [SetResponse_Response$json], + '9': [ + {'1': 7, '2': 8}, + ], +}; + +@$core.Deprecated('Use setResponseDescriptor instead') +const SetResponse_Response$json = { + '1': 'Response', + '2': [ + {'1': 'devInfo', '3': 1, '4': 1, '5': 11, '6': '.Zp.DevInfoPage', '9': 0, '10': 'devInfo'}, + {'1': 'trainerSimParam', '3': 2, '4': 1, '5': 11, '6': '.Zp.TrainerSimulationParam', '9': 0, '10': 'trainerSimParam'}, + {'1': 'trainerOptions', '3': 3, '4': 1, '5': 11, '6': '.Zp.TrainerOptions', '9': 0, '10': 'trainerOptions'}, + {'1': 'deviceTiltConfig', '3': 4, '4': 1, '5': 11, '6': '.Zp.DeviceTiltConfigPage', '9': 0, '10': 'deviceTiltConfig'}, + {'1': 'controllerInputConfig', '3': 5, '4': 1, '5': 11, '6': '.Zp.ControllerInputConfigPage', '9': 0, '10': 'controllerInputConfig'}, + {'1': 'trainerGearIndexConfig', '3': 6, '4': 1, '5': 11, '6': '.Zp.TrainerGearIndexConfigPage', '9': 0, '10': 'trainerGearIndexConfig'}, + {'1': 'wifiConfig', '3': 8, '4': 1, '5': 11, '6': '.Zp.WifiConfigurationPage', '9': 0, '10': 'wifiConfig'}, + {'1': 'dateTimeConfig', '3': 9, '4': 1, '5': 11, '6': '.Zp.DateTimePage', '9': 0, '10': 'dateTimeConfig'}, + ], + '8': [ + {'1': 'page'}, + ], +}; + +/// Descriptor for `SetResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List setResponseDescriptor = $convert.base64Decode( + 'CgtTZXRSZXNwb25zZRIdCgZzdGF0dXMYASABKA1CBZI/AjgIUgZzdGF0dXMSGwoFbXNnSWQYAi' + 'ABKA1CBZI/AjgIUgVtc2dJZBI0CghyZXNwb25zZRgDIAEoCzIYLlpwLlNldFJlc3BvbnNlLlJl' + 'c3BvbnNlUghyZXNwb25zZRq3BAoIUmVzcG9uc2USKwoHZGV2SW5mbxgBIAEoCzIPLlpwLkRldk' + 'luZm9QYWdlSABSB2RldkluZm8SRgoPdHJhaW5lclNpbVBhcmFtGAIgASgLMhouWnAuVHJhaW5l' + 'clNpbXVsYXRpb25QYXJhbUgAUg90cmFpbmVyU2ltUGFyYW0SPAoOdHJhaW5lck9wdGlvbnMYAy' + 'ABKAsyEi5acC5UcmFpbmVyT3B0aW9uc0gAUg50cmFpbmVyT3B0aW9ucxJGChBkZXZpY2VUaWx0' + 'Q29uZmlnGAQgASgLMhguWnAuRGV2aWNlVGlsdENvbmZpZ1BhZ2VIAFIQZGV2aWNlVGlsdENvbm' + 'ZpZxJVChVjb250cm9sbGVySW5wdXRDb25maWcYBSABKAsyHS5acC5Db250cm9sbGVySW5wdXRD' + 'b25maWdQYWdlSABSFWNvbnRyb2xsZXJJbnB1dENvbmZpZxJYChZ0cmFpbmVyR2VhckluZGV4Q2' + '9uZmlnGAYgASgLMh4uWnAuVHJhaW5lckdlYXJJbmRleENvbmZpZ1BhZ2VIAFIWdHJhaW5lckdl' + 'YXJJbmRleENvbmZpZxI7Cgp3aWZpQ29uZmlnGAggASgLMhkuWnAuV2lmaUNvbmZpZ3VyYXRpb2' + '5QYWdlSABSCndpZmlDb25maWcSOgoOZGF0ZVRpbWVDb25maWcYCSABKAsyEC5acC5EYXRlVGlt' + 'ZVBhZ2VIAFIOZGF0ZVRpbWVDb25maWdCBgoEcGFnZUoECAcQCA=='); + +@$core.Deprecated('Use logLevelSetDescriptor instead') +const LogLevelSet$json = { + '1': 'LogLevelSet', + '2': [ + {'1': 'logLevel', '3': 1, '4': 1, '5': 14, '6': '.Zp.LogLevel', '10': 'logLevel'}, + ], +}; + +/// Descriptor for `LogLevelSet`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List logLevelSetDescriptor = $convert.base64Decode( + 'CgtMb2dMZXZlbFNldBIoCghsb2dMZXZlbBgBIAEoDjIMLlpwLkxvZ0xldmVsUghsb2dMZXZlbA' + '=='); + +@$core.Deprecated('Use dataChangeNotificationDescriptor instead') +const DataChangeNotification$json = { + '1': 'DataChangeNotification', + '2': [ + {'1': 'devInfo', '3': 1, '4': 1, '5': 11, '6': '.Zp.DevInfoPage', '9': 0, '10': 'devInfo'}, + {'1': 'trainerSimParam', '3': 2, '4': 1, '5': 11, '6': '.Zp.TrainerSimulationParam', '9': 0, '10': 'trainerSimParam'}, + {'1': 'trainerOptions', '3': 3, '4': 1, '5': 11, '6': '.Zp.TrainerOptions', '9': 0, '10': 'trainerOptions'}, + {'1': 'deviceTiltConfig', '3': 4, '4': 1, '5': 11, '6': '.Zp.DeviceTiltConfigPage', '9': 0, '10': 'deviceTiltConfig'}, + {'1': 'controllerInputConfig', '3': 5, '4': 1, '5': 11, '6': '.Zp.ControllerInputConfigPage', '9': 0, '10': 'controllerInputConfig'}, + {'1': 'trainerGearIndexConfig', '3': 6, '4': 1, '5': 11, '6': '.Zp.TrainerGearIndexConfigPage', '9': 0, '10': 'trainerGearIndexConfig'}, + {'1': 'wifiConfig', '3': 8, '4': 1, '5': 11, '6': '.Zp.WifiConfigurationPage', '9': 0, '10': 'wifiConfig'}, + {'1': 'deviceUpdatePage', '3': 9, '4': 1, '5': 11, '6': '.Zp.DeviceUpdatePage', '9': 0, '10': 'deviceUpdatePage'}, + {'1': 'dateTimePage', '3': 10, '4': 1, '5': 11, '6': '.Zp.DateTimePage', '9': 0, '10': 'dateTimePage'}, + {'1': 'bleSecurityPage', '3': 11, '4': 1, '5': 11, '6': '.Zp.BleSecurityPage', '9': 0, '10': 'bleSecurityPage'}, + ], + '8': [ + {'1': 'notification'}, + ], + '9': [ + {'1': 7, '2': 8}, + ], +}; + +/// Descriptor for `DataChangeNotification`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List dataChangeNotificationDescriptor = $convert.base64Decode( + 'ChZEYXRhQ2hhbmdlTm90aWZpY2F0aW9uEisKB2RldkluZm8YASABKAsyDy5acC5EZXZJbmZvUG' + 'FnZUgAUgdkZXZJbmZvEkYKD3RyYWluZXJTaW1QYXJhbRgCIAEoCzIaLlpwLlRyYWluZXJTaW11' + 'bGF0aW9uUGFyYW1IAFIPdHJhaW5lclNpbVBhcmFtEjwKDnRyYWluZXJPcHRpb25zGAMgASgLMh' + 'IuWnAuVHJhaW5lck9wdGlvbnNIAFIOdHJhaW5lck9wdGlvbnMSRgoQZGV2aWNlVGlsdENvbmZp' + 'ZxgEIAEoCzIYLlpwLkRldmljZVRpbHRDb25maWdQYWdlSABSEGRldmljZVRpbHRDb25maWcSVQ' + 'oVY29udHJvbGxlcklucHV0Q29uZmlnGAUgASgLMh0uWnAuQ29udHJvbGxlcklucHV0Q29uZmln' + 'UGFnZUgAUhVjb250cm9sbGVySW5wdXRDb25maWcSWAoWdHJhaW5lckdlYXJJbmRleENvbmZpZx' + 'gGIAEoCzIeLlpwLlRyYWluZXJHZWFySW5kZXhDb25maWdQYWdlSABSFnRyYWluZXJHZWFySW5k' + 'ZXhDb25maWcSOwoKd2lmaUNvbmZpZxgIIAEoCzIZLlpwLldpZmlDb25maWd1cmF0aW9uUGFnZU' + 'gAUgp3aWZpQ29uZmlnEkIKEGRldmljZVVwZGF0ZVBhZ2UYCSABKAsyFC5acC5EZXZpY2VVcGRh' + 'dGVQYWdlSABSEGRldmljZVVwZGF0ZVBhZ2USNgoMZGF0ZVRpbWVQYWdlGAogASgLMhAuWnAuRG' + 'F0ZVRpbWVQYWdlSABSDGRhdGVUaW1lUGFnZRI/Cg9ibGVTZWN1cml0eVBhZ2UYCyABKAsyEy5a' + 'cC5CbGVTZWN1cml0eVBhZ2VIAFIPYmxlU2VjdXJpdHlQYWdlQg4KDG5vdGlmaWNhdGlvbkoECA' + 'cQCA=='); + +@$core.Deprecated('Use gameStateNotificationDescriptor instead') +const GameStateNotification$json = { + '1': 'GameStateNotification', + '2': [ + {'1': 'gameSpeed', '3': 1, '4': 1, '5': 11, '6': '.Zp.GameStateNotification.GameSpeed', '9': 0, '10': 'gameSpeed'}, + {'1': 'roadSurface', '3': 2, '4': 1, '5': 11, '6': '.Zp.GameStateNotification.RoadSurface', '9': 0, '10': 'roadSurface'}, + ], + '3': [GameStateNotification_GameSpeed$json, GameStateNotification_RoadSurface$json], + '8': [ + {'1': 'notification'}, + ], +}; + +@$core.Deprecated('Use gameStateNotificationDescriptor instead') +const GameStateNotification_GameSpeed$json = { + '1': 'GameSpeed', + '2': [ + {'1': 'normalisedSpeed', '3': 1, '4': 1, '5': 17, '8': {}, '10': 'normalisedSpeed'}, + {'1': 'speed', '3': 2, '4': 1, '5': 17, '8': {}, '10': 'speed'}, + ], +}; + +@$core.Deprecated('Use gameStateNotificationDescriptor instead') +const GameStateNotification_RoadSurface$json = { + '1': 'RoadSurface', + '2': [ + {'1': 'roadSurfaceType', '3': 1, '4': 1, '5': 14, '6': '.Zp.RoadSurfaceType', '10': 'roadSurfaceType'}, + {'1': 'roughness', '3': 2, '4': 1, '5': 13, '8': {}, '10': 'roughness'}, + ], +}; + +/// Descriptor for `GameStateNotification`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List gameStateNotificationDescriptor = $convert.base64Decode( + 'ChVHYW1lU3RhdGVOb3RpZmljYXRpb24SQwoJZ2FtZVNwZWVkGAEgASgLMiMuWnAuR2FtZVN0YX' + 'RlTm90aWZpY2F0aW9uLkdhbWVTcGVlZEgAUglnYW1lU3BlZWQSSQoLcm9hZFN1cmZhY2UYAiAB' + 'KAsyJS5acC5HYW1lU3RhdGVOb3RpZmljYXRpb24uUm9hZFN1cmZhY2VIAFILcm9hZFN1cmZhY2' + 'UaWQoJR2FtZVNwZWVkEi8KD25vcm1hbGlzZWRTcGVlZBgBIAEoEUIFkj8COAhSD25vcm1hbGlz' + 'ZWRTcGVlZBIbCgVzcGVlZBgCIAEoEUIFkj8COBBSBXNwZWVkGnEKC1JvYWRTdXJmYWNlEj0KD3' + 'JvYWRTdXJmYWNlVHlwZRgBIAEoDjITLlpwLlJvYWRTdXJmYWNlVHlwZVIPcm9hZFN1cmZhY2VU' + 'eXBlEiMKCXJvdWdobmVzcxgCIAEoDUIFkj8COAhSCXJvdWdobmVzc0IOCgxub3RpZmljYXRpb2' + '4='); + +@$core.Deprecated('Use sensorRelayConfigDescriptor instead') +const SensorRelayConfig$json = { + '1': 'SensorRelayConfig', + '2': [ + {'1': 'search', '3': 1, '4': 1, '5': 8, '9': 0, '10': 'search'}, + {'1': 'connect', '3': 2, '4': 1, '5': 13, '8': {}, '9': 0, '10': 'connect'}, + {'1': 'disconnect', '3': 3, '4': 1, '5': 13, '8': {}, '9': 0, '10': 'disconnect'}, + {'1': 'clearAll', '3': 4, '4': 1, '5': 8, '9': 0, '10': 'clearAll'}, + {'1': 'disconnectAll', '3': 5, '4': 1, '5': 8, '9': 0, '10': 'disconnectAll'}, + ], + '8': [ + {'1': 'procedure'}, + ], +}; + +/// Descriptor for `SensorRelayConfig`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List sensorRelayConfigDescriptor = $convert.base64Decode( + 'ChFTZW5zb3JSZWxheUNvbmZpZxIYCgZzZWFyY2gYASABKAhIAFIGc2VhcmNoEiEKB2Nvbm5lY3' + 'QYAiABKA1CBZI/AjgISABSB2Nvbm5lY3QSJwoKZGlzY29ubmVjdBgDIAEoDUIFkj8COAhIAFIK' + 'ZGlzY29ubmVjdBIcCghjbGVhckFsbBgEIAEoCEgAUghjbGVhckFsbBImCg1kaXNjb25uZWN0QW' + 'xsGAUgASgISABSDWRpc2Nvbm5lY3RBbGxCCwoJcHJvY2VkdXJl'); + +@$core.Deprecated('Use sensorRelayGetDescriptor instead') +const SensorRelayGet$json = { + '1': 'SensorRelayGet', + '2': [ + {'1': 'pairedSensorInfo', '3': 1, '4': 1, '5': 13, '8': {}, '9': 0, '10': 'pairedSensorInfo'}, + ], + '8': [ + {'1': 'request'}, + ], +}; + +/// Descriptor for `SensorRelayGet`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List sensorRelayGetDescriptor = $convert.base64Decode( + 'Cg5TZW5zb3JSZWxheUdldBIzChBwYWlyZWRTZW5zb3JJbmZvGAEgASgNQgWSPwI4CEgAUhBwYW' + 'lyZWRTZW5zb3JJbmZvQgkKB3JlcXVlc3Q='); + +@$core.Deprecated('Use sensorRelayResponseDescriptor instead') +const SensorRelayResponse$json = { + '1': 'SensorRelayResponse', + '2': [ + {'1': 'pairedSensorInfoList', '3': 1, '4': 1, '5': 11, '6': '.Zp.SensorInfoList', '9': 0, '10': 'pairedSensorInfoList'}, + ], + '8': [ + {'1': 'response'}, + ], +}; + +/// Descriptor for `SensorRelayResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List sensorRelayResponseDescriptor = $convert.base64Decode( + 'ChNTZW5zb3JSZWxheVJlc3BvbnNlEkgKFHBhaXJlZFNlbnNvckluZm9MaXN0GAEgASgLMhIuWn' + 'AuU2Vuc29ySW5mb0xpc3RIAFIUcGFpcmVkU2Vuc29ySW5mb0xpc3RCCgoIcmVzcG9uc2U='); + +@$core.Deprecated('Use sensorRelayNotificationDescriptor instead') +const SensorRelayNotification$json = { + '1': 'SensorRelayNotification', + '2': [ + {'1': 'newSensor', '3': 1, '4': 1, '5': 11, '6': '.Zp.SensorInfo', '9': 0, '10': 'newSensor'}, + {'1': 'sensorStatus', '3': 2, '4': 1, '5': 11, '6': '.Zp.SensorInfo', '9': 0, '10': 'sensorStatus'}, + ], + '8': [ + {'1': 'notification'}, + ], +}; + +/// Descriptor for `SensorRelayNotification`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List sensorRelayNotificationDescriptor = $convert.base64Decode( + 'ChdTZW5zb3JSZWxheU5vdGlmaWNhdGlvbhIuCgluZXdTZW5zb3IYASABKAsyDi5acC5TZW5zb3' + 'JJbmZvSABSCW5ld1NlbnNvchI0CgxzZW5zb3JTdGF0dXMYAiABKAsyDi5acC5TZW5zb3JJbmZv' + 'SABSDHNlbnNvclN0YXR1c0IOCgxub3RpZmljYXRpb24='); + +@$core.Deprecated('Use hrmDataNotificationDescriptor instead') +const HrmDataNotification$json = { + '1': 'HrmDataNotification', + '2': [ + {'1': 'sensorId', '3': 1, '4': 1, '5': 13, '8': {}, '10': 'sensorId'}, + {'1': 'hrm', '3': 2, '4': 1, '5': 13, '8': {}, '10': 'hrm'}, + {'1': 'energyExpended', '3': 3, '4': 1, '5': 13, '8': {}, '10': 'energyExpended'}, + {'1': 'rrInterval', '3': 4, '4': 3, '5': 13, '8': {}, '10': 'rrInterval'}, + ], +}; + +/// Descriptor for `HrmDataNotification`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List hrmDataNotificationDescriptor = $convert.base64Decode( + 'ChNIcm1EYXRhTm90aWZpY2F0aW9uEiEKCHNlbnNvcklkGAEgASgNQgWSPwI4CFIIc2Vuc29ySW' + 'QSFwoDaHJtGAIgASgNQgWSPwI4EFIDaHJtEi0KDmVuZXJneUV4cGVuZGVkGAMgASgNQgWSPwI4' + 'EFIOZW5lcmd5RXhwZW5kZWQSJQoKcnJJbnRlcnZhbBgEIAMoDUIFkj8CEA9SCnJySW50ZXJ2YW' + 'w='); + +@$core.Deprecated('Use wifiConfigurationRequestDescriptor instead') +const WifiConfigurationRequest$json = { + '1': 'WifiConfigurationRequest', + '2': [ + {'1': 'startScan', '3': 1, '4': 1, '5': 8, '9': 0, '10': 'startScan'}, + {'1': 'connect', '3': 2, '4': 1, '5': 11, '6': '.Zp.WifiNetwork', '9': 0, '10': 'connect'}, + {'1': 'forget', '3': 3, '4': 1, '5': 8, '9': 0, '10': 'forget'}, + {'1': 'setRegionCode', '3': 4, '4': 1, '5': 11, '6': '.Zp.WifiRegionCode', '9': 0, '10': 'setRegionCode'}, + ], + '8': [ + {'1': 'request'}, + ], +}; + +/// Descriptor for `WifiConfigurationRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List wifiConfigurationRequestDescriptor = $convert.base64Decode( + 'ChhXaWZpQ29uZmlndXJhdGlvblJlcXVlc3QSHgoJc3RhcnRTY2FuGAEgASgISABSCXN0YXJ0U2' + 'NhbhIrCgdjb25uZWN0GAIgASgLMg8uWnAuV2lmaU5ldHdvcmtIAFIHY29ubmVjdBIYCgZmb3Jn' + 'ZXQYAyABKAhIAFIGZm9yZ2V0EjoKDXNldFJlZ2lvbkNvZGUYBCABKAsyEi5acC5XaWZpUmVnaW' + '9uQ29kZUgAUg1zZXRSZWdpb25Db2RlQgkKB3JlcXVlc3Q='); + +@$core.Deprecated('Use wifiNotificationDescriptor instead') +const WifiNotification$json = { + '1': 'WifiNotification', + '2': [ + {'1': 'discoveredNetwork', '3': 1, '4': 1, '5': 11, '6': '.Zp.WifiNetworkDetails', '9': 0, '10': 'discoveredNetwork'}, + {'1': 'wifiStatus', '3': 2, '4': 1, '5': 11, '6': '.Zp.WifiNotification.WifiStatus', '9': 0, '10': 'wifiStatus'}, + ], + '3': [WifiNotification_WifiStatus$json], + '8': [ + {'1': 'notification'}, + ], +}; + +@$core.Deprecated('Use wifiNotificationDescriptor instead') +const WifiNotification_WifiStatus$json = { + '1': 'WifiStatus', + '2': [ + {'1': 'wifiStatusCode', '3': 1, '4': 1, '5': 14, '6': '.Zp.WifiStatusCode', '10': 'wifiStatusCode'}, + {'1': 'wifiErrorCode', '3': 2, '4': 1, '5': 14, '6': '.Zp.WifiErrorCode', '10': 'wifiErrorCode'}, + ], +}; + +/// Descriptor for `WifiNotification`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List wifiNotificationDescriptor = $convert.base64Decode( + 'ChBXaWZpTm90aWZpY2F0aW9uEkYKEWRpc2NvdmVyZWROZXR3b3JrGAEgASgLMhYuWnAuV2lmaU' + '5ldHdvcmtEZXRhaWxzSABSEWRpc2NvdmVyZWROZXR3b3JrEkEKCndpZmlTdGF0dXMYAiABKAsy' + 'Hy5acC5XaWZpTm90aWZpY2F0aW9uLldpZmlTdGF0dXNIAFIKd2lmaVN0YXR1cxqBAQoKV2lmaV' + 'N0YXR1cxI6Cg53aWZpU3RhdHVzQ29kZRgBIAEoDjISLlpwLldpZmlTdGF0dXNDb2RlUg53aWZp' + 'U3RhdHVzQ29kZRI3Cg13aWZpRXJyb3JDb2RlGAIgASgOMhEuWnAuV2lmaUVycm9yQ29kZVINd2' + 'lmaUVycm9yQ29kZUIOCgxub3RpZmljYXRpb24='); + +@$core.Deprecated('Use powerDataNotificationDescriptor instead') +const PowerDataNotification$json = { + '1': 'PowerDataNotification', + '2': [ + {'1': 'sensorId', '3': 1, '4': 1, '5': 13, '8': {}, '10': 'sensorId'}, + {'1': 'power', '3': 2, '4': 1, '5': 13, '8': {}, '10': 'power'}, + ], +}; + +/// Descriptor for `PowerDataNotification`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List powerDataNotificationDescriptor = $convert.base64Decode( + 'ChVQb3dlckRhdGFOb3RpZmljYXRpb24SIQoIc2Vuc29ySWQYASABKA1CBZI/AjgIUghzZW5zb3' + 'JJZBIbCgVwb3dlchgCIAEoDUIFkj8COBBSBXBvd2Vy'); + +@$core.Deprecated('Use cadenceDataNotificationDescriptor instead') +const CadenceDataNotification$json = { + '1': 'CadenceDataNotification', + '2': [ + {'1': 'sensorId', '3': 1, '4': 1, '5': 13, '8': {}, '10': 'sensorId'}, + {'1': 'cadence', '3': 2, '4': 1, '5': 13, '8': {}, '10': 'cadence'}, + ], +}; + +/// Descriptor for `CadenceDataNotification`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List cadenceDataNotificationDescriptor = $convert.base64Decode( + 'ChdDYWRlbmNlRGF0YU5vdGlmaWNhdGlvbhIhCghzZW5zb3JJZBgBIAEoDUIFkj8COAhSCHNlbn' + 'NvcklkEh8KB2NhZGVuY2UYAiABKA1CBZI/AjgQUgdjYWRlbmNl'); + +@$core.Deprecated('Use deviceUpdateRequestDescriptor instead') +const DeviceUpdateRequest$json = { + '1': 'DeviceUpdateRequest', + '2': [ + {'1': 'checkForUpdates', '3': 1, '4': 1, '5': 8, '9': 0, '10': 'checkForUpdates'}, + {'1': 'activateUpdates', '3': 2, '4': 1, '5': 8, '9': 0, '10': 'activateUpdates'}, + ], + '8': [ + {'1': 'procedure'}, + ], +}; + +/// Descriptor for `DeviceUpdateRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List deviceUpdateRequestDescriptor = $convert.base64Decode( + 'ChNEZXZpY2VVcGRhdGVSZXF1ZXN0EioKD2NoZWNrRm9yVXBkYXRlcxgBIAEoCEgAUg9jaGVja0' + 'ZvclVwZGF0ZXMSKgoPYWN0aXZhdGVVcGRhdGVzGAIgASgISABSD2FjdGl2YXRlVXBkYXRlc0IL' + 'Cglwcm9jZWR1cmU='); + +@$core.Deprecated('Use relayZpMessageDescriptor instead') +const RelayZpMessage$json = { + '1': 'RelayZpMessage', + '2': [ + {'1': 'relayAssignedId', '3': 1, '4': 1, '5': 13, '8': {}, '10': 'relayAssignedId'}, + {'1': 'payload', '3': 2, '4': 1, '5': 12, '8': {}, '10': 'payload'}, + ], +}; + +/// Descriptor for `RelayZpMessage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List relayZpMessageDescriptor = $convert.base64Decode( + 'Cg5SZWxheVpwTWVzc2FnZRIvCg9yZWxheUFzc2lnbmVkSWQYASABKA1CBZI/AjgIUg9yZWxheU' + 'Fzc2lnbmVkSWQSIAoHcGF5bG9hZBgCIAEoDEIGkj8DCO4BUgdwYXlsb2Fk'); + diff --git a/lib/bluetooth/devices/zwift/protocol/zp.pbserver.dart b/lib/bluetooth/devices/zwift/protocol/zp.pbserver.dart new file mode 100644 index 000000000..c34f0cfdf --- /dev/null +++ b/lib/bluetooth/devices/zwift/protocol/zp.pbserver.dart @@ -0,0 +1,14 @@ +// +// Generated code. Do not modify. +// source: zp.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +export 'zp.pb.dart'; + diff --git a/lib/bluetooth/devices/zwift/protocol/zp_vendor.pb.dart b/lib/bluetooth/devices/zwift/protocol/zp_vendor.pb.dart new file mode 100644 index 000000000..664ce39c7 --- /dev/null +++ b/lib/bluetooth/devices/zwift/protocol/zp_vendor.pb.dart @@ -0,0 +1,896 @@ +// +// Generated code. Do not modify. +// source: zp_vendor.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'zp_vendor.pbenum.dart'; + +export 'zp_vendor.pbenum.dart'; + +class ControllerSync extends $pb.GeneratedMessage { + factory ControllerSync({ + ControllerSyncStatus? status, + $core.int? timeStamp, + }) { + final $result = create(); + if (status != null) { + $result.status = status; + } + if (timeStamp != null) { + $result.timeStamp = timeStamp; + } + return $result; + } + ControllerSync._() : super(); + factory ControllerSync.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ControllerSync.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ControllerSync', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create) + ..e(1, _omitFieldNames ? '' : 'status', $pb.PbFieldType.OE, defaultOrMaker: ControllerSyncStatus.NOT_CONNECTED, valueOf: ControllerSyncStatus.valueOf, enumValues: ControllerSyncStatus.values) + ..a<$core.int>(2, _omitFieldNames ? '' : 'timeStamp', $pb.PbFieldType.O3, protoName: 'timeStamp') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ControllerSync clone() => ControllerSync()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ControllerSync copyWith(void Function(ControllerSync) updates) => super.copyWith((message) => updates(message as ControllerSync)) as ControllerSync; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ControllerSync create() => ControllerSync._(); + ControllerSync createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ControllerSync getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ControllerSync? _defaultInstance; + + /// optional in code; proto3 treats as present when non-zero + @$pb.TagNumber(1) + ControllerSyncStatus get status => $_getN(0); + @$pb.TagNumber(1) + set status(ControllerSyncStatus v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasStatus() => $_has(0); + @$pb.TagNumber(1) + void clearStatus() => clearField(1); + + @$pb.TagNumber(2) + $core.int get timeStamp => $_getIZ(1); + @$pb.TagNumber(2) + set timeStamp($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasTimeStamp() => $_has(1); + @$pb.TagNumber(2) + void clearTimeStamp() => clearField(2); +} + +class EnableTestMode extends $pb.GeneratedMessage { + factory EnableTestMode({ + $core.bool? enable, + }) { + final $result = create(); + if (enable != null) { + $result.enable = enable; + } + return $result; + } + EnableTestMode._() : super(); + factory EnableTestMode.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory EnableTestMode.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'EnableTestMode', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'enable') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + EnableTestMode clone() => EnableTestMode()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + EnableTestMode copyWith(void Function(EnableTestMode) updates) => super.copyWith((message) => updates(message as EnableTestMode)) as EnableTestMode; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static EnableTestMode create() => EnableTestMode._(); + EnableTestMode createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static EnableTestMode getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static EnableTestMode? _defaultInstance; + + @$pb.TagNumber(1) + $core.bool get enable => $_getBF(0); + @$pb.TagNumber(1) + set enable($core.bool v) { $_setBool(0, v); } + @$pb.TagNumber(1) + $core.bool hasEnable() => $_has(0); + @$pb.TagNumber(1) + void clearEnable() => clearField(1); +} + +class PairDevices extends $pb.GeneratedMessage { + factory PairDevices({ + $core.bool? pair, + PairDeviceType? type, + $core.List<$core.int>? deviceId, + }) { + final $result = create(); + if (pair != null) { + $result.pair = pair; + } + if (type != null) { + $result.type = type; + } + if (deviceId != null) { + $result.deviceId = deviceId; + } + return $result; + } + PairDevices._() : super(); + factory PairDevices.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory PairDevices.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'PairDevices', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'pair') + ..e(2, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: PairDeviceType.BLE, valueOf: PairDeviceType.valueOf, enumValues: PairDeviceType.values) + ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'deviceId', $pb.PbFieldType.OY, protoName: 'deviceId') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + PairDevices clone() => PairDevices()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + PairDevices copyWith(void Function(PairDevices) updates) => super.copyWith((message) => updates(message as PairDevices)) as PairDevices; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static PairDevices create() => PairDevices._(); + PairDevices createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static PairDevices getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static PairDevices? _defaultInstance; + + @$pb.TagNumber(1) + $core.bool get pair => $_getBF(0); + @$pb.TagNumber(1) + set pair($core.bool v) { $_setBool(0, v); } + @$pb.TagNumber(1) + $core.bool hasPair() => $_has(0); + @$pb.TagNumber(1) + void clearPair() => clearField(1); + + @$pb.TagNumber(2) + PairDeviceType get type => $_getN(1); + @$pb.TagNumber(2) + set type(PairDeviceType v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasType() => $_has(1); + @$pb.TagNumber(2) + void clearType() => clearField(2); + + @$pb.TagNumber(3) + $core.List<$core.int> get deviceId => $_getN(2); + @$pb.TagNumber(3) + set deviceId($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasDeviceId() => $_has(2); + @$pb.TagNumber(3) + void clearDeviceId() => clearField(3); +} + +class DevicePairingDataPage_PairedDevice extends $pb.GeneratedMessage { + factory DevicePairingDataPage_PairedDevice({ + $core.List<$core.int>? device, + }) { + final $result = create(); + if (device != null) { + $result.device = device; + } + return $result; + } + DevicePairingDataPage_PairedDevice._() : super(); + factory DevicePairingDataPage_PairedDevice.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DevicePairingDataPage_PairedDevice.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DevicePairingDataPage.PairedDevice', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'device', $pb.PbFieldType.OY) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + DevicePairingDataPage_PairedDevice clone() => DevicePairingDataPage_PairedDevice()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DevicePairingDataPage_PairedDevice copyWith(void Function(DevicePairingDataPage_PairedDevice) updates) => super.copyWith((message) => updates(message as DevicePairingDataPage_PairedDevice)) as DevicePairingDataPage_PairedDevice; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DevicePairingDataPage_PairedDevice create() => DevicePairingDataPage_PairedDevice._(); + DevicePairingDataPage_PairedDevice createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DevicePairingDataPage_PairedDevice getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DevicePairingDataPage_PairedDevice? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get device => $_getN(0); + @$pb.TagNumber(1) + set device($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasDevice() => $_has(0); + @$pb.TagNumber(1) + void clearDevice() => clearField(1); +} + +class DevicePairingDataPage extends $pb.GeneratedMessage { + factory DevicePairingDataPage({ + $core.int? devicesCount, + $core.int? pairingStatus, + $core.Iterable? pairingDevList, + }) { + final $result = create(); + if (devicesCount != null) { + $result.devicesCount = devicesCount; + } + if (pairingStatus != null) { + $result.pairingStatus = pairingStatus; + } + if (pairingDevList != null) { + $result.pairingDevList.addAll(pairingDevList); + } + return $result; + } + DevicePairingDataPage._() : super(); + factory DevicePairingDataPage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory DevicePairingDataPage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'DevicePairingDataPage', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'devicesCount', $pb.PbFieldType.O3, protoName: 'devicesCount') + ..a<$core.int>(2, _omitFieldNames ? '' : 'pairingStatus', $pb.PbFieldType.O3, protoName: 'pairingStatus') + ..pc(3, _omitFieldNames ? '' : 'pairingDevList', $pb.PbFieldType.PM, protoName: 'pairingDevList', subBuilder: DevicePairingDataPage_PairedDevice.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + DevicePairingDataPage clone() => DevicePairingDataPage()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + DevicePairingDataPage copyWith(void Function(DevicePairingDataPage) updates) => super.copyWith((message) => updates(message as DevicePairingDataPage)) as DevicePairingDataPage; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static DevicePairingDataPage create() => DevicePairingDataPage._(); + DevicePairingDataPage createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static DevicePairingDataPage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static DevicePairingDataPage? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get devicesCount => $_getIZ(0); + @$pb.TagNumber(1) + set devicesCount($core.int v) { $_setSignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasDevicesCount() => $_has(0); + @$pb.TagNumber(1) + void clearDevicesCount() => clearField(1); + + @$pb.TagNumber(2) + $core.int get pairingStatus => $_getIZ(1); + @$pb.TagNumber(2) + set pairingStatus($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasPairingStatus() => $_has(1); + @$pb.TagNumber(2) + void clearPairingStatus() => clearField(2); + + @$pb.TagNumber(3) + $core.List get pairingDevList => $_getList(2); +} + +enum SetDfuTest_TestCase { + failedEnterDfu, + failedStartAdvertising, + crcFailure, + notSet +} + +class SetDfuTest extends $pb.GeneratedMessage { + factory SetDfuTest({ + $core.bool? failedEnterDfu, + $core.bool? failedStartAdvertising, + $core.int? crcFailure, + }) { + final $result = create(); + if (failedEnterDfu != null) { + $result.failedEnterDfu = failedEnterDfu; + } + if (failedStartAdvertising != null) { + $result.failedStartAdvertising = failedStartAdvertising; + } + if (crcFailure != null) { + $result.crcFailure = crcFailure; + } + return $result; + } + SetDfuTest._() : super(); + factory SetDfuTest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SetDfuTest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static const $core.Map<$core.int, SetDfuTest_TestCase> _SetDfuTest_TestCaseByTag = { + 1 : SetDfuTest_TestCase.failedEnterDfu, + 2 : SetDfuTest_TestCase.failedStartAdvertising, + 3 : SetDfuTest_TestCase.crcFailure, + 0 : SetDfuTest_TestCase.notSet + }; + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SetDfuTest', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create) + ..oo(0, [1, 2, 3]) + ..aOB(1, _omitFieldNames ? '' : 'failedEnterDfu', protoName: 'failedEnterDfu') + ..aOB(2, _omitFieldNames ? '' : 'failedStartAdvertising', protoName: 'failedStartAdvertising') + ..a<$core.int>(3, _omitFieldNames ? '' : 'crcFailure', $pb.PbFieldType.O3, protoName: 'crcFailure') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SetDfuTest clone() => SetDfuTest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SetDfuTest copyWith(void Function(SetDfuTest) updates) => super.copyWith((message) => updates(message as SetDfuTest)) as SetDfuTest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SetDfuTest create() => SetDfuTest._(); + SetDfuTest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SetDfuTest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SetDfuTest? _defaultInstance; + + SetDfuTest_TestCase whichTestCase() => _SetDfuTest_TestCaseByTag[$_whichOneof(0)]!; + void clearTestCase() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + $core.bool get failedEnterDfu => $_getBF(0); + @$pb.TagNumber(1) + set failedEnterDfu($core.bool v) { $_setBool(0, v); } + @$pb.TagNumber(1) + $core.bool hasFailedEnterDfu() => $_has(0); + @$pb.TagNumber(1) + void clearFailedEnterDfu() => clearField(1); + + @$pb.TagNumber(2) + $core.bool get failedStartAdvertising => $_getBF(1); + @$pb.TagNumber(2) + set failedStartAdvertising($core.bool v) { $_setBool(1, v); } + @$pb.TagNumber(2) + $core.bool hasFailedStartAdvertising() => $_has(1); + @$pb.TagNumber(2) + void clearFailedStartAdvertising() => clearField(2); + + @$pb.TagNumber(3) + $core.int get crcFailure => $_getIZ(2); + @$pb.TagNumber(3) + set crcFailure($core.int v) { $_setSignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasCrcFailure() => $_has(2); + @$pb.TagNumber(3) + void clearCrcFailure() => clearField(3); +} + +class SetGearTestData extends $pb.GeneratedMessage { + factory SetGearTestData({ + $core.int? frontGearIdx, + $core.int? rearGearIdx, + }) { + final $result = create(); + if (frontGearIdx != null) { + $result.frontGearIdx = frontGearIdx; + } + if (rearGearIdx != null) { + $result.rearGearIdx = rearGearIdx; + } + return $result; + } + SetGearTestData._() : super(); + factory SetGearTestData.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SetGearTestData.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SetGearTestData', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'frontGearIdx', $pb.PbFieldType.O3, protoName: 'frontGearIdx') + ..a<$core.int>(2, _omitFieldNames ? '' : 'rearGearIdx', $pb.PbFieldType.O3, protoName: 'rearGearIdx') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SetGearTestData clone() => SetGearTestData()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SetGearTestData copyWith(void Function(SetGearTestData) updates) => super.copyWith((message) => updates(message as SetGearTestData)) as SetGearTestData; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SetGearTestData create() => SetGearTestData._(); + SetGearTestData createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SetGearTestData getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SetGearTestData? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get frontGearIdx => $_getIZ(0); + @$pb.TagNumber(1) + set frontGearIdx($core.int v) { $_setSignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasFrontGearIdx() => $_has(0); + @$pb.TagNumber(1) + void clearFrontGearIdx() => clearField(1); + + @$pb.TagNumber(2) + $core.int get rearGearIdx => $_getIZ(1); + @$pb.TagNumber(2) + set rearGearIdx($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasRearGearIdx() => $_has(1); + @$pb.TagNumber(2) + void clearRearGearIdx() => clearField(2); +} + +class SetHrmTestData extends $pb.GeneratedMessage { + factory SetHrmTestData({ + $core.bool? hrmPresent, + $core.int? heartRate, + }) { + final $result = create(); + if (hrmPresent != null) { + $result.hrmPresent = hrmPresent; + } + if (heartRate != null) { + $result.heartRate = heartRate; + } + return $result; + } + SetHrmTestData._() : super(); + factory SetHrmTestData.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SetHrmTestData.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SetHrmTestData', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'hrmPresent', protoName: 'hrmPresent') + ..a<$core.int>(2, _omitFieldNames ? '' : 'heartRate', $pb.PbFieldType.O3, protoName: 'heartRate') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SetHrmTestData clone() => SetHrmTestData()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SetHrmTestData copyWith(void Function(SetHrmTestData) updates) => super.copyWith((message) => updates(message as SetHrmTestData)) as SetHrmTestData; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SetHrmTestData create() => SetHrmTestData._(); + SetHrmTestData createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SetHrmTestData getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SetHrmTestData? _defaultInstance; + + @$pb.TagNumber(1) + $core.bool get hrmPresent => $_getBF(0); + @$pb.TagNumber(1) + set hrmPresent($core.bool v) { $_setBool(0, v); } + @$pb.TagNumber(1) + $core.bool hasHrmPresent() => $_has(0); + @$pb.TagNumber(1) + void clearHrmPresent() => clearField(1); + + @$pb.TagNumber(2) + $core.int get heartRate => $_getIZ(1); + @$pb.TagNumber(2) + set heartRate($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasHeartRate() => $_has(1); + @$pb.TagNumber(2) + void clearHeartRate() => clearField(2); +} + +class SetInputDeviceTestData_ControllerAnalogEvent extends $pb.GeneratedMessage { + factory SetInputDeviceTestData_ControllerAnalogEvent({ + $core.int? sensorId, + $core.int? value, + }) { + final $result = create(); + if (sensorId != null) { + $result.sensorId = sensorId; + } + if (value != null) { + $result.value = value; + } + return $result; + } + SetInputDeviceTestData_ControllerAnalogEvent._() : super(); + factory SetInputDeviceTestData_ControllerAnalogEvent.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SetInputDeviceTestData_ControllerAnalogEvent.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SetInputDeviceTestData.ControllerAnalogEvent', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'sensorId', $pb.PbFieldType.O3, protoName: 'sensorId') + ..a<$core.int>(2, _omitFieldNames ? '' : 'value', $pb.PbFieldType.O3) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SetInputDeviceTestData_ControllerAnalogEvent clone() => SetInputDeviceTestData_ControllerAnalogEvent()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SetInputDeviceTestData_ControllerAnalogEvent copyWith(void Function(SetInputDeviceTestData_ControllerAnalogEvent) updates) => super.copyWith((message) => updates(message as SetInputDeviceTestData_ControllerAnalogEvent)) as SetInputDeviceTestData_ControllerAnalogEvent; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SetInputDeviceTestData_ControllerAnalogEvent create() => SetInputDeviceTestData_ControllerAnalogEvent._(); + SetInputDeviceTestData_ControllerAnalogEvent createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SetInputDeviceTestData_ControllerAnalogEvent getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SetInputDeviceTestData_ControllerAnalogEvent? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get sensorId => $_getIZ(0); + @$pb.TagNumber(1) + set sensorId($core.int v) { $_setSignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasSensorId() => $_has(0); + @$pb.TagNumber(1) + void clearSensorId() => clearField(1); + + @$pb.TagNumber(2) + $core.int get value => $_getIZ(1); + @$pb.TagNumber(2) + set value($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasValue() => $_has(1); + @$pb.TagNumber(2) + void clearValue() => clearField(2); +} + +class SetInputDeviceTestData extends $pb.GeneratedMessage { + factory SetInputDeviceTestData({ + $core.int? duration, + $core.int? buttonEvent, + $core.Iterable? analogEventList, + }) { + final $result = create(); + if (duration != null) { + $result.duration = duration; + } + if (buttonEvent != null) { + $result.buttonEvent = buttonEvent; + } + if (analogEventList != null) { + $result.analogEventList.addAll(analogEventList); + } + return $result; + } + SetInputDeviceTestData._() : super(); + factory SetInputDeviceTestData.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SetInputDeviceTestData.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SetInputDeviceTestData', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'duration', $pb.PbFieldType.O3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'buttonEvent', $pb.PbFieldType.O3, protoName: 'buttonEvent') + ..pc(3, _omitFieldNames ? '' : 'analogEventList', $pb.PbFieldType.PM, protoName: 'analogEventList', subBuilder: SetInputDeviceTestData_ControllerAnalogEvent.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SetInputDeviceTestData clone() => SetInputDeviceTestData()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SetInputDeviceTestData copyWith(void Function(SetInputDeviceTestData) updates) => super.copyWith((message) => updates(message as SetInputDeviceTestData)) as SetInputDeviceTestData; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SetInputDeviceTestData create() => SetInputDeviceTestData._(); + SetInputDeviceTestData createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SetInputDeviceTestData getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SetInputDeviceTestData? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get duration => $_getIZ(0); + @$pb.TagNumber(1) + set duration($core.int v) { $_setSignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasDuration() => $_has(0); + @$pb.TagNumber(1) + void clearDuration() => clearField(1); + + @$pb.TagNumber(2) + $core.int get buttonEvent => $_getIZ(1); + @$pb.TagNumber(2) + set buttonEvent($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasButtonEvent() => $_has(1); + @$pb.TagNumber(2) + void clearButtonEvent() => clearField(2); + + @$pb.TagNumber(3) + $core.List get analogEventList => $_getList(2); +} + +class SetTrainerTestData extends $pb.GeneratedMessage { + factory SetTrainerTestData({ + $core.int? dataMode, + $core.int? interfaces, + TestTrainerData? testTrainerData, + }) { + final $result = create(); + if (dataMode != null) { + $result.dataMode = dataMode; + } + if (interfaces != null) { + $result.interfaces = interfaces; + } + if (testTrainerData != null) { + $result.testTrainerData = testTrainerData; + } + return $result; + } + SetTrainerTestData._() : super(); + factory SetTrainerTestData.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SetTrainerTestData.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SetTrainerTestData', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'dataMode', $pb.PbFieldType.O3, protoName: 'dataMode') + ..a<$core.int>(2, _omitFieldNames ? '' : 'interfaces', $pb.PbFieldType.O3) + ..aOM(3, _omitFieldNames ? '' : 'testTrainerData', protoName: 'testTrainerData', subBuilder: TestTrainerData.create) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SetTrainerTestData clone() => SetTrainerTestData()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SetTrainerTestData copyWith(void Function(SetTrainerTestData) updates) => super.copyWith((message) => updates(message as SetTrainerTestData)) as SetTrainerTestData; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SetTrainerTestData create() => SetTrainerTestData._(); + SetTrainerTestData createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SetTrainerTestData getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SetTrainerTestData? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get dataMode => $_getIZ(0); + @$pb.TagNumber(1) + set dataMode($core.int v) { $_setSignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasDataMode() => $_has(0); + @$pb.TagNumber(1) + void clearDataMode() => clearField(1); + + @$pb.TagNumber(2) + $core.int get interfaces => $_getIZ(1); + @$pb.TagNumber(2) + set interfaces($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasInterfaces() => $_has(1); + @$pb.TagNumber(2) + void clearInterfaces() => clearField(2); + + @$pb.TagNumber(3) + TestTrainerData get testTrainerData => $_getN(2); + @$pb.TagNumber(3) + set testTrainerData(TestTrainerData v) { setField(3, v); } + @$pb.TagNumber(3) + $core.bool hasTestTrainerData() => $_has(2); + @$pb.TagNumber(3) + void clearTestTrainerData() => clearField(3); + @$pb.TagNumber(3) + TestTrainerData ensureTestTrainerData() => $_ensure(2); +} + +class TestTrainerData extends $pb.GeneratedMessage { + factory TestTrainerData({ + $core.int? power, + $core.int? cadence, + $core.int? bikeSpeed, + $core.int? averagedPower, + $core.int? wheelSpeed, + $core.int? calculatedRealGearRatio, + }) { + final $result = create(); + if (power != null) { + $result.power = power; + } + if (cadence != null) { + $result.cadence = cadence; + } + if (bikeSpeed != null) { + $result.bikeSpeed = bikeSpeed; + } + if (averagedPower != null) { + $result.averagedPower = averagedPower; + } + if (wheelSpeed != null) { + $result.wheelSpeed = wheelSpeed; + } + if (calculatedRealGearRatio != null) { + $result.calculatedRealGearRatio = calculatedRealGearRatio; + } + return $result; + } + TestTrainerData._() : super(); + factory TestTrainerData.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory TestTrainerData.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'TestTrainerData', package: const $pb.PackageName(_omitMessageNames ? '' : 'com.zwift.protobuf'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'power', $pb.PbFieldType.O3) + ..a<$core.int>(2, _omitFieldNames ? '' : 'cadence', $pb.PbFieldType.O3) + ..a<$core.int>(3, _omitFieldNames ? '' : 'bikeSpeed', $pb.PbFieldType.O3, protoName: 'bikeSpeed') + ..a<$core.int>(4, _omitFieldNames ? '' : 'averagedPower', $pb.PbFieldType.O3, protoName: 'averagedPower') + ..a<$core.int>(5, _omitFieldNames ? '' : 'wheelSpeed', $pb.PbFieldType.O3, protoName: 'wheelSpeed') + ..a<$core.int>(6, _omitFieldNames ? '' : 'calculatedRealGearRatio', $pb.PbFieldType.O3, protoName: 'calculatedRealGearRatio') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + TestTrainerData clone() => TestTrainerData()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + TestTrainerData copyWith(void Function(TestTrainerData) updates) => super.copyWith((message) => updates(message as TestTrainerData)) as TestTrainerData; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static TestTrainerData create() => TestTrainerData._(); + TestTrainerData createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static TestTrainerData getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static TestTrainerData? _defaultInstance; + + @$pb.TagNumber(1) + $core.int get power => $_getIZ(0); + @$pb.TagNumber(1) + set power($core.int v) { $_setSignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasPower() => $_has(0); + @$pb.TagNumber(1) + void clearPower() => clearField(1); + + @$pb.TagNumber(2) + $core.int get cadence => $_getIZ(1); + @$pb.TagNumber(2) + set cadence($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasCadence() => $_has(1); + @$pb.TagNumber(2) + void clearCadence() => clearField(2); + + @$pb.TagNumber(3) + $core.int get bikeSpeed => $_getIZ(2); + @$pb.TagNumber(3) + set bikeSpeed($core.int v) { $_setSignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasBikeSpeed() => $_has(2); + @$pb.TagNumber(3) + void clearBikeSpeed() => clearField(3); + + @$pb.TagNumber(4) + $core.int get averagedPower => $_getIZ(3); + @$pb.TagNumber(4) + set averagedPower($core.int v) { $_setSignedInt32(3, v); } + @$pb.TagNumber(4) + $core.bool hasAveragedPower() => $_has(3); + @$pb.TagNumber(4) + void clearAveragedPower() => clearField(4); + + @$pb.TagNumber(5) + $core.int get wheelSpeed => $_getIZ(4); + @$pb.TagNumber(5) + set wheelSpeed($core.int v) { $_setSignedInt32(4, v); } + @$pb.TagNumber(5) + $core.bool hasWheelSpeed() => $_has(4); + @$pb.TagNumber(5) + void clearWheelSpeed() => clearField(5); + + @$pb.TagNumber(6) + $core.int get calculatedRealGearRatio => $_getIZ(5); + @$pb.TagNumber(6) + set calculatedRealGearRatio($core.int v) { $_setSignedInt32(5, v); } + @$pb.TagNumber(6) + $core.bool hasCalculatedRealGearRatio() => $_has(5); + @$pb.TagNumber(6) + void clearCalculatedRealGearRatio() => clearField(6); +} + + +const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); +const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/lib/bluetooth/devices/zwift/protocol/zp_vendor.pbenum.dart b/lib/bluetooth/devices/zwift/protocol/zp_vendor.pbenum.dart new file mode 100644 index 000000000..a087a0c73 --- /dev/null +++ b/lib/bluetooth/devices/zwift/protocol/zp_vendor.pbenum.dart @@ -0,0 +1,101 @@ +// +// Generated code. Do not modify. +// source: zp_vendor.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class VendorOpcode extends $pb.ProtobufEnum { + static const VendorOpcode UNDEFINED = VendorOpcode._(0, _omitEnumNames ? '' : 'UNDEFINED'); + static const VendorOpcode CONTROLLER_SYNC = VendorOpcode._(1, _omitEnumNames ? '' : 'CONTROLLER_SYNC'); + static const VendorOpcode PAIR_DEVICES = VendorOpcode._(2, _omitEnumNames ? '' : 'PAIR_DEVICES'); + static const VendorOpcode ENABLE_TEST_MODE = VendorOpcode._(65280, _omitEnumNames ? '' : 'ENABLE_TEST_MODE'); + static const VendorOpcode SET_DFU_TEST = VendorOpcode._(65281, _omitEnumNames ? '' : 'SET_DFU_TEST'); + static const VendorOpcode SET_TRAINER_TEST_DATA = VendorOpcode._(65282, _omitEnumNames ? '' : 'SET_TRAINER_TEST_DATA'); + static const VendorOpcode SET_INPUT_DEVICE_TEST_DATA = VendorOpcode._(65283, _omitEnumNames ? '' : 'SET_INPUT_DEVICE_TEST_DATA'); + static const VendorOpcode SET_GEAR_TEST_DATA = VendorOpcode._(65284, _omitEnumNames ? '' : 'SET_GEAR_TEST_DATA'); + static const VendorOpcode SET_HRM_TEST_DATA = VendorOpcode._(65285, _omitEnumNames ? '' : 'SET_HRM_TEST_DATA'); + static const VendorOpcode SET_TEST_DATA = VendorOpcode._(65286, _omitEnumNames ? '' : 'SET_TEST_DATA'); + + static const $core.List values = [ + UNDEFINED, + CONTROLLER_SYNC, + PAIR_DEVICES, + ENABLE_TEST_MODE, + SET_DFU_TEST, + SET_TRAINER_TEST_DATA, + SET_INPUT_DEVICE_TEST_DATA, + SET_GEAR_TEST_DATA, + SET_HRM_TEST_DATA, + SET_TEST_DATA, + ]; + + static final $core.Map<$core.int, VendorOpcode> _byValue = $pb.ProtobufEnum.initByValue(values); + static VendorOpcode? valueOf($core.int value) => _byValue[value]; + + const VendorOpcode._($core.int v, $core.String n) : super(v, n); +} + +class PairDeviceType extends $pb.ProtobufEnum { + static const PairDeviceType BLE = PairDeviceType._(0, _omitEnumNames ? '' : 'BLE'); + static const PairDeviceType ANT = PairDeviceType._(1, _omitEnumNames ? '' : 'ANT'); + + static const $core.List values = [ + BLE, + ANT, + ]; + + static final $core.Map<$core.int, PairDeviceType> _byValue = $pb.ProtobufEnum.initByValue(values); + static PairDeviceType? valueOf($core.int value) => _byValue[value]; + + const PairDeviceType._($core.int v, $core.String n) : super(v, n); +} + +/// Status used by ControllerSync +class ControllerSyncStatus extends $pb.ProtobufEnum { + static const ControllerSyncStatus NOT_CONNECTED = ControllerSyncStatus._(0, _omitEnumNames ? '' : 'NOT_CONNECTED'); + static const ControllerSyncStatus CONNECTED = ControllerSyncStatus._(1, _omitEnumNames ? '' : 'CONNECTED'); + + static const $core.List values = [ + NOT_CONNECTED, + CONNECTED, + ]; + + static final $core.Map<$core.int, ControllerSyncStatus> _byValue = $pb.ProtobufEnum.initByValue(values); + static ControllerSyncStatus? valueOf($core.int value) => _byValue[value]; + + const ControllerSyncStatus._($core.int v, $core.String n) : super(v, n); +} + +/// Looks like “data object / page” IDs used with pairing pages +class VendorDO extends $pb.ProtobufEnum { + static const VendorDO NO_CLUE = VendorDO._(0, _omitEnumNames ? '' : 'NO_CLUE'); + static const VendorDO PAGE_DEVICE_PAIRING = VendorDO._(61440, _omitEnumNames ? '' : 'PAGE_DEVICE_PAIRING'); + static const VendorDO DEVICE_COUNT = VendorDO._(61441, _omitEnumNames ? '' : 'DEVICE_COUNT'); + static const VendorDO PAIRING_STATUS = VendorDO._(61442, _omitEnumNames ? '' : 'PAIRING_STATUS'); + static const VendorDO PAIRED_DEVICE = VendorDO._(61443, _omitEnumNames ? '' : 'PAIRED_DEVICE'); + + static const $core.List values = [ + NO_CLUE, + PAGE_DEVICE_PAIRING, + DEVICE_COUNT, + PAIRING_STATUS, + PAIRED_DEVICE, + ]; + + static final $core.Map<$core.int, VendorDO> _byValue = $pb.ProtobufEnum.initByValue(values); + static VendorDO? valueOf($core.int value) => _byValue[value]; + + const VendorDO._($core.int v, $core.String n) : super(v, n); +} + + +const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/lib/bluetooth/devices/zwift/protocol/zp_vendor.pbjson.dart b/lib/bluetooth/devices/zwift/protocol/zp_vendor.pbjson.dart new file mode 100644 index 000000000..cb55d70ac --- /dev/null +++ b/lib/bluetooth/devices/zwift/protocol/zp_vendor.pbjson.dart @@ -0,0 +1,267 @@ +// +// Generated code. Do not modify. +// source: zp_vendor.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use vendorOpcodeDescriptor instead') +const VendorOpcode$json = { + '1': 'VendorOpcode', + '2': [ + {'1': 'UNDEFINED', '2': 0}, + {'1': 'CONTROLLER_SYNC', '2': 1}, + {'1': 'PAIR_DEVICES', '2': 2}, + {'1': 'ENABLE_TEST_MODE', '2': 65280}, + {'1': 'SET_DFU_TEST', '2': 65281}, + {'1': 'SET_TRAINER_TEST_DATA', '2': 65282}, + {'1': 'SET_INPUT_DEVICE_TEST_DATA', '2': 65283}, + {'1': 'SET_GEAR_TEST_DATA', '2': 65284}, + {'1': 'SET_HRM_TEST_DATA', '2': 65285}, + {'1': 'SET_TEST_DATA', '2': 65286}, + ], +}; + +/// Descriptor for `VendorOpcode`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List vendorOpcodeDescriptor = $convert.base64Decode( + 'CgxWZW5kb3JPcGNvZGUSDQoJVU5ERUZJTkVEEAASEwoPQ09OVFJPTExFUl9TWU5DEAESEAoMUE' + 'FJUl9ERVZJQ0VTEAISFgoQRU5BQkxFX1RFU1RfTU9ERRCA/gMSEgoMU0VUX0RGVV9URVNUEIH+' + 'AxIbChVTRVRfVFJBSU5FUl9URVNUX0RBVEEQgv4DEiAKGlNFVF9JTlBVVF9ERVZJQ0VfVEVTVF' + '9EQVRBEIP+AxIYChJTRVRfR0VBUl9URVNUX0RBVEEQhP4DEhcKEVNFVF9IUk1fVEVTVF9EQVRB' + 'EIX+AxITCg1TRVRfVEVTVF9EQVRBEIb+Aw=='); + +@$core.Deprecated('Use pairDeviceTypeDescriptor instead') +const PairDeviceType$json = { + '1': 'PairDeviceType', + '2': [ + {'1': 'BLE', '2': 0}, + {'1': 'ANT', '2': 1}, + ], +}; + +/// Descriptor for `PairDeviceType`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List pairDeviceTypeDescriptor = $convert.base64Decode( + 'Cg5QYWlyRGV2aWNlVHlwZRIHCgNCTEUQABIHCgNBTlQQAQ=='); + +@$core.Deprecated('Use controllerSyncStatusDescriptor instead') +const ControllerSyncStatus$json = { + '1': 'ControllerSyncStatus', + '2': [ + {'1': 'NOT_CONNECTED', '2': 0}, + {'1': 'CONNECTED', '2': 1}, + ], +}; + +/// Descriptor for `ControllerSyncStatus`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List controllerSyncStatusDescriptor = $convert.base64Decode( + 'ChRDb250cm9sbGVyU3luY1N0YXR1cxIRCg1OT1RfQ09OTkVDVEVEEAASDQoJQ09OTkVDVEVEEA' + 'E='); + +@$core.Deprecated('Use vendorDODescriptor instead') +const VendorDO$json = { + '1': 'VendorDO', + '2': [ + {'1': 'NO_CLUE', '2': 0}, + {'1': 'PAGE_DEVICE_PAIRING', '2': 61440}, + {'1': 'DEVICE_COUNT', '2': 61441}, + {'1': 'PAIRING_STATUS', '2': 61442}, + {'1': 'PAIRED_DEVICE', '2': 61443}, + ], +}; + +/// Descriptor for `VendorDO`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List vendorDODescriptor = $convert.base64Decode( + 'CghWZW5kb3JETxILCgdOT19DTFVFEAASGQoTUEFHRV9ERVZJQ0VfUEFJUklORxCA4AMSEgoMRE' + 'VWSUNFX0NPVU5UEIHgAxIUCg5QQUlSSU5HX1NUQVRVUxCC4AMSEwoNUEFJUkVEX0RFVklDRRCD' + '4AM='); + +@$core.Deprecated('Use controllerSyncDescriptor instead') +const ControllerSync$json = { + '1': 'ControllerSync', + '2': [ + {'1': 'status', '3': 1, '4': 1, '5': 14, '6': '.com.zwift.protobuf.ControllerSyncStatus', '10': 'status'}, + {'1': 'timeStamp', '3': 2, '4': 1, '5': 5, '10': 'timeStamp'}, + ], +}; + +/// Descriptor for `ControllerSync`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List controllerSyncDescriptor = $convert.base64Decode( + 'Cg5Db250cm9sbGVyU3luYxJACgZzdGF0dXMYASABKA4yKC5jb20uendpZnQucHJvdG9idWYuQ2' + '9udHJvbGxlclN5bmNTdGF0dXNSBnN0YXR1cxIcCgl0aW1lU3RhbXAYAiABKAVSCXRpbWVTdGFt' + 'cA=='); + +@$core.Deprecated('Use enableTestModeDescriptor instead') +const EnableTestMode$json = { + '1': 'EnableTestMode', + '2': [ + {'1': 'enable', '3': 1, '4': 1, '5': 8, '10': 'enable'}, + ], +}; + +/// Descriptor for `EnableTestMode`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List enableTestModeDescriptor = $convert.base64Decode( + 'Cg5FbmFibGVUZXN0TW9kZRIWCgZlbmFibGUYASABKAhSBmVuYWJsZQ=='); + +@$core.Deprecated('Use pairDevicesDescriptor instead') +const PairDevices$json = { + '1': 'PairDevices', + '2': [ + {'1': 'pair', '3': 1, '4': 1, '5': 8, '10': 'pair'}, + {'1': 'type', '3': 2, '4': 1, '5': 14, '6': '.com.zwift.protobuf.PairDeviceType', '10': 'type'}, + {'1': 'deviceId', '3': 3, '4': 1, '5': 12, '10': 'deviceId'}, + ], +}; + +/// Descriptor for `PairDevices`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List pairDevicesDescriptor = $convert.base64Decode( + 'CgtQYWlyRGV2aWNlcxISCgRwYWlyGAEgASgIUgRwYWlyEjYKBHR5cGUYAiABKA4yIi5jb20uen' + 'dpZnQucHJvdG9idWYuUGFpckRldmljZVR5cGVSBHR5cGUSGgoIZGV2aWNlSWQYAyABKAxSCGRl' + 'dmljZUlk'); + +@$core.Deprecated('Use devicePairingDataPageDescriptor instead') +const DevicePairingDataPage$json = { + '1': 'DevicePairingDataPage', + '2': [ + {'1': 'devicesCount', '3': 1, '4': 1, '5': 5, '10': 'devicesCount'}, + {'1': 'pairingStatus', '3': 2, '4': 1, '5': 5, '10': 'pairingStatus'}, + {'1': 'pairingDevList', '3': 3, '4': 3, '5': 11, '6': '.com.zwift.protobuf.DevicePairingDataPage.PairedDevice', '10': 'pairingDevList'}, + ], + '3': [DevicePairingDataPage_PairedDevice$json], +}; + +@$core.Deprecated('Use devicePairingDataPageDescriptor instead') +const DevicePairingDataPage_PairedDevice$json = { + '1': 'PairedDevice', + '2': [ + {'1': 'device', '3': 1, '4': 1, '5': 12, '10': 'device'}, + ], +}; + +/// Descriptor for `DevicePairingDataPage`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List devicePairingDataPageDescriptor = $convert.base64Decode( + 'ChVEZXZpY2VQYWlyaW5nRGF0YVBhZ2USIgoMZGV2aWNlc0NvdW50GAEgASgFUgxkZXZpY2VzQ2' + '91bnQSJAoNcGFpcmluZ1N0YXR1cxgCIAEoBVINcGFpcmluZ1N0YXR1cxJeCg5wYWlyaW5nRGV2' + 'TGlzdBgDIAMoCzI2LmNvbS56d2lmdC5wcm90b2J1Zi5EZXZpY2VQYWlyaW5nRGF0YVBhZ2UuUG' + 'FpcmVkRGV2aWNlUg5wYWlyaW5nRGV2TGlzdBomCgxQYWlyZWREZXZpY2USFgoGZGV2aWNlGAEg' + 'ASgMUgZkZXZpY2U='); + +@$core.Deprecated('Use setDfuTestDescriptor instead') +const SetDfuTest$json = { + '1': 'SetDfuTest', + '2': [ + {'1': 'failedEnterDfu', '3': 1, '4': 1, '5': 8, '9': 0, '10': 'failedEnterDfu'}, + {'1': 'failedStartAdvertising', '3': 2, '4': 1, '5': 8, '9': 0, '10': 'failedStartAdvertising'}, + {'1': 'crcFailure', '3': 3, '4': 1, '5': 5, '9': 0, '10': 'crcFailure'}, + ], + '8': [ + {'1': 'test_case'}, + ], +}; + +/// Descriptor for `SetDfuTest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List setDfuTestDescriptor = $convert.base64Decode( + 'CgpTZXREZnVUZXN0EigKDmZhaWxlZEVudGVyRGZ1GAEgASgISABSDmZhaWxlZEVudGVyRGZ1Ej' + 'gKFmZhaWxlZFN0YXJ0QWR2ZXJ0aXNpbmcYAiABKAhIAFIWZmFpbGVkU3RhcnRBZHZlcnRpc2lu' + 'ZxIgCgpjcmNGYWlsdXJlGAMgASgFSABSCmNyY0ZhaWx1cmVCCwoJdGVzdF9jYXNl'); + +@$core.Deprecated('Use setGearTestDataDescriptor instead') +const SetGearTestData$json = { + '1': 'SetGearTestData', + '2': [ + {'1': 'frontGearIdx', '3': 1, '4': 1, '5': 5, '10': 'frontGearIdx'}, + {'1': 'rearGearIdx', '3': 2, '4': 1, '5': 5, '10': 'rearGearIdx'}, + ], +}; + +/// Descriptor for `SetGearTestData`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List setGearTestDataDescriptor = $convert.base64Decode( + 'Cg9TZXRHZWFyVGVzdERhdGESIgoMZnJvbnRHZWFySWR4GAEgASgFUgxmcm9udEdlYXJJZHgSIA' + 'oLcmVhckdlYXJJZHgYAiABKAVSC3JlYXJHZWFySWR4'); + +@$core.Deprecated('Use setHrmTestDataDescriptor instead') +const SetHrmTestData$json = { + '1': 'SetHrmTestData', + '2': [ + {'1': 'hrmPresent', '3': 1, '4': 1, '5': 8, '10': 'hrmPresent'}, + {'1': 'heartRate', '3': 2, '4': 1, '5': 5, '10': 'heartRate'}, + ], +}; + +/// Descriptor for `SetHrmTestData`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List setHrmTestDataDescriptor = $convert.base64Decode( + 'Cg5TZXRIcm1UZXN0RGF0YRIeCgpocm1QcmVzZW50GAEgASgIUgpocm1QcmVzZW50EhwKCWhlYX' + 'J0UmF0ZRgCIAEoBVIJaGVhcnRSYXRl'); + +@$core.Deprecated('Use setInputDeviceTestDataDescriptor instead') +const SetInputDeviceTestData$json = { + '1': 'SetInputDeviceTestData', + '2': [ + {'1': 'duration', '3': 1, '4': 1, '5': 5, '10': 'duration'}, + {'1': 'buttonEvent', '3': 2, '4': 1, '5': 5, '10': 'buttonEvent'}, + {'1': 'analogEventList', '3': 3, '4': 3, '5': 11, '6': '.com.zwift.protobuf.SetInputDeviceTestData.ControllerAnalogEvent', '10': 'analogEventList'}, + ], + '3': [SetInputDeviceTestData_ControllerAnalogEvent$json], +}; + +@$core.Deprecated('Use setInputDeviceTestDataDescriptor instead') +const SetInputDeviceTestData_ControllerAnalogEvent$json = { + '1': 'ControllerAnalogEvent', + '2': [ + {'1': 'sensorId', '3': 1, '4': 1, '5': 5, '10': 'sensorId'}, + {'1': 'value', '3': 2, '4': 1, '5': 5, '10': 'value'}, + ], +}; + +/// Descriptor for `SetInputDeviceTestData`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List setInputDeviceTestDataDescriptor = $convert.base64Decode( + 'ChZTZXRJbnB1dERldmljZVRlc3REYXRhEhoKCGR1cmF0aW9uGAEgASgFUghkdXJhdGlvbhIgCg' + 'tidXR0b25FdmVudBgCIAEoBVILYnV0dG9uRXZlbnQSagoPYW5hbG9nRXZlbnRMaXN0GAMgAygL' + 'MkAuY29tLnp3aWZ0LnByb3RvYnVmLlNldElucHV0RGV2aWNlVGVzdERhdGEuQ29udHJvbGxlck' + 'FuYWxvZ0V2ZW50Ug9hbmFsb2dFdmVudExpc3QaSQoVQ29udHJvbGxlckFuYWxvZ0V2ZW50EhoK' + 'CHNlbnNvcklkGAEgASgFUghzZW5zb3JJZBIUCgV2YWx1ZRgCIAEoBVIFdmFsdWU='); + +@$core.Deprecated('Use setTrainerTestDataDescriptor instead') +const SetTrainerTestData$json = { + '1': 'SetTrainerTestData', + '2': [ + {'1': 'dataMode', '3': 1, '4': 1, '5': 5, '10': 'dataMode'}, + {'1': 'interfaces', '3': 2, '4': 1, '5': 5, '10': 'interfaces'}, + {'1': 'testTrainerData', '3': 3, '4': 1, '5': 11, '6': '.com.zwift.protobuf.TestTrainerData', '10': 'testTrainerData'}, + ], +}; + +/// Descriptor for `SetTrainerTestData`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List setTrainerTestDataDescriptor = $convert.base64Decode( + 'ChJTZXRUcmFpbmVyVGVzdERhdGESGgoIZGF0YU1vZGUYASABKAVSCGRhdGFNb2RlEh4KCmludG' + 'VyZmFjZXMYAiABKAVSCmludGVyZmFjZXMSTQoPdGVzdFRyYWluZXJEYXRhGAMgASgLMiMuY29t' + 'Lnp3aWZ0LnByb3RvYnVmLlRlc3RUcmFpbmVyRGF0YVIPdGVzdFRyYWluZXJEYXRh'); + +@$core.Deprecated('Use testTrainerDataDescriptor instead') +const TestTrainerData$json = { + '1': 'TestTrainerData', + '2': [ + {'1': 'power', '3': 1, '4': 1, '5': 5, '10': 'power'}, + {'1': 'cadence', '3': 2, '4': 1, '5': 5, '10': 'cadence'}, + {'1': 'bikeSpeed', '3': 3, '4': 1, '5': 5, '10': 'bikeSpeed'}, + {'1': 'averagedPower', '3': 4, '4': 1, '5': 5, '10': 'averagedPower'}, + {'1': 'wheelSpeed', '3': 5, '4': 1, '5': 5, '10': 'wheelSpeed'}, + {'1': 'calculatedRealGearRatio', '3': 6, '4': 1, '5': 5, '10': 'calculatedRealGearRatio'}, + ], +}; + +/// Descriptor for `TestTrainerData`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List testTrainerDataDescriptor = $convert.base64Decode( + 'Cg9UZXN0VHJhaW5lckRhdGESFAoFcG93ZXIYASABKAVSBXBvd2VyEhgKB2NhZGVuY2UYAiABKA' + 'VSB2NhZGVuY2USHAoJYmlrZVNwZWVkGAMgASgFUgliaWtlU3BlZWQSJAoNYXZlcmFnZWRQb3dl' + 'chgEIAEoBVINYXZlcmFnZWRQb3dlchIeCgp3aGVlbFNwZWVkGAUgASgFUgp3aGVlbFNwZWVkEj' + 'gKF2NhbGN1bGF0ZWRSZWFsR2VhclJhdGlvGAYgASgFUhdjYWxjdWxhdGVkUmVhbEdlYXJSYXRp' + 'bw=='); + diff --git a/lib/bluetooth/devices/zwift/protocol/zp_vendor.pbserver.dart b/lib/bluetooth/devices/zwift/protocol/zp_vendor.pbserver.dart new file mode 100644 index 000000000..f57b0a489 --- /dev/null +++ b/lib/bluetooth/devices/zwift/protocol/zp_vendor.pbserver.dart @@ -0,0 +1,14 @@ +// +// Generated code. Do not modify. +// source: zp_vendor.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names +// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +export 'zp_vendor.pb.dart'; + diff --git a/lib/protocol/zwift.pb.dart b/lib/bluetooth/devices/zwift/protocol/zwift.pb.dart similarity index 92% rename from lib/protocol/zwift.pb.dart rename to lib/bluetooth/devices/zwift/protocol/zwift.pb.dart index db9986b08..cb04bea6c 100644 --- a/lib/protocol/zwift.pb.dart +++ b/lib/bluetooth/devices/zwift/protocol/zwift.pb.dart @@ -25,8 +25,8 @@ class PlayKeyPadStatus extends $pb.GeneratedMessage { PlayButtonStatus? buttonZLeft, PlayButtonStatus? buttonARight, PlayButtonStatus? buttonBDown, - PlayButtonStatus? buttonOn, PlayButtonStatus? buttonShift, + PlayButtonStatus? buttonOn, $core.int? analogLR, $core.int? analogUD, }) { @@ -46,12 +46,12 @@ class PlayKeyPadStatus extends $pb.GeneratedMessage { if (buttonBDown != null) { $result.buttonBDown = buttonBDown; } - if (buttonOn != null) { - $result.buttonOn = buttonOn; - } if (buttonShift != null) { $result.buttonShift = buttonShift; } + if (buttonOn != null) { + $result.buttonOn = buttonOn; + } if (analogLR != null) { $result.analogLR = analogLR; } @@ -70,8 +70,8 @@ class PlayKeyPadStatus extends $pb.GeneratedMessage { ..e(3, _omitFieldNames ? '' : 'ButtonZLeft', $pb.PbFieldType.OE, protoName: 'Button_Z_Left', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values) ..e(4, _omitFieldNames ? '' : 'ButtonARight', $pb.PbFieldType.OE, protoName: 'Button_A_Right', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values) ..e(5, _omitFieldNames ? '' : 'ButtonBDown', $pb.PbFieldType.OE, protoName: 'Button_B_Down', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values) - ..e(6, _omitFieldNames ? '' : 'ButtonOn', $pb.PbFieldType.OE, protoName: 'Button_On', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values) - ..e(7, _omitFieldNames ? '' : 'ButtonShift', $pb.PbFieldType.OE, protoName: 'Button_Shift', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values) + ..e(6, _omitFieldNames ? '' : 'ButtonShift', $pb.PbFieldType.OE, protoName: 'Button_Shift', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values) + ..e(7, _omitFieldNames ? '' : 'ButtonOn', $pb.PbFieldType.OE, protoName: 'Button_On', defaultOrMaker: PlayButtonStatus.ON, valueOf: PlayButtonStatus.valueOf, enumValues: PlayButtonStatus.values) ..a<$core.int>(8, _omitFieldNames ? '' : 'AnalogLR', $pb.PbFieldType.OS3, protoName: 'Analog_LR') ..a<$core.int>(9, _omitFieldNames ? '' : 'AnalogUD', $pb.PbFieldType.OS3, protoName: 'Analog_UD') ..hasRequiredFields = false @@ -144,22 +144,22 @@ class PlayKeyPadStatus extends $pb.GeneratedMessage { void clearButtonBDown() => clearField(5); @$pb.TagNumber(6) - PlayButtonStatus get buttonOn => $_getN(5); + PlayButtonStatus get buttonShift => $_getN(5); @$pb.TagNumber(6) - set buttonOn(PlayButtonStatus v) { setField(6, v); } + set buttonShift(PlayButtonStatus v) { setField(6, v); } @$pb.TagNumber(6) - $core.bool hasButtonOn() => $_has(5); + $core.bool hasButtonShift() => $_has(5); @$pb.TagNumber(6) - void clearButtonOn() => clearField(6); + void clearButtonShift() => clearField(6); @$pb.TagNumber(7) - PlayButtonStatus get buttonShift => $_getN(6); + PlayButtonStatus get buttonOn => $_getN(6); @$pb.TagNumber(7) - set buttonShift(PlayButtonStatus v) { setField(7, v); } + set buttonOn(PlayButtonStatus v) { setField(7, v); } @$pb.TagNumber(7) - $core.bool hasButtonShift() => $_has(6); + $core.bool hasButtonOn() => $_has(6); @$pb.TagNumber(7) - void clearButtonShift() => clearField(7); + void clearButtonOn() => clearField(7); @$pb.TagNumber(8) $core.int get analogLR => $_getIZ(7); @@ -480,62 +480,19 @@ class RideAnalogKeyPress extends $pb.GeneratedMessage { void clearAnalogValue() => clearField(2); } -class RideAnalogKeyGroup extends $pb.GeneratedMessage { - factory RideAnalogKeyGroup({ - $core.Iterable? groupStatus, - }) { - final $result = create(); - if (groupStatus != null) { - $result.groupStatus.addAll(groupStatus); - } - return $result; - } - RideAnalogKeyGroup._() : super(); - factory RideAnalogKeyGroup.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory RideAnalogKeyGroup.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'RideAnalogKeyGroup', package: const $pb.PackageName(_omitMessageNames ? '' : 'de.jonasbark'), createEmptyInstance: create) - ..pc(1, _omitFieldNames ? '' : 'GroupStatus', $pb.PbFieldType.PM, protoName: 'GroupStatus', subBuilder: RideAnalogKeyPress.create) - ..hasRequiredFields = false - ; - - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' - 'Will be removed in next major version') - RideAnalogKeyGroup clone() => RideAnalogKeyGroup()..mergeFromMessage(this); - @$core.Deprecated( - 'Using this can add significant overhead to your binary. ' - 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' - 'Will be removed in next major version') - RideAnalogKeyGroup copyWith(void Function(RideAnalogKeyGroup) updates) => super.copyWith((message) => updates(message as RideAnalogKeyGroup)) as RideAnalogKeyGroup; - - $pb.BuilderInfo get info_ => _i; - - @$core.pragma('dart2js:noInline') - static RideAnalogKeyGroup create() => RideAnalogKeyGroup._(); - RideAnalogKeyGroup createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); - @$core.pragma('dart2js:noInline') - static RideAnalogKeyGroup getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static RideAnalogKeyGroup? _defaultInstance; - - @$pb.TagNumber(1) - $core.List get groupStatus => $_getList(0); -} - /// The command code prepending this message is 0x23 +/// All analog paddles (L0-L3) appear as repeated RideAnalogKeyPress in field 3 class RideKeyPadStatus extends $pb.GeneratedMessage { factory RideKeyPadStatus({ $core.int? buttonMap, - RideAnalogKeyGroup? analogButtons, + $core.Iterable? analogPaddles, }) { final $result = create(); if (buttonMap != null) { $result.buttonMap = buttonMap; } - if (analogButtons != null) { - $result.analogButtons = analogButtons; + if (analogPaddles != null) { + $result.analogPaddles.addAll(analogPaddles); } return $result; } @@ -545,7 +502,7 @@ class RideKeyPadStatus extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'RideKeyPadStatus', package: const $pb.PackageName(_omitMessageNames ? '' : 'de.jonasbark'), createEmptyInstance: create) ..a<$core.int>(1, _omitFieldNames ? '' : 'ButtonMap', $pb.PbFieldType.OU3, protoName: 'ButtonMap') - ..aOM(2, _omitFieldNames ? '' : 'AnalogButtons', protoName: 'AnalogButtons', subBuilder: RideAnalogKeyGroup.create) + ..pc(3, _omitFieldNames ? '' : 'AnalogPaddles', $pb.PbFieldType.PM, protoName: 'AnalogPaddles', subBuilder: RideAnalogKeyPress.create) ..hasRequiredFields = false ; @@ -579,16 +536,8 @@ class RideKeyPadStatus extends $pb.GeneratedMessage { @$pb.TagNumber(1) void clearButtonMap() => clearField(1); - @$pb.TagNumber(2) - RideAnalogKeyGroup get analogButtons => $_getN(1); - @$pb.TagNumber(2) - set analogButtons(RideAnalogKeyGroup v) { setField(2, v); } - @$pb.TagNumber(2) - $core.bool hasAnalogButtons() => $_has(1); - @$pb.TagNumber(2) - void clearAnalogButtons() => clearField(2); - @$pb.TagNumber(2) - RideAnalogKeyGroup ensureAnalogButtons() => $_ensure(1); + @$pb.TagNumber(3) + $core.List get analogPaddles => $_getList(1); } /// ------------------ Zwift Click messages diff --git a/lib/protocol/zwift.pbenum.dart b/lib/bluetooth/devices/zwift/protocol/zwift.pbenum.dart similarity index 100% rename from lib/protocol/zwift.pbenum.dart rename to lib/bluetooth/devices/zwift/protocol/zwift.pbenum.dart diff --git a/lib/protocol/zwift.pbjson.dart b/lib/bluetooth/devices/zwift/protocol/zwift.pbjson.dart similarity index 91% rename from lib/protocol/zwift.pbjson.dart rename to lib/bluetooth/devices/zwift/protocol/zwift.pbjson.dart index e67347665..9951bb123 100644 --- a/lib/protocol/zwift.pbjson.dart +++ b/lib/bluetooth/devices/zwift/protocol/zwift.pbjson.dart @@ -82,8 +82,8 @@ const PlayKeyPadStatus$json = { {'1': 'Button_Z_Left', '3': 3, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonZLeft'}, {'1': 'Button_A_Right', '3': 4, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonARight'}, {'1': 'Button_B_Down', '3': 5, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonBDown'}, - {'1': 'Button_On', '3': 6, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonOn'}, - {'1': 'Button_Shift', '3': 7, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonShift'}, + {'1': 'Button_Shift', '3': 6, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonShift'}, + {'1': 'Button_On', '3': 7, '4': 1, '5': 14, '6': '.de.jonasbark.PlayButtonStatus', '10': 'ButtonOn'}, {'1': 'Analog_LR', '3': 8, '4': 1, '5': 17, '10': 'AnalogLR'}, {'1': 'Analog_UD', '3': 9, '4': 1, '5': 17, '10': 'AnalogUD'}, ], @@ -97,9 +97,9 @@ final $typed_data.Uint8List playKeyPadStatusDescriptor = $convert.base64Decode( '4uZGUuam9uYXNiYXJrLlBsYXlCdXR0b25TdGF0dXNSC0J1dHRvblpMZWZ0EkQKDkJ1dHRvbl9B' 'X1JpZ2h0GAQgASgOMh4uZGUuam9uYXNiYXJrLlBsYXlCdXR0b25TdGF0dXNSDEJ1dHRvbkFSaW' 'dodBJCCg1CdXR0b25fQl9Eb3duGAUgASgOMh4uZGUuam9uYXNiYXJrLlBsYXlCdXR0b25TdGF0' - 'dXNSC0J1dHRvbkJEb3duEjsKCUJ1dHRvbl9PbhgGIAEoDjIeLmRlLmpvbmFzYmFyay5QbGF5Qn' - 'V0dG9uU3RhdHVzUghCdXR0b25PbhJBCgxCdXR0b25fU2hpZnQYByABKA4yHi5kZS5qb25hc2Jh' - 'cmsuUGxheUJ1dHRvblN0YXR1c1ILQnV0dG9uU2hpZnQSGwoJQW5hbG9nX0xSGAggASgRUghBbm' + 'dXNSC0J1dHRvbkJEb3duEkEKDEJ1dHRvbl9TaGlmdBgGIAEoDjIeLmRlLmpvbmFzYmFyay5QbG' + 'F5QnV0dG9uU3RhdHVzUgtCdXR0b25TaGlmdBI7CglCdXR0b25fT24YByABKA4yHi5kZS5qb25h' + 'c2JhcmsuUGxheUJ1dHRvblN0YXR1c1IIQnV0dG9uT24SGwoJQW5hbG9nX0xSGAggASgRUghBbm' 'Fsb2dMUhIbCglBbmFsb2dfVUQYCSABKBFSCEFuYWxvZ1VE'); @$core.Deprecated('Use playCommandParametersDescriptor instead') @@ -170,33 +170,20 @@ final $typed_data.Uint8List rideAnalogKeyPressDescriptor = $convert.base64Decode 'lkZUFuYWxvZ0xvY2F0aW9uUghMb2NhdGlvbhIgCgtBbmFsb2dWYWx1ZRgCIAEoEVILQW5hbG9n' 'VmFsdWU='); -@$core.Deprecated('Use rideAnalogKeyGroupDescriptor instead') -const RideAnalogKeyGroup$json = { - '1': 'RideAnalogKeyGroup', - '2': [ - {'1': 'GroupStatus', '3': 1, '4': 3, '5': 11, '6': '.de.jonasbark.RideAnalogKeyPress', '10': 'GroupStatus'}, - ], -}; - -/// Descriptor for `RideAnalogKeyGroup`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List rideAnalogKeyGroupDescriptor = $convert.base64Decode( - 'ChJSaWRlQW5hbG9nS2V5R3JvdXASQgoLR3JvdXBTdGF0dXMYASADKAsyIC5kZS5qb25hc2Jhcm' - 'suUmlkZUFuYWxvZ0tleVByZXNzUgtHcm91cFN0YXR1cw=='); - @$core.Deprecated('Use rideKeyPadStatusDescriptor instead') const RideKeyPadStatus$json = { '1': 'RideKeyPadStatus', '2': [ {'1': 'ButtonMap', '3': 1, '4': 1, '5': 13, '10': 'ButtonMap'}, - {'1': 'AnalogButtons', '3': 2, '4': 1, '5': 11, '6': '.de.jonasbark.RideAnalogKeyGroup', '10': 'AnalogButtons'}, + {'1': 'AnalogPaddles', '3': 3, '4': 3, '5': 11, '6': '.de.jonasbark.RideAnalogKeyPress', '10': 'AnalogPaddles'}, ], }; /// Descriptor for `RideKeyPadStatus`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List rideKeyPadStatusDescriptor = $convert.base64Decode( 'ChBSaWRlS2V5UGFkU3RhdHVzEhwKCUJ1dHRvbk1hcBgBIAEoDVIJQnV0dG9uTWFwEkYKDUFuYW' - 'xvZ0J1dHRvbnMYAiABKAsyIC5kZS5qb25hc2JhcmsuUmlkZUFuYWxvZ0tleUdyb3VwUg1BbmFs' - 'b2dCdXR0b25z'); + 'xvZ1BhZGRsZXMYAyADKAsyIC5kZS5qb25hc2JhcmsuUmlkZUFuYWxvZ0tleVByZXNzUg1BbmFs' + 'b2dQYWRkbGVz'); @$core.Deprecated('Use clickKeyPadStatusDescriptor instead') const ClickKeyPadStatus$json = { diff --git a/lib/protocol/zwift.pbserver.dart b/lib/bluetooth/devices/zwift/protocol/zwift.pbserver.dart similarity index 100% rename from lib/protocol/zwift.pbserver.dart rename to lib/bluetooth/devices/zwift/protocol/zwift.pbserver.dart diff --git a/lib/bluetooth/devices/zwift/zwift_click.dart b/lib/bluetooth/devices/zwift/zwift_click.dart new file mode 100644 index 000000000..64f4e9a3d --- /dev/null +++ b/lib/bluetooth/devices/zwift/zwift_click.dart @@ -0,0 +1,23 @@ +import 'package:flutter/foundation.dart'; +import 'package:swift_control/bluetooth/devices/zwift/protocol/zwift.pb.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_device.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; + +import 'constants.dart'; + +class ZwiftClick extends ZwiftDevice { + ZwiftClick(super.scanResult) : super(availableButtons: [ZwiftButtons.shiftUpRight, ZwiftButtons.shiftUpLeft]); + + @override + List processClickNotification(Uint8List message) { + final status = ClickKeyPadStatus.fromBuffer(message); + final buttonsClicked = [ + if (status.buttonPlus == PlayButtonStatus.ON) ZwiftButtons.shiftUpRight, + if (status.buttonMinus == PlayButtonStatus.ON) ZwiftButtons.shiftUpLeft, + ]; + return buttonsClicked; + } + + @override + String get latestFirmwareVersion => '1.1.0'; +} diff --git a/lib/bluetooth/devices/zwift/zwift_clickv2.dart b/lib/bluetooth/devices/zwift/zwift_clickv2.dart new file mode 100644 index 000000000..3cf6fed91 --- /dev/null +++ b/lib/bluetooth/devices/zwift/zwift_clickv2.dart @@ -0,0 +1,168 @@ +import 'package:dartx/dartx.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:swift_control/bluetooth/devices/zwift/constants.dart'; +import 'package:swift_control/bluetooth/devices/zwift/protocol/zp.pbenum.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_ride.dart'; +import 'package:swift_control/bluetooth/messages/notification.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/pages/markdown.dart'; +import 'package:swift_control/widgets/warning.dart'; + +class ZwiftClickV2 extends ZwiftRide { + ZwiftClickV2(super.scanResult) + : super( + isBeta: true, + availableButtons: [ + ZwiftButtons.navigationLeft, + ZwiftButtons.navigationRight, + ZwiftButtons.navigationUp, + ZwiftButtons.navigationDown, + ZwiftButtons.a, + ZwiftButtons.b, + ZwiftButtons.y, + ZwiftButtons.z, + ZwiftButtons.shiftUpLeft, + ZwiftButtons.shiftUpRight, + ], + ); + + bool _noLongerSendsEvents = false; + + @override + List get startCommand => ZwiftConstants.RIDE_ON + ZwiftConstants.RESPONSE_START_CLICK_V2; + + @override + String get latestFirmwareVersion => '1.1.0'; + + @override + bool get canVibrate => false; + + @override + Future setupHandshake() async { + super.setupHandshake(); + await sendCommandBuffer(Uint8List.fromList([0xFF, 0x04, 0x00])); + } + + @override + Future processData(Uint8List bytes) { + if (bytes.startsWith(ZwiftConstants.RESPONSE_STOPPED_CLICK_V2_VARIANT_1) || + bytes.startsWith(ZwiftConstants.RESPONSE_STOPPED_CLICK_V2_VARIANT_2)) { + _noLongerSendsEvents = true; + actionStreamInternal.add( + LogNotification( + 'Your Zwift Click V2 no longer sends events. Connect it in the Zwift app once each session.', + ), + ); + } + return super.processData(bytes); + } + + @override + Widget showInformation(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + super.showInformation(context), + + if (isConnected && _noLongerSendsEvents && settings.getShowZwiftClickV2ReconnectWarning()) + Warning( + children: [ + Text( + '''To make your Zwift Click V2 work best you should connect it in the Zwift app once each day.\nIf you don't do that BikeControl will need to reconnect every minute. + +1. Open Zwift app +2. Log in (subscription not required) and open the device connection screen +3. Connect your Trainer, then connect the Zwift Click V2 +4. Close the Zwift app again and connect again in BikeControl''', + ), + Row( + children: [ + TextButton( + onPressed: () { + sendCommand(Opcode.RESET, null); + }, + child: Text('Reset now'), + ), + TextButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => MarkdownPage(assetPath: 'TROUBLESHOOTING.md'), + ), + ); + }, + child: Text('Troubleshooting'), + ), + if (kDebugMode && false) + TextButton( + onPressed: () { + test(); + }, + child: Text('Test'), + ), + Expanded(child: SizedBox()), + TextButton( + onPressed: () { + settings.setShowZwiftClickV2ReconnectWarning(false); + }, + child: Text('Dismiss'), + ), + ], + ), + ], + ), + ], + ); + } + + Future test() async { + await sendCommand(Opcode.RESET, null); + //await sendCommand(Opcode.GET, Get(dataObjectId: VendorDO.PAGE_DEVICE_PAIRING.value)); // 0008 82E0 03 + + /*await sendCommand(Opcode.GET, Get(dataObjectId: DO.PAGE_DEV_INFO.value)); // 0008 00 + await sendCommand(Opcode.LOG_LEVEL_SET, LogLevelSet(logLevel: LogLevel.LOGLEVEL_TRACE)); // 4108 05 + + await sendCommand(Opcode.GET, Get(dataObjectId: DO.PAGE_CLIENT_SERVER_CONFIGURATION.value)); // 0008 10 + await sendCommand(Opcode.GET, Get(dataObjectId: DO.PAGE_CLIENT_SERVER_CONFIGURATION.value)); // 0008 10 + await sendCommand(Opcode.GET, Get(dataObjectId: DO.PAGE_CLIENT_SERVER_CONFIGURATION.value)); // 0008 10 + + await sendCommand(Opcode.GET, Get(dataObjectId: DO.PAGE_CONTROLLER_INPUT_CONFIG.value)); // 0008 80 08 + + await sendCommand(Opcode.GET, Get(dataObjectId: DO.BATTERY_STATE.value)); // 0008 83 06 + + // Value: FF04 000A 1540 E9D9 C96B 7463 C27F 1B4E 4D9F 1CB1 205D 882E D7CE + // Value: FF04 000A 15B2 6324 0A31 D6C6 B81F C129 D6A4 E99D FFFC B9FC 418D + await sendCommandBuffer( + Uint8List.fromList([ + 0xFF, + 0x04, + 0x00, + 0x0A, + 0x15, + 0xC2, + 0x63, + 0x24, + 0x0A, + 0x31, + 0xD6, + 0xC6, + 0xB8, + 0x1F, + 0xC1, + 0x29, + 0xD6, + 0xA4, + 0xE9, + 0x9D, + 0xFF, + 0xFC, + 0xB9, + 0xFC, + 0x41, + 0x8D, + ]), + );*/ + } +} diff --git a/lib/bluetooth/devices/zwift/zwift_device.dart b/lib/bluetooth/devices/zwift/zwift_device.dart new file mode 100644 index 000000000..be6aabfce --- /dev/null +++ b/lib/bluetooth/devices/zwift/zwift_device.dart @@ -0,0 +1,179 @@ +import 'dart:async'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/foundation.dart'; +import 'package:swift_control/bluetooth/devices/bluetooth_device.dart'; +import 'package:swift_control/bluetooth/devices/zwift/constants.dart'; +import 'package:swift_control/bluetooth/messages/notification.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/utils/single_line_exception.dart'; +import 'package:universal_ble/universal_ble.dart'; + +abstract class ZwiftDevice extends BluetoothDevice { + ZwiftDevice(super.scanResult, {required super.availableButtons, super.isBeta}); + + BleCharacteristic? syncRxCharacteristic; + + List? _lastButtonsClicked; + + String get latestFirmwareVersion; + List get startCommand => ZwiftConstants.RIDE_ON + ZwiftConstants.RESPONSE_START_CLICK; + String get customServiceId => ZwiftConstants.ZWIFT_CUSTOM_SERVICE_UUID; + bool get canVibrate => false; + + @override + Future handleServices(List services) async { + final customService = services.firstOrNullWhere((service) => service.uuid == customServiceId.toLowerCase()); + + if (customService == null) { + throw Exception( + 'Custom service $customServiceId not found for device $this ${device.name ?? device.rawName}.\nYou may need to update the firmware in Zwift Companion app.\nWe found: ${services.joinToString(transform: (s) => s.uuid)}', + ); + } + + final asyncCharacteristic = customService.characteristics.firstOrNullWhere( + (characteristic) => characteristic.uuid == ZwiftConstants.ZWIFT_ASYNC_CHARACTERISTIC_UUID.toLowerCase(), + ); + final syncTxCharacteristic = customService.characteristics.firstOrNullWhere( + (characteristic) => characteristic.uuid == ZwiftConstants.ZWIFT_SYNC_TX_CHARACTERISTIC_UUID.toLowerCase(), + ); + syncRxCharacteristic = customService.characteristics.firstOrNullWhere( + (characteristic) => characteristic.uuid == ZwiftConstants.ZWIFT_SYNC_RX_CHARACTERISTIC_UUID.toLowerCase(), + ); + + if (asyncCharacteristic == null || syncTxCharacteristic == null || syncRxCharacteristic == null) { + throw Exception('Characteristics not found'); + } + + await UniversalBle.subscribeNotifications(device.deviceId, customService.uuid, asyncCharacteristic.uuid); + await UniversalBle.subscribeIndications(device.deviceId, customService.uuid, syncTxCharacteristic.uuid); + + await setupHandshake(); + + if (firmwareVersion != latestFirmwareVersion) { + actionStreamInternal.add( + LogNotification( + 'A new firmware version is available for ${device.name ?? device.rawName}: $latestFirmwareVersion (current: $firmwareVersion). Please update it in Zwift Companion app.', + ), + ); + } + } + + Future setupHandshake() async { + await UniversalBle.write( + device.deviceId, + customServiceId, + syncRxCharacteristic!.uuid, + ZwiftConstants.RIDE_ON, + withoutResponse: true, + ); + } + + @override + Future processCharacteristic(String characteristic, Uint8List bytes) async { + if (kDebugMode) { + actionStreamInternal.add( + LogNotification( + "${DateTime.now().toString().split(" ").last} Received data on $characteristic: ${bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}", + ), + ); + } + if (bytes.isEmpty) { + return; + } + + try { + if (bytes.startsWith(startCommand)) { + processDevicePublicKeyResponse(bytes); + } else { + processData(bytes); + } + } catch (e, stackTrace) { + print("Error processing data: $e"); + print("Stack Trace: $stackTrace"); + if (e is SingleLineException) { + actionStreamInternal.add(LogNotification(e.message)); + } else { + actionStreamInternal.add(LogNotification("$e\n$stackTrace")); + } + } + } + + void processDevicePublicKeyResponse(Uint8List bytes) { + final devicePublicKeyBytes = bytes.sublist( + ZwiftConstants.RIDE_ON.length + ZwiftConstants.RESPONSE_START_CLICK.length, + ); + if (kDebugMode) { + print("Device Public Key - ${devicePublicKeyBytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}"); + } + } + + Future processData(Uint8List bytes) async { + int type = bytes[0]; + Uint8List message = bytes.sublist(1); + + switch (type) { + case ZwiftConstants.EMPTY_MESSAGE_TYPE: + //print("Empty Message"); // expected when nothing happening + break; + case ZwiftConstants.BATTERY_LEVEL_TYPE: + if (batteryLevel != message[1]) { + batteryLevel = message[1]; + connection.signalChange(this); + } + break; + case ZwiftConstants.CLICK_NOTIFICATION_MESSAGE_TYPE: + case ZwiftConstants.PLAY_NOTIFICATION_MESSAGE_TYPE: + case ZwiftConstants.RIDE_NOTIFICATION_MESSAGE_TYPE: + try { + final buttonsClicked = processClickNotification(message); + handleButtonsClicked(buttonsClicked); + } catch (e) { + actionStreamInternal.add(LogNotification(e.toString())); + } + break; + } + } + + @override + Future handleButtonsClicked(List? buttonsClicked) async { + // the same messages are sent multiple times, so ignore + if (_lastButtonsClicked == null || _lastButtonsClicked?.contentEquals(buttonsClicked ?? []) == false) { + super.handleButtonsClicked(buttonsClicked); + } + _lastButtonsClicked = buttonsClicked; + } + + List processClickNotification(Uint8List message); + + @override + Future performDown(List buttonsClicked) async { + if (buttonsClicked.any(((e) => e.action == InGameAction.shiftDown || e.action == InGameAction.shiftUp)) && + settings.getVibrationEnabled()) { + await _vibrate(); + } + return super.performDown(buttonsClicked); + } + + @override + Future performClick(List buttonsClicked) async { + if (buttonsClicked.any(((e) => e.action == InGameAction.shiftDown || e.action == InGameAction.shiftUp)) && + settings.getVibrationEnabled() && + canVibrate) { + await _vibrate(); + } + return super.performClick(buttonsClicked); + } + + Future _vibrate() async { + final vibrateCommand = Uint8List.fromList([...ZwiftConstants.VIBRATE_PATTERN, 0x20]); + await UniversalBle.write( + device.deviceId, + customServiceId, + syncRxCharacteristic!.uuid, + vibrateCommand, + withoutResponse: true, + ); + } +} diff --git a/lib/bluetooth/devices/zwift/zwift_emulator.dart b/lib/bluetooth/devices/zwift/zwift_emulator.dart new file mode 100644 index 000000000..b1c9bfae8 --- /dev/null +++ b/lib/bluetooth/devices/zwift/zwift_emulator.dart @@ -0,0 +1,368 @@ +import 'dart:io'; + +import 'package:bluetooth_low_energy/bluetooth_low_energy.dart'; +import 'package:dartx/dartx.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart' hide ConnectionState; +import 'package:permission_handler/permission_handler.dart'; +import 'package:swift_control/bluetooth/ble.dart'; +import 'package:swift_control/bluetooth/devices/zwift/constants.dart'; +import 'package:swift_control/bluetooth/devices/zwift/protocol/zp.pbenum.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_ride.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/actions/base_actions.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/utils/requirements/multi.dart'; +import 'package:swift_control/widgets/title.dart'; + +import 'protocol/zwift.pb.dart' show RideKeyPadStatus; + +final zwiftEmulator = ZwiftEmulator(); + +class ZwiftEmulator { + static final List supportedActions = [ + InGameAction.shiftUp, + InGameAction.shiftDown, + InGameAction.uturn, + InGameAction.steerLeft, + InGameAction.steerRight, + InGameAction.openActionBar, + InGameAction.usePowerUp, + InGameAction.select, + InGameAction.back, + InGameAction.rideOnBomb, + ]; + + ValueNotifier isConnected = ValueNotifier(false); + bool get isAdvertising => _isAdvertising; + bool get isLoading => _isLoading; + + late final _peripheralManager = PeripheralManager(); + bool _isAdvertising = false; + bool _isLoading = false; + bool _isServiceAdded = false; + bool _isSubscribedToEvents = false; + Central? _central; + GATTCharacteristic? _asyncCharacteristic; + + Future reconnect() async { + await _peripheralManager.stopAdvertising(); + await _peripheralManager.removeAllServices(); + _isServiceAdded = false; + _isAdvertising = false; + startAdvertising(() {}); + } + + Future startAdvertising(VoidCallback onUpdate) async { + _isLoading = true; + onUpdate(); + + _peripheralManager.stateChanged.forEach((state) { + print('Peripheral manager state: ${state.state}'); + }); + + if (!kIsWeb && Platform.isAndroid) { + if (Platform.isAndroid) { + _peripheralManager.connectionStateChanged.forEach((state) { + print('Peripheral connection state: ${state.state} of ${state.central.uuid}'); + if (state.state == ConnectionState.connected) { + } else if (state.state == ConnectionState.disconnected) { + _central = null; + isConnected.value = false; + onUpdate(); + } + }); + } + + final status = await Permission.bluetoothAdvertise.request(); + if (!status.isGranted) { + print('Bluetooth advertise permission not granted'); + _isAdvertising = false; + onUpdate(); + return; + } + } + + while (_peripheralManager.state != BluetoothLowEnergyState.poweredOn) { + print('Waiting for peripheral manager to be powered on...'); + if (settings.getLastTarget() == Target.thisDevice) { + return; + } + await Future.delayed(Duration(seconds: 1)); + } + + final syncTxCharacteristic = GATTCharacteristic.mutable( + uuid: UUID.fromString(ZwiftConstants.ZWIFT_SYNC_TX_CHARACTERISTIC_UUID), + descriptors: [], + properties: [ + GATTCharacteristicProperty.read, + GATTCharacteristicProperty.indicate, + ], + permissions: [ + GATTCharacteristicPermission.read, + ], + ); + + _asyncCharacteristic = GATTCharacteristic.mutable( + uuid: UUID.fromString(ZwiftConstants.ZWIFT_ASYNC_CHARACTERISTIC_UUID), + descriptors: [], + properties: [ + GATTCharacteristicProperty.notify, + ], + permissions: [], + ); + + if (!_isServiceAdded) { + await Future.delayed(Duration(seconds: 1)); + + if (!_isSubscribedToEvents) { + _isSubscribedToEvents = true; + _peripheralManager.characteristicReadRequested.forEach((eventArgs) async { + print('Read request for characteristic: ${eventArgs.characteristic.uuid}'); + + switch (eventArgs.characteristic.uuid.toString().toUpperCase()) { + case ZwiftConstants.ZWIFT_SYNC_TX_CHARACTERISTIC_UUID: + print('Handling read request for SYNC TX characteristic'); + break; + case BleUuid.DEVICE_INFORMATION_CHARACTERISTIC_BATTERY_LEVEL: + await _peripheralManager.respondReadRequestWithValue( + eventArgs.request, + value: Uint8List.fromList([100]), + ); + break; + default: + print('Unhandled read request for characteristic: ${eventArgs.characteristic.uuid}'); + } + + final request = eventArgs.request; + final trimmedValue = Uint8List.fromList([]); + await _peripheralManager.respondReadRequestWithValue( + request, + value: trimmedValue, + ); + // You can respond to read requests here if needed + }); + + _peripheralManager.characteristicNotifyStateChanged.forEach((char) { + print( + 'Notify state changed for characteristic: ${char.characteristic.uuid}: ${char.state}', + ); + }); + _peripheralManager.characteristicWriteRequested.forEach((eventArgs) async { + _central = eventArgs.central; + isConnected.value = true; + + final characteristic = eventArgs.characteristic; + final request = eventArgs.request; + final value = request.value; + print( + 'Write request for characteristic: ${characteristic.uuid}', + ); + + switch (eventArgs.characteristic.uuid.toString().toUpperCase()) { + case ZwiftConstants.ZWIFT_SYNC_RX_CHARACTERISTIC_UUID: + print( + 'Handling write request for SYNC RX characteristic, value: ${value.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}\n${String.fromCharCodes(value)}', + ); + + final handshake = [...ZwiftConstants.RIDE_ON, ...ZwiftConstants.RESPONSE_START_CLICK_V2]; + final handshakeAlternative = ZwiftConstants.RIDE_ON; // e.g. Rouvy + + if (value.contentEquals(handshake) || value.contentEquals(handshakeAlternative)) { + print('Sending handshake'); + await _peripheralManager.notifyCharacteristic( + _central!, + syncTxCharacteristic, + value: ZwiftConstants.RIDE_ON, + ); + onUpdate(); + } + break; + default: + print('Unhandled write request for characteristic: ${eventArgs.characteristic.uuid}'); + } + + await _peripheralManager.respondWriteRequest(request); + }); + } + + // Device Information + await _peripheralManager.addService( + GATTService( + uuid: UUID.fromString('180A'), + isPrimary: true, + characteristics: [ + GATTCharacteristic.immutable( + uuid: UUID.fromString('2A29'), + value: Uint8List.fromList('BikeControl'.codeUnits), + descriptors: [], + ), + GATTCharacteristic.immutable( + uuid: UUID.fromString('2A25'), + value: Uint8List.fromList('09-B48123283828F1337'.codeUnits), + descriptors: [], + ), + GATTCharacteristic.immutable( + uuid: UUID.fromString('2A27'), + value: Uint8List.fromList('A.0'.codeUnits), + descriptors: [], + ), + GATTCharacteristic.immutable( + uuid: UUID.fromString('2A26'), + value: Uint8List.fromList((packageInfoValue?.version ?? '1.0.0').codeUnits), + descriptors: [], + ), + ], + includedServices: [], + ), + ); + + // Battery Service + await _peripheralManager.addService( + GATTService( + uuid: UUID.fromString('180F'), + isPrimary: true, + characteristics: [ + GATTCharacteristic.mutable( + uuid: UUID.fromString('2A19'), + descriptors: [], + properties: [ + GATTCharacteristicProperty.read, + GATTCharacteristicProperty.notify, + ], + permissions: [ + GATTCharacteristicPermission.read, + ], + ), + ], + includedServices: [], + ), + ); + + // Unknown Service + await _peripheralManager.addService( + GATTService( + uuid: UUID.fromString(ZwiftConstants.ZWIFT_RIDE_CUSTOM_SERVICE_UUID_SHORT), + isPrimary: true, + characteristics: [ + _asyncCharacteristic!, + GATTCharacteristic.mutable( + uuid: UUID.fromString(ZwiftConstants.ZWIFT_SYNC_RX_CHARACTERISTIC_UUID), + descriptors: [], + properties: [ + GATTCharacteristicProperty.writeWithoutResponse, + ], + permissions: [], + ), + syncTxCharacteristic, + GATTCharacteristic.mutable( + uuid: UUID.fromString('00000005-19CA-4651-86E5-FA29DCDD09D1'), + descriptors: [], + properties: [ + GATTCharacteristicProperty.notify, + ], + permissions: [], + ), + GATTCharacteristic.mutable( + uuid: UUID.fromString('00000006-19CA-4651-86E5-FA29DCDD09D1'), + descriptors: [], + properties: [ + GATTCharacteristicProperty.indicate, + GATTCharacteristicProperty.read, + GATTCharacteristicProperty.writeWithoutResponse, + GATTCharacteristicProperty.write, + ], + permissions: [ + GATTCharacteristicPermission.read, + GATTCharacteristicPermission.write, + ], + ), + ], + includedServices: [], + ), + ); + _isServiceAdded = true; + } + + final advertisement = Advertisement( + name: 'BikeControl', + serviceUUIDs: [UUID.fromString(ZwiftConstants.ZWIFT_RIDE_CUSTOM_SERVICE_UUID_SHORT)], + serviceData: { + UUID.fromString(ZwiftConstants.ZWIFT_RIDE_CUSTOM_SERVICE_UUID_SHORT): Uint8List.fromList([0x02]), + }, + manufacturerSpecificData: [ + ManufacturerSpecificData( + id: 0x094A, + data: Uint8List.fromList([ZwiftConstants.CLICK_V2_LEFT_SIDE, 0x13, 0x37]), + ), + ], + ); + print('Starting advertising with Zwift service...'); + + await _peripheralManager.startAdvertising(advertisement); + _isAdvertising = true; + _isLoading = false; + onUpdate(); + } + + Future stopAdvertising() async { + await _peripheralManager.stopAdvertising(); + _isAdvertising = false; + _isLoading = false; + } + + Future sendAction(InGameAction inGameAction, int? inGameActionValue) async { + final button = switch (inGameAction) { + InGameAction.shiftUp => RideButtonMask.SHFT_UP_R_BTN, + InGameAction.shiftDown => RideButtonMask.SHFT_UP_L_BTN, + InGameAction.uturn => RideButtonMask.DOWN_BTN, + InGameAction.steerLeft => RideButtonMask.LEFT_BTN, + InGameAction.steerRight => RideButtonMask.RIGHT_BTN, + InGameAction.openActionBar => RideButtonMask.UP_BTN, + InGameAction.usePowerUp => RideButtonMask.Y_BTN, + InGameAction.select => RideButtonMask.A_BTN, + InGameAction.back => RideButtonMask.B_BTN, + InGameAction.rideOnBomb => RideButtonMask.Z_BTN, + _ => null, + }; + + if (button == null) { + return Error('Action ${inGameAction.name} not supported by Zwift Emulator'); + } + + final status = RideKeyPadStatus() + ..buttonMap = (~button.mask) & 0xFFFFFFFF + ..analogPaddles.clear(); + + final bytes = status.writeToBuffer(); + + final commandProto = Uint8List.fromList([ + Opcode.CONTROLLER_NOTIFICATION.value, + ...bytes, + ]); + + _peripheralManager.notifyCharacteristic(_central!, _asyncCharacteristic!, value: commandProto); + + final zero = Uint8List.fromList([0x23, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F]); + _peripheralManager.notifyCharacteristic(_central!, _asyncCharacteristic!, value: zero); + return Success('Sent action: ${inGameAction.name}'); + } +} + +class ZwiftEmulatorInformation extends StatelessWidget { + const ZwiftEmulatorInformation({super.key}); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: zwiftEmulator.isConnected, + builder: (context, isConnected, _) { + return StatefulBuilder( + builder: (context, setState) { + return Text('Zwift is ${isConnected ? 'connected' : 'not connected'}'); + }, + ); + }, + ); + } +} diff --git a/lib/bluetooth/devices/zwift/zwift_play.dart b/lib/bluetooth/devices/zwift/zwift_play.dart new file mode 100644 index 000000000..7db20a788 --- /dev/null +++ b/lib/bluetooth/devices/zwift/zwift_play.dart @@ -0,0 +1,62 @@ +import 'package:flutter/foundation.dart'; +import 'package:swift_control/bluetooth/devices/zwift/constants.dart'; +import 'package:swift_control/bluetooth/devices/zwift/protocol/zwift.pb.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_device.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; + +class ZwiftPlay extends ZwiftDevice { + ZwiftPlay(super.scanResult) + : super( + availableButtons: [ + ZwiftButtons.y, + ZwiftButtons.z, + ZwiftButtons.a, + ZwiftButtons.b, + ZwiftButtons.onOffRight, + ZwiftButtons.sideButtonRight, + ZwiftButtons.paddleRight, + ZwiftButtons.navigationUp, + ZwiftButtons.navigationLeft, + ZwiftButtons.navigationRight, + ZwiftButtons.navigationDown, + ZwiftButtons.onOffLeft, + ZwiftButtons.sideButtonLeft, + ZwiftButtons.paddleLeft, + ], + ); + + @override + List get startCommand => ZwiftConstants.RIDE_ON + ZwiftConstants.RESPONSE_START_PLAY; + + @override + bool get canVibrate => true; + + @override + List processClickNotification(Uint8List message) { + final status = PlayKeyPadStatus.fromBuffer(message); + + return [ + if (status.rightPad == PlayButtonStatus.ON) ...[ + if (status.buttonYUp == PlayButtonStatus.ON) ZwiftButtons.y, + if (status.buttonZLeft == PlayButtonStatus.ON) ZwiftButtons.z, + if (status.buttonARight == PlayButtonStatus.ON) ZwiftButtons.a, + if (status.buttonBDown == PlayButtonStatus.ON) ZwiftButtons.b, + if (status.buttonOn == PlayButtonStatus.ON) ZwiftButtons.onOffRight, + if (status.buttonShift == PlayButtonStatus.ON) ZwiftButtons.sideButtonRight, + if (status.analogLR.abs() == 100) ZwiftButtons.paddleRight, + ], + if (status.rightPad == PlayButtonStatus.OFF) ...[ + if (status.buttonYUp == PlayButtonStatus.ON) ZwiftButtons.navigationUp, + if (status.buttonZLeft == PlayButtonStatus.ON) ZwiftButtons.navigationLeft, + if (status.buttonARight == PlayButtonStatus.ON) ZwiftButtons.navigationRight, + if (status.buttonBDown == PlayButtonStatus.ON) ZwiftButtons.navigationDown, + if (status.buttonOn == PlayButtonStatus.ON) ZwiftButtons.onOffLeft, + if (status.buttonShift == PlayButtonStatus.ON) ZwiftButtons.sideButtonLeft, + if (status.analogLR.abs() == 100) ZwiftButtons.paddleLeft, + ], + ]; + } + + @override + String get latestFirmwareVersion => '1.3.1'; +} diff --git a/lib/bluetooth/devices/zwift/zwift_ride.dart b/lib/bluetooth/devices/zwift/zwift_ride.dart new file mode 100644 index 000000000..babae43a3 --- /dev/null +++ b/lib/bluetooth/devices/zwift/zwift_ride.dart @@ -0,0 +1,282 @@ +import 'package:dartx/dartx.dart'; +import 'package:flutter/foundation.dart'; +import 'package:protobuf/protobuf.dart' as $pb; +import 'package:swift_control/bluetooth/devices/zwift/constants.dart'; +import 'package:swift_control/bluetooth/devices/zwift/protocol/zp.pb.dart'; +import 'package:swift_control/bluetooth/devices/zwift/protocol/zp_vendor.pb.dart'; +import 'package:swift_control/bluetooth/devices/zwift/protocol/zwift.pb.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_device.dart'; +import 'package:swift_control/bluetooth/messages/notification.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:universal_ble/universal_ble.dart'; + +class ZwiftRide extends ZwiftDevice { + /// Minimum absolute analog value (0-100) required to trigger paddle button press. + /// Values below this threshold are ignored to prevent accidental triggers from + /// analog drift or light touches. + static const int analogPaddleThreshold = 25; + + ZwiftRide(super.scanResult, {super.isBeta, List? availableButtons}) + : super( + availableButtons: + availableButtons ?? + [ + ZwiftButtons.navigationLeft, + ZwiftButtons.navigationRight, + ZwiftButtons.navigationUp, + ZwiftButtons.navigationDown, + ZwiftButtons.a, + ZwiftButtons.b, + ZwiftButtons.y, + ZwiftButtons.z, + ZwiftButtons.shiftUpLeft, + ZwiftButtons.shiftDownLeft, + ZwiftButtons.shiftUpRight, + ZwiftButtons.shiftDownRight, + ZwiftButtons.powerUpLeft, + ZwiftButtons.powerUpRight, + ZwiftButtons.onOffLeft, + ZwiftButtons.onOffRight, + ZwiftButtons.paddleLeft, + ZwiftButtons.paddleRight, + ], + ); + + @override + String get customServiceId => ZwiftConstants.ZWIFT_RIDE_CUSTOM_SERVICE_UUID; + + @override + String get latestFirmwareVersion => '1.2.0'; + + @override + bool get canVibrate => true; + + @override + Future processData(Uint8List bytes) async { + Opcode? opcode = Opcode.valueOf(bytes[0]); + Uint8List message = bytes.sublist(1); + + if (kDebugMode) { + print( + '${DateTime.now().toString().split(" ").last} Received $opcode: ${bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')} => ${String.fromCharCodes(bytes)} ', + ); + } + + switch (opcode) { + case Opcode.RIDE_ON: + //print("Empty RideOn response - unencrypted mode"); + + break; + case Opcode.STATUS_RESPONSE: + final status = StatusResponse.fromBuffer(message); + if (kDebugMode) { + print('StatusResponse: ${status.command} status: ${Status.valueOf(status.status)}'); + } + break; + case Opcode.GET_RESPONSE: + final response = GetResponse.fromBuffer(message); + final dataObjectType = DO.valueOf(response.dataObjectId); + if (kDebugMode) { + print( + 'GetResponse: ${dataObjectType?.value.toRadixString(16).padLeft(4, '0') ?? response.dataObjectId} $dataObjectType', + ); + } + + switch (dataObjectType) { + case DO.PAGE_DEV_INFO: + final pageDevInfo = DevInfoPage.fromBuffer(response.dataObjectData); + if (kDebugMode) { + print('PageDevInfo: $pageDevInfo'); + } + break; + case DO.PAGE_DATE_TIME: + final pageDateTime = DateTimePage.fromBuffer(response.dataObjectData); + if (kDebugMode) { + print('PageDateTime: $pageDateTime'); + } + break; + case DO.PAGE_CONTROLLER_INPUT_CONFIG: + final pageDateTime = ControllerInputConfigPage.fromBuffer(response.dataObjectData); + if (kDebugMode) { + print('PageDateTime: $pageDateTime'); + } + break; + case null: + final vendorDO = VendorDO.valueOf(response.dataObjectId); + if (kDebugMode) { + print('VendorDO: $vendorDO'); + } + switch (vendorDO) { + case VendorDO.DEVICE_COUNT: + // TODO: Handle this case. + break; + case VendorDO.NO_CLUE: + // TODO: Handle this case. + break; + case VendorDO.PAGE_DEVICE_PAIRING: + final page = DevicePairingDataPage.fromBuffer(response.dataObjectData); + if (kDebugMode) { + // this should show the right click device + // pairingStatus = 1 => connected and paired, otherwise it can be paired but not connected + print( + 'PageDevicePairing: $page => ${page.pairingDevList.map((e) => e.device.reversed.map((d) => d.toRadixString(16).padLeft(2, '0'))).join(', ')}', + ); + } + break; + case VendorDO.PAIRED_DEVICE: + // TODO: Handle this case. + break; + case VendorDO.PAIRING_STATUS: + break; + } + break; + default: + break; + } + break; + case Opcode.VENDOR_MESSAGE: + final vendorOpCode = VendorOpcode.valueOf(message.second); + print('VendorOpcode: $vendorOpCode'); + break; + case Opcode.LOG_DATA: + final logMessage = LogDataNotification.fromBuffer(message); + if (kDebugMode) { + actionStreamInternal.add(LogNotification(logMessage.toString())); + } + break; + case Opcode.BATTERY_NOTIF: + final notification = BatteryNotification.fromBuffer(message); + if (batteryLevel != notification.newPercLevel) { + batteryLevel = notification.newPercLevel; + connection.signalChange(this); + } + break; + case Opcode.CONTROLLER_NOTIFICATION: + try { + final buttonsClicked = processClickNotification(message); + handleButtonsClicked(buttonsClicked); + } catch (e) { + actionStreamInternal.add(LogNotification(e.toString())); + } + break; + case null: + if (bytes[0] == 0x1A) { + final batteryStatus = BatteryStatus.fromBuffer(message); + if (kDebugMode) { + print('BatteryStatus: $batteryStatus'); + } + } + break; + } + } + + @override + List processClickNotification(Uint8List message) { + final status = RideKeyPadStatus.fromBuffer(message); + + // Debug: Log all button mask detections (moved to ZwiftRide.processClickNotification) + + // Process DIGITAL buttons separately + final buttonsClicked = [ + if (status.buttonMap & RideButtonMask.LEFT_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.navigationLeft, + if (status.buttonMap & RideButtonMask.RIGHT_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.navigationRight, + if (status.buttonMap & RideButtonMask.UP_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.navigationUp, + if (status.buttonMap & RideButtonMask.DOWN_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.navigationDown, + if (status.buttonMap & RideButtonMask.A_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.a, + if (status.buttonMap & RideButtonMask.B_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.b, + if (status.buttonMap & RideButtonMask.Y_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.y, + if (status.buttonMap & RideButtonMask.Z_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.z, + if (status.buttonMap & RideButtonMask.SHFT_UP_L_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.shiftUpLeft, + if (status.buttonMap & RideButtonMask.SHFT_DN_L_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.shiftDownLeft, + if (status.buttonMap & RideButtonMask.SHFT_UP_R_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.shiftUpRight, + if (status.buttonMap & RideButtonMask.SHFT_DN_R_BTN.mask == PlayButtonStatus.ON.value) + ZwiftButtons.shiftDownRight, + if (status.buttonMap & RideButtonMask.POWERUP_L_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.powerUpLeft, + if (status.buttonMap & RideButtonMask.POWERUP_R_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.powerUpRight, + if (status.buttonMap & RideButtonMask.ONOFF_L_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.onOffLeft, + if (status.buttonMap & RideButtonMask.ONOFF_R_BTN.mask == PlayButtonStatus.ON.value) ZwiftButtons.onOffRight, + ]; + + // Process ANALOG inputs separately - now properly separated from digital + // All analog paddles (L0-L3) appear in field 3 as repeated RideAnalogKeyPress + final List analogButtons = []; + try { + for (final paddle in status.analogPaddles) { + if (paddle.hasLocation() && paddle.hasAnalogValue()) { + if (paddle.analogValue.abs() >= analogPaddleThreshold) { + final button = switch (paddle.location.value) { + 0 => ZwiftButtons.paddleLeft, // L0 = left paddle + 1 => ZwiftButtons.paddleRight, // L1 = right paddle + _ => null, // L2, L3 unused + }; + + if (button != null) { + buttonsClicked.add(button); + analogButtons.add(button); + } + } + } + } + } catch (e) { + if (kDebugMode) { + print('Error parsing analog paddle data: $e'); + } + } + return buttonsClicked; + } + + Future sendCommand(Opcode opCode, $pb.GeneratedMessage? message) async { + final buffer = Uint8List.fromList([opCode.value, ...message?.writeToBuffer() ?? []]); + if (kDebugMode) { + print("Sending $opCode: ${buffer.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}"); + } + await UniversalBle.write( + device.deviceId, + customServiceId, + syncRxCharacteristic!.uuid, + buffer, + withoutResponse: true, + ); + await Future.delayed(Duration(milliseconds: 500)); + } + + Future sendCommandBuffer(Uint8List buffer) async { + if (kDebugMode) { + print("Sending ${buffer.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}"); + } + await UniversalBle.write( + device.deviceId, + customServiceId, + syncRxCharacteristic!.uuid, + buffer, + withoutResponse: true, + ); + } +} + +enum RideButtonMask { + LEFT_BTN(0x00001), + UP_BTN(0x00002), + RIGHT_BTN(0x00004), + DOWN_BTN(0x00008), + + A_BTN(0x00010), + B_BTN(0x00020), + Y_BTN(0x00040), + Z_BTN(0x00080), + + SHFT_UP_L_BTN(0x00100), + SHFT_DN_L_BTN(0x00200), + SHFT_UP_R_BTN(0x01000), + SHFT_DN_R_BTN(0x02000), + + POWERUP_L_BTN(0x00400), + POWERUP_R_BTN(0x04000), + ONOFF_L_BTN(0x00800), + ONOFF_R_BTN(0x08000); + + final int mask; + + const RideButtonMask(this.mask); +} diff --git a/lib/bluetooth/messages/notification.dart b/lib/bluetooth/messages/notification.dart new file mode 100644 index 000000000..cac6d24d5 --- /dev/null +++ b/lib/bluetooth/messages/notification.dart @@ -0,0 +1,60 @@ +import 'package:dartx/dartx.dart'; +import 'package:swift_control/utils/actions/base_actions.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/widgets/keymap_explanation.dart'; + +class BaseNotification {} + +class LogNotification extends BaseNotification { + final String message; + + LogNotification(this.message); + + @override + String toString() { + return message; + } +} + +class BluetoothAvailabilityNotification extends BaseNotification { + final bool isAvailable; + + BluetoothAvailabilityNotification(this.isAvailable); + + @override + String toString() { + return 'Bluetooth is ${isAvailable ? "available" : "unavailable"}'; + } +} + +class ButtonNotification extends BaseNotification { + List buttonsClicked; + + ButtonNotification({this.buttonsClicked = const []}); + + @override + String toString() { + return 'Buttons: ${buttonsClicked.joinToString(transform: (e) => e.name.splitByUpperCase())}'; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ButtonNotification && + runtimeType == other.runtimeType && + buttonsClicked.contentEquals(other.buttonsClicked); + + @override + int get hashCode => buttonsClicked.hashCode; +} + +class ActionNotification extends BaseNotification { + final ActionResult result; + + ActionNotification(this.result); + + @override + String toString() { + return result.message; + } +} diff --git a/lib/main.dart b/lib/main.dart index 07c232e4b..9e7412fff 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,32 +1,176 @@ -import 'package:accessibility/accessibility.dart'; +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:swift_control/pages/requirements.dart'; import 'package:swift_control/theme.dart'; -import 'package:swift_control/utils/connection.dart'; +import 'package:swift_control/utils/actions/android.dart'; +import 'package:swift_control/utils/actions/desktop.dart'; +import 'package:swift_control/utils/actions/remote.dart'; +import 'package:swift_control/utils/settings/settings.dart'; +import 'package:swift_control/widgets/menu.dart'; +import 'bluetooth/connection.dart'; +import 'bluetooth/devices/link/link.dart'; import 'utils/actions/base_actions.dart'; final connection = Connection(); -final actionHandler = ActionHandler(); -final accessibilityHandler = Accessibility(); +final navigatorKey = GlobalKey(); +late BaseActions actionHandler; final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); +final settings = Settings(); +final whooshLink = WhooshLink(); +var screenshotMode = false; + +void main() async { + // setup crash reporting + + // Catch errors that happen in other isolates + if (!kIsWeb) { + Isolate.current.addErrorListener( + RawReceivePort((dynamic pair) { + final List errorAndStack = pair as List; + final error = errorAndStack.first; + final stack = errorAndStack.last as StackTrace?; + _recordError(error, stack, context: 'Isolate'); + }).sendPort, + ); + } + + runZonedGuarded>( + () async { + // Catch Flutter framework errors (build/layout/paint) + FlutterError.onError = (FlutterErrorDetails details) { + _recordFlutterError(details); + // Optionally forward to default behavior in debug: + FlutterError.presentError(details); + }; + + // Catch errors from platform dispatcher (async) + PlatformDispatcher.instance.onError = (Object error, StackTrace stack) { + _recordError(error, stack, context: 'PlatformDispatcher'); + // Return true means "handled" + return true; + }; + + WidgetsFlutterBinding.ensureInitialized(); + + final error = await settings.init(); + + runApp(SwiftPlayApp(error: error)); + }, + (Object error, StackTrace stack) { + // Zone-level uncaught errors (async, timers, futures) + _recordError(error, stack, context: 'Zone'); + }, + ); +} + +Future _recordFlutterError(FlutterErrorDetails details) async { + await _persistCrash( + type: 'flutter', + error: details.exceptionAsString(), + stack: details.stack, + information: details.informationCollector?.call().join('\n'), + ); +} + +Future _recordError( + Object error, + StackTrace? stack, { + required String context, +}) async { + await _persistCrash( + type: 'dart', + error: error.toString(), + stack: stack, + information: 'Context: $context', + ); +} + +Future _persistCrash({ + required String type, + required String error, + StackTrace? stack, + String? information, +}) async { + try { + final timestamp = DateTime.now().toIso8601String(); + final crashData = StringBuffer() + ..writeln('--- $timestamp ---') + ..writeln('Type: $type') + ..writeln('Error: $error') + ..writeln('Stack: ${stack ?? 'no stack'}') + ..writeln('Info: ${information ?? ''}') + ..writeln(debugText()) + ..writeln() + ..writeln(); + + final directory = await _getLogDirectory(); + final file = File('${directory.path}/app.logs'); + await file.writeAsString(crashData.toString(), mode: FileMode.append); + } catch (_) { + // Avoid throwing from the crash logger + } +} + +// Minimal implementation; customize per platform if needed. +Future _getLogDirectory() async { + // On mobile, you might choose applicationDocumentsDirectory via platform channel, + // but staying pure Dart, use currentDirectory as a placeholder. + return Directory.current; +} -void main() { - runApp(const SwiftPlayApp()); +enum ConnectionType { + unknown, + local, + remote, +} + +Future initializeActions(ConnectionType connectionType) async { + if (kIsWeb) { + actionHandler = StubActions(); + } else if (Platform.isAndroid) { + actionHandler = switch (connectionType) { + ConnectionType.local => AndroidActions(), + ConnectionType.remote => RemoteActions(), + ConnectionType.unknown => StubActions(), + }; + } else if (Platform.isIOS) { + actionHandler = switch (connectionType) { + ConnectionType.local => StubActions(), + ConnectionType.remote => RemoteActions(), + ConnectionType.unknown => StubActions(), + }; + } else { + actionHandler = switch (connectionType) { + ConnectionType.local => DesktopActions(), + ConnectionType.remote => RemoteActions(), + ConnectionType.unknown => StubActions(), + }; + } + actionHandler.init(settings.getKeyMap()); } class SwiftPlayApp extends StatelessWidget { - const SwiftPlayApp({super.key}); + final String? error; + const SwiftPlayApp({super.key, this.error}); @override Widget build(BuildContext context) { return MaterialApp( - title: 'SwiftControl', + navigatorKey: navigatorKey, + debugShowCheckedModeBanner: false, + title: 'BikeControl', theme: AppTheme.light, darkTheme: AppTheme.dark, themeMode: ThemeMode.system, - home: const RequirementsPage(), + home: error != null + ? Text('There was an error starting the App. Please contact support:\n$error') + : const RequirementsPage(), ); } } diff --git a/lib/pages/device.dart b/lib/pages/device.dart index cbe298c13..b9363f05d 100644 --- a/lib/pages/device.dart +++ b/lib/pages/device.dart @@ -1,11 +1,42 @@ import 'dart:async'; +import 'dart:io'; -import 'package:dartx/dartx.dart'; +import 'package:device_auto_rotate_checker/device_auto_rotate_checker.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_device.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_emulator.dart'; import 'package:swift_control/main.dart'; -import 'package:swift_control/utils/devices/ble_device.dart'; +import 'package:swift_control/utils/actions/desktop.dart'; +import 'package:swift_control/utils/keymap/apps/my_whoosh.dart'; +import 'package:swift_control/utils/keymap/manager.dart'; +import 'package:swift_control/utils/requirements/multi.dart'; +import 'package:swift_control/widgets/apps/mywhoosh_link_tile.dart'; +import 'package:swift_control/widgets/apps/zwift_tile.dart'; +import 'package:swift_control/widgets/beta_pill.dart'; +import 'package:swift_control/widgets/keymap_explanation.dart'; +import 'package:swift_control/widgets/logviewer.dart'; +import 'package:swift_control/widgets/scan.dart'; +import 'package:swift_control/widgets/small_progress_indicator.dart'; +import 'package:swift_control/widgets/status.dart'; +import 'package:swift_control/widgets/testbed.dart'; +import 'package:swift_control/widgets/title.dart'; +import 'package:swift_control/widgets/warning.dart'; +import 'package:universal_ble/universal_ble.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; -import '../utils/messages/notification.dart'; +import '../bluetooth/devices/base_device.dart'; +import '../utils/actions/android.dart'; +import '../utils/actions/remote.dart'; +import '../utils/keymap/apps/custom_app.dart'; +import '../utils/keymap/apps/supported_app.dart'; +import '../utils/requirements/remote.dart'; +import '../widgets/changelog_dialog.dart'; +import '../widgets/menu.dart'; class DevicePage extends StatefulWidget { const DevicePage({super.key}); @@ -14,90 +45,574 @@ class DevicePage extends StatefulWidget { State createState() => _DevicePageState(); } -class _DevicePageState extends State { - List _actions = []; - - late StreamSubscription _connectionStateSubscription; - - late StreamSubscription _actionSubscription; +class _DevicePageState extends State with WidgetsBindingObserver { + late StreamSubscription _connectionStateSubscription; + final controller = TextEditingController(text: actionHandler.supportedApp?.name); + final _snackBarMessengerKey = GlobalKey(); + bool _showAutoRotationWarning = false; + bool _showMiuiWarning = false; + bool _showNameChangeWarning = false; + StreamSubscription? _autoRotateStream; @override void initState() { super.initState(); - _actionSubscription = connection.actionStream.listen((data) { - if (mounted) { - setState(() { - _actions.add('${DateTime.now().toString().split(" ").last}: $data'); - _actions = _actions.takeLast(30).toList(); + // keep screen on - this is required for iOS to keep the bluetooth connection alive + if (!screenshotMode) { + WakelockPlus.enable(); + } + _showNameChangeWarning = !settings.knowsAboutNameChange(); + WidgetsBinding.instance.addObserver(this); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkAndShowChangelog(); + }); + + if (!kIsWeb) { + whooshLink.isStarted.addListener(() { + if (mounted) setState(() {}); + }); + + zwiftEmulator.isConnected.addListener(() { + if (mounted) setState(() {}); + }); + + if (settings.getZwiftEmulatorEnabled() && settings.getTrainerApp()?.supportsZwiftEmulation == true) { + zwiftEmulator.startAdvertising(() { + if (mounted) setState(() {}); }); } - }); + } + + if (actionHandler is RemoteActions && !kIsWeb && Platform.isIOS && (actionHandler as RemoteActions).isConnected) { + WidgetsBinding.instance.addPostFrameCallback((_) { + // show snackbar to inform user that the app needs to stay in foreground + _snackBarMessengerKey.currentState?.showSnackBar( + SnackBar( + content: Text('To simulate touches the app needs to stay in the foreground.'), + duration: Duration(seconds: 5), + ), + ); + }); + } _connectionStateSubscription = connection.connectionStream.listen((state) async { setState(() {}); }); + + if (!kIsWeb && Platform.isAndroid) { + DeviceAutoRotateChecker.checkAutoRotate().then((isEnabled) { + if (!isEnabled) { + setState(() { + _showAutoRotationWarning = true; + }); + } + }); + _autoRotateStream = DeviceAutoRotateChecker.autoRotateStream.listen((isEnabled) { + setState(() { + _showAutoRotationWarning = !isEnabled; + }); + }); + + // Check if device is MIUI and using local accessibility service + if (actionHandler is AndroidActions) { + _checkMiuiDevice(); + } + } } @override void dispose() { + WidgetsBinding.instance.removeObserver(this); + + _autoRotateStream?.cancel(); _connectionStateSubscription.cancel(); - _actionSubscription.cancel(); + controller.dispose(); super.dispose(); } - final _snackBarMessengerKey = GlobalKey(); + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + if (actionHandler is RemoteActions && Platform.isIOS && (actionHandler as RemoteActions).isConnected) { + UniversalBle.getBluetoothAvailabilityState().then((state) { + if (state == AvailabilityState.poweredOn) { + final requirement = RemoteRequirement(); + requirement.reconnect(); + _snackBarMessengerKey.currentState?.showSnackBar( + SnackBar( + content: Text('To simulate touches the app needs to stay in the foreground.'), + duration: Duration(seconds: 5), + ), + ); + } + }); + } + } + } + + Future _checkMiuiDevice() async { + try { + // Don't show if user has dismissed the warning + if (settings.getMiuiWarningDismissed()) { + return; + } + + final deviceInfo = await DeviceInfoPlugin().androidInfo; + final isMiui = + deviceInfo.manufacturer.toLowerCase() == 'xiaomi' || + deviceInfo.brand.toLowerCase() == 'xiaomi' || + deviceInfo.brand.toLowerCase() == 'redmi' || + deviceInfo.brand.toLowerCase() == 'poco'; + if (isMiui && mounted) { + setState(() { + _showMiuiWarning = true; + }); + } + } catch (e) { + // Silently fail if device info is not available + } + } + + Future _checkAndShowChangelog() async { + try { + final packageInfo = await PackageInfo.fromPlatform(); + final currentVersion = packageInfo.version; + final lastSeenVersion = settings.getLastSeenVersion(); + + if (mounted) { + await ChangelogDialog.showIfNeeded(context, currentVersion, lastSeenVersion); + } + + // Update last seen version + await settings.setLastSeenVersion(currentVersion); + } catch (e) { + print('Failed to check changelog: $e'); + } + } @override Widget build(BuildContext context) { + final canVibrate = connection.bluetoothDevices.any( + (device) => device.isConnected && device is ZwiftDevice && device.canVibrate, + ); + + final paddingMultiplicator = actionHandler is DesktopActions ? 2.5 : 1.0; + return ScaffoldMessenger( key: _snackBarMessengerKey, child: PopScope( onPopInvokedWithResult: (hello, _) { connection.reset(); }, - child: Scaffold( - appBar: AppBar( - title: Text('SwiftControl'), - actions: [ - IconButton( - onPressed: () { - _actions.clear(); - setState(() {}); - }, - icon: Icon(Icons.clear), + child: Stack( + children: [ + Scaffold( + appBar: AppBar( + title: AppTitle(), + actions: buildMenuButtons(), + backgroundColor: Theme.brightnessOf(context) == Brightness.light + ? Theme.of(context).colorScheme.inversePrimary + : null, ), - ], - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - ), - body: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Devices:\n${connection.devices.joinToString(separator: '\n', transform: (it) { - return "${it.device.platformName}: ${it.device.isConnected ? 'Connected' : 'Not connected'}"; - })}', + body: SingleChildScrollView( + padding: EdgeInsets.only( + top: 16, + left: 8.0 * paddingMultiplicator, + right: 8 * paddingMultiplicator, + bottom: 8, ), - Divider(color: Theme.of(context).colorScheme.primary, height: 30), - Expanded( - child: ListView( - children: - _actions - .map( - (action) => Text( - action, - style: TextStyle(fontSize: 12, fontFeatures: [FontFeature.tabularFigures()]), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_showNameChangeWarning && !screenshotMode) + Warning( + important: false, + children: [ + Text( + 'SwiftControl is now BikeControl!\nIt is part of the OpenBikeControl project, advocating for open standards in smart bike trainers - and building affordable hardware controllers!', + ), + SizedBox(height: 8), + TextButton( + onPressed: () { + setState(() { + _showNameChangeWarning = false; + }); + launchUrlString('https://openbikecontrol.org'); + }, + child: Text('More Information'), + ), + ], + ), + if (_showAutoRotationWarning) + Warning( + important: false, + children: [ + Text('Enable auto-rotation on your device to make sure the app works correctly.'), + ], + ), + if (_showMiuiWarning) + Warning( + children: [ + Row( + children: [ + Icon(Icons.warning_amber, color: Theme.of(context).colorScheme.error), + SizedBox(width: 8), + Expanded( + child: Text( + 'MIUI Device Detected', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.error, + ), + ), + ), + IconButton( + icon: Icon(Icons.close), + onPressed: () async { + await settings.setMiuiWarningDismissed(true); + setState(() { + _showMiuiWarning = false; + }); + }, + tooltip: 'Dismiss', + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + ), + ], + ), + SizedBox(height: 8), + Text( + 'Your device is running MIUI, which is known to aggressively kill background services and accessibility services.', + style: TextStyle(fontSize: 14), + ), + SizedBox(height: 8), + Text( + 'To ensure BikeControl works properly:', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600), + ), + Text( + '• Disable battery optimization for BikeControl', + style: TextStyle(fontSize: 14), + ), + Text( + '• Enable autostart for BikeControl', + style: TextStyle(fontSize: 14), + ), + Text( + '• Lock the app in recent apps', + style: TextStyle(fontSize: 14), + ), + SizedBox(height: 12), + ElevatedButton.icon( + onPressed: () async { + final url = Uri.parse('https://dontkillmyapp.com/xiaomi'); + if (await canLaunchUrl(url)) { + await launchUrl(url, mode: LaunchMode.externalApplication); + } + }, + icon: Icon(Icons.open_in_new), + label: Text('View Detailed Instructions'), + ), + ], + ), + Card( + child: Padding( + padding: EdgeInsets.only( + left: 16.0, + right: 16, + top: 16, + bottom: actionHandler is RemoteActions ? 0 : 12, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: const EdgeInsets.only(bottom: 8.0), + width: double.infinity, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).colorScheme.primaryContainer, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Connected Controllers', + style: TextStyle(fontWeight: FontWeight.bold), + ), + if (connection.controllerDevices.isEmpty) SmallProgressIndicator(), + ], + ), + ), + ), + if (connection.controllerDevices.isEmpty) + ScanWidget() + else + ...connection.controllerDevices.map( + (device) => device.showInformation(context), + ), + + if (actionHandler is RemoteActions || + whooshLink.isCompatible(settings.getLastTarget() ?? Target.thisDevice) || + actionHandler.supportedApp?.supportsZwiftEmulation == true) + Container( + margin: const EdgeInsets.only(bottom: 8.0), + width: double.infinity, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).colorScheme.primaryContainer, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + 'Remote Connections', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + + if (settings.getTrainerApp() is MyWhoosh && + whooshLink.isCompatible(settings.getLastTarget()!)) + MyWhooshLinkTile(), + if (settings.getTrainerApp()?.supportsZwiftEmulation == true) + ZwiftTile( + onUpdate: () { + setState(() {}); + }, ), - ) - .toList(), - ), + + if (actionHandler is RemoteActions && isAdvertisingPeripheral) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Remote Control Mode: ${(actionHandler as RemoteActions).isConnected ? 'Connected' : 'Not connected (optional)'}', + ), + PopupMenuButton( + itemBuilder: (_) => [ + PopupMenuItem( + child: Text('Reconnect'), + onTap: () async { + final requirement = RemoteRequirement(); + await requirement.reconnect(); + }, + ), + ], + ), + ], + ) + else + SizedBox(height: 8), + ], + ), + ), + ), + + SizedBox(height: 20), + StatusWidget(), + SizedBox(height: 20), + if (!kIsWeb) ...[ + Card( + child: Padding( + padding: EdgeInsets.only( + left: 16.0, + right: 16, + top: 16, + bottom: canVibrate ? 0 : 12, + ), + child: Column( + spacing: 12, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: const EdgeInsets.only(bottom: 8.0), + width: double.infinity, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).colorScheme.primaryContainer, + ), + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + 'Customize ${screenshotMode ? 'Trainer app' : settings.getTrainerApp()?.name} on ${settings.getLastTarget()?.title}', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + + if (settings.getLastTarget()?.warning != null) ...[ + Row( + children: [ + Icon( + Icons.warning_amber, + color: Theme.of(context).colorScheme.error, + ), + Text( + settings.getLastTarget()!.warning!, + style: TextStyle(color: Colors.red), + ), + ], + ), + ], + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + spacing: 8, + children: [ + Expanded( + child: DropdownMenu( + controller: controller, + dropdownMenuEntries: [ + ..._getAllApps().map( + (app) => DropdownMenuEntry( + value: app, + label: screenshotMode ? 'Trainer app' : app.name, + labelWidget: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(app.name), + if (app is CustomApp) BetaPill(text: 'CUSTOM'), + ], + ), + ), + ), + DropdownMenuEntry( + value: CustomApp(profileName: 'New'), + label: 'Create new keymap', + labelWidget: Text('Create new keymap'), + leadingIcon: Icon(Icons.add), + ), + ], + label: Text('Select Keymap'), + onSelected: (app) async { + if (app == null) { + return; + } else if (app.name == 'New') { + final profileName = await KeymapManager().showNewProfileDialog(context); + if (profileName != null && profileName.isNotEmpty) { + final customApp = CustomApp(profileName: profileName); + actionHandler.init(customApp); + await settings.setKeyMap(customApp); + controller.text = profileName; + setState(() {}); + } + } else { + controller.text = app.name ?? ''; + actionHandler.supportedApp = app; + await settings.setKeyMap(app); + setState(() {}); + } + }, + initialSelection: actionHandler.supportedApp, + hintText: 'Select your Keymap', + ), + ), + Row( + children: [ + KeymapManager().getManageProfileDialog( + context, + actionHandler.supportedApp is CustomApp + ? actionHandler.supportedApp?.name + : null, + onDone: () { + setState(() {}); + controller.text = actionHandler.supportedApp?.name ?? ''; + }, + ), + ], + ), + ], + ), + if (actionHandler.supportedApp is! CustomApp) + Text( + 'Customize the keymap if you experience any issues (e.g. wrong keyboard output, or misaligned touch placements)', + style: TextStyle(fontSize: 12), + ), + if (actionHandler.supportedApp != null && connection.controllerDevices.isNotEmpty) + KeymapExplanation( + key: Key(actionHandler.supportedApp!.keymap.runtimeType.toString()), + keymap: actionHandler.supportedApp!.keymap, + onUpdate: () { + setState(() {}); + controller.text = actionHandler.supportedApp?.name ?? ''; + + if (actionHandler.supportedApp is CustomApp) { + settings.setKeyMap(actionHandler.supportedApp!); + } + }, + ), + if (canVibrate) ...[ + SwitchListTile( + title: Text('Enable vibration feedback when shifting gears'), + value: settings.getVibrationEnabled(), + contentPadding: EdgeInsets.zero, + onChanged: (value) async { + await settings.setVibrationEnabled(value); + setState(() {}); + }, + ), + ], + ], + ), + ), + ), + ], + SizedBox(height: 20), + ExpansionTile( + title: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Text('Logs', style: Theme.of(context).textTheme.titleMedium), + ), + maintainState: true, + children: [ + SizedBox(height: 500, child: LogViewer()), + ], + ), + ], ), - ], + ), ), - ), + Positioned.fill(child: Testbed()), + ], ), ), ); } + + List _getAllApps() { + final baseApp = settings.getTrainerApp(); + final customProfiles = settings.getCustomAppProfiles(); + + final customApps = customProfiles.map((profile) { + final customApp = CustomApp(profileName: profile); + final savedKeymap = settings.getCustomAppKeymap(profile); + if (savedKeymap != null) { + customApp.decodeKeymap(savedKeymap); + } + return customApp; + }).toList(); + + // If no custom profiles exist, add the default "Custom" one + if (customApps.isEmpty) { + customApps.add(CustomApp()); + } + + return [if (baseApp != null) baseApp, ...customApps]; + } +} + +extension Screenshot on String { + String get screenshot => screenshotMode ? replaceAll('Zwift ', '') : this; } diff --git a/lib/pages/markdown.dart b/lib/pages/markdown.dart new file mode 100644 index 000000000..b92d3d33f --- /dev/null +++ b/lib/pages/markdown.dart @@ -0,0 +1,88 @@ +import 'package:flex_color_scheme/flex_color_scheme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_md/flutter_md.dart'; +import 'package:http/http.dart' as http; +import 'package:url_launcher/url_launcher_string.dart'; + +class MarkdownPage extends StatefulWidget { + final String assetPath; + const MarkdownPage({super.key, required this.assetPath}); + + @override + State createState() => _ChangelogPageState(); +} + +class _ChangelogPageState extends State { + Markdown? _markdown; + String? _error; + + @override + void initState() { + super.initState(); + _loadChangelog(); + } + + Future _loadChangelog() async { + try { + final md = await rootBundle.loadString(widget.assetPath); + setState(() { + _markdown = Markdown.fromString(md); + }); + + // load latest version + final response = await http.get( + Uri.parse('https://raw.githubusercontent.com/jonasbark/swiftcontrol/refs/heads/main/${widget.assetPath}'), + ); + if (response.statusCode == 200) { + final latestMd = response.body; + if (latestMd != md) { + setState(() { + _markdown = Markdown.fromString(md); + }); + } + } + } catch (e) { + setState(() { + _error = 'Failed to load changelog: $e'; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.assetPath.replaceAll('.md', '').toLowerCase().capitalize), + backgroundColor: Theme.brightnessOf(context) == Brightness.light + ? Theme.of(context).colorScheme.inversePrimary + : null, + ), + body: _error != null + ? Center(child: Text(_error!)) + : _markdown == null + ? Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + padding: EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: MarkdownWidget( + markdown: _markdown!, + theme: MarkdownThemeData( + textStyle: TextStyle( + fontSize: 14.0, + color: Theme.brightnessOf(context) == Brightness.dark ? Colors.white70 : Colors.black87, + ), + onLinkTap: (title, url) { + launchUrlString(url); + }, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/requirements.dart b/lib/pages/requirements.dart index 124d6a738..98c9885bb 100644 --- a/lib/pages/requirements.dart +++ b/lib/pages/requirements.dart @@ -3,8 +3,13 @@ import 'dart:io'; import 'package:dartx/dartx.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:swift_control/bluetooth/messages/notification.dart'; import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/requirements/multi.dart'; import 'package:swift_control/utils/requirements/platform.dart'; +import 'package:swift_control/widgets/menu.dart'; +import 'package:swift_control/widgets/small_progress_indicator.dart'; +import 'package:swift_control/widgets/title.dart'; import 'device.dart'; @@ -28,7 +33,7 @@ class _RequirementsPageState extends State with WidgetsBinding // call after first frame WidgetsBinding.instance.addPostFrameCallback((_) { if (!kIsWeb && Platform.isMacOS) { - // add more delay due tu CBManagerStateUnknown + // add more delay due to CBManagerStateUnknown Future.delayed(const Duration(seconds: 2), () { _reloadRequirements(); }); @@ -36,12 +41,6 @@ class _RequirementsPageState extends State with WidgetsBinding _reloadRequirements(); } }); - - connection.hasDevices.addListener(() { - if (connection.hasDevices.value) { - Navigator.push(context, MaterialPageRoute(builder: (c) => DevicePage())); - } - }); } @override @@ -60,72 +59,170 @@ class _RequirementsPageState extends State with WidgetsBinding @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text('SwiftControl'), backgroundColor: Theme.of(context).colorScheme.inversePrimary), - body: - _requirements.isEmpty - ? Center(child: CircularProgressIndicator()) - : Stepper( - currentStep: _currentStep, - connectorColor: WidgetStateProperty.resolveWith( - (Set states) => Theme.of(context).colorScheme.primary, + appBar: AppBar( + title: AppTitle(), + actions: buildMenuButtons(), + backgroundColor: Theme.brightnessOf(context) == Brightness.light + ? Theme.of(context).colorScheme.inversePrimary + : null, + ), + body: SingleChildScrollView( + child: Column( + spacing: 12, + children: [ + SizedBox(height: 12), + Row( + mainAxisSize: MainAxisSize.min, + spacing: 12, + children: [ + Image.asset('icon.png', width: 64, height: 64), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Welcome to BikeControl!', style: Theme.of(context).textTheme.titleMedium), + Container( + constraints: BoxConstraints(maxWidth: MediaQuery.sizeOf(context).width - 140), + + child: Text.rich( + TextSpan( + children: [ + TextSpan(text: 'Need help? Click on the '), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Icon(Icons.help_outline), + ), + ), + TextSpan(text: ' button on top and don\'t hesitate to contact us.'), + ], + ), + ), + ), + ], ), - onStepContinue: - _currentStep < _requirements.length - ? () { - setState(() { - _currentStep += 1; - }); + ], + ), + _requirements.isEmpty + ? Center(child: SmallProgressIndicator()) + : Card( + margin: EdgeInsets.symmetric(horizontal: 16, vertical: 16), + child: Stepper( + key: ObjectKey(_requirements.length), + physics: NeverScrollableScrollPhysics(), + currentStep: _currentStep, + connectorColor: WidgetStateProperty.resolveWith( + (Set states) => Theme.of(context).colorScheme.primary, + ), + onStepContinue: _currentStep < _requirements.length + ? () { + setState(() { + _currentStep += 1; + }); + } + : null, + onStepTapped: (step) { + if (_requirements[step].status && _requirements[step] is! TargetRequirement) { + return; + } + final hasEarlierIncomplete = + _requirements.indexWhere((req) => !req.status) != -1 && + _requirements.indexWhere((req) => !req.status) < step; + if (hasEarlierIncomplete) { + return; } - : null, - onStepTapped: (step) { - if (_requirements[step].status) { - return; - } - final hasEarlierIncomplete = _requirements.indexWhere((req) => !req.status) < step; - if (hasEarlierIncomplete) { - return; - } - setState(() { - _currentStep = step; - }); - }, - controlsBuilder: (context, details) => Container(), - steps: - _requirements - .mapIndexed( - (index, req) => Step( - title: Text(req.name), - content: - (index == _currentStep - ? req.build(context, () { - _reloadRequirements(); - }) - : null) ?? - ElevatedButton( - onPressed: req.status ? null : () => _callRequirement(req), - child: Text(req.name), - ), - state: req.status ? StepState.complete : StepState.indexed, - ), - ) - .toList(), - ), + setState(() { + _currentStep = step; + }); + }, + controlsBuilder: (context, details) => Container(), + steps: _requirements + .mapIndexed( + (index, req) => Step( + title: Text(req.name, style: TextStyle(fontWeight: FontWeight.w600)), + subtitle: + req.buildDescription() ?? (req.description != null ? Text(req.description!) : null), + content: Container( + padding: const EdgeInsets.only(top: 16.0), + alignment: Alignment.centerLeft, + child: + (index == _currentStep + ? req.build(context, () { + _reloadRequirements(); + }) + : null) ?? + ElevatedButton( + onPressed: req.status + ? null + : () => _callRequirement(req, context, () { + _reloadRequirements(); + }), + child: Text(req.name), + ), + ), + state: req.status ? StepState.complete : StepState.indexed, + ), + ) + .toList(), + ), + ), + ], + ), + ), ); } - void _callRequirement(PlatformRequirement req) { - req.call().then((_) { - _reloadRequirements(); - }); + void _callRequirement(PlatformRequirement req, BuildContext context, VoidCallback onUpdate) { + req + .call(context, onUpdate) + .then((_) { + return _reloadRequirements(); + }) + .catchError((e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error handling requirement "${req.name}": $e'), + ), + ); + }); } - void _reloadRequirements() { - getRequirements().then((req) { + void _reloadRequirements() async { + try { + final req = await getRequirements( + settings.getLastTarget()?.connectionType ?? ConnectionType.unknown, + ); _requirements = req; - _currentStep = req.indexWhere((req) => !req.status); - if (mounted) { - setState(() {}); + _currentStep = _currentStep >= _requirements.length ? 0 : _currentStep; + setState(() {}); + final unresolvedIndex = req.indexWhere((req) => !req.status); + if (unresolvedIndex != -1) { + _currentStep = unresolvedIndex; + } else if (mounted) { + String? currentPath; + navigatorKey.currentState?.popUntil((route) { + currentPath = route.settings.name; + return true; + }); + if (currentPath == '/') { + Navigator.push( + context, + MaterialPageRoute( + builder: (c) => DevicePage(), + settings: RouteSettings(name: '/device'), + ), + ); + } } - }); + } catch (e) { + connection.signalNotification(LogNotification('Error loading requirements: $e')); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error loading requirements: $e'), + ), + ); + _currentStep = 0; + _requirements = [ErrorRequirement('Error loading requirements: $e')]; + setState(() {}); + } } } diff --git a/lib/pages/scan.dart b/lib/pages/scan.dart deleted file mode 100644 index b7d01f0c9..000000000 --- a/lib/pages/scan.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_blue_plus_windows/flutter_blue_plus_windows.dart'; -import 'package:swift_control/main.dart'; -import 'package:swift_control/utils/ble.dart'; -import 'package:swift_control/widgets/small_progress_indicator.dart'; - -class ScanWidget extends StatefulWidget { - const ScanWidget({super.key}); - - @override - State createState() => _ScanWidgetState(); -} - -class _ScanWidgetState extends State { - bool _isScanning = false; - late StreamSubscription _isScanningSubscription; - - @override - void initState() { - super.initState(); - - connection.startScanning(); - - _isScanningSubscription = FlutterBluePlus.isScanning.listen((state) { - _isScanning = state; - if (mounted) { - setState(() {}); - } - }); - - // after the first frame - WidgetsBinding.instance.addPostFrameCallback((_) { - onScanPressed(); - }); - } - - @override - void dispose() { - _isScanningSubscription.cancel(); - super.dispose(); - } - - Future onScanPressed() async { - try { - await FlutterBluePlus.startScan( - timeout: const Duration(seconds: 30), - withServices: [BleUuid.ZWIFT_CUSTOM_SERVICE_UUID, BleUuid.ZWIFT_RIDE_CUSTOM_SERVICE_UUID], - webOptionalServices: kIsWeb ? [BleUuid.ZWIFT_CUSTOM_SERVICE_UUID] : [], - ); - } catch (e, backtrace) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Error: $e'), duration: const Duration(seconds: 5))); - print(e); - print("backtrace: $backtrace"); - } - if (mounted) { - setState(() {}); - } - } - - Future onStopPressed() async { - try { - FlutterBluePlus.stopScan(); - } catch (e, backtrace) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Error: $e'), duration: const Duration(seconds: 5))); - print(e); - print("backtrace: $backtrace"); - } - } - - Widget buildScanButton(BuildContext context) { - if (FlutterBluePlus.isScanningNow) { - return ElevatedButton(onPressed: onStopPressed, child: const Icon(Icons.stop)); - } else { - return ElevatedButton(onPressed: onScanPressed, child: const Text("SCAN")); - } - } - - @override - Widget build(BuildContext context) { - return Container( - constraints: BoxConstraints(minHeight: 200), - child: ListView( - padding: EdgeInsets.all(16), - shrinkWrap: true, - children: [if (_isScanning) SmallProgressIndicator() else buildScanButton(context)], - ), - ); - } -} diff --git a/lib/pages/touch_area.dart b/lib/pages/touch_area.dart new file mode 100644 index 000000000..c35a2962a --- /dev/null +++ b/lib/pages/touch_area.dart @@ -0,0 +1,466 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/requirements/multi.dart'; +import 'package:swift_control/widgets/button_widget.dart'; +import 'package:swift_control/widgets/keymap_explanation.dart'; +import 'package:swift_control/widgets/testbed.dart'; +import 'package:window_manager/window_manager.dart'; + +import '../utils/actions/base_actions.dart'; +import '../utils/keymap/keymap.dart'; + +final touchAreaSize = 42.0; + +class TouchAreaSetupPage extends StatefulWidget { + final KeyPair keyPair; + const TouchAreaSetupPage({super.key, required this.keyPair}); + + @override + State createState() => _TouchAreaSetupPageState(); +} + +class _TouchAreaSetupPageState extends State { + Uint8List? _backgroundImage; + final TransformationController _transformationController = TransformationController(); + + late Rect _imageRect; + + bool _showFaded = true; + + Future _pickScreenshot() async { + final picker = ImagePicker(); + final result = await picker.pickImage(source: ImageSource.gallery); + if (result != null) { + final image = File(result.path); + final Directory tempDir = await getTemporaryDirectory(); + final tempImage = File('${tempDir.path}/${actionHandler.supportedApp?.name ?? 'temp'}_screenshot.png'); + await image.copy(tempImage.path); + _backgroundImage = tempImage.readAsBytesSync(); + await _calculateBounds(); + } + } + + Future _calculateBounds() async { + if (_backgroundImage == null) return; + + // need to decode image to get its size so we can have a percentage mapping + final decodedImage = await decodeImageFromList(_backgroundImage!); + // calculate image rectangle in the current screen, given it's boxfit contain + final screenSize = MediaQuery.sizeOf(context); + final imageAspectRatio = decodedImage.width / decodedImage.height; + final screenAspectRatio = screenSize.width / screenSize.height; + if (imageAspectRatio > screenAspectRatio) { + // image is wider than screen + final width = screenSize.width; + final height = width / imageAspectRatio; + final top = (screenSize.height - height) / 2; + _imageRect = Rect.fromLTWH(0, top, width, height); + } else { + // image is taller than screen + final height = screenSize.height; + final width = height * imageAspectRatio; + final left = (screenSize.width - width) / 2; + _imageRect = Rect.fromLTWH(left, 0, width, height); + } + setState(() {}); + } + + void _saveAndClose() { + Navigator.of(context).pop(true); + } + + @override + void dispose() { + super.dispose(); + // Exit full screen + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); + // Reset orientation preferences to allow all orientations + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { + windowManager.setFullScreen(false); + } + } + + @override + void initState() { + super.initState(); + + // initialize _imageRect by using Flutter view size + final flutterView = WidgetsBinding.instance.platformDispatcher.views.first; + final size = flutterView.physicalSize / flutterView.devicePixelRatio; + _imageRect = Rect.fromLTWH(0, 0, size.width, size.height); + + // Force landscape orientation during keymap editing + SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]); + + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky, overlays: []); + if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) { + windowManager.setFullScreen(true); + } + getTemporaryDirectory().then((tempDir) async { + final tempImage = File('${tempDir.path}/${actionHandler.supportedApp?.name ?? 'temp'}_screenshot.png'); + if (tempImage.existsSync()) { + _backgroundImage = tempImage.readAsBytesSync(); + setState(() {}); + + // wait a bit until device rotation is done + SchedulerBinding.instance.addPostFrameCallback((_) { + _calculateBounds(); + }); + } + }); + } + + Widget _buildDraggableArea({ + required bool enableTouch, + required void Function(Offset newPosition) onPositionChanged, + required Color color, + required KeyPair keyPair, + }) { + // map the percentage position to the image rect + final relativeX = min(100.0, keyPair.touchPosition.dx) / 100.0; + final relativeY = min(100.0, keyPair.touchPosition.dy) / 100.0; + //print('Relative position: $relativeX, $relativeY'); + final flutterView = WidgetsBinding.instance.platformDispatcher.views.first; + + // figure out notch height for e.g. macOS. On Windows the display size is not available (0,0). + final differenceInHeight = (flutterView.display.size.height > 0 && !Platform.isIOS) + ? (flutterView.display.size.height - flutterView.physicalSize.height) / flutterView.devicePixelRatio + : 0.0; + + // Store the initial drag position to calculate drag distance + Offset? dragStartPosition; + + if (kDebugMode && false) { + print('Display Size: ${flutterView.display.size}'); + print('View size: ${flutterView.physicalSize}'); + print('Difference: $differenceInHeight'); + } + + //final isOnTheRightEdge = position.dx > (MediaQuery.sizeOf(context).width - 250); + + final iconSize = 40.0; + + final Offset position = Offset( + _imageRect.left + relativeX * _imageRect.width - iconSize / 2, + _imageRect.top + relativeY * _imageRect.height - differenceInHeight - iconSize / 2, + ); + + final icon = Container( + constraints: BoxConstraints(minHeight: iconSize, minWidth: iconSize), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (keyPair.buttons.singleOrNull?.color == null) + Container( + decoration: BoxDecoration( + color: color.withOpacity(0.4), + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 2), + ), + width: iconSize, + height: iconSize, + child: Icon( + keyPair.icon, + size: iconSize - 12, + shadows: [ + Shadow(color: Colors.white, offset: Offset(1, 1)), + Shadow(color: Colors.white, offset: Offset(-1, -1)), + Shadow(color: Colors.white, offset: Offset(-1, 1)), + Shadow(color: Colors.white, offset: Offset(-1, 1)), + Shadow(color: Colors.white, offset: Offset(1, -1)), + ], + ), + ), + KeypairExplanation(withKey: true, keyPair: keyPair), + ], + ), + ); + + return Positioned( + left: position.dx, + top: position.dy, + child: Tooltip( + message: 'Drag to reposition', + child: AnimatedOpacity( + opacity: _showFaded && widget.keyPair != keyPair ? 0.2 : 1.0, + duration: Duration(milliseconds: 300), + child: Draggable( + dragAnchorStrategy: (widget, context, position) { + final scale = _transformationController.value.getMaxScaleOnAxis(); + final RenderBox renderObject = context.findRenderObject() as RenderBox; + return renderObject.globalToLocal(position).scale(scale, scale); + }, + feedback: Material( + color: Colors.transparent, + child: icon, + ), + childWhenDragging: const SizedBox.shrink(), + onDragStarted: () { + // Capture the starting position to calculate drag distance later + dragStartPosition = position; + if (keyPair != widget.keyPair && _showFaded) { + setState(() { + _showFaded = false; + }); + } + }, + onDragEnd: (details) { + // Calculate drag distance to prevent accidental repositioning from clicks + // while allowing legitimate drags even with low velocity (e.g., when overlapping buttons) + final dragDistance = dragStartPosition != null + ? (details.offset - dragStartPosition!).distance + : double.infinity; + + // Only update position if dragged more than 5 pixels (prevents accidental clicks) + if (dragDistance > 5) { + final matrix = Matrix4.inverted(_transformationController.value); + final height = 0; + final sceneY = details.offset.dy - height; + final viewportPoint = MatrixUtils.transformPoint( + matrix, + Offset(details.offset.dx, sceneY) + Offset(iconSize / 2, differenceInHeight + iconSize / 2), + ); + setState(() => onPositionChanged(viewportPoint)); + } + }, + child: icon, + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: LayoutBuilder( + builder: (context, constraints) { + if (_backgroundImage == null && constraints.biggest != _imageRect.size) { + _imageRect = Rect.fromLTWH(0, 0, constraints.maxWidth, constraints.maxHeight); + } + final keyPairsToShow = + actionHandler.supportedApp?.keymap.keyPairs + .where((kp) => kp.touchPosition != Offset.zero && !kp.isSpecialKey) + .toList() ?? + []; + return InteractiveViewer( + transformationController: _transformationController, + child: Stack( + children: [ + if (_backgroundImage != null) + Positioned.fill( + child: Opacity( + opacity: 0.5, + child: Image.memory( + _backgroundImage!, + fit: BoxFit.contain, + ), + ), + ), + + // draw _imageRect for debugging + if (kDebugMode) + Positioned( + left: _imageRect.left, + top: _imageRect.top, + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.green, width: 2), + ), + child: SizedBox.fromSize(size: _imageRect.size), + ), + ), + + for (final keyPair in keyPairsToShow) + _buildDraggableArea( + enableTouch: true, + keyPair: keyPair, + onPositionChanged: (newPos) { + // convert to percentage + final relativeX = ((newPos.dx - _imageRect.left) / _imageRect.width).clamp(0.0, 1.0); + final relativeY = ((newPos.dy - _imageRect.top) / _imageRect.height).clamp(0.0, 1.0); + keyPair.touchPosition = Offset(relativeX * 100.0, relativeY * 100.0); + setState(() {}); + }, + color: Colors.red, + ), + + Positioned.fill(child: Testbed()), + + if (_backgroundImage == null) + Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + IgnorePointer( + child: Text( + '''1. Create an in-game screenshot of your app (e.g. within MyWhoosh) in landscape orientation +2. Load the screenshot with the button below +3. The app is automatically set to landscape orientation for accurate mapping +4. Press a button on your Click device to create a touch area +5. Drag the touch areas to the desired position on the screenshot +6. Save and close this screen''', + ), + ), + ElevatedButton( + onPressed: () { + _pickScreenshot(); + }, + child: Text('Load in-game screenshot for placement'), + ), + ], + ), + ), + ), + + Positioned( + top: 40, + right: 20, + child: Row( + spacing: 8, + children: [ + ElevatedButton.icon( + onPressed: _saveAndClose, + icon: const Icon(Icons.save), + label: const Text("Save"), + ), + PopupMenuButton( + itemBuilder: (c) => [ + PopupMenuItem( + child: Text('Choose another screenshot'), + onTap: () { + _pickScreenshot(); + }, + ), + PopupMenuItem( + child: Text('Reset'), + onTap: () { + _backgroundImage = null; + + actionHandler.supportedApp?.keymap.reset(); + setState(() {}); + }, + ), + ], + icon: Icon(Icons.more_vert), + ), + ], + ), + ), + ], + ), + ); + }, + ), + ); + } +} + +class KeypairExplanation extends StatelessWidget { + final bool withKey; + final KeyPair keyPair; + + const KeypairExplanation({super.key, required this.keyPair, this.withKey = false}); + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 4, + runSpacing: 4, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + if (withKey) + Row( + children: keyPair.buttons.map((b) => ButtonWidget(button: b, big: true)).toList(), + ) + else + Icon(keyPair.icon), + if (keyPair.inGameAction != null && + ((whooshLink.isCompatible(settings.getLastTarget() ?? Target.thisDevice) && + settings.getMyWhooshLinkEnabled()) || + (settings.getTrainerApp()?.supportsZwiftEmulation == true && settings.getZwiftEmulatorEnabled()))) + _KeyWidget( + label: [ + keyPair.inGameAction.toString().split('.').last, + if (keyPair.inGameActionValue != null) ': ${keyPair.inGameActionValue}', + ].joinToString(separator: ''), + ) + else if (keyPair.isSpecialKey && actionHandler.supportedModes.contains(SupportedMode.media)) + _KeyWidget( + label: switch (keyPair.physicalKey) { + PhysicalKeyboardKey.mediaPlayPause => 'Play/Pause', + PhysicalKeyboardKey.mediaStop => 'Stop', + PhysicalKeyboardKey.mediaTrackPrevious => 'Previous', + PhysicalKeyboardKey.mediaTrackNext => 'Next', + PhysicalKeyboardKey.audioVolumeUp => 'Volume Up', + PhysicalKeyboardKey.audioVolumeDown => 'Volume Down', + _ => 'Unknown', + }, + ) + else if (keyPair.physicalKey != null && actionHandler.supportedModes.contains(SupportedMode.keyboard)) ...[ + _KeyWidget( + label: [ + ...keyPair.modifiers.map((e) => e.name.replaceAll('Modifier', '')), + keyPair.logicalKey?.keyLabel ?? 'Unknown', + ].joinToString(separator: '+'), + ), + if (keyPair.isLongPress) Text('long\npress', style: TextStyle(fontSize: 10)), + ] else ...[ + if (!withKey && keyPair.touchPosition != Offset.zero) + _KeyWidget(label: 'X:${keyPair.touchPosition.dx.toInt()}, Y:${keyPair.touchPosition.dy.toInt()}'), + if (keyPair.isLongPress) Text('long\npress', style: TextStyle(fontSize: 10)), + ], + ], + ); + } +} + +class _KeyWidget extends StatelessWidget { + final String label; + const _KeyWidget({super.key, required this.label}); + + @override + Widget build(BuildContext context) { + return IntrinsicWidth( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), + constraints: BoxConstraints(minWidth: 30), + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).colorScheme.primary), + borderRadius: BorderRadius.circular(4), + color: Theme.of(context).colorScheme.primaryContainer, + ), + child: Center( + child: Text( + label.splitByUpperCase(), + style: TextStyle( + fontFamily: screenshotMode ? null : 'monospace', + fontSize: 12, + color: Theme.of(context).colorScheme.onPrimaryContainer, + ), + ), + ), + ), + ); + } +} diff --git a/lib/protocol/zwift.proto b/lib/protocol/zwift.proto deleted file mode 100644 index 709db86fc..000000000 --- a/lib/protocol/zwift.proto +++ /dev/null @@ -1,126 +0,0 @@ -syntax = "proto2"; -package de.jonasbark; - -// derived from qdomyos-zwift project - -//---------------- Zwift Play messages - -enum PlayButtonStatus { - ON = 0; - OFF = 1; -} -// The command code prepending this message is 0x07 -message PlayKeyPadStatus { - optional PlayButtonStatus RightPad = 1; - optional PlayButtonStatus Button_Y_Up = 2; - optional PlayButtonStatus Button_Z_Left = 3; - optional PlayButtonStatus Button_A_Right = 4; - optional PlayButtonStatus Button_B_Down = 5; - optional PlayButtonStatus Button_On = 6; - optional PlayButtonStatus Button_Shift = 7; - optional sint32 Analog_LR = 8; - optional sint32 Analog_UD = 9; -} - - -message PlayCommandParameters { - optional uint32 param1 = 1; - optional uint32 param2 = 2; - optional uint32 HapticPattern = 3; -} - -message PlayCommandContents { - optional PlayCommandParameters CommandParameters = 1; -} - -// The command code prepending this message is 0x12 -// This is sent to the control point to configure and make the controller vibrate -message PlayCommand { - optional PlayCommandContents CommandContents = 2; -} - -// The command code prepending this message is 0x19 -// This is sent periodically when there are no button presses -message Idle { - optional uint32 Unknown2 = 2; -} - -//----------------- Zwift Ride messages -enum RideButtonMask { - LEFT_BTN = 0x00001; - UP_BTN = 0x00002; - RIGHT_BTN = 0x00004; - DOWN_BTN = 0x00008; - A_BTN = 0x00010; - B_BTN = 0x00020; - Y_BTN = 0x00040; - - Z_BTN = 0x00100; - SHFT_UP_L_BTN = 0x00200; - SHFT_DN_L_BTN = 0x00400; - POWERUP_L_BTN = 0x00800; - ONOFF_L_BTN = 0x01000; - SHFT_UP_R_BTN = 0x02000; - SHFT_DN_R_BTN = 0x04000; - - POWERUP_R_BTN = 0x10000; - ONOFF_R_BTN = 0x20000; -} - -enum RideAnalogLocation { - LEFT = 0; - RIGHT = 1; - UP = 2; - DOWN = 3; -} - -message RideAnalogKeyPress { - optional RideAnalogLocation Location = 1; - optional sint32 AnalogValue = 2; -} - -message RideAnalogKeyGroup { - repeated RideAnalogKeyPress GroupStatus = 1; -} - -// The command code prepending this message is 0x23 -message RideKeyPadStatus { - optional uint32 ButtonMap = 1; - optional RideAnalogKeyGroup AnalogButtons = 2; -} - -//------------------ Zwift Click messages -// The command code prepending this message is 0x37 -message ClickKeyPadStatus { - optional PlayButtonStatus Button_Plus = 1; - optional PlayButtonStatus Button_Minus = 2; -} - -//------------------ Device Information requested after connection -// The command code prepending this message is 0x3c -message DeviceInformationContent { - optional uint32 Unknown1 = 1; - repeated uint32 SoftwareVersion = 2; - optional string DeviceName = 3; - optional uint32 Unknown4 = 4; - optional uint32 Unknown5 =5; - optional string SerialNumber = 6; - optional string HardwareVersion = 7; - repeated uint32 ReplyData = 8; - optional uint32 Unknown9 = 9; - optional uint32 Unknown10 = 10; - optional uint32 Unknown13 = 13; -} - -message SubContent { - optional DeviceInformationContent Content = 1; - optional uint32 Unknown2 = 2; - optional uint32 Unknown4 = 4; - optional uint32 Unknown5 = 5; - optional uint32 Unknown6 = 6; -} - -message DeviceInformation { - optional uint32 InformationId = 1; - optional SubContent SubContent = 2; -} diff --git a/lib/theme.dart b/lib/theme.dart index 931cbf4bc..624a69e2e 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -7,6 +7,9 @@ abstract final class AppTheme { static ThemeData light = FlexThemeData.light( // Using FlexColorScheme built-in FlexScheme enum based colors scheme: FlexScheme.redM3, + primary: Color(0xFF0E74B7), + primaryContainer: Color(0x7C0E9297), + onPrimaryContainer: Colors.black, // Component theme configurations for light mode. subThemesData: const FlexSubThemesData( interactionEffects: true, @@ -23,27 +26,33 @@ abstract final class AppTheme { ); // The FlexColorScheme defined dark mode ThemeData. - static ThemeData dark = FlexThemeData.dark( - // Using FlexColorScheme built-in FlexScheme enum based colors. - scheme: FlexScheme.redM3, - // Component theme configurations for dark mode. - subThemesData: const FlexSubThemesData( - interactionEffects: true, - tintedDisabledControls: true, - blendOnColors: true, - useM2StyleDividerInM3: true, - inputDecoratorIsFilled: true, - inputDecoratorBorderType: FlexInputBorderType.outline, - alignedDropdown: true, - navigationRailUseIndicator: true, - ), - // Direct ThemeData properties. - visualDensity: FlexColorScheme.comfortablePlatformDensity, - cupertinoOverrideTheme: const CupertinoThemeData(applyThemeToAll: true), - ).copyWith( - scaffoldBackgroundColor: Color(0xff0b1623), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom(backgroundColor: Colors.red, foregroundColor: Colors.white), - ), - ); + static ThemeData dark = + FlexThemeData.dark( + // Using FlexColorScheme built-in FlexScheme enum based colors. + scheme: FlexScheme.redM3, + primary: Color(0xFF0E74B7), + primaryContainer: Color(0x7C0E9297), + onPrimaryContainer: Colors.white, + // Component theme configurations for dark mode. + subThemesData: const FlexSubThemesData( + interactionEffects: true, + tintedDisabledControls: true, + blendOnColors: true, + useM2StyleDividerInM3: true, + inputDecoratorIsFilled: true, + inputDecoratorBorderType: FlexInputBorderType.outline, + alignedDropdown: true, + navigationRailUseIndicator: true, + ), + // Direct ThemeData properties. + visualDensity: FlexColorScheme.comfortablePlatformDensity, + cupertinoOverrideTheme: const CupertinoThemeData(applyThemeToAll: true), + ).copyWith( + appBarTheme: AppBarTheme( + backgroundColor: Color(0xFF0E74B7), + ), + cardTheme: CardThemeData( + color: Colors.white24, + ), + ); } diff --git a/lib/utils/actions/android.dart b/lib/utils/actions/android.dart index b34df2bc5..a11c45d75 100644 --- a/lib/utils/actions/android.dart +++ b/lib/utils/actions/android.dart @@ -1,50 +1,96 @@ -import 'dart:ui'; - import 'package:accessibility/accessibility.dart'; +import 'package:dartx/dartx.dart'; +import 'package:flutter/services.dart'; +import 'package:swift_control/bluetooth/devices/hid/hid_device.dart'; import 'package:swift_control/main.dart'; import 'package:swift_control/utils/actions/base_actions.dart'; -import 'package:swift_control/utils/keymap/keymap.dart'; +import 'package:swift_control/utils/keymap/apps/custom_app.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/widgets/keymap_explanation.dart'; -class AndroidActions extends BaseActions { - static const MYWHOOSH_APP_PACKAGE = "com.mywhoosh.whooshgame"; - static const TRAININGPEAKS_APP_PACKAGE = "com.indieVelo.client"; +import '../keymap/apps/supported_app.dart'; +import '../single_line_exception.dart'; +class AndroidActions extends BaseActions { WindowEvent? windowInfo; + final accessibilityHandler = Accessibility(); + + AndroidActions({super.supportedModes = const [SupportedMode.touch, SupportedMode.media]}); + @override - void init(Keymap? keymap) { + void init(SupportedApp? supportedApp) { + super.init(supportedApp); streamEvents().listen((windowEvent) { - windowInfo = windowEvent; + if (supportedApp != null) { + windowInfo = windowEvent; + } + }); + + hidKeyPressed().listen((keyPressed) { + if (supportedApp is CustomApp) { + final button = supportedApp.keymap.getOrAddButton(keyPressed, () => ControllerButton(keyPressed)); + + final hidDevice = HidDevice('HID Device'); + var availableDevice = connection.controllerDevices.firstOrNullWhere((e) => e.name == hidDevice.name); + if (availableDevice == null) { + connection.addDevices([hidDevice]); + availableDevice = hidDevice; + } + availableDevice.handleButtonsClicked([button]); + availableDevice.handleButtonsClicked([]); + } }); } @override - void decreaseGear() { - if (windowInfo == null) { - throw Exception("Decrease gear: No window info"); - } else { - final point = switch (windowInfo!.packageName) { - MYWHOOSH_APP_PACKAGE => Offset(windowInfo!.windowWidth * 0.80, windowInfo!.windowHeight * 0.94), - TRAININGPEAKS_APP_PACKAGE => Offset(windowInfo!.windowWidth / 2 * 1.15, windowInfo!.windowHeight * 0.74), - _ => throw UnimplementedError("Decreasing gear not supported for ${windowInfo!.packageName}"), - }; - - accessibilityHandler.performTouch(point.dx, point.dy); + Future performAction(ControllerButton button, {bool isKeyDown = true, bool isKeyUp = false}) async { + if (supportedApp == null) { + return Error("Could not perform ${button.name.splitByUpperCase()}: No keymap set"); } - } - @override - void increaseGear() { - if (windowInfo == null) { - throw Exception("Increasing gear: No window info"); - } else { - final point = switch (windowInfo!.packageName) { - MYWHOOSH_APP_PACKAGE => Offset(windowInfo!.windowWidth * 0.98, windowInfo!.windowHeight * 0.94), - TRAININGPEAKS_APP_PACKAGE => Offset(windowInfo!.windowWidth / 2 * 1.32, windowInfo!.windowHeight * 0.74), - _ => throw UnimplementedError("Increasing gear not supported for ${windowInfo!.packageName}"), - }; - - accessibilityHandler.performTouch(point.dx, point.dy); + final keyPair = supportedApp!.keymap.getKeyPair(button); + + if (keyPair == null) { + return Error("Could not perform ${button.name.splitByUpperCase()}: No action assigned"); + } else if (keyPair.hasNoAction) { + return Error('No action assigned for ${button.toString().splitByUpperCase()}'); + } + + final directConnectHandled = await handleDirectConnect(keyPair); + + if (directConnectHandled != null) { + return directConnectHandled; + } else if (keyPair.isSpecialKey) { + await accessibilityHandler.controlMedia(switch (keyPair.physicalKey) { + PhysicalKeyboardKey.mediaTrackNext => MediaAction.next, + PhysicalKeyboardKey.mediaPlayPause => MediaAction.playPause, + PhysicalKeyboardKey.audioVolumeUp => MediaAction.volumeUp, + PhysicalKeyboardKey.audioVolumeDown => MediaAction.volumeDown, + _ => throw SingleLineException("No action for key: ${keyPair.physicalKey}"), + }); + return Success("Key pressed: ${keyPair.toString()}"); } + + final point = await resolveTouchPosition(keyPair: keyPair, windowInfo: windowInfo); + if (point != Offset.zero) { + try { + await accessibilityHandler.performTouch(point.dx, point.dy, isKeyDown: isKeyDown, isKeyUp: isKeyUp); + } on PlatformException catch (e) { + return Error("Accessibility Service not working. Follow instructions at https://dontkillmyapp.com/"); + } + return Success( + "Touch performed at: ${point.dx.toInt()}, ${point.dy.toInt()} -> ${isKeyDown && isKeyUp + ? "click" + : isKeyDown + ? "down" + : "up"}", + ); + } + return Error('No action assigned for ${button.toString().splitByUpperCase()}'); + } + + void ignoreHidDevices() { + accessibilityHandler.ignoreHidDevices(); } } diff --git a/lib/utils/actions/base_actions.dart b/lib/utils/actions/base_actions.dart index 0c452fd67..fe9b33ae5 100644 --- a/lib/utils/actions/base_actions.dart +++ b/lib/utils/actions/base_actions.dart @@ -1,55 +1,134 @@ import 'dart:io'; +import 'dart:math'; +import 'package:accessibility/accessibility.dart'; +import 'package:dartx/dartx.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:screen_retriever/screen_retriever.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_emulator.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/actions/android.dart'; +import 'package:swift_control/utils/actions/desktop.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/utils/keymap/keymap.dart'; -import '../keymap/keymap.dart'; -import 'android.dart'; -import 'desktop.dart'; +import '../keymap/apps/supported_app.dart'; -abstract class BaseActions { - Keymap? get keymap => null; +enum SupportedMode { keyboard, touch, media } - void init(Keymap? keymap) {} - void increaseGear(); - void decreaseGear(); +sealed class ActionResult { + final String message; + const ActionResult(this.message); } -class StubActions extends BaseActions { - @override - void decreaseGear() { - print('Decrease gear'); - } +class Success extends ActionResult { + const Success(super.message); +} - @override - void increaseGear() { - print('Increase gear'); - } +class Error extends ActionResult { + const Error(super.message); } -class ActionHandler { - late BaseActions actions; +abstract class BaseActions { + final List supportedModes; + + SupportedApp? supportedApp; + + BaseActions({required this.supportedModes}); + + void init(SupportedApp? supportedApp) { + this.supportedApp = supportedApp; + print('Supported app: ${supportedApp?.name ?? "None"}'); + + if (supportedApp != null) { + final allButtons = connection.devices.map((e) => e.availableButtons).flatten().distinct(); - ActionHandler() { - if (kIsWeb) { - actions = StubActions(); - } else if (Platform.isAndroid) { - actions = AndroidActions(); - } else { - actions = DesktopActions(); + final newButtons = allButtons.filter( + (button) => supportedApp.keymap.getKeyPair(button) == null, + ); + for (final button in newButtons) { + supportedApp.keymap.addKeyPair( + KeyPair( + touchPosition: Offset.zero, + buttons: [button], + physicalKey: null, + logicalKey: null, + isLongPress: false, + ), + ); + } } } - Keymap? get keymap => actions.keymap; + Future resolveTouchPosition({required KeyPair keyPair, required WindowEvent? windowInfo}) async { + if (keyPair.touchPosition != Offset.zero) { + // convert relative position to absolute position based on window info - void init(Keymap? keymap) { - actions.init(keymap); + // TODO support multiple screens + final Size displaySize; + final double devicePixelRatio; + if (Platform.isWindows) { + // TODO remove once https://github.com/flutter/flutter/pull/164460 is available in stable + final display = await screenRetriever.getPrimaryDisplay(); + displaySize = display.size; + devicePixelRatio = 1.0; + } else { + final display = WidgetsBinding.instance.platformDispatcher.views.first.display; + displaySize = display.size; + devicePixelRatio = display.devicePixelRatio; + } + + late final Size physicalSize; + if (this is AndroidActions) { + if (windowInfo != null && windowInfo.packageName != 'de.jonasbark.swiftcontrol') { + // a trainer app is in foreground, so use the always assume landscape + physicalSize = Size(max(displaySize.width, displaySize.height), min(displaySize.width, displaySize.height)); + } else { + // display size is already in physical pixels + physicalSize = displaySize; + } + } else if (this is DesktopActions) { + // display size is in logical pixels, convert to physical pixels + // TODO on macOS the notch is included here, but it's not part of the usable screen area, so we should exclude it + physicalSize = displaySize / devicePixelRatio; + } else { + physicalSize = displaySize; + } + + final x = (keyPair.touchPosition.dx / 100.0) * physicalSize.width; + final y = (keyPair.touchPosition.dy / 100.0) * physicalSize.height; + + if (kDebugMode) { + print("Screen size: $physicalSize vs $displaySize => Touch at: $x, $y"); + } + return Offset(x, y); + } + return Offset.zero; } - void increaseGear() { - actions.increaseGear(); + Future performAction(ControllerButton action, {bool isKeyDown = true, bool isKeyUp = false}); + + Future? handleDirectConnect(KeyPair keyPair) { + if (keyPair.inGameAction != null) { + if (whooshLink.isConnected.value) { + return Future.value(whooshLink.sendAction(keyPair.inGameAction!, keyPair.inGameActionValue)); + } else if (zwiftEmulator.isConnected.value) { + return zwiftEmulator.sendAction(keyPair.inGameAction!, keyPair.inGameActionValue); + } + } + return null; } +} + +class StubActions extends BaseActions { + StubActions({super.supportedModes = const []}); - void decreaseGear() { - actions.decreaseGear(); + final List performedActions = []; + + @override + Future performAction(ControllerButton action, {bool isKeyDown = true, bool isKeyUp = false}) { + performedActions.add(action); + return Future.value(Success(action.name)); } } diff --git a/lib/utils/actions/desktop.dart b/lib/utils/actions/desktop.dart index cdafca0cb..1d8c60a04 100644 --- a/lib/utils/actions/desktop.dart +++ b/lib/utils/actions/desktop.dart @@ -1,32 +1,72 @@ +import 'dart:ui'; + import 'package:keypress_simulator/keypress_simulator.dart'; import 'package:swift_control/utils/actions/base_actions.dart'; - -import '../keymap/keymap.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/widgets/keymap_explanation.dart'; class DesktopActions extends BaseActions { - Keymap? _keymap; + DesktopActions({super.supportedModes = const [SupportedMode.keyboard, SupportedMode.touch, SupportedMode.media]}); - @override - Keymap? get keymap => _keymap; + // Track keys that are currently held down in long press mode @override - void init(Keymap? keymap) { - _keymap = keymap; - } + Future performAction(ControllerButton action, {bool isKeyDown = true, bool isKeyUp = false}) async { + if (supportedApp == null) { + return Error('Supported app is not set'); + } - @override - void decreaseGear() { - if (keymap == null) { - throw Exception('Keymap is not set'); + final keyPair = supportedApp!.keymap.getKeyPair(action); + if (keyPair == null) { + return Error('Keymap entry not found for action: ${action.toString().splitByUpperCase()}'); + } else if (keyPair.hasNoAction) { + return Error('No action assigned for ${action.toString().splitByUpperCase()}'); + } + + final directConnectHandled = await handleDirectConnect(keyPair); + + if (directConnectHandled != null) { + return directConnectHandled; + } else if (keyPair.physicalKey != null) { + if (isKeyDown && isKeyUp) { + await keyPressSimulator.simulateKeyDown(keyPair.physicalKey, keyPair.modifiers); + await keyPressSimulator.simulateKeyUp(keyPair.physicalKey, keyPair.modifiers); + return Success('Key clicked: $keyPair'); + } else if (isKeyDown) { + await keyPressSimulator.simulateKeyDown(keyPair.physicalKey, keyPair.modifiers); + return Success('Key pressed: $keyPair'); + } else { + await keyPressSimulator.simulateKeyUp(keyPair.physicalKey, keyPair.modifiers); + return Success('Key released: $keyPair'); + } + } else { + final point = await resolveTouchPosition(keyPair: keyPair, windowInfo: null); + if (point != Offset.zero) { + if (isKeyDown && isKeyUp) { + await keyPressSimulator.simulateMouseClickDown(point); + // slight move to register clicks on some apps, see issue #116 + await keyPressSimulator.simulateMouseClickUp(point); + return Success('Mouse clicked at: ${point.dx.toInt()} ${point.dy.toInt()}'); + } else if (isKeyDown) { + await keyPressSimulator.simulateMouseClickDown(point); + return Success('Mouse down at: ${point.dx.toInt()} ${point.dy.toInt()}'); + } else { + await keyPressSimulator.simulateMouseClickUp(point); + return Success('Mouse up at: ${point.dx.toInt()} ${point.dy.toInt()}'); + } + } else { + return Error('No action assigned for ${action.toString().splitByUpperCase()}'); + } } - keyPressSimulator.simulateKeyDown(_keymap!.decrease); } - @override - void increaseGear() { - if (keymap == null) { - throw Exception('Keymap is not set'); + // Release all held keys (useful for cleanup) + Future releaseAllHeldKeys(List list) async { + for (final action in list) { + final keyPair = supportedApp?.keymap.getKeyPair(action); + if (keyPair?.physicalKey != null) { + await keyPressSimulator.simulateKeyUp(keyPair!.physicalKey); + } } - keyPressSimulator.simulateKeyDown(_keymap!.increase); } } diff --git a/lib/utils/actions/remote.dart b/lib/utils/actions/remote.dart new file mode 100644 index 000000000..daed2b0d5 --- /dev/null +++ b/lib/utils/actions/remote.dart @@ -0,0 +1,91 @@ +import 'dart:ui'; + +import 'package:accessibility/accessibility.dart'; +import 'package:bluetooth_low_energy/bluetooth_low_energy.dart'; +import 'package:flutter/foundation.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_click.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/actions/base_actions.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/utils/keymap/keymap.dart'; +import 'package:swift_control/widgets/keymap_explanation.dart'; +import 'package:universal_ble/universal_ble.dart'; + +import '../requirements/remote.dart'; + +class RemoteActions extends BaseActions { + RemoteActions({super.supportedModes = const [SupportedMode.touch]}); + + @override + Future performAction(ControllerButton action, {bool isKeyDown = true, bool isKeyUp = false}) async { + if (supportedApp == null) { + return Error('Supported app is not set'); + } + + final keyPair = supportedApp!.keymap.getKeyPair(action); + if (keyPair == null) { + return Error('Keymap entry not found for action: ${action.toString().splitByUpperCase()}'); + } else if (keyPair.hasNoAction) { + return Error('No action assigned for ${action.toString().splitByUpperCase()}'); + } + + final directConnectHandled = await handleDirectConnect(keyPair); + + if (directConnectHandled != null) { + return directConnectHandled; + } else if (!(actionHandler as RemoteActions).isConnected) { + return Error('Not connected to a ${settings.getLastTarget()?.name ?? 'remote'} device'); + } + + if (keyPair.physicalKey != null && keyPair.touchPosition == Offset.zero) { + return Error('Physical key actions are not supported, yet'); + } else { + final point = await resolveTouchPosition(keyPair: keyPair, windowInfo: null); + final point2 = point; //Offset(100, 99.0); + await sendAbsMouseReport(0, point2.dx.toInt(), point2.dy.toInt()); + await sendAbsMouseReport(1, point2.dx.toInt(), point2.dy.toInt()); + await sendAbsMouseReport(0, point2.dx.toInt(), point2.dy.toInt()); + + return Success('Mouse clicked at: ${point2.dx.toInt()} ${point2.dy.toInt()}'); + } + } + + @override + Future resolveTouchPosition({required KeyPair keyPair, required WindowEvent? windowInfo}) async { + // for remote actions we use the relative position only + return keyPair.touchPosition; + } + + Uint8List absMouseReport(int buttons3bit, int x, int y) { + final b = buttons3bit & 0x07; + final xi = x.clamp(0, 100); + final yi = y.clamp(0, 100); + return Uint8List.fromList([b, xi, yi]); + } + + // Send a relative mouse move + button state as 3-byte report: [buttons, dx, dy] + Future sendAbsMouseReport(int buttons, int dx, int dy) async { + final bytes = absMouseReport(buttons, dx, dy); + if (kDebugMode) { + print('Preparing to send abs mouse report: buttons=$buttons, dx=$dx, dy=$dy'); + print('Sending abs mouse report: ${bytes.map((e) => e.toRadixString(16).padLeft(2, '0'))}'); + } + + await peripheralManager.notifyCharacteristic(connectedCentral!, connectedCharacteristic!, value: bytes); + + // we don't want to overwhelm the target device + await Future.delayed(Duration(milliseconds: 10)); + } + + Central? connectedCentral; + GATTCharacteristic? connectedCharacteristic; + + void setConnectedCentral(Central? central, GATTCharacteristic? gattCharacteristic) { + connectedCentral = central; + connectedCharacteristic = gattCharacteristic; + + connection.signalChange(ZwiftClick(BleDevice(deviceId: 'deviceId', name: 'name'))); + } + + bool get isConnected => connectedCentral != null; +} diff --git a/lib/utils/ble.dart b/lib/utils/ble.dart deleted file mode 100644 index 5344a56b9..000000000 --- a/lib/utils/ble.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'dart:typed_data'; - -import 'package:flutter_blue_plus/flutter_blue_plus.dart'; - -class BleUuid { - static final ZWIFT_CUSTOM_SERVICE_UUID = Guid("00000001-19CA-4651-86E5-FA29DCDD09D1"); - static final ZWIFT_RIDE_CUSTOM_SERVICE_UUID = Guid("0000fc82-0000-1000-8000-00805f9b34fb"); - static final ZWIFT_ASYNC_CHARACTERISTIC_UUID = Guid("00000002-19CA-4651-86E5-FA29DCDD09D1"); - static final ZWIFT_SYNC_RX_CHARACTERISTIC_UUID = Guid("00000003-19CA-4651-86E5-FA29DCDD09D1"); - static final ZWIFT_SYNC_TX_CHARACTERISTIC_UUID = Guid("00000004-19CA-4651-86E5-FA29DCDD09D1"); -} - -class Constants { - static const ZWIFT_MANUFACTURER_ID = 2378; // Zwift, Inc - - // Zwift Play = RC1 - static const RC1_LEFT_SIDE = 0x03; - static const RC1_RIGHT_SIDE = 0x02; - - // Zwift Click = BC1 - static const BC1 = 0x09; - - static final RIDE_ON = Uint8List.fromList([0x52, 0x69, 0x64, 0x65, 0x4f, 0x6e]); - - // these don't actually seem to matter, its just the header has to be 7 bytes RIDEON + 2 - static final REQUEST_START = Uint8List.fromList([0, 9]); //byteArrayOf(1, 2) - static final RESPONSE_START_CLICK = Uint8List.fromList([1, 3]); // from device - static final RESPONSE_START_PLAY = Uint8List.fromList([1, 4]); // from device - - // Message types received from device - static const CONTROLLER_NOTIFICATION_MESSAGE_TYPE = 07; - static const EMPTY_MESSAGE_TYPE = 21; - static const BATTERY_LEVEL_TYPE = 25; - - // not figured out the protobuf type this really is, the content is just two varints. - static const int CLICK_NOTIFICATION_MESSAGE_TYPE = 55; - static const int PLAY_NOTIFICATION_MESSAGE_TYPE = 7; - static const int RIDE_NOTIFICATION_MESSAGE_TYPE = 35; - - // see this if connected to Core then Zwift connects to it. just one byte - static const DISCONNECT_MESSAGE_TYPE = 0xFE; -} - -enum DeviceType { - click, - ride, - playLeft, - playRight; - - @override - String toString() { - return super.toString().split('.').last; - } - - // add constructor - static DeviceType? fromManufacturerData(int data) { - switch (data) { - case Constants.BC1: - return DeviceType.click; - case Constants.RC1_LEFT_SIDE: - return DeviceType.playLeft; - case Constants.RC1_RIGHT_SIDE: - return DeviceType.playRight; - } - return null; - } -} diff --git a/lib/utils/connection.dart b/lib/utils/connection.dart deleted file mode 100644 index 0836cbbf1..000000000 --- a/lib/utils/connection.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:dartx/dartx.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_blue_plus/flutter_blue_plus.dart'; -import 'package:swift_control/main.dart'; -import 'package:swift_control/utils/devices/ble_device.dart'; -import 'package:swift_control/utils/requirements/android.dart'; - -import 'messages/notification.dart'; - -class Connection { - final devices = []; - var androidNotificationsSetup = false; - - final Map> _streamSubscriptions = {}; - final StreamController _actionStreams = StreamController.broadcast(); - Stream get actionStream => _actionStreams.stream; - - final Map> _connectionSubscriptions = {}; - final StreamController _connectionStreams = StreamController.broadcast(); - Stream get connectionStream => _connectionStreams.stream; - - final ValueNotifier hasDevices = ValueNotifier(false); - late StreamSubscription> _scanResultsSubscription; - - void startScanning() { - _scanResultsSubscription = FlutterBluePlus.scanResults.listen( - (results) { - final scanResults = results.mapNotNull(BleDevice.fromScanResult).toList(); - _addDevices(scanResults); - }, - onError: (e) { - _actionStreams.add(LogNotification(e.toString())); - }, - ); - } - - void _addDevices(List dev) { - final newDevices = dev.where((device) => !devices.contains(device)).toList(); - devices.addAll(newDevices); - - for (final device in newDevices) { - _connect(device).then((_) {}); - } - hasDevices.value = devices.isNotEmpty; - if (devices.isNotEmpty && !androidNotificationsSetup && !kIsWeb && Platform.isAndroid) { - androidNotificationsSetup = true; - actionHandler.init(null); - NotificationRequirement.setup().catchError((e) { - _actionStreams.add(LogNotification(e.toString())); - }); - } - } - - Future _connect(BleDevice bleDevice) async { - try { - await bleDevice.connect(); - - final actionSubscription = bleDevice.actionStream.listen((data) { - _actionStreams.add(data); - }); - _streamSubscriptions[bleDevice] = actionSubscription; - - final connectionStateSubscription = bleDevice.device.connectionState.listen((state) async { - _connectionStreams.add(bleDevice); - }); - _connectionSubscriptions[bleDevice] = connectionStateSubscription; - } catch (e, backtrace) { - if (e is FlutterBluePlusException && e.code == FbpErrorCode.connectionCanceled.index) { - // ignore connections canceled by the user - } else { - _actionStreams.add(LogNotification(e.toString())); - if (kDebugMode) { - print(e); - print("backtrace: $backtrace"); - } - } - } - } - - void reset() { - FlutterBluePlus.stopScan(); - for (var device in devices) { - device.device.disconnect(); - } - devices.clear(); - } -} diff --git a/lib/utils/crypto/encryption_utils.dart b/lib/utils/crypto/encryption_utils.dart deleted file mode 100644 index afde94bc1..000000000 --- a/lib/utils/crypto/encryption_utils.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'dart:typed_data'; - -import 'package:pointycastle/digests/sha256.dart'; -import 'package:pointycastle/ecc/api.dart'; -import 'package:pointycastle/key_derivators/api.dart'; -import 'package:pointycastle/key_derivators/hkdf.dart'; - -class EncryptionUtils { - static const int KEY_LENGTH = 32; - static const int HKDF_LENGTH = 36; - static const int MAC_LENGTH = 4; - - static Uint8List publicKeyToByteArray(ECPublicKey ecPublicKey) { - final affineX = ecPublicKey.Q!.x!.toBigInteger(); - final affineY = ecPublicKey.Q!.y!.toBigInteger(); - final affineXUnsigned = _bigIntToUnsignedBytes(affineX!); - final affineYUnsigned = _bigIntToUnsignedBytes(affineY!); - return Uint8List.fromList([...affineXUnsigned, ...affineYUnsigned]); - } - - static ECPublicKey generatePublicKey(Uint8List publicKeyBytes, ECDomainParameters params) { - final bitLength = params.n.bitLength ~/ 8; - final xBytes = publicKeyBytes.sublist(0, bitLength); - final yBytes = publicKeyBytes.sublist(bitLength, 2 * bitLength); - final x = BigInt.parse(xBytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(), radix: 16); - final y = BigInt.parse(yBytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(), radix: 16); - final ecPoint = params.curve.createPoint(x, y); - return ECPublicKey(ecPoint, params); - } - - static Uint8List generateSharedSecretBytes(ECPrivateKey privateKey, ECPublicKey serverPublicKey) { - final ecdh = ECDHBasicAgreement(); - ecdh.init(privateKey); - final sharedSecret = ecdh.calculateAgreement(serverPublicKey); - return _bigIntToUnsignedBytes(sharedSecret); - } - - static Uint8List generateHKDFBytes(Uint8List secretKey, Uint8List salt) { - final hkdf = HKDFKeyDerivator(SHA256Digest()); - final params = HkdfParameters(secretKey, HKDF_LENGTH, salt); - hkdf.init(params); - final result = Uint8List(HKDF_LENGTH); - hkdf.deriveKey(null, 0, result, 0); - return result; - } - - static Uint8List _bigIntToUnsignedBytes(BigInt number) { - final bytes = number.toRadixString(16).padLeft((number.bitLength + 7) >> 3 << 1, '0'); - return Uint8List.fromList( - List.generate(bytes.length ~/ 2, (i) => int.parse(bytes.substring(i * 2, i * 2 + 2), radix: 16)), - ); - } -} diff --git a/lib/utils/crypto/local_key_provider.dart b/lib/utils/crypto/local_key_provider.dart deleted file mode 100644 index e60f7560b..000000000 --- a/lib/utils/crypto/local_key_provider.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'dart:math'; -import 'dart:typed_data'; - -import 'package:pointycastle/export.dart'; -import 'package:swift_control/utils/crypto/encryption_utils.dart'; - -class LocalKeyProvider { - late AsymmetricKeyPair pair; - - LocalKeyProvider() { - generateKeyPair(); - } - - Uint8List getPublicKeyBytes() { - return EncryptionUtils.publicKeyToByteArray(pair.publicKey as ECPublicKey); - } - - ECPublicKey getPublicKey() { - return pair.publicKey as ECPublicKey; - } - - ECPrivateKey getPrivateKey() { - return pair.privateKey as ECPrivateKey; - } - - void generateKeyPair() { - final keyParams = ECKeyGeneratorParameters(ECCurve_secp256r1()); - final secureRandom = FortunaRandom(); - final random = Random.secure(); - final seeds = List.generate(32, (_) => random.nextInt(256)); - secureRandom.seed(KeyParameter(Uint8List.fromList(seeds))); - - final keyGenerator = ECKeyGenerator(); - keyGenerator.init(ParametersWithRandom(keyParams, secureRandom)); - pair = keyGenerator.generateKeyPair(); - } -} diff --git a/lib/utils/crypto/zap_crypto.dart b/lib/utils/crypto/zap_crypto.dart deleted file mode 100644 index 45135e227..000000000 --- a/lib/utils/crypto/zap_crypto.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'dart:typed_data'; - -import 'package:pointycastle/export.dart'; -import 'package:swift_control/utils/crypto/encryption_utils.dart'; - -import 'local_key_provider.dart'; - -class ZapCrypto { - final LocalKeyProvider localKeyProvider; - final AESEngine aesEngine = AESEngine(); - - Uint8List? encryptionKeyBytes; - Uint8List? ivBytes; - int counter = 0; - - ZapCrypto(this.localKeyProvider); - - void initialise(Uint8List devicePublicKeyBytes) { - final hkdfBytes = generateHmacKeyDerivationFunctionBytes(devicePublicKeyBytes); - encryptionKeyBytes = hkdfBytes.sublist(0, EncryptionUtils.KEY_LENGTH); - ivBytes = hkdfBytes.sublist(32, EncryptionUtils.HKDF_LENGTH); - } - - Uint8List encrypt(Uint8List data) { - assert(encryptionKeyBytes != null && ivBytes != null, "Not initialised"); - - final counterValue = counter; - counter++; - - final nonceBytes = createNonce(ivBytes!, counterValue); - final encrypted = encryptDecrypt(true, nonceBytes, data); - return Uint8List.fromList(createCounterBytes(counterValue) + encrypted); - } - - Uint8List decrypt(Uint8List counterArray, Uint8List payload) { - assert(encryptionKeyBytes != null && ivBytes != null, "Not initialised"); - - final nonceBytes = Uint8List.fromList(ivBytes! + counterArray); - return encryptDecrypt(false, nonceBytes, payload); - } - - Uint8List encryptDecrypt(bool encrypt, Uint8List nonceBytes, Uint8List data) { - final aeadParameters = AEADParameters( - KeyParameter(encryptionKeyBytes!), - EncryptionUtils.MAC_LENGTH * 8, - nonceBytes, - Uint8List(0), - ); - final ccmBlockCipher = CCMBlockCipher(aesEngine); - ccmBlockCipher.init(encrypt, aeadParameters); - final processed = Uint8List(ccmBlockCipher.getOutputSize(data.length)); - ccmBlockCipher.processBytes(data, 0, data.length, processed, 0); - ccmBlockCipher.doFinal(processed, 0); - return processed; - } - - Uint8List generateHmacKeyDerivationFunctionBytes(Uint8List devicePublicKeyBytes) { - final serverPublicKey = EncryptionUtils.generatePublicKey( - devicePublicKeyBytes, - localKeyProvider.getPublicKey().parameters!, - ); - final sharedSecretBytes = EncryptionUtils.generateSharedSecretBytes( - localKeyProvider.getPrivateKey(), - serverPublicKey, - ); - final salt = Uint8List.fromList( - EncryptionUtils.publicKeyToByteArray(serverPublicKey) + localKeyProvider.getPublicKeyBytes(), - ); - return EncryptionUtils.generateHKDFBytes(sharedSecretBytes, salt); - } - - Uint8List createNonce(Uint8List iv, int messageCounter) { - return Uint8List.fromList(iv + createCounterBytes(messageCounter)); - } - - Uint8List createCounterBytes(int messageCounter) { - final buffer = ByteData(4); - buffer.setInt32(0, messageCounter, Endian.big); - return buffer.buffer.asUint8List(); - } -} diff --git a/lib/utils/devices/ble_device.dart b/lib/utils/devices/ble_device.dart deleted file mode 100644 index 9fa8bf41a..000000000 --- a/lib/utils/devices/ble_device.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter_blue_plus/flutter_blue_plus.dart'; -import 'package:swift_control/utils/ble.dart'; -import 'package:swift_control/utils/crypto/local_key_provider.dart'; -import 'package:swift_control/utils/devices/zwift_click.dart'; -import 'package:swift_control/utils/devices/zwift_play.dart'; -import 'package:swift_control/utils/devices/zwift_ride.dart'; - -import '../crypto/zap_crypto.dart'; -import '../messages/notification.dart'; - -abstract class BleDevice { - final ScanResult scanResult; - final zapEncryption = ZapCrypto(LocalKeyProvider()); - - bool supportsEncryption = true; - - BleDevice(this.scanResult); - - static BleDevice? fromScanResult(ScanResult scanResult) { - if (scanResult.device.platformName == 'Zwift Ride') { - return ZwiftRide(scanResult); - } - final manufacturerData = scanResult.advertisementData.manufacturerData; - final data = manufacturerData[Constants.ZWIFT_MANUFACTURER_ID]; - if (data == null || data.isEmpty) { - return null; - } - final type = DeviceType.fromManufacturerData(data.first); - return switch (type) { - DeviceType.click => ZwiftClick(scanResult), - DeviceType.playRight => ZwiftPlay(scanResult), - DeviceType.playLeft => ZwiftPlay(scanResult), - _ => null, - }; - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is BleDevice && runtimeType == other.runtimeType && scanResult == other.scanResult; - - @override - int get hashCode => scanResult.hashCode; - - @override - String toString() { - return runtimeType.toString(); - } - - BluetoothDevice get device => scanResult.device; - final StreamController actionStreamInternal = StreamController.broadcast(); - Stream get actionStream => actionStreamInternal.stream; - - Future connect() async { - await device.connect(autoConnect: false).timeout(const Duration(seconds: 3)); - - var filteredStateStream = device.connectionState.where((s) => s == BluetoothConnectionState.connected); - - // Start listening now, before invokeMethod, to ensure we don't miss the response - Future futureState = filteredStateStream.first; - - // wait for connection - await futureState.timeout( - const Duration(seconds: 10), - onTimeout: () { - throw TimeoutException('Failed to connect in time.'); - }, - ); - - if (!kIsWeb && Platform.isAndroid) { - await device.requestMtu(256); - } - final services = await device.discoverServices(); - - if (device.isConnected) { - await handleServices(services); - } - } - - Future handleServices(List services); -} diff --git a/lib/utils/devices/zwift_click.dart b/lib/utils/devices/zwift_click.dart deleted file mode 100644 index cb4958af2..000000000 --- a/lib/utils/devices/zwift_click.dart +++ /dev/null @@ -1,156 +0,0 @@ -import 'dart:io'; - -import 'package:dartx/dartx.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_blue_plus/flutter_blue_plus.dart'; -import 'package:swift_control/main.dart'; -import 'package:swift_control/utils/devices/ble_device.dart'; -import 'package:swift_control/utils/messages/notification.dart'; - -import '../ble.dart'; -import '../crypto/encryption_utils.dart'; -import '../messages/click_notification.dart'; - -class ZwiftClick extends BleDevice { - ZwiftClick(super.scanResult); - - List get startCommand => Constants.RIDE_ON + Constants.RESPONSE_START_CLICK; - Guid get customServiceId => BleUuid.ZWIFT_CUSTOM_SERVICE_UUID; - - @override - Future handleServices(List services) async { - final customService = services.firstOrNullWhere((service) => service.uuid == customServiceId); - - if (customService == null) { - throw Exception('Custom service not found'); - } - - final asyncCharacteristic = customService.characteristics.firstOrNullWhere( - (characteristic) => characteristic.uuid == BleUuid.ZWIFT_ASYNC_CHARACTERISTIC_UUID, - ); - final syncTxCharacteristic = customService.characteristics.firstOrNullWhere( - (characteristic) => characteristic.uuid == BleUuid.ZWIFT_SYNC_TX_CHARACTERISTIC_UUID, - ); - final syncRxCharacteristic = customService.characteristics.firstOrNullWhere( - (characteristic) => characteristic.uuid == BleUuid.ZWIFT_SYNC_RX_CHARACTERISTIC_UUID, - ); - - if (asyncCharacteristic == null || syncTxCharacteristic == null || syncRxCharacteristic == null) { - throw Exception('Characteristics not found'); - } - - if (!asyncCharacteristic.isNotifying) { - await asyncCharacteristic.setNotifyValue(true); - } - final asyncSubscription = asyncCharacteristic.lastValueStream.listen((onData) { - _processCharacteristic('async', Uint8List.fromList(onData)); - }); - device.cancelWhenDisconnected(asyncSubscription); - - if (!syncTxCharacteristic.isNotifying) { - await syncTxCharacteristic.setNotifyValue(true, forceIndications: !kIsWeb && Platform.isAndroid); - } - final syncSubscription = syncTxCharacteristic.lastValueStream.listen((onData) { - _processCharacteristic('sync', Uint8List.fromList(onData)); - }); - device.cancelWhenDisconnected(syncSubscription); - - await _setupHandshake(syncRxCharacteristic); - } - - Future _setupHandshake(BluetoothCharacteristic syncRxCharacteristic) async { - if (supportsEncryption) { - await syncRxCharacteristic.write([ - ...Constants.RIDE_ON, - ...Constants.REQUEST_START, - ...zapEncryption.localKeyProvider.getPublicKeyBytes(), - ], withoutResponse: true); - } else { - await syncRxCharacteristic.write(Constants.RIDE_ON, withoutResponse: true); - } - } - - void _processCharacteristic(String tag, Uint8List bytes) { - if (kDebugMode && false) { - print('Received $tag: ${bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}'); - print('Received $tag: ${String.fromCharCodes(bytes)}'); - } - - if (bytes.isEmpty) { - return; - } - - try { - if (bytes.startsWith(startCommand)) { - _processDevicePublicKeyResponse(bytes); - } else if (bytes.startsWith(Constants.RIDE_ON)) { - //print("Empty RideOn response - unencrypted mode"); - } else if (!supportsEncryption || (bytes.length > Int32List.bytesPerElement + EncryptionUtils.MAC_LENGTH)) { - _processData(bytes); - } else if (bytes[0] == Constants.DISCONNECT_MESSAGE_TYPE) { - //print("Disconnect message"); - } else { - //print("Unprocessed - Data Type: ${bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}"); - } - } catch (e, stackTrace) { - print("Error processing data: $e"); - print("Stack Trace: $stackTrace"); - actionStreamInternal.add(LogNotification(e.toString())); - } - } - - ClickNotification? _lastClickNotification; - - void _processData(Uint8List bytes) { - int type; - Uint8List message; - - if (supportsEncryption) { - final counter = bytes.sublist(0, 4); // Int.SIZE_BYTES is 4 - final payload = bytes.sublist(4); - - final data = zapEncryption.decrypt(counter, payload); - type = data[0]; - message = data.sublist(1); - } else { - type = bytes[0]; - message = bytes.sublist(1); - } - - switch (type) { - case Constants.EMPTY_MESSAGE_TYPE: - //print("Empty Message"); // expected when nothing happening - break; - case Constants.BATTERY_LEVEL_TYPE: - //print("Battery level update: $message"); - break; - case Constants.CLICK_NOTIFICATION_MESSAGE_TYPE: - case Constants.PLAY_NOTIFICATION_MESSAGE_TYPE: - case Constants.RIDE_NOTIFICATION_MESSAGE_TYPE: // untested - processClickNotification(message); - break; - } - } - - void _processDevicePublicKeyResponse(Uint8List bytes) { - final devicePublicKeyBytes = bytes.sublist(Constants.RIDE_ON.length + Constants.RESPONSE_START_CLICK.length); - zapEncryption.initialise(devicePublicKeyBytes); - if (kDebugMode) { - print("Device Public Key - ${devicePublicKeyBytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}"); - } - } - - void processClickNotification(Uint8List message) { - final ClickNotification clickNotification = ClickNotification(message); - if (_lastClickNotification == null || _lastClickNotification != clickNotification) { - _lastClickNotification = clickNotification; - actionStreamInternal.add(clickNotification); - - if (clickNotification.buttonUp) { - actionHandler.increaseGear(); - } else if (clickNotification.buttonDown) { - actionHandler.decreaseGear(); - } - } - } -} diff --git a/lib/utils/devices/zwift_play.dart b/lib/utils/devices/zwift_play.dart deleted file mode 100644 index 42eddc6b9..000000000 --- a/lib/utils/devices/zwift_play.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:swift_control/utils/devices/zwift_click.dart'; -import 'package:swift_control/utils/messages/controller_notification.dart'; - -import '../../main.dart'; -import '../ble.dart'; - -class ZwiftPlay extends ZwiftClick { - ZwiftPlay(super.scanResult); - - ControllerNotification? _lastControllerNotification; - - @override - List get startCommand => Constants.RIDE_ON + Constants.RESPONSE_START_PLAY; - - @override - void processClickNotification(Uint8List message) { - final ControllerNotification clickNotification = ControllerNotification(message); - if (_lastControllerNotification == null || _lastControllerNotification != clickNotification) { - _lastControllerNotification = clickNotification; - actionStreamInternal.add(clickNotification); - - if (clickNotification.rightPad && clickNotification.analogLR.abs() == 100) { - actionHandler.increaseGear(); - } else if (!clickNotification.rightPad && clickNotification.analogLR.abs() == 100) { - actionHandler.decreaseGear(); - } - } - } -} diff --git a/lib/utils/devices/zwift_ride.dart b/lib/utils/devices/zwift_ride.dart deleted file mode 100644 index 99d0410e7..000000000 --- a/lib/utils/devices/zwift_ride.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:flutter_blue_plus/flutter_blue_plus.dart'; -import 'package:swift_control/utils/devices/zwift_play.dart'; - -import '../ble.dart'; - -class ZwiftRide extends ZwiftPlay { - ZwiftRide(super.scanResult); - - @override - Guid get customServiceId => BleUuid.ZWIFT_RIDE_CUSTOM_SERVICE_UUID; - - @override - bool get supportsEncryption => false; -} diff --git a/lib/utils/keymap/apps/biketerra.dart b/lib/utils/keymap/apps/biketerra.dart new file mode 100644 index 000000000..0241fccb4 --- /dev/null +++ b/lib/utils/keymap/apps/biketerra.dart @@ -0,0 +1,48 @@ +import 'dart:io'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/services.dart'; +import 'package:swift_control/utils/keymap/apps/supported_app.dart'; +import 'package:swift_control/utils/requirements/multi.dart'; + +import '../buttons.dart'; +import '../keymap.dart'; + +class Biketerra extends SupportedApp { + Biketerra() + : super( + name: 'Biketerra', + packageName: "biketerra", + compatibleTargets: Target.values, + supportsZwiftEmulation: !(Platform.isIOS || Platform.isMacOS), + keymap: Keymap( + keyPairs: [ + KeyPair( + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.shiftDown).toList(), + physicalKey: PhysicalKeyboardKey.keyS, + logicalKey: LogicalKeyboardKey.keyS, + ), + KeyPair( + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.shiftUp).toList(), + physicalKey: PhysicalKeyboardKey.keyW, + logicalKey: LogicalKeyboardKey.keyW, + ), + KeyPair( + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.navigateRight).toList(), + physicalKey: PhysicalKeyboardKey.arrowRight, + logicalKey: LogicalKeyboardKey.arrowRight, + ), + KeyPair( + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.navigateLeft).toList(), + physicalKey: PhysicalKeyboardKey.arrowLeft, + logicalKey: LogicalKeyboardKey.arrowLeft, + ), + KeyPair( + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.toggleUi).toList(), + physicalKey: PhysicalKeyboardKey.keyU, + logicalKey: LogicalKeyboardKey.keyU, + ), + ], + ), + ); +} diff --git a/lib/utils/keymap/apps/custom_app.dart b/lib/utils/keymap/apps/custom_app.dart new file mode 100644 index 000000000..610849ef3 --- /dev/null +++ b/lib/utils/keymap/apps/custom_app.dart @@ -0,0 +1,86 @@ +import 'dart:io'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:swift_control/utils/keymap/apps/supported_app.dart'; +import 'package:swift_control/utils/requirements/multi.dart'; + +import '../buttons.dart'; +import '../keymap.dart'; + +class CustomApp extends SupportedApp { + final String profileName; + + CustomApp({this.profileName = 'Other'}) + : super( + name: profileName, + compatibleTargets: kIsWeb + ? [Target.thisDevice] + : [ + if (!Platform.isIOS) Target.thisDevice, + Target.macOS, + Target.windows, + Target.iOS, + Target.android, + ], + packageName: "custom_$profileName", + supportsZwiftEmulation: !kIsWeb && !(Platform.isIOS || Platform.isMacOS), + keymap: Keymap(keyPairs: []), + ); + + List encodeKeymap() { + // encode to save in preferences + return keymap.keyPairs.map((e) => e.encode()).toList(); + } + + void decodeKeymap(List data) { + // decode from preferences + + if (data.isEmpty) { + return; + } + + final keyPairs = data.map((e) => KeyPair.decode(e)).whereNotNull().toList(); + if (keyPairs.isEmpty) { + return; + } + keymap.keyPairs = keyPairs; + } + + void setKey( + ControllerButton zwiftButton, { + required PhysicalKeyboardKey? physicalKey, + required LogicalKeyboardKey? logicalKey, + List modifiers = const [], + bool isLongPress = false, + Offset? touchPosition, + InGameAction? inGameAction, + int? inGameActionValue, + }) { + // set the key for the zwift button + final keyPair = keymap.getKeyPair(zwiftButton); + if (keyPair != null) { + keyPair.physicalKey = physicalKey; + keyPair.logicalKey = logicalKey; + keyPair.modifiers = modifiers; + keyPair.isLongPress = isLongPress; + keyPair.touchPosition = touchPosition ?? Offset.zero; + keyPair.inGameAction = inGameAction; + keyPair.inGameActionValue = inGameActionValue; + } else { + keymap.addKeyPair( + KeyPair( + buttons: [zwiftButton], + physicalKey: physicalKey, + logicalKey: logicalKey, + modifiers: modifiers, + isLongPress: isLongPress, + touchPosition: touchPosition ?? Offset.zero, + inGameAction: inGameAction, + inGameActionValue: inGameActionValue, + ), + ); + } + } +} diff --git a/lib/utils/keymap/apps/my_whoosh.dart b/lib/utils/keymap/apps/my_whoosh.dart new file mode 100644 index 000000000..7efe67e60 --- /dev/null +++ b/lib/utils/keymap/apps/my_whoosh.dart @@ -0,0 +1,62 @@ +import 'dart:io'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:swift_control/utils/keymap/apps/supported_app.dart'; +import 'package:swift_control/utils/requirements/multi.dart'; + +import '../buttons.dart'; +import '../keymap.dart'; + +class MyWhoosh extends SupportedApp { + MyWhoosh() + : super( + name: 'MyWhoosh', + packageName: "com.mywhoosh.whooshgame", + compatibleTargets: !kIsWeb && Platform.isIOS + ? Target.values.filterNot((e) => e == Target.thisDevice).toList() + : Target.values, + supportsZwiftEmulation: false, + keymap: Keymap( + keyPairs: [ + KeyPair( + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.shiftDown).toList(), + physicalKey: PhysicalKeyboardKey.keyI, + logicalKey: LogicalKeyboardKey.keyI, + touchPosition: Offset(80, 94), + inGameAction: InGameAction.shiftDown, + ), + KeyPair( + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.shiftUp).toList(), + physicalKey: PhysicalKeyboardKey.keyK, + logicalKey: LogicalKeyboardKey.keyK, + touchPosition: Offset(97, 94), + inGameAction: InGameAction.shiftUp, + ), + KeyPair( + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.navigateRight).toList(), + physicalKey: PhysicalKeyboardKey.arrowRight, + logicalKey: LogicalKeyboardKey.arrowRight, + touchPosition: Offset(60, 80), + isLongPress: true, + inGameAction: InGameAction.navigateRight, + ), + KeyPair( + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.navigateLeft).toList(), + physicalKey: PhysicalKeyboardKey.arrowLeft, + logicalKey: LogicalKeyboardKey.arrowLeft, + touchPosition: Offset(32, 80), + isLongPress: true, + inGameAction: InGameAction.navigateLeft, + ), + KeyPair( + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.toggleUi).toList(), + physicalKey: PhysicalKeyboardKey.keyH, + logicalKey: LogicalKeyboardKey.keyH, + inGameAction: InGameAction.toggleUi, + ), + ], + ), + ); +} diff --git a/lib/utils/keymap/apps/rouvy.dart b/lib/utils/keymap/apps/rouvy.dart new file mode 100644 index 000000000..7f9ab5074 --- /dev/null +++ b/lib/utils/keymap/apps/rouvy.dart @@ -0,0 +1,49 @@ +import 'dart:io'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:swift_control/bluetooth/devices/zwift/constants.dart'; +import 'package:swift_control/utils/keymap/apps/supported_app.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/utils/requirements/multi.dart'; + +import '../keymap.dart'; + +class Rouvy extends SupportedApp { + Rouvy() + : super( + name: 'Rouvy', + packageName: "eu.virtualtraining.rouvy.android", + compatibleTargets: !kIsWeb && Platform.isIOS + ? Target.values.filterNot((e) => e == Target.thisDevice).toList() + : Target.values, + supportsZwiftEmulation: !kIsWeb && Platform.isAndroid, + keymap: Keymap( + keyPairs: [ + // https://support.rouvy.com/hc/de/articles/32452137189393-Virtuelles-Schalten#h_01K5GMVG4KVYZ0Y6W7RBRZC9MA + KeyPair( + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.shiftDown).toList(), + inGameAction: InGameAction.shiftDown, + physicalKey: PhysicalKeyboardKey.comma, + logicalKey: LogicalKeyboardKey.comma, + touchPosition: Offset(94, 80), + ), + KeyPair( + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.shiftUp).toList(), + inGameAction: InGameAction.shiftUp, + physicalKey: PhysicalKeyboardKey.period, + logicalKey: LogicalKeyboardKey.period, + touchPosition: Offset(94, 72), + ), + // like escape + KeyPair( + buttons: [ZwiftButtons.b], + physicalKey: PhysicalKeyboardKey.keyB, + logicalKey: LogicalKeyboardKey.keyB, + inGameAction: InGameAction.back, + ), + ], + ), + ); +} diff --git a/lib/utils/keymap/apps/supported_app.dart b/lib/utils/keymap/apps/supported_app.dart new file mode 100644 index 000000000..e77dc449b --- /dev/null +++ b/lib/utils/keymap/apps/supported_app.dart @@ -0,0 +1,39 @@ +import 'package:swift_control/utils/keymap/apps/biketerra.dart'; +import 'package:swift_control/utils/keymap/apps/rouvy.dart'; +import 'package:swift_control/utils/keymap/apps/training_peaks.dart'; +import 'package:swift_control/utils/keymap/apps/zwift.dart'; +import 'package:swift_control/utils/requirements/multi.dart'; + +import '../keymap.dart'; +import 'custom_app.dart'; +import 'my_whoosh.dart'; + +abstract class SupportedApp { + final List compatibleTargets; + final String packageName; + final String name; + final Keymap keymap; + final bool supportsZwiftEmulation; + + const SupportedApp({ + required this.name, + required this.packageName, + required this.keymap, + required this.compatibleTargets, + required this.supportsZwiftEmulation, + }); + + static final List supportedApps = [ + MyWhoosh(), + Zwift(), + TrainingPeaks(), + Biketerra(), + Rouvy(), + CustomApp(), + ]; + + @override + String toString() { + return runtimeType.toString(); + } +} diff --git a/lib/utils/keymap/apps/training_peaks.dart b/lib/utils/keymap/apps/training_peaks.dart new file mode 100644 index 000000000..686b56673 --- /dev/null +++ b/lib/utils/keymap/apps/training_peaks.dart @@ -0,0 +1,116 @@ +import 'dart:io'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:swift_control/bluetooth/devices/elite/elite_square.dart'; +import 'package:swift_control/bluetooth/devices/zwift/constants.dart'; +import 'package:swift_control/utils/keymap/apps/supported_app.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/utils/requirements/multi.dart'; + +import '../keymap.dart'; + +class TrainingPeaks extends SupportedApp { + TrainingPeaks() + : super( + name: 'TrainingPeaks Virtual / IndieVelo', + packageName: "com.indieVelo.client", + compatibleTargets: !kIsWeb && Platform.isIOS + ? Target.values.filterNot((e) => e == Target.thisDevice).toList() + : Target.values, + supportsZwiftEmulation: false, + keymap: Keymap( + keyPairs: [ + // Explicit controller-button mappings with updated touch coordinates + KeyPair( + buttons: [ZwiftButtons.shiftUpRight], + physicalKey: PhysicalKeyboardKey.numpadAdd, + logicalKey: LogicalKeyboardKey.numpadAdd, + touchPosition: Offset(22.65384615384622, 7.0769230769229665), + ), + KeyPair( + buttons: [ZwiftButtons.shiftUpLeft], + physicalKey: PhysicalKeyboardKey.numpadAdd, + logicalKey: LogicalKeyboardKey.numpadAdd, + touchPosition: Offset(18.14448747554958, 6.772862761010401), + ), + KeyPair( + buttons: [ZwiftButtons.shiftDownLeft], + physicalKey: PhysicalKeyboardKey.numpadSubtract, + logicalKey: LogicalKeyboardKey.numpadSubtract, + touchPosition: Offset(18.128205128205135, 6.75213675213675), + ), + KeyPair( + buttons: [ZwiftButtons.shiftDownRight], + physicalKey: PhysicalKeyboardKey.numpadSubtract, + logicalKey: LogicalKeyboardKey.numpadSubtract, + touchPosition: Offset(22.61769250748708, 8.13909075507417), + ), + + // Navigation buttons (keep arrow key mappings and add touch positions) + KeyPair( + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.steerRight).toList(), + physicalKey: PhysicalKeyboardKey.arrowRight, + logicalKey: LogicalKeyboardKey.arrowRight, + touchPosition: Offset(56.75858807279006, 92.42753954973301), + ), + KeyPair( + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.steerLeft).toList(), + physicalKey: PhysicalKeyboardKey.arrowLeft, + logicalKey: LogicalKeyboardKey.arrowLeft, + touchPosition: Offset(41.11538461538456, 92.64957264957286), + ), + KeyPair( + buttons: [ZwiftButtons.navigationUp], + physicalKey: PhysicalKeyboardKey.arrowUp, + logicalKey: LogicalKeyboardKey.arrowUp, + touchPosition: Offset(42.28406293368177, 92.61854987939971), + ), + + // Face buttons with touch positions and keyboard fallbacks where sensible + KeyPair( + buttons: [ZwiftButtons.z, EliteSquareButtons.z], + physicalKey: null, + logicalKey: null, + touchPosition: Offset(33.993890038715456, 92.43667306401531), + ), + KeyPair( + buttons: [ZwiftButtons.a, EliteSquareButtons.a], + physicalKey: null, + logicalKey: null, + touchPosition: Offset(47.37191097597044, 92.86963594239016), + ), + KeyPair( + buttons: [ZwiftButtons.b, EliteSquareButtons.b], + physicalKey: null, + logicalKey: null, + touchPosition: Offset(41.12364102683652, 83.72743323236598), + ), + KeyPair( + buttons: [ZwiftButtons.y, EliteSquareButtons.y], + physicalKey: null, + logicalKey: null, + touchPosition: Offset(58.52936866684111, 84.31131200977018), + ), + + // Keep other existing mappings (toggle UI, increase/decrease resistance) + KeyPair( + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.toggleUi).toList(), + physicalKey: PhysicalKeyboardKey.keyH, + logicalKey: LogicalKeyboardKey.keyH, + ), + KeyPair( + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.increaseResistance).toList(), + physicalKey: PhysicalKeyboardKey.pageUp, + logicalKey: LogicalKeyboardKey.pageUp, + ), + KeyPair( + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.decreaseResistance).toList(), + physicalKey: PhysicalKeyboardKey.pageDown, + logicalKey: LogicalKeyboardKey.pageDown, + ), + ], + ), + ); +} diff --git a/lib/utils/keymap/apps/zwift.dart b/lib/utils/keymap/apps/zwift.dart new file mode 100644 index 000000000..d905e1530 --- /dev/null +++ b/lib/utils/keymap/apps/zwift.dart @@ -0,0 +1,114 @@ +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:swift_control/bluetooth/devices/zwift/constants.dart'; +import 'package:swift_control/utils/keymap/apps/supported_app.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/utils/requirements/multi.dart'; + +import '../keymap.dart'; + +class Zwift extends SupportedApp { + Zwift() + : super( + name: 'Zwift', + packageName: "com.zwift.zwiftgame", + supportsZwiftEmulation: !(Platform.isIOS || Platform.isMacOS), + compatibleTargets: [ + if (!Platform.isIOS) Target.thisDevice, + if (Platform.isAndroid) Target.otherDevice, + Target.macOS, + Target.windows, + Target.iOS, + Target.android, + ], + keymap: Keymap( + keyPairs: [ + KeyPair( + buttons: [ZwiftButtons.navigationUp], + physicalKey: PhysicalKeyboardKey.arrowUp, + logicalKey: LogicalKeyboardKey.arrowUp, + inGameAction: InGameAction.openActionBar, + ), + KeyPair( + buttons: [ZwiftButtons.navigationDown], + physicalKey: PhysicalKeyboardKey.arrowDown, + logicalKey: LogicalKeyboardKey.arrowDown, + inGameAction: InGameAction.uturn, + ), + KeyPair( + buttons: [ZwiftButtons.navigationLeft], + physicalKey: PhysicalKeyboardKey.arrowLeft, + logicalKey: LogicalKeyboardKey.arrowLeft, + inGameAction: InGameAction.steerLeft, + ), + KeyPair( + buttons: [ZwiftButtons.navigationRight], + physicalKey: PhysicalKeyboardKey.arrowRight, + logicalKey: LogicalKeyboardKey.arrowRight, + inGameAction: InGameAction.steerRight, + ), + KeyPair( + buttons: [ZwiftButtons.shiftUpLeft], + physicalKey: null, + logicalKey: null, + inGameAction: InGameAction.shiftDown, + ), + KeyPair( + buttons: [ZwiftButtons.shiftUpRight], + physicalKey: null, + logicalKey: null, + inGameAction: InGameAction.shiftUp, + ), + KeyPair( + buttons: [ZwiftButtons.shiftDownLeft], + physicalKey: null, + logicalKey: null, + inGameAction: InGameAction.shiftDown, + ), + KeyPair( + buttons: [ZwiftButtons.shiftDownRight], + physicalKey: null, + logicalKey: null, + inGameAction: InGameAction.shiftUp, + ), + KeyPair( + buttons: [ZwiftButtons.paddleLeft], + physicalKey: null, + logicalKey: null, + inGameAction: InGameAction.shiftDown, + ), + KeyPair( + buttons: [ZwiftButtons.paddleRight], + physicalKey: null, + logicalKey: null, + inGameAction: InGameAction.shiftUp, + ), + KeyPair( + buttons: [ZwiftButtons.y], + physicalKey: PhysicalKeyboardKey.space, + logicalKey: LogicalKeyboardKey.space, + inGameAction: InGameAction.usePowerUp, + ), + KeyPair( + buttons: [ZwiftButtons.a], + physicalKey: PhysicalKeyboardKey.enter, + logicalKey: LogicalKeyboardKey.enter, + inGameAction: InGameAction.select, + ), + KeyPair( + buttons: [ZwiftButtons.b], + physicalKey: PhysicalKeyboardKey.escape, + logicalKey: LogicalKeyboardKey.escape, + inGameAction: InGameAction.back, + ), + KeyPair( + buttons: [ZwiftButtons.z], + physicalKey: null, + logicalKey: null, + inGameAction: InGameAction.rideOnBomb, + ), + ], + ), + ); +} diff --git a/lib/utils/keymap/buttons.dart b/lib/utils/keymap/buttons.dart new file mode 100644 index 000000000..c1e590ecf --- /dev/null +++ b/lib/utils/keymap/buttons.dart @@ -0,0 +1,81 @@ +import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; +import 'package:swift_control/bluetooth/devices/cycplus/cycplus_bc2.dart'; +import 'package:swift_control/bluetooth/devices/elite/elite_square.dart'; +import 'package:swift_control/bluetooth/devices/elite/elite_sterzo.dart'; +import 'package:swift_control/bluetooth/devices/wahoo/wahoo_kickr_bike_shift.dart'; +import 'package:swift_control/bluetooth/devices/zwift/constants.dart'; + +enum InGameAction { + shiftUp('Shift Up'), + shiftDown('Shift Down'), + uturn('U-Turn'), + steerLeft('Steer Left'), + steerRight('Steer Right'), + + // mywhoosh + cameraAngle('Change Camera Angle', possibleValues: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), + emote('Emote', possibleValues: [1, 2, 3, 4, 5, 6]), + toggleUi('Toggle UI'), + navigateLeft('Navigate Left'), + navigateRight('Navigate Right'), + increaseResistance('Increase Resistance'), + decreaseResistance('Decrease Resistance'), + + // zwift + openActionBar('Open Action Bar'), + usePowerUp('Use Power-Up'), + select('Select'), + back('Back'), + rideOnBomb('Ride On Bomb'); + + final String title; + final List? possibleValues; + + const InGameAction(this.title, {this.possibleValues}); + + @override + String toString() { + return title; + } +} + +class ControllerButton { + final String name; + final InGameAction? action; + final Color? color; + final IconData? icon; + + const ControllerButton( + this.name, { + this.color, + this.icon, + this.action, + }); + + @override + String toString() { + return name; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ControllerButton && + runtimeType == other.runtimeType && + name == other.name && + action == other.action && + color == other.color && + icon == other.icon; + + @override + int get hashCode => Object.hash(name, action, color, icon); + + static List get values => [ + ...SterzoButtons.values, + ...ZwiftButtons.values, + ...EliteSquareButtons.values, + ...WahooKickrShiftButtons.values, + ...CycplusBc2Buttons.values, + ].distinct().toList(); +} diff --git a/lib/utils/keymap/keymap.dart b/lib/utils/keymap/keymap.dart index e3933b810..ba36210da 100644 --- a/lib/utils/keymap/keymap.dart +++ b/lib/utils/keymap/keymap.dart @@ -1,12 +1,229 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/keymap/apps/my_whoosh.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; + +import '../actions/base_actions.dart'; +import 'apps/custom_app.dart'; + +class Keymap { + static Keymap custom = Keymap(keyPairs: []); + + List keyPairs; + + Keymap({required this.keyPairs}); + + final StreamController _updateStream = StreamController.broadcast(); + Stream get updateStream => _updateStream.stream; + + @override + String toString() { + return keyPairs.joinToString( + separator: ('\n---------\n'), + transform: (k) => + '''Button: ${k.buttons.joinToString(transform: (e) => e.name)}\nKeyboard key: ${k.logicalKey?.keyLabel ?? 'Not assigned'}\nAction: ${k.buttons.firstOrNull?.action}${k.touchPosition != Offset.zero ? '\nTouch Position: ${k.touchPosition.toString()}' : ''}${k.isLongPress ? '\nLong Press: Enabled' : ''}''', + ); + } + + PhysicalKeyboardKey? getPhysicalKey(ControllerButton action) { + // get the key pair by in game action + return keyPairs.firstOrNullWhere((element) => element.buttons.contains(action))?.physicalKey; + } + + KeyPair? getKeyPair(ControllerButton action) { + // get the key pair by in game action + return keyPairs.firstOrNullWhere((element) => element.buttons.contains(action)); + } + + void reset() { + for (final keyPair in keyPairs) { + keyPair.physicalKey = null; + keyPair.logicalKey = null; + keyPair.touchPosition = Offset.zero; + keyPair.isLongPress = false; + keyPair.inGameAction = null; + keyPair.inGameActionValue = null; + } + _updateStream.add(null); + } + + void addKeyPair(KeyPair keyPair) { + keyPairs.add(keyPair); + _updateStream.add(null); + + if (actionHandler.supportedApp is CustomApp) { + settings.setKeyMap(actionHandler.supportedApp!); + } + } + + ControllerButton getOrAddButton(String name, ControllerButton Function() button) { + final allButtons = keyPairs.expand((kp) => kp.buttons).toSet().toList(); + if (allButtons.none((b) => b.name == name)) { + final newButton = button(); + addKeyPair( + KeyPair( + touchPosition: Offset.zero, + buttons: [newButton], + physicalKey: null, + logicalKey: null, + isLongPress: false, + ), + ); + return newButton; + } else { + return allButtons.firstWhere((b) => b.name == name); + } + } +} + +class KeyPair { + final List buttons; + PhysicalKeyboardKey? physicalKey; + LogicalKeyboardKey? logicalKey; + List modifiers; + Offset touchPosition; + bool isLongPress; + InGameAction? inGameAction; + int? inGameActionValue; + + KeyPair({ + required this.buttons, + required this.physicalKey, + required this.logicalKey, + this.modifiers = const [], + this.touchPosition = Offset.zero, + this.isLongPress = false, + this.inGameAction, + this.inGameActionValue, + }); + + bool get isSpecialKey => + physicalKey == PhysicalKeyboardKey.mediaPlayPause || + physicalKey == PhysicalKeyboardKey.mediaTrackNext || + physicalKey == PhysicalKeyboardKey.mediaTrackPrevious || + physicalKey == PhysicalKeyboardKey.mediaStop || + physicalKey == PhysicalKeyboardKey.audioVolumeUp || + physicalKey == PhysicalKeyboardKey.audioVolumeDown; + + IconData get icon { + return switch (physicalKey) { + PhysicalKeyboardKey.mediaPlayPause || + PhysicalKeyboardKey.mediaStop || + PhysicalKeyboardKey.mediaTrackPrevious || + PhysicalKeyboardKey.mediaTrackNext || + PhysicalKeyboardKey.audioVolumeUp || + PhysicalKeyboardKey.audioVolumeDown => Icons.music_note_outlined, + _ when physicalKey != null && actionHandler.supportedModes.contains(SupportedMode.keyboard) => Icons.keyboard, + _ + when inGameAction != null && + ((settings.getTrainerApp() is MyWhoosh && settings.getMyWhooshLinkEnabled()) || + (settings.getTrainerApp()?.supportsZwiftEmulation == true && settings.getZwiftEmulatorEnabled())) => + Icons.link, + _ => Icons.touch_app, + }; + } + + bool get hasNoAction => + logicalKey == null && physicalKey == null && touchPosition == Offset.zero && inGameAction == null; + + @override + String toString() { + final baseKey = + logicalKey?.keyLabel ?? + switch (physicalKey) { + PhysicalKeyboardKey.mediaPlayPause => 'Play/Pause', + PhysicalKeyboardKey.mediaTrackNext => 'Next Track', + PhysicalKeyboardKey.mediaTrackPrevious => 'Previous Track', + PhysicalKeyboardKey.mediaStop => 'Stop', + PhysicalKeyboardKey.audioVolumeUp => 'Volume Up', + PhysicalKeyboardKey.audioVolumeDown => 'Volume Down', + _ => 'Not assigned', + }; + + if (modifiers.isEmpty || baseKey == 'Not assigned') { + return baseKey; + } + + // Format modifiers + key (e.g., "Ctrl+Alt+R") + final modifierStrings = modifiers.map((m) { + return switch (m) { + ModifierKey.shiftModifier => 'Shift', + ModifierKey.controlModifier => 'Ctrl', + ModifierKey.altModifier => 'Alt', + ModifierKey.metaModifier => 'Meta', + ModifierKey.functionModifier => 'Fn', + _ => m.name, + }; + }).toList(); + + return '${modifierStrings.join('+')}+$baseKey'; + } + + String encode() { + // encode to save in preferences + + return jsonEncode({ + 'actions': buttons.map((e) => e.name).toList(), + if (logicalKey != null) 'logicalKey': logicalKey?.keyId.toString(), + if (physicalKey != null) 'physicalKey': physicalKey?.usbHidUsage.toString() ?? '0', + if (modifiers.isNotEmpty) 'modifiers': modifiers.map((e) => e.name).toList(), + if (touchPosition != Offset.zero) 'touchPosition': {'x': touchPosition.dx, 'y': touchPosition.dy}, + 'isLongPress': isLongPress, + 'inGameAction': inGameAction?.name, + 'inGameActionValue': inGameActionValue, + }); + } + + static KeyPair? decode(String data) { + // decode from preferences + final decoded = jsonDecode(data); + + // Support both percentage-based (new) and pixel-based (old) formats for backward compatibility + final Offset touchPosition = decoded.containsKey('touchPosition') + ? Offset( + (decoded['touchPosition']['x'] as num).toDouble(), + (decoded['touchPosition']['y'] as num).toDouble(), + ) + : Offset.zero; -enum Keymap { - myWhoosh(increase: PhysicalKeyboardKey.keyK, decrease: PhysicalKeyboardKey.keyI), - indieVelo(increase: PhysicalKeyboardKey(0x70030), decrease: PhysicalKeyboardKey(0x70038)), - plusMinus(increase: PhysicalKeyboardKey(0x70030), decrease: PhysicalKeyboardKey(0x70038)); + final buttons = decoded['actions'] + .map( + (e) => ControllerButton.values.firstOrNullWhere((element) => element.name == e) ?? ControllerButton(e), + ) + .cast() + .toList(); + if (buttons.isEmpty) { + return null; + } - final PhysicalKeyboardKey increase; - final PhysicalKeyboardKey decrease; + // Decode modifiers if present + final List modifiers = decoded.containsKey('modifiers') + ? (decoded['modifiers'] as List) + .map((e) => ModifierKey.values.firstOrNullWhere((element) => element.name == e)) + .whereType() + .toList() + : []; - const Keymap({required this.increase, required this.decrease}); + return KeyPair( + buttons: buttons, + logicalKey: decoded.containsKey('logicalKey') && int.parse(decoded['logicalKey']) != 0 + ? LogicalKeyboardKey(int.parse(decoded['logicalKey'])) + : null, + physicalKey: decoded.containsKey('physicalKey') && int.parse(decoded['physicalKey']) != 0 + ? PhysicalKeyboardKey(int.parse(decoded['physicalKey'])) + : null, + modifiers: modifiers, + touchPosition: touchPosition, + isLongPress: decoded['isLongPress'] ?? false, + inGameAction: decoded.containsKey('inGameAction') + ? InGameAction.values.firstOrNullWhere((element) => element.name == decoded['inGameAction']) + : null, + inGameActionValue: decoded['inGameActionValue'], + ); + } } diff --git a/lib/utils/keymap/manager.dart b/lib/utils/keymap/manager.dart new file mode 100644 index 000000000..9dd3f756e --- /dev/null +++ b/lib/utils/keymap/manager.dart @@ -0,0 +1,277 @@ +import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:swift_control/main.dart'; + +import 'apps/custom_app.dart'; + +class KeymapManager { + // Singleton instance + static final KeymapManager _instance = KeymapManager._internal(); + + // Private constructor + KeymapManager._internal(); + + // Factory constructor to return the singleton instance + factory KeymapManager() { + return _instance; + } + + Future showNewProfileDialog(BuildContext context) async { + final controller = TextEditingController(); + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('New Custom Profile'), + content: TextField( + controller: controller, + decoration: InputDecoration(labelText: 'Profile Name', hintText: 'e.g., Workout, Race, Event'), + autofocus: true, + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancel')), + TextButton(onPressed: () => Navigator.pop(context, controller.text), child: Text('Create')), + ], + ), + ); + } + + PopupMenuButton getManageProfileDialog( + BuildContext context, + String? currentProfile, { + required VoidCallback onDone, + }) { + return PopupMenuButton( + itemBuilder: (context) => [ + if (currentProfile != null && actionHandler.supportedApp is CustomApp) + PopupMenuItem( + child: Text('Rename'), + onTap: () async { + final newName = await _showRenameProfileDialog( + context, + currentProfile, + ); + if (newName != null && newName.isNotEmpty && newName != currentProfile) { + await settings.duplicateCustomAppProfile(currentProfile, newName); + await settings.deleteCustomAppProfile(currentProfile); + final customApp = CustomApp(profileName: newName); + final savedKeymap = settings.getCustomAppKeymap(newName); + if (savedKeymap != null) { + customApp.decodeKeymap(savedKeymap); + } + actionHandler.supportedApp = customApp; + await settings.setKeyMap(customApp); + } + onDone(); + }, + ), + if (currentProfile != null) + PopupMenuItem( + child: Text('Duplicate'), + onTap: () async { + final newName = await duplicate( + context, + currentProfile, + ); + onDone(); + }, + ), + PopupMenuItem( + child: Text('Import'), + onTap: () async { + final jsonData = await _showImportDialog(context); + if (jsonData != null && jsonData.isNotEmpty) { + final success = await settings.importCustomAppProfile(jsonData); + if (success) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Profile imported successfully'), + duration: Duration(seconds: 5), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to import profile. Invalid format.'), + duration: Duration(seconds: 5), + backgroundColor: Colors.red, + ), + ); + } + } + }, + ), + if (currentProfile != null) + PopupMenuItem( + child: Text('Export'), + onTap: () { + final currentProfile = (actionHandler.supportedApp as CustomApp).profileName; + final jsonData = settings.exportCustomAppProfile(currentProfile); + if (jsonData != null) { + Clipboard.setData(ClipboardData(text: jsonData)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Profile "$currentProfile" exported to clipboard', + ), + duration: Duration(seconds: 5), + ), + ); + } + }, + ), + if (currentProfile != null) + PopupMenuItem( + value: 'delete', + onTap: () async { + final confirmed = await _showDeleteConfirmDialog( + context, + currentProfile, + ); + if (confirmed == true) { + await settings.deleteCustomAppProfile(currentProfile); + } + onDone(); + }, + child: Text('Delete', style: TextStyle(color: Theme.of(context).colorScheme.error)), + ), + ], + ); + } + + Future _showRenameProfileDialog(BuildContext context, String currentName) async { + final controller = TextEditingController(text: currentName); + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Rename Profile'), + content: TextField( + controller: controller, + decoration: InputDecoration(labelText: 'Profile Name'), + autofocus: true, + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancel')), + TextButton(onPressed: () => Navigator.pop(context, controller.text), child: Text('Rename')), + ], + ), + ); + } + + Future _showDuplicateProfileDialog(BuildContext context, String currentName) async { + final controller = TextEditingController(text: '$currentName (Copy)'); + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Create new custom profile by duplicating "$currentName"'), + content: TextField( + controller: controller, + decoration: InputDecoration(labelText: 'New Profile Name'), + autofocus: true, + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancel')), + TextButton(onPressed: () => Navigator.pop(context, controller.text), child: Text('Duplicate')), + ], + ), + ); + } + + Future _showDeleteConfirmDialog(BuildContext context, String profileName) async { + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Delete Profile'), + content: Text('Are you sure you want to delete "$profileName"? This action cannot be undone.'), + actions: [ + TextButton(onPressed: () => Navigator.pop(context, false), child: Text('Cancel')), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: Text('Delete'), + style: TextButton.styleFrom(foregroundColor: Colors.red), + ), + ], + ), + ); + } + + Future _showImportDialog(BuildContext context) async { + final controller = TextEditingController(); + + // Try to get data from clipboard + try { + final clipboardData = await Clipboard.getData('text/plain'); + if (clipboardData?.text != null) { + controller.text = clipboardData!.text!; + } + } catch (e) { + // Ignore clipboard errors + } + + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Import Profile'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Paste the exported JSON data below:'), + SizedBox(height: 16), + TextField( + controller: controller, + decoration: InputDecoration(labelText: 'JSON Data', border: OutlineInputBorder()), + maxLines: 5, + autofocus: true, + ), + ], + ), + actions: [ + TextButton(onPressed: () => Navigator.pop(context), child: Text('Cancel')), + TextButton(onPressed: () => Navigator.pop(context, controller.text), child: Text('Import')), + ], + ), + ); + } + + Future duplicate(BuildContext context, String currentProfile, {String? skipName}) async { + final newName = skipName ?? await _showDuplicateProfileDialog(context, currentProfile); + if (newName != null && newName.isNotEmpty) { + if (actionHandler.supportedApp is CustomApp) { + await settings.duplicateCustomAppProfile(currentProfile, newName); + final customApp = CustomApp(profileName: newName); + final savedKeymap = settings.getCustomAppKeymap(newName); + if (savedKeymap != null) { + customApp.decodeKeymap(savedKeymap); + } + actionHandler.supportedApp = customApp; + await settings.setKeyMap(customApp); + return newName; + } else { + final customApp = CustomApp(profileName: newName); + + final connectedDevice = connection.devices.firstOrNull; + actionHandler.supportedApp!.keymap.keyPairs.forEachIndexed((pair, index) { + pair.buttons.filter((button) => connectedDevice?.availableButtons.contains(button) == true).forEachIndexed(( + button, + indexB, + ) { + customApp.setKey( + button, + physicalKey: pair.physicalKey, + logicalKey: pair.logicalKey, + isLongPress: pair.isLongPress, + touchPosition: pair.touchPosition, + inGameAction: pair.inGameAction, + inGameActionValue: pair.inGameActionValue, + ); + }); + }); + + actionHandler.supportedApp = customApp; + await settings.setKeyMap(customApp); + return newName; + } + } + return null; + } +} diff --git a/lib/utils/messages/click_notification.dart b/lib/utils/messages/click_notification.dart deleted file mode 100644 index 05e18b5bc..000000000 --- a/lib/utils/messages/click_notification.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:typed_data'; - -import 'package:swift_control/utils/messages/notification.dart'; - -import '../../protocol/zwift.pb.dart'; - -class ClickNotification extends BaseNotification { - static const int BTN_PRESSED = 0; - - bool buttonUp = false; - bool buttonDown = false; - - ClickNotification(Uint8List message) { - final status = ClickKeyPadStatus.fromBuffer(message); - buttonUp = status.buttonPlus.value == BTN_PRESSED; - buttonDown = status.buttonMinus.value == BTN_PRESSED; - } - - @override - String toString() { - return 'Click: {buttonUp: $buttonUp, buttonDown: $buttonDown}'; - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ClickNotification && - runtimeType == other.runtimeType && - buttonUp == other.buttonUp && - buttonDown == other.buttonDown; - - @override - int get hashCode => buttonUp.hashCode ^ buttonDown.hashCode; -} diff --git a/lib/utils/messages/controller_notification.dart b/lib/utils/messages/controller_notification.dart deleted file mode 100644 index 7758c3043..000000000 --- a/lib/utils/messages/controller_notification.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'dart:typed_data'; - -import 'package:swift_control/utils/messages/notification.dart'; - -import '../../protocol/zwift.pb.dart'; - -class ControllerNotification extends BaseNotification { - static const int BTN_PRESSED = 0; - - late bool rightPad, buttonY, buttonZ, buttonA, buttonB, buttonOn, buttonShift; - late int analogLR, analogUD; - - ControllerNotification(Uint8List message) { - final status = PlayKeyPadStatus.fromBuffer(message); - - rightPad = status.rightPad.value == BTN_PRESSED; - buttonY = status.buttonYUp.value == BTN_PRESSED; - buttonZ = status.buttonZLeft.value == BTN_PRESSED; - buttonA = status.buttonARight.value == BTN_PRESSED; - buttonB = status.buttonBDown.value == BTN_PRESSED; - buttonOn = status.buttonOn.value == BTN_PRESSED; - buttonShift = status.buttonShift.value == BTN_PRESSED; - analogLR = status.analogLR; - analogUD = status.analogUD; - } - - @override - String toString() { - final allTrueParameters = [ - //if (rightPad) 'rightPad', - if (buttonY) 'buttonY', - if (buttonZ) 'buttonZ', - if (buttonA) 'buttonA', - if (buttonB) 'buttonB', - if (buttonOn) 'buttonOn', - if (buttonShift) 'buttonShift', - ]; - return '${rightPad ? 'Right' : 'Left'}: {$allTrueParameters, analogLR: $analogLR, analogUD: $analogUD}'; - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ControllerNotification && - runtimeType == other.runtimeType && - rightPad == other.rightPad && - buttonY == other.buttonY && - buttonZ == other.buttonZ && - buttonA == other.buttonA && - buttonB == other.buttonB && - buttonOn == other.buttonOn && - buttonShift == other.buttonShift && - analogLR == other.analogLR && - analogUD == other.analogUD; - - @override - int get hashCode => - rightPad.hashCode ^ - buttonY.hashCode ^ - buttonZ.hashCode ^ - buttonA.hashCode ^ - buttonB.hashCode ^ - buttonOn.hashCode ^ - buttonShift.hashCode ^ - analogLR.hashCode ^ - analogUD.hashCode; -} diff --git a/lib/utils/messages/notification.dart b/lib/utils/messages/notification.dart deleted file mode 100644 index 28b0649c9..000000000 --- a/lib/utils/messages/notification.dart +++ /dev/null @@ -1,12 +0,0 @@ -class BaseNotification {} - -class LogNotification extends BaseNotification { - final String message; - - LogNotification(this.message); - - @override - String toString() { - return message; - } -} diff --git a/lib/utils/requirements/android.dart b/lib/utils/requirements/android.dart index bd1f30ec1..cbb676404 100644 --- a/lib/utils/requirements/android.dart +++ b/lib/utils/requirements/android.dart @@ -1,28 +1,106 @@ -import 'dart:io'; +import 'dart:isolate'; +import 'dart:ui'; +import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/actions/android.dart'; import 'package:swift_control/utils/requirements/platform.dart'; +import 'package:swift_control/widgets/accessibility_disclosure_dialog.dart'; +import 'package:universal_ble/universal_ble.dart'; class AccessibilityRequirement extends PlatformRequirement { - AccessibilityRequirement() : super('Allow Accessibility Service'); + AccessibilityRequirement() + : super( + 'Allow Accessibility Service', + description: 'BikeControl needs accessibility permission to control your training apps.', + ); @override - Future call() async { - return accessibilityHandler.openPermissions(); + Future call(BuildContext context, VoidCallback onUpdate) async { + _showDisclosureDialog(context, onUpdate); } @override Future getStatus() async { - status = await accessibilityHandler.hasPermission(); + status = await (actionHandler as AndroidActions).accessibilityHandler.hasPermission(); + } + + Future _showDisclosureDialog(BuildContext context, VoidCallback onUpdate) async { + return showDialog( + context: context, + barrierDismissible: false, // Prevent dismissing by tapping outside + builder: (BuildContext context) { + return AccessibilityDisclosureDialog( + onAccept: () { + Navigator.of(context).pop(); + // Open accessibility settings after user consents + (actionHandler as AndroidActions).accessibilityHandler.openPermissions().then((_) { + onUpdate(); + }); + }, + onDeny: () { + Navigator.of(context).pop(); + // User denied, no action taken + }, + ); + }, + ); + } +} + +class BluetoothScanRequirement extends PlatformRequirement { + BluetoothScanRequirement() : super('Allow Bluetooth Scan'); + + @override + Future call(BuildContext context, VoidCallback onUpdate) async { + await Permission.bluetoothScan.request(); + } + + @override + Future getStatus() async { + final state = await Permission.bluetoothScan.status; + status = state.isGranted || state.isLimited; + } +} + +class LocationRequirement extends PlatformRequirement { + LocationRequirement() : super('Allow Location so Bluetooth scan works'); + + @override + Future call(BuildContext context, VoidCallback onUpdate) async { + await Permission.locationWhenInUse.request(); + } + + @override + Future getStatus() async { + final state = await Permission.locationWhenInUse.status; + status = state.isGranted || state.isLimited; + } +} + +class BluetoothConnectRequirement extends PlatformRequirement { + BluetoothConnectRequirement() : super('Allow Bluetooth Connections'); + + @override + Future call(BuildContext context, VoidCallback onUpdate) async { + await Permission.bluetoothConnect.request(); + } + + @override + Future getStatus() async { + final state = await Permission.bluetoothConnect.status; + status = state.isGranted || state.isLimited; } } class NotificationRequirement extends PlatformRequirement { - NotificationRequirement() : super('Allow adding persistent Notification'); + NotificationRequirement() + : super('Allow persistent Notification', description: 'This keeps the app alive in background'); @override - Future call() async { + Future call(BuildContext context, VoidCallback onUpdate) async { await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation() ?.requestNotificationsPermission(); @@ -48,14 +126,11 @@ class NotificationRequirement extends PlatformRequirement { InitializationSettings(android: initializationSettingsAndroid), onDidReceiveBackgroundNotificationResponse: notificationTapBackground, onDidReceiveNotificationResponse: (n) { - if (n.actionId != null) { - connection.reset(); - exit(0); - } + notificationTapBackground(n); }, ); - const String channelGroupId = 'SwiftControl'; + const String channelGroupId = 'BikeControl'; // create the group first await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation()! @@ -78,21 +153,39 @@ class NotificationRequirement extends PlatformRequirement { await AndroidFlutterLocalNotificationsPlugin().startForegroundService( 1, channelGroupId, - 'Bluetooth keep alive', + 'Allows BikeControl to keep running in background', foregroundServiceTypes: {AndroidServiceForegroundType.foregroundServiceTypeConnectedDevice}, + startType: AndroidServiceStartType.startRedeliverIntent, notificationDetails: AndroidNotificationDetails( channelGroupId, 'Keep Alive', - actions: [AndroidNotificationAction('Exit', 'Exit', cancelNotification: true, showsUserInterface: false)], + actions: [ + AndroidNotificationAction( + 'Disconnect Devices', + 'Disconnect Devices', + cancelNotification: true, + showsUserInterface: false, + ), + ], ), ); + + final receivePort = ReceivePort(); + IsolateNameServer.registerPortWithName(receivePort.sendPort, '_backgroundChannelKey'); + final backgroundMessagePort = receivePort.asBroadcastStream(); + backgroundMessagePort.listen((_) { + UniversalBle.onAvailabilityChange = null; + connection.reset(); + //exit(0); + }); } } @pragma('vm:entry-point') void notificationTapBackground(NotificationResponse notificationResponse) { if (notificationResponse.actionId != null) { - connection.reset(); - exit(0); + final sendPort = IsolateNameServer.lookupPortByName('_backgroundChannelKey'); + sendPort?.send('notificationResponse'); + //exit(0); } } diff --git a/lib/utils/requirements/multi.dart b/lib/utils/requirements/multi.dart index 405de4106..b98be8a59 100644 --- a/lib/utils/requirements/multi.dart +++ b/lib/utils/requirements/multi.dart @@ -1,21 +1,32 @@ import 'dart:io'; import 'package:dartx/dartx.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:keypress_simulator/keypress_simulator.dart'; -import 'package:swift_control/pages/scan.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/keymap/apps/custom_app.dart'; +import 'package:swift_control/utils/keymap/apps/my_whoosh.dart'; +import 'package:swift_control/utils/keymap/apps/supported_app.dart'; +import 'package:swift_control/utils/keymap/apps/zwift.dart'; import 'package:swift_control/utils/requirements/platform.dart'; - -import '../../main.dart'; -import '../keymap/keymap.dart'; +import 'package:swift_control/utils/requirements/remote.dart'; +import 'package:swift_control/widgets/beta_pill.dart'; +import 'package:universal_ble/universal_ble.dart'; class KeyboardRequirement extends PlatformRequirement { KeyboardRequirement() : super('Keyboard access'); @override - Future call() async { - return keyPressSimulator.requestAccess(onlyOpenPrefPane: Platform.isMacOS); + Future call(BuildContext context, VoidCallback onUpdate) async { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Enable keyboard access in the following screen for BikeControl. If you don\'t see BikeControl, please add it manually.', + ), + ), + ); + await keyPressSimulator.requestAccess(onlyOpenPrefPane: Platform.isMacOS); } @override @@ -24,74 +35,404 @@ class KeyboardRequirement extends PlatformRequirement { } } -class KeymapRequirement extends PlatformRequirement { - KeymapRequirement() : super('Select your Keymap / App'); - - @override - Future call() async {} +class BluetoothTurnedOn extends PlatformRequirement { + BluetoothTurnedOn() : super('Bluetooth turned on'); @override - Future getStatus() async { - status = actionHandler.keymap != null; + Future call(BuildContext context, VoidCallback onUpdate) async { + final currentState = await UniversalBle.getBluetoothAvailabilityState(); + if (!kIsWeb && Platform.isIOS) { + // on iOS we cannot programmatically enable Bluetooth, just open settings + await peripheralManager.showAppSettings(); + } else if (currentState == AvailabilityState.poweredOff) { + await UniversalBle.enableBluetooth(); + } else { + // I guess bluetooth is on now + // TODO move UniversalBle.onAvailabilityChange + getStatus(); + onUpdate(); + } } @override Widget? build(BuildContext context, VoidCallback onUpdate) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: DropdownMenu( - dropdownMenuEntries: - Keymap.values.map((key) => DropdownMenuEntry(value: key, label: key.name.capitalize())).toList(), - onSelected: (keymap) { - actionHandler.init(keymap); - onUpdate(); - }, - initialSelection: null, - hintText: 'Keymap', - ), + return ElevatedButton( + onPressed: () { + call(context, onUpdate); + }, + child: Text('Enable Bluetooth'), ); } -} - -class BluetoothTurnedOn extends PlatformRequirement { - BluetoothTurnedOn() : super('Bluetooth turned on'); - - @override - Future call() async { - return FlutterBluePlus.turnOn(); - } @override Future getStatus() async { - status = FlutterBluePlus.adapterStateNow != BluetoothAdapterState.off; + final currentState = screenshotMode + ? AvailabilityState.poweredOn + : await UniversalBle.getBluetoothAvailabilityState(); + status = currentState == AvailabilityState.poweredOn || screenshotMode; } } class UnsupportedPlatform extends PlatformRequirement { - UnsupportedPlatform() : super('Unsupported platform :(') { + UnsupportedPlatform() + : super('This ${kIsWeb ? 'Browser does not support Web Bluetooth and ' : 'platform'} is not supported :(') { status = false; } @override - Future call() async {} + Future call(BuildContext context, VoidCallback onUpdate) async {} @override Future getStatus() async {} } -class BluetoothScanning extends PlatformRequirement { - BluetoothScanning() : super('Bluetooth Scanning') { +class ErrorRequirement extends PlatformRequirement { + ErrorRequirement(super.name) { status = false; } @override - Future call() async {} + Future call(BuildContext context, VoidCallback onUpdate) async { + onUpdate(); + } @override Future getStatus() async {} +} + +typedef BoolFunction = bool Function(); + +enum Target { + thisDevice( + title: 'This Device', + icon: Icons.devices, + ), + otherDevice( + title: 'Other Device', + icon: Icons.settings_remote_outlined, + ), + iOS( + title: 'iPhone / iPad / Apple TV', + icon: Icons.settings_remote_outlined, + ), + android( + title: 'Android Device', + icon: Icons.settings_remote_outlined, + ), + macOS( + title: 'Mac', + icon: Icons.settings_remote_outlined, + ), + windows( + title: 'Windows PC', + icon: Icons.settings_remote_outlined, + ); + + final String title; + final IconData icon; + + const Target({required this.title, required this.icon}); + + bool get isCompatible { + return settings.getTrainerApp()?.compatibleTargets.contains(this) == true; + } + + bool get isBeta { + final supportedApp = settings.getTrainerApp(); + + if (supportedApp is Zwift && !(Platform.isIOS || Platform.isMacOS)) { + // everything is supported, this device is not compatible anyway + return false; + } + + return switch (this) { + Target.thisDevice => false, + _ => true, + }; + } + + String getDescription(SupportedApp? app) { + return switch (this) { + Target.thisDevice when !isCompatible => + 'Due to platform restrictions only controlling ${app?.name ?? 'the Trainer app'} on other devices is supported.', + Target.thisDevice => 'Run ${app?.name ?? 'the Trainer app'} on this device.', + Target.iOS => + 'Run ${app?.name ?? 'the Trainer app'} on an Apple device and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Direct Connect' : ''}.', + Target.android => + 'Run ${app?.name ?? 'the Trainer app'} on an Android device and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Direct Connect' : ''}.', + Target.macOS => + 'Run ${app?.name ?? 'the Trainer app'} on a Mac and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Direct Connect' : ''}.', + Target.windows => + 'Run ${app?.name ?? 'the Trainer app'} on a Windows PC and control it remotely from this device${app is MyWhoosh ? ', e.g. by using MyWhoosh Direct Connect' : ''}.', + Target.otherDevice => + 'Run ${app?.name ?? 'the Trainer app'} on another device and control it remotely from this device.', + }; + } + + String? get warning { + if (settings.getTrainerApp()?.supportsZwiftEmulation == true) { + // no warnings for zwift emulation + return null; + } + return switch (this) { + Target.android when Platform.isAndroid => + "Select 'This device' unless you want to control another Android device. Are you sure?", + Target.macOS when Platform.isMacOS => + "Select 'This device' unless you want to control another macOS device. Are you sure?", + Target.windows when Platform.isWindows => + "Select 'This device' unless you want to control another Windows device. Are you sure?", + Target.android => "We highly recommended to download and use BikeControl on that Android device.", + Target.macOS => "We highly recommended to download and use BikeControl on that macOS device.", + Target.windows => "We highly recommended to download and use BikeControl on that Windows device.", + _ => null, + }; + } + + ConnectionType get connectionType { + return switch (this) { + Target.thisDevice => ConnectionType.local, + _ => ConnectionType.remote, + }; + } +} + +class TargetRequirement extends PlatformRequirement { + TargetRequirement() + : super( + 'Select Trainer App & Target Device', + description: 'Select your Target Device where you want to run your trainer app on', + ) { + status = false; + } + + @override + Future call(BuildContext context, VoidCallback onUpdate) async {} + + @override + Future getStatus() async { + status = settings.getLastTarget() != null && settings.getTrainerApp() != null; + } @override Widget? build(BuildContext context, VoidCallback onUpdate) { - return ScanWidget(); + return StatefulBuilder( + builder: (c, setState) => Column( + spacing: 8, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Select Trainer App', style: TextStyle(fontWeight: FontWeight.bold)), + ConstrainedBox( + constraints: BoxConstraints(maxWidth: 400), + child: DropdownMenu( + dropdownMenuEntries: SupportedApp.supportedApps.map((app) { + return DropdownMenuEntry( + value: app, + label: app.name, + labelWidget: app is Zwift && !(Platform.isWindows || Platform.isAndroid) + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(app.name), + Row( + children: [ + Expanded( + child: Text( + 'When running BikeControl on Apple devices you are limited to on-screen controls (so no virtual shifting) only due to platform restrictions :(', + style: TextStyle(fontSize: 12, color: Colors.grey), + ), + ), + Icon(Icons.warning_amber), + ], + ), + ], + ) + : null, + ); + }).toList(), + hintText: 'Select Trainer app', + initialSelection: settings.getTrainerApp(), + onSelected: (selectedApp) async { + if (settings.getTrainerApp() is MyWhoosh && selectedApp is! MyWhoosh && whooshLink.isStarted.value) { + whooshLink.stopServer(); + } + settings.setTrainerApp(selectedApp!); + if (settings.getLastTarget() == null && Target.thisDevice.isCompatible) { + await settings.setLastTarget(Target.thisDevice); + } + if (actionHandler.supportedApp == null || + (actionHandler.supportedApp is! CustomApp && selectedApp is! CustomApp)) { + actionHandler.init(selectedApp); + settings.setKeyMap(selectedApp); + } + setState(() {}); + }, + ), + ), + SizedBox(height: 8), + Text( + 'Select Target where ${settings.getTrainerApp()?.name ?? 'the Trainer app'} runs on', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ConstrainedBox( + constraints: BoxConstraints(maxWidth: 400), + child: DropdownMenu( + dropdownMenuEntries: [Target.thisDevice, Target.otherDevice].map((target) { + return DropdownMenuEntry( + value: target, + label: target.title, + leadingIcon: Icon(target.icon), + enabled: target.isCompatible, + labelWidget: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(target.title), + Text( + target.getDescription(settings.getTrainerApp()), + style: TextStyle(fontSize: 10, color: Colors.grey), + ), + ], + ), + ), + ); + }).toList(), + hintText: 'Select Target device', + initialSelection: settings.getLastTarget() != Target.thisDevice ? Target.otherDevice : Target.thisDevice, + enabled: settings.getTrainerApp() != null, + onSelected: (target) async { + if (target != null) { + await settings.setLastTarget(target); + if (target.warning != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(target.warning!), + duration: Duration(seconds: 10), + ), + ); + } + setState(() {}); + } + }, + ), + ), + if (settings.getLastTarget() != null && settings.getLastTarget() != Target.thisDevice) ...[ + SizedBox(height: 8), + Text( + 'Select the other device where ${settings.getTrainerApp()?.name ?? 'the Trainer app'} runs on', + style: TextStyle(fontWeight: FontWeight.bold), + ), + ConstrainedBox( + constraints: BoxConstraints(maxWidth: 400), + child: DropdownMenu( + dropdownMenuEntries: Target.values + .whereNot((e) => [Target.thisDevice, Target.otherDevice].contains(e)) + .map((target) { + return DropdownMenuEntry( + value: target, + label: target.title, + enabled: target.isCompatible, + leadingIcon: Icon(target.icon), + labelWidget: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + target.title, + style: TextStyle( + fontWeight: target == Target.thisDevice && target.isCompatible + ? FontWeight.bold + : null, + ), + ), + if (target.isBeta) BetaPill(), + ], + ), + Text( + target.getDescription(settings.getTrainerApp()), + style: TextStyle(fontSize: 10, color: Colors.grey), + ), + ], + ), + ), + ); + }) + .toList(), + hintText: 'Select Target device', + initialSelection: settings.getLastTarget(), + enabled: settings.getTrainerApp() != null, + onSelected: (target) async { + if (target != null) { + await settings.setLastTarget(target); + initializeActions(target.connectionType); + if (target.warning != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(target.warning!), + duration: Duration(seconds: 10), + ), + ); + } + setState(() {}); + } + }, + ), + ), + ], + SizedBox(height: 8), + ElevatedButton( + onPressed: settings.getTrainerApp() != null && settings.getLastTarget() != null + ? () { + onUpdate(); + } + : null, + child: Text('Continue'), + ), + ], + ), + ); + } + + @override + Widget? buildDescription() { + final trainer = settings.getTrainerApp(); + final target = settings.getLastTarget(); + + if (target != null && trainer != null) { + if (target.warning != null) { + return Row( + spacing: 8, + children: [ + Icon(Icons.warning, color: Colors.red, size: 16), + Expanded( + child: Text( + settings.getLastTarget()!.warning!, + style: TextStyle(color: Colors.red), + ), + ), + ], + ); + } else { + return Text('${trainer.name} on ${target.title}'); + } + } else { + return null; + } + } +} + +class PlaceholderRequirement extends PlatformRequirement { + PlaceholderRequirement() : super('Requirement'); + + @override + Future call(BuildContext context, VoidCallback onUpdate) async {} + + @override + Future getStatus() async { + status = false; } } diff --git a/lib/utils/requirements/platform.dart b/lib/utils/requirements/platform.dart index d8530d27d..f3569cb76 100644 --- a/lib/utils/requirements/platform.dart +++ b/lib/utils/requirements/platform.dart @@ -1,35 +1,92 @@ import 'dart:io'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:swift_control/main.dart'; import 'package:swift_control/utils/requirements/android.dart'; import 'package:swift_control/utils/requirements/multi.dart'; +import 'package:swift_control/utils/requirements/remote.dart'; +import 'package:universal_ble/universal_ble.dart'; abstract class PlatformRequirement { String name; + String? description; late bool status; - PlatformRequirement(this.name); + PlatformRequirement(this.name, {this.description}); Future getStatus(); - Future call(); + Future call(BuildContext context, VoidCallback onUpdate); Widget? build(BuildContext context, VoidCallback onUpdate) { return null; } + + Widget? buildDescription() { + return null; + } } -Future> getRequirements() async { +Future> getRequirements(ConnectionType connectionType) async { List list; if (kIsWeb) { - list = [BluetoothTurnedOn(), BluetoothScanning()]; + final availablity = await UniversalBle.getBluetoothAvailabilityState(); + if (availablity == AvailabilityState.unsupported) { + list = [UnsupportedPlatform()]; + } else { + list = [BluetoothTurnedOn()]; + } } else if (Platform.isMacOS) { - list = [BluetoothTurnedOn(), KeyboardRequirement(), KeymapRequirement(), BluetoothScanning()]; + list = [ + TargetRequirement(), + BluetoothTurnedOn(), + switch (connectionType) { + ConnectionType.local => KeyboardRequirement(), + ConnectionType.remote => RemoteRequirement(), + ConnectionType.unknown => PlaceholderRequirement(), + }, + ]; + } else if (Platform.isIOS) { + list = [ + TargetRequirement(), + BluetoothTurnedOn(), + switch (connectionType) { + ConnectionType.local => RemoteRequirement(), + ConnectionType.remote => RemoteRequirement(), + ConnectionType.unknown => PlaceholderRequirement(), + }, + ]; } else if (Platform.isWindows) { - list = [BluetoothTurnedOn(), KeyboardRequirement(), KeymapRequirement(), BluetoothScanning()]; + list = [ + TargetRequirement(), + BluetoothTurnedOn(), + switch (connectionType) { + ConnectionType.local => KeyboardRequirement(), + ConnectionType.remote => RemoteRequirement(), + ConnectionType.unknown => PlaceholderRequirement(), + }, + ]; } else if (Platform.isAndroid) { - list = [BluetoothTurnedOn(), AccessibilityRequirement(), NotificationRequirement(), BluetoothScanning()]; + final deviceInfoPlugin = DeviceInfoPlugin(); + final deviceInfo = await deviceInfoPlugin.androidInfo; + list = [ + TargetRequirement(), + BluetoothTurnedOn(), + NotificationRequirement(), + if (deviceInfo.version.sdkInt <= 30) + LocationRequirement() + else ...[ + BluetoothScanRequirement(), + BluetoothConnectRequirement(), + ], + switch (connectionType) { + ConnectionType.local => AccessibilityRequirement(), + ConnectionType.remote => RemoteRequirement(), + ConnectionType.unknown => PlaceholderRequirement(), + }, + ]; } else { list = [UnsupportedPlatform()]; } diff --git a/lib/utils/requirements/remote.dart b/lib/utils/requirements/remote.dart new file mode 100644 index 000000000..82ea7a298 --- /dev/null +++ b/lib/utils/requirements/remote.dart @@ -0,0 +1,432 @@ +import 'dart:io'; + +import 'package:bluetooth_low_energy/bluetooth_low_energy.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart' hide ConnectionState; +import 'package:permission_handler/permission_handler.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/pages/device.dart'; +import 'package:swift_control/utils/actions/remote.dart'; +import 'package:swift_control/utils/keymap/apps/my_whoosh.dart'; +import 'package:swift_control/utils/requirements/multi.dart'; +import 'package:swift_control/utils/requirements/platform.dart'; +import 'package:swift_control/widgets/beta_pill.dart'; +import 'package:swift_control/widgets/small_progress_indicator.dart'; + +import '../../pages/markdown.dart'; + +final peripheralManager = PeripheralManager(); +bool isAdvertisingPeripheral = false; +bool _isLoading = false; +bool _isServiceAdded = false; +bool _isSubscribedToEvents = false; + +class RemoteRequirement extends PlatformRequirement { + RemoteRequirement() + : super( + 'Connect to your target device', + ); + + @override + Future call(BuildContext context, VoidCallback onUpdate) async {} + + @override + Widget? buildDescription() { + return Text('Choose your preferred connection method'); + } + + Future reconnect() async { + await peripheralManager.stopAdvertising(); + await peripheralManager.removeAllServices(); + _isServiceAdded = false; + isAdvertisingPeripheral = false; + (actionHandler as RemoteActions).setConnectedCentral(null, null); + startAdvertising(() {}); + } + + Future startAdvertising(VoidCallback onUpdate) async { + // Input report characteristic (notify) + final inputReport = GATTCharacteristic.mutable( + uuid: UUID.fromString('2A4D'), + permissions: [GATTCharacteristicPermission.read], + properties: [GATTCharacteristicProperty.notify, GATTCharacteristicProperty.read], + descriptors: [ + GATTDescriptor.immutable( + // Report Reference: ID=1, Type=Input(1) + uuid: UUID.fromString('2908'), + value: Uint8List.fromList([0x01, 0x01]), + ), + ], + ); + + peripheralManager.stateChanged.forEach((state) { + print('Peripheral manager state: ${state.state}'); + }); + + if (!kIsWeb && Platform.isAndroid) { + if (Platform.isAndroid) { + peripheralManager.connectionStateChanged.forEach((state) { + print('Peripheral connection state: ${state.state} of ${state.central.uuid}'); + if (state.state == ConnectionState.connected) { + /*(actionHandler as RemoteActions).setConnectedCentral(state.central, inputReport); + //peripheralManager.stopAdvertising(); + onUpdate();*/ + } else if (state.state == ConnectionState.disconnected) { + (actionHandler as RemoteActions).setConnectedCentral(null, null); + onUpdate(); + } + }); + } + + final status = await Permission.bluetoothAdvertise.request(); + if (!status.isGranted) { + print('Bluetooth advertise permission not granted'); + isAdvertisingPeripheral = false; + onUpdate(); + return; + } + } + if (kDebugMode && false) { + print('Continuing'); + return; + } + + while (peripheralManager.state != BluetoothLowEnergyState.poweredOn) { + print('Waiting for peripheral manager to be powered on... ${peripheralManager.state}'); + if (settings.getLastTarget() == Target.thisDevice) { + return; + } + await Future.delayed(Duration(seconds: 1)); + } + if (!_isServiceAdded) { + await Future.delayed(Duration(seconds: 1)); + final reportMapDataAbsolute = Uint8List.fromList([ + 0x05, 0x01, // Usage Page (Generic Desktop) + 0x09, 0x02, // Usage (Mouse) + 0xA1, 0x01, // Collection (Application) + 0x85, 0x01, // Report ID (1) + 0x09, 0x01, // Usage (Pointer) + 0xA1, 0x00, // Collection (Physical) + 0x05, 0x09, // Usage Page (Button) + 0x19, 0x01, // Usage Min (1) + 0x29, 0x03, // Usage Max (3) + 0x15, 0x00, // Logical Min (0) + 0x25, 0x01, // Logical Max (1) + 0x95, 0x03, // Report Count (3) + 0x75, 0x01, // Report Size (1) + 0x81, 0x02, // Input (Data,Var,Abs) // buttons + 0x95, 0x01, // Report Count (1) + 0x75, 0x05, // Report Size (5) + 0x81, 0x03, // Input (Const,Var,Abs) // padding + 0x05, 0x01, // Usage Page (Generic Desktop) + 0x09, 0x30, // Usage (X) + 0x09, 0x31, // Usage (Y) + 0x15, 0x00, // Logical Min (0) + 0x25, 0x64, // Logical Max (100) + 0x75, 0x08, // Report Size (8) + 0x95, 0x02, // Report Count (2) + 0x81, 0x02, // Input (Data,Var,Abs) + 0xC0, + 0xC0, + ]); + + // 1) Build characteristics + final hidInfo = GATTCharacteristic.immutable( + uuid: UUID.fromString('2A4A'), + value: Uint8List.fromList([0x11, 0x01, 0x00, 0x02]), + descriptors: [], // HID v1.11, country=0, flags=2 + ); + + final reportMap = GATTCharacteristic.immutable( + uuid: UUID.fromString('2A4B'), + //properties: [GATTCharacteristicProperty.read], + //permissions: [GATTCharacteristicPermission.read], + value: reportMapDataAbsolute, + descriptors: [ + GATTDescriptor.immutable(uuid: UUID.fromString('2908'), value: Uint8List.fromList([0x0, 0x0])), + ], + ); + + final protocolMode = GATTCharacteristic.mutable( + uuid: UUID.fromString('2A4E'), + properties: [GATTCharacteristicProperty.read, GATTCharacteristicProperty.writeWithoutResponse], + permissions: [GATTCharacteristicPermission.read, GATTCharacteristicPermission.write], + descriptors: [], + ); + + final hidControlPoint = GATTCharacteristic.mutable( + uuid: UUID.fromString('2A4C'), + properties: [GATTCharacteristicProperty.writeWithoutResponse], + permissions: [GATTCharacteristicPermission.write], + descriptors: [], + ); + + // Input report characteristic (notify) + final keyboardInputReport = GATTCharacteristic.mutable( + uuid: UUID.fromString('2A4D'), + permissions: [GATTCharacteristicPermission.read], + properties: [GATTCharacteristicProperty.notify, GATTCharacteristicProperty.read], + descriptors: [ + GATTDescriptor.immutable( + // Report Reference: ID=1, Type=Input(1) + uuid: UUID.fromString('2908'), + value: Uint8List.fromList([0x02, 0x01]), + ), + ], + ); + + final outputReport = GATTCharacteristic.mutable( + uuid: UUID.fromString('2A4D'), + permissions: [GATTCharacteristicPermission.read, GATTCharacteristicPermission.write], + properties: [ + GATTCharacteristicProperty.read, + GATTCharacteristicProperty.write, + GATTCharacteristicProperty.writeWithoutResponse, + ], + descriptors: [ + GATTDescriptor.immutable( + // Report Reference: ID=1, Type=Input(1) + uuid: UUID.fromString('2908'), + value: Uint8List.fromList([0x02, 0x02]), + ), + ], + ); + + // 2) HID service + final hidService = GATTService( + uuid: UUID.fromString(Platform.isIOS ? '1812' : '00001812-0000-1000-8000-00805F9B34FB'), + isPrimary: true, + characteristics: [ + hidInfo, + reportMap, + protocolMode, + outputReport, + hidControlPoint, + keyboardInputReport, + inputReport, + ], + includedServices: [], + ); + + if (!_isSubscribedToEvents) { + _isSubscribedToEvents = true; + peripheralManager.characteristicReadRequested.forEach((char) { + print('Read request for characteristic: ${char}'); + // You can respond to read requests here if needed + }); + + peripheralManager.characteristicNotifyStateChanged.forEach((char) { + if (char.characteristic.uuid == inputReport.uuid) { + if (char.state) { + (actionHandler as RemoteActions).setConnectedCentral(char.central, char.characteristic); + } else { + (actionHandler as RemoteActions).setConnectedCentral(null, null); + } + onUpdate(); + } + print( + 'Notify state changed for characteristic: ${char.characteristic.uuid} vs ${char.characteristic.uuid == inputReport.uuid}: ${char.state}', + ); + }); + } + await peripheralManager.addService(hidService); + + // 3) Optional Battery service + await peripheralManager.addService( + GATTService( + uuid: UUID.fromString('180F'), + isPrimary: true, + characteristics: [ + GATTCharacteristic.immutable( + uuid: UUID.fromString('2A19'), + value: Uint8List.fromList([100]), + descriptors: [], + ), + ], + includedServices: [], + ), + ); + _isServiceAdded = true; + } + + final advertisement = Advertisement( + name: + 'BikeControl ${Platform.isIOS + ? 'iOS' + : Platform.isAndroid + ? 'Android' + : ''}', + serviceUUIDs: [UUID.fromString(Platform.isIOS ? '1812' : '00001812-0000-1000-8000-00805F9B34FB')], + ); + /*pm.connectionStateChanged.forEach((state) { + print('Peripheral connection state: $state'); + });*/ + print('Starting advertising with HID service...'); + + await peripheralManager.startAdvertising(advertisement); + isAdvertisingPeripheral = true; + onUpdate(); + } + + @override + Widget? build(BuildContext context, VoidCallback onUpdate) { + return _PairWidget(onUpdate: onUpdate, requirement: this); + } + + @override + Future getStatus() async { + status = (actionHandler is RemoteActions && (actionHandler as RemoteActions).isConnected) || screenshotMode; + } +} + +class _PairWidget extends StatefulWidget { + final RemoteRequirement requirement; + final VoidCallback onUpdate; + const _PairWidget({super.key, required this.onUpdate, required this.requirement}); + + @override + State<_PairWidget> createState() => _PairWidgetState(); +} + +class _PairWidgetState extends State<_PairWidget> { + @override + Widget build(BuildContext context) { + return Column( + spacing: 16, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + spacing: 10, + children: [ + ElevatedButton( + onPressed: () async { + await toggle(); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isAdvertisingPeripheral + ? 'Stop Pairing process' + : 'Start connecting to\n${settings.getLastTarget()?.title ?? 'remote'} device', + ), + Text( + 'Pairing allows full customizability,\nbut may not work on all devices.', + style: TextStyle( + fontSize: 12, + color: Theme.brightnessOf(context) == Brightness.dark ? Colors.white70 : Colors.black87, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + BetaPill(), + ], + ), + ), + ), + if (isAdvertisingPeripheral || _isLoading) SizedBox(height: 20, width: 20, child: SmallProgressIndicator()), + ], + ), + if (isAdvertisingPeripheral) + Text( + switch (settings.getLastTarget()) { + Target.iOS => + 'On your iPad go to Settings > Accessibility > Touch > AssistiveTouch > Pointer Devices > Devices and pair your device. Make sure AssistiveTouch is enabled.', + _ => + 'On your ${settings.getLastTarget()?.title} go into Bluetooth settings and look for BikeControl or your machines name. Pairing is required if you want to use the remote control feature.', + }, + ), + if (isAdvertisingPeripheral) ...[ + TextButton( + onPressed: () { + Navigator.push(context, MaterialPageRoute(builder: (c) => MarkdownPage(assetPath: 'TROUBLESHOOTING.md'))); + }, + child: Text('Check the troubleshooting guide'), + ), + ], + if (settings.getTrainerApp() is MyWhoosh) + ElevatedButton( + onPressed: () async { + settings.setMyWhooshLinkEnabled(true); + Navigator.push( + context, + MaterialPageRoute( + builder: (c) => DevicePage(), + settings: RouteSettings(name: '/device'), + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Connect via MyWhoosh Direct Connect'), + Text( + 'Most reliable way to control MyWhoosh.', + style: TextStyle( + fontSize: 12, + color: Theme.brightnessOf(context) == Brightness.dark ? Colors.white70 : Colors.black87, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + ), + ), + + if (settings.getTrainerApp()?.supportsZwiftEmulation == true) + ElevatedButton( + onPressed: () async { + Navigator.push( + context, + MaterialPageRoute( + builder: (c) => DevicePage(), + settings: RouteSettings(name: '/device'), + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Connect to ${settings.getTrainerApp()?.name} as controller'), + Text( + 'Most reliable way to control ${settings.getTrainerApp()?.name}.', + style: TextStyle( + fontSize: 12, + color: Theme.brightnessOf(context) == Brightness.dark ? Colors.white70 : Colors.black87, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + ), + ), + ], + ); + } + + Future toggle() async { + if (isAdvertisingPeripheral) { + await peripheralManager.stopAdvertising(); + isAdvertisingPeripheral = false; + (actionHandler as RemoteActions).setConnectedCentral(null, null); + widget.onUpdate(); + _isLoading = false; + setState(() {}); + } else { + _isLoading = true; + setState(() {}); + await widget.requirement.startAdvertising(widget.onUpdate); + _isLoading = false; + if (mounted) setState(() {}); + } + } +} diff --git a/lib/utils/settings/settings.dart b/lib/utils/settings/settings.dart new file mode 100644 index 000000000..b484fb781 --- /dev/null +++ b/lib/utils/settings/settings.dart @@ -0,0 +1,265 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:dartx/dartx.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider_windows/path_provider_windows.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_preferences_windows/shared_preferences_windows.dart'; +import 'package:swift_control/utils/keymap/apps/supported_app.dart'; +import 'package:swift_control/utils/requirements/multi.dart'; +import 'package:window_manager/window_manager.dart'; + +import '../../main.dart'; +import '../actions/desktop.dart'; +import '../keymap/apps/custom_app.dart'; + +class Settings { + late final SharedPreferences prefs; + + Future init({bool retried = false}) async { + try { + prefs = await SharedPreferences.getInstance(); + initializeActions(getLastTarget()?.connectionType ?? ConnectionType.unknown); + + if (actionHandler is DesktopActions) { + // Must add this line. + await windowManager.ensureInitialized(); + } + + final app = getKeyMap(); + actionHandler.init(app); + return null; + } catch (e, s) { + if (!retried) { + if (Platform.isWindows) { + // delete settings file + final fs = SharedPreferencesWindows.instance.fs; + + final pathProvider = PathProviderWindows(); + final String? directory = await pathProvider.getApplicationSupportPath(); + if (directory == null) { + return null; + } + final String fileLocation = path.join(directory, 'shared_preferences.json'); + final file = fs.file(fileLocation); + if (await file.exists()) { + await file.delete(); + } + } + return init(retried: true); + } else { + return '$e\n$s'; + } + } + } + + Future reset() async { + await prefs.clear(); + actionHandler.init(null); + } + + void setTrainerApp(SupportedApp app) { + prefs.setString('trainer_app', app.name); + } + + SupportedApp? getTrainerApp() { + final appName = prefs.getString('trainer_app'); + if (appName == null) { + return null; + } + return SupportedApp.supportedApps.firstOrNullWhere((e) => e.name == appName); + } + + bool knowsAboutNameChange() { + final knows = prefs.getBool('name_change') == true; + prefs.setBool('name_change', true); + return knows; + } + + Future setKeyMap(SupportedApp app) async { + if (app is CustomApp) { + await prefs.setStringList('customapp_${app.profileName}', app.encodeKeymap()); + } + await prefs.setString('app', app.name); + } + + SupportedApp? getKeyMap() { + final appName = prefs.getString('app'); + if (appName == null) { + return null; + } + + // Check if it's a custom app with a profile name + if (appName.startsWith('Custom') || prefs.containsKey('customapp_$appName')) { + final customApp = CustomApp(profileName: appName); + final appSetting = prefs.getStringList('customapp_$appName'); + if (appSetting != null) { + customApp.decodeKeymap(appSetting); + } + return customApp; + } else { + return SupportedApp.supportedApps.firstOrNullWhere((e) => e.name == appName); + } + } + + List getCustomAppProfiles() { + // Get all keys starting with 'customapp_' + final keys = prefs.getKeys().where((key) => key.startsWith('customapp_')).toList(); + return keys.map((key) => key.replaceFirst('customapp_', '')).toList(); + } + + List? getCustomAppKeymap(String profileName) { + return prefs.getStringList('customapp_$profileName'); + } + + Future deleteCustomAppProfile(String profileName) async { + await prefs.remove('customapp_$profileName'); + // If the current app is the one being deleted, reset + if (prefs.getString('app') == profileName) { + actionHandler.init(null); + await prefs.remove('app'); + } + } + + Future duplicateCustomAppProfile(String sourceProfileName, String newProfileName) async { + final sourceData = prefs.getStringList('customapp_$sourceProfileName'); + if (sourceData != null) { + await prefs.setStringList('customapp_$newProfileName', sourceData); + } + } + + String? exportCustomAppProfile(String profileName) { + final data = prefs.getStringList('customapp_$profileName'); + if (data == null) return null; + var encoder = JsonEncoder.withIndent(" "); + return encoder.convert({ + 'version': 1, + 'profileName': profileName, + 'keymap': data.map((e) => jsonDecode(e)).toList(), + }); + } + + Future importCustomAppProfile(String jsonData, {String? newProfileName}) async { + try { + final decoded = jsonDecode(jsonData); + + // Validate the structure + if (decoded['version'] == null || decoded['keymap'] == null) { + return false; + } + + final profileName = newProfileName ?? decoded['profileName'] ?? 'Imported'; + final keymap = (decoded['keymap'] as List).map((e) => jsonEncode(e)).toList().cast(); + + await prefs.setStringList('customapp_$profileName', keymap); + return true; + } catch (e) { + print(e); + return false; + } + } + + String? getLastSeenVersion() { + return prefs.getString('last_seen_version'); + } + + Target? getLastTarget() { + final targetString = prefs.getString('last_target'); + if (targetString == null) return null; + return Target.values.firstOrNullWhere((e) => e.name == targetString); + } + + Future setLastTarget(Target target) async { + await prefs.setString('last_target', target.name); + initializeActions(target.connectionType); + } + + Future setLastSeenVersion(String version) async { + await prefs.setString('last_seen_version', version); + } + + bool getVibrationEnabled() { + return prefs.getBool('vibration_enabled') ?? true; + } + + Future setVibrationEnabled(bool enabled) async { + await prefs.setBool('vibration_enabled', enabled); + } + + bool getMyWhooshLinkEnabled() { + return prefs.getBool('mywhoosh_link_enabled') ?? true; + } + + Future setMyWhooshLinkEnabled(bool enabled) async { + await prefs.setBool('mywhoosh_link_enabled', enabled); + } + + bool getZwiftEmulatorEnabled() { + return prefs.getBool('zwift_emulator_enabled') ?? true; + } + + Future setZwiftEmulatorEnabled(bool enabled) async { + await prefs.setBool('zwift_emulator_enabled', enabled); + } + + bool getMiuiWarningDismissed() { + return prefs.getBool('miui_warning_dismissed') ?? false; + } + + Future setMiuiWarningDismissed(bool dismissed) async { + await prefs.setBool('miui_warning_dismissed', dismissed); + } + + List _getIgnoredDeviceIds() { + return prefs.getStringList('ignored_device_ids') ?? []; + } + + List _getIgnoredDeviceNames() { + return prefs.getStringList('ignored_device_names') ?? []; + } + + Future addIgnoredDevice(String deviceId, String deviceName) async { + final ids = _getIgnoredDeviceIds(); + final names = _getIgnoredDeviceNames(); + + if (!ids.contains(deviceId)) { + ids.add(deviceId); + names.add(deviceName); + await prefs.setStringList('ignored_device_ids', ids); + await prefs.setStringList('ignored_device_names', names); + } + } + + Future removeIgnoredDevice(String deviceId) async { + final ids = _getIgnoredDeviceIds(); + final names = _getIgnoredDeviceNames(); + + final index = ids.indexOf(deviceId); + if (index != -1) { + ids.removeAt(index); + names.removeAt(index); + await prefs.setStringList('ignored_device_ids', ids); + await prefs.setStringList('ignored_device_names', names); + } + } + + List<({String id, String name})> getIgnoredDevices() { + final ids = _getIgnoredDeviceIds(); + final names = _getIgnoredDeviceNames(); + + final result = <({String id, String name})>[]; + for (int i = 0; i < ids.length && i < names.length; i++) { + result.add((id: ids[i], name: names[i])); + } + return result; + } + + bool getShowZwiftClickV2ReconnectWarning() { + return prefs.getBool('zwift_click_v2_reconnect_warning') ?? true; + } + + Future setShowZwiftClickV2ReconnectWarning(bool show) async { + await prefs.setBool('zwift_click_v2_reconnect_warning', show); + } +} diff --git a/lib/utils/single_line_exception.dart b/lib/utils/single_line_exception.dart new file mode 100644 index 000000000..b27d7dce5 --- /dev/null +++ b/lib/utils/single_line_exception.dart @@ -0,0 +1,10 @@ +class SingleLineException implements Exception { + final String message; + + SingleLineException(this.message); + + @override + String toString() { + return message; + } +} diff --git a/lib/widgets/accessibility_disclosure_dialog.dart b/lib/widgets/accessibility_disclosure_dialog.dart new file mode 100644 index 000000000..6db833282 --- /dev/null +++ b/lib/widgets/accessibility_disclosure_dialog.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:swift_control/main.dart'; + +class AccessibilityDisclosureDialog extends StatelessWidget { + final VoidCallback onAccept; + final VoidCallback onDeny; + + const AccessibilityDisclosureDialog({ + super.key, + required this.onAccept, + required this.onDeny, + }); + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, // Prevent back navigation from dismissing dialog + child: AlertDialog( + title: const Text('Accessibility Service Permission Required'), + content: const SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'BikeControl needs to use Android\'s AccessibilityService API to function properly.', + style: TextStyle(fontWeight: FontWeight.bold), + ), + SizedBox(height: 16), + Text('Why is this permission needed?'), + SizedBox(height: 8), + Text('• To simulate touch gestures on your screen for controlling trainer apps'), + Text('• To detect which training app window is currently active'), + Text('• To enable you to control apps like MyWhoosh, IndieVelo, and others using your Zwift devices'), + SizedBox(height: 16), + Text( + 'How does BikeControl use this permission?', + style: TextStyle(fontWeight: FontWeight.w600), + ), + SizedBox(height: 8), + Text('• When you press buttons on your Zwift Click, Zwift Ride, or Zwift Play devices, BikeControl simulates touch gestures at specific screen locations'), + Text('• The app monitors which training app window is active to ensure gestures are sent to the correct app'), + Text('• No personal data is accessed or collected through this service'), + SizedBox(height: 16), + Text( + 'BikeControl will only access your screen to perform the gestures you configure. No other accessibility features or personal information will be accessed.', + style: TextStyle(fontStyle: FontStyle.italic), + ), + SizedBox(height: 16), + Text( + 'You must choose to either Allow or Deny this permission to continue.', + style: TextStyle(fontWeight: FontWeight.w600, color: Colors.deepOrange), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: onDeny, + style: TextButton.styleFrom( + foregroundColor: Colors.red, + ), + child: const Text('Deny'), + ), + ElevatedButton( + onPressed: onAccept, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + child: const Text('Allow'), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/apps/mywhoosh_link_tile.dart b/lib/widgets/apps/mywhoosh_link_tile.dart new file mode 100644 index 000000000..1ce7b8846 --- /dev/null +++ b/lib/widgets/apps/mywhoosh_link_tile.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/actions/remote.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +import '../small_progress_indicator.dart'; + +class MyWhooshLinkTile extends StatefulWidget { + const MyWhooshLinkTile({super.key}); + + @override + State createState() => _MywhooshLinkTileState(); +} + +class _MywhooshLinkTileState extends State { + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: whooshLink.isStarted, + builder: (context, isStarted, _) { + return ValueListenableBuilder( + valueListenable: whooshLink.isConnected, + builder: (context, isConnected, _) { + return StatefulBuilder( + builder: (context, setState) { + final myWhooshExplanation = actionHandler is RemoteActions + ? 'MyWhoosh Direct Connect allows you to do some additional features such as Emotes and turn directions.' + : 'MyWhoosh Direct Connect is optional, but allows you to do some additional features such as Emotes and turn directions.'; + return Row( + children: [ + Expanded( + child: SwitchListTile.adaptive( + contentPadding: EdgeInsets.zero, + value: isStarted, + onChanged: (value) { + settings.setMyWhooshLinkEnabled(value); + if (!value) { + whooshLink.stopServer(); + } else if (value) { + connection.startMyWhooshServer().catchError((e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Error starting MyWhoosh Direct Connect server. Please make sure the "MyWhoosh Link" app is not already running on this device.', + ), + ), + ); + }); + } + setState(() {}); + }, + title: Text('Enable MyWhoosh Direct Connect'), + subtitle: Row( + spacing: 12, + children: [ + if (!isStarted) + Expanded( + child: Text( + myWhooshExplanation, + style: TextStyle(fontSize: 12), + ), + ) + else ...[ + Expanded( + child: Text( + isConnected ? "Connected" : "Connecting to MyWhoosh...\n$myWhooshExplanation", + + style: TextStyle(fontSize: 12), + ), + ), + if (isStarted) SmallProgressIndicator(), + ], + ], + ), + ), + ), + + IconButton( + onPressed: () { + launchUrlString('https://www.youtube.com/watch?v=p8sgQhuufeI'); + }, + icon: Icon(Icons.help_outline), + ), + ], + ); + }, + ); + }, + ); + }, + ); + } +} diff --git a/lib/widgets/apps/zwift_tile.dart b/lib/widgets/apps/zwift_tile.dart new file mode 100644 index 000000000..baedee6bb --- /dev/null +++ b/lib/widgets/apps/zwift_tile.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_emulator.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/keymap/apps/zwift.dart'; +import 'package:swift_control/widgets/small_progress_indicator.dart'; + +class ZwiftTile extends StatefulWidget { + final VoidCallback onUpdate; + + const ZwiftTile({super.key, required this.onUpdate}); + + @override + State createState() => _ZwiftTileState(); +} + +class _ZwiftTileState extends State { + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: zwiftEmulator.isConnected, + builder: (context, isConnected, _) { + return StatefulBuilder( + builder: (context, setState) { + return SwitchListTile.adaptive( + contentPadding: EdgeInsets.zero, + value: settings.getZwiftEmulatorEnabled(), + onChanged: (value) { + settings.setZwiftEmulatorEnabled(value); + if (!value) { + zwiftEmulator.stopAdvertising(); + } else if (value) { + zwiftEmulator.startAdvertising(widget.onUpdate); + } + setState(() {}); + }, + title: Text('Enable Zwift Controller'), + subtitle: Row( + spacing: 12, + children: [ + if (!settings.getZwiftEmulatorEnabled()) + Expanded( + child: Text( + 'Disabled. ${settings.getTrainerApp() is Zwift ? 'Virtual shifting and on screen navigation will not work.' : ''}', + ), + ) + else ...[ + Expanded( + child: Text( + isConnected + ? "Connected" + : "Waiting for connection. Choose BikeControl in ${settings.getTrainerApp()?.name}'s controller pairing menu.", + ), + ), + if (!isConnected) SmallProgressIndicator(), + ], + ], + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/widgets/beta_pill.dart b/lib/widgets/beta_pill.dart new file mode 100644 index 000000000..46e164954 --- /dev/null +++ b/lib/widgets/beta_pill.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +class BetaPill extends StatelessWidget { + final String text; + const BetaPill({super.key, this.text = 'BETA'}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.orange, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + text, + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } +} diff --git a/lib/widgets/button_widget.dart b/lib/widgets/button_widget.dart new file mode 100644 index 000000000..7140472c0 --- /dev/null +++ b/lib/widgets/button_widget.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/widgets/keymap_explanation.dart'; + +class ButtonWidget extends StatelessWidget { + final ControllerButton button; + final bool big; + const ButtonWidget({super.key, required this.button, this.big = false}); + + @override + Widget build(BuildContext context) { + return IntrinsicWidth( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), + constraints: BoxConstraints( + minWidth: big && button.color != null ? 40 : 30, + minHeight: big && button.color != null ? 40 : 0, + ), + decoration: BoxDecoration( + border: Border.all(color: button.color != null ? Colors.black : Theme.of(context).colorScheme.primary), + shape: button.color != null || button.icon != null ? BoxShape.circle : BoxShape.rectangle, + borderRadius: button.color != null || button.icon != null ? null : BorderRadius.circular(4), + color: button.color ?? Theme.of(context).colorScheme.primaryContainer, + ), + child: Center( + child: button.icon != null + ? Icon( + button.icon, + color: Colors.white, + size: big && button.color != null ? null : 14, + ) + : Text( + button.name.splitByUpperCase(), + style: TextStyle( + fontFamily: screenshotMode ? null : 'monospace', + fontSize: big && button.color != null ? 20 : 12, + fontWeight: button.color != null ? FontWeight.bold : null, + color: button.color != null ? Colors.white : Theme.of(context).colorScheme.onPrimaryContainer, + ), + ), + ), + ), + ); + } +} diff --git a/lib/widgets/changelog_dialog.dart b/lib/widgets/changelog_dialog.dart new file mode 100644 index 000000000..980dd260d --- /dev/null +++ b/lib/widgets/changelog_dialog.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_md/flutter_md.dart'; +import 'package:swift_control/main.dart'; + +class ChangelogDialog extends StatelessWidget { + final Markdown entry; + + const ChangelogDialog({super.key, required this.entry}); + + @override + Widget build(BuildContext context) { + final latestVersion = Markdown( + blocks: entry.blocks.skip(1).takeWhile((b) => b.type != 'heading').toList(), + markdown: entry.markdown, + ); + return AlertDialog( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text('What\'s New'), + SizedBox(height: 4), + Text( + 'Version ${entry.blocks.first.text}', + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.normal), + ), + ], + ), + content: Container( + constraints: BoxConstraints(minWidth: 460), + child: MarkdownWidget(markdown: latestVersion), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('Got it!'), + ), + ], + ); + } + + static Future showIfNeeded(BuildContext context, String currentVersion, String? lastSeenVersion) async { + // Show dialog if this is a new version + if (lastSeenVersion != currentVersion && !screenshotMode) { + try { + final entry = await rootBundle.loadString('CHANGELOG.md'); + if (context.mounted) { + final markdown = Markdown.fromString(entry); + showDialog( + context: context, + useRootNavigator: true, + routeSettings: RouteSettings(name: '/changelog'), + builder: (context) => ChangelogDialog(entry: markdown), + ); + } + } catch (e) { + print('Failed to load changelog for dialog: $e'); + } + } + } +} diff --git a/lib/widgets/custom_keymap_selector.dart b/lib/widgets/custom_keymap_selector.dart new file mode 100644 index 000000000..8fd8e122b --- /dev/null +++ b/lib/widgets/custom_keymap_selector.dart @@ -0,0 +1,161 @@ +import 'dart:async'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:swift_control/bluetooth/messages/notification.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/utils/keymap/keymap.dart'; + +import '../utils/keymap/apps/custom_app.dart'; + +class HotKeyListenerDialog extends StatefulWidget { + final CustomApp customApp; + final KeyPair? keyPair; + const HotKeyListenerDialog({super.key, required this.customApp, required this.keyPair}); + + @override + State createState() => _HotKeyListenerState(); +} + +class _HotKeyListenerState extends State { + late StreamSubscription _actionSubscription; + + final FocusNode _focusNode = FocusNode(); + KeyDownEvent? _pressedKey; + ControllerButton? _pressedButton; + final Set _activeModifiers = {}; + + @override + void initState() { + super.initState(); + _pressedButton = widget.keyPair?.buttons.firstOrNull; + _actionSubscription = connection.actionStream.listen((data) { + if (!mounted || widget.keyPair != null) { + return; + } + if (data is ButtonNotification) { + setState(() { + _pressedButton = data.buttonsClicked.singleOrNull; + }); + } + }); + _focusNode.requestFocus(); + } + + @override + void dispose() { + _actionSubscription.cancel(); + _focusNode.dispose(); + super.dispose(); + } + + void _onKey(KeyEvent event) { + setState(() { + // Track modifier keys + if (event is KeyDownEvent) { + final wasModifier = _updateModifierState(event.logicalKey, add: true); + // Regular key pressed - record it along with active modifiers + if (!wasModifier) { + if (_pressedKey?.logicalKey != event.logicalKey) {} + _pressedKey = event; + widget.customApp.setKey( + _pressedButton!, + physicalKey: _pressedKey!.physicalKey, + logicalKey: _pressedKey!.logicalKey, + modifiers: _activeModifiers.toList(), + touchPosition: widget.keyPair?.touchPosition, + ); + } + } else if (event is KeyUpEvent) { + // Clear modifier when released + _updateModifierState(event.logicalKey, add: false); + } + }); + } + + bool _updateModifierState(LogicalKeyboardKey key, {required bool add}) { + ModifierKey? modifier; + + if (key == LogicalKeyboardKey.shift || + key == LogicalKeyboardKey.shiftLeft || + key == LogicalKeyboardKey.shiftRight) { + modifier = ModifierKey.shiftModifier; + } else if (key == LogicalKeyboardKey.control || + key == LogicalKeyboardKey.controlLeft || + key == LogicalKeyboardKey.controlRight) { + modifier = ModifierKey.controlModifier; + } else if (key == LogicalKeyboardKey.alt || + key == LogicalKeyboardKey.altLeft || + key == LogicalKeyboardKey.altRight) { + modifier = ModifierKey.altModifier; + } else if (key == LogicalKeyboardKey.meta || + key == LogicalKeyboardKey.metaLeft || + key == LogicalKeyboardKey.metaRight) { + modifier = ModifierKey.metaModifier; + } else if (key == LogicalKeyboardKey.fn) { + modifier = ModifierKey.functionModifier; + } + + if (modifier != null) { + if (add) { + _activeModifiers.add(modifier); + } else { + _activeModifiers.remove(modifier); + } + return true; + } + return false; + } + + String _formatModifierName(ModifierKey m) { + return switch (m) { + ModifierKey.shiftModifier => 'Shift', + ModifierKey.controlModifier => 'Ctrl', + ModifierKey.altModifier => 'Alt', + ModifierKey.metaModifier => 'Meta', + ModifierKey.functionModifier => 'Fn', + _ => m.name, + }; + } + + String _formatKey(KeyDownEvent? key) { + if (key == null) { + return _activeModifiers.isEmpty ? 'Waiting...' : '${_activeModifiers.map(_formatModifierName).join('+')}+...'; + } + + if (_activeModifiers.isEmpty) { + return key.logicalKey.keyLabel; + } + + final modifierStrings = _activeModifiers.map(_formatModifierName); + + return '${modifierStrings.join('+')}+${key.logicalKey.keyLabel}'; + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + content: _pressedButton == null + ? Text('Press a button on your Click device') + : KeyboardListener( + focusNode: _focusNode, + autofocus: true, + onKeyEvent: _onKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + spacing: 20, + children: [ + Text("Press a key on your keyboard to assign to ${_pressedButton.toString()}"), + Text(_formatKey(_pressedKey)), + ], + ), + ), + + actions: [TextButton(onPressed: () => Navigator.of(context).pop(_pressedKey), child: Text("OK"))], + ); + } +} diff --git a/lib/widgets/ignored_devices_dialog.dart b/lib/widgets/ignored_devices_dialog.dart new file mode 100644 index 000000000..27f384edb --- /dev/null +++ b/lib/widgets/ignored_devices_dialog.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:swift_control/main.dart'; + +class IgnoredDevicesDialog extends StatefulWidget { + const IgnoredDevicesDialog({super.key}); + + @override + State createState() => _IgnoredDevicesDialogState(); +} + +class _IgnoredDevicesDialogState extends State { + List<({String id, String name})> _ignoredDevices = []; + + @override + void initState() { + super.initState(); + _loadIgnoredDevices(); + } + + void _loadIgnoredDevices() { + setState(() { + _ignoredDevices = settings.getIgnoredDevices(); + }); + } + + Future _removeDevice(String deviceId) async { + await settings.removeIgnoredDevice(deviceId); + _loadIgnoredDevices(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text('Ignored Devices'), + content: SizedBox( + width: double.maxFinite, + child: _ignoredDevices.isEmpty + ? Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + 'No ignored devices.', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ) + : ListView.builder( + shrinkWrap: true, + itemCount: _ignoredDevices.length, + itemBuilder: (context, index) { + final device = _ignoredDevices[index]; + return ListTile( + title: Text(device.name), + subtitle: Text( + device.id, + style: TextStyle(fontSize: 12), + ), + trailing: IconButton( + icon: Icon(Icons.delete_outline), + tooltip: 'Remove from ignored list', + onPressed: () => _removeDevice(device.id), + ), + ); + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('Close'), + ), + ], + ); + } +} diff --git a/lib/widgets/keymap_explanation.dart b/lib/widgets/keymap_explanation.dart new file mode 100644 index 000000000..5bc2738f4 --- /dev/null +++ b/lib/widgets/keymap_explanation.dart @@ -0,0 +1,532 @@ +import 'dart:async'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_emulator.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/pages/device.dart'; +import 'package:swift_control/utils/actions/base_actions.dart'; +import 'package:swift_control/utils/keymap/apps/custom_app.dart'; +import 'package:swift_control/utils/keymap/keymap.dart'; +import 'package:swift_control/utils/keymap/manager.dart'; +import 'package:swift_control/widgets/button_widget.dart'; +import 'package:swift_control/widgets/custom_keymap_selector.dart'; + +import '../bluetooth/devices/link/link.dart'; +import '../pages/touch_area.dart'; + +class KeymapExplanation extends StatefulWidget { + final Keymap keymap; + final VoidCallback onUpdate; + const KeymapExplanation({super.key, required this.keymap, required this.onUpdate}); + + @override + State createState() => _KeymapExplanationState(); +} + +class _KeymapExplanationState extends State { + late StreamSubscription _updateStreamListener; + + @override + void initState() { + super.initState(); + _updateStreamListener = widget.keymap.updateStream.listen((_) { + setState(() {}); + }); + } + + @override + void didUpdateWidget(KeymapExplanation oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.keymap != widget.keymap) { + _updateStreamListener.cancel(); + _updateStreamListener = widget.keymap.updateStream.listen((_) { + setState(() {}); + }); + } + } + + @override + void dispose() { + super.dispose(); + _updateStreamListener.cancel(); + } + + @override + Widget build(BuildContext context) { + final availableKeypairs = widget.keymap.keyPairs; + final allAvailableButtons = connection.devices.flatMap((d) => d.availableButtons); + + return ValueListenableBuilder( + valueListenable: whooshLink.isConnected, + builder: (c, _, _) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + spacing: 8, + children: [ + Table( + border: TableBorder.symmetric( + borderRadius: BorderRadius.circular(9), + inside: BorderSide( + color: Theme.of(context).colorScheme.primaryContainer, + ), + outside: BorderSide( + color: Theme.of(context).colorScheme.primaryContainer, + ), + ), + children: [ + TableRow( + children: [ + Padding( + padding: const EdgeInsets.all(6), + child: Text( + 'Button on your ${connection.devices.isEmpty ? 'Device' : connection.devices.joinToString(transform: (d) => d.name.screenshot)}', + style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: const EdgeInsets.all(6), + child: Text( + 'Action', + style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold), + ), + ), + ], + ), + for (final keyPair in availableKeypairs) ...[ + TableRow( + children: [ + TableCell( + verticalAlignment: TableCellVerticalAlignment.middle, + child: Container( + padding: const EdgeInsets.all(6), + child: Wrap( + spacing: 8, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + if (actionHandler.supportedApp is! CustomApp) + if (keyPair.buttons.filter((b) => allAvailableButtons.contains(b)).isEmpty) + Text('No button assigned for your connected device') + else + for (final button in keyPair.buttons.filter((b) => allAvailableButtons.contains(b))) + IntrinsicWidth(child: ButtonWidget(button: button)) + else + for (final button in keyPair.buttons) IntrinsicWidth(child: ButtonWidget(button: button)), + ], + ), + ), + ), + TableCell( + verticalAlignment: TableCellVerticalAlignment.middle, + child: Padding( + padding: const EdgeInsets.all(6), + child: _ButtonEditor(keyPair: keyPair, onUpdate: widget.onUpdate), + ), + ), + ], + ), + ], + ], + ), + ], + ), + ); + } +} + +class _ButtonEditor extends StatelessWidget { + final KeyPair keyPair; + final VoidCallback onUpdate; + const _ButtonEditor({required this.onUpdate, super.key, required this.keyPair}); + + @override + Widget build(BuildContext context) { + final trainerApp = settings.getTrainerApp(); + final actions = [ + if (settings.getMyWhooshLinkEnabled() && whooshLink.isCompatible(settings.getLastTarget()!)) + PopupMenuItem( + child: PopupMenuButton( + itemBuilder: (_) => WhooshLink.supportedActions.map( + (ingame) { + return PopupMenuItem( + value: ingame, + child: ingame.possibleValues != null + ? PopupMenuButton( + itemBuilder: (c) => ingame.possibleValues! + .map( + (value) => PopupMenuItem( + value: value, + child: Text(value.toString()), + onTap: () { + keyPair.inGameAction = ingame; + keyPair.inGameActionValue = value; + onUpdate(); + }, + ), + ) + .toList(), + child: Row( + children: [ + Expanded(child: Text(ingame.toString())), + Icon(Icons.arrow_right), + ], + ), + ) + : Text(ingame.toString()), + onTap: () { + keyPair.inGameAction = ingame; + keyPair.inGameActionValue = null; + onUpdate(); + }, + ); + }, + ).toList(), + child: SizedBox( + height: 52, + child: Row( + spacing: 14, + children: [ + Icon(Icons.link), + Expanded(child: Text('MyWhoosh Direct Connect Action')), + Icon(Icons.arrow_right), + ], + ), + ), + ), + ), + if (settings.getZwiftEmulatorEnabled() && settings.getTrainerApp()?.supportsZwiftEmulation == true) + PopupMenuItem( + child: PopupMenuButton( + itemBuilder: (_) => ZwiftEmulator.supportedActions.map( + (ingame) { + return PopupMenuItem( + value: ingame, + child: ingame.possibleValues != null + ? PopupMenuButton( + itemBuilder: (c) => ingame.possibleValues! + .map( + (value) => PopupMenuItem( + value: value, + child: Text(value.toString()), + onTap: () { + keyPair.inGameAction = ingame; + keyPair.inGameActionValue = value; + onUpdate(); + }, + ), + ) + .toList(), + child: Row( + children: [ + Expanded(child: Text(ingame.toString())), + Icon(Icons.arrow_right), + ], + ), + ) + : Text(ingame.toString()), + onTap: () { + keyPair.inGameAction = ingame; + keyPair.inGameActionValue = null; + onUpdate(); + }, + ); + }, + ).toList(), + child: SizedBox( + height: 52, + child: Row( + spacing: 14, + children: [ + Icon(Icons.link), + Expanded(child: Text('Zwift Controller Action')), + Icon(Icons.arrow_right), + ], + ), + ), + ), + ), + if (trainerApp != null && trainerApp is! CustomApp) + PopupMenuItem( + child: PopupMenuButton( + itemBuilder: (_) { + // Get actions from the current trainer app's keymap that have inGameAction + final actionsWithInGameAction = trainerApp.keymap.keyPairs + .where((kp) => kp.inGameAction != null) + .toList(); + + if (actionsWithInGameAction.isEmpty) { + return [ + PopupMenuItem( + enabled: false, + child: Text('No predefined actions available'), + ), + ]; + } + + return actionsWithInGameAction.map((keyPairAction) { + return PopupMenuItem( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text(_formatActionDescription(keyPairAction).split(' = ').first), + Text( + _formatActionDescription(keyPairAction).split(' = ').last, + style: TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ), + onTap: () { + // Copy all properties from the selected predefined action + keyPair.physicalKey = keyPairAction.physicalKey; + keyPair.logicalKey = keyPairAction.logicalKey; + keyPair.modifiers = List.of(keyPairAction.modifiers); + keyPair.touchPosition = keyPairAction.touchPosition; + keyPair.isLongPress = keyPairAction.isLongPress; + keyPair.inGameAction = keyPairAction.inGameAction; + keyPair.inGameActionValue = keyPairAction.inGameActionValue; + onUpdate(); + }, + ); + }).toList(); + }, + child: SizedBox( + height: 52, + child: Row( + spacing: 14, + children: [ + Icon(Icons.file_copy_outlined), + Expanded(child: Text('${trainerApp.name} action')), + Icon(Icons.arrow_right), + ], + ), + ), + ), + ), + if (actionHandler.supportedModes.contains(SupportedMode.keyboard)) + PopupMenuItem( + value: null, + child: ListTile( + leading: Icon(Icons.keyboard_alt_outlined), + title: const Text('Simulate Keyboard shortcut'), + trailing: keyPair.physicalKey != null ? Checkbox(value: true, onChanged: null) : null, + ), + onTap: () async { + await showDialog( + context: context, + barrierDismissible: false, // enable Escape key + builder: (c) => + HotKeyListenerDialog(customApp: actionHandler.supportedApp! as CustomApp, keyPair: keyPair), + ); + onUpdate(); + }, + ), + if (actionHandler.supportedModes.contains(SupportedMode.touch)) + PopupMenuItem( + value: null, + child: ListTile( + title: const Text('Simulate Touch'), + leading: Icon(Icons.touch_app_outlined), + trailing: keyPair.physicalKey == null && keyPair.touchPosition != Offset.zero + ? Checkbox(value: true, onChanged: null) + : null, + ), + onTap: () async { + if (keyPair.touchPosition == Offset.zero) { + keyPair.touchPosition = Offset(50, 50); + } + keyPair.physicalKey = null; + keyPair.logicalKey = null; + await Navigator.of(context).push( + MaterialPageRoute( + builder: (c) => TouchAreaSetupPage( + keyPair: keyPair, + ), + ), + ); + onUpdate(); + }, + ), + + if (actionHandler.supportedModes.contains(SupportedMode.media)) + PopupMenuItem( + child: PopupMenuButton( + padding: EdgeInsets.zero, + itemBuilder: (context) => [ + PopupMenuItem( + value: PhysicalKeyboardKey.mediaPlayPause, + child: const Text('Media: Play/Pause'), + ), + PopupMenuItem( + value: PhysicalKeyboardKey.mediaStop, + child: const Text('Media: Stop'), + ), + PopupMenuItem( + value: PhysicalKeyboardKey.mediaTrackPrevious, + child: const Text('Media: Previous'), + ), + PopupMenuItem( + value: PhysicalKeyboardKey.mediaTrackNext, + child: const Text('Media: Next'), + ), + PopupMenuItem( + value: PhysicalKeyboardKey.audioVolumeUp, + child: const Text('Media: Volume Up'), + ), + PopupMenuItem( + value: PhysicalKeyboardKey.audioVolumeDown, + child: const Text('Media: Volume Down'), + ), + ], + onSelected: (key) { + keyPair.physicalKey = key; + keyPair.logicalKey = null; + + onUpdate(); + }, + child: ListTile( + leading: Icon(Icons.music_note_outlined), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (keyPair.isSpecialKey) Checkbox(value: true, onChanged: null), + Icon(Icons.arrow_right), + ], + ), + title: Text('Simulate Media key'), + ), + ), + ), + + PopupMenuDivider(), + PopupMenuItem( + value: null, + onTap: () { + keyPair.isLongPress = !keyPair.isLongPress; + onUpdate(); + }, + padding: EdgeInsets.zero, + child: Row( + spacing: 6, + children: [ + Checkbox( + value: keyPair.isLongPress, + onChanged: (value) { + keyPair.isLongPress = value ?? false; + + onUpdate(); + Navigator.of(context).pop(); + }, + ), + const Text('Long Press Mode (vs. repeating)'), + ], + ), + ), + PopupMenuItem( + value: null, + onTap: () { + keyPair.isLongPress = false; + keyPair.physicalKey = null; + keyPair.logicalKey = null; + keyPair.modifiers = []; + keyPair.touchPosition = Offset.zero; + keyPair.inGameAction = null; + keyPair.inGameActionValue = null; + onUpdate(); + }, + child: Row( + spacing: 14, + children: [ + Icon(Icons.delete_outline), + const Text('Unassign action'), + ], + ), + ), + ]; + + return Container( + constraints: BoxConstraints(minHeight: kMinInteractiveDimension - 6), + padding: EdgeInsets.only(right: actionHandler.supportedApp is CustomApp ? 4 : 0), + child: PopupMenuButton( + itemBuilder: (c) => actions, + enabled: actionHandler.supportedApp is CustomApp, + child: InkWell( + onTap: actionHandler.supportedApp is! CustomApp + ? () async { + final currentProfile = actionHandler.supportedApp!.name; + final newName = await KeymapManager().duplicate( + context, + currentProfile, + skipName: '$currentProfile (Copy)', + ); + if (newName != null) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Created a new custom profile: $newName'))); + } + onUpdate(); + } + : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 6, + children: [ + if (keyPair.buttons.isNotEmpty && + (keyPair.physicalKey != null || keyPair.touchPosition != Offset.zero || keyPair.inGameAction != null)) + Expanded( + child: KeypairExplanation( + keyPair: keyPair, + ), + ) + else + Expanded( + child: Text( + 'No action assigned', + style: TextStyle(color: Colors.grey), + ), + ), + Icon(Icons.edit, size: 14), + ], + ), + ), + ), + ); + } + + String _formatActionDescription(KeyPair keyPairAction) { + final parts = []; + + if (keyPairAction.inGameAction != null) { + parts.add(keyPairAction.inGameAction!.toString()); + if (keyPairAction.inGameActionValue != null) { + parts.add('(${keyPairAction.inGameActionValue})'); + } + } + + // Use KeyPair's toString() which formats the key with modifiers (e.g., "Ctrl+Alt+R") + final keyLabel = keyPairAction.toString(); + if (keyLabel != 'Not assigned') { + parts.add('Key: $keyLabel'); + } + + if (keyPairAction.touchPosition != Offset.zero) { + parts.add( + 'Touch: (${keyPairAction.touchPosition.dx.toStringAsFixed(1)}, ${keyPairAction.touchPosition.dy.toStringAsFixed(1)})', + ); + } + + if (keyPairAction.isLongPress) { + parts.add('[Long Press]'); + } + + return parts.isNotEmpty ? [parts.first, ' = ', parts.skip(1).join(' • ')].join() : 'Action'; + } +} + +extension SplitByUppercase on String { + String splitByUpperCase() { + return replaceAllMapped(RegExp(r'([a-z])([A-Z])'), (match) => '${match.group(1)} ${match.group(2)}').capitalize(); + } +} diff --git a/lib/widgets/loading_widget.dart b/lib/widgets/loading_widget.dart new file mode 100644 index 000000000..176c92553 --- /dev/null +++ b/lib/widgets/loading_widget.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; + +typedef RenderLoadCallback = Widget Function(); +typedef OnErrorCallback = void Function(BuildContext context, dynamic error); +typedef OnLoadCallback = void Function(bool isLoading); +typedef RenderChildCallback = Widget Function(bool isLoading, VoidCallback? onTap); +typedef FutureCallback = Future Function(); + +enum LoadingState { Error, Loading, Success } + +class LoadingWidget extends StatefulWidget { + const LoadingWidget({ + super.key, + this.renderLoad, + this.renderChild, + this.onErrorCallback, + this.futureCallback, + this.onLoadCallback, + }); + + final RenderLoadCallback? renderLoad; + final RenderChildCallback? renderChild; + final OnErrorCallback? onErrorCallback; + final OnLoadCallback? onLoadCallback; + final FutureCallback? futureCallback; + + @override + State createState() => LoadingWidgetState(); +} + +class LoadingWidgetState extends State { + var _loadingState = LoadingState.Success; + dynamic _error; + + Future reloadState() { + return _initState(); + } + + Future _initState() async { + if (!mounted) { + return; + } + + if (widget.onLoadCallback != null) { + widget.onLoadCallback!(true); + } + setState(() { + _loadingState = LoadingState.Loading; + }); + + try { + await widget.futureCallback!(); + + if (!mounted) { + return; + } + + if (widget.onLoadCallback != null) { + widget.onLoadCallback!(false); + } + + setState(() { + _loadingState = LoadingState.Success; + }); + } catch (e) { + if (widget.onLoadCallback != null) { + widget.onLoadCallback!(false); + } + debugPrint(e.toString()); + if (mounted) { + setState(() { + _error = e; + _loadingState = LoadingState.Error; + if (widget.onErrorCallback != null) { + widget.onErrorCallback!(context, _error); + } else { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(_error.toString()))); + } + }); + } + } + } + + @override + Widget build(BuildContext context) { + if (_loadingState == LoadingState.Loading && widget.renderLoad != null) { + return widget.renderLoad!(); + } + + final isLoading = _loadingState == LoadingState.Loading; + return widget.renderChild!(isLoading, isLoading ? null : () => reloadState()); + } +} diff --git a/lib/widgets/logviewer.dart b/lib/widgets/logviewer.dart new file mode 100644 index 000000000..0eed6576e --- /dev/null +++ b/lib/widgets/logviewer.dart @@ -0,0 +1,131 @@ +import 'dart:async'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../bluetooth/messages/notification.dart'; +import '../main.dart'; + +class LogViewer extends StatefulWidget { + const LogViewer({super.key}); + + @override + State createState() => _LogviewerState(); +} + +class _LogviewerState extends State { + List<({DateTime date, String entry})> _actions = []; + + late StreamSubscription _actionSubscription; + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + + _actionSubscription = connection.actionStream.listen((data) { + if (mounted) { + if (data is BluetoothAvailabilityNotification) { + if (!data.isAvailable && Navigator.canPop(context)) { + Navigator.popUntil(context, (route) => route.isFirst); + } + } + setState(() { + _actions.add((date: DateTime.now(), entry: data.toString())); + _actions = _actions.takeLast(kIsWeb ? 1000 : 60).toList(); + }); + if (_scrollController.hasClients) { + // scroll to the bottom + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 60), + curve: Curves.easeInOut, + ); + } + } + }); + } + + @override + void dispose() { + _actionSubscription.cancel(); + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _actions.isEmpty + ? Container() + : SafeArea( + child: Card( + child: Padding( + padding: const EdgeInsets.all(8), + child: Stack( + children: [ + SelectionArea( + child: GestureDetector( + onLongPress: () { + setState(() { + _actions = []; + }); + }, + child: ListView( + physics: const NeverScrollableScrollPhysics(), + controller: _scrollController, + shrinkWrap: true, + reverse: true, + children: _actions + .map( + (action) => Text.rich( + TextSpan( + children: [ + TextSpan( + text: action.date.toString().split(" ").last, + style: TextStyle( + fontSize: 12, + fontFeatures: [FontFeature.tabularFigures()], + fontFamily: "monospace", + fontFamilyFallback: ["Courier"], + ), + ), + TextSpan( + text: " ${action.entry}", + style: TextStyle( + fontSize: 12, + fontFeatures: [FontFeature.tabularFigures()], + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ) + .toList(), + ), + ), + ), + Align( + alignment: Alignment.topRight, + child: IconButton( + onPressed: () { + final logText = _actions + .map((e) => "${e.date.toString().split(" ").last} ${e.entry}") + .join("\n"); + Clipboard.setData(ClipboardData(text: logText)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Log copied to clipboard')), + ); + }, + icon: Icon(Icons.copy), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/menu.dart b/lib/widgets/menu.dart new file mode 100644 index 000000000..463db8c8f --- /dev/null +++ b/lib/widgets/menu.dart @@ -0,0 +1,215 @@ +import 'dart:io'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/pages/markdown.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/widgets/title.dart'; +import 'package:universal_ble/universal_ble.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +import '../bluetooth/devices/zwift/zwift_click.dart'; +import 'ignored_devices_dialog.dart'; + +List buildMenuButtons() { + return [ + if (kIsWeb || (!Platform.isIOS && !Platform.isMacOS)) ...[ + PopupMenuButton( + itemBuilder: (BuildContext context) { + return [ + PopupMenuItem( + child: Text('via Credit Card, Google Pay, Apple Pay and others'), + onTap: () { + final currency = NumberFormat.simpleCurrency(locale: kIsWeb ? 'de_DE' : Platform.localeName); + final link = switch (currency.currencyName) { + 'USD' => 'https://donate.stripe.com/8x24gzc5c4ZE3VJdt36J201', + _ => 'https://donate.stripe.com/9B6aEX0muajY8bZ1Kl6J200', + }; + launchUrlString(link); + }, + ), + if (!kIsWeb && Platform.isAndroid && isFromPlayStore == false) + PopupMenuItem( + child: Text('by buying the app from Play Store'), + onTap: () { + launchUrlString('https://play.google.com/store/apps/details?id=de.jonasbark.swiftcontrol'); + }, + ), + PopupMenuItem( + child: Text('via PayPal'), + onTap: () { + launchUrlString('https://paypal.me/boni'); + }, + ), + ]; + }, + icon: Text( + 'Donate ♥', + style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold), + ), + ), + SizedBox(width: 8), + ], + PopupMenuButton( + itemBuilder: (BuildContext context) { + return [ + PopupMenuItem( + child: Text('Instructions'), + onTap: () { + final instructions = Platform.isAndroid + ? 'INSTRUCTIONS_ANDROID.md' + : Platform.isIOS + ? 'INSTRUCTIONS_IOS.md' + : Platform.isMacOS + ? 'INSTRUCTIONS_MACOS.md' + : 'INSTRUCTIONS_WINDOWS.md'; + Navigator.push( + context, + MaterialPageRoute(builder: (c) => MarkdownPage(assetPath: instructions)), + ); + }, + ), + PopupMenuItem( + child: Text('Troubleshooting Guide'), + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (c) => MarkdownPage(assetPath: 'TROUBLESHOOTING.md')), + ); + }, + ), + PopupMenuItem( + child: Text('Provide Feedback'), + onTap: () { + launchUrlString('https://github.com/OpenBikeControl/bikecontrol/issues'); + }, + ), + if (!kIsWeb) + PopupMenuItem( + child: Text('Get Support'), + onTap: () { + final isFromStore = (Platform.isAndroid ? isFromPlayStore == true : Platform.isIOS); + final suffix = isFromStore ? '' : '-sw'; + + String email = Uri.encodeComponent('jonas$suffix@bikecontrol.app'); + String subject = Uri.encodeComponent("Help requested for BikeControl v${packageInfoValue?.version}"); + String body = Uri.encodeComponent(""" + ${debugText()} + +Please also attach the file ${File('${Directory.current.path}/app.logs').path}, if it exists. +Please don't remove this information, it helps me to assist you better."""); + Uri mail = Uri.parse("mailto:$email?subject=$subject&body=$body"); + + launchUrl(mail); + }, + ), + ]; + }, + icon: Icon(Icons.help_outline), + ), + SizedBox(width: 8), + const MenuButton(), + SizedBox(width: 8), + ]; +} + +String debugText() { + return ''' + +--- +App Version: ${packageInfoValue?.version}${shorebirdPatch?.number != null ? '+${shorebirdPatch!.number}' : ''} +Platform: ${Platform.operatingSystem} ${Platform.operatingSystemVersion} +Target: ${settings.getLastTarget()?.title ?? '-'} +Trainer App: ${settings.getTrainerApp()?.name ?? '-'} +Connected Controllers: ${connection.devices.map((e) => e.toString()).join(', ')} +Logs: +${connection.lastLogEntries.reversed.joinToString(separator: '\n', transform: (e) => '${e.date.toString().split('.').first} - ${e.entry}')} +'''; +} + +class MenuButton extends StatelessWidget { + const MenuButton({super.key}); + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + itemBuilder: (c) => [ + if (kDebugMode) ...[ + PopupMenuItem( + child: PopupMenuButton( + child: Text("Simulate buttons"), + itemBuilder: (_) { + return ControllerButton.values + .map( + (e) => PopupMenuItem( + child: Text(e.name), + onTap: () { + Future.delayed(Duration(seconds: 2)).then((_) { + if (connection.devices.isNotEmpty) { + connection.devices.firstOrNull?.handleButtonsClicked([e]); + connection.devices.firstOrNull?.handleButtonsClicked([]); + } else { + actionHandler.performAction(e); + } + }); + }, + ), + ) + .toList(); + }, + ), + ), + PopupMenuItem( + child: Text('Continue'), + onTap: () { + //Navigator.push(context, MaterialPageRoute(builder: (c) => DevicePage())); + /*connection.addDevices([ + ZwiftClick( + BleDevice( + name: 'Controller', + deviceId: '00:11:22:33:44:55', + ), + ) + ..firmwareVersion = '1.2.0' + ..rssi = -51 + ..batteryLevel = 81, + ]);*/ + }, + ), + PopupMenuItem( + child: Text('Reset'), + onTap: () async { + await settings.reset(); + }, + ), + PopupMenuItem(child: PopupMenuDivider()), + ], + PopupMenuItem( + child: Text('Changelog'), + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (c) => MarkdownPage(assetPath: 'CHANGELOG.md'))); + }, + ), + PopupMenuItem( + child: Text('Ignored Devices'), + onTap: () { + showDialog( + context: context, + builder: (context) => IgnoredDevicesDialog(), + ); + }, + ), + PopupMenuItem( + child: Text('License'), + onTap: () { + showLicensePage(context: context); + }, + ), + ], + ); + } +} diff --git a/lib/widgets/scan.dart b/lib/widgets/scan.dart new file mode 100644 index 000000000..cc29202f1 --- /dev/null +++ b/lib/widgets/scan.dart @@ -0,0 +1,113 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/pages/markdown.dart'; + +class ScanWidget extends StatefulWidget { + const ScanWidget({super.key}); + + @override + State createState() => _ScanWidgetState(); +} + +class _ScanWidgetState extends State { + @override + void initState() { + super.initState(); + + connection.initialize(); + + /*_isScanningSubscription = FlutterBluePlus.isScanning.listen((state) { + _isScanning = state; + if (mounted) { + setState(() {}); + } + });*/ + + // after the first frame + WidgetsBinding.instance.addPostFrameCallback((_) { + // must be called from a button + if (!kIsWeb) { + Future.delayed(Duration(seconds: 1)) + .then((_) { + return connection.performScanning(); + }) + .catchError((e) { + print(e); + }); + } + }); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ValueListenableBuilder( + valueListenable: connection.isScanning, + builder: (context, isScanning, widget) { + if (isScanning) { + return Column( + spacing: 12, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Scanning for devices... Make sure they are powered on and in range and not connected to another device.', + ), + if (!kIsWeb && (Platform.isMacOS || Platform.isWindows)) + ValueListenableBuilder( + valueListenable: connection.isMediaKeyDetectionEnabled, + builder: (context, value, child) { + return SwitchListTile.adaptive( + value: value, + contentPadding: EdgeInsets.zero, + dense: true, + subtitle: Text( + 'Enable this option to allow BikeControl to detect bluetooth remotes. In order to do so BikeControl needs to act as a media player.', + ), + title: Row( + children: [ + const Text("Enable Media Key Detection"), + ], + ), + onChanged: (change) { + connection.isMediaKeyDetectionEnabled.value = change; + }, + ); + }, + ), + TextButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (c) => MarkdownPage(assetPath: 'TROUBLESHOOTING.md')), + ); + }, + child: const Text("Show Troubleshooting Guide"), + ), + SizedBox(), + ], + ); + } else { + return Row( + children: [ + ElevatedButton( + onPressed: () { + connection.performScanning(); + }, + child: const Text("SCAN"), + ), + ], + ); + } + }, + ), + ], + ); + } +} diff --git a/lib/widgets/status.dart b/lib/widgets/status.dart new file mode 100644 index 000000000..995bc16cc --- /dev/null +++ b/lib/widgets/status.dart @@ -0,0 +1,152 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_emulator.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/actions/android.dart'; +import 'package:swift_control/utils/actions/remote.dart'; +import 'package:swift_control/utils/requirements/multi.dart'; +import 'package:swift_control/utils/requirements/remote.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +class StatusWidget extends StatefulWidget { + const StatusWidget({super.key}); + + @override + State createState() => _StatusWidgetState(); +} + +class _StatusWidgetState extends State { + bool? _isRunningAndroidService; + + @override + void initState() { + super.initState(); + if (!kIsWeb && Platform.isAndroid && actionHandler is AndroidActions) { + (actionHandler as AndroidActions).accessibilityHandler.isRunning().then((isRunning) { + setState(() { + _isRunningAndroidService = isRunning; + }); + }); + } + } + + @override + Widget build(BuildContext context) { + final isRemote = actionHandler is RemoteActions && isAdvertisingPeripheral; + final isZwift = settings.getTrainerApp()?.supportsZwiftEmulation == true && settings.getZwiftEmulatorEnabled(); + return Card( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + spacing: 8, + children: [ + if (connection.controllerDevices.isEmpty) + _Status(color: Colors.red, text: 'No connected controllers') + else + _Status( + color: Colors.green, + text: + '${connection.controllerDevices.length == 1 ? 'Controller connected' : '${connection.controllerDevices.length} controllers connected'} ', + ), + if (whooshLink.isCompatible(settings.getLastTarget() ?? Target.thisDevice) && + settings.getMyWhooshLinkEnabled() && + !isZwift) + _Status( + color: whooshLink.isConnected.value + ? Colors.green + : Platform.isAndroid + ? Colors.yellow + : Colors.red, + text: + 'MyWhoosh Direct Connect ${whooshLink.isConnected.value + ? "connected" + : Platform.isAndroid + ? "not connected (optional)" + : "not connected"}', + ), + + if (isRemote) + _Status( + color: (actionHandler as RemoteActions).isConnected ? Colors.green : Colors.red, + text: 'Remote ${(actionHandler as RemoteActions).isConnected ? "connected" : "not connected"}', + ), + if (isZwift) + _Status( + color: zwiftEmulator.isConnected.value ? Colors.green : Colors.red, + text: 'Zwift Emulation ${zwiftEmulator.isConnected.value ? "connected" : "not connected"}', + ), + if (!isRemote && !isZwift && !screenshotMode && settings.getLastTarget() != Target.thisDevice) + _Status( + color: Colors.red, + text: 'Not connected to a remote device', + ), + if (_isRunningAndroidService != null) + _Status( + color: _isRunningAndroidService! ? Colors.green : Colors.red, + text: 'Accessibility service is ${_isRunningAndroidService! ? 'available' : 'not available'}', + trailing: !_isRunningAndroidService! + ? Row( + spacing: 8, + children: [ + Text('Follow instructions at'), + Expanded( + child: TextButton( + style: TextButton.styleFrom(padding: EdgeInsets.zero, minimumSize: Size(0, 0)), + child: Text('https://dontkillmyapp.com/'), + onPressed: () { + launchUrlString('https://dontkillmyapp.com/'); + }, + ), + ), + IconButton( + onPressed: () { + (actionHandler as AndroidActions).accessibilityHandler.isRunning().then(( + isRunning, + ) { + setState(() { + _isRunningAndroidService = isRunning; + }); + }); + }, + icon: Icon(Icons.refresh), + ), + ], + ) + : null, + ), + ], + ), + ), + ); + } +} + +class _Status extends StatelessWidget { + final Color color; + final String text; + final Widget? trailing; + const _Status({super.key, required this.color, required this.text, this.trailing}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Row( + children: [ + Icon(Icons.circle, color: color, size: 16), + const SizedBox(width: 8), + Text(text), + ], + ), + if (trailing != null) ...[ + Padding( + padding: const EdgeInsets.only(left: 16 + 8.0), + child: trailing!, + ), + ], + ], + ); + } +} diff --git a/lib/widgets/testbed.dart b/lib/widgets/testbed.dart new file mode 100644 index 000000000..0692c5a63 --- /dev/null +++ b/lib/widgets/testbed.dart @@ -0,0 +1,502 @@ +import 'dart:async'; +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/actions/base_actions.dart' as actions; +import 'package:swift_control/utils/keymap/apps/custom_app.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/widgets/button_widget.dart'; + +import '../bluetooth/messages/notification.dart'; + +/// A developer overlay that visualizes touches and keyboard events. +/// - Touch dots appear where you touch and fade out over [touchRevealDuration]. +/// - Keyboard events are listed temporarily and fade out over [keyboardRevealDuration]. +class Testbed extends StatefulWidget { + const Testbed({ + super.key, + this.enabled = true, + this.showTouches = true, + this.showKeyboard = true, + this.touchRevealDuration = const Duration(seconds: 3), + this.keyboardRevealDuration = const Duration(seconds: 3), + this.maxKeyboardEvents = 6, + this.touchColor = const Color(0xFF00BCD4), // cyan-ish + this.keyboardBadgeColor = const Color(0xCC000000), // translucent black + this.keyboardTextStyle = const TextStyle(color: Colors.white, fontSize: 12), + }); + + final bool enabled; + final bool showTouches; + final bool showKeyboard; + + final Duration touchRevealDuration; + final Duration keyboardRevealDuration; + final int maxKeyboardEvents; + + final Color touchColor; + final Color keyboardBadgeColor; + final TextStyle keyboardTextStyle; + + @override + State createState() => _TestbedState(); +} + +class _TestbedState extends State with SingleTickerProviderStateMixin { + late final Ticker _ticker; + late StreamSubscription _actionSubscription; + + // ----- Touch tracking ----- + final Map _active = {}; + final List<_TouchSample> _history = <_TouchSample>[]; + + // ----- Keyboard tracking ----- + // ----- Keyboard tracking ----- + final List<_KeySample> _keys = <_KeySample>[]; + final List<_ActionSample> _actions = <_ActionSample>[]; + + // Focus to receive key events without stealing focus from inputs. + late final FocusNode _focusNode; + + @override + void initState() { + super.initState(); + _focusNode = FocusNode(debugLabel: 'TestbedFocus', canRequestFocus: true, skipTraversal: true); + _actionSubscription = connection.actionStream.listen((data) async { + if (!mounted) { + return; + } + if (data is ButtonNotification) { + for (final button in data.buttonsClicked) { + final sample = _KeySample( + button: button, + text: '🔘 ${button.name}', + timestamp: DateTime.now(), + ); + _keys.insert(0, sample); + if (_keys.length > widget.maxKeyboardEvents) { + _keys.removeLast(); + } + + if (actionHandler.supportedApp is! CustomApp && + actionHandler.supportedApp?.keymap.getKeyPair(button) == null) { + ScaffoldMessenger.maybeOf( + context, + )?.showSnackBar( + SnackBar( + padding: EdgeInsets.only(left: 70, top: 12, bottom: 12, right: 12), + content: Text.rich( + TextSpan( + children: [ + const TextSpan(text: 'Use a custom keymap to support the '), + WidgetSpan( + child: ButtonWidget(button: button), + ), + const TextSpan( + text: ' button.', + ), + ], + ), + ), + ), + ); + } + } + setState(() {}); + } else if (data is ActionNotification) { + final sample = _ActionSample( + text: data.result.message, + timestamp: DateTime.now(), + isError: data.result is actions.Error, + ); + _actions.insert(0, sample); + if (_actions.length > widget.maxKeyboardEvents) { + _actions.removeLast(); + } + setState(() {}); + } + }); + + _ticker = createTicker((_) { + // Cull expired touch and key samples. + final now = DateTime.now(); + _history.removeWhere((s) => now.difference(s.timestamp) > widget.touchRevealDuration); + _keys.removeWhere((k) => now.difference(k.timestamp) > widget.keyboardRevealDuration); + _actions.removeWhere((k) => now.difference(k.timestamp) > widget.keyboardRevealDuration); + + if (mounted) setState(() {}); + })..start(); + } + + @override + void dispose() { + _ticker.dispose(); + _focusNode.dispose(); + super.dispose(); + } + + void _onPointerDown(PointerDownEvent e) { + if (!widget.enabled || + !widget.showTouches || + (e.kind != PointerDeviceKind.unknown && e.kind != PointerDeviceKind.mouse)) { + return; + } + final sample = _TouchSample( + pointer: e.pointer, + position: e.position, + timestamp: DateTime.now(), + phase: _TouchPhase.down, + ); + _active[e.pointer] = sample; + _history.add(sample); + setState(() {}); + } + + void _onPointerUp(PointerUpEvent e) { + if (!widget.enabled || + !widget.showTouches || + (e.kind != PointerDeviceKind.unknown && e.kind != PointerDeviceKind.mouse)) { + return; + } + final sample = _TouchSample( + pointer: e.pointer, + position: e.position, + timestamp: DateTime.now(), + phase: _TouchPhase.up, + ); + _active[e.pointer] = sample; + _history.add(sample); + setState(() {}); + } + + void _onPointerCancel(PointerCancelEvent e) { + if (!widget.enabled || !widget.showTouches || !mounted) return; + _active.remove(e.pointer); + setState(() {}); + } + + KeyEventResult _onKey(FocusNode node, KeyEvent event) { + if (!widget.enabled || !widget.showKeyboard || event is KeyUpEvent) return KeyEventResult.ignored; + + final label = event.logicalKey.keyLabel; + final keyName = label.isNotEmpty ? label : event.logicalKey.debugName ?? 'Key'; + final isDown = event is KeyDownEvent; + final isUp = event is KeyUpEvent; + + // Filter out repeat KeyDowns if desired (optional). + // Here we keep them; comment this block in to drop repeats: + // if (event.repeat) return KeyEventResult.handled; + + final sample = _KeySample( + text: + '${isDown + ? "↓" + : isUp + ? "↑" + : "•"} $keyName', + timestamp: DateTime.now(), + ); + _keys.insert(0, sample); + if (_keys.length > widget.maxKeyboardEvents) { + _keys.removeLast(); + } + setState(() {}); + // We don't want to prevent normal text input, so we return ignored. + return KeyEventResult.ignored; + } + + @override + Widget build(BuildContext context) { + return Listener( + onPointerDown: _onPointerDown, + onPointerUp: _onPointerUp, + onPointerCancel: _onPointerCancel, + behavior: HitTestBehavior.translucent, + child: Focus( + focusNode: _focusNode, + autofocus: true, + canRequestFocus: true, + descendantsAreFocusable: true, + onKeyEvent: _onKey, + child: Stack( + fit: StackFit.passthrough, + children: [ + if (widget.showTouches) + Positioned.fill( + child: IgnorePointer( + child: CustomPaint( + painter: _TouchesPainter( + now: DateTime.now(), + samples: _history, + duration: widget.touchRevealDuration, + color: widget.touchColor, + ), + ), + ), + ), + if (widget.showKeyboard) + Positioned( + left: 12, + bottom: 12, + child: IgnorePointer( + child: _KeyboardOverlay( + items: _keys, + duration: widget.keyboardRevealDuration, + badgeColor: widget.keyboardBadgeColor, + textStyle: widget.keyboardTextStyle, + ), + ), + ), + if (widget.showKeyboard) + Positioned( + right: 12, + bottom: 12, + child: IgnorePointer( + child: _ActionOverlay( + items: _actions, + duration: widget.keyboardRevealDuration, + badgeColor: widget.keyboardBadgeColor, + textStyle: widget.keyboardTextStyle, + ), + ), + ), + ], + ), + ), + ); + } +} + +// ===== Touches ===== + +enum _TouchPhase { down, move, up } + +class _TouchSample { + _TouchSample({required this.pointer, required this.position, required this.timestamp, required this.phase}); + + final int pointer; + final Offset position; + final DateTime timestamp; + final _TouchPhase phase; +} + +class _TouchesPainter extends CustomPainter { + _TouchesPainter({required this.now, required this.samples, required this.duration, required this.color}); + + final DateTime now; + final List<_TouchSample> samples; + final Duration duration; + final Color color; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2; + + for (final s in samples) { + final age = now.difference(s.timestamp); + if (age > duration) continue; + + final color = s.phase == _TouchPhase.down ? this.color : Colors.red; + + final t = age.inMilliseconds / duration.inMilliseconds.clamp(1, 1 << 30); + final fade = (1.0 - t).clamp(0.0, 1.0); + + // Two concentric circles: inner filled pulse + outer ring. + final baseRadius = 22.0; + final pulse = 1.0 + 0.5 * math.sin(t * math.pi); // subtle pulsing + final rOuter = baseRadius * (1.0 + 0.35 * t); + final rInner = baseRadius * 0.5 * pulse; + + // Outer ring (stroke, fading) + paint + ..style = PaintingStyle.stroke + ..color = color.withOpacity(0.35 * fade); + canvas.drawCircle(s.position, rOuter, paint); + + // Inner fill (stronger) + final fill = Paint() + ..style = PaintingStyle.fill + ..color = color.withOpacity(0.35 + 0.35 * fade); + canvas.drawCircle(s.position, rInner, fill); + + // Tiny center dot for precision + final center = Paint() + ..style = PaintingStyle.fill + ..color = color.withOpacity(0.9 * fade); + canvas.drawCircle(s.position, 2.5, center); + } + } + + @override + bool shouldRepaint(covariant _TouchesPainter oldDelegate) { + return oldDelegate.now != now || + oldDelegate.samples != samples || + oldDelegate.duration != duration || + oldDelegate.color != color; + } +} + +// ===== Keyboard overlay ===== + +class _KeySample { + _KeySample({required this.text, required this.timestamp, this.button}); + final ControllerButton? button; + final String text; + final DateTime timestamp; +} + +class _KeyboardOverlay extends StatelessWidget { + const _KeyboardOverlay({ + super.key, + required this.items, + required this.duration, + required this.badgeColor, + required this.textStyle, + }); + + final List<_KeySample> items; + final Duration duration; + final Color badgeColor; + final TextStyle textStyle; + + @override + Widget build(BuildContext context) { + final now = DateTime.now(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (final item in items) + _KeyboardToast( + item: item, + age: now.difference(item.timestamp), + duration: duration, + badgeColor: badgeColor, + textStyle: textStyle, + ), + ], + ); + } +} + +class _KeyboardToast extends StatelessWidget { + const _KeyboardToast({ + required this.item, + required this.age, + required this.duration, + required this.badgeColor, + required this.textStyle, + }); + + final _KeySample item; + final Duration age; + final Duration duration; + final Color badgeColor; + final TextStyle textStyle; + + @override + Widget build(BuildContext context) { + final t = (age.inMilliseconds / duration.inMilliseconds.clamp(1, 1 << 30)).clamp(0.0, 1.0); + final fade = 1.0 - t; + + return Material( + color: Colors.transparent, + child: Opacity( + opacity: fade, + child: Container( + margin: const EdgeInsets.only(bottom: 6), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration(color: badgeColor, borderRadius: BorderRadius.circular(12)), + child: item.button != null ? ButtonWidget(button: item.button!) : Text(item.text, style: textStyle), + ), + ), + ); + } +} + +// ===== Action overlay ===== + +class _ActionSample { + _ActionSample({required this.text, required this.timestamp, required this.isError}); + final String text; + final DateTime timestamp; + final bool isError; +} + +class _ActionOverlay extends StatelessWidget { + const _ActionOverlay({ + super.key, + required this.items, + required this.duration, + required this.badgeColor, + required this.textStyle, + }); + + final List<_ActionSample> items; + final Duration duration; + final Color badgeColor; + final TextStyle textStyle; + + @override + Widget build(BuildContext context) { + final now = DateTime.now(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + for (final item in items) + _ActionToast( + item: item, + age: now.difference(item.timestamp), + duration: duration, + badgeColor: badgeColor, + textStyle: textStyle, + ), + ], + ); + } +} + +class _ActionToast extends StatelessWidget { + const _ActionToast({ + required this.item, + required this.age, + required this.duration, + required this.badgeColor, + required this.textStyle, + }); + + final _ActionSample item; + final Duration age; + final Duration duration; + final Color badgeColor; + final TextStyle textStyle; + + @override + Widget build(BuildContext context) { + final t = (age.inMilliseconds / duration.inMilliseconds.clamp(1, 1 << 30)).clamp(0.0, 1.0); + final fade = 1.0 - t; + + return Material( + color: Colors.transparent, + child: Opacity( + opacity: fade, + child: Container( + margin: const EdgeInsets.only(bottom: 6), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: item.isError ? Colors.red.withOpacity(0.8) : badgeColor, + borderRadius: BorderRadius.circular(12), + ), + child: Text(item.text, style: textStyle), + ), + ), + ); + } +} diff --git a/lib/widgets/title.dart b/lib/widgets/title.dart new file mode 100755 index 000000000..68496d8c1 --- /dev/null +++ b/lib/widgets/title.dart @@ -0,0 +1,210 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:in_app_update/in_app_update.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:restart_app/restart_app.dart'; +import 'package:shorebird_code_push/shorebird_code_push.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/widgets/small_progress_indicator.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:version/version.dart'; + +PackageInfo? packageInfoValue; +bool? isFromPlayStore; +Patch? shorebirdPatch; + +class AppTitle extends StatefulWidget { + const AppTitle({super.key}); + + @override + State createState() => _AppTitleState(); +} + +class _AppTitleState extends State { + final updater = ShorebirdUpdater(); + + @override + void initState() { + super.initState(); + + if (updater.isAvailable) { + updater.readCurrentPatch().then((patch) { + setState(() { + shorebirdPatch = patch; + }); + }); + } + + if (packageInfoValue == null) { + PackageInfo.fromPlatform().then((value) { + setState(() { + packageInfoValue = value; + }); + _checkForUpdate(); + }); + } + } + + void _checkForUpdate() async { + if (screenshotMode) { + return; + } else if (updater.isAvailable) { + final updateStatus = await updater.checkForUpdate(); + if (updateStatus == UpdateStatus.outdated) { + updater + .update() + .then((value) { + _showShorebirdRestartSnackbar(); + }) + .catchError((e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to update: $e'), + duration: Duration(seconds: 5), + ), + ); + }); + } else if (updateStatus == UpdateStatus.restartRequired) { + _showShorebirdRestartSnackbar(); + } + } + + if (kIsWeb) { + // no-op + } else if (Platform.isAndroid) { + try { + final appUpdateInfo = await InAppUpdate.checkForUpdate(); + if (context.mounted && appUpdateInfo.updateAvailability == UpdateAvailability.updateAvailable) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('New version available'), + duration: Duration(seconds: 1337), + action: SnackBarAction( + label: 'Update', + onPressed: () { + InAppUpdate.performImmediateUpdate(); + }, + ), + ), + ); + } + isFromPlayStore = true; + return null; + } on Exception catch (e) { + isFromPlayStore = false; + print('Failed to check for update: $e'); + } + setState(() {}); + } else if (Platform.isIOS) { + final url = Uri.parse('https://itunes.apple.com/lookup?id=6753721284'); + final response = await http.get(url); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + if (data['resultCount'] > 0) { + final versionString = data['results'][0]['version'] as String; + _compareVersion(versionString); + } + } + } else if (Platform.isMacOS) { + final url = Uri.parse('https://apps.apple.com/us/app/swiftcontrol/id6753721284?platform=mac'); + final res = await http.get(url, headers: {'User-Agent': 'Mozilla/5.0'}); + if (res.statusCode != 200) return null; + + final body = res.body; + final regex = RegExp( + r'whats-new__latest__version">Version ([0-9]{1,2}\.[0-9]{1,2}.[0-9]{1,2})

', + dotAll: true, + ); + final match = regex.firstMatch(body); + if (match == null) return null; + final versionString = match.group(1); + + if (versionString != null) { + _compareVersion(versionString); + } + } else if (Platform.isWindows) { + final url = Uri.parse( + 'https://raw.githubusercontent.com/OpenBikeControl/bikecontrol/refs/heads/main/WINDOWS_STORE_VERSION.txt', + ); + final res = await http.get(url, headers: {'User-Agent': 'Mozilla/5.0'}); + if (res.statusCode != 200) return null; + + final body = res.body.trim(); + _compareVersion(body); + } + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('BikeControl', style: TextStyle(fontWeight: FontWeight.bold)), + if (packageInfoValue != null) + Text( + 'v${packageInfoValue!.version}${shorebirdPatch != null ? '+${shorebirdPatch!.number}' : ''}${kIsWeb || (Platform.isAndroid && isFromPlayStore == false) ? ' (sideloaded)' : ''}', + style: screenshotMode + ? TextStyle(fontSize: 12) + : TextStyle(fontFamily: "monospace", fontFamilyFallback: ["Courier"], fontSize: 12), + ) + else + SmallProgressIndicator(), + ], + ); + } + + void _showShorebirdRestartSnackbar() { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Force-close the app to use the new version'), + duration: Duration(seconds: 10), + action: SnackBarAction( + label: 'Restart', + onPressed: () { + if (Platform.isIOS) { + connection.reset(); + Restart.restartApp(delayBeforeRestart: 1000); + } else { + connection.reset(); + exit(0); + } + }, + ), + ), + ); + } + + void _compareVersion(String versionString) { + final parsed = Version.parse(versionString); + final current = Version.parse(packageInfoValue!.version); + if (parsed > current && mounted && !kDebugMode) { + if (Platform.isAndroid) { + _showUpdateSnackbar(parsed, 'https://play.google.com/store/apps/details?id=org.jonasbark.swiftcontrol'); + } else if (Platform.isIOS || Platform.isMacOS) { + _showUpdateSnackbar(parsed, 'https://apps.apple.com/app/id6753721284'); + } else if (Platform.isWindows) { + _showUpdateSnackbar(parsed, 'ms-windows-store://pdp/?productid=9NP42GS03Z26'); + } + } + } + + void _showUpdateSnackbar(Version newVersion, String url) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('New version available: ${newVersion.toString()}'), + duration: Duration(seconds: 1337), + action: SnackBarAction( + label: 'Download', + onPressed: () { + launchUrlString(url); + }, + ), + ), + ); + } +} diff --git a/lib/widgets/warning.dart b/lib/widgets/warning.dart new file mode 100644 index 000000000..8aa211877 --- /dev/null +++ b/lib/widgets/warning.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class Warning extends StatelessWidget { + final bool important; + final List children; + const Warning({super.key, required this.children, this.important = true}); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.all(4), + width: double.infinity, + decoration: BoxDecoration( + color: important + ? Theme.of(context).colorScheme.errorContainer + : Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: children, + ), + ); + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d23..1c4575052 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,34 @@ #include "generated_plugin_registrant.h" +#include +#include +#include +#include +#include +#include +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) bluetooth_low_energy_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "BluetoothLowEnergyLinuxPlugin"); + bluetooth_low_energy_linux_plugin_register_with_registrar(bluetooth_low_energy_linux_registrar); + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) gamepads_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GamepadsLinuxPlugin"); + gamepads_linux_plugin_register_with_registrar(gamepads_linux_registrar); + g_autoptr(FlPluginRegistrar) media_key_detector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKeyDetectorPlugin"); + media_key_detector_plugin_register_with_registrar(media_key_detector_linux_registrar); + g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); + screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); + window_manager_plugin_register_with_registrar(window_manager_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87a7..4f57d1b10 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,13 @@ # list(APPEND FLUTTER_PLUGIN_LIST + bluetooth_low_energy_linux + file_selector_linux + gamepads_linux + media_key_detector_linux + screen_retriever_linux + url_launcher_linux + window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/logo.png b/logo.png new file mode 100644 index 000000000..88d0f4208 Binary files /dev/null and b/logo.png differ diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 65581d2d7..59294bfec 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,14 +5,36 @@ import FlutterMacOS import Foundation -import flutter_blue_plus_darwin +import bluetooth_low_energy_darwin +import device_info_plus +import file_selector_macos import flutter_local_notifications +import gamepads_darwin import keypress_simulator_macos +import media_key_detector_macos +import package_info_plus import path_provider_foundation +import screen_retriever_macos +import shared_preferences_foundation +import universal_ble +import url_launcher_macos +import wakelock_plus +import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin")) + BluetoothLowEnergyDarwinPlugin.register(with: registry.registrar(forPlugin: "BluetoothLowEnergyDarwinPlugin")) + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) + GamepadsDarwinPlugin.register(with: registry.registrar(forPlugin: "GamepadsDarwinPlugin")) KeypressSimulatorMacosPlugin.register(with: registry.registrar(forPlugin: "KeypressSimulatorMacosPlugin")) + MediaKeyDetectorPlugin.register(with: registry.registrar(forPlugin: "MediaKeyDetectorPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UniversalBlePlugin.register(with: registry.registrar(forPlugin: "UniversalBlePlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/macos/Podfile b/macos/Podfile index 29c8eb329..ff5ddb3b8 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/macos/Podfile.lock b/macos/Podfile.lock index ea7c5064d..c2b94d995 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,42 +1,110 @@ PODS: - - flutter_blue_plus_darwin (0.0.2): + - bluetooth_low_energy_darwin (0.0.1): - Flutter - FlutterMacOS + - device_info_plus (0.0.1): + - FlutterMacOS + - file_selector_macos (0.0.1): + - FlutterMacOS - flutter_local_notifications (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) + - gamepads_darwin (0.1.1): + - FlutterMacOS - keypress_simulator_macos (0.0.1): - FlutterMacOS + - media_key_detector_macos (0.0.1): + - FlutterMacOS + - package_info_plus (0.0.1): + - FlutterMacOS - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - screen_retriever_macos (0.0.1): + - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - universal_ble (0.0.1): + - Flutter + - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS + - wakelock_plus (0.0.1): + - FlutterMacOS + - window_manager (0.5.0): + - FlutterMacOS DEPENDENCIES: - - flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`) + - bluetooth_low_energy_darwin (from `Flutter/ephemeral/.symlinks/plugins/bluetooth_low_energy_darwin/darwin`) + - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) + - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - FlutterMacOS (from `Flutter/ephemeral`) + - gamepads_darwin (from `Flutter/ephemeral/.symlinks/plugins/gamepads_darwin/macos`) - keypress_simulator_macos (from `Flutter/ephemeral/.symlinks/plugins/keypress_simulator_macos/macos`) + - media_key_detector_macos (from `Flutter/ephemeral/.symlinks/plugins/media_key_detector_macos/macos`) + - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - universal_ble (from `Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) + - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) EXTERNAL SOURCES: - flutter_blue_plus_darwin: - :path: Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin + bluetooth_low_energy_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/bluetooth_low_energy_darwin/darwin + device_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos + file_selector_macos: + :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos flutter_local_notifications: :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos FlutterMacOS: :path: Flutter/ephemeral + gamepads_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/gamepads_darwin/macos keypress_simulator_macos: :path: Flutter/ephemeral/.symlinks/plugins/keypress_simulator_macos/macos + media_key_detector_macos: + :path: Flutter/ephemeral/.symlinks/plugins/media_key_detector_macos/macos + package_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + screen_retriever_macos: + :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + universal_ble: + :path: Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + wakelock_plus: + :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos + window_manager: + :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - flutter_blue_plus_darwin: 3ea4ec9133b377febcc8a70b28cd2d2dc9242bd9 + bluetooth_low_energy_darwin: 764d8d1ae5abefbcdb839e812b4b25c0061fcf8b + device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215 + file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d flutter_local_notifications: 4ccab5b7a22835214a6672e3f9c5e8ae207dab36 - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + gamepads_darwin: 07af6c60c282902b66574c800e20b2b26e68fda8 keypress_simulator_macos: f8556f9101f9f2f175652e0bceddf0fe82a4c6b2 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + media_key_detector_macos: a93757a483b4b47283ade432b1af9e427c47329f + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b + path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba + screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 + wakelock_plus: 9d63063ffb7af1c215209769067c57103bde719d + window_manager: e25faf20d88283a0d46e7b1a759d07261ca27575 -PODFILE CHECKSUM: 7eb978b976557c8c1cd717d8185ec483fd090a82 +PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 COCOAPODS: 1.16.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj old mode 100644 new mode 100755 index dc266f783..4aec9f878 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -24,10 +24,10 @@ 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; 74C2E383F32990193E6CB903 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B3C733522C0D4DA32BDE037 /* Pods_RunnerTests.framework */; }; + F02438522EA222AE00C673A3 /* assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F02438512EA222AE00C673A3 /* assets.xcassets */; }; FDDED5BC8AE9088C0B2FD749 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38D2ED8D4AF83D5BAAC8B750 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ @@ -67,9 +67,8 @@ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* swift_play.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = swift_play.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* BikeControl.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BikeControl.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; @@ -87,6 +86,7 @@ A126A3BE8A8EB6B8BE260F53 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; A7250649471FBC62B0CBB090 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; E286BD279B92C2469232E49F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + F02438512EA222AE00C673A3 /* assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = assets.xcassets; sourceTree = ""; }; FC66CAEEF21DAD867099FB4F /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -120,7 +120,6 @@ 1214C73C0A9D0DF8E46E2062 /* Pods-RunnerTests.release.xcconfig */, 6F6E35429F102BF1B750D24C /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -158,7 +157,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* swift_play.app */, + 33CC10ED2044A3C60003C045 /* BikeControl.app */, 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; @@ -167,7 +166,7 @@ 33CC11242044D66E0003C045 /* Resources */ = { isa = PBXGroup; children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, + F02438512EA222AE00C673A3 /* assets.xcassets */, 33CC10F42044A3C60003C045 /* MainMenu.xib */, 33CC10F72044A3C60003C045 /* Info.plist */, ); @@ -249,7 +248,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* swift_play.app */; + productReference = 33CC10ED2044A3C60003C045 /* BikeControl.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -270,7 +269,6 @@ 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Sandbox = { enabled = 1; @@ -315,8 +313,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + F02438522EA222AE00C673A3 /* assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -557,7 +555,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -570,16 +568,27 @@ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=macosx*]" = UZRHKPVWN9; + ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = BikeControl; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + PRODUCT_BUNDLE_IDENTIFIER = de.jonasbark.swiftcontrol.darwin; + PRODUCT_NAME = BikeControl; PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "mac app store"; SWIFT_VERSION = 5.0; }; name = Profile; @@ -639,7 +648,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -689,7 +698,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -702,15 +711,23 @@ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = UZRHKPVWN9; + ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = BikeControl; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + PRODUCT_BUNDLE_IDENTIFIER = de.jonasbark.swiftcontrol.darwin; + PRODUCT_NAME = BikeControl; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -722,16 +739,27 @@ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=macosx*]" = UZRHKPVWN9; + ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = BikeControl; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + PRODUCT_BUNDLE_IDENTIFIER = de.jonasbark.swiftcontrol.darwin; + PRODUCT_NAME = BikeControl; PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "mac app store"; SWIFT_VERSION = 5.0; }; name = Release; diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 7af84c5c2..f3d1e50ed 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -31,7 +31,7 @@ @@ -66,7 +66,7 @@ @@ -83,7 +83,7 @@ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 96d3fee1a..000000000 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "info": { - "version": 1, - "author": "xcode" - }, - "images": [ - { - "size": "16x16", - "idiom": "mac", - "filename": "app_icon_16.png", - "scale": "1x" - }, - { - "size": "16x16", - "idiom": "mac", - "filename": "app_icon_32.png", - "scale": "2x" - }, - { - "size": "32x32", - "idiom": "mac", - "filename": "app_icon_32.png", - "scale": "1x" - }, - { - "size": "32x32", - "idiom": "mac", - "filename": "app_icon_64.png", - "scale": "2x" - }, - { - "size": "128x128", - "idiom": "mac", - "filename": "app_icon_128.png", - "scale": "1x" - }, - { - "size": "128x128", - "idiom": "mac", - "filename": "app_icon_256.png", - "scale": "2x" - }, - { - "size": "256x256", - "idiom": "mac", - "filename": "app_icon_256.png", - "scale": "1x" - }, - { - "size": "256x256", - "idiom": "mac", - "filename": "app_icon_512.png", - "scale": "2x" - }, - { - "size": "512x512", - "idiom": "mac", - "filename": "app_icon_512.png", - "scale": "1x" - }, - { - "size": "512x512", - "idiom": "mac", - "filename": "app_icon_1024.png", - "scale": "2x" - } - ] -} \ No newline at end of file diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 2280a66b8..000000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index 2491130a9..000000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 39918661e..000000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index 840cd46c9..000000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index 47189f2c6..000000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index 0e1598407..000000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 463e51fb8..000000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig index b44a43fd4..ee9098216 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -5,7 +5,7 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = swift_play +PRODUCT_NAME = SwiftControl // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = de.jonasbark.swiftPlay diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index 14a39b7ec..1c52ecfe2 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -10,5 +10,9 @@ com.apple.security.network.server + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist old mode 100644 new mode 100755 index e142a464b..6a98d525c --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -2,12 +2,21 @@ + UIBackgroundModes + + bluetooth-peripheral + bluetooth-central + + LSApplicationCategoryType + public.app-category.healthcare-fitness CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile + ITSAppUsesNonExemptEncryption + CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion @@ -22,13 +31,17 @@ $(FLUTTER_BUILD_NUMBER) LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) + NSBluetoothAlwaysUsageDescription + BikeControl requires Bluetooth to connect to your devices. + NSLocalNetworkUsageDescription + This app connects to your trainer app on your local network. NSHumanReadableCopyright $(PRODUCT_COPYRIGHT) NSMainNibFile MainMenu NSPrincipalClass NSApplication - NSBluetoothAlwaysUsageDescription - We need BT access because it's a BT App. + NSAccessibilityUsageDescription + BikeControl needs to send keys to your trainer app. diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements old mode 100644 new mode 100755 index c269f520c..133e190b7 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -4,7 +4,13 @@ com.apple.security.app-sandbox + com.apple.security.cs.allow-unsigned-executable-memory + com.apple.security.device.bluetooth + com.apple.security.network.client + + com.apple.security.files.user-selected.read-only + diff --git a/macos/assets.xcassets/AppIcon.appiconset/AppIcon-min.png b/macos/assets.xcassets/AppIcon.appiconset/AppIcon-min.png new file mode 100644 index 000000000..c3f402816 Binary files /dev/null and b/macos/assets.xcassets/AppIcon.appiconset/AppIcon-min.png differ diff --git a/macos/assets.xcassets/AppIcon.appiconset/Contents.json b/macos/assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..df789877b --- /dev/null +++ b/macos/assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,59 @@ +{ + "images" : [ + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "AppIcon-min.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macos/assets.xcassets/Contents.json b/macos/assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/macos/assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/media_key_detector/.gitignore b/media_key_detector/.gitignore new file mode 100644 index 000000000..4aa0df8df --- /dev/null +++ b/media_key_detector/.gitignore @@ -0,0 +1,48 @@ +.DS_Store +.atom/ +.idea/ +.vscode/ + +.packages +.pub/ +.dart_tool/ +pubspec.lock +flutter_export_environment.sh +coverage/ + +Podfile.lock +Pods/ +.symlinks/ +**/Flutter/App.framework/ +**/Flutter/ephemeral/ +**/Flutter/Flutter.podspec +**/Flutter/Flutter.framework/ +**/Flutter/Generated.xcconfig +**/Flutter/flutter_assets/ + +ServiceDefinitions.json +xcuserdata/ +**/DerivedData/ + +local.properties +keystore.properties +.gradle/ +gradlew +gradlew.bat +gradle-wrapper.jar +.flutter-plugins-dependencies +*.iml + +generated_plugin_registrant.cc +generated_plugin_registrant.h +generated_plugin_registrant.dart +GeneratedPluginRegistrant.java +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m +GeneratedPluginRegistrant.swift +build/ +.flutter-plugins + +.project +.classpath +.settings \ No newline at end of file diff --git a/media_key_detector/README.md b/media_key_detector/README.md new file mode 100644 index 000000000..27c3a6b4f --- /dev/null +++ b/media_key_detector/README.md @@ -0,0 +1,43 @@ +# media_key_detector + +[![Very Good Ventures][logo_white]][very_good_ventures_link_dark] +[![Very Good Ventures][logo_black]][very_good_ventures_link_light] + +Developed with 💙 by [Very Good Ventures][very_good_ventures_link] 🦄 + +![coverage][coverage_badge] +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +A Very Good Flutter Federated Plugin created by the [Very Good Ventures Team][very_good_ventures_link]. + +Generated by the [Very Good CLI][very_good_cli_link] 🤖 + + +### Integration tests 🧪 + +Very Good Flutter Plugin uses [fluttium][fluttium_link] for integration tests. Those tests are located +in the front facing package `media_key_detector` example. + +**❗ In order to run the integration tests, you need to have the `fluttium_cli` installed. [See how][fluttium_install].** + +To run the integration tests, run the following command from the root of the project: + +```sh +cd media_key_detector/example +fluttium test flows/test_platform_name.yaml +``` + +[coverage_badge]: media_key_detector/coverage_badge.svg +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[logo_black]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_black.png#gh-light-mode-only +[logo_white]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_white.png#gh-dark-mode-only +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis +[very_good_cli_link]: https://github.com/VeryGoodOpenSource/very_good_cli +[very_good_ventures_link]: https://verygood.ventures/?utm_source=github&utm_medium=banner&utm_campaign=core +[very_good_ventures_link_dark]: https://verygood.ventures/?utm_source=github&utm_medium=banner&utm_campaign=core#gh-dark-mode-only +[very_good_ventures_link_light]: https://verygood.ventures/?utm_source=github&utm_medium=banner&utm_campaign=core#gh-light-mode-only +[fluttium_link]: https://fluttium.dev/ +[fluttium_install]: https://fluttium.dev/docs/getting-started/installing-cli \ No newline at end of file diff --git a/media_key_detector/media_key_detector/CHANGELOG.md b/media_key_detector/media_key_detector/CHANGELOG.md new file mode 100644 index 000000000..589555b74 --- /dev/null +++ b/media_key_detector/media_key_detector/CHANGELOG.md @@ -0,0 +1,7 @@ +# 0.0.2 + +- TBD + +# 0.0.1 + +- Initial Release diff --git a/media_key_detector/media_key_detector/LICENSE b/media_key_detector/media_key_detector/LICENSE new file mode 100644 index 000000000..a4110b7e0 --- /dev/null +++ b/media_key_detector/media_key_detector/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 Evan Kaiser + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/media_key_detector/media_key_detector/README.md b/media_key_detector/media_key_detector/README.md new file mode 100644 index 000000000..7d2c32b6e --- /dev/null +++ b/media_key_detector/media_key_detector/README.md @@ -0,0 +1,110 @@ +[![Pub][pub_badge]][pub_link] +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +# media_key_detector + +Triggers events when keyboard media keys are pressed. On MacOS, it registers +your app as a [Now Playable App](https://developer.apple.com/documentation/mediaplayer/becoming_a_now_playable_app), +allowing it to respond to media events, regardless of whether the event came +from a keyboard, headset, or media remote. + +## Features + +- Captures and triggeres events for the following keys: + - Play / Pause + - Previous / Rewind + - Next / Fast-forward + +## Rationale + +In general media key capture works fine using normal keyboard approaches, such +as RawKeyListener or FocusNode. However, it only works on Windows/Linux. In +MacOS, the key events are not detected, and often will even send the events to +an inactive window, like Music. + +## Getting started + +1. Add the latest version of this package: + +- Run `flutter pub add media_key_detector` -or- +- Edit `pubspec.yaml` and then run `flutter pub get`: + +```yaml +dependencies: + media_key_detector: ^latest_version +``` + +2. Import the package + +``` +import 'package:media_key_detector/media_key_detector.dart'; +``` + +## Usage + +```dart + void _mediaKeyListener(MediaKey mediaKey) { + debugPrint('$mediaKey pressed'); + } + + @override + void initState() { + super.initState(); + mediaKeyDetector.addListener(_mediaKeyListener); + } + + @override + void dispose() { + mediaKeyDetector.removeListener(_mediaKeyListener); + super.dispose(); + } + + /// The following two methods are only really needed if you're relying on + /// the plugin to track the playing state. On MacOS/iOS, this is helpful to + /// display the status in the "Command Center". + + /// Get whether the media is playing. Note that it starts out "paused", + /// so if your app plays media on open, you should call + /// [mediaKeyDetector.setIsPlaying(true)] + Future getIsMediaPlaying() async { + return await mediaKeyDetector.getIsPlaying(); + } + + /// The app tracks the playing state when the user presses the media key, + /// but there are some cases, i.e. when a play button is pressed on the UI + /// interface, where you may need to set it yourself + Future play() async { + return await mediaKeyDetector.setIsPlaying(isPlaying: true); + } +``` + +## Code Generation + +[![Very Good Ventures][logo_white]][very_good_ventures_link_dark] +[![Very Good Ventures][logo_black]][very_good_ventures_link_light] + +Developed with 💙 by [Very Good Ventures][very_good_ventures_link] 🦄 + +A Very Good Flutter Federated Plugin created by the [Very Good Ventures Team][very_good_ventures_link]. + +Generated by the [Very Good CLI][very_good_cli_link] 🤖 + +## Support + +You can support me by buying me a coffee Buy me a coffee + +And also don't forget to star this package on GitHub + +[pub_badge]: https://img.shields.io/pub/v/media_key_detector +[pub_link]: https://pub.dev/packages/media_key_detector +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[logo_black]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_black.png#gh-light-mode-only +[logo_white]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_white.png#gh-dark-mode-only +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis +[very_good_cli_link]: https://github.com/VeryGoodOpenSource/very_good_cli +[very_good_ventures_link]: https://verygood.ventures/?utm_source=github&utm_medium=banner&utm_campaign=core +[very_good_ventures_link_dark]: https://verygood.ventures/?utm_source=github&utm_medium=banner&utm_campaign=core#gh-dark-mode-only +[very_good_ventures_link_light]: https://verygood.ventures/?utm_source=github&utm_medium=banner&utm_campaign=core#gh-light-mode-only diff --git a/media_key_detector/media_key_detector/analysis_options.yaml b/media_key_detector/media_key_detector/analysis_options.yaml new file mode 100644 index 000000000..799268d3e --- /dev/null +++ b/media_key_detector/media_key_detector/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.5.1.0.yaml diff --git a/media_key_detector/media_key_detector/coverage_badge.svg b/media_key_detector/media_key_detector/coverage_badge.svg new file mode 100644 index 000000000..499e98ce2 --- /dev/null +++ b/media_key_detector/media_key_detector/coverage_badge.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + coverage + coverage + 100% + 100% + + diff --git a/media_key_detector/media_key_detector/example/.gitignore b/media_key_detector/media_key_detector/example/.gitignore new file mode 100644 index 000000000..8c5866d2a --- /dev/null +++ b/media_key_detector/media_key_detector/example/.gitignore @@ -0,0 +1,49 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# Fluttium related files +.fluttium_*_launcher.dart \ No newline at end of file diff --git a/media_key_detector/media_key_detector/example/.metadata b/media_key_detector/media_key_detector/example/.metadata new file mode 100644 index 000000000..19d783704 --- /dev/null +++ b/media_key_detector/media_key_detector/example/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: 7048ed95a5ad3e43d697e0c397464193991fc230 + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 7048ed95a5ad3e43d697e0c397464193991fc230 + base_revision: 7048ed95a5ad3e43d697e0c397464193991fc230 + - platform: android + create_revision: 7048ed95a5ad3e43d697e0c397464193991fc230 + base_revision: 7048ed95a5ad3e43d697e0c397464193991fc230 + - platform: ios + create_revision: 7048ed95a5ad3e43d697e0c397464193991fc230 + base_revision: 7048ed95a5ad3e43d697e0c397464193991fc230 + - platform: linux + create_revision: 7048ed95a5ad3e43d697e0c397464193991fc230 + base_revision: 7048ed95a5ad3e43d697e0c397464193991fc230 + - platform: macos + create_revision: 7048ed95a5ad3e43d697e0c397464193991fc230 + base_revision: 7048ed95a5ad3e43d697e0c397464193991fc230 + - platform: web + create_revision: 7048ed95a5ad3e43d697e0c397464193991fc230 + base_revision: 7048ed95a5ad3e43d697e0c397464193991fc230 + - platform: windows + create_revision: 7048ed95a5ad3e43d697e0c397464193991fc230 + base_revision: 7048ed95a5ad3e43d697e0c397464193991fc230 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/media_key_detector/media_key_detector/example/README.md b/media_key_detector/media_key_detector/example/README.md new file mode 100644 index 000000000..2fbfe4831 --- /dev/null +++ b/media_key_detector/media_key_detector/example/README.md @@ -0,0 +1,3 @@ +# media_key_detector_example + +Demonstrates how to use the media_key_detector plugin. diff --git a/media_key_detector/media_key_detector/example/analysis_options.yaml b/media_key_detector/media_key_detector/example/analysis_options.yaml new file mode 100644 index 000000000..59cf34c28 --- /dev/null +++ b/media_key_detector/media_key_detector/example/analysis_options.yaml @@ -0,0 +1,6 @@ +include: package:very_good_analysis/analysis_options.5.1.0.yaml +linter: + rules: + public_member_api_docs: false + require_trailing_commas: false + lines_longer_than_80_chars: false diff --git a/media_key_detector/media_key_detector/example/lib/main.dart b/media_key_detector/media_key_detector/example/lib/main.dart new file mode 100644 index 000000000..812bc317e --- /dev/null +++ b/media_key_detector/media_key_detector/example/lib/main.dart @@ -0,0 +1,144 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:media_key_detector/media_key_detector.dart'; + +void main() => runApp(const MyApp()); + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp(home: HomePage()); + } +} + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + String? _platformName; + bool _isPlaying = false; + Map keyPressed = { + MediaKey.playPause: false, + MediaKey.rewind: false, + MediaKey.fastForward: false, + }; + + void _mediaKeyListener(MediaKey mediaKey) { + debugPrint('$mediaKey pressed'); + + mediaKeyDetector + .getIsPlaying() + .then((playing) => setState(() => _isPlaying = playing)); + + if (keyPressed[mediaKey] == false) { + setState(() => keyPressed[mediaKey] = true); + Timer(const Duration(seconds: 3), () { + setState(() => keyPressed[mediaKey] = false); + }); + } + } + + Future _togglePlayPause() async { + setState(() => _isPlaying = !_isPlaying); + await mediaKeyDetector.setIsPlaying(isPlaying: _isPlaying); + } + + @override + void initState() { + super.initState(); + mediaKeyDetector.addListener(_mediaKeyListener); + } + + @override + void dispose() { + mediaKeyDetector.removeListener(_mediaKeyListener); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final colors = Theme.of(context).colorScheme; + + return Scaffold( + appBar: AppBar(title: const Text('MediaKeyDetector Example')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Press a Media button on your IO device to highlight the corresponding icon.'), + const Text( + 'Press the play/pause button to send now playing info to plugin.'), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.fast_rewind_rounded, + size: 40, + color: (keyPressed[MediaKey.rewind] ?? false) + ? colors.inversePrimary + : colors.onBackground, + ), + IconButton.filled( + onPressed: _togglePlayPause, + style: + IconButton.styleFrom(backgroundColor: colors.secondary), + icon: Icon( + _isPlaying + ? Icons.pause_circle_rounded + : Icons.play_circle_rounded, + size: 40, + color: (keyPressed[MediaKey.playPause] ?? false) + ? colors.inversePrimary + : colors.onPrimary, + ), + ), + Icon( + Icons.fast_forward_rounded, + size: 40, + color: (keyPressed[MediaKey.fastForward] ?? false) + ? colors.inversePrimary + : colors.onBackground, + ), + ], + ), + Text('Is currently playing: $_isPlaying'), + if (_platformName == null) + const SizedBox.shrink() + else + Text( + 'Platform Name: $_platformName', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () async { + if (!context.mounted) return; + try { + final result = await getPlatformName(); + setState(() => _platformName = result); + } catch (error) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: Theme.of(context).primaryColor, + content: Text('$error'), + ), + ); + } + }, + child: const Text('Get Platform Name'), + ), + ], + ), + ), + ); + } +} diff --git a/media_key_detector/media_key_detector/example/linux/.gitignore b/media_key_detector/media_key_detector/example/linux/.gitignore new file mode 100644 index 000000000..d3896c984 --- /dev/null +++ b/media_key_detector/media_key_detector/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/media_key_detector/media_key_detector/example/linux/CMakeLists.txt b/media_key_detector/media_key_detector/example/linux/CMakeLists.txt new file mode 100644 index 000000000..3337c386a --- /dev/null +++ b/media_key_detector/media_key_detector/example/linux/CMakeLists.txt @@ -0,0 +1,106 @@ +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +set(BINARY_NAME "example") +set(APPLICATION_ID "com.example.verygoodcore_example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Application build +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) +apply_standard_settings(${BINARY_NAME}) +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) +add_dependencies(${BINARY_NAME} flutter_assemble) +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/media_key_detector/media_key_detector/example/linux/flutter/CMakeLists.txt b/media_key_detector/media_key_detector/example/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000..4f48a7ced --- /dev/null +++ b/media_key_detector/media_key_detector/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) +pkg_check_modules(BLKID REQUIRED IMPORTED_TARGET blkid) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO + PkgConfig::BLKID +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + linux-x64 ${CMAKE_BUILD_TYPE} +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/media_key_detector/media_key_detector/example/linux/flutter/generated_plugins.cmake b/media_key_detector/media_key_detector/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000..6f3c49242 --- /dev/null +++ b/media_key_detector/media_key_detector/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + media_key_detector_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/media_key_detector/media_key_detector/example/linux/main.cc b/media_key_detector/media_key_detector/example/linux/main.cc new file mode 100644 index 000000000..88a5fd45c --- /dev/null +++ b/media_key_detector/media_key_detector/example/linux/main.cc @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "my_application.h" + +int main(int argc, char** argv) { + // Only X11 is currently supported. + // Wayland support is being developed: + // https://github.com/flutter/flutter/issues/57932. + gdk_set_allowed_backends("x11"); + + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/media_key_detector/media_key_detector/example/linux/my_application.cc b/media_key_detector/media_key_detector/example/linux/my_application.cc new file mode 100644 index 000000000..9cb411ba4 --- /dev/null +++ b/media_key_detector/media_key_detector/example/linux/my_application.cc @@ -0,0 +1,49 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "my_application.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new( + my_application_get_type(), "application-id", APPLICATION_ID, nullptr)); +} diff --git a/media_key_detector/media_key_detector/example/linux/my_application.h b/media_key_detector/media_key_detector/example/linux/my_application.h new file mode 100644 index 000000000..6e9f0c3ff --- /dev/null +++ b/media_key_detector/media_key_detector/example/linux/my_application.h @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/media_key_detector/media_key_detector/example/macos/.gitignore b/media_key_detector/media_key_detector/example/macos/.gitignore new file mode 100644 index 000000000..746adbb6b --- /dev/null +++ b/media_key_detector/media_key_detector/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/media_key_detector/media_key_detector/example/macos/Flutter/Flutter-Debug.xcconfig b/media_key_detector/media_key_detector/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000..4b81f9b2d --- /dev/null +++ b/media_key_detector/media_key_detector/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/media_key_detector/media_key_detector/example/macos/Flutter/Flutter-Release.xcconfig b/media_key_detector/media_key_detector/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000..5caa9d157 --- /dev/null +++ b/media_key_detector/media_key_detector/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/media_key_detector/media_key_detector/example/macos/Podfile b/media_key_detector/media_key_detector/example/macos/Podfile new file mode 100644 index 000000000..049abe295 --- /dev/null +++ b/media_key_detector/media_key_detector/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/media_key_detector/media_key_detector/example/macos/Runner.xcodeproj/project.pbxproj b/media_key_detector/media_key_detector/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..9d02f0359 --- /dev/null +++ b/media_key_detector/media_key_detector/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,633 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 8056B9E1FA9168F10A6879E3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF88737E4227AF4483D73A72 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 082C0DA75B3DC0A853AE53D1 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 604426B4C55353DB93E63726 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 7CC079B1077C0BC3449BC7C1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + BF88737E4227AF4483D73A72 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8056B9E1FA9168F10A6879E3 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0EEA7CE099CD931C8BF18132 /* Pods */ = { + isa = PBXGroup; + children = ( + 604426B4C55353DB93E63726 /* Pods-Runner.debug.xcconfig */, + 7CC079B1077C0BC3449BC7C1 /* Pods-Runner.release.xcconfig */, + 082C0DA75B3DC0A853AE53D1 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 0EEA7CE099CD931C8BF18132 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + BF88737E4227AF4483D73A72 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + BA51270DFB65A51AF6561C86 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 01C1BE47E39E4FF65E75DFDD /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 01C1BE47E39E4FF65E75DFDD /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + BA51270DFB65A51AF6561C86 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/media_key_detector/media_key_detector/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/media_key_detector/media_key_detector/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/media_key_detector/media_key_detector/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/media_key_detector/media_key_detector/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/media_key_detector/media_key_detector/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000..5b055a3a3 --- /dev/null +++ b/media_key_detector/media_key_detector/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/media_key_detector/media_key_detector/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/media_key_detector/media_key_detector/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..21a3cc14c --- /dev/null +++ b/media_key_detector/media_key_detector/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/media_key_detector/media_key_detector/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/media_key_detector/media_key_detector/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/media_key_detector/media_key_detector/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/media_key_detector/media_key_detector/example/macos/Runner/AppDelegate.swift b/media_key_detector/media_key_detector/example/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000..d53ef6437 --- /dev/null +++ b/media_key_detector/media_key_detector/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..a2ec33f19 --- /dev/null +++ b/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 000000000..82b6f9d9a Binary files /dev/null and b/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 000000000..13b35eba5 Binary files /dev/null and b/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 000000000..0a3f5fa40 Binary files /dev/null and b/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 000000000..bdb57226d Binary files /dev/null and b/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 000000000..f083318e0 Binary files /dev/null and b/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 000000000..326c0e72c Binary files /dev/null and b/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 000000000..2f1632cfd Binary files /dev/null and b/media_key_detector/media_key_detector/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/media_key_detector/media_key_detector/example/macos/Runner/Base.lproj/MainMenu.xib b/media_key_detector/media_key_detector/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000..80e867a4e --- /dev/null +++ b/media_key_detector/media_key_detector/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/media_key_detector/media_key_detector/example/macos/Runner/Configs/AppInfo.xcconfig b/media_key_detector/media_key_detector/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000..c4c4609bb --- /dev/null +++ b/media_key_detector/media_key_detector/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.myplugin.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 com.example.verygoodcore. All rights reserved. diff --git a/media_key_detector/media_key_detector/example/macos/Runner/Configs/Debug.xcconfig b/media_key_detector/media_key_detector/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000..36b0fd946 --- /dev/null +++ b/media_key_detector/media_key_detector/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/media_key_detector/media_key_detector/example/macos/Runner/Configs/Release.xcconfig b/media_key_detector/media_key_detector/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000..dff4f4956 --- /dev/null +++ b/media_key_detector/media_key_detector/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/media_key_detector/media_key_detector/example/macos/Runner/Configs/Warnings.xcconfig b/media_key_detector/media_key_detector/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000..42bcbf478 --- /dev/null +++ b/media_key_detector/media_key_detector/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/media_key_detector/media_key_detector/example/macos/Runner/DebugProfile.entitlements b/media_key_detector/media_key_detector/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000..dddb8a30c --- /dev/null +++ b/media_key_detector/media_key_detector/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/media_key_detector/media_key_detector/example/macos/Runner/Info.plist b/media_key_detector/media_key_detector/example/macos/Runner/Info.plist new file mode 100644 index 000000000..4789daa6a --- /dev/null +++ b/media_key_detector/media_key_detector/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/media_key_detector/media_key_detector/example/macos/Runner/MainFlutterWindow.swift b/media_key_detector/media_key_detector/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000..2722837ec --- /dev/null +++ b/media_key_detector/media_key_detector/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/media_key_detector/media_key_detector/example/macos/Runner/Release.entitlements b/media_key_detector/media_key_detector/example/macos/Runner/Release.entitlements new file mode 100644 index 000000000..852fa1a47 --- /dev/null +++ b/media_key_detector/media_key_detector/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/media_key_detector/media_key_detector/example/pubspec.yaml b/media_key_detector/media_key_detector/example/pubspec.yaml new file mode 100644 index 000000000..50d03af5a --- /dev/null +++ b/media_key_detector/media_key_detector/example/pubspec.yaml @@ -0,0 +1,30 @@ +name: media_key_detector_example +description: Demonstrates how to use the media_key_detector plugin. +version: 0.1.0+1 +publish_to: none + +environment: + sdk: "^3.3.0" + +dependencies: + flutter: + sdk: flutter + media_key_detector: + # When depending on this package from a real application you should use: + # media_key_detector: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + very_good_analysis: ^5.1.0 + +flutter: + uses-material-design: true diff --git a/media_key_detector/media_key_detector/example/windows/.gitignore b/media_key_detector/media_key_detector/example/windows/.gitignore new file mode 100644 index 000000000..d492d0d98 --- /dev/null +++ b/media_key_detector/media_key_detector/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/media_key_detector/media_key_detector/example/windows/CMakeLists.txt b/media_key_detector/media_key_detector/example/windows/CMakeLists.txt new file mode 100644 index 000000000..1633297a0 --- /dev/null +++ b/media_key_detector/media_key_detector/example/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.14) +project(example LANGUAGES CXX) + +set(BINARY_NAME "example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() + +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/media_key_detector/media_key_detector/example/windows/flutter/CMakeLists.txt b/media_key_detector/media_key_detector/example/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000..b2e4bd8d6 --- /dev/null +++ b/media_key_detector/media_key_detector/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,103 @@ +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/media_key_detector/media_key_detector/example/windows/flutter/generated_plugins.cmake b/media_key_detector/media_key_detector/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000..aa414b8e4 --- /dev/null +++ b/media_key_detector/media_key_detector/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + media_key_detector_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/media_key_detector/media_key_detector/example/windows/runner/CMakeLists.txt b/media_key_detector/media_key_detector/example/windows/runner/CMakeLists.txt new file mode 100644 index 000000000..de2d8916b --- /dev/null +++ b/media_key_detector/media_key_detector/example/windows/runner/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) +apply_standard_settings(${BINARY_NAME}) +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/media_key_detector/media_key_detector/example/windows/runner/Runner.rc b/media_key_detector/media_key_detector/example/windows/runner/Runner.rc new file mode 100644 index 000000000..f11b007b1 --- /dev/null +++ b/media_key_detector/media_key_detector/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "dev.flutter.plugins" "\0" + VALUE "FileDescription", "example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 dev.flutter.plugins. All rights reserved." "\0" + VALUE "OriginalFilename", "example.exe" "\0" + VALUE "ProductName", "example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/media_key_detector/media_key_detector/example/windows/runner/flutter_window.cpp b/media_key_detector/media_key_detector/example/windows/runner/flutter_window.cpp new file mode 100644 index 000000000..b43b9095e --- /dev/null +++ b/media_key_detector/media_key_detector/example/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/media_key_detector/media_key_detector/example/windows/runner/flutter_window.h b/media_key_detector/media_key_detector/example/windows/runner/flutter_window.h new file mode 100644 index 000000000..6da0652f0 --- /dev/null +++ b/media_key_detector/media_key_detector/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/media_key_detector/media_key_detector/example/windows/runner/main.cpp b/media_key_detector/media_key_detector/example/windows/runner/main.cpp new file mode 100644 index 000000000..bcb57b0e2 --- /dev/null +++ b/media_key_detector/media_key_detector/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/media_key_detector/media_key_detector/example/windows/runner/resource.h b/media_key_detector/media_key_detector/example/windows/runner/resource.h new file mode 100644 index 000000000..d7b448fc5 --- /dev/null +++ b/media_key_detector/media_key_detector/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +// +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/media_key_detector/media_key_detector/example/windows/runner/resources/app_icon.ico b/media_key_detector/media_key_detector/example/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000..c04e20caf Binary files /dev/null and b/media_key_detector/media_key_detector/example/windows/runner/resources/app_icon.ico differ diff --git a/media_key_detector/media_key_detector/example/windows/runner/runner.exe.manifest b/media_key_detector/media_key_detector/example/windows/runner/runner.exe.manifest new file mode 100644 index 000000000..c977c4a42 --- /dev/null +++ b/media_key_detector/media_key_detector/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/media_key_detector/media_key_detector/example/windows/runner/utils.cpp b/media_key_detector/media_key_detector/example/windows/runner/utils.cpp new file mode 100644 index 000000000..d19bdbbcc --- /dev/null +++ b/media_key_detector/media_key_detector/example/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + if (target_length == 0) { + return std::string(); + } + std::string utf8_string; + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/media_key_detector/media_key_detector/example/windows/runner/utils.h b/media_key_detector/media_key_detector/example/windows/runner/utils.h new file mode 100644 index 000000000..3879d5475 --- /dev/null +++ b/media_key_detector/media_key_detector/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/media_key_detector/media_key_detector/example/windows/runner/win32_window.cpp b/media_key_detector/media_key_detector/example/windows/runner/win32_window.cpp new file mode 100644 index 000000000..c10f08dc7 --- /dev/null +++ b/media_key_detector/media_key_detector/example/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/media_key_detector/media_key_detector/example/windows/runner/win32_window.h b/media_key_detector/media_key_detector/example/windows/runner/win32_window.h new file mode 100644 index 000000000..17ba43112 --- /dev/null +++ b/media_key_detector/media_key_detector/example/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/media_key_detector/media_key_detector/lib/media_key_detector.dart b/media_key_detector/media_key_detector/lib/media_key_detector.dart new file mode 100644 index 000000000..6a9c4e2ab --- /dev/null +++ b/media_key_detector/media_key_detector/lib/media_key_detector.dart @@ -0,0 +1,16 @@ +/// Plugin which allows you to detect the media buttons on the keyboard, which +/// does not fire for regular key events on MacOS +library media_key_detector; + +import 'package:media_key_detector_platform_interface/media_key_detector_platform_interface.dart'; + +export './src/exports.dart'; + +MediaKeyDetectorPlatform get _platform => MediaKeyDetectorPlatform.instance; + +/// Returns the name of the current platform. +Future getPlatformName() async { + final platformName = await _platform.getPlatformName(); + if (platformName == null) throw Exception('Unable to get platform name.'); + return platformName; +} diff --git a/media_key_detector/media_key_detector/lib/src/exports.dart b/media_key_detector/media_key_detector/lib/src/exports.dart new file mode 100644 index 000000000..0c1b27386 --- /dev/null +++ b/media_key_detector/media_key_detector/lib/src/exports.dart @@ -0,0 +1,4 @@ +export 'package:media_key_detector_platform_interface/media_key_detector_platform_interface.dart' + show MediaKey; +export './main.dart' show getPlatformName; +export './media_key_detector.dart' show MediaKeyDetector, mediaKeyDetector; diff --git a/media_key_detector/media_key_detector/lib/src/main.dart b/media_key_detector/media_key_detector/lib/src/main.dart new file mode 100644 index 000000000..8055cb4d7 --- /dev/null +++ b/media_key_detector/media_key_detector/lib/src/main.dart @@ -0,0 +1,10 @@ +import 'package:media_key_detector_platform_interface/media_key_detector_platform_interface.dart'; + +MediaKeyDetectorPlatform get _platform => MediaKeyDetectorPlatform.instance; + +/// Returns the name of the current platform. +Future getPlatformName() async { + final platformName = await _platform.getPlatformName(); + if (platformName == null) throw Exception('Unable to get platform name.'); + return platformName; +} diff --git a/media_key_detector/media_key_detector/lib/src/media_key_detector.dart b/media_key_detector/media_key_detector/lib/src/media_key_detector.dart new file mode 100644 index 000000000..eb48472f5 --- /dev/null +++ b/media_key_detector/media_key_detector/lib/src/media_key_detector.dart @@ -0,0 +1,46 @@ +import 'package:media_key_detector_platform_interface/media_key_detector_platform_interface.dart'; + +MediaKeyDetectorPlatform get _platform => MediaKeyDetectorPlatform.instance; + +/// Contains methods to add/remove listeners for the media key +class MediaKeyDetector { + /// Contains methods to add/remove listeners for the media key + factory MediaKeyDetector() { + return _singleton; + } + + MediaKeyDetector._internal(); + + static final MediaKeyDetector _singleton = MediaKeyDetector._internal(); + + bool _initialized = false; + + /// Listen for the media key event + void addListener(void Function(MediaKey mediaKey) listener) { + _lazilyInitialize(); + _platform.addListener(listener); + } + + /// Remove the previously registered listener + void removeListener(void Function(MediaKey mediaKey) listener) { + _lazilyInitialize(); + _platform.removeListener(listener); + } + + void _lazilyInitialize() { + if (!_initialized) { + _platform.initialize(); + _initialized = true; + } + } + + /// Get whether the active audio player is currently playing. + Future getIsPlaying() => _platform.getIsPlaying(); + + /// Set whether the active audio player is currently playing. + Future setIsPlaying({required bool isPlaying}) => + _platform.setIsPlaying(isPlaying: isPlaying); +} + +/// Global singleton instance of the [MediaKeyDetector] +final mediaKeyDetector = MediaKeyDetector._singleton; diff --git a/media_key_detector/media_key_detector/pubspec.yaml b/media_key_detector/media_key_detector/pubspec.yaml new file mode 100644 index 000000000..248559289 --- /dev/null +++ b/media_key_detector/media_key_detector/pubspec.yaml @@ -0,0 +1,42 @@ +name: media_key_detector +description: Triggers events when keyboard media keys are pressed. +version: 0.0.2 +homepage: https://github.com/holotrek/media_key_detector + +environment: + sdk: "^3.3.0" + flutter: ">=3.19.3" + +flutter: + plugin: + platforms: + linux: + default_package: media_key_detector_linux + macos: + default_package: media_key_detector_macos + ios: + default_package: media_key_detector_ios + windows: + default_package: media_key_detector_windows + +dependencies: + flutter: + sdk: flutter + + media_key_detector_ios: + path: ../media_key_detector_ios + media_key_detector_linux: + path: ../media_key_detector_linux + media_key_detector_macos: + path: ../media_key_detector_macos + media_key_detector_platform_interface: + path: ../media_key_detector_platform_interface + media_key_detector_windows: + path: ../media_key_detector_windows + +dev_dependencies: + flutter_test: + sdk: flutter + mocktail: ^1.0.3 + plugin_platform_interface: ^2.1.8 + very_good_analysis: ^5.1.0 diff --git a/media_key_detector/media_key_detector/test/media_key_detector_test.dart b/media_key_detector/media_key_detector/test/media_key_detector_test.dart new file mode 100644 index 000000000..1795190cc --- /dev/null +++ b/media_key_detector/media_key_detector/test/media_key_detector_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:media_key_detector/media_key_detector.dart'; +import 'package:media_key_detector_platform_interface/media_key_detector_platform_interface.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class MockMediaKeyDetectorPlatform extends Mock + with MockPlatformInterfaceMixin + implements MediaKeyDetectorPlatform {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('MediaKeyDetector', () { + late MediaKeyDetectorPlatform mediaKeyDetectorPlatform; + + setUp(() { + mediaKeyDetectorPlatform = MockMediaKeyDetectorPlatform(); + MediaKeyDetectorPlatform.instance = mediaKeyDetectorPlatform; + }); + + group('getPlatformName', () { + test('returns correct name when platform implementation exists', + () async { + const platformName = '__test_platform__'; + when( + () => mediaKeyDetectorPlatform.getPlatformName(), + ).thenAnswer((_) async => platformName); + + final actualPlatformName = await getPlatformName(); + expect(actualPlatformName, equals(platformName)); + }); + + test('throws exception when platform implementation is missing', + () async { + when( + () => mediaKeyDetectorPlatform.getPlatformName(), + ).thenAnswer((_) async => null); + + expect(getPlatformName, throwsException); + }); + }); + }); +} diff --git a/media_key_detector/media_key_detector_ios/.gitignore b/media_key_detector/media_key_detector_ios/.gitignore new file mode 100644 index 000000000..53e92cc41 --- /dev/null +++ b/media_key_detector/media_key_detector_ios/.gitignore @@ -0,0 +1,3 @@ +.packages +.flutter-plugins +pubspec.lock diff --git a/media_key_detector/media_key_detector_ios/CHANGELOG.md b/media_key_detector/media_key_detector_ios/CHANGELOG.md new file mode 100644 index 000000000..589555b74 --- /dev/null +++ b/media_key_detector/media_key_detector_ios/CHANGELOG.md @@ -0,0 +1,7 @@ +# 0.0.2 + +- TBD + +# 0.0.1 + +- Initial Release diff --git a/media_key_detector/media_key_detector_ios/LICENSE b/media_key_detector/media_key_detector_ios/LICENSE new file mode 100644 index 000000000..a4110b7e0 --- /dev/null +++ b/media_key_detector/media_key_detector_ios/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 Evan Kaiser + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/media_key_detector/media_key_detector_ios/README.md b/media_key_detector/media_key_detector_ios/README.md new file mode 100644 index 000000000..9645477ca --- /dev/null +++ b/media_key_detector/media_key_detector_ios/README.md @@ -0,0 +1,14 @@ +# media_key_detector_macos + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] + +The macos implementation of `media_key_detector`. + +## Usage + +This package is [endorsed][endorsed_link], which means you can simply use `media_key_detector` +normally. This package will be automatically included in your app when you do. + +[endorsed_link]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis diff --git a/media_key_detector/media_key_detector_ios/analysis_options.yaml b/media_key_detector/media_key_detector_ios/analysis_options.yaml new file mode 100644 index 000000000..799268d3e --- /dev/null +++ b/media_key_detector/media_key_detector_ios/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.5.1.0.yaml diff --git a/media_key_detector/media_key_detector_ios/ios/Classes/MediaKeyDetectorPlugin.swift b/media_key_detector/media_key_detector_ios/ios/Classes/MediaKeyDetectorPlugin.swift new file mode 100644 index 000000000..6c3a5d975 --- /dev/null +++ b/media_key_detector/media_key_detector_ios/ios/Classes/MediaKeyDetectorPlugin.swift @@ -0,0 +1,143 @@ +import Flutter +import MediaPlayer +import Foundation + +public class MediaKeyDetectorPlugin: NSObject, FlutterPlugin { + private var mediaKeyHandler = MediaKeyHandler() + var isNowPlayable = false + var playPauseTarget: Any?; + var previousTrackTarget: Any?; + var nextTrackTarget: Any?; + private var player: AVPlayer? + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel( + name: "media_key_detector_ios", + binaryMessenger: registrar.messenger()) + + // Setup an event channel in addition to the method channel. This will react to native events and then notify Dart code. + let eventChannel = FlutterEventChannel( + name: "media_key_detector_ios_events", + binaryMessenger: registrar.messenger()) + + let instance = MediaKeyDetectorPlugin() + + // Add the MediaKeyHandler to the event channel stream + eventChannel.setStreamHandler(instance.mediaKeyHandler) + + // Register the instance to listen to method calls from Dart + registrar.addMethodCallDelegate(instance, channel: channel) + + // Register the instance to handle the FlutterAppLifecycle + registrar.addApplicationDelegate(instance) + } + + func makeNowPlayable() throws { + + // 1) Activate audio session + let session = AVAudioSession.sharedInstance() + try session.setCategory(.playback, mode: .default, options: []) + try session.setActive(true) + + + var url = URL(string: "https://github.com/anars/blank-audio/raw/refs/heads/master/5-seconds-of-silence.mp3")! + + // 2) Player + let item = AVPlayerItem(url: url) + player = AVPlayer(playerItem: item) + player?.play() + + + // 2) Seed Now Playing info + var info: [String: Any] = [ + MPMediaItemPropertyTitle: "BikeControl", + MPMediaItemPropertyArtist: "BikeControl", + MPNowPlayingInfoPropertyElapsedPlaybackTime: 0, + MPMediaItemPropertyPlaybackDuration: 1337, // nonzero duration helps + MPNowPlayingInfoPropertyPlaybackRate: 1 // paused + ] + MPNowPlayingInfoCenter.default().nowPlayingInfo = info + MPNowPlayingInfoCenter.default().playbackState = .paused + + // 3) Enable and add handlers + let cc = MPRemoteCommandCenter.shared() + cc.togglePlayPauseCommand.isEnabled = true + cc.previousTrackCommand.isEnabled = true + cc.nextTrackCommand.isEnabled = true + + + playPauseTarget = cc.togglePlayPauseCommand.addTarget { [weak self] _ in + guard let self else { return .commandFailed } + self.mediaKeyHandler.onKeyEvent(key: 0) + + let playing = MPNowPlayingInfoCenter.default().playbackState == .playing + MPNowPlayingInfoCenter.default().playbackState = playing ? .paused : .playing + info[MPNowPlayingInfoPropertyPlaybackRate] = playing ? 0 : 1 + MPNowPlayingInfoCenter.default().nowPlayingInfo = info + return .success + } + previousTrackTarget = cc.previousTrackCommand.addTarget { [weak self] _ in + self?.mediaKeyHandler.onKeyEvent(key: 1) + return .success + } + nextTrackTarget = cc.nextTrackCommand.addTarget { [weak self] _ in + self?.mediaKeyHandler.onKeyEvent(key: 2) + return .success + } + isNowPlayable = true + } + + public func applicationWillTerminate(_ application: UIApplication) { + + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformName": + result("MacOS") + case "getIsPlaying": + result(MPNowPlayingInfoCenter.default().playbackState == .playing) + case "setIsPlaying": + guard let args = call.arguments as? Dictionary else {return} + let isPlaying = args["isPlaying"] as! Bool + if (isPlaying) { + do { + try makeNowPlayable() + } + catch { + print(error) + } + + } else { + player?.pause() + + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.togglePlayPauseCommand.removeTarget(self.playPauseTarget) + commandCenter.togglePlayPauseCommand.removeTarget(self.previousTrackTarget) + commandCenter.togglePlayPauseCommand.removeTarget(self.nextTrackTarget) + MPNowPlayingInfoCenter.default().playbackState = .stopped + } + default: + result(FlutterMethodNotImplemented) + } + } +} + +class MediaKeyHandler: NSObject, FlutterStreamHandler { + // Declare our eventSink, it will be initialized later + private var eventSink: FlutterEventSink? + + func onKeyEvent(key: Int32) { + self.eventSink?(key) + } + + func onListen(withArguments arguments: Any?, eventSink: @escaping FlutterEventSink) -> FlutterError? { + self.eventSink = eventSink + return nil + } + + func onCancel(withArguments arguments: Any?) -> FlutterError? { + eventSink = nil + return nil + } +} diff --git a/media_key_detector/media_key_detector_ios/ios/media_key_detector_ios.podspec b/media_key_detector/media_key_detector_ios/ios/media_key_detector_ios.podspec new file mode 100644 index 000000000..8a6bc2364 --- /dev/null +++ b/media_key_detector/media_key_detector_ios/ios/media_key_detector_ios.podspec @@ -0,0 +1,22 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'media_key_detector_ios' + s.version = '0.0.1' + s.summary = 'An iOS implementation of the media_key_detector plugin.' + s.description = <<-DESC + A macOS implementation of the media_key_detector plugin. + DESC + s.homepage = 'http://example.com' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + + s.platform = :ios, '13.0' + s.ios.deployment_target = '13.0' + s.swift_version = '5.0' +end + diff --git a/media_key_detector/media_key_detector_ios/lib/media_key_detector_ios.dart b/media_key_detector/media_key_detector_ios/lib/media_key_detector_ios.dart new file mode 100644 index 000000000..355a0395a --- /dev/null +++ b/media_key_detector/media_key_detector_ios/lib/media_key_detector_ios.dart @@ -0,0 +1,47 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:media_key_detector_platform_interface/media_key_detector_platform_interface.dart'; + +/// The iOS implementation of [MediaKeyDetectorPlatform]. +class MediaKeyDetectorIOS extends MediaKeyDetectorPlatform { + final _eventChannel = const EventChannel('media_key_detector_ios_events'); + + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('media_key_detector_ios'); + + /// Registers this class as the default instance of [MediaKeyDetectorPlatform] + static void registerWith() { + MediaKeyDetectorPlatform.instance = MediaKeyDetectorIOS(); + } + + @override + void initialize() { + _eventChannel.receiveBroadcastStream().listen((event) { + final keyIdx = event as int; + MediaKey? key; + if (keyIdx > -1 && keyIdx < MediaKey.values.length) { + key = MediaKey.values[keyIdx]; + } + if (key != null) { + triggerListeners(key); + } + }); + } + + @override + Future getPlatformName() { + return methodChannel.invokeMethod('getPlatformName'); + } + + @override + Future getIsPlaying() async { + final isPlaying = await methodChannel.invokeMethod('getIsPlaying'); + return isPlaying ?? false; + } + + @override + Future setIsPlaying({required bool isPlaying}) { + return methodChannel.invokeMethod('setIsPlaying', {'isPlaying': isPlaying}); + } +} diff --git a/media_key_detector/media_key_detector_ios/pubspec.yaml b/media_key_detector/media_key_detector_ios/pubspec.yaml new file mode 100644 index 000000000..e0af74f13 --- /dev/null +++ b/media_key_detector/media_key_detector_ios/pubspec.yaml @@ -0,0 +1,27 @@ +name: media_key_detector_ios +description: iOS implementation of the media_key_detector plugin +version: 0.0.2 +homepage: https://github.com/holotrek/media_key_detector + +environment: + sdk: "^3.3.0" + flutter: ">=3.19.3" + +flutter: + plugin: + implements: media_key_detector + platforms: + ios: + pluginClass: MediaKeyDetectorPlugin + dartPluginClass: MediaKeyDetectorIOS + +dependencies: + flutter: + sdk: flutter + media_key_detector_platform_interface: + path: ../media_key_detector_platform_interface + +dev_dependencies: + flutter_test: + sdk: flutter + very_good_analysis: ^5.1.0 diff --git a/media_key_detector/media_key_detector_linux/.gitignore b/media_key_detector/media_key_detector_linux/.gitignore new file mode 100644 index 000000000..e9dc58d3d --- /dev/null +++ b/media_key_detector/media_key_detector_linux/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ diff --git a/media_key_detector/media_key_detector_linux/CHANGELOG.md b/media_key_detector/media_key_detector_linux/CHANGELOG.md new file mode 100644 index 000000000..589555b74 --- /dev/null +++ b/media_key_detector/media_key_detector_linux/CHANGELOG.md @@ -0,0 +1,7 @@ +# 0.0.2 + +- TBD + +# 0.0.1 + +- Initial Release diff --git a/media_key_detector/media_key_detector_linux/LICENSE b/media_key_detector/media_key_detector_linux/LICENSE new file mode 100644 index 000000000..a4110b7e0 --- /dev/null +++ b/media_key_detector/media_key_detector_linux/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 Evan Kaiser + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/media_key_detector/media_key_detector_linux/README.md b/media_key_detector/media_key_detector_linux/README.md new file mode 100644 index 000000000..fa2b93789 --- /dev/null +++ b/media_key_detector/media_key_detector_linux/README.md @@ -0,0 +1,14 @@ +# media_key_detector_linux + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] + +The linux implementation of `media_key_detector`. + +## Usage + +This package is [endorsed][endorsed_link], which means you can simply use `media_key_detector` +normally. This package will be automatically included in your app when you do. + +[endorsed_link]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis diff --git a/media_key_detector/media_key_detector_linux/analysis_options.yaml b/media_key_detector/media_key_detector_linux/analysis_options.yaml new file mode 100644 index 000000000..799268d3e --- /dev/null +++ b/media_key_detector/media_key_detector_linux/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.5.1.0.yaml diff --git a/media_key_detector/media_key_detector_linux/lib/media_key_detector_linux.dart b/media_key_detector/media_key_detector_linux/lib/media_key_detector_linux.dart new file mode 100644 index 000000000..e84dd9c71 --- /dev/null +++ b/media_key_detector/media_key_detector_linux/lib/media_key_detector_linux.dart @@ -0,0 +1 @@ +export 'src/media_key_detector_linux.dart'; diff --git a/media_key_detector/media_key_detector_linux/lib/src/media_key_detector_linux.dart b/media_key_detector/media_key_detector_linux/lib/src/media_key_detector_linux.dart new file mode 100644 index 000000000..3090337e1 --- /dev/null +++ b/media_key_detector/media_key_detector_linux/lib/src/media_key_detector_linux.dart @@ -0,0 +1,37 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:media_key_detector_platform_interface/media_key_detector_platform_interface.dart'; + +/// The Linux implementation of [MediaKeyDetectorPlatform]. +class MediaKeyDetectorLinux extends MediaKeyDetectorPlatform { + bool _isPlaying = false; + + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('media_key_detector_linux'); + + /// Registers this class as the default instance of [MediaKeyDetectorPlatform] + static void registerWith() { + MediaKeyDetectorPlatform.instance = MediaKeyDetectorLinux(); + } + + @override + void initialize() { + ServicesBinding.instance.keyboard.addHandler(defaultHandler); + } + + @override + Future getPlatformName() { + return methodChannel.invokeMethod('getPlatformName'); + } + + @override + Future getIsPlaying() async { + return _isPlaying; + } + + @override + Future setIsPlaying({required bool isPlaying}) async { + _isPlaying = isPlaying; + } +} diff --git a/media_key_detector/media_key_detector_linux/linux/.gitignore b/media_key_detector/media_key_detector_linux/linux/.gitignore new file mode 100644 index 000000000..b3eb2be16 --- /dev/null +++ b/media_key_detector/media_key_detector_linux/linux/.gitignore @@ -0,0 +1,17 @@ +flutter/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/media_key_detector/media_key_detector_linux/linux/CMakeLists.txt b/media_key_detector/media_key_detector_linux/linux/CMakeLists.txt new file mode 100644 index 000000000..2d2f3ee4a --- /dev/null +++ b/media_key_detector/media_key_detector_linux/linux/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.10) +set(PROJECT_NAME "media_key_detector_linux") +project(${PROJECT_NAME} LANGUAGES CXX) + +set(PLUGIN_NAME "${PROJECT_NAME}_plugin") + +list(APPEND PLUGIN_SOURCES + "media_key_detector_linux_plugin.cc" +) + +add_library(${PLUGIN_NAME} SHARED + ${PLUGIN_SOURCES} +) +apply_standard_settings(${PLUGIN_NAME}) +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter) +target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK) diff --git a/media_key_detector/media_key_detector_linux/linux/include/media_key_detector_linux/media_key_detector_plugin.h b/media_key_detector/media_key_detector_linux/linux/include/media_key_detector_linux/media_key_detector_plugin.h new file mode 100644 index 000000000..00d790b51 --- /dev/null +++ b/media_key_detector/media_key_detector_linux/linux/include/media_key_detector_linux/media_key_detector_plugin.h @@ -0,0 +1,25 @@ +#ifndef FLUTTER_PLUGIN_MEDIA_KEY_DETECTOR_LINUX_PLUGIN_H_ +#define FLUTTER_PLUGIN_MEDIA_KEY_DETECTOR_LINUX_PLUGIN_H_ + +#include + +G_BEGIN_DECLS + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default"))) +#else +#define FLUTTER_PLUGIN_EXPORT +#endif + +G_DECLARE_FINAL_TYPE(FlMediaKeyDetectorPlugin, fl_media_key_detector_plugin, FL, + MEDIA_KEY_DETECTOR_PLUGIN, GObject) + +FLUTTER_PLUGIN_EXPORT FlMediaKeyDetectorPlugin* fl_media_key_detector_plugin_new( + FlPluginRegistrar* registrar); + +FLUTTER_PLUGIN_EXPORT void media_key_detector_plugin_register_with_registrar( + FlPluginRegistrar* registrar); + +G_END_DECLS + +#endif // FLUTTER_PLUGIN_MEDIA_KEY_DETECTOR_LINUX_PLUGIN_H_ \ No newline at end of file diff --git a/media_key_detector/media_key_detector_linux/linux/media_key_detector_linux_plugin.cc b/media_key_detector/media_key_detector_linux/linux/media_key_detector_linux_plugin.cc new file mode 100644 index 000000000..1d0b1dc04 --- /dev/null +++ b/media_key_detector/media_key_detector_linux/linux/media_key_detector_linux_plugin.cc @@ -0,0 +1,68 @@ +#include "include/media_key_detector_linux/media_key_detector_plugin.h" + +#include +#include +#include + +#include + +const char kChannelName[] = "media_key_detector_linux"; +const char kGetPlatformName[] = "getPlatformName"; + +struct _FlMediaKeyDetectorPlugin { + GObject parent_instance; + + FlPluginRegistrar* registrar; + + // Connection to Flutter engine. + FlMethodChannel* channel; +}; + +G_DEFINE_TYPE(FlMediaKeyDetectorPlugin, fl_media_key_detector_plugin, g_object_get_type()) + +// Called when a method call is received from Flutter. +static void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call, + gpointer user_data) { + const gchar* method = fl_method_call_get_name(method_call); + + g_autoptr(FlMethodResponse) response = nullptr; + if (strcmp(method, kGetPlatformName) == 0) + response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_string("Linux"))); + else + response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); + + g_autoptr(GError) error = nullptr; + if (!fl_method_call_respond(method_call, response, &error)) + g_warning("Failed to send method call response: %s", error->message); +} + +static void fl_media_key_detector_plugin_dispose(GObject* object) { + G_OBJECT_CLASS(fl_media_key_detector_plugin_parent_class)->dispose(object); +} + +static void fl_media_key_detector_plugin_class_init(FlMediaKeyDetectorPluginClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_media_key_detector_plugin_dispose; +} + +FlMediaKeyDetectorPlugin* fl_media_key_detector_plugin_new(FlPluginRegistrar* registrar) { + FlMediaKeyDetectorPlugin* self = FL_MEDIA_KEY_DETECTOR_PLUGIN( + g_object_new(fl_media_key_detector_plugin_get_type(), nullptr)); + + self->registrar = FL_PLUGIN_REGISTRAR(g_object_ref(registrar)); + + g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); + self->channel = + fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar), + kChannelName, FL_METHOD_CODEC(codec)); + fl_method_channel_set_method_call_handler(self->channel, method_call_cb, + g_object_ref(self), g_object_unref); + + return self; +} + +static void fl_media_key_detector_plugin_init(FlMediaKeyDetectorPlugin* self) {} + +void media_key_detector_plugin_register_with_registrar(FlPluginRegistrar* registrar) { + FlMediaKeyDetectorPlugin* plugin = fl_media_key_detector_plugin_new(registrar); + g_object_unref(plugin); +} diff --git a/media_key_detector/media_key_detector_linux/pubspec.yaml b/media_key_detector/media_key_detector_linux/pubspec.yaml new file mode 100644 index 000000000..2371e7eab --- /dev/null +++ b/media_key_detector/media_key_detector_linux/pubspec.yaml @@ -0,0 +1,27 @@ +name: media_key_detector_linux +description: Linux implementation of the media_key_detector plugin +version: 0.0.2 +homepage: https://github.com/holotrek/media_key_detector + +environment: + sdk: "^3.3.0" + flutter: ">=3.19.3" + +flutter: + plugin: + implements: media_key_detector + platforms: + linux: + pluginClass: MediaKeyDetectorPlugin + dartPluginClass: MediaKeyDetectorLinux + +dependencies: + flutter: + sdk: flutter + media_key_detector_platform_interface: + path: ../media_key_detector_platform_interface + +dev_dependencies: + flutter_test: + sdk: flutter + very_good_analysis: ^5.1.0 diff --git a/media_key_detector/media_key_detector_linux/test/media_key_detector_linux_test.dart b/media_key_detector/media_key_detector_linux/test/media_key_detector_linux_test.dart new file mode 100644 index 000000000..8a849e8b5 --- /dev/null +++ b/media_key_detector/media_key_detector_linux/test/media_key_detector_linux_test.dart @@ -0,0 +1,45 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:media_key_detector_linux/media_key_detector_linux.dart'; +import 'package:media_key_detector_platform_interface/media_key_detector_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('MediaKeyDetectorLinux', () { + const kPlatformName = 'Linux'; + late MediaKeyDetectorLinux mediaKeyDetector; + late List log; + + setUp(() async { + mediaKeyDetector = MediaKeyDetectorLinux(); + + log = []; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(mediaKeyDetector.methodChannel, + (methodCall) async { + log.add(methodCall); + switch (methodCall.method) { + case 'getPlatformName': + return kPlatformName; + default: + return null; + } + }); + }); + + test('can be registered', () { + MediaKeyDetectorLinux.registerWith(); + expect(MediaKeyDetectorPlatform.instance, isA()); + }); + + test('getPlatformName returns correct name', () async { + final name = await mediaKeyDetector.getPlatformName(); + expect( + log, + [isMethodCall('getPlatformName', arguments: null)], + ); + expect(name, equals(kPlatformName)); + }); + }); +} diff --git a/media_key_detector/media_key_detector_macos/.gitignore b/media_key_detector/media_key_detector_macos/.gitignore new file mode 100644 index 000000000..53e92cc41 --- /dev/null +++ b/media_key_detector/media_key_detector_macos/.gitignore @@ -0,0 +1,3 @@ +.packages +.flutter-plugins +pubspec.lock diff --git a/media_key_detector/media_key_detector_macos/CHANGELOG.md b/media_key_detector/media_key_detector_macos/CHANGELOG.md new file mode 100644 index 000000000..589555b74 --- /dev/null +++ b/media_key_detector/media_key_detector_macos/CHANGELOG.md @@ -0,0 +1,7 @@ +# 0.0.2 + +- TBD + +# 0.0.1 + +- Initial Release diff --git a/media_key_detector/media_key_detector_macos/LICENSE b/media_key_detector/media_key_detector_macos/LICENSE new file mode 100644 index 000000000..a4110b7e0 --- /dev/null +++ b/media_key_detector/media_key_detector_macos/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 Evan Kaiser + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/media_key_detector/media_key_detector_macos/README.md b/media_key_detector/media_key_detector_macos/README.md new file mode 100644 index 000000000..9645477ca --- /dev/null +++ b/media_key_detector/media_key_detector_macos/README.md @@ -0,0 +1,14 @@ +# media_key_detector_macos + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] + +The macos implementation of `media_key_detector`. + +## Usage + +This package is [endorsed][endorsed_link], which means you can simply use `media_key_detector` +normally. This package will be automatically included in your app when you do. + +[endorsed_link]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis diff --git a/media_key_detector/media_key_detector_macos/analysis_options.yaml b/media_key_detector/media_key_detector_macos/analysis_options.yaml new file mode 100644 index 000000000..799268d3e --- /dev/null +++ b/media_key_detector/media_key_detector_macos/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.5.1.0.yaml diff --git a/media_key_detector/media_key_detector_macos/lib/media_key_detector_macos.dart b/media_key_detector/media_key_detector_macos/lib/media_key_detector_macos.dart new file mode 100644 index 000000000..17f959a49 --- /dev/null +++ b/media_key_detector/media_key_detector_macos/lib/media_key_detector_macos.dart @@ -0,0 +1,49 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:media_key_detector_platform_interface/media_key_detector_platform_interface.dart'; + +/// The MacOS implementation of [MediaKeyDetectorPlatform]. +class MediaKeyDetectorMacOS extends MediaKeyDetectorPlatform { + final _eventChannel = const EventChannel('media_key_detector_macos_events'); + + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('media_key_detector_macos'); + + /// Registers this class as the default instance of [MediaKeyDetectorPlatform] + static void registerWith() { + MediaKeyDetectorPlatform.instance = MediaKeyDetectorMacOS(); + } + + @override + void initialize() { + _eventChannel.receiveBroadcastStream().listen((event) { + final keyIdx = event as int; + MediaKey? key; + if (keyIdx > -1 && keyIdx < MediaKey.values.length) { + key = MediaKey.values[keyIdx]; + } + if (key != null) { + triggerListeners(key); + } + }); + } + + @override + Future getPlatformName() { + return methodChannel.invokeMethod('getPlatformName'); + } + + @override + Future getIsPlaying() async { + final isPlaying = await methodChannel.invokeMethod('getIsPlaying'); + return isPlaying ?? false; + } + + @override + Future setIsPlaying({required bool isPlaying}) { + return methodChannel.invokeMethod('setIsPlaying', { + 'isPlaying': isPlaying, + }); + } +} diff --git a/media_key_detector/media_key_detector_macos/macos/Classes/MediaKeyDetectorPlugin.swift b/media_key_detector/media_key_detector_macos/macos/Classes/MediaKeyDetectorPlugin.swift new file mode 100644 index 000000000..bffe46ae7 --- /dev/null +++ b/media_key_detector/media_key_detector_macos/macos/Classes/MediaKeyDetectorPlugin.swift @@ -0,0 +1,106 @@ +import FlutterMacOS +import MediaPlayer +import Foundation + +public class MediaKeyDetectorPlugin: NSObject, FlutterPlugin, FlutterAppLifecycleDelegate { + private var mediaKeyHandler = MediaKeyHandler() + var isNowPlayable = false + var playPauseTarget: Any?; + var previousTrackTarget: Any?; + var nextTrackTarget: Any?; + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel( + name: "media_key_detector_macos", + binaryMessenger: registrar.messenger) + + // Setup an event channel in addition to the method channel. This will react to native events and then notify Dart code. + let eventChannel = FlutterEventChannel( + name: "media_key_detector_macos_events", + binaryMessenger: registrar.messenger) + + let instance = MediaKeyDetectorPlugin() + + // Add the MediaKeyHandler to the event channel stream + eventChannel.setStreamHandler(instance.mediaKeyHandler) + + // Register the instance to listen to method calls from Dart + registrar.addMethodCallDelegate(instance, channel: channel) + + // Register the instance to handle the FlutterAppLifecycle + registrar.addApplicationDelegate(instance) + } + + func makeNowPlayable() throws { + if (isNowPlayable) { + return + } + + let commandCenter = MPRemoteCommandCenter.shared() + self.playPauseTarget = commandCenter.togglePlayPauseCommand.addTarget { (commandEvent) -> MPRemoteCommandHandlerStatus in + self.mediaKeyHandler.onKeyEvent(key: 0); + MPNowPlayingInfoCenter.default().playbackState = MPNowPlayingInfoCenter.default().playbackState == .playing ? .paused : .playing + return .success + } + self.previousTrackTarget = commandCenter.previousTrackCommand.addTarget { (commandEvent) -> MPRemoteCommandHandlerStatus in + self.mediaKeyHandler.onKeyEvent(key: 1); + return .success + } + self.nextTrackTarget = commandCenter.nextTrackCommand.addTarget { (commandEvent) -> MPRemoteCommandHandlerStatus in + self.mediaKeyHandler.onKeyEvent(key: 2); + return .success + } + MPNowPlayingInfoCenter.default().playbackState = .playing + MPNowPlayingInfoCenter.default().playbackState = .paused + isNowPlayable = true + } + + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformName": + result("MacOS") + case "getIsPlaying": + result(MPNowPlayingInfoCenter.default().playbackState == .playing) + case "setIsPlaying": + guard let args = call.arguments as? Dictionary else {return} + let isPlaying = args["isPlaying"] as! Bool + if (isPlaying) { + do { + try makeNowPlayable() + } catch { + print(error) + } + MPNowPlayingInfoCenter.default().playbackState = isPlaying ? .playing : .paused + } else { + + let commandCenter = MPRemoteCommandCenter.shared() + commandCenter.togglePlayPauseCommand.removeTarget(self.playPauseTarget) + commandCenter.togglePlayPauseCommand.removeTarget(self.previousTrackTarget) + commandCenter.togglePlayPauseCommand.removeTarget(self.nextTrackTarget) + MPNowPlayingInfoCenter.default().playbackState = .stopped + } + default: + result(FlutterMethodNotImplemented) + } + } +} + +class MediaKeyHandler: NSObject, FlutterStreamHandler { + // Declare our eventSink, it will be initialized later + private var eventSink: FlutterEventSink? + + func onKeyEvent(key: Int32) { + self.eventSink?(key) + } + + func onListen(withArguments arguments: Any?, eventSink: @escaping FlutterEventSink) -> FlutterError? { + self.eventSink = eventSink + return nil + } + + func onCancel(withArguments arguments: Any?) -> FlutterError? { + eventSink = nil + return nil + } +} diff --git a/media_key_detector/media_key_detector_macos/macos/media_key_detector_macos.podspec b/media_key_detector/media_key_detector_macos/macos/media_key_detector_macos.podspec new file mode 100644 index 000000000..40e61f7d1 --- /dev/null +++ b/media_key_detector/media_key_detector_macos/macos/media_key_detector_macos.podspec @@ -0,0 +1,22 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'media_key_detector_macos' + s.version = '0.0.1' + s.summary = 'A macOS implementation of the media_key_detector plugin.' + s.description = <<-DESC + A macOS implementation of the media_key_detector plugin. + DESC + s.homepage = 'http://example.com' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + + s.platform = :osx + s.osx.deployment_target = '10.11' + s.swift_version = '5.0' +end + diff --git a/media_key_detector/media_key_detector_macos/pubspec.yaml b/media_key_detector/media_key_detector_macos/pubspec.yaml new file mode 100644 index 000000000..98b5206b9 --- /dev/null +++ b/media_key_detector/media_key_detector_macos/pubspec.yaml @@ -0,0 +1,27 @@ +name: media_key_detector_macos +description: MacOS implementation of the media_key_detector plugin +version: 0.0.2 +homepage: https://github.com/holotrek/media_key_detector + +environment: + sdk: "^3.3.0" + flutter: ">=3.19.3" + +flutter: + plugin: + implements: media_key_detector + platforms: + macos: + pluginClass: MediaKeyDetectorPlugin + dartPluginClass: MediaKeyDetectorMacOS + +dependencies: + flutter: + sdk: flutter + media_key_detector_platform_interface: + path: ../media_key_detector_platform_interface + +dev_dependencies: + flutter_test: + sdk: flutter + very_good_analysis: ^5.1.0 diff --git a/media_key_detector/media_key_detector_macos/test/media_key_detector_macos_test.dart b/media_key_detector/media_key_detector_macos/test/media_key_detector_macos_test.dart new file mode 100644 index 000000000..e0b746cf1 --- /dev/null +++ b/media_key_detector/media_key_detector_macos/test/media_key_detector_macos_test.dart @@ -0,0 +1,45 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:media_key_detector_macos/media_key_detector_macos.dart'; +import 'package:media_key_detector_platform_interface/media_key_detector_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('MediaKeyDetectorMacOS', () { + const kPlatformName = 'MacOS'; + late MediaKeyDetectorMacOS mediaKeyDetector; + late List log; + + setUp(() async { + mediaKeyDetector = MediaKeyDetectorMacOS(); + + log = []; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(mediaKeyDetector.methodChannel, + (methodCall) async { + log.add(methodCall); + switch (methodCall.method) { + case 'getPlatformName': + return kPlatformName; + default: + return null; + } + }); + }); + + test('can be registered', () { + MediaKeyDetectorMacOS.registerWith(); + expect(MediaKeyDetectorPlatform.instance, isA()); + }); + + test('getPlatformName returns correct name', () async { + final name = await mediaKeyDetector.getPlatformName(); + expect( + log, + [isMethodCall('getPlatformName', arguments: null)], + ); + expect(name, equals(kPlatformName)); + }); + }); +} diff --git a/media_key_detector/media_key_detector_platform_interface/CHANGELOG.md b/media_key_detector/media_key_detector_platform_interface/CHANGELOG.md new file mode 100644 index 000000000..589555b74 --- /dev/null +++ b/media_key_detector/media_key_detector_platform_interface/CHANGELOG.md @@ -0,0 +1,7 @@ +# 0.0.2 + +- TBD + +# 0.0.1 + +- Initial Release diff --git a/media_key_detector/media_key_detector_platform_interface/LICENSE b/media_key_detector/media_key_detector_platform_interface/LICENSE new file mode 100644 index 000000000..a4110b7e0 --- /dev/null +++ b/media_key_detector/media_key_detector_platform_interface/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 Evan Kaiser + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/media_key_detector/media_key_detector_platform_interface/README.md b/media_key_detector/media_key_detector_platform_interface/README.md new file mode 100644 index 000000000..d4ef2ae11 --- /dev/null +++ b/media_key_detector/media_key_detector_platform_interface/README.md @@ -0,0 +1,14 @@ +# media_key_detector_platform_interface + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] + +A common platform interface for the `media_key_detector` plugin. + +This interface allows platform-specific implementations of the `media_key_detector` plugin, as well as the plugin itself, to ensure they are supporting the same interface. + +# Usage + +To implement a new platform-specific implementation of `media_key_detector`, extend `MediaKeyDetectorPlatform` with an implementation that performs the platform-specific behavior. + +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis \ No newline at end of file diff --git a/media_key_detector/media_key_detector_platform_interface/analysis_options.yaml b/media_key_detector/media_key_detector_platform_interface/analysis_options.yaml new file mode 100644 index 000000000..799268d3e --- /dev/null +++ b/media_key_detector/media_key_detector_platform_interface/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.5.1.0.yaml diff --git a/media_key_detector/media_key_detector_platform_interface/lib/media_key_detector_platform_interface.dart b/media_key_detector/media_key_detector_platform_interface/lib/media_key_detector_platform_interface.dart new file mode 100644 index 000000000..30e42793b --- /dev/null +++ b/media_key_detector/media_key_detector_platform_interface/lib/media_key_detector_platform_interface.dart @@ -0,0 +1,89 @@ +/// The base interface for media_key_detector +library media_key_detector_platform_interface; + +import 'package:flutter/services.dart'; +import 'package:media_key_detector_platform_interface/src/media_key.dart'; +import 'package:media_key_detector_platform_interface/src/method_channel_media_key_detector.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +export './src/exports.dart'; + +/// The interface that implementations of media_key_detector must implement. +/// +/// Platform implementations should extend this class +/// rather than implement it as `MediaKeyDetector`. +/// Extending this class (using `extends`) ensures that the subclass will get +/// the default implementation, while platform implementations that `implements` +/// this interface will be broken by newly added [MediaKeyDetectorPlatform] +/// methods. +abstract class MediaKeyDetectorPlatform extends PlatformInterface { + /// Constructs a MediaKeyDetectorPlatform. + MediaKeyDetectorPlatform() : super(token: _token); + + static final Object _token = Object(); + + static MediaKeyDetectorPlatform _instance = MethodChannelMediaKeyDetector(); + + /// The default instance of [MediaKeyDetectorPlatform] to use. + /// + /// Defaults to [MethodChannelMediaKeyDetector]. + static MediaKeyDetectorPlatform get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [MediaKeyDetectorPlatform] when they register + /// themselves. + static set instance(MediaKeyDetectorPlatform instance) { + PlatformInterface.verify(instance, _token); + _instance = instance; + } + + /// Return the current platform name. + Future getPlatformName(); + + /// Get whether the active audio player is currently playing. + Future getIsPlaying(); + + /// Set whether the active audio player is currently playing. + Future setIsPlaying({required bool isPlaying}); + + /// Indicates that the platform should initialize. + void initialize(); + + final List _listeners = []; + + /// Listen for the media key event + void addListener(void Function(MediaKey mediaKey) listener) { + if (!_listeners.contains(listener)) { + _listeners.add(listener); + } + } + + /// Remove the previously registered listener + void removeListener(void Function(MediaKey mediaKey) listener) { + _listeners.remove(listener); + } + + /// Trigger all listeners to indicate that the specified media key was pressed + void triggerListeners(MediaKey mediaKey) { + for (final l in _listeners) { + l(mediaKey); + } + } + + final Map _keyMap = { + LogicalKeyboardKey.mediaPlay: MediaKey.playPause, + LogicalKeyboardKey.mediaRewind: MediaKey.rewind, + LogicalKeyboardKey.mediaFastForward: MediaKey.fastForward, + }; + + /// The default handler to use if this platform doesn't need to implement any + /// platform specific code to listen for the media key event + bool defaultHandler(KeyEvent event) { + if (_keyMap.containsKey(event.logicalKey)) { + final key = _keyMap[event.logicalKey]!; + triggerListeners(key); + return true; + } + return false; + } +} diff --git a/media_key_detector/media_key_detector_platform_interface/lib/src/exports.dart b/media_key_detector/media_key_detector_platform_interface/lib/src/exports.dart new file mode 100644 index 000000000..4ce730ab9 --- /dev/null +++ b/media_key_detector/media_key_detector_platform_interface/lib/src/exports.dart @@ -0,0 +1,3 @@ +export './media_key.dart' show MediaKey; +export './method_channel_media_key_detector.dart' + show MethodChannelMediaKeyDetector; diff --git a/media_key_detector/media_key_detector_platform_interface/lib/src/media_key.dart b/media_key_detector/media_key_detector_platform_interface/lib/src/media_key.dart new file mode 100644 index 000000000..6baa44ec1 --- /dev/null +++ b/media_key_detector/media_key_detector_platform_interface/lib/src/media_key.dart @@ -0,0 +1,11 @@ +/// An enum representing the media key detected +enum MediaKey { + /// The play/pause media button + playPause, + + /// The rewind media button + rewind, + + /// The fast-forward media button + fastForward, +} diff --git a/media_key_detector/media_key_detector_platform_interface/lib/src/method_channel_media_key_detector.dart b/media_key_detector/media_key_detector_platform_interface/lib/src/method_channel_media_key_detector.dart new file mode 100644 index 000000000..d0e4a8cdf --- /dev/null +++ b/media_key_detector/media_key_detector_platform_interface/lib/src/method_channel_media_key_detector.dart @@ -0,0 +1,29 @@ +import 'package:flutter/foundation.dart' show visibleForTesting; +import 'package:flutter/services.dart'; +import 'package:media_key_detector_platform_interface/media_key_detector_platform_interface.dart'; + +/// An implementation of [MediaKeyDetectorPlatform] that uses method channels. +class MethodChannelMediaKeyDetector extends MediaKeyDetectorPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('media_key_detector'); + + @override + Future getPlatformName() { + return methodChannel.invokeMethod('getPlatformName'); + } + + @override + Future getIsPlaying() async { + final isPlaying = await methodChannel.invokeMethod('getIsPlaying'); + return isPlaying ?? false; + } + + @override + Future setIsPlaying({required bool isPlaying}) { + return methodChannel.invokeMethod('setIsPlaying', [isPlaying]); + } + + @override + void initialize() {} +} diff --git a/media_key_detector/media_key_detector_platform_interface/pubspec.yaml b/media_key_detector/media_key_detector_platform_interface/pubspec.yaml new file mode 100644 index 000000000..ae2e48c83 --- /dev/null +++ b/media_key_detector/media_key_detector_platform_interface/pubspec.yaml @@ -0,0 +1,18 @@ +name: media_key_detector_platform_interface +description: A common platform interface for the media_key_detector plugin. +version: 0.0.2 +homepage: https://github.com/holotrek/media_key_detector + +environment: + sdk: "^3.3.0" + flutter: ">=3.19.3" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.1.0 + +dev_dependencies: + flutter_test: + sdk: flutter + very_good_analysis: ^5.1.0 diff --git a/media_key_detector/media_key_detector_platform_interface/test/media_key_detector_platform_interface_test.dart b/media_key_detector/media_key_detector_platform_interface/test/media_key_detector_platform_interface_test.dart new file mode 100644 index 000000000..d7868e308 --- /dev/null +++ b/media_key_detector/media_key_detector_platform_interface/test/media_key_detector_platform_interface_test.dart @@ -0,0 +1,47 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:media_key_detector_platform_interface/media_key_detector_platform_interface.dart'; + +class MediaKeyDetectorMock extends MediaKeyDetectorPlatform { + static const mockPlatformName = 'Mock'; + bool isPlaying = false; + + @override + Future getPlatformName() async => mockPlatformName; + + @override + void addListener(void Function(MediaKey mediaKey) listener) {} + + @override + Future getIsPlaying() async { + return isPlaying; + } + + @override + Future setIsPlaying({required bool isPlaying}) async { + this.isPlaying = isPlaying; + } + + @override + void initialize() {} +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + group('MediaKeyDetectorPlatformInterface', () { + late MediaKeyDetectorPlatform mediaKeyDetectorPlatform; + + setUp(() { + mediaKeyDetectorPlatform = MediaKeyDetectorMock(); + MediaKeyDetectorPlatform.instance = mediaKeyDetectorPlatform; + }); + + group('getPlatformName', () { + test('returns correct name', () async { + expect( + await MediaKeyDetectorPlatform.instance.getPlatformName(), + equals(MediaKeyDetectorMock.mockPlatformName), + ); + }); + }); + }); +} diff --git a/media_key_detector/media_key_detector_platform_interface/test/src/method_channel_media_key_detector_test.dart b/media_key_detector/media_key_detector_platform_interface/test/src/method_channel_media_key_detector_test.dart new file mode 100644 index 000000000..e44b16f61 --- /dev/null +++ b/media_key_detector/media_key_detector_platform_interface/test/src/method_channel_media_key_detector_test.dart @@ -0,0 +1,42 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:media_key_detector_platform_interface/media_key_detector_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + const kPlatformName = 'platformName'; + + group('$MethodChannelMediaKeyDetector', () { + late MethodChannelMediaKeyDetector methodChannelMediaKeyDetector; + final log = []; + + setUp(() async { + methodChannelMediaKeyDetector = MethodChannelMediaKeyDetector(); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + methodChannelMediaKeyDetector.methodChannel, + (methodCall) async { + log.add(methodCall); + switch (methodCall.method) { + case 'getPlatformName': + return kPlatformName; + default: + return null; + } + }, + ); + }); + + tearDown(log.clear); + + test('getPlatformName', () async { + final platformName = + await methodChannelMediaKeyDetector.getPlatformName(); + expect( + log, + [isMethodCall('getPlatformName', arguments: null)], + ); + expect(platformName, equals(kPlatformName)); + }); + }); +} diff --git a/media_key_detector/media_key_detector_windows/.gitignore b/media_key_detector/media_key_detector_windows/.gitignore new file mode 100644 index 000000000..9be145fde --- /dev/null +++ b/media_key_detector/media_key_detector_windows/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/media_key_detector/media_key_detector_windows/.metadata b/media_key_detector/media_key_detector_windows/.metadata new file mode 100644 index 000000000..8c15ad72b --- /dev/null +++ b/media_key_detector/media_key_detector_windows/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b + channel: stable + +project_type: plugin diff --git a/media_key_detector/media_key_detector_windows/CHANGELOG.md b/media_key_detector/media_key_detector_windows/CHANGELOG.md new file mode 100644 index 000000000..589555b74 --- /dev/null +++ b/media_key_detector/media_key_detector_windows/CHANGELOG.md @@ -0,0 +1,7 @@ +# 0.0.2 + +- TBD + +# 0.0.1 + +- Initial Release diff --git a/media_key_detector/media_key_detector_windows/LICENSE b/media_key_detector/media_key_detector_windows/LICENSE new file mode 100644 index 000000000..a4110b7e0 --- /dev/null +++ b/media_key_detector/media_key_detector_windows/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 Evan Kaiser + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/media_key_detector/media_key_detector_windows/README.md b/media_key_detector/media_key_detector_windows/README.md new file mode 100644 index 000000000..f64f63803 --- /dev/null +++ b/media_key_detector/media_key_detector_windows/README.md @@ -0,0 +1,14 @@ +# media_key_detector_windows + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] + +The windows implementation of `media_key_detector`. + +## Usage + +This package is [endorsed][endorsed_link], which means you can simply use `media_key_detector` +normally. This package will be automatically included in your app when you do. + +[endorsed_link]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis diff --git a/media_key_detector/media_key_detector_windows/analysis_options.yaml b/media_key_detector/media_key_detector_windows/analysis_options.yaml new file mode 100644 index 000000000..799268d3e --- /dev/null +++ b/media_key_detector/media_key_detector_windows/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.5.1.0.yaml diff --git a/media_key_detector/media_key_detector_windows/lib/media_key_detector_windows.dart b/media_key_detector/media_key_detector_windows/lib/media_key_detector_windows.dart new file mode 100644 index 000000000..6d9b07d64 --- /dev/null +++ b/media_key_detector/media_key_detector_windows/lib/media_key_detector_windows.dart @@ -0,0 +1,37 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:media_key_detector_platform_interface/media_key_detector_platform_interface.dart'; + +/// The Windows implementation of [MediaKeyDetectorPlatform]. +class MediaKeyDetectorWindows extends MediaKeyDetectorPlatform { + bool _isPlaying = false; + + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('media_key_detector_windows'); + + /// Registers this class as the default instance of [MediaKeyDetectorPlatform] + static void registerWith() { + MediaKeyDetectorPlatform.instance = MediaKeyDetectorWindows(); + } + + @override + void initialize() { + ServicesBinding.instance.keyboard.addHandler(defaultHandler); + } + + @override + Future getPlatformName() { + return methodChannel.invokeMethod('getPlatformName'); + } + + @override + Future getIsPlaying() async { + return _isPlaying; + } + + @override + Future setIsPlaying({required bool isPlaying}) async { + _isPlaying = isPlaying; + } +} diff --git a/media_key_detector/media_key_detector_windows/pubspec.yaml b/media_key_detector/media_key_detector_windows/pubspec.yaml new file mode 100644 index 000000000..1627c5ba1 --- /dev/null +++ b/media_key_detector/media_key_detector_windows/pubspec.yaml @@ -0,0 +1,27 @@ +name: media_key_detector_windows +description: Windows implementation of the media_key_detector plugin +version: 0.0.2 +homepage: https://github.com/holotrek/media_key_detector + +environment: + sdk: "^3.3.0" + flutter: ">=3.19.3" + +flutter: + plugin: + implements: media_key_detector + platforms: + windows: + pluginClass: MediaKeyDetectorWindows + dartPluginClass: MediaKeyDetectorWindows + +dependencies: + flutter: + sdk: flutter + media_key_detector_platform_interface: + path: ../media_key_detector_platform_interface + +dev_dependencies: + flutter_test: + sdk: flutter + very_good_analysis: ^5.1.0 diff --git a/media_key_detector/media_key_detector_windows/test/media_key_detector_windows_test.dart b/media_key_detector/media_key_detector_windows/test/media_key_detector_windows_test.dart new file mode 100644 index 000000000..e77443649 --- /dev/null +++ b/media_key_detector/media_key_detector_windows/test/media_key_detector_windows_test.dart @@ -0,0 +1,45 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:media_key_detector_platform_interface/media_key_detector_platform_interface.dart'; +import 'package:media_key_detector_windows/media_key_detector_windows.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('MediaKeyDetectorWindows', () { + const kPlatformName = 'Windows'; + late MediaKeyDetectorWindows mediaKeyDetector; + late List log; + + setUp(() async { + mediaKeyDetector = MediaKeyDetectorWindows(); + + log = []; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(mediaKeyDetector.methodChannel, + (methodCall) async { + log.add(methodCall); + switch (methodCall.method) { + case 'getPlatformName': + return kPlatformName; + default: + return null; + } + }); + }); + + test('can be registered', () { + MediaKeyDetectorWindows.registerWith(); + expect(MediaKeyDetectorPlatform.instance, isA()); + }); + + test('getPlatformName returns correct name', () async { + final name = await mediaKeyDetector.getPlatformName(); + expect( + log, + [isMethodCall('getPlatformName', arguments: null)], + ); + expect(name, equals(kPlatformName)); + }); + }); +} diff --git a/media_key_detector/media_key_detector_windows/windows/.gitignore b/media_key_detector/media_key_detector_windows/windows/.gitignore new file mode 100644 index 000000000..b3eb2be16 --- /dev/null +++ b/media_key_detector/media_key_detector_windows/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/media_key_detector/media_key_detector_windows/windows/CMakeLists.txt b/media_key_detector/media_key_detector_windows/windows/CMakeLists.txt new file mode 100644 index 000000000..28ec896f2 --- /dev/null +++ b/media_key_detector/media_key_detector_windows/windows/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.14) +set(PROJECT_NAME "media_key_detector_windows") +project(${PROJECT_NAME} LANGUAGES CXX) + +set(PLUGIN_NAME "${PROJECT_NAME}_plugin") + +add_library(${PLUGIN_NAME} SHARED + "media_key_detector_windows_plugin.cpp" + "include/media_key_detector_windows/media_key_detector_windows.h" +) +apply_standard_settings(${PLUGIN_NAME}) +set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) + +# List of absolute paths to libraries that should be bundled with the plugin +set(media_key_detector_bundled_libraries + "" + PARENT_SCOPE +) \ No newline at end of file diff --git a/media_key_detector/media_key_detector_windows/windows/include/media_key_detector_windows/media_key_detector_windows.h b/media_key_detector/media_key_detector_windows/windows/include/media_key_detector_windows/media_key_detector_windows.h new file mode 100644 index 000000000..3ce547bf8 --- /dev/null +++ b/media_key_detector/media_key_detector_windows/windows/include/media_key_detector_windows/media_key_detector_windows.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_MEDIA_KEY_DETECTOR_WINDOWS_PLUGIN_H_ +#define FLUTTER_PLUGIN_MEDIA_KEY_DETECTOR_WINDOWS_PLUGIN_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void MediaKeyDetectorWindowsRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_MEDIA_KEY_DETECTOR_WINDOWS_PLUGIN_H_ diff --git a/media_key_detector/media_key_detector_windows/windows/media_key_detector_windows_plugin.cpp b/media_key_detector/media_key_detector_windows/windows/media_key_detector_windows_plugin.cpp new file mode 100644 index 000000000..f3587ac99 --- /dev/null +++ b/media_key_detector/media_key_detector_windows/windows/media_key_detector_windows_plugin.cpp @@ -0,0 +1,72 @@ +#include "include/media_key_detector_windows/media_key_detector_windows.h" + +// This must be included before many other Windows headers. +#include + +#include +#include +#include + +#include +#include + +namespace { + +using flutter::EncodableValue; + +class MediaKeyDetectorWindows : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); + + MediaKeyDetectorWindows(); + + virtual ~MediaKeyDetectorWindows(); + + private: + // Called when a method is called on this plugin's channel from Dart. + void HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr> result); +}; + +// static +void MediaKeyDetectorWindows::RegisterWithRegistrar( + flutter::PluginRegistrarWindows *registrar) { + auto channel = + std::make_unique>( + registrar->messenger(), "media_key_detector_windows", + &flutter::StandardMethodCodec::GetInstance()); + + auto plugin = std::make_unique(); + + channel->SetMethodCallHandler( + [plugin_pointer = plugin.get()](const auto &call, auto result) { + plugin_pointer->HandleMethodCall(call, std::move(result)); + }); + + registrar->AddPlugin(std::move(plugin)); +} + +MediaKeyDetectorWindows::MediaKeyDetectorWindows() {} + +MediaKeyDetectorWindows::~MediaKeyDetectorWindows() {} + +void MediaKeyDetectorWindows::HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr> result) { + if (method_call.method_name().compare("getPlatformName") == 0) { + result->Success(EncodableValue("Windows")); + } + else { + result->NotImplemented(); + } +} + +} // namespace + +void MediaKeyDetectorWindowsRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + MediaKeyDetectorWindows::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/playstoreassets/mac_screenshot_1.png b/playstoreassets/mac_screenshot_1.png new file mode 100644 index 000000000..5b8f209f6 Binary files /dev/null and b/playstoreassets/mac_screenshot_1.png differ diff --git a/playstoreassets/mac_screenshot_2.png b/playstoreassets/mac_screenshot_2.png new file mode 100644 index 000000000..e6595cb7e Binary files /dev/null and b/playstoreassets/mac_screenshot_2.png differ diff --git a/playstoreassets/mob1.png b/playstoreassets/mob1.png new file mode 100644 index 000000000..0792cac08 Binary files /dev/null and b/playstoreassets/mob1.png differ diff --git a/playstoreassets/mob2.png b/playstoreassets/mob2.png new file mode 100644 index 000000000..39c45b203 Binary files /dev/null and b/playstoreassets/mob2.png differ diff --git a/playstoreassets/tab1.png b/playstoreassets/tab1.png new file mode 100644 index 000000000..4d9a0cdc0 Binary files /dev/null and b/playstoreassets/tab1.png differ diff --git a/playstoreassets/tab2.png b/playstoreassets/tab2.png new file mode 100644 index 000000000..b8602ba09 Binary files /dev/null and b/playstoreassets/tab2.png differ diff --git a/pubspec.lock b/pubspec.lock old mode 100644 new mode 100755 index 15761c086..b47779490 --- a/pubspec.lock +++ b/pubspec.lock @@ -12,10 +12,10 @@ packages: dependency: transitive description: name: archive - sha256: "7dcbd0f87fe5f61cb28da39a1a8b70dbc106e2fe0516f7836eb7bb2948481a12" + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" url: "https://pub.dev" source: hosted - version: "4.0.5" + version: "4.0.7" args: dependency: transitive description: @@ -28,10 +28,58 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" + bluetooth_low_energy: + dependency: "direct main" + description: + name: bluetooth_low_energy + sha256: "5dec5831412c7d82b77df878dd3e08a82132426d2fb4c5d7c98c9a8cd0ed79e0" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + bluetooth_low_energy_android: + dependency: transitive + description: + name: bluetooth_low_energy_android + sha256: "32c0f84f88770845e3189e04b0ddf4780dc8743fd7a8ade60b99b6cb414b8a89" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + bluetooth_low_energy_darwin: + dependency: transitive + description: + name: bluetooth_low_energy_darwin + sha256: fbbe3be175cb54093884a84f6f0826d6e8a2a2e29dfeae9b367d5e8e9ee1db38 + url: "https://pub.dev" + source: hosted + version: "6.1.0" + bluetooth_low_energy_linux: + dependency: transitive + description: + name: bluetooth_low_energy_linux + sha256: a5c740f445dc8d2e940767fa94ed3bb24c32e77bc962a67ab23cb1f218180705 + url: "https://pub.dev" + source: hosted + version: "6.1.0" + bluetooth_low_energy_platform_interface: + dependency: transitive + description: + name: bluetooth_low_energy_platform_interface + sha256: dd76c0f8e31dcfb984059b03e73cb2998c29cffd17425f4ce946365b63abb3dc + url: "https://pub.dev" + source: hosted + version: "6.1.0" + bluetooth_low_energy_windows: + dependency: transitive + description: + name: bluetooth_low_energy_windows + sha256: "7a651259f7bc3ae2bb042c21e15e1e4f88acea57da1f69b3165f239124724791" + url: "https://pub.dev" + source: hosted + version: "6.1.0" bluez: dependency: transitive description: @@ -56,14 +104,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff - url: "https://pub.dev" - source: hosted - version: "2.0.3" cli_util: dependency: transitive description: @@ -88,14 +128,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" - convert: + console: + dependency: transitive + description: + name: console + sha256: e04e7824384c5b39389acdd6dc7d33f3efe6b232f6f16d7626f194f6a01ad69a + url: "https://pub.dev" + source: hosted + version: "4.1.0" + cross_file: dependency: transitive description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "0.3.4+2" crypto: dependency: transitive description: @@ -120,14 +168,39 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" + device_auto_rotate_checker: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "2cd4bb52733f664875cec2780a8091d4698226cb" + url: "https://github.com/mpcreza/device_auto_rotate_checker.git" + source: git + version: "0.0.2" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "49413c8ca514dea7633e8def233b25efdf83ec8522955cc2c0e3ad802927e7c6" + url: "https://pub.dev" + source: hosted + version: "12.1.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + url: "https://pub.dev" + source: hosted + version: "7.0.3" fake_async: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -136,115 +209,96 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - fixnum: + file: dependency: transitive description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "1.1.1" - flex_color_scheme: - dependency: "direct main" + version: "7.0.1" + file_selector_linux: + dependency: transitive description: - name: flex_color_scheme - sha256: "3344f8f6536c6ce0473b98e9f084ef80ca89024ad3b454f9c32cf840206f4387" + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" url: "https://pub.dev" source: hosted - version: "8.2.0" - flex_seed_scheme: + version: "0.9.3+2" + file_selector_macos: dependency: transitive description: - name: flex_seed_scheme - sha256: b06d8b367b84cbf7ca5c5603c858fa5edae88486c4e4da79ac1044d73b6c62ec + name: file_selector_macos + sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c" url: "https://pub.dev" source: hosted - version: "3.5.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_blue_plus: - dependency: "direct main" + version: "0.9.4+4" + file_selector_platform_interface: + dependency: transitive description: - name: flutter_blue_plus - sha256: "2d926dbef0fd6c58d4be8fca9eaaf1ba747c0ccb8373ddd5386665317e26eb61" + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b url: "https://pub.dev" source: hosted - version: "1.35.3" - flutter_blue_plus_android: + version: "2.6.2" + file_selector_windows: dependency: transitive description: - name: flutter_blue_plus_android - sha256: c1d83f84b514e46345a8a58599c428f20b11e78379521e0d3b0611c7b7cbf2c1 + name: file_selector_windows + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" url: "https://pub.dev" source: hosted - version: "3.0.0" - flutter_blue_plus_darwin: + version: "0.9.3+4" + fixnum: dependency: transitive description: - name: flutter_blue_plus_darwin - sha256: "8d0a0f11f83b13dda173396b7e4028b4e8656bc8dbbc82c26a7e49aafc62644b" + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "3.0.0" - flutter_blue_plus_linux: - dependency: transitive + version: "1.1.1" + flex_color_scheme: + dependency: "direct main" description: - name: flutter_blue_plus_linux - sha256: "1d367ed378b2bd6c3b9685fda7044e1d2f169884802b7dec7badb31a99a72660" + name: flex_color_scheme + sha256: "034d5720747e6af39b2ad090d82dd92d33fde68e7964f1814b714c9d49ddbd64" url: "https://pub.dev" source: hosted - version: "3.0.0" - flutter_blue_plus_platform_interface: + version: "8.3.0" + flex_seed_scheme: dependency: transitive description: - name: flutter_blue_plus_platform_interface - sha256: "114f8e85a03a28a48d707a4df6cc9218e1f2005cf260c5e815e5585a00da5778" + name: flex_seed_scheme + sha256: b06d8b367b84cbf7ca5c5603c858fa5edae88486c4e4da79ac1044d73b6c62ec url: "https://pub.dev" source: hosted - version: "3.0.0" - flutter_blue_plus_web: - dependency: "direct overridden" - description: - path: "packages/flutter_blue_plus_web" - ref: HEAD - resolved-ref: "5fc128ed6ff6b1bc038ec9f91a6d9789c2cebc53" - url: "https://github.com/chipweinberger/flutter_blue_plus.git" - source: git - version: "3.0.0" - flutter_blue_plus_windows: + version: "3.5.1" + flutter: dependency: "direct main" - description: - path: flutter_blue_plus_windows - relative: true - source: path - version: "1.26.1" - flutter_launcher_icons: - dependency: "direct dev" - description: - name: flutter_launcher_icons - sha256: bfa04787c85d80ecb3f8777bde5fc10c3de809240c48fa061a2c2bf15ea5211c - url: "https://pub.dev" - source: hosted - version: "0.14.3" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" flutter_local_notifications: dependency: "direct main" description: name: flutter_local_notifications - sha256: d59eeafd6df92174b1d5f68fc9d66634c97ce2e7cfe2293476236547bb19bbbd + sha256: "7ed76be64e8a7d01dfdf250b8434618e2a028c9dfa2a3c41dc9b531d4b3fc8a5" url: "https://pub.dev" source: hosted - version: "19.0.0" + version: "19.4.2" flutter_local_notifications_linux: dependency: transitive description: @@ -257,36 +311,130 @@ packages: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "2569b973fc9d1f63a37410a9f7c1c552081226c597190cb359ef5d5762d1631c" + sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe" url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "9.1.0" flutter_local_notifications_windows: dependency: transitive description: name: flutter_local_notifications_windows - sha256: f8fc0652a601f83419d623c85723a3e82ad81f92b33eaa9bcc21ea1b94773e6e + sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.3" + flutter_md: + dependency: "direct main" + description: + name: flutter_md + sha256: b5a67ae49135f7a76a0cc6f938ee3e8754e71d8448b97cf99c11512877f1d055 + url: "https://pub.dev" + source: hosted + version: "0.0.7" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: b0694b7fb1689b0e6cc193b3f1fcac6423c4f93c74fb20b806c6b6f196db0c31 + url: "https://pub.dev" + source: hosted + version: "2.0.30" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_bluetooth: + dependency: transitive + description: + name: flutter_web_bluetooth + sha256: ad26a1b3fef95b86ea5f63793b9a0cdc1a33490f35d754e4e711046cae3ebbf8 + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter_web_plugins: dependency: transitive description: flutter source: sdk version: "0.0.0" - http: + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + gamepads: + dependency: "direct main" + description: + name: gamepads + sha256: "5f8f1f31e241bce1c77e7bc5dcf5c03d9ea51a04ed775c8c299e788c512e0b54" + url: "https://pub.dev" + source: hosted + version: "0.1.8+2" + gamepads_android: + dependency: transitive + description: + name: gamepads_android + sha256: "7913cd171ff06d5b588cb3e1dae64390c6e1352dd2999ff19a96d822eb7441fd" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + gamepads_darwin: + dependency: transitive + description: + name: gamepads_darwin + sha256: "91250975ee196703816c55117502ec53ce4a1b881ca50dec1d0fbcb9fbb75eff" + url: "https://pub.dev" + source: hosted + version: "0.1.2+2" + gamepads_ios: + dependency: transitive + description: + name: gamepads_ios + sha256: "085459e2f677c18c4b15aee5dacc66e0b05491d4ed32bd3041c8328394e00d3a" + url: "https://pub.dev" + source: hosted + version: "0.1.3+1" + gamepads_linux: + dependency: transitive + description: + name: gamepads_linux + sha256: f4c17915a84400d7f624aadb6371424ada596eedff2a25663121453e65917e0d + url: "https://pub.dev" + source: hosted + version: "0.1.1+3" + gamepads_platform_interface: dependency: transitive + description: + name: gamepads_platform_interface + sha256: ddab8677a4137d92e381b04cc97a8081ae4b75673dc0f24c846618d2b5226c4f + url: "https://pub.dev" + source: hosted + version: "0.1.2+1" + gamepads_windows: + dependency: "direct overridden" + description: + path: "packages/gamepads_windows" + ref: windows-api-rework + resolved-ref: a4b8ded91b14d799ef0998a738b1f2031fef0bd2 + url: "https://github.com/lea108/gamepads.git" + source: git + version: "0.1.4+1" + get_it: + dependency: transitive + description: + name: get_it + sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b + url: "https://pub.dev" + source: hosted + version: "8.2.0" + http: + dependency: "direct main" description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" http_parser: dependency: transitive description: @@ -295,6 +443,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + hybrid_logging: + dependency: transitive + description: + name: hybrid_logging + sha256: "54248d52ce68c14702a42fbc4083bac5c6be30f6afad8a41be4bbadd197b8af5" + url: "https://pub.dev" + source: hosted + version: "1.0.0" image: dependency: transitive description: @@ -303,6 +459,91 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.4" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "8dfe08ea7fcf7467dbaf6889e72eebd5e0d6711caae201fdac780eb45232cd02" + url: "https://pub.dev" + source: hosted + version: "0.8.13+3" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e + url: "https://pub.dev" + source: hosted + version: "0.8.13" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04 + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" + in_app_update: + dependency: "direct main" + description: + name: in_app_update + sha256: "9924a3efe592e1c0ec89dda3683b3cfec3d4cd02d908e6de00c24b759038ddb1" + url: "https://pub.dev" + source: hosted + version: "4.2.5" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" json_annotation: dependency: transitive description: @@ -314,67 +555,71 @@ packages: keypress_simulator: dependency: "direct main" description: - name: keypress_simulator - sha256: d5aa5ed472b6b396f41fd6dcee99f4afb2c7ac6202af622b4cec7955de6ed7f6 - url: "https://pub.dev" - source: hosted + path: "keypress_simulator/packages/keypress_simulator" + relative: true + source: path version: "0.2.0" keypress_simulator_macos: dependency: transitive description: - name: keypress_simulator_macos - sha256: babb698b1331cff0301de839c7bc6b051d84f98ddb137cf9a6dda6c6caeb78ac - url: "https://pub.dev" - source: hosted + path: "keypress_simulator/packages/keypress_simulator_macos" + relative: true + source: path version: "0.2.0" keypress_simulator_platform_interface: dependency: transitive description: - name: keypress_simulator_platform_interface - sha256: "38c35fee6b107ff10cfb6bdb61e32eb0db17545ed64399a357794431212ca4b4" - url: "https://pub.dev" - source: hosted + path: "keypress_simulator/packages/keypress_simulator_platform_interface" + relative: true + source: path version: "0.2.0" keypress_simulator_windows: dependency: transitive description: - name: keypress_simulator_windows - sha256: b4ff055131a2e5ea920eb3b6a185e1889fe00749b027df3b83aa726ed590a9b5 - url: "https://pub.dev" - source: hosted + path: "keypress_simulator/packages/keypress_simulator_windows" + relative: true + source: path version: "0.2.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "6.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: @@ -391,6 +636,48 @@ packages: url: "https://pub.dev" source: hosted version: "0.11.1" + media_key_detector: + dependency: "direct main" + description: + path: "media_key_detector/media_key_detector" + relative: true + source: path + version: "0.0.2" + media_key_detector_ios: + dependency: transitive + description: + path: "media_key_detector/media_key_detector_ios" + relative: true + source: path + version: "0.0.2" + media_key_detector_linux: + dependency: transitive + description: + path: "media_key_detector/media_key_detector_linux" + relative: true + source: path + version: "0.0.2" + media_key_detector_macos: + dependency: transitive + description: + path: "media_key_detector/media_key_detector_macos" + relative: true + source: path + version: "0.0.2" + media_key_detector_platform_interface: + dependency: transitive + description: + path: "media_key_detector/media_key_detector_platform_interface" + relative: true + source: path + version: "0.0.2" + media_key_detector_windows: + dependency: transitive + description: + path: "media_key_detector/media_key_detector_windows" + relative: true + source: path + version: "0.0.2" meta: dependency: transitive description: @@ -399,6 +686,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + msix: + dependency: "direct dev" + description: + name: msix + sha256: f88033fcb9e0dd8de5b18897cbebbd28ea30596810f4a7c86b12b0c03ace87e5 + url: "https://pub.dev" + source: hosted + version: "3.16.12" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d + url: "https://pub.dev" + source: hosted + version: "9.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" path: dependency: transitive description: @@ -408,7 +735,7 @@ packages: source: hosted version: "1.9.1" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" @@ -419,18 +746,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" + sha256: e122c5ea805bb6773bb12ce667611265980940145be920cd09a4b0ec0285cb16 url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.2.20" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + sha256: efaec349ddfc181528345c56f8eda9d6cccd71c177511b132c6a0ddaefaa2738 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.3" path_provider_linux: dependency: transitive description: @@ -455,14 +782,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.dev" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" petitparser: dependency: transitive description: name: petitparser - sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "7.0.1" platform: dependency: transitive description: @@ -479,38 +854,151 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pointycastle: - dependency: "direct main" + posix: + dependency: transitive description: - name: pointycastle - sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" url: "https://pub.dev" source: hosted - version: "4.0.0" - posix: + version: "6.0.3" + process: dependency: transitive description: - name: posix - sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a + name: process + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 url: "https://pub.dev" source: hosted - version: "6.0.1" + version: "5.0.5" protobuf: dependency: "direct main" description: name: protobuf - sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + sha256: de9c9eb2c33f8e933a42932fe1dc504800ca45ebc3d673e6ed7f39754ee4053e url: "https://pub.dev" source: hosted - version: "3.1.0" - rxdart: + version: "4.2.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + restart_app: + dependency: "direct main" + description: + path: "." + ref: "feature/resolve-all-open-issues" + resolved-ref: de208e0d393812f1a6edb0a20cef7fb49cec957c + url: "https://github.com/maple135790/restart_app.git" + source: git + version: "1.4.0" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_linux: + dependency: transitive + description: + name: screen_retriever_linux + sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_macos: + dependency: transitive + description: + name: screen_retriever_macos + sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_platform_interface: dependency: transitive description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + name: screen_retriever_platform_interface + sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 url: "https://pub.dev" source: hosted - version: "0.28.0" + version: "0.2.0" + screen_retriever_windows: + dependency: transitive + description: + name: screen_retriever_windows + sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "0b0f98d535319cb5cdd4f65783c2a54ee6d417a2f093dbb18be3e36e4c3d181f" + url: "https://pub.dev" + source: hosted + version: "2.4.14" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shorebird_code_push: + dependency: "direct main" + description: + name: shorebird_code_push + sha256: "82203f39a66c78548da944dbe4079c2aa2a60fa5bc1105ed707b144c94f04349" + url: "https://pub.dev" + source: hosted + version: "2.0.5" sky_engine: dependency: transitive description: flutter @@ -540,14 +1028,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - stream_with_value: - dependency: transitive - description: - name: stream_with_value - sha256: "483d79bf604fdea5274e31207956b2f624f5f03a506cacf081b65cdfcfa647a6" - url: "https://pub.dev" - source: hosted - version: "0.5.0" string_scanner: dependency: transitive description: @@ -556,6 +1036,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" term_glyph: dependency: transitive description: @@ -568,10 +1056,18 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.6" + test_screenshot: + dependency: "direct dev" + description: + name: test_screenshot + sha256: "2a7620f404cf514601b5181a154c7af7495015e51c52e0175c397ac579371b4a" + url: "https://pub.dev" + source: hosted + version: "0.0.8" time: dependency: transitive description: @@ -584,10 +1080,10 @@ packages: dependency: transitive description: name: timezone - sha256: ffc9d5f4d1193534ef051f9254063fa53d588609418c84299956c3db9383587d + sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 url: "https://pub.dev" source: hosted - version: "0.10.0" + version: "0.10.1" typed_data: dependency: transitive description: @@ -604,22 +1100,119 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.3" + universal_ble: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "22b713600511ef5adff60aa70b941ada99ba2890" + url: "https://github.com/jonasbark/universal_ble.git" + source: git + version: "0.21.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: c0fb544b9ac7efa10254efaf00a951615c362d1ea1877472f8f6c0fa00fcf15b + url: "https://pub.dev" + source: hosted + version: "6.3.23" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 + url: "https://pub.dev" + source: hosted + version: "6.3.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f + url: "https://pub.dev" + source: hosted + version: "3.2.3" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" + version: + dependency: "direct main" + description: + name: version + sha256: "3d4140128e6ea10d83da32fef2fa4003fccbf6852217bb854845802f04191f94" + url: "https://pub.dev" + source: hosted + version: "3.0.2" vm_service: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + wakelock_plus: + dependency: "direct main" + description: + name: wakelock_plus + sha256: "9296d40c9adbedaba95d1e704f4e0b434be446e2792948d0e4aa977048104228" url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "1.4.0" + wakelock_plus_platform_interface: + dependency: transitive + description: + name: wakelock_plus_platform_interface + sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2" + url: "https://pub.dev" + source: hosted + version: "1.3.0" web: dependency: transitive description: @@ -628,14 +1221,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - win_ble: + webdriver: dependency: transitive description: - name: win_ble - sha256: "2a867e13c4b355b101fc2c6e2ac85eeebf965db34eca46856f8b478e93b41e96" + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "3.1.0" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + window_manager: + dependency: "direct main" + description: + name: window_manager + sha256: "7eb6d6c4164ec08e1bf978d6e733f3cebe792e2a23fb07cbca25c2872bfdbdcd" + url: "https://pub.dev" + source: hosted + version: "0.5.1" xdg_directories: dependency: transitive description: @@ -648,10 +1265,10 @@ packages: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.6.1" yaml: dependency: transitive description: @@ -661,5 +1278,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.7.0 <4.0.0" - flutter: ">=3.29.0" + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml old mode 100644 new mode 100755 index 30923625f..d2837dce0 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,39 +1,94 @@ name: swift_control -description: "SwiftControl - Control your virtual riding" +description: "BikeControl - Control your virtual riding" publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.0.0+1 +version: 3.6.1+45 environment: - sdk: ^3.7.0 + sdk: ^3.9.0 dependencies: flutter: sdk: flutter - flutter_local_notifications: ^19.0.0 - flutter_blue_plus: ^1.35.3 - protobuf: ^3.1.0 + url_launcher: ^6.3.1 + flutter_local_notifications: ^19.4.1 + + # fork to allow iOS background BLE connections + universal_ble: + git: + url: https://github.com/jonasbark/universal_ble.git + gamepads: ^0.1.8+2 + + path_provider: ^2.1.5 + intl: any + version: ^3.0.0 + bluetooth_low_energy: ^6.1.0 + wakelock_plus: ^1.4.0 + protobuf: ^4.2.0 + flutter_md: ^0.0.7 + permission_handler: ^12.0.1 dartx: any - pointycastle: any - keypress_simulator: ^0.2.0 - flex_color_scheme: ^8.2.0 - flutter_blue_plus_windows: - path: flutter_blue_plus_windows + image_picker: ^1.1.2 + window_manager: ^0.5.1 + device_info_plus: ^12.1.0 + keypress_simulator: + path: keypress_simulator/packages/keypress_simulator + shared_preferences: ^2.5.3 + flex_color_scheme: ^8.3.0 + media_key_detector: + path: media_key_detector/media_key_detector accessibility: path: accessibility -dependency_overrides: - flutter_blue_plus_web: + device_auto_rotate_checker: + git: + url: https://github.com/mpcreza/device_auto_rotate_checker.git + + # app update related + shorebird_code_push: ^2.0.5 + restart_app: git: - url: https://github.com/chipweinberger/flutter_blue_plus.git - path: packages/flutter_blue_plus_web + url: https://github.com/maple135790/restart_app.git + ref: feature/resolve-all-open-issues + package_info_plus: ^9.0.0 + in_app_update: ^4.2.5 + http: ^1.3.0 dev_dependencies: flutter_test: sdk: flutter + integration_test: + sdk: flutter - flutter_launcher_icons: "^0.14.3" - flutter_lints: ^5.0.0 + test_screenshot: 0.0.8 + flutter_lints: ^6.0.0 + msix: ^3.16.12 + +dependency_overrides: + gamepads_windows: + git: + url: https://github.com/lea108/gamepads.git + ref: windows-api-rework + path: packages/gamepads_windows flutter: uses-material-design: true + assets: + - CHANGELOG.md + - TROUBLESHOOTING.md + - INSTRUCTIONS_ANDROID.md + - INSTRUCTIONS_IOS.md + - INSTRUCTIONS_WINDOWS.md + - INSTRUCTIONS_MACOS.md + - shorebird.yaml + - icon.png + +msix_config: + display_name: BikeControl + publisher_display_name: Jonas Bark + identity_name: JonasBark.SwiftControl + publisher: CN=EDA6D86E-3E52-4054-9F3A-4277AFCCB2C4 + store: true + logo_path: logo.png + build_windows: false + capabilities: internetClient,bluetooth,inputInjectionBrokered diff --git a/scripts/RELEASE_NOTES.md b/scripts/RELEASE_NOTES.md new file mode 100644 index 000000000..c3e62140e --- /dev/null +++ b/scripts/RELEASE_NOTES.md @@ -0,0 +1,9 @@ +I recommend downloading from the official stores: + +GetItOnGooglePlay_Badge_Web_color_English + +App Store + +Mac App Store + +Microsoft Store diff --git a/scripts/generate_release_body.sh b/scripts/generate_release_body.sh new file mode 100755 index 000000000..7fa53c1ce --- /dev/null +++ b/scripts/generate_release_body.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Script to generate GitHub release body with changelog and store links +# Usage: ./scripts/generate_release_body.sh + +# Get the directory where this script is located +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +RELEASE_NOTES_FILE="$SCRIPT_DIR/RELEASE_NOTES.md" + +if [ ! -f "$RELEASE_NOTES_FILE" ]; then + echo "Error: RELEASE_NOTES.md not found at $RELEASE_NOTES_FILE" + exit 1 +fi + +# Extract the first changelog entry using get_latest_changelog.sh +# For GitHub releases, we want to include the version header +VERSION_HEADER=$(awk '/^### / {print; exit}' "$SCRIPT_DIR/../CHANGELOG.md") +LATEST_CHANGELOG=$("$SCRIPT_DIR/get_latest_changelog.sh") + +# Combine changelog with release notes +echo "$VERSION_HEADER" +echo "$LATEST_CHANGELOG" +echo "" +echo "---" +echo "" +cat "$RELEASE_NOTES_FILE" diff --git a/scripts/get_latest_changelog.sh b/scripts/get_latest_changelog.sh new file mode 100755 index 000000000..cf8d08a1c --- /dev/null +++ b/scripts/get_latest_changelog.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# Script to extract the latest changelog entry for Play Store uploads +# Usage: ./scripts/get_latest_changelog.sh + +# Get the directory where this script is located +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +CHANGELOG_FILE="$PROJECT_ROOT/CHANGELOG.md" + +if [ ! -f "$CHANGELOG_FILE" ]; then + echo "Error: CHANGELOG.md not found at $CHANGELOG_FILE" + exit 1 +fi + +# Extract the first changelog entry (between first ### and second ###) +awk '/^### / {if (count++) exit} count' "$CHANGELOG_FILE" | tail -n +2 | sed 's/^- /• /' diff --git a/shorebird.yaml b/shorebird.yaml new file mode 100644 index 000000000..ad7de7a51 --- /dev/null +++ b/shorebird.yaml @@ -0,0 +1,2 @@ +app_id: 21d821f0-6795-4fca-aeb1-5a5dbe2d2b62 +auto_update: false diff --git a/test/accessibility_disclosure_test.dart b/test/accessibility_disclosure_test.dart new file mode 100644 index 000000000..98083091a --- /dev/null +++ b/test/accessibility_disclosure_test.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:swift_control/widgets/accessibility_disclosure_dialog.dart'; + +void main() { + group('AccessibilityDisclosureDialog', () { + testWidgets('shows proper consent options with two buttons', (WidgetTester tester) async { + bool acceptCalled = false; + bool denyCalled = false; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: AccessibilityDisclosureDialog( + onAccept: () => acceptCalled = true, + onDeny: () => denyCalled = true, + ), + ), + ), + ); + + // Verify dialog shows proper title + expect(find.text('Accessibility Service Permission Required'), findsOneWidget); + + // Verify both consent options are present + expect(find.text('Allow'), findsOneWidget); + expect(find.text('Deny'), findsOneWidget); + + // Verify explanation text is present + expect(find.textContaining('AccessibilityService API'), findsOneWidget); + expect(find.textContaining('simulate touch gestures'), findsOneWidget); + expect(find.textContaining('No personal data'), findsOneWidget); + + // Test deny button + await tester.tap(find.text('Deny')); + await tester.pump(); + expect(denyCalled, isTrue); + + // Reset and test accept button + denyCalled = false; + await tester.tap(find.text('Allow')); + await tester.pump(); + expect(acceptCalled, isTrue); + }); + + testWidgets('includes required disclosure information', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: AccessibilityDisclosureDialog( + onAccept: () {}, + onDeny: () {}, + ), + ), + ), + ); + + // Check for key disclosure elements required by Play Store + expect(find.textContaining('Why is this permission needed?'), findsOneWidget); + expect(find.textContaining('How does SwiftControl use this permission?'), findsOneWidget); + expect(find.textContaining('Zwift Click, Zwift Ride, or Zwift Play'), findsOneWidget); + expect(find.textContaining('training app window is active'), findsOneWidget); + expect(find.textContaining('You must choose to either Allow or Deny'), findsOneWidget); + }); + + testWidgets('prevents dismissal via back navigation', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: AccessibilityDisclosureDialog( + onAccept: () {}, + onDeny: () {}, + ), + ), + ), + ); + + // Verify PopScope is present to prevent back navigation + expect(find.byType(PopScope), findsOneWidget); + + // Get the PopScope widget and verify canPop is false + final popScope = tester.widget(find.byType(PopScope)); + expect(popScope.canPop, isFalse); + }); + }); +} \ No newline at end of file diff --git a/test/bluetooth_device_detection.dart b/test/bluetooth_device_detection.dart new file mode 100644 index 000000000..ba7662dc8 --- /dev/null +++ b/test/bluetooth_device_detection.dart @@ -0,0 +1,124 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:swift_control/bluetooth/devices/bluetooth_device.dart'; +import 'package:swift_control/bluetooth/devices/cycplus/cycplus_bc2.dart'; +import 'package:swift_control/bluetooth/devices/elite/elite_square.dart'; +import 'package:swift_control/bluetooth/devices/elite/elite_sterzo.dart'; +import 'package:swift_control/bluetooth/devices/shimano/shimano_di2.dart'; +import 'package:swift_control/bluetooth/devices/wahoo/wahoo_kickr_bike_shift.dart'; +import 'package:swift_control/bluetooth/devices/zwift/constants.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_click.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_clickv2.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_play.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_ride.dart'; +import 'package:universal_ble/universal_ble.dart'; + +void main() { + group('Detect Zwift devices', () { + test('Detect Zwift Play', () { + final device = _createBleDevice( + name: 'Zwift Play', + manufacturerData: [ + ManufacturerData(ZwiftConstants.ZWIFT_MANUFACTURER_ID, Uint8List.fromList([ZwiftConstants.RC1_RIGHT_SIDE])), + ], + ); + expect(BluetoothDevice.fromScanResult(device), isInstanceOf()); + }); + + test('Detect Zwift Ride', () { + final device = _createBleDevice( + name: 'Zwift Ride', + manufacturerData: [ + ManufacturerData(ZwiftConstants.ZWIFT_MANUFACTURER_ID, Uint8List.fromList([ZwiftConstants.RIDE_LEFT_SIDE])), + ], + services: [ZwiftConstants.ZWIFT_CUSTOM_SERVICE_UUID.toLowerCase()], + ); + expect(BluetoothDevice.fromScanResult(device), isInstanceOf()); + }); + test('Detect Zwift Ride old firmware', () { + final device = _createBleDevice( + name: 'Zwift Ride', + manufacturerData: [ + ManufacturerData(ZwiftConstants.ZWIFT_MANUFACTURER_ID, Uint8List.fromList([ZwiftConstants.RIDE_LEFT_SIDE])), + ], + services: [ZwiftConstants.ZWIFT_RIDE_CUSTOM_SERVICE_UUID.toLowerCase()], + ); + expect(BluetoothDevice.fromScanResult(device), isInstanceOf()); + }); + + test('Detect Zwift Click V1', () { + final device = _createBleDevice( + name: 'Zwift Click', + manufacturerData: [ + ManufacturerData(ZwiftConstants.ZWIFT_MANUFACTURER_ID, Uint8List.fromList([ZwiftConstants.BC1])), + ], + services: [ZwiftConstants.ZWIFT_CUSTOM_SERVICE_UUID.toLowerCase()], + ); + expect(BluetoothDevice.fromScanResult(device), isInstanceOf()); + }); + + test('Detect Zwift Click V2', () { + final device = _createBleDevice( + name: 'Zwift Click', + manufacturerData: [ + ManufacturerData( + ZwiftConstants.ZWIFT_MANUFACTURER_ID, + Uint8List.fromList([ZwiftConstants.CLICK_V2_LEFT_SIDE]), + ), + ], + services: [ZwiftConstants.ZWIFT_CUSTOM_SERVICE_UUID.toLowerCase()], + ); + expect(BluetoothDevice.fromScanResult(device), isInstanceOf()); + }); + }); + + group('Detect Elite devices', () { + test('Elite Square', () { + final device = _createBleDevice(name: 'SQUARE 1337'); + expect(BluetoothDevice.fromScanResult(device), isInstanceOf()); + }); + test('Elite Sterzo', () { + final device = _createBleDevice(name: 'STERZO 1337'); + expect(BluetoothDevice.fromScanResult(device), isInstanceOf()); + }); + }); + + group('Detect Wahoo devices', () { + test('Kickr Bike Shift', () { + final device = _createBleDevice(name: '133 KICKR BIKE SHIFT 133'); + expect(BluetoothDevice.fromScanResult(device), isInstanceOf()); + }); + }); + + group('Detect Cycplus devices', () { + test('Cycplus BC2', () { + final device = _createBleDevice(name: 'Cycplus BC2'); + expect(BluetoothDevice.fromScanResult(device), isInstanceOf()); + }); + test('Other cycplus', () { + final device = _createBleDevice(name: 'Cycplus 1337'); + expect(BluetoothDevice.fromScanResult(device), isNull); + }); + }); + + group('Detect Shimano Di2', () { + test('Shimano Di2', () { + final device = _createBleDevice(name: 'RDR 1337', services: [ShimanoDi2Constants.SERVICE_UUID.toLowerCase()]); + expect(BluetoothDevice.fromScanResult(device), isInstanceOf()); + }); + }); +} + +BleDevice _createBleDevice({ + required String name, + List manufacturerData = const [], + List services = const [], +}) { + return BleDevice( + deviceId: '1337', + name: name, + manufacturerDataList: manufacturerData, + services: services, + ); +} diff --git a/test/custom_profile_test.dart b/test/custom_profile_test.dart new file mode 100644 index 000000000..601c83e03 --- /dev/null +++ b/test/custom_profile_test.dart @@ -0,0 +1,129 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/keymap/apps/custom_app.dart'; +import 'package:swift_control/utils/settings/settings.dart'; + +void main() { + group('Custom Profile Tests', () { + setUp(() async { + // Initialize SharedPreferences with in-memory storage for testing + SharedPreferences.setMockInitialValues({}); + await settings.init(); + }); + + test('Should create custom app with default profile name', () { + final customApp = CustomApp(); + expect(customApp.profileName, 'Other'); + expect(customApp.name, 'Other'); + }); + + test('Should create custom app with custom profile name', () { + final customApp = CustomApp(profileName: 'Workout'); + expect(customApp.profileName, 'Workout'); + expect(customApp.name, 'Workout'); + }); + + test('Should save and retrieve custom profile', () async { + final customApp = CustomApp(profileName: 'Race'); + await settings.setKeyMap(customApp); + + final profiles = settings.getCustomAppProfiles(); + expect(profiles.contains('Race'), true); + }); + + test('Should list multiple custom profiles', () async { + final workout = CustomApp(profileName: 'Workout'); + final race = CustomApp(profileName: 'Race'); + final event = CustomApp(profileName: 'Event'); + + await settings.setKeyMap(workout); + await settings.setKeyMap(race); + await settings.setKeyMap(event); + + final profiles = settings.getCustomAppProfiles(); + expect(profiles.contains('Workout'), true); + expect(profiles.contains('Race'), true); + expect(profiles.contains('Event'), true); + expect(profiles.length, 3); + }); + + test('Should duplicate custom profile', () async { + await settings.reset(); + final original = CustomApp(profileName: 'Original'); + await settings.setKeyMap(original); + + await settings.duplicateCustomAppProfile('Original', 'Copy'); + + final profiles = settings.getCustomAppProfiles(); + expect(profiles.contains('Original'), true); + expect(profiles.contains('Copy'), true); + expect(profiles.length, 2); + }); + + test('Should delete custom profile', () async { + final customApp = CustomApp(profileName: 'ToDelete'); + await settings.setKeyMap(customApp); + + var profiles = settings.getCustomAppProfiles(); + expect(profiles.contains('ToDelete'), true); + + await settings.deleteCustomAppProfile('ToDelete'); + + profiles = settings.getCustomAppProfiles(); + expect(profiles.contains('ToDelete'), false); + }); + + test('Should not duplicate migration if already migrated', () async { + SharedPreferences.setMockInitialValues({ + 'customapp': ['old_data'], + 'customapp_Custom': ['new_data'], + 'app': 'Custom', + }); + + final newSettings = Settings(); + await newSettings.init(); + + // Old key should still exist because new key already existed + expect(newSettings.getCustomAppKeymap('customapp'), null); + final customKeymap = newSettings.getCustomAppKeymap('Custom'); + expect(customKeymap, isNotNull); + }); + + test('Should export custom profile as JSON', () async { + final customApp = CustomApp(profileName: 'TestProfile'); + await settings.setKeyMap(customApp); + + final jsonData = settings.exportCustomAppProfile('TestProfile'); + expect(jsonData, isNotNull); + expect(jsonData, contains('version')); + expect(jsonData, contains('profileName')); + expect(jsonData, contains('keymap')); + }); + + test('Should import custom profile from JSON', () async { + // First export a profile + final customApp = CustomApp(profileName: 'ExportTest'); + await settings.setKeyMap(customApp); + final jsonData = settings.exportCustomAppProfile('ExportTest'); + + // Import with a new name + final success = await settings.importCustomAppProfile(jsonData!, newProfileName: 'ImportTest'); + + expect(success, true); + final profiles = settings.getCustomAppProfiles(); + expect(profiles.contains('ImportTest'), true); + }); + + test('Should fail to import invalid JSON', () async { + final success = await settings.importCustomAppProfile('invalid json'); + expect(success, false); + }); + + test('Should fail to import JSON with missing fields', () async { + final invalidJson = '{"version": 1}'; + final success = await settings.importCustomAppProfile(invalidJson); + expect(success, false); + }); + }); +} diff --git a/test/cycplus_bc2_test.dart b/test/cycplus_bc2_test.dart new file mode 100644 index 000000000..f18353c7a --- /dev/null +++ b/test/cycplus_bc2_test.dart @@ -0,0 +1,125 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:swift_control/bluetooth/devices/cycplus/cycplus_bc2.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/actions/base_actions.dart'; +import 'package:universal_ble/universal_ble.dart'; + +void main() { + group('CYCPLUS BC2 Virtual Shifter Tests', () { + test('Test state machine with full sequence', () { + actionHandler = StubActions(); + + final stubActions = actionHandler as StubActions; + + final device = CycplusBc2(BleDevice(deviceId: 'deviceId', name: 'name')); + + // Packet 0: [6]=01 [7]=03 -> No trigger (lock state) + device.processCharacteristic( + CycplusBc2Constants.TX_CHARACTERISTIC_UUID, + _hexToUint8List('FEEFFFEE0206010397565E000155'), + ); + expect(stubActions.performedActions.isEmpty, true); + + // Packet 1: [6]=03 [7]=03 -> Trigger: shiftUp + device.processCharacteristic( + CycplusBc2Constants.TX_CHARACTERISTIC_UUID, + _hexToUint8List('FEEFFFEE0206030398565E000158'), + ); + expect(stubActions.performedActions.length, 1); + expect(stubActions.performedActions.first, CycplusBc2Buttons.shiftUp); + stubActions.performedActions.clear(); + + // Packet 2: [6]=03 [7]=01 -> Trigger: shiftDown + device.processCharacteristic( + CycplusBc2Constants.TX_CHARACTERISTIC_UUID, + _hexToUint8List('FEEFFFEE0206030198575E000157'), + ); + expect(stubActions.performedActions.length, 1); + expect(stubActions.performedActions.first, CycplusBc2Buttons.shiftDown); + stubActions.performedActions.clear(); + + // Packet 3: [6]=03 [7]=03 -> No trigger (lock state) + device.processCharacteristic( + CycplusBc2Constants.TX_CHARACTERISTIC_UUID, + _hexToUint8List('FEEFFFEE0206030398585E00015A'), + ); + expect(stubActions.performedActions.isEmpty, true); + + // Packet 4: [6]=01 [7]=03 -> Trigger: shiftUp + device.processCharacteristic( + CycplusBc2Constants.TX_CHARACTERISTIC_UUID, + _hexToUint8List('FEEFFFEE0206010399585E000159'), + ); + expect(stubActions.performedActions.length, 1); + expect(stubActions.performedActions.first, CycplusBc2Buttons.shiftUp); + stubActions.performedActions.clear(); + }); + + test('Test release and re-press behavior', () { + actionHandler = StubActions(); + final stubActions = actionHandler as StubActions; + final device = CycplusBc2(BleDevice(deviceId: 'deviceId', name: 'name')); + + // Press: lock state + device.processCharacteristic( + CycplusBc2Constants.TX_CHARACTERISTIC_UUID, + _hexToUint8List('FEEFFFEE0206010300005E000100'), + ); + expect(stubActions.performedActions.isEmpty, true); + + // Release: reset state + device.processCharacteristic( + CycplusBc2Constants.TX_CHARACTERISTIC_UUID, + _hexToUint8List('FEEFFFEE0206000000005E000100'), + ); + expect(stubActions.performedActions.isEmpty, true); + + // Press again: lock state (no trigger since we reset) + device.processCharacteristic( + CycplusBc2Constants.TX_CHARACTERISTIC_UUID, + _hexToUint8List('FEEFFFEE0206020300005E000100'), + ); + expect(stubActions.performedActions.isEmpty, true); + + // Change to different pressed value: trigger + device.processCharacteristic( + CycplusBc2Constants.TX_CHARACTERISTIC_UUID, + _hexToUint8List('FEEFFFEE0206010300005E000100'), + ); + expect(stubActions.performedActions.length, 1); + expect(stubActions.performedActions.first, CycplusBc2Buttons.shiftUp); + }); + + test('Test both buttons can trigger simultaneously', () { + actionHandler = StubActions(); + final stubActions = actionHandler as StubActions; + final device = CycplusBc2(BleDevice(deviceId: 'deviceId', name: 'name')); + + // Lock both states + device.processCharacteristic( + CycplusBc2Constants.TX_CHARACTERISTIC_UUID, + _hexToUint8List('FEEFFFEE0206010100005E000100'), + ); + expect(stubActions.performedActions.isEmpty, true); + + // Change both: trigger both + device.processCharacteristic( + CycplusBc2Constants.TX_CHARACTERISTIC_UUID, + _hexToUint8List('FEEFFFEE0206020200005E000100'), + ); + expect(stubActions.performedActions.length, 1); + expect(stubActions.performedActions.contains(CycplusBc2Buttons.shiftUp), true); + }); + }); +} + +Uint8List _hexToUint8List(String seq) { + return Uint8List.fromList( + List.generate( + seq.length ~/ 2, + (i) => int.parse(seq.substring(i * 2, i * 2 + 2), radix: 16), + ), + ); +} diff --git a/test/elite_square_test.dart b/test/elite_square_test.dart new file mode 100644 index 000000000..732fc9c05 --- /dev/null +++ b/test/elite_square_test.dart @@ -0,0 +1,132 @@ +import 'package:flutter_test/flutter_test.dart'; + +// Helper function matching the Elite Square implementation +// Extracts the 8-character button code from positions 6-14 of the hex string +String extractButtonCode(String hexValue) { + if (hexValue.length >= 14) { + return hexValue.substring(6, 14); + } + return hexValue.substring(6); +} + +String bytesToHex(List bytes) { + return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); +} + +void main() { + group('Elite Square Button Detection Tests', () { + test('Should extract correct button code from hex string', () { + // Test with actual dump data + expect(extractButtonCode('030153000000020318f40101'), equals('00000002')); + expect(extractButtonCode('030153000000010318f40101'), equals('00000001')); + expect(extractButtonCode('030153000004000318f40101'), equals('00000400')); + expect(extractButtonCode('030153000001000318f40101'), equals('00000100')); + expect(extractButtonCode('030153000008000318f40101'), equals('00000800')); + expect(extractButtonCode('030153000002000318f40101'), equals('00000200')); + expect(extractButtonCode('030153000000000318f40101'), equals('00000000')); + }); + + test('Should detect button changes correctly', () { + // Test that button code extraction is consistent for comparison + final idleState = '030153000000000318f40101'; + final buttonPressed = '030153000000020318f40101'; + + final idleCode = extractButtonCode(idleState); + final pressedCode = extractButtonCode(buttonPressed); + + expect(idleCode, equals('00000000')); + expect(pressedCode, equals('00000002')); + expect(idleCode != pressedCode, isTrue); + }); + + test('Should handle button release correctly', () { + // Simulate button press and release + final states = [ + '030153000000000318f40101', // idle + '030153000000020318f40101', // button pressed + '030153000000000318f40101', // button released (back to idle) + ]; + + final parts = states.map(extractButtonCode).toList(); + + expect(parts[0], equals('00000000')); // idle + expect(parts[1], equals('00000002')); // pressed + expect(parts[2], equals('00000000')); // released + + // Verify state transitions + expect(parts[0] != parts[1], isTrue); // idle -> pressed + expect(parts[1] != parts[2], isTrue); // pressed -> released + expect(parts[0] == parts[2], isTrue); // back to idle + }); + + test('Should handle all button codes from mapping', () { + // Test all button codes from the mapping + final buttonCodes = { + "00000200": "up", + "00000100": "left", + "00000800": "down", + "00000400": "right", + "00002000": "x", + "00001000": "square", + "00008000": "campagnoloLeft", + "00004000": "leftBrake", + "00000002": "leftShift1", + "00000001": "leftShift2", + "02000000": "y", + "01000000": "a", + "08000000": "b", + "04000000": "z", + "20000000": "circle", + "10000000": "triangle", + "80000000": "campagnoloRight", + "40000000": "rightBrake", + "00020000": "rightShift1", + "00010000": "rightShift2", + }; + + // Verify all button codes are 8 characters + for (final code in buttonCodes.keys) { + expect(code.length, equals(8), reason: 'Button code $code should be 8 characters'); + } + }); + + test('Should convert bytes to hex correctly', () { + // Test with sample data from the dump + // 030153000000020318f40101 = [0x03, 0x01, 0x53, 0x00, 0x00, 0x00, 0x02, 0x03, 0x18, 0xf4, 0x01, 0x01] + final bytes = [0x03, 0x01, 0x53, 0x00, 0x00, 0x00, 0x02, 0x03, 0x18, 0xf4, 0x01, 0x01]; + final hex = bytesToHex(bytes); + + expect(hex, equals('030153000000020318f40101')); + }); + + test('Should handle edge cases', () { + // Test with short strings + expect(extractButtonCode('0123456789'), equals('6789')); + expect(extractButtonCode('01234567'), equals('67')); + + // Test with exact length (14 chars extracts positions 6-14) + expect(extractButtonCode('01234567890123'), equals('67890123')); + + // Test strings shorter than 14 extract from position 6 to end + expect(extractButtonCode('0123456789ABC'), equals('6789ABC')); + }); + }); + + group('Elite Square Protocol Tests', () { + test('Should recognize button press pattern', () { + // According to the dump, the pattern is: + // Base: 030153000000000318f40101 + // Byte positions 6-13 (8 chars) change to indicate button + + final baseHex = '030153000000000318f40101'; + final buttonHex = '030153000000020318f40101'; + + // Extract the button part (positions 6-14) + final baseButton = baseHex.substring(6, 14); + final pressedButton = buttonHex.substring(6, 14); + + expect(baseButton, equals('00000000')); + expect(pressedButton, equals('00000002')); + }); + }); +} diff --git a/test/elite_sterzo_test.dart b/test/elite_sterzo_test.dart new file mode 100644 index 000000000..e785c747f --- /dev/null +++ b/test/elite_sterzo_test.dart @@ -0,0 +1,70 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Elite Sterzo Smart Calibration Tests', () { + test('Should ignore NaN values during calibration', () { + // Test that NaN values are filtered out + final samples = [double.nan, 1.5, 2.0, 2.5, 3.0, double.nan, 3.5, 4.0, 4.5, 5.0, 5.5]; + final validSamples = samples.where((s) => !s.isNaN).take(10).toList(); + + expect(validSamples.length, equals(9)); + expect(validSamples.every((s) => !s.isNaN), isTrue); + }); + + test('Should compute correct calibration offset from samples', () { + // Test offset calculation + final samples = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]; + final offset = samples.reduce((a, b) => a + b) / samples.length; + + expect(offset, equals(5.5)); + }); + + test('Should round angles to whole degrees', () { + // Test rounding behavior + expect(4.3.round(), equals(4)); + expect(4.6.round(), equals(5)); + expect(4.7.round(), equals(5)); + expect((-4.3).round(), equals(-4)); + expect((-4.6).round(), equals(-5)); + }); + }); + + group('Elite Sterzo Smart PWM Keypress Tests', () { + test('Should calculate correct keypress levels', () { + // Test level calculation with LEVEL_DEGREE_STEP = 10 and MAX_LEVELS = 5 + const levelDegreeStep = 10.0; + const maxLevels = 5; + + int calculateLevels(int absAngle) { + final levels = (absAngle / levelDegreeStep).floor(); + return levels.clamp(1, maxLevels); + } + + expect(calculateLevels(5), equals(1)); // Below threshold but level 1 + expect(calculateLevels(10), equals(1)); // 10 / 10 = 1 + expect(calculateLevels(15), equals(1)); // 15 / 10 = 1.5 floor = 1 + expect(calculateLevels(20), equals(2)); // 20 / 10 = 2 + expect(calculateLevels(35), equals(3)); // 35 / 10 = 3.5 floor = 3 + expect(calculateLevels(50), equals(5)); // 50 / 10 = 5 (max) + expect(calculateLevels(100), equals(5)); // 100 / 10 = 10 but clamped to 5 + }); + + test('Should determine correct steering direction', () { + // Test direction determination + expect(25 > 0, isTrue); // Positive = RIGHT + expect(-25 > 0, isFalse); // Negative = LEFT + }); + }); + + group('Elite Sterzo Smart Threshold Tests', () { + test('Should correctly apply steering threshold', () { + const steeringThreshold = 10.0; + + // Test threshold logic + expect(5.abs() > steeringThreshold, isFalse); // Below threshold + expect(10.abs() > steeringThreshold, isFalse); // At threshold + expect(11.abs() > steeringThreshold, isTrue); // Above threshold + expect((-11).abs() > steeringThreshold, isTrue); // Above threshold (negative) + }); + }); +} diff --git a/test/long_press_test.dart b/test/long_press_test.dart new file mode 100644 index 000000000..8a5802413 --- /dev/null +++ b/test/long_press_test.dart @@ -0,0 +1,75 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:swift_control/bluetooth/devices/zwift/constants.dart'; +import 'package:swift_control/utils/keymap/keymap.dart'; + +void main() { + group('Long Press KeyPair Tests', () { + test('KeyPair should encode and decode isLongPress property', () { + // Create a KeyPair with long press enabled + final keyPair = KeyPair( + buttons: [ZwiftButtons.a], + physicalKey: PhysicalKeyboardKey.keyA, + logicalKey: LogicalKeyboardKey.keyA, + isLongPress: true, + ); + + // Encode the KeyPair + final encoded = keyPair.encode(); + + // Decode the KeyPair + final decoded = KeyPair.decode(encoded); + + // Verify the decoded KeyPair has the correct properties + expect(decoded, isNotNull); + expect(decoded!.isLongPress, true); + expect(decoded.buttons, equals([ZwiftButtons.a])); + expect(decoded.physicalKey, equals(PhysicalKeyboardKey.keyA)); + expect(decoded.logicalKey, equals(LogicalKeyboardKey.keyA)); + }); + + test('KeyPair should default isLongPress to false when not specified in decode', () { + // Create a legacy encoded KeyPair without isLongPress property + const legacyEncoded = ''' + { + "actions": ["a"], + "logicalKey": "97", + "physicalKey": "458752", + "touchPosition": {"x": 0.0, "y": 0.0} + } + '''; + + // Decode the legacy KeyPair + final decoded = KeyPair.decode(legacyEncoded); + + // Verify the decoded KeyPair defaults isLongPress to false + expect(decoded, isNotNull); + expect(decoded!.isLongPress, false); + }); + + test('KeyPair constructor should default isLongPress to false', () { + final keyPair = KeyPair( + buttons: [ZwiftButtons.a], + physicalKey: PhysicalKeyboardKey.keyA, + logicalKey: LogicalKeyboardKey.keyA, + ); + + expect(keyPair.isLongPress, false); + }); + + test('KeyPair should correctly encode isLongPress false', () { + final keyPair = KeyPair( + buttons: [ZwiftButtons.a], + physicalKey: PhysicalKeyboardKey.keyA, + logicalKey: LogicalKeyboardKey.keyA, + isLongPress: false, + ); + + final encoded = keyPair.encode(); + final decoded = KeyPair.decode(encoded); + + expect(decoded, isNotNull); + expect(decoded!.isLongPress, false); + }); + }); +} diff --git a/test/modifier_keys_test.dart b/test/modifier_keys_test.dart new file mode 100644 index 000000000..b5eff88bd --- /dev/null +++ b/test/modifier_keys_test.dart @@ -0,0 +1,142 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:swift_control/bluetooth/devices/zwift/constants.dart'; +import 'package:swift_control/utils/keymap/keymap.dart'; + +void main() { + group('Modifier Keys KeyPair Tests', () { + test('KeyPair should encode and decode modifiers property', () { + // Create a KeyPair with modifiers + final keyPair = KeyPair( + buttons: [ZwiftButtons.a], + physicalKey: PhysicalKeyboardKey.keyR, + logicalKey: LogicalKeyboardKey.keyR, + modifiers: [ModifierKey.controlModifier, ModifierKey.altModifier], + ); + + // Encode the KeyPair + final encoded = keyPair.encode(); + + // Decode the KeyPair + final decoded = KeyPair.decode(encoded); + + // Verify the decoded KeyPair has the correct properties + expect(decoded, isNotNull); + expect(decoded!.modifiers.length, 2); + expect(decoded.modifiers, contains(ModifierKey.controlModifier)); + expect(decoded.modifiers, contains(ModifierKey.altModifier)); + expect(decoded.buttons, equals([ZwiftButtons.a])); + expect(decoded.physicalKey, equals(PhysicalKeyboardKey.keyR)); + expect(decoded.logicalKey, equals(LogicalKeyboardKey.keyR)); + }); + + test('KeyPair should default modifiers to empty list when not specified in decode', () { + // Create a legacy encoded KeyPair without modifiers property + const legacyEncoded = ''' + { + "actions": ["a"], + "logicalKey": "97", + "physicalKey": "458752", + "touchPosition": {"x": 0.0, "y": 0.0}, + "isLongPress": false + } + '''; + + // Decode the legacy KeyPair + final decoded = KeyPair.decode(legacyEncoded); + + // Verify the decoded KeyPair defaults modifiers to empty + expect(decoded, isNotNull); + expect(decoded!.modifiers, isEmpty); + }); + + test('KeyPair constructor should default modifiers to empty list', () { + final keyPair = KeyPair( + buttons: [ZwiftButtons.a], + physicalKey: PhysicalKeyboardKey.keyA, + logicalKey: LogicalKeyboardKey.keyA, + ); + + expect(keyPair.modifiers, isEmpty); + }); + + test('KeyPair should correctly encode empty modifiers', () { + final keyPair = KeyPair( + buttons: [ZwiftButtons.a], + physicalKey: PhysicalKeyboardKey.keyA, + logicalKey: LogicalKeyboardKey.keyA, + modifiers: [], + ); + + final encoded = keyPair.encode(); + final decoded = KeyPair.decode(encoded); + + expect(decoded, isNotNull); + expect(decoded!.modifiers, isEmpty); + }); + + test('KeyPair toString should format modifiers correctly', () { + final keyPairWithCtrlAlt = KeyPair( + buttons: [ZwiftButtons.a], + physicalKey: PhysicalKeyboardKey.keyR, + logicalKey: LogicalKeyboardKey.keyR, + modifiers: [ModifierKey.controlModifier, ModifierKey.altModifier], + ); + + final result = keyPairWithCtrlAlt.toString(); + expect(result, contains('Ctrl')); + expect(result, contains('Alt')); + expect(result, contains('R')); + }); + + test('KeyPair toString should handle single modifier', () { + final keyPairWithShift = KeyPair( + buttons: [ZwiftButtons.a], + physicalKey: PhysicalKeyboardKey.keyA, + logicalKey: LogicalKeyboardKey.keyA, + modifiers: [ModifierKey.shiftModifier], + ); + + final result = keyPairWithShift.toString(); + expect(result, contains('Shift')); + expect(result, contains('A')); + }); + + test('KeyPair toString should handle no modifiers', () { + final keyPairNoModifier = KeyPair( + buttons: [ZwiftButtons.a], + physicalKey: PhysicalKeyboardKey.keyA, + logicalKey: LogicalKeyboardKey.keyA, + modifiers: [], + ); + + final result = keyPairNoModifier.toString(); + expect(result, equals('A')); + expect(result, isNot(contains('+'))); + }); + + test('KeyPair should encode and decode all modifier types', () { + final keyPair = KeyPair( + buttons: [ZwiftButtons.a], + physicalKey: PhysicalKeyboardKey.keyF, + logicalKey: LogicalKeyboardKey.keyF, + modifiers: [ + ModifierKey.shiftModifier, + ModifierKey.controlModifier, + ModifierKey.altModifier, + ModifierKey.metaModifier, + ], + ); + + final encoded = keyPair.encode(); + final decoded = KeyPair.decode(encoded); + + expect(decoded, isNotNull); + expect(decoded!.modifiers.length, 4); + expect(decoded.modifiers, contains(ModifierKey.shiftModifier)); + expect(decoded.modifiers, contains(ModifierKey.controlModifier)); + expect(decoded.modifiers, contains(ModifierKey.altModifier)); + expect(decoded.modifiers, contains(ModifierKey.metaModifier)); + }); + }); +} diff --git a/test/screenshot_test.dart b/test/screenshot_test.dart new file mode 100644 index 000000000..4e7010440 --- /dev/null +++ b/test/screenshot_test.dart @@ -0,0 +1,134 @@ +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_ride.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/pages/device.dart'; +import 'package:swift_control/theme.dart'; +import 'package:swift_control/utils/keymap/apps/my_whoosh.dart'; +import 'package:swift_control/utils/requirements/multi.dart'; +import 'package:test_screenshot/test_screenshot.dart'; +import 'package:universal_ble/universal_ble.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + PackageInfo.setMockInitialValues( + appName: 'BikeControl', + packageName: 'de.jonasbark.swiftcontrol', + version: '3.5.0', + buildNumber: '1', + buildSignature: '', + ); + SharedPreferences.setMockInitialValues({}); + + group('Screenshot Tests', () { + final List<(String type, Size size)> sizes = [ + ('Phone', Size(400, 800)), + ('iPhone', Size(1242, 2688)), + ('macOS', Size(1280, 800)), + ('GitHub', Size(600, 900)), + ]; + + testWidgets('Requirements', (WidgetTester tester) async { + await tester.loadFonts(); + for (final size in sizes) { + await _createRequirementScreenshot(tester, size); + } + + // Reset + }); + testWidgets('Device', (WidgetTester tester) async { + await tester.loadFonts(); + for (final size in sizes) { + await _createDeviceScreenshot(tester, size); + } + }); + }); +} + +Future _createDeviceScreenshot(WidgetTester tester, (String type, Size size) spec) async { + // Set phone screen size (typical Android phone - 1140x2616 to match existing) + tester.view.physicalSize = spec.$2; + tester.view.devicePixelRatio = 1; + + screenshotMode = true; + + await settings.init(); + await settings.reset(); + settings.setTrainerApp(MyWhoosh()); + settings.setKeyMap(MyWhoosh()); + settings.setLastTarget(Target.thisDevice); + + connection.addDevices([ + ZwiftRide( + BleDevice( + name: 'Controller', + deviceId: '00:11:22:33:44:55', + ), + ) + ..firmwareVersion = '1.2.0' + ..rssi = -51 + ..batteryLevel = 81, + ]); + + await tester.pumpWidget( + Screenshotter( + child: MaterialApp( + navigatorKey: navigatorKey, + debugShowCheckedModeBanner: false, + title: 'BikeControl', + theme: AppTheme.light, + darkTheme: AppTheme.dark, + themeMode: ThemeMode.light, + home: const DevicePage(), + ), + ), + ); + + const wait = 1; + + try { + await tester.pumpAndSettle(Duration(seconds: wait), EnginePhase.sendSemanticsUpdate, Duration(seconds: wait)); + } catch (e) { + // Ignore timeout errors + } + + await _takeScreenshot(tester, 'device-${spec.$1}-${spec.$2.width.toInt()}x${spec.$2.height.toInt()}.png', spec.$2); +} + +Future _createRequirementScreenshot(WidgetTester tester, (String type, Size size) spec) async { + // Set phone screen size (typical Android phone - 1140x2616 to match existing) + tester.view.physicalSize = spec.$2; + tester.view.devicePixelRatio = 1; + + await settings.init(); + await settings.reset(); + screenshotMode = true; + await tester.pumpWidget( + Screenshotter( + child: SwiftPlayApp(), + ), + ); + await tester.pumpAndSettle(); + + await _takeScreenshot( + tester, + 'screenshot-${spec.$1}-${spec.$2.width.toInt()}x${spec.$2.height.toInt()}.png', + spec.$2, + ); +} + +Future _takeScreenshot(WidgetTester tester, String path, Size size) async { + const FileSystem fs = LocalFileSystem(); + final file = fs.file('screenshots/$path'); + await fs.directory('screenshots').create(); + print('File path: ${file.absolute.path}'); + + await tester.screenshot(path: 'screenshots/$path'); + final decodedImage = await decodeImageFromList(file.readAsBytesSync()); + // resize image +} diff --git a/test/shimano_di2_test.dart b/test/shimano_di2_test.dart new file mode 100644 index 000000000..44aad8ac1 --- /dev/null +++ b/test/shimano_di2_test.dart @@ -0,0 +1,122 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Shimano DI2 Tests', () { + test('Should validate service UUID format', () { + const serviceUuid = "000018ef-5348-494d-414e-4f5f424c4500"; + + expect(serviceUuid.length, equals(36)); + expect(serviceUuid.contains('-'), isTrue); + expect(serviceUuid.toLowerCase(), equals(serviceUuid)); + }); + + test('Should validate D-Fly Channel UUID format', () { + const dFlyChannelUuid = "00002ac2-5348-494d-414e-4f5f424c4500"; + + expect(dFlyChannelUuid.length, equals(36)); + expect(dFlyChannelUuid.contains('-'), isTrue); + expect(dFlyChannelUuid.toLowerCase(), equals(dFlyChannelUuid)); + }); + + test('Should handle button state initialization without triggering', () { + // Simulate initial button states + final initialStates = [0x00, 0x01, 0x00, 0x00]; // 4 channels + + // On first data reception, these should not trigger any button presses + // This is the expected behavior after the fix + expect(initialStates.length, equals(4)); + + // Verify all channels have a value + for (var state in initialStates) { + expect(state, isNotNull); + } + }); + + test('Should detect button press after initialization', () { + // Initial state + final initialStates = [0x00, 0x00, 0x00, 0x00]; + + // Button pressed on channel 1 + final newStates = [0x01, 0x00, 0x00, 0x00]; + + // Channel 0 changed from 0x00 to 0x01 + expect(initialStates[0] != newStates[0], isTrue); + // Other channels remain unchanged + expect(initialStates[1] == newStates[1], isTrue); + expect(initialStates[2] == newStates[2], isTrue); + expect(initialStates[3] == newStates[3], isTrue); + }); + + test('Should detect button release after press', () { + // Button pressed state + final pressedStates = [0x01, 0x00, 0x00, 0x00]; + + // Button released + final releasedStates = [0x00, 0x00, 0x00, 0x00]; + + // Channel 0 changed from 0x01 to 0x00 + expect(pressedStates[0] != releasedStates[0], isTrue); + }); + + test('Should handle multiple simultaneous button presses', () { + // Initial state + final initialStates = [0x00, 0x00, 0x00, 0x00]; + + // Multiple buttons pressed + final pressedStates = [0x01, 0x01, 0x00, 0x00]; + + // Channels 0 and 1 changed + expect(initialStates[0] != pressedStates[0], isTrue); + expect(initialStates[1] != pressedStates[1], isTrue); + // Channels 2 and 3 unchanged + expect(initialStates[2] == pressedStates[2], isTrue); + expect(initialStates[3] == pressedStates[3], isTrue); + }); + + test('Should recognize RDR device name prefix', () { + const deviceName1 = 'RDR'; + const deviceName2 = 'RDR-8070'; + const deviceName3 = 'rdr-di2'; + + expect(deviceName1.toUpperCase().startsWith('RDR'), isTrue); + expect(deviceName2.toUpperCase().startsWith('RDR'), isTrue); + expect(deviceName3.toUpperCase().startsWith('RDR'), isTrue); + }); + + test('Should not match non-RDR devices', () { + const deviceName1 = 'Zwift Click'; + const deviceName2 = 'Elite Sterzo'; + const deviceName3 = 'CYCPLUS BC2'; + + expect(deviceName1.toUpperCase().startsWith('RDR'), isFalse); + expect(deviceName2.toUpperCase().startsWith('RDR'), isFalse); + expect(deviceName3.toUpperCase().startsWith('RDR'), isFalse); + }); + + test('Should handle D-Fly channel naming', () { + // Channels are 0-indexed in code but displayed as 1-indexed + final channelIndex = 0; + final readableIndex = channelIndex + 1; + final channelName = 'D-Fly Channel $readableIndex'; + + expect(channelName, equals('D-Fly Channel 1')); + expect(readableIndex, equals(1)); + }); + + test('Should maintain separate state for each channel', () { + // State map simulating _lastButtons + final stateMap = { + 0: 0x00, + 1: 0x01, + 2: 0x00, + 3: 0x00, + }; + + // Each channel should have its own independent state + expect(stateMap[0], equals(0x00)); + expect(stateMap[1], equals(0x01)); + expect(stateMap[2], equals(0x00)); + expect(stateMap[3], equals(0x00)); + }); + }); +} diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index b0504cb74..000000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,29 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:swift_control/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const SwiftPlayApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/web/favicon.ico b/web/favicon.ico new file mode 100644 index 000000000..6a06f5210 Binary files /dev/null and b/web/favicon.ico differ diff --git a/web/favicon.png b/web/favicon.png deleted file mode 100644 index 39918661e..000000000 Binary files a/web/favicon.png and /dev/null differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png index 1e24bd1a6..1622aff01 100644 Binary files a/web/icons/Icon-192.png and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png index 0e1598407..4063cc353 100644 Binary files a/web/icons/Icon-512.png and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png index 1e24bd1a6..b0da3594b 100644 Binary files a/web/icons/Icon-maskable-192.png and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png index 0e1598407..a9d54d027 100644 Binary files a/web/icons/Icon-maskable-512.png and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html index 3fe0aaaab..b3102de3f 100644 --- a/web/index.html +++ b/web/index.html @@ -1,24 +1,11 @@ - - + @@ -27,9 +14,9 @@ - + - swift_play + BikeControl diff --git a/web/manifest.json b/web/manifest.json index 7e2f0f65f..50bb91ac3 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -1,11 +1,11 @@ { - "name": "SwiftControl", - "short_name": "SwiftControl", + "name": "BikeControl", + "short_name": "BikeControl", "start_url": ".", "display": "standalone", - "background_color": "#hexcode", - "theme_color": "#hexcode", - "description": "SwiftControl - Control your virtual riding", + "background_color": "#ffffff", + "theme_color": "#ffffff", + "description": "BikeControl - Control your virtual riding", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 5c3f5f7be..204f93699 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -1,10 +1,10 @@ # Project-level configuration. cmake_minimum_required(VERSION 3.14) -project(swift_play LANGUAGES CXX) +project(swift_control LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "swift_play") +set(BINARY_NAME "swift_control") # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 110b575aa..077017146 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,36 @@ #include "generated_plugin_registrant.h" +#include +#include +#include #include +#include +#include +#include +#include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + BluetoothLowEnergyWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("BluetoothLowEnergyWindowsPluginCApi")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + GamepadsWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GamepadsWindowsPluginCApi")); KeypressSimulatorWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("KeypressSimulatorWindowsPluginCApi")); + MediaKeyDetectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("MediaKeyDetectorWindows")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); + UniversalBlePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UniversalBlePluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index cc18f110b..911d77dbc 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,7 +3,16 @@ # list(APPEND FLUTTER_PLUGIN_LIST + bluetooth_low_energy_windows + file_selector_windows + gamepads_windows keypress_simulator_windows + media_key_detector_windows + permission_handler_windows + screen_retriever_windows + universal_ble + url_launcher_windows + window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index 4d99b65d2..38ec33673 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -90,12 +90,12 @@ BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "de.jonasbark" "\0" - VALUE "FileDescription", "swift_play" "\0" + VALUE "FileDescription", "swift_control" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "swift_play" "\0" + VALUE "InternalName", "swift_control" "\0" VALUE "LegalCopyright", "Copyright (C) 2025 de.jonasbark. All rights reserved." "\0" - VALUE "OriginalFilename", "swift_play.exe" "\0" - VALUE "ProductName", "swift_play" "\0" + VALUE "OriginalFilename", "swift_control.exe" "\0" + VALUE "ProductName", "swift_control" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index c5885a6bd..4141ea94e 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -27,7 +27,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.Create(L"swift_play", origin, size)) { + if (!window.Create(L"swift_control", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico index 975d4c9b4..6eec5790d 100644 Binary files a/windows/runner/resources/app_icon.ico and b/windows/runner/resources/app_icon.ico differ