Compare commits
176 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57961aec5d | ||
|
|
1675d7f2d0 | ||
|
|
baec8d24c3 | ||
|
|
a968723277 | ||
|
|
8668957738 | ||
|
|
ac550fad5b | ||
|
|
c511ac32b6 | ||
|
|
ee48ce0f4e | ||
|
|
8a3d64491b | ||
|
|
b72cc803f0 | ||
|
|
ea17b2e142 | ||
|
|
da62fc4dc6 | ||
|
|
239630f681 | ||
|
|
d95d0cf8cf | ||
|
|
2b25ba942c | ||
|
|
c65369a746 | ||
|
|
fa7d5e7853 | ||
|
|
8ac47cbd4d | ||
|
|
eb85844503 | ||
|
|
010d0ed331 | ||
|
|
1f8f7765a3 | ||
|
|
68f416dda3 | ||
|
|
49e45faec0 | ||
|
|
c81516350a | ||
|
|
890f393fd6 | ||
|
|
e46969c5c4 | ||
|
|
1ec9b55645 | ||
|
|
b0caf7c13b | ||
|
|
302fc15dd7 | ||
|
|
6a2cf1a1c9 | ||
|
|
8ea73bc54a | ||
|
|
7cbab3925f | ||
|
|
246a1bd2be | ||
|
|
f7e2a89ed6 | ||
|
|
f94252edb9 | ||
|
|
b7b6b9803f | ||
|
|
807d868b74 | ||
|
|
c3e8c4666c | ||
|
|
926651ebb3 | ||
|
|
a7d5624582 | ||
|
|
03209740ec | ||
|
|
af6ae3433e | ||
|
|
41f4dd1d57 | ||
|
|
d5c1b67675 | ||
|
|
0b18d74ac9 | ||
|
|
fb3fe5f8c0 | ||
|
|
796c973fd4 | ||
|
|
7c6335c4d1 | ||
|
|
af2267c486 | ||
|
|
56d9e62610 | ||
|
|
7e18a169d4 | ||
|
|
74280eda34 | ||
|
|
e1309d4d95 | ||
|
|
14aa6f7454 | ||
|
|
1368d7d24e | ||
|
|
8c09b170c3 | ||
|
|
080409b984 | ||
|
|
f0ec276547 | ||
|
|
23aafcd7bc | ||
|
|
3718a126ac | ||
|
|
846dd07bf4 | ||
|
|
ba60062a24 | ||
|
|
ed4f928fde | ||
|
|
2a09d550e5 | ||
|
|
bb1ae4e616 | ||
|
|
828aa70a56 | ||
|
|
4021f3131d | ||
|
|
80ef81ca64 | ||
|
|
fec13d012b | ||
|
|
e8ca3fc287 | ||
|
|
d5260d801c | ||
|
|
916b1ec1fc | ||
|
|
7380bb5001 | ||
|
|
2e95fb556a | ||
|
|
90591cbfa2 | ||
|
|
929409db71 | ||
|
|
4263375fb2 | ||
|
|
bb5d149ba4 | ||
|
|
1a322dc0d3 | ||
|
|
d10da94f20 | ||
|
|
7eb28881cb | ||
|
|
823e04d189 | ||
|
|
ca5d4aeadb | ||
|
|
a4d937c4f3 | ||
|
|
fa4add6797 | ||
|
|
ec2ed4e6c5 | ||
|
|
6bd41d9a54 | ||
|
|
1ff2a205bc | ||
|
|
dd73c3249b | ||
|
|
75eef49317 | ||
|
|
e8858e0c7d | ||
|
|
df9142a6bf | ||
|
|
36f312403b | ||
|
|
d8983889ae | ||
|
|
bfaf2f2d29 | ||
|
|
2ba9c284ba | ||
|
|
ef2b4af28a | ||
|
|
ba042cd07d | ||
|
|
f8cb4cff4f | ||
|
|
92010b787b | ||
|
|
e142a8c587 | ||
|
|
759dcaa8b8 | ||
|
|
05939dcf1e | ||
|
|
34494819f5 | ||
|
|
a9491b7fa5 | ||
|
|
311a676aea | ||
|
|
2eab9c581c | ||
|
|
1284499c25 | ||
|
|
a74471b9f8 | ||
|
|
81f61a5b87 | ||
|
|
7b2446b6e0 | ||
|
|
60898f7536 | ||
|
|
b2fa7870b6 | ||
|
|
6ef2ff711a | ||
|
|
9f58dca10e | ||
|
|
35e499720b | ||
|
|
7820a80241 | ||
|
|
ffc6409488 | ||
|
|
8eaa411a80 | ||
|
|
f08714f25a | ||
|
|
1f3352ff80 | ||
|
|
2601844970 | ||
|
|
e4bbb8b279 | ||
|
|
a13e2aa494 | ||
|
|
b8383a2280 | ||
|
|
2cb5ef03ce | ||
|
|
5203c3a576 | ||
|
|
36dfb2dc0b | ||
|
|
3f6434b5a3 | ||
|
|
d9595a3485 | ||
|
|
b3352d0c1c | ||
|
|
7e15df1f15 | ||
|
|
b7e086c326 | ||
|
|
659e7b0585 | ||
|
|
501ab48da5 | ||
|
|
3b9ceea64b | ||
|
|
f3c7bbbcbf | ||
|
|
9b21a2775e | ||
|
|
b669d4c5ea | ||
|
|
a744242c70 | ||
|
|
7f963f71f8 | ||
|
|
9f0ab53e1f | ||
|
|
5fc16e9fb7 | ||
|
|
4329afba1c | ||
|
|
01f87beef5 | ||
|
|
45fecfb4f6 | ||
|
|
9b020e09ae | ||
|
|
3b1c05aba4 | ||
|
|
90a111944a | ||
|
|
ceb029afb0 | ||
|
|
dc769ce6a0 | ||
|
|
66b7e74f84 | ||
|
|
29ef0dfaf4 | ||
|
|
90144948f4 | ||
|
|
bda384953e | ||
|
|
b0fd2a8413 | ||
|
|
2403971063 | ||
|
|
22a0379202 | ||
|
|
c06a426490 | ||
|
|
908e144e1b | ||
|
|
0189019e54 | ||
|
|
5995835d03 | ||
|
|
16e637b256 | ||
|
|
ac2522e860 | ||
|
|
fdb3ad0efc | ||
|
|
f7a01f3c32 | ||
|
|
94fd2c7eff | ||
|
|
f917dfbbb2 | ||
|
|
40bfad6810 | ||
|
|
fefde66b7b | ||
|
|
6869adcc09 | ||
|
|
f5abaec551 | ||
|
|
52fbf693b5 | ||
|
|
bf3995496e | ||
|
|
f7470a032a | ||
|
|
64c9fe5f03 |
124
.github/workflows/build.yml
vendored
@@ -6,23 +6,33 @@ on:
|
||||
build_mac:
|
||||
description: 'Build for macOS'
|
||||
required: false
|
||||
default: 'true'
|
||||
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'
|
||||
default: true
|
||||
type: boolean
|
||||
build_android:
|
||||
description: 'Build for Android'
|
||||
required: false
|
||||
default: 'true'
|
||||
default: true
|
||||
type: boolean
|
||||
build_ios:
|
||||
description: 'Build for iOS'
|
||||
required: false
|
||||
default: 'true'
|
||||
default: true
|
||||
type: boolean
|
||||
build_web:
|
||||
description: 'Build for Web'
|
||||
required: false
|
||||
default: 'true'
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
SHOREBIRD_TOKEN: ${{ secrets.SHOREBIRD_TOKEN }}
|
||||
@@ -44,7 +54,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install certificates
|
||||
if: github.event.inputs.build_mac == 'true' || github.event.inputs.build_ios == 'true'
|
||||
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 }}
|
||||
@@ -87,25 +97,26 @@ jobs:
|
||||
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: github.event.inputs.build_mac == 'true'
|
||||
if: inputs.build_mac
|
||||
uses: shorebirdtech/shorebird-release@v1
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
platform: macos
|
||||
|
||||
- name: Decode Keystore
|
||||
if: github.event.inputs.build_android == 'true'
|
||||
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: github.event.inputs.build_android == 'true'
|
||||
if: inputs.build_android
|
||||
uses: shorebirdtech/shorebird-release@v1
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
@@ -113,25 +124,25 @@ jobs:
|
||||
args: "--artifact=apk"
|
||||
|
||||
- name: Set Up Flutter
|
||||
if: github.event.inputs.build_web == 'true'
|
||||
if: inputs.build_web
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
|
||||
- name: Build Web
|
||||
if: github.event.inputs.build_web == 'true'
|
||||
if: inputs.build_web
|
||||
run: flutter build web --release --base-href "/swiftcontrol/"
|
||||
|
||||
- name: Upload static files as artifact
|
||||
if: github.event.inputs.build_web == 'true'
|
||||
if: inputs.build_web
|
||||
id: deployment
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: build/web
|
||||
|
||||
- name: Web Deploy
|
||||
if: github.event.inputs.build_web == 'true'
|
||||
if: inputs.build_web
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
- name: Extract latest changelog
|
||||
@@ -142,7 +153,7 @@ jobs:
|
||||
./scripts/get_latest_changelog.sh | head -c 500 > whatsnew/whatsnew-en-US
|
||||
|
||||
- name: 🚀 Shorebird Release iOS
|
||||
if: github.event.inputs.build_ios == 'true'
|
||||
if: inputs.build_ios
|
||||
uses: shorebirdtech/shorebird-release@v1
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
@@ -150,7 +161,7 @@ jobs:
|
||||
args: "--export-options-plist ios/ExportOptions.plist"
|
||||
|
||||
- name: Prepare App Store authentication key
|
||||
if: github.event.inputs.build_ios == 'true' || github.event.inputs.build_mac == 'true'
|
||||
if: inputs.build_ios || inputs.build_mac
|
||||
env:
|
||||
API_KEY_BASE64: ${{ secrets.APPSTORE_API_KEY_FILE_BASE64 }}
|
||||
APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }}
|
||||
@@ -159,7 +170,7 @@ jobs:
|
||||
printf %s "$API_KEY_BASE64" | base64 -D > "./private_keys/AuthKey_${APPSTORE_API_KEY}.p8";
|
||||
|
||||
- name: Upload to Play Store
|
||||
if: github.event.inputs.build_android == 'true'
|
||||
if: inputs.build_android
|
||||
uses: r0adkll/upload-google-play@v1
|
||||
with:
|
||||
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
|
||||
@@ -169,7 +180,7 @@ jobs:
|
||||
whatsNewDirectory: whatsnew
|
||||
|
||||
- name: Upload to macOS App Store
|
||||
if: github.event.inputs.build_mac == 'true'
|
||||
if: inputs.build_mac
|
||||
env:
|
||||
APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }}
|
||||
APPSTORE_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }}
|
||||
@@ -178,7 +189,7 @@ jobs:
|
||||
xcrun altool --upload-app -f SwiftControl.pkg -t osx --apiKey "$APPSTORE_API_KEY" --apiIssuer "$APPSTORE_API_ISSUER_ID";
|
||||
|
||||
- name: Upload to iOS App Store
|
||||
if: github.event.inputs.build_ios == 'true'
|
||||
if: inputs.build_ios
|
||||
env:
|
||||
APPSTORE_API_KEY: ${{ secrets.APPSTORE_API_KEY }}
|
||||
APPSTORE_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }}
|
||||
@@ -186,78 +197,64 @@ jobs:
|
||||
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: github.event.inputs.build_android == 'true'
|
||||
if: inputs.build_android && inputs.build_github
|
||||
run: |
|
||||
cp build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/SwiftControl.android.apk
|
||||
|
||||
- name: Code Signing of macOS app
|
||||
if: github.event.inputs.build_mac == 'true'
|
||||
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 SwiftControl.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: github.event.inputs.build_mac == 'true'
|
||||
if: inputs.build_mac && inputs.build_github
|
||||
run: |
|
||||
cd build/macos/Build/Products/Release/
|
||||
zip -r SwiftControl.macos.zip SwiftControl.app/
|
||||
|
||||
- name: Upload Android Artifacts
|
||||
if: github.event.inputs.build_android == 'true'
|
||||
if: inputs.build_android && inputs.build_github
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
overwrite: true
|
||||
name: Releases
|
||||
path: |
|
||||
build/app/outputs/flutter-apk/SwiftControl.android.apk
|
||||
|
||||
- name: Upload macOS Artifacts
|
||||
if: github.event.inputs.build_mac == 'true'
|
||||
if: inputs.build_mac && inputs.build_github
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
overwrite: true
|
||||
name: Releases
|
||||
path: |
|
||||
build/macos/Build/Products/Release/SwiftControl.macos.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
|
||||
|
||||
#11 Check if Tag Exists
|
||||
- name: Check if Tag Exists
|
||||
id: check_tag
|
||||
run: |
|
||||
if git rev-parse "v${{ env.VERSION }}" >/dev/null 2>&1; then
|
||||
echo "TAG_EXISTS=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "TAG_EXISTS=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
#12 Modify Tag if it Exists
|
||||
- name: Modify Tag
|
||||
if: env.TAG_EXISTS == 'true'
|
||||
id: modify_tag
|
||||
run: |
|
||||
new_version="${{ env.VERSION }}-build-${{ github.run_number }}"
|
||||
echo "VERSION=$new_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/SwiftControl.android.apk,build/macos/Build/Products/Release/SwiftControl.macos.zip"
|
||||
allowUpdates: true
|
||||
prerelease: ${{ endsWith(env.VERSION, '1337') }}
|
||||
prerelease: true
|
||||
bodyFile: scripts/RELEASE_NOTES.md
|
||||
tag: v${{ env.VERSION }}
|
||||
token: ${{ secrets.TOKEN }}
|
||||
|
||||
windows:
|
||||
needs: build
|
||||
if: github.event.inputs.build_windows == 'true'
|
||||
if: inputs.build_windows
|
||||
name: Build & Release on Windows
|
||||
runs-on: windows-latest
|
||||
|
||||
@@ -266,13 +263,6 @@ jobs:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
#2 Setup Java
|
||||
- name: Set Up Java
|
||||
uses: actions/setup-java@v3.12.0
|
||||
with:
|
||||
distribution: 'oracle'
|
||||
java-version: '17'
|
||||
|
||||
- name: 🐦 Setup Shorebird
|
||||
uses: shorebirdtech/setup-shorebird@v1
|
||||
with:
|
||||
@@ -308,7 +298,31 @@ jobs:
|
||||
}
|
||||
Compress-Archive -Path "build/windows/x64/runner/Release/*" -DestinationPath "build/windows/x64/runner/Release/SwiftControl.windows.zip"
|
||||
|
||||
#9 Upload Artifacts
|
||||
- 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 SwiftControl.windows.msix
|
||||
shell: pwsh
|
||||
run: |
|
||||
Rename-Item -Path "build/windows/x64/runner/Release/swift_control.msix" -NewName "SwiftControl.windows.msix"
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -316,8 +330,8 @@ jobs:
|
||||
name: Releases
|
||||
path: |
|
||||
build/windows/x64/runner/Release/SwiftControl.windows.zip
|
||||
build/windows/x64/runner/Release/SwiftControl.windows.msix
|
||||
|
||||
#10 Extract Version
|
||||
- name: Extract version from pubspec.yaml (Windows)
|
||||
shell: pwsh
|
||||
run: |
|
||||
@@ -326,13 +340,13 @@ jobs:
|
||||
}
|
||||
echo "VERSION=$version" >> $env:GITHUB_ENV
|
||||
|
||||
# add artifact to release
|
||||
|
||||
- name: Create Release
|
||||
- name: Update Release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
allowUpdates: true
|
||||
artifacts: "build/windows/x64/runner/Release/SwiftControl.windows.zip"
|
||||
artifacts: "build/windows/x64/runner/Release/SwiftControl.windows.zip,build/windows/x64/runner/Release/SwiftControl.windows.msix"
|
||||
bodyFile: scripts/RELEASE_NOTES.md
|
||||
prerelease: true
|
||||
tag: v${{ env.VERSION }}
|
||||
token: ${{ secrets.TOKEN }}
|
||||
|
||||
17
.github/workflows/patch.yml
vendored
@@ -75,25 +75,26 @@ jobs:
|
||||
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'
|
||||
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'
|
||||
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'
|
||||
args: '--allow-asset-diffs --allow-native-diffs'
|
||||
|
||||
- name: Set Up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
@@ -103,15 +104,17 @@ jobs:
|
||||
|
||||
# shorebird struggles with the app from GitHub
|
||||
- name: Build macOS
|
||||
run: flutter build macos --release;
|
||||
|
||||
- name: Sign macOS build
|
||||
env:
|
||||
DEVELOPER_ID_APPLICATION_SIGNING_IDENTITY: ${{ secrets.DEVELOPER_ID_APPLICATION_SIGNING_IDENTITY }}
|
||||
run: |
|
||||
flutter build macos --release;
|
||||
cd build/macos/Build/Products/Release/;
|
||||
zip -r SwiftControl.macos.zip SwiftControl.app/;
|
||||
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 SwiftControl.app -v;
|
||||
zip -r SwiftControl.macos.zip SwiftControl.app/;
|
||||
|
||||
#9 Upload Artifacts
|
||||
- name: Upload Artifacts
|
||||
@@ -158,4 +161,4 @@ jobs:
|
||||
with:
|
||||
platform: windows
|
||||
release-version: latest
|
||||
args: '--allow-asset-diffs'
|
||||
args: '--allow-asset-diffs --allow-native-diffs'
|
||||
|
||||
2
.github/workflows/web.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: "Build"
|
||||
name: "Build Web"
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
1
.gitignore
vendored
@@ -10,6 +10,7 @@
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
debug/
|
||||
migrate_working_dir/
|
||||
|
||||
android/keystore.properties
|
||||
|
||||
41
CHANGELOG.md
@@ -1,7 +1,42 @@
|
||||
### 3.0.4 (not released yet)
|
||||
### 3.3.1 (unreleased)
|
||||
**New Features:**
|
||||
- Support for Shimano Di2
|
||||
|
||||
**Fixes:**
|
||||
- fix detection of Elite Square Sterzo devices
|
||||
|
||||
### 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
|
||||
- SwiftControl 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)
|
||||
- initial support for Wahook Kickr Bike Shift (thanks @MattW2)
|
||||
- initial support for Elite Square Smart Frame
|
||||
- support for Wahook Kickr Bike Shift (thanks @MattW2)
|
||||
- initial support for Elite Square Smart Frame
|
||||
- reconnects to your device automatically when connection is lost
|
||||
- SwiftControl now warns you if your device firmware is outdated
|
||||
- SwiftControl is now available in Microsoft Store: https://apps.microsoft.com/detail/9NP42GS03Z26
|
||||
|
||||
### 3.0.3 (2025-10-12)
|
||||
- SwiftControl now supports iOS!
|
||||
|
||||
1
INSTRUCTIONS_ANDROID.md
Normal file
@@ -0,0 +1 @@
|
||||
Instructions will be added soon
|
||||
10
INSTRUCTIONS_IOS.md
Normal file
@@ -0,0 +1,10 @@
|
||||
**Instructions for using the MyWhoosh Link method**
|
||||
1) launch MyWhoosh on the device of your choice
|
||||
2) launch MyWhoosh Link, check if the "Link" connection works
|
||||
3) close MyWhoosh Link, open SwiftControl, follow on screen instructions
|
||||
Step 2 is not necessary after that once step 3 works
|
||||
|
||||
And here's a video with a few explanations:
|
||||
|
||||
[](https://www.youtube.com/watch?v=p8sgQhuufeI)
|
||||
[https://www.youtube.com/watch?v=p8sgQhuufeI](https://www.youtube.com/watch?v=p8sgQhuufeI)
|
||||
1
INSTRUCTIONS_MACOS.md
Normal file
@@ -0,0 +1 @@
|
||||
Instructions will be added soon
|
||||
1
INSTRUCTIONS_WINDOWS.md
Normal file
@@ -0,0 +1 @@
|
||||
Instructions will be added soon
|
||||
65
README.md
@@ -1,15 +1,15 @@
|
||||
# SwiftControl
|
||||
|
||||
<img src="logo.jpg" alt="SwiftControl Logo"/>
|
||||
<img src="logo.png" alt="SwiftControl Logo"/>
|
||||
|
||||
## Description
|
||||
|
||||
With SwiftControl you can **control your favorite trainer app** using your Zwift Click, Zwift Ride or Zwift Play devices. Here's what you can do with it, depending on your configuration:
|
||||
With SwiftControl 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
|
||||
- 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 SwiftControl
|
||||
- more? If you can do it via keyboard, mouse, or touch, you can do it with SwiftControl
|
||||
|
||||
|
||||
https://github.com/user-attachments/assets/1f81b674-1628-4763-ad66-5f3ed7a3f159
|
||||
@@ -21,56 +21,73 @@ https://github.com/user-attachments/assets/1f81b674-1628-4763-ad66-5f3ed7a3f159
|
||||
Check the compatibility matrix below!
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=de.jonasbark.swiftcontrol"><img width="270" height="80" alt="GetItOnGooglePlay_Badge_Web_color_English" src="https://github.com/user-attachments/assets/a059d5a1-2efb-4f65-8117-ef6a99823b21" /></a>
|
||||
<a href="https://apps.apple.com/us/app/swiftcontrol/id6753721284?platform=iphone"><img width="270" height="80" alt="App Store" src="https://github.com/user-attachments/assets/c23f977a-48f6-4951-811e-ae530dbfa014" /></a>
|
||||
|
||||
<a href="https://apps.apple.com/us/app/swiftcontrol/id6753721284?platform=iphone"><img width="270" alt="App Store" src="https://github.com/user-attachments/assets/c23f977a-48f6-4951-811e-ae530dbfa014" /></a>
|
||||
|
||||
<a href="https://apps.apple.com/us/app/swiftcontrol/id6753721284?platform=mac"><img width="270" height="80" alt="Mac App Store" src="https://github.com/user-attachments/assets/b3552436-409c-43b0-ba7d-b6a72ae30ff1" /></a>
|
||||
|
||||
|
||||
Get the latest version for Windows here: https://github.com/jonasbark/swiftcontrol/releases
|
||||
<a href="https://apps.microsoft.com/detail/9NP42GS03Z26"><img width="270" alt="Microsoft Store" src="https://github.com/user-attachments/assets/7a8a3cd6-ec26-4678-a850-732eedd27c48" /></a>
|
||||
|
||||
## Supported Apps
|
||||
- MyWhoosh
|
||||
- TrainingPeaks Virtual / indieVelo
|
||||
- Biketerra.com (they do offer native integration already - check it out)
|
||||
- Rouvy (most Zwift devices are already supported by Rouvy)
|
||||
- any other! You can add custom mapping and adjust touch points or keyboard shortcuts to your liking
|
||||
- Biketerra.com
|
||||
- Rouvy
|
||||
- Zwift
|
||||
- running SwiftControl 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)
|
||||
- Wahoo Kickr Bike Shift (beta)
|
||||
- Gamepads (beta)
|
||||
- Cheap Bluetooth buttons such as [these](https://www.amazon.com/s?k=bluetooth+remote) (beta)
|
||||
- works on Android
|
||||
- on iOS, macOS and Windows it would require SwiftControl to act as media player - let me know if that works for you
|
||||
|
||||
Support for other devices can be added; check the issues tab here on GitHub.
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
| Platform you want to run your Trainer app, e.g. MyWhoosh on | Possible | Link | Information |
|
||||
Follow this compatibility matrix. It all depends on where you want to run your trainer app (e.g. MyWhoosh on):
|
||||
|
||||
| Run Trainer app (MyWhoosh, ...) on: | Possible | Link | Information |
|
||||
|-------------------------------------------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Android | ✅ | <a href="https://play.google.com/store/apps/details?id=de.jonasbark.swiftcontrol"><img width="270" height="80" alt="GetItOnGooglePlay_Badge_Web_color_English" src="https://github.com/user-attachments/assets/a059d5a1-2efb-4f65-8117-ef6a99823b21" /></a> | |
|
||||
| iPad (and possibly Apple TV) | ✅ | <a href="https://apps.apple.com/us/app/swiftcontrol/id6753721284?platform=iphone"><img width="270" height="80" alt="App Store" src="https://github.com/user-attachments/assets/c23f977a-48f6-4951-811e-ae530dbfa014" /></a> | You will need to use SwiftControl as a "remote" to control the trainer app on your iPad. Typically you would use an iPhone or an Android phone for that. |
|
||||
| Windows | ✅ | [Get it here](https://github.com/jonasbark/swiftcontrol/releases) | - Windows may flag the app as virus. It likely does so because the app controls the mouse and keyboard.<br>- Bluetooth connection unstable? You may need to use an [external Bluetooth adapter](https://github.com/jonasbark/swiftcontrol/issues/14#issuecomment-3193839509).<br>- Make sure your Zwift device is not paired with Windows Bluetooth settings: [more information](https://github.com/jonasbark/swiftcontrol/issues/70). |
|
||||
| iPad | ✅ | <a href="https://apps.apple.com/us/app/swiftcontrol/id6753721284?platform=iphone"><img width="270" height="80" alt="App Store" src="https://github.com/user-attachments/assets/c23f977a-48f6-4951-811e-ae530dbfa014" /></a> | You will need to use SwiftControl as a "remote" to control the trainer app on your iPad. Typically, you would use an iPhone or an Android phone for that. |
|
||||
| Windows | ✅ | <a href="https://apps.microsoft.com/detail/9NP42GS03Z26"><img width="270" alt="Microsoft Store" src="https://github.com/user-attachments/assets/7a8a3cd6-ec26-4678-a850-732eedd27c48" /></a> | - Windows may flag the app as virus. It likely does so because the app controls the mouse and keyboard.<br>- Bluetooth connection unstable? You may need to use an [external Bluetooth adapter](https://github.com/jonasbark/swiftcontrol/issues/14#issuecomment-3193839509).<br>- Make sure your Zwift device is not paired with Windows Bluetooth settings: [more information](https://github.com/jonasbark/swiftcontrol/issues/70). |
|
||||
| macOS | ✅ | <a href="https://apps.apple.com/us/app/swiftcontrol/id6753721284?platform=mac"><img width="270" height="80" alt="Mac App Store" src="https://github.com/user-attachments/assets/b3552436-409c-43b0-ba7d-b6a72ae30ff1" /></a> | |
|
||||
| iPhone | ❌ | | Note that you can't run SwiftControl and your trainer app on the same iPhone due to iOS limitations, but you can use it to remotely control MyWhoosh and similar on e.g. an iPad. |
|
||||
| iPhone | (✅) | <a href="https://apps.apple.com/us/app/swiftcontrol/id6753721284?platform=iphone"><img width="270" height="80" alt="App Store" src="https://github.com/user-attachments/assets/c23f977a-48f6-4951-811e-ae530dbfa014" /></a> | Note that you can't run SwiftControl and your trainer app on the same iPhone due to iOS limitations, but you could use the Link method on another device to control MyWhoosh (and only MyWhoosh) on an iPhone. |
|
||||
| Apple TV | (✅*) | | *only MyWhoosh using the Link method is supported - but you cannot also use MyWhoosh Link at the same time |
|
||||
|
||||
|
||||
For testing purposes you can also run it on [Web](https://jonasbark.github.io/swiftcontrol/) but this is just a tech demo - you won't be able to control other apps.
|
||||
For testing purposes, you can also run it on [Web](https://jonasbark.github.io/swiftcontrol/), but this is just a tech demo - you won't be able to control other apps.
|
||||
|
||||
## Troubleshooting
|
||||
Check the troubleshooting guide [here](TROUBLESHOOTING.md).
|
||||
|
||||
## How does it work?
|
||||
The app connects to your Zwift devices automatically. It does not connect to your trainer itself.
|
||||
The app connects to your Controller devices (such as Zwift ones) automatically. It does not connect to your trainer itself.
|
||||
|
||||
- **Android**: SwiftControl 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 SwiftControl as "remote control" for other devices, such as an iPad. Example scenario:
|
||||
- your phone (Android/iOS) runs SwiftControl and connects to your Zwift devices
|
||||
- **iOS**: use SwiftControl as a "remote control" for other devices, such as an iPad. Example scenario:
|
||||
- your phone (Android/iOS) runs SwiftControl and connects to your Controller devices
|
||||
- your iPad or other tablet runs e.g. MyWhoosh (does not need to have SwiftControl installed)
|
||||
- after pairing SwiftControl to your iPad / tablet via Bluetooth your phone will send the button presses to your iPad / tablet
|
||||
- If you want to use MyWhoosh, you can use the Link method to directly connect to MyWhoosh
|
||||
- For other trainer apps, you need to pair SwiftControl 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
|
||||
</details>
|
||||
- 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.
|
||||
|
||||
@@ -15,6 +15,13 @@ If you don't do that SwiftControl will need to reconnect every minute.
|
||||
3. Connect your Trainer, then connect the Click V2
|
||||
4. Close the Zwift app again and connect again in SwiftControl
|
||||
|
||||
## 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 SwiftControl
|
||||
- enable auto start of SwiftControl
|
||||
- grant accessibility permission for SwiftControl
|
||||
- see [https://github.com/jonasbark/swiftcontrol/issues/38](https://github.com/jonasbark/swiftcontrol/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 SwiftControl
|
||||
@@ -26,3 +33,20 @@ If you don't do that SwiftControl will need to reconnect every minute.
|
||||
iOS seems to be buggy here - try this in the iOS settings:
|
||||
AssistiveTouch settings > Pointer Devices > Devices > Connected Devices > iPhone (or SwiftControl iOS) > Button 1
|
||||
switch the setting to None, then back to Single-Tap and it should work again
|
||||
|
||||
## SwiftControl crashes on Windows when searching for the device
|
||||
You're probably running into [this](https://github.com/jonasbark/swiftcontrol/issues/70) issue. Disconnect your controller device (e.g. Zwift Play) from Windows Bluetooth settings.
|
||||
|
||||
## Link requirement for MyWhoosh stuck at "Waiting for MyWhoosh"
|
||||
The same network restrictions apply for SwiftControl 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/)
|
||||
|
||||
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
|
||||
|
||||
|
||||
1
WINDOWS_STORE_VERSION.txt
Normal file
@@ -0,0 +1 @@
|
||||
3.3.0
|
||||
@@ -5,6 +5,7 @@ import android.accessibilityservice.GestureDescription
|
||||
import android.accessibilityservice.GestureDescription.StrokeDescription
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.ViewConfiguration
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
@@ -56,7 +57,12 @@ class AccessibilityService : AccessibilityService(), Listener {
|
||||
path.moveTo(x.toFloat(), y.toFloat())
|
||||
path.lineTo(x.toFloat()+1, y.toFloat())
|
||||
|
||||
val stroke = StrokeDescription(path, 0, ViewConfiguration.getTapTimeout().toLong(), isKeyDown && !isKeyUp)
|
||||
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)
|
||||
|
||||
@@ -1,5 +1,46 @@
|
||||
package de.jonasbark.swiftcontrol
|
||||
|
||||
import android.hardware.input.InputManager
|
||||
import android.os.Handler
|
||||
import android.view.InputDevice
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import org.flame_engine.gamepads_android.GamepadsCompatibleActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
class MainActivity: FlutterActivity(), GamepadsCompatibleActivity {
|
||||
var keyListener: ((KeyEvent) -> 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
android:src="@mipmap/ic_launcher" />
|
||||
</item>
|
||||
</layer-list>
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
android:src="@mipmap/ic_launcher" />
|
||||
</item>
|
||||
</layer-list>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 8.0 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 4.3 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 125 KiB |
@@ -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: true
|
||||
# image_path_ios: "assets/icon/icon.png"
|
||||
remove_alpha_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: "#ffffff"
|
||||
theme_color: "#ffffff"
|
||||
|
||||
windows:
|
||||
generate: true
|
||||
image_path: "icon.png"
|
||||
icon_size: 48 # min:48, max:256, default: 48
|
||||
|
||||
macos:
|
||||
generate: true
|
||||
image_path: "icon.png"
|
||||
BIN
icon.png
|
Before Width: | Height: | Size: 555 KiB After Width: | Height: | Size: 26 KiB |
@@ -7,13 +7,16 @@ PODS:
|
||||
- Flutter (1.0.0)
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- Flutter
|
||||
- gamepads_ios (0.1.1):
|
||||
- Flutter
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- permission_handler_apple (9.3.0):
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- restart (1.0.0):
|
||||
- FlutterMacOS
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- restart_app (0.0.1):
|
||||
- Flutter
|
||||
@@ -33,10 +36,11 @@ DEPENDENCIES:
|
||||
- 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`)
|
||||
- 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 (from `.symlinks/plugins/restart/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`)
|
||||
@@ -52,14 +56,16 @@ EXTERNAL SOURCES:
|
||||
: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"
|
||||
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:
|
||||
:path: ".symlinks/plugins/restart/ios"
|
||||
restart_app:
|
||||
:path: ".symlinks/plugins/restart_app/ios"
|
||||
shared_preferences_foundation:
|
||||
@@ -76,10 +82,11 @@ SPEC CHECKSUMS:
|
||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f
|
||||
gamepads_ios: 1d2930c7a4450a9a1b57444ebf305a6a6cbeea0b
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
restart: b5fe16e6e038f0024b2f3af43768e9d2a1557554
|
||||
restart_app: 806659942bf932f6ce51c5372f91ce5e81c8c14a
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
|
||||
|
||||
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 866 B |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 35 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 30 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
@@ -1 +1,134 @@
|
||||
{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"filename": "AppIcon@2x.png",
|
||||
"idiom": "iphone",
|
||||
"scale": "2x",
|
||||
"size": "60x60"
|
||||
},
|
||||
{
|
||||
"filename": "AppIcon@3x.png",
|
||||
"idiom": "iphone",
|
||||
"scale": "3x",
|
||||
"size": "60x60"
|
||||
},
|
||||
{
|
||||
"filename": "AppIcon~ipad.png",
|
||||
"idiom": "ipad",
|
||||
"scale": "1x",
|
||||
"size": "76x76"
|
||||
},
|
||||
{
|
||||
"filename": "AppIcon@2x~ipad.png",
|
||||
"idiom": "ipad",
|
||||
"scale": "2x",
|
||||
"size": "76x76"
|
||||
},
|
||||
{
|
||||
"filename": "AppIcon-83.5@2x~ipad.png",
|
||||
"idiom": "ipad",
|
||||
"scale": "2x",
|
||||
"size": "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename": "AppIcon-40@2x.png",
|
||||
"idiom": "iphone",
|
||||
"scale": "2x",
|
||||
"size": "40x40"
|
||||
},
|
||||
{
|
||||
"filename": "AppIcon-40@3x.png",
|
||||
"idiom": "iphone",
|
||||
"scale": "3x",
|
||||
"size": "40x40"
|
||||
},
|
||||
{
|
||||
"filename": "AppIcon-40~ipad.png",
|
||||
"idiom": "ipad",
|
||||
"scale": "1x",
|
||||
"size": "40x40"
|
||||
},
|
||||
{
|
||||
"filename": "AppIcon-40@2x~ipad.png",
|
||||
"idiom": "ipad",
|
||||
"scale": "2x",
|
||||
"size": "40x40"
|
||||
},
|
||||
{
|
||||
"filename": "AppIcon-20@2x.png",
|
||||
"idiom": "iphone",
|
||||
"scale": "2x",
|
||||
"size": "20x20"
|
||||
},
|
||||
{
|
||||
"filename": "AppIcon-20@3x.png",
|
||||
"idiom": "iphone",
|
||||
"scale": "3x",
|
||||
"size": "20x20"
|
||||
},
|
||||
{
|
||||
"filename": "AppIcon-20~ipad.png",
|
||||
"idiom": "ipad",
|
||||
"scale": "1x",
|
||||
"size": "20x20"
|
||||
},
|
||||
{
|
||||
"filename": "AppIcon-20@2x~ipad.png",
|
||||
"idiom": "ipad",
|
||||
"scale": "2x",
|
||||
"size": "20x20"
|
||||
},
|
||||
{
|
||||
"filename": "AppIcon-29.png",
|
||||
"idiom": "iphone",
|
||||
"scale": "1x",
|
||||
"size": "29x29"
|
||||
},
|
||||
{
|
||||
"filename": "AppIcon-29@2x.png",
|
||||
"idiom": "iphone",
|
||||
"scale": "2x",
|
||||
"size": "29x29"
|
||||
},
|
||||
{
|
||||
"filename": "AppIcon-29@3x.png",
|
||||
"idiom": "iphone",
|
||||
"scale": "3x",
|
||||
"size": "29x29"
|
||||
},
|
||||
{
|
||||
"filename": "AppIcon-29~ipad.png",
|
||||
"idiom": "ipad",
|
||||
"scale": "1x",
|
||||
"size": "29x29"
|
||||
},
|
||||
{
|
||||
"filename": "AppIcon-29@2x~ipad.png",
|
||||
"idiom": "ipad",
|
||||
"scale": "2x",
|
||||
"size": "29x29"
|
||||
},
|
||||
{
|
||||
"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": {
|
||||
"author": "iconkitchen",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 880 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 37 KiB |
6
ios/Runner/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/AppIcon-min 1.png
vendored
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/AppIcon-min 2.png
vendored
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/AppIcon-min.png
vendored
Normal file
|
After Width: | Height: | Size: 212 KiB |
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 68 B |
|
Before Width: | Height: | Size: 68 B |
|
Before Width: | Height: | Size: 68 B |
@@ -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.
|
||||
@@ -1,8 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
@@ -14,9 +16,11 @@
|
||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
<rect key="frame" x="111.33333333333333" y="340.66666666666669" width="170.66666666666669" height="170.66666666666669"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
@@ -28,10 +32,10 @@
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
<point key="canvasLocation" x="80.916030534351137" y="264.08450704225356"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
<image name="LaunchImage" width="170.66667175292969" height="170.66667175292969"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>This app connects to your trainer app on your local network.</string>
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
<string>SwiftControl uses Bluetooth to connect to accessories.</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
|
||||
@@ -67,11 +67,11 @@ public class KeypressSimulatorMacosPlugin: NSObject, FlutterPlugin {
|
||||
let point = CGPoint(x: x, y: y)
|
||||
|
||||
// Move mouse to the point
|
||||
/*let move = CGEvent(mouseEventSource: nil,
|
||||
let move = CGEvent(mouseEventSource: nil,
|
||||
mouseType: .mouseMoved,
|
||||
mouseCursorPosition: point,
|
||||
mouseButton: .left)
|
||||
move?.post(tap: .cghidEventTap)*/
|
||||
move?.post(tap: .cghidEventTap)
|
||||
|
||||
if (keyDown) {
|
||||
// Mouse down
|
||||
|
||||
@@ -1,126 +1,7 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class BleUuid {
|
||||
static final DEVICE_INFORMATION_SERVICE_UUID = "0000180a-0000-1000-8000-00805f9b34fb".toLowerCase();
|
||||
static final DEVICE_INFORMATION_CHARACTERISTIC_FIRMWARE_REVISION = "00002a26-0000-1000-8000-00805f9b34fb"
|
||||
.toLowerCase();
|
||||
static const DEVICE_INFORMATION_SERVICE_UUID = "0000180a-0000-1000-8000-00805f9b34fb";
|
||||
static const DEVICE_INFORMATION_CHARACTERISTIC_FIRMWARE_REVISION = "00002a26-0000-1000-8000-00805f9b34fb";
|
||||
|
||||
static final DEVICE_BATTERY_SERVICE_UUID = "0000180f-0000-1000-8000-00805f9b34fb".toLowerCase();
|
||||
static final DEVICE_INFORMATION_CHARACTERISTIC_BATTERY_LEVEL = "00002a19-0000-1000-8000-00805f9b34fb".toLowerCase();
|
||||
static final ZWIFT_CUSTOM_SERVICE_UUID = "00000001-19CA-4651-86E5-FA29DCDD09D1".toLowerCase();
|
||||
static final ZWIFT_RIDE_CUSTOM_SERVICE_UUID = "0000fc82-0000-1000-8000-00805f9b34fb".toLowerCase();
|
||||
static final ZWIFT_ASYNC_CHARACTERISTIC_UUID = "00000002-19CA-4651-86E5-FA29DCDD09D1".toLowerCase();
|
||||
static final ZWIFT_SYNC_RX_CHARACTERISTIC_UUID = "00000003-19CA-4651-86E5-FA29DCDD09D1".toLowerCase();
|
||||
static final ZWIFT_SYNC_TX_CHARACTERISTIC_UUID = "00000004-19CA-4651-86E5-FA29DCDD09D1".toLowerCase();
|
||||
}
|
||||
|
||||
class Constants {
|
||||
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([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
|
||||
static final RESPONSE_START_CLICK_V2 = Uint8List.fromList([0x02, 0x03]); // from device
|
||||
static final RESPONSE_STOPPED_CLICK_V2 = Uint8List.fromList([
|
||||
0xff,
|
||||
0x05,
|
||||
0x00,
|
||||
0xea,
|
||||
0x05,
|
||||
0x19,
|
||||
0x0a,
|
||||
0x0c,
|
||||
0x35,
|
||||
0x38,
|
||||
0x44,
|
||||
0x31,
|
||||
0x35,
|
||||
0x41,
|
||||
0x42,
|
||||
0x42,
|
||||
0x34,
|
||||
0x33,
|
||||
0x36,
|
||||
0x33,
|
||||
0x10,
|
||||
0x01,
|
||||
0x18,
|
||||
0x84,
|
||||
0x07,
|
||||
0x20,
|
||||
0x08,
|
||||
0x28,
|
||||
0x09,
|
||||
0x30,
|
||||
]); // 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;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
enum DeviceType {
|
||||
click,
|
||||
clickV2Right,
|
||||
clickV2Left,
|
||||
playLeft,
|
||||
playRight,
|
||||
rideRight,
|
||||
rideLeft;
|
||||
|
||||
@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.CLICK_V2_RIGHT_SIDE:
|
||||
return DeviceType.clickV2Right;
|
||||
case Constants.CLICK_V2_LEFT_SIDE:
|
||||
return DeviceType.clickV2Left;
|
||||
case Constants.RC1_LEFT_SIDE:
|
||||
return DeviceType.playLeft;
|
||||
case Constants.RC1_RIGHT_SIDE:
|
||||
return DeviceType.playRight;
|
||||
case Constants.RIDE_RIGHT_SIDE:
|
||||
return DeviceType.rideRight;
|
||||
case Constants.RIDE_LEFT_SIDE:
|
||||
return DeviceType.rideLeft;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
static const DEVICE_BATTERY_SERVICE_UUID = "0000180f-0000-1000-8000-00805f9b34fb";
|
||||
static const DEVICE_INFORMATION_CHARACTERISTIC_BATTERY_LEVEL = "00002A19-0000-1000-8000-00805F9B34FB";
|
||||
}
|
||||
|
||||
@@ -1,20 +1,33 @@
|
||||
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:swift_control/bluetooth/devices/bluetooth_device.dart';
|
||||
import 'package:swift_control/bluetooth/devices/gamepad/gamepad_device.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/actions/android.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 '../bluetooth/ble.dart';
|
||||
import '../utils/keymap/apps/my_whoosh.dart';
|
||||
import 'devices/base_device.dart';
|
||||
import 'devices/link/link_device.dart';
|
||||
import 'devices/zwift/constants.dart';
|
||||
import 'messages/notification.dart';
|
||||
|
||||
class Connection {
|
||||
final devices = <BaseDevice>[];
|
||||
|
||||
List<BluetoothDevice> get bluetoothDevices => devices.whereType<BluetoothDevice>().toList();
|
||||
List<GamepadDevice> get gamepadDevices => devices.whereType<GamepadDevice>().toList();
|
||||
List<BaseDevice> get controllerDevices => [...bluetoothDevices, ...gamepadDevices];
|
||||
List<BaseDevice> get remoteDevices => devices.whereNot((d) => d is BluetoothDevice || d is GamepadDevice).toList();
|
||||
|
||||
var _androidNotificationsSetup = false;
|
||||
|
||||
final _connectionQueue = <BaseDevice>[];
|
||||
@@ -32,11 +45,35 @@ class Connection {
|
||||
final ValueNotifier<bool> hasDevices = ValueNotifier(false);
|
||||
final ValueNotifier<bool> isScanning = ValueNotifier(false);
|
||||
|
||||
Timer? _gamePadSearchTimer;
|
||||
|
||||
void initialize() {
|
||||
UniversalBle.onAvailabilityChange = (available) {
|
||||
_actionStreams.add(BluetoothAvailabilityNotification(available == AvailabilityState.poweredOn));
|
||||
if (available == AvailabilityState.poweredOn && !kIsWeb) {
|
||||
performScanning();
|
||||
} else if (available == AvailabilityState.poweredOff) {
|
||||
reset();
|
||||
}
|
||||
};
|
||||
UniversalBle.onScanResult = (result) {
|
||||
if (_lastScanResult.none((e) => e.deviceId == result.deviceId)) {
|
||||
// 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);
|
||||
final scanResult = BaseDevice.fromScanResult(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}'));
|
||||
@@ -44,15 +81,17 @@ class Connection {
|
||||
} else {
|
||||
final manufacturerData = result.manufacturerDataList;
|
||||
final data = manufacturerData
|
||||
.firstOrNullWhere((e) => e.companyId == Constants.ZWIFT_MANUFACTURER_ID)
|
||||
.firstOrNullWhere((e) => e.companyId == ZwiftConstants.ZWIFT_MANUFACTURER_ID)
|
||||
?.payload;
|
||||
_actionStreams.add(LogNotification('Found unknown device with identifier: ${data?.firstOrNull}'));
|
||||
if (data != null && kDebugMode) {
|
||||
_actionStreams.add(LogNotification('Found unknown device with identifier: ${data.firstOrNull}'));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
UniversalBle.onValueChange = (deviceId, characteristicUuid, value) {
|
||||
final device = devices.firstOrNullWhere((e) => e.device.deviceId == deviceId);
|
||||
final device = bluetoothDevices.firstOrNullWhere((e) => e.device.deviceId == deviceId);
|
||||
if (device == null) {
|
||||
_actionStreams.add(LogNotification('Device not found: $deviceId'));
|
||||
UniversalBle.disconnect(deviceId);
|
||||
@@ -61,18 +100,29 @@ class Connection {
|
||||
device.processCharacteristic(characteristicUuid, value);
|
||||
}
|
||||
};
|
||||
|
||||
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<void> 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: BaseDevice.servicesToScan,
|
||||
withServices: BluetoothDevice.servicesToScan,
|
||||
).then((devices) async {
|
||||
final baseDevices = devices.mapNotNull(BaseDevice.fromScanResult).toList();
|
||||
final baseDevices = devices.mapNotNull(BluetoothDevice.fromScanResult).toList();
|
||||
if (baseDevices.isNotEmpty) {
|
||||
_addDevices(baseDevices);
|
||||
}
|
||||
@@ -80,22 +130,68 @@ class Connection {
|
||||
}
|
||||
|
||||
await UniversalBle.startScan(
|
||||
scanFilter: ScanFilter(withServices: BaseDevice.servicesToScan),
|
||||
platformConfig: PlatformConfig(web: WebOptions(optionalServices: BaseDevice.servicesToScan)),
|
||||
// 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) {
|
||||
startMyWhooshServer();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> startMyWhooshServer() {
|
||||
return whooshLink.startServer(
|
||||
onConnected: (socket) {
|
||||
final existing = remoteDevices.firstOrNullWhere(
|
||||
(e) => e is LinkDevice && e.identifier == socket.remoteAddress.address,
|
||||
);
|
||||
if (existing != null) {
|
||||
existing.isConnected = true;
|
||||
signalChange(existing);
|
||||
}
|
||||
},
|
||||
onDisconnected: (socket) {
|
||||
final device = devices.firstOrNullWhere(
|
||||
(device) => device is LinkDevice && device.identifier == socket.remoteAddress.address,
|
||||
);
|
||||
if (device != null) {
|
||||
devices.remove(device);
|
||||
signalChange(device);
|
||||
}
|
||||
},
|
||||
);
|
||||
Future.delayed(Duration(seconds: 30)).then((_) {
|
||||
if (isScanning.value) {
|
||||
UniversalBle.stopScan();
|
||||
isScanning.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _addDevices(List<BaseDevice> dev) {
|
||||
final newDevices = dev.where((device) => !devices.contains(device)).toList();
|
||||
devices.addAll(newDevices);
|
||||
|
||||
_connectionQueue.addAll(newDevices);
|
||||
|
||||
_handleConnectionQueue();
|
||||
|
||||
hasDevices.value = devices.isNotEmpty;
|
||||
@@ -112,18 +208,20 @@ class Connection {
|
||||
if (_connectionQueue.isNotEmpty && !_handlingConnectionQueue) {
|
||||
_handlingConnectionQueue = true;
|
||||
final device = _connectionQueue.removeAt(0);
|
||||
_actionStreams.add(LogNotification('Connecting to: ${device.device.name ?? device.runtimeType}'));
|
||||
_actionStreams.add(LogNotification('Connecting to: ${device.name}'));
|
||||
_connect(device)
|
||||
.then((_) {
|
||||
_handlingConnectionQueue = false;
|
||||
_actionStreams.add(LogNotification('Connection finished: ${device.device.name ?? device.runtimeType}'));
|
||||
_actionStreams.add(LogNotification('Connection finished: ${device.name}'));
|
||||
if (_connectionQueue.isNotEmpty) {
|
||||
_handleConnectionQueue();
|
||||
}
|
||||
})
|
||||
.catchError((e) {
|
||||
_handlingConnectionQueue = false;
|
||||
_actionStreams.add(LogNotification('Connection failed: ${device.device.name ?? device.runtimeType} - $e'));
|
||||
_actionStreams.add(
|
||||
LogNotification('Connection failed: ${device.name} - $e'),
|
||||
);
|
||||
if (_connectionQueue.isNotEmpty) {
|
||||
_handleConnectionQueue();
|
||||
}
|
||||
@@ -131,32 +229,43 @@ class Connection {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _connect(BaseDevice bleDevice) async {
|
||||
Future<void> _connect(BaseDevice device) async {
|
||||
try {
|
||||
final actionSubscription = bleDevice.actionStream.listen((data) {
|
||||
final actionSubscription = device.actionStream.listen((data) {
|
||||
_actionStreams.add(data);
|
||||
});
|
||||
final connectionStateSubscription = UniversalBle.connectionStream(bleDevice.device.deviceId).listen((state) {
|
||||
bleDevice.isConnected = state;
|
||||
_connectionStreams.add(bleDevice);
|
||||
if (!bleDevice.isConnected) {
|
||||
devices.remove(bleDevice);
|
||||
_streamSubscriptions[bleDevice]?.cancel();
|
||||
_streamSubscriptions.remove(bleDevice);
|
||||
_connectionSubscriptions[bleDevice]?.cancel();
|
||||
_connectionSubscriptions.remove(bleDevice);
|
||||
_lastScanResult.clear();
|
||||
// try reconnect
|
||||
if (!isScanning.value) {
|
||||
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: true);
|
||||
// try reconnect
|
||||
performScanning();
|
||||
}
|
||||
}
|
||||
});
|
||||
_connectionSubscriptions[bleDevice] = connectionStateSubscription;
|
||||
});
|
||||
_connectionSubscriptions[device] = connectionStateSubscription;
|
||||
}
|
||||
|
||||
await bleDevice.connect();
|
||||
await device.connect();
|
||||
signalChange(device);
|
||||
|
||||
_streamSubscriptions[bleDevice] = actionSubscription;
|
||||
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) {
|
||||
@@ -167,21 +276,26 @@ class Connection {
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
Future<void> reset() async {
|
||||
_actionStreams.add(LogNotification('Disconnecting all devices'));
|
||||
if (actionHandler is AndroidActions) {
|
||||
AndroidFlutterLocalNotificationsPlugin().stopForegroundService();
|
||||
_androidNotificationsSetup = false;
|
||||
}
|
||||
UniversalBle.stopScan();
|
||||
final isBtEnabled = (await UniversalBle.getBluetoothAvailabilityState()) == AvailabilityState.poweredOn;
|
||||
if (isBtEnabled) {
|
||||
UniversalBle.stopScan();
|
||||
}
|
||||
isScanning.value = false;
|
||||
for (var device in devices) {
|
||||
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();
|
||||
@@ -194,4 +308,22 @@ class Connection {
|
||||
void signalChange(BaseDevice baseDevice) {
|
||||
_connectionStreams.add(baseDevice);
|
||||
}
|
||||
|
||||
Future<void> disconnect(BaseDevice device, {required bool forget}) async {
|
||||
if (device.isConnected) {
|
||||
await device.disconnect();
|
||||
}
|
||||
if (device is! LinkDevice) {
|
||||
// keep it in the list to allow reconnect
|
||||
devices.remove(device);
|
||||
}
|
||||
if (!forget && device is BluetoothDevice) {
|
||||
_lastScanResult.removeWhere((b) => b.deviceId == device.device.deviceId);
|
||||
_streamSubscriptions[device]?.cancel();
|
||||
_streamSubscriptions.remove(device);
|
||||
_connectionSubscriptions[device]?.cancel();
|
||||
_connectionSubscriptions.remove(device);
|
||||
}
|
||||
signalChange(device);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,143 +1,45 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:swift_control/bluetooth/ble.dart';
|
||||
import 'package:swift_control/bluetooth/devices/wahoo/wahoo_kickr_bike_shift.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:flutter/material.dart';
|
||||
import 'package:swift_control/bluetooth/devices/zwift/constants.dart';
|
||||
import 'package:swift_control/main.dart';
|
||||
import 'package:swift_control/utils/actions/desktop.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
import 'package:swift_control/utils/keymap/apps/custom_app.dart';
|
||||
import 'package:swift_control/utils/keymap/keymap.dart';
|
||||
|
||||
import '../../utils/keymap/buttons.dart';
|
||||
import '../messages/notification.dart';
|
||||
import 'elite/elite_square.dart';
|
||||
|
||||
abstract class BaseDevice {
|
||||
final BleDevice scanResult;
|
||||
final String name;
|
||||
final bool isBeta;
|
||||
final List<ControllerButton> availableButtons;
|
||||
|
||||
BaseDevice(this.scanResult, {required this.availableButtons, this.isBeta = false});
|
||||
BaseDevice(this.name, {required this.availableButtons, this.isBeta = false});
|
||||
|
||||
bool isConnected = false;
|
||||
int? batteryLevel;
|
||||
String? firmwareVersion;
|
||||
|
||||
Timer? _longPressTimer;
|
||||
Set<ControllerButton> _previouslyPressedButtons = <ControllerButton>{};
|
||||
|
||||
static List<String> servicesToScan = [
|
||||
BleUuid.ZWIFT_CUSTOM_SERVICE_UUID,
|
||||
BleUuid.ZWIFT_RIDE_CUSTOM_SERVICE_UUID,
|
||||
SquareConstants.SERVICE_UUID,
|
||||
WahooKickrBikeShiftConstants.SERVICE_UUID,
|
||||
];
|
||||
|
||||
static BaseDevice? fromScanResult(BleDevice scanResult) {
|
||||
// Use the name first as the "System Devices" and Web (android sometimes Windows) don't have manufacturer data
|
||||
BaseDevice? device;
|
||||
if (kIsWeb) {
|
||||
device = switch (scanResult.name) {
|
||||
'Zwift Ride' => ZwiftRide(scanResult),
|
||||
'Zwift Play' => ZwiftPlay(scanResult),
|
||||
'Zwift Click' => ZwiftClickV2(scanResult),
|
||||
'SQUARE' => EliteSquare(scanResult),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (scanResult.name != null && scanResult.name!.toUpperCase().startsWith('KICKR BIKE SHIFT')) {
|
||||
device = WahooKickrBikeShift(scanResult);
|
||||
}
|
||||
} else {
|
||||
device = switch (scanResult.name) {
|
||||
//'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
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
if (device != null) {
|
||||
return device;
|
||||
} else if (scanResult.services.containsAny([
|
||||
BleUuid.ZWIFT_CUSTOM_SERVICE_UUID,
|
||||
BleUuid.ZWIFT_RIDE_CUSTOM_SERVICE_UUID,
|
||||
])) {
|
||||
// otherwise use the manufacturer data to identify the device
|
||||
final manufacturerData = scanResult.manufacturerDataList;
|
||||
final data = manufacturerData.firstOrNullWhere((e) => e.companyId == Constants.ZWIFT_MANUFACTURER_ID)?.payload;
|
||||
|
||||
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),
|
||||
DeviceType.rideLeft => ZwiftRide(scanResult),
|
||||
//DeviceType.rideRight => ZwiftRide(scanResult), // see comment above
|
||||
DeviceType.clickV2Left => ZwiftClickV2(scanResult),
|
||||
//DeviceType.clickV2Right => ZwiftClickV2(scanResult), // see comment above
|
||||
_ => null,
|
||||
};
|
||||
} else if (scanResult.services.contains(SquareConstants.SERVICE_UUID)) {
|
||||
return EliteSquare(scanResult);
|
||||
} else if (scanResult.services.contains(WahooKickrBikeShiftConstants.SERVICE_UUID)) {
|
||||
if (scanResult.name != null && !scanResult.name!.toUpperCase().contains('KICKR BIKE SHIFT')) {
|
||||
return WahooKickrBikeShift(scanResult);
|
||||
} else if (kIsWeb && scanResult.name == null) {
|
||||
// some devices don't broadcast the name, so we must rely on the service UUID
|
||||
return WahooKickrBikeShift(scanResult);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is BaseDevice && runtimeType == other.runtimeType && scanResult == other.scanResult;
|
||||
identical(this, other) || other is BaseDevice && runtimeType == other.runtimeType && name == other.name;
|
||||
|
||||
@override
|
||||
int get hashCode => scanResult.hashCode;
|
||||
int get hashCode => name.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return runtimeType.toString();
|
||||
}
|
||||
|
||||
BleDevice get device => scanResult;
|
||||
final StreamController<BaseNotification> actionStreamInternal = StreamController<BaseNotification>.broadcast();
|
||||
|
||||
Stream<BaseNotification> get actionStream => actionStreamInternal.stream;
|
||||
|
||||
Future<void> 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);
|
||||
await handleServices(services);
|
||||
}
|
||||
|
||||
Future<void> handleServices(List<BleService> services);
|
||||
Future<void> processCharacteristic(String characteristic, Uint8List bytes);
|
||||
Future<void> connect();
|
||||
|
||||
Future<void> handleButtonsClicked(List<ControllerButton>? buttonsClicked) async {
|
||||
if (buttonsClicked == null) {
|
||||
@@ -156,6 +58,8 @@ abstract class BaseDevice {
|
||||
}
|
||||
_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 =
|
||||
@@ -170,8 +74,8 @@ abstract class BaseDevice {
|
||||
actionHandler.supportedApp?.keymap.getKeyPair(buttonsClicked.single)?.isLongPress == true;
|
||||
|
||||
if (!isLongPress &&
|
||||
!(buttonsClicked.singleOrNull == ControllerButton.onOffLeft ||
|
||||
buttonsClicked.singleOrNull == ControllerButton.onOffRight)) {
|
||||
!(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 {
|
||||
@@ -221,7 +125,31 @@ abstract class BaseDevice {
|
||||
await (actionHandler as DesktopActions).releaseAllHeldKeys(_previouslyPressedButtons.toList());
|
||||
}
|
||||
_previouslyPressedButtons.clear();
|
||||
await UniversalBle.disconnect(device.deviceId);
|
||||
isConnected = false;
|
||||
}
|
||||
|
||||
Widget showInformation(BuildContext context);
|
||||
|
||||
ControllerButton? getOrAddButton(String name, ControllerButton Function() button) {
|
||||
if (actionHandler.supportedApp is CustomApp) {
|
||||
final allButtons = actionHandler.supportedApp!.keymap.keyPairs.expand((kp) => kp.buttons).toSet().toList();
|
||||
if (allButtons.none((b) => b.name == name)) {
|
||||
final newButton = button();
|
||||
availableButtons.add(newButton);
|
||||
actionHandler.supportedApp!.keymap.addKeyPair(
|
||||
KeyPair(
|
||||
touchPosition: Offset.zero,
|
||||
buttons: [newButton],
|
||||
physicalKey: null,
|
||||
logicalKey: null,
|
||||
isLongPress: false,
|
||||
),
|
||||
);
|
||||
return newButton;
|
||||
} else {
|
||||
return allButtons.firstWhere((b) => b.name == name);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
251
lib/bluetooth/devices/bluetooth_device.dart
Normal file
@@ -0,0 +1,251 @@
|
||||
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_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<String> 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('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('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 == other.scanResult;
|
||||
|
||||
@override
|
||||
int get hashCode => scanResult.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return runtimeType.toString();
|
||||
}
|
||||
|
||||
BleDevice get device => scanResult;
|
||||
|
||||
@override
|
||||
Future<void> 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<void> handleServices(List<BleService> services);
|
||||
Future<void> processCharacteristic(String characteristic, Uint8List bytes);
|
||||
|
||||
@override
|
||||
Future<void> disconnect() async {
|
||||
await UniversalBle.disconnect(device.deviceId);
|
||||
super.disconnect();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget showInformation(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
device.name?.screenshot ?? device.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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
97
lib/bluetooth/devices/cycplus/cycplus_bc2.dart
Normal file
@@ -0,0 +1,97 @@
|
||||
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<void> handleServices(List<BleService> 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);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> processCharacteristic(String characteristic, Uint8List bytes) {
|
||||
if (characteristic.toLowerCase() == CycplusBc2Constants.TX_CHARACTERISTIC_UUID.toLowerCase()) {
|
||||
// Process CYCPLUS BC2 data
|
||||
// The BC2 typically sends button press data as simple byte values
|
||||
// Common patterns for virtual shifters:
|
||||
// - 0x01 or similar for shift up
|
||||
// - 0x02 or similar for shift down
|
||||
// - 0x00 for button release
|
||||
|
||||
if (bytes.isNotEmpty) {
|
||||
final buttonCode = bytes[0];
|
||||
|
||||
switch (buttonCode) {
|
||||
case 0x01:
|
||||
// Shift up button pressed
|
||||
handleButtonsClicked([CycplusBc2Buttons.shiftUp]);
|
||||
break;
|
||||
case 0x02:
|
||||
// Shift down button pressed
|
||||
handleButtonsClicked([CycplusBc2Buttons.shiftDown]);
|
||||
break;
|
||||
case 0x00:
|
||||
// Button released
|
||||
handleButtonsClicked([]);
|
||||
break;
|
||||
default:
|
||||
// Unknown button code - log for debugging
|
||||
actionStreamInternal.add(
|
||||
LogNotification('CYCPLUS BC2: Unknown button code: 0x${buttonCode.toRadixString(16)}'),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Future.value();
|
||||
}
|
||||
}
|
||||
|
||||
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<ControllerButton> values = [
|
||||
shiftUp,
|
||||
shiftDown,
|
||||
];
|
||||
}
|
||||