├── .github └── workflows │ ├── integration.yml │ ├── publish.yml │ └── release.yml ├── .gitignore ├── .releaserc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── android ├── .gitignore ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── cz │ │ └── dronetag │ │ └── flutter_opendroneid │ │ └── Pigeon.java │ └── kotlin │ └── cz │ └── dronetag │ └── flutter_opendroneid │ ├── FlutterOpendroneidPlugin.kt │ ├── StreamHandler.kt │ └── scanner │ ├── BluetoothScanner.kt │ ├── ODIDScanner.kt │ ├── WifiNaNScanner.kt │ └── WifiScanner.kt ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle.kts │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── flutter_opendroneid_example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle.kts │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle.kts ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h ├── lib │ ├── home_page.dart │ ├── main.dart │ ├── message_container_view.dart │ ├── request_permission_button.dart │ └── scan_button.dart └── pubspec.yaml ├── flutter_opendroneid_logo.png ├── ios ├── .gitignore ├── Assets │ └── .gitkeep ├── Classes │ ├── FlutterOpendroneidPlugin.h │ ├── FlutterOpendroneidPlugin.m │ ├── Scanner │ │ └── BluetoothScanner.swift │ ├── SwiftFlutterOpendroneidPlugin.swift │ ├── flutter_opendroneid.h │ ├── pigeon.h │ ├── pigeon.m │ └── utils │ │ ├── StreamHandler.swift │ │ └── Utils.swift └── flutter_opendroneid.podspec ├── lib ├── exceptions │ └── odid_message_parsing_exception.dart ├── extensions │ ├── compare_extension.dart │ └── list_extension.dart ├── flutter_opendroneid.dart ├── models │ ├── constants.dart │ ├── dri_source_type.dart │ ├── message_container.dart │ └── permissions_missing_exception.dart ├── pigeon.dart └── utils │ └── conversions.dart ├── pigeon └── schema.dart ├── pubspec.yaml └── scripts └── pigeon_generate.sh /.github/workflows/integration.yml: -------------------------------------------------------------------------------- 1 | name: Integration 2 | 3 | on: 4 | pull_request: 5 | workflow_call: 6 | 7 | env: 8 | FLUTTER_VERSION: "3.29.2" 9 | 10 | jobs: 11 | lint: 12 | name: Check for linting or typing errors 13 | runs-on: ubuntu-22.04 14 | timeout-minutes: 5 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v3 18 | - name: Install the project 19 | uses: dronetag/gha-shared/.github/actions/flutter-install@master 20 | with: 21 | flutter-version: ${{ env.FLUTTER_VERSION }} 22 | enforce-lockfile: false 23 | - name: Run code analysis 24 | run: flutter analyze --no-fatal-infos 25 | 26 | # test: 27 | # name: Run unit tests suite 28 | # runs-on: ubuntu-22.04 29 | # timeout-minutes: 5 30 | # steps: 31 | # - name: Checkout repository 32 | # uses: actions/checkout@v3 33 | # - name: Install the project 34 | # uses: dronetag/gha-shared/.github/actions/flutter-install@master 35 | # with: 36 | # flutter-version: ${{ env.FLUTTER_VERSION }} 37 | # - name: Run test suite 38 | # run: flutter test -r expanded -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to pub.dev 2 | 3 | # Publish is triggered on new version tag push event 4 | on: 5 | push: 6 | tags: 7 | - 'v[0-9]+.[0-9]+.[0-9]+*' # for tags like: 'v1.2.3' 8 | 9 | env: 10 | FLUTTER_VERSION: "3.16.7" 11 | 12 | jobs: 13 | publish: 14 | name: Publish to pub.dev 15 | permissions: 16 | id-token: write 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v3 21 | - name: Install the project 22 | uses: dronetag/gha-shared/.github/actions/flutter-install@master 23 | with: 24 | flutter-version: ${{ env.FLUTTER_VERSION }} 25 | setup-java: false 26 | enforce-lockfile: false 27 | # Sensitive step, check action script before each upgrade of flutter-actions/setup-pubdev-credentials 28 | - uses: flutter-actions/setup-pubdev-credentials@2ffa6245d17992c9f0acf9dc2be626e3b0b888c1 29 | - name: Publish to pub.dev 30 | run: flutter pub publish -f 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Github release 2 | 3 | # Release workflow creates a new Github release 4 | # with new version and updates changelog 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | 11 | env: 12 | FLUTTER_VERSION: "3.16.7" 13 | 14 | jobs: 15 | integration: 16 | # Prevent release commits from triggering integration check 17 | if: "!contains(github.event.head_commit.message, 'chore(release)')" 18 | name: Run integration 19 | uses: ./.github/workflows/integration.yml 20 | 21 | release-version: 22 | # Prevent release commits from triggering release again 23 | if: "!contains(github.event.head_commit.message, 'chore(release)')" 24 | name: Release new version 25 | needs: integration 26 | uses: dronetag/gha-shared/.github/workflows/create-release.yml@master 27 | concurrency: release-version-${{ github.repository }} 28 | with: 29 | install-changelog-plugin: true 30 | install-yq: true 31 | must-release: false 32 | create-github-release: false # Release performed by semantic-release 33 | secrets: 34 | github-token: ${{ secrets.RELEASE_PAT }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pubspec.lock 2 | 3 | # Miscellaneous 4 | *.class 5 | *.log 6 | *.pyc 7 | *.swp 8 | .DS_Store 9 | .atom/ 10 | .buildlog/ 11 | .history 12 | .svn/ 13 | 14 | # IntelliJ related 15 | .idea/ 16 | 17 | # The .vscode folder contains launch configuration and tasks you configure in 18 | # VS Code which you may wish to be included in version control, so this line 19 | # is commented out by default. 20 | .vscode/ 21 | 22 | # Flutter/Dart/Pub related 23 | **/doc/api/ 24 | **/ios/Flutter/.last_build_id 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | 47 | .dart_tool/ 48 | 49 | .packages 50 | .pub/ 51 | .vscode/ 52 | 53 | # Mac 54 | .DS_Store 55 | build/ 56 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "master", 4 | { 5 | "name": "alpha", 6 | "prerelease": true 7 | }, 8 | { 9 | "name": "beta", 10 | "prerelease": true 11 | } 12 | ], 13 | "plugins": [ 14 | [ 15 | "@semantic-release/commit-analyzer", 16 | { 17 | "releaseRules": [ 18 | { 19 | "type": "docs", 20 | "release": "patch" 21 | }, 22 | { 23 | "type": "refactor", 24 | "release": "patch" 25 | }, 26 | { 27 | "type": "style", 28 | "release": "patch" 29 | }, 30 | { 31 | "type": "chore", 32 | "release": "patch" 33 | }, 34 | { 35 | "type": "perf", 36 | "release": "patch" 37 | }, 38 | { 39 | "type": "test", 40 | "release": "patch" 41 | } 42 | ] 43 | } 44 | ], 45 | "@semantic-release/release-notes-generator", 46 | [ 47 | "@semantic-release/changelog", 48 | { 49 | "changelogFile": "CHANGELOG.md" 50 | } 51 | ], 52 | [ 53 | "@semantic-release/exec", 54 | { 55 | "prepareCmd": "yq -i '.version = \"${nextRelease.version}\"' pubspec.yaml" 56 | } 57 | ], 58 | [ 59 | "@semantic-release/git", 60 | { 61 | "assets": [ 62 | "CHANGELOG.md", 63 | "pubspec.yaml" 64 | ], 65 | "message": "chore(release): Release ${nextRelease.version}\n\n${nextRelease.notes}" 66 | } 67 | ], 68 | [ 69 | "@semantic-release/github", 70 | { 71 | "successComment": false, 72 | "failComment": false 73 | } 74 | ] 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [0.23.0](https://github.com/dronetag/flutter-opendroneid/compare/v0.22.1...v0.23.0) (2025-04-30) 2 | 3 | 4 | ### Features 5 | 6 | * add example add for ios and android ([db28336](https://github.com/dronetag/flutter-opendroneid/commit/db283362a5142f6a999285dd322ab617e31de056)) 7 | * add view of last received message container ([b75db75](https://github.com/dronetag/flutter-opendroneid/commit/b75db75a98fb653b2762b60ab038683dfa859088)) 8 | * implement demo app ui ([5f34b64](https://github.com/dronetag/flutter-opendroneid/commit/5f34b6411045da4584865a98b2924d70b594c37e)) 9 | * wrap app body in SafeArea ([0a4dbcb](https://github.com/dronetag/flutter-opendroneid/commit/0a4dbcbbbaa7479c43551a1df401a0a4dd007302)) 10 | 11 | ## [0.22.1](https://github.com/dronetag/flutter-opendroneid/compare/v0.22.0...v0.22.1) (2025-03-26) 12 | 13 | # [0.22.0](https://github.com/dronetag/flutter-opendroneid/compare/v0.21.1...v0.22.0) (2025-03-25) 14 | 15 | 16 | ### Features 17 | 18 | * add extended ODIDMetadata ([#42](https://github.com/dronetag/flutter-opendroneid/issues/42)) ([db2be75](https://github.com/dronetag/flutter-opendroneid/commit/db2be754e47167e880b47be0d5f7b653b926550e)) 19 | * add ODIDMetadata to pigeon schema, regenerate files ([cdfe41c](https://github.com/dronetag/flutter-opendroneid/commit/cdfe41cd0c3b95f8ea8a04dd70658f3704abd13d)) 20 | * use ODIDMetadata in main dart file ([c143f26](https://github.com/dronetag/flutter-opendroneid/commit/c143f26de4559d6b41ef9df7783c1d34c4a1b761)) 21 | * use ODIDMetadata in message container ([b76a743](https://github.com/dronetag/flutter-opendroneid/commit/b76a7437cc93deaaa1452259817fef5df28e280e)) 22 | * use ODIDMetadata in native ([41c83e1](https://github.com/dronetag/flutter-opendroneid/commit/41c83e1d6217a9ee8cb8cb8310ac1dc28d2f48bd)) 23 | 24 | ## [0.21.1](https://github.com/dronetag/flutter-opendroneid/compare/v0.21.0...v0.21.1) (2025-03-16) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * cancel scan when bt is turned off ([18ca59c](https://github.com/dronetag/flutter-opendroneid/commit/18ca59c1abe13de9efc8d2ef94ea49d129dd103b)) 30 | 31 | ## [0.19.5](https://github.com/dronetag/flutter-opendroneid/compare/v0.19.4...v0.19.5) (2025-01-05) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * **and:** stop scans only if they are active in onDetachedFromEngine ([d00de4e](https://github.com/dronetag/flutter-opendroneid/commit/d00de4e41741189ae0dc2d7067a75a9ec6937528)) 37 | * **and:** unregisterReceiver on onDetachedFromActivity ([c737347](https://github.com/dronetag/flutter-opendroneid/commit/c737347aa0ba9d81491bfc50f9764f2945a170e1)) 38 | 39 | ## [0.19.4](https://github.com/dronetag/flutter-opendroneid/compare/v0.19.3...v0.19.4) (2024-11-27) 40 | 41 | 42 | ### Bug Fixes 43 | 44 | * setting up wifi event channel ([71c8d8c](https://github.com/dronetag/flutter-opendroneid/commit/71c8d8cacfb3a5f8bc3e62ad3616e583788ca39c)) 45 | 46 | ## [0.19.3](https://github.com/dronetag/flutter-opendroneid/compare/v0.19.2...v0.19.3) (2024-11-26) 47 | 48 | ## [0.19.2](https://github.com/dronetag/flutter-opendroneid/compare/v0.19.1...v0.19.2) (2024-11-26) 49 | 50 | ## [0.19.1](https://github.com/dronetag/flutter-opendroneid/compare/v0.19.0...v0.19.1) (2024-10-02) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * return from scan method if wifiAwareSupported is false ([f000816](https://github.com/dronetag/flutter-opendroneid/commit/f000816342e61a4a4f5c07c2416092466eb80733)) 56 | * solve exception when stopping Wi-Fi scans ([#37](https://github.com/dronetag/flutter-opendroneid/issues/37)) ([3d3ff20](https://github.com/dronetag/flutter-opendroneid/commit/3d3ff20ae29aff5709898570f01854ae4fb82e8c)) 57 | 58 | # [0.19.0](https://github.com/dronetag/flutter-opendroneid/compare/v0.18.1...v0.19.0) (2024-07-19) 59 | 60 | 61 | ### Features 62 | 63 | * add bt name to ODIDPayload ([4683e23](https://github.com/dronetag/flutter-opendroneid/commit/4683e23a6c665b8fff1842697ed8ef8b9a46ebbd)) 64 | * add ODIDMessageParsingException ([715e231](https://github.com/dronetag/flutter-opendroneid/commit/715e231bb92aaba93fcc13b6bcdc1fb032e9ba4e)) 65 | * include Bluetooth metadata to parsing exceptions ([#36](https://github.com/dronetag/flutter-opendroneid/issues/36)) ([89a29b6](https://github.com/dronetag/flutter-opendroneid/commit/89a29b6dcff676cc6598bf047ce37721438c51cc)) 66 | 67 | ## [0.18.1](https://github.com/dronetag/flutter-opendroneid/compare/v0.18.0...v0.18.1) (2024-06-20) 68 | 69 | # [0.18.0](https://github.com/dronetag/flutter-opendroneid/compare/v0.17.0...v0.18.0) (2024-06-11) 70 | 71 | 72 | ### Bug Fixes 73 | 74 | * pass rssi to update method when receiving message pack ([5af8a37](https://github.com/dronetag/flutter-opendroneid/commit/5af8a37bd99ed2af053c64ee585a6a738af5d4ad)) 75 | * rssi of messages in message pack ([#33](https://github.com/dronetag/flutter-opendroneid/issues/33)) ([845cac2](https://github.com/dronetag/flutter-opendroneid/commit/845cac28e205ac348b7ac05cefd8e8aba36d1fc4)) 76 | * Unregister receiver on cancellation ([11bbc04](https://github.com/dronetag/flutter-opendroneid/commit/11bbc04d71a8bc0685528cba650fc49b16dfa7eb)) 77 | 78 | 79 | ### Features 80 | 81 | * Add missing wifi state receiver unregistration on cancel ([#32](https://github.com/dronetag/flutter-opendroneid/issues/32)) ([0e3c7db](https://github.com/dronetag/flutter-opendroneid/commit/0e3c7db7c7c25fbfffe17b2b0a01071e42c39859)) 82 | * Add toString method for MessageContainer ([0174cfb](https://github.com/dronetag/flutter-opendroneid/commit/0174cfb475ee5857398dfe9acd86a8464cc300e5)) 83 | 84 | ## 0.17.0 85 | 86 | * Allow multiple Basic ID messages in container 87 | 88 | ## 0.16.0 89 | 90 | * Gradle & dependencies updates 91 | * Flutter version bumped to 3.16.7 92 | * Fixed event channels not correctly disposed when the app quits 93 | 94 | ## 0.15.2 95 | 96 | * Updated permission_handler dependent library to v11.x.x 97 | 98 | ## [0.15.1](https://github.com/dronetag/flutter-opendroneid/compare/v0.15.0...v0.15.1) (2023-10-03) 99 | 100 | 101 | ### Bug Fixes 102 | 103 | * Fix runtimeType incorrectly used for UASID.asString() ([#24](https://github.com/dronetag/flutter-opendroneid/issues/24)) ([e6ee8b5](https://github.com/dronetag/flutter-opendroneid/commit/e6ee8b5d30b9a945852a31bed687e51a2c1c3acf)) 104 | * uasid asString conversion ([6018e98](https://github.com/dronetag/flutter-opendroneid/commit/6018e98fa243d0ba68442a6dc5b385d2dc753ccc)) 105 | 106 | # [0.15.0](https://github.com/dronetag/flutter-opendroneid/compare/v0.14.2...v0.15.0) (2023-10-01) 107 | 108 | 109 | ### Features 110 | 111 | * Re-export Dart ODID types ([0055df7](https://github.com/dronetag/flutter-opendroneid/commit/0055df71b1368bb9d003d12175fa40808f53850e)) 112 | 113 | ## [0.14.2](https://github.com/dronetag/flutter-opendroneid/compare/v0.14.1...v0.14.2) (2023-10-01) 114 | 115 | 116 | ### Bug Fixes 117 | 118 | * edit uasid string conversion ([c32c65c](https://github.com/dronetag/flutter-opendroneid/commit/c32c65c0cebfe94f8e164e3b1d3679871630d90b)) 119 | * Fix UAS ID string conversion ([#23](https://github.com/dronetag/flutter-opendroneid/issues/23)) ([3686963](https://github.com/dronetag/flutter-opendroneid/commit/368696300b72edd29d752c602c7a3c72e292e6aa)) 120 | 121 | ## [0.14.1](https://github.com/dronetag/flutter-opendroneid/compare/v0.14.0...v0.14.1) (2023-09-21) 122 | 123 | # [0.14.0](https://github.com/dronetag/flutter-opendroneid/compare/v0.13.0...v0.14.0) (2023-09-12) 124 | 125 | 126 | ### Bug Fixes 127 | 128 | * update container with source of current message update ([c477388](https://github.com/dronetag/flutter-opendroneid/commit/c47738842cc2f9c115af3bbb59f8deba431b3d08)) 129 | 130 | 131 | ### Features 132 | 133 | * add conversions of message values to strings ([7b43b50](https://github.com/dronetag/flutter-opendroneid/commit/7b43b5058d013ffaa67b369662e72e39114e215b)) 134 | * put back duplicate messages filtering ([f1d1001](https://github.com/dronetag/flutter-opendroneid/commit/f1d1001fe70240d1dee954bf68ec9c9f9296669a)) 135 | * remove message parsing, use dart-odid ([c3dda27](https://github.com/dronetag/flutter-opendroneid/commit/c3dda27db63533623d8bba5c7970775b0ee19319)) 136 | * shorted BLE advertisements to max 31 bytes ([8da4ea2](https://github.com/dronetag/flutter-opendroneid/commit/8da4ea2aaff5b75845a63066e809719553a0442a)) 137 | * Use dart-opendroneid for parsing & pass raw bytes from native (DT-2604) ([#21](https://github.com/dronetag/flutter-opendroneid/issues/21)) ([87f5481](https://github.com/dronetag/flutter-opendroneid/commit/87f548145dc9bd2bdb30eb12c2636ed293e187b0)) 138 | 139 | ## 0.13.0 140 | 141 | - Streams are de-duplicated by fields comparison 142 | - Android SDK requirement bumped to version 33 143 | 144 | ## 0.12.1 145 | 146 | We require location permission for Bluetooth scanning on all Android versions, following the [Android official documentation](https://developer.android.com/guide/topics/connectivity/bluetooth/permissions#declare). 147 | 148 | ## 0.12.0 149 | 150 | Changes made to ensure compatibility with new Flutter 3.10 and Dart 3. 151 | 152 | ## 0.11.3 153 | 154 | - Use the NEARBY_WIFI_DEVICES permission for Android >=13 155 | 156 | ## 0.11.2 157 | 158 | Fixed failure preventing listener initialization which was meant to run after starting scan. 159 | 160 | ## 0.11.1 161 | 162 | Bumped dependencies 163 | 164 | ## 0.11.0 165 | 166 | - Added methods for checking required Bluetooth & Wi-Fi permissions and eventually reporting when some of them are missing. 167 | - We've moved the responsibility to obtain necessary permissions to the target apps using this library, to avoid multiple permission requests in the target apps. 168 | 169 | ## 0.10.0 170 | 171 | Added new options to set Bluetooth scan priority 172 | 173 | ## 0.9.8 174 | 175 | Added methods for checking the validity of public part of UAS Operator ID 176 | 177 | ## 0.9.7 178 | 179 | Added explicit checks for internal enum structures 180 | 181 | ## 0.9.6 182 | 183 | - Added missing support for Bluetooth variant of MESSAGE_PACK messages 184 | - Fixed Bluetooth adapter state handling on iOS 185 | 186 | ## 0.9.5 187 | 188 | - Add missing support for OpenDroneId MESSAGE_PACK message type 189 | - Fix wrong byterange when parsing UAS IDs 190 | - Fix parsing of Wi-Fi beacons 191 | 192 | ## 0.9.4 193 | 194 | Add methods to detect bluetooth and wifi adapter states 195 | 196 | ## 0.9.3 197 | 198 | Updated `pigeon` library to v3 199 | 200 | ## 0.9.2 201 | 202 | Minor fixed in speeds & area calculation formulas 203 | 204 | ## 0.9.1 205 | 206 | Fixed technology detection logic and added implemented location validation 207 | 208 | ## 0.9.0 209 | 210 | Initial public release 211 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | No license -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # flutter_opendroneid 4 | 5 | A flutter plugin for reading Wi-Fi and Bluetooth Remote ID advertisements using native Android and iOS platform-specific implementation. The format of data is defined in the [ASTM F3411](https://www.astm.org/f3411-22a.html) Remote ID and the [ASD-STAN prEN 4709-002](http://asd-stan.org/downloads/asd-stan-pren-4709-002-p1/) Direct Remote ID specifications. 6 | 7 | The platform-specific implementation reads raw message bytes from Wi-Fi and Bluetooth Remote ID advertisements. Then the raw payload with metadata is passed using event channels to the Dart side. Raw data are parsed to Remote ID messages using [Dart-opendroneid library](https://github.com/dronetag/dart-opendroneid). 8 | 9 | [The pigeon library](https://pub.dev/packages/pigeon) is used to define the messaging protocol between the platform host and the Flutter client. The messaging protocol is defined in [schema.dart](pigeon/schema.dart). 10 | 11 | The architecture of native code is inspired by [OpenDroneID Android receiver application](https://github.com/opendroneid/receiver-android). 12 | 13 | ## Pre-requisities 14 | 15 | - Flutter 3.16.7 or newer 16 | 17 | ## Getting Started 18 | 19 | This project is a Flutter [plug-in package](https://flutter.dev/developing-packages/), a specialized package that includes platform-specific implementation code for Android and/or iOS. 20 | 21 | For help getting started with Flutter, view 22 | [online documentation](https://flutter.dev/docs), which offers tutorials, 23 | samples, guidance on mobile development, and a full API reference. 24 | 25 | ## Work in progress 26 | 27 | > ⚠️ While we made this library public to allow [Drone Scanner](https://github.com/dronetag/drone-scanner) to be published as open-source, we're still not satisfied with the state of this repository, missing documentation and contribution guidelines. Stay tuned, we're working on it. 28 | 29 | ## Installing 30 | 31 | 1. Install the project using `flutter pub get` 32 | 2. Generate Pigeon classes by running shell script in `scripts/pigeon_generate.sh` 33 | 34 | ## Setting up permissions 35 | 36 | Enabling scanning the surroundings for Wi-Fi and Bluetooth Remote ID advertisements requires setting up permissions. App has to request required permissions, the plugin only checks that permissions are granted. If some permissions are missing, the plugin throws `PermissionsMissingException` when attempting to start the scan. Use for example the [permission handler package](https://pub.dev/packages/permission_handler) to request permissions. 37 | 38 | 39 | ### Android Setup 40 | Android allows both Wi-Fi and Bluetooth scanning. Bluetooth scanning requires Bluetooth and Bluetooth Scan permission. Location permission is required for Bluetooth scanning since Android 12 (API level 31). 41 | Check the [documentation on Bluetooth permissions](https://developer.android.com/develop/connectivity/bluetooth/bt-permissions). 42 | 43 | Wi-Fi scanning requires location permission up to version 12 (API level 31), since version 13, the Nearby Wifi Devices permission is required. 44 | Check the [documentation on Wi-Fi permissions](https://developer.android.com/develop/connectivity/wifi/wifi-permissions). 45 | 46 | Permissions need to be added to `AndroidManifest.xml` file: 47 | 48 | ``` 49 | 51 | 52 | 53 | 54 | 57 | 58 | ``` 59 | 60 | ### iOS Setup 61 | 62 | iOS does not allow Wi-Fi scanning, only Bluetooth scanning is possible. Bluetooth permission is required. Apart from requesting permission, it also needs to be added to `Info.plist`. 63 | 64 | - add `NSBluetoothAlwaysUsageDescription key` to `Info.plist` with the `string` type. Use any description, for example the one in code snippet. It will be shown in dialog when requesting permission. 65 | ``` 66 | 67 | ... 68 | NSBluetoothAlwaysUsageDescription 69 | The application needs Bluetooth permission to acquire data from nearby aircraft. 70 | ... 71 | ``` 72 | 73 | - permission handler requires setting macros in `Podfile`. Set `PERMISSION_BLUETOOTH` to 1. 74 | ``` 75 | post_install do |installer| 76 | installer.pods_project.targets.each do |target| 77 | flutter_additional_ios_build_settings(target) 78 | target.build_configurations.each do |config| 79 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ 80 | '$(inherited)', 81 | 82 | ## dart: PermissionGroup.bluetooth 83 | 'PERMISSION_BLUETOOTH=1', 84 | ] 85 | end 86 | end 87 | end 88 | ``` 89 | 90 | --- 91 | 92 | © [Dronetag 2025](https://www.dronetag.cz) 93 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'cz.dronetag.flutter_opendroneid' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | repositories { 6 | google() 7 | mavenCentral() 8 | } 9 | 10 | dependencies { 11 | classpath "com.android.tools.build:gradle:$agpVersion" 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" 13 | } 14 | } 15 | 16 | rootProject.allprojects { 17 | repositories { 18 | google() 19 | mavenCentral() 20 | } 21 | } 22 | 23 | apply plugin: 'com.android.library' 24 | apply plugin: 'kotlin-android' 25 | 26 | android { 27 | namespace = "cz.dronetag.flutter_opendroneid" 28 | 29 | sourceSets { 30 | main.java.srcDirs += 'src/main/kotlin' 31 | } 32 | defaultConfig { 33 | compileSdk 33 34 | minSdkVersion 21 35 | targetSdkVersion 33 36 | } 37 | compileOptions { 38 | sourceCompatibility JavaVersion.VERSION_1_8 39 | targetCompatibility JavaVersion.VERSION_1_8 40 | } 41 | kotlinOptions { 42 | jvmTarget = '1.8' 43 | } 44 | } 45 | 46 | dependencies {} 47 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | kotlinVersion=1.8.21 5 | agpVersion=7.4.2 6 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Feb 02 14:52:12 SGT 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'flutter_opendroneid' 2 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /android/src/main/kotlin/cz/dronetag/flutter_opendroneid/FlutterOpendroneidPlugin.kt: -------------------------------------------------------------------------------- 1 | package cz.dronetag.flutter_opendroneid 2 | 3 | import android.app.Activity 4 | import android.bluetooth.BluetoothAdapter 5 | import android.content.Context 6 | import android.content.IntentFilter 7 | import android.net.wifi.WifiManager 8 | import android.net.wifi.aware.WifiAwareManager 9 | import android.os.Build 10 | import androidx.annotation.NonNull 11 | import androidx.annotation.RequiresApi 12 | import io.flutter.Log 13 | import io.flutter.embedding.engine.plugins.FlutterPlugin 14 | import io.flutter.embedding.engine.plugins.activity.ActivityAware 15 | import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding 16 | import io.flutter.plugin.common.MethodCall 17 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler 18 | import io.flutter.plugin.common.MethodChannel.Result 19 | 20 | /** FlutterOpendroneidPlugin */ 21 | class FlutterOpendroneidPlugin : FlutterPlugin, ActivityAware, Pigeon.Api { 22 | private var boundActivity: Activity? = null 23 | 24 | private lateinit var context: Context 25 | private lateinit var activity: Activity 26 | 27 | private val bluetoothOdidPayloadStreamHandler = StreamHandler() 28 | private val wifiOdidPayloadStreamHandler = StreamHandler() 29 | private val bluetoothStateStreamHandler = StreamHandler() 30 | private val wifiStateStreamHandler = StreamHandler() 31 | 32 | private var scanner: BluetoothScanner = 33 | BluetoothScanner( 34 | bluetoothOdidPayloadStreamHandler, bluetoothStateStreamHandler, 35 | ) 36 | private lateinit var wifiScanner: WifiScanner 37 | private lateinit var wifiNaNScanner: WifiNaNScanner 38 | 39 | @RequiresApi(Build.VERSION_CODES.O) 40 | override fun onAttachedToEngine( 41 | @NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding 42 | ) { 43 | Pigeon.Api.setup(flutterPluginBinding.binaryMessenger, this) 44 | 45 | StreamHandler.bindMultipleHandlers( 46 | flutterPluginBinding.binaryMessenger, 47 | mapOf( 48 | "flutter_odid_data_bt" to bluetoothOdidPayloadStreamHandler, 49 | "flutter_odid_data_wifi" to wifiOdidPayloadStreamHandler, 50 | "flutter_odid_state_bt" to bluetoothStateStreamHandler, 51 | "flutter_odid_state_wifi" to wifiStateStreamHandler 52 | ) 53 | ) 54 | 55 | context = flutterPluginBinding.applicationContext 56 | 57 | val wifiManager: WifiManager? = 58 | context.getSystemService(Context.WIFI_SERVICE) as WifiManager? 59 | val wifiAwareManager: WifiAwareManager? = 60 | context.getSystemService(Context.WIFI_AWARE_SERVICE) as WifiAwareManager? 61 | 62 | wifiScanner = 63 | WifiScanner( 64 | wifiOdidPayloadStreamHandler, wifiStateStreamHandler, wifiManager, context 65 | ) 66 | wifiNaNScanner = 67 | WifiNaNScanner( 68 | wifiOdidPayloadStreamHandler, wifiStateStreamHandler, wifiAwareManager, context 69 | ) 70 | } 71 | 72 | override fun onAttachedToActivity(binding: ActivityPluginBinding) { 73 | binding.activity.registerReceiver( 74 | scanner.adapterStateReceiver, 75 | IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) 76 | ) 77 | binding.activity.registerReceiver( 78 | wifiScanner.adapterStateReceiver, 79 | IntentFilter(WifiManager.ACTION_WIFI_SCAN_AVAILABILITY_CHANGED) 80 | ) 81 | boundActivity = binding.activity 82 | } 83 | 84 | override fun onDetachedFromActivityForConfigChanges() {} 85 | 86 | override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {} 87 | 88 | override fun onDetachedFromActivity() { 89 | boundActivity?.unregisterReceiver(scanner.adapterStateReceiver) 90 | boundActivity?.unregisterReceiver(wifiScanner.adapterStateReceiver) 91 | } 92 | 93 | @RequiresApi(Build.VERSION_CODES.O) 94 | override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { 95 | Pigeon.Api.setup(binding.binaryMessenger, null) 96 | if(scanner.isScanning) 97 | scanner.cancel() 98 | if(wifiScanner.isScanning) 99 | wifiScanner.cancel() 100 | if(wifiNaNScanner.isScanning) 101 | wifiNaNScanner.cancel() 102 | StreamHandler.clearMultipleHandlers( 103 | binding.binaryMessenger, 104 | listOf( 105 | "flutter_odid_data_bt", 106 | "flutter_odid_data_wifi", 107 | "flutter_odid_state_bt", 108 | "flutter_odid_state_wifi", 109 | ) 110 | ) 111 | } 112 | 113 | @RequiresApi(Build.VERSION_CODES.O) 114 | override fun startScanBluetooth(result: Pigeon.Result) { 115 | scanner.scan() 116 | result.success(null) 117 | } 118 | 119 | override fun startScanWifi(result: Pigeon.Result) { 120 | wifiScanner.scan() 121 | wifiNaNScanner.scan() 122 | result.success(null) 123 | } 124 | 125 | override fun stopScanBluetooth(result: Pigeon.Result) { 126 | scanner.cancel() 127 | result.success(null) 128 | } 129 | 130 | @RequiresApi(Build.VERSION_CODES.O) 131 | override fun stopScanWifi(result: Pigeon.Result) { 132 | wifiScanner.cancel() 133 | wifiNaNScanner.cancel() 134 | result.success(null) 135 | } 136 | 137 | override fun setBtScanPriority(priority: Pigeon.ScanPriority, result: Pigeon.Result) { 138 | scanner.setScanPriority(priority) 139 | result.success(null) 140 | } 141 | 142 | override fun isScanningBluetooth(result: Pigeon.Result){ 143 | result.success(scanner.isScanning) 144 | } 145 | 146 | override fun isScanningWifi(result: Pigeon.Result){ 147 | result.success(wifiScanner.isScanning || wifiNaNScanner.isScanning) 148 | } 149 | 150 | override fun bluetoothState(result: Pigeon.Result){ 151 | result.success(scanner.getAdapterState().toLong()) 152 | } 153 | 154 | override fun wifiState(result: Pigeon.Result){ 155 | result.success(wifiScanner.getAdapterState().toLong()) 156 | } 157 | 158 | @RequiresApi(Build.VERSION_CODES.O) 159 | override fun btExtendedSupported(result: Pigeon.Result) { 160 | result.success(scanner.isBtExtendedSupported()); 161 | } 162 | 163 | @RequiresApi(Build.VERSION_CODES.O) 164 | override fun btMaxAdvDataLen(result: Pigeon.Result) { 165 | result.success(scanner.maxAdvDataLen().toLong()); 166 | } 167 | 168 | override fun wifiNaNSupported(result: Pigeon.Result) { 169 | result.success(wifiNaNScanner.isWifiAwareSupported()); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /android/src/main/kotlin/cz/dronetag/flutter_opendroneid/StreamHandler.kt: -------------------------------------------------------------------------------- 1 | package cz.dronetag.flutter_opendroneid 2 | 3 | import io.flutter.plugin.common.BinaryMessenger 4 | import io.flutter.plugin.common.EventChannel 5 | 6 | open class StreamHandler : EventChannel.StreamHandler { 7 | private var eventSink: EventChannel.EventSink? = null 8 | 9 | override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { 10 | if (events != null) { 11 | eventSink = events 12 | } 13 | } 14 | 15 | override fun onCancel(arguments: Any?) { 16 | eventSink = null 17 | } 18 | 19 | fun send(data: Any) { 20 | eventSink?.success(data) 21 | } 22 | 23 | companion object { 24 | fun bindMultipleHandlers(messenger: BinaryMessenger, bindings: Map) { 25 | for (binding in bindings) { 26 | EventChannel(messenger, binding.key).setStreamHandler(binding.value) 27 | } 28 | } 29 | fun clearMultipleHandlers(messenger: BinaryMessenger, names: List) { 30 | for (name in names) { 31 | EventChannel(messenger, name).setStreamHandler(null) 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /android/src/main/kotlin/cz/dronetag/flutter_opendroneid/scanner/BluetoothScanner.kt: -------------------------------------------------------------------------------- 1 | package cz.dronetag.flutter_opendroneid 2 | 3 | import android.bluetooth.BluetoothAdapter 4 | import android.bluetooth.BluetoothDevice 5 | import android.bluetooth.le.* 6 | import android.content.BroadcastReceiver 7 | import android.content.Context 8 | import android.content.Intent 9 | import android.os.Build 10 | import android.os.ParcelUuid 11 | import androidx.annotation.RequiresApi 12 | import io.flutter.Log 13 | import java.util.* 14 | import java.nio.ByteBuffer 15 | import java.nio.ByteOrder 16 | 17 | class BluetoothScanner( 18 | odidPayloadStreamHandler: StreamHandler, 19 | private val bluetoothStateHandler: StreamHandler, 20 | ) : ODIDScanner(odidPayloadStreamHandler) { 21 | 22 | companion object { 23 | const val BT_OFFSET = 6 24 | const val MAX_BLE_ADV_SIZE = 31 25 | } 26 | 27 | private val TAG: String = BluetoothScanner::class.java.getSimpleName() 28 | private val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() 29 | 30 | private var scanMode = ScanSettings.SCAN_MODE_LOW_LATENCY 31 | 32 | /// OpenDroneID Bluetooth beacons identify themselves by setting the GAP AD Type to 33 | /// "Service Data - 16-bit UUID" and the value to 0xFFFA for ASTM International, ASTM Remote ID. 34 | /// https://www.bluetooth.com/specifications/assigned-numbers/ -> "Generic Access Profile" 35 | /// https://www.bluetooth.com/specifications/assigned-numbers/ -> "16-bit UUIDs" 36 | /// Vol 3, Part B, Section 2.5.1 of the Bluetooth 5.1 Core Specification 37 | /// The AD Application Code is set to 0x0D = Open Drone ID. 38 | private val serviceUuid = UUID.fromString("0000fffa-0000-1000-8000-00805f9b34fb") 39 | private val serviceParcelUuid = ParcelUuid(serviceUuid) 40 | private val odidAdCode = byteArrayOf(0x0D.toByte()) 41 | 42 | /// Callback for receiving data: read data from ScanRecord and call receiveData 43 | private val scanCallback: ScanCallback = object : ScanCallback() { 44 | override fun onScanResult(callbackType: Int, result: ScanResult) { 45 | val scanRecord: ScanRecord = result.scanRecord ?: return 46 | val bytes = scanRecord.bytes ?: return 47 | var source = Pigeon.MessageSource.BLUETOOTH_LEGACY; 48 | 49 | if (bytes.size < BT_OFFSET + MAX_MESSAGE_SIZE) return 50 | 51 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && bluetoothAdapter.isLeCodedPhySupported()) { 52 | if (result.getPrimaryPhy() == BluetoothDevice.PHY_LE_CODED) 53 | source = Pigeon.MessageSource.BLUETOOTH_LONG_RANGE; 54 | } 55 | 56 | // if using BLE, max size of data is MAX_BLE_ADV_SIZE 57 | // if using BT5, data can be longer up to 256 bytes 58 | val isBLE = maxAdvDataLen() <= MAX_BLE_ADV_SIZE 59 | 60 | val metadataBuilder = Pigeon.ODIDMetadata.Builder().apply { 61 | setMacAddress(result.device.address) 62 | setSource(source) 63 | setRssi(result.rssi.toLong()) 64 | setBtName(result.device.name) 65 | setPrimaryPhy(phyFromInt(result.getPrimaryPhy())) 66 | setSecondaryPhy(phyFromInt(result.getSecondaryPhy())) 67 | } 68 | 69 | receiveData( 70 | if(isBLE) getDataFromIndex(bytes, BT_OFFSET, MAX_BLE_ADV_SIZE) else offsetData(bytes, BT_OFFSET), 71 | metadataBuilder.build(), 72 | ) 73 | } 74 | 75 | override fun onBatchScanResults(results: List?) { 76 | Log.e(TAG, "Got batch scan results, unable to handle") 77 | } 78 | 79 | override fun onScanFailed(errorCode: Int) { 80 | Log.e(TAG, "Scan failed: $errorCode") 81 | } 82 | } 83 | 84 | @RequiresApi(Build.VERSION_CODES.O) 85 | override fun scan() { 86 | if (!bluetoothAdapter.isEnabled) return 87 | val bluetoothLeScanner: BluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner 88 | val builder: ScanFilter.Builder = ScanFilter.Builder() 89 | builder.setServiceData(serviceParcelUuid, odidAdCode) 90 | val scanFilters: MutableList = ArrayList() 91 | scanFilters.add(builder.build()) 92 | 93 | logAdapterInfo(bluetoothAdapter) 94 | 95 | var scanSettings = buildScanSettings() 96 | 97 | bluetoothLeScanner.startScan(scanFilters, scanSettings, scanCallback) 98 | isScanning = true 99 | bluetoothStateHandler.send(true) 100 | } 101 | 102 | override fun cancel() { 103 | isScanning = false 104 | bluetoothStateHandler.send(false) 105 | bluetoothAdapter.bluetoothLeScanner.stopScan(scanCallback) 106 | } 107 | 108 | @RequiresApi(Build.VERSION_CODES.O) 109 | override fun onAdapterStateReceived() { 110 | val rawState = bluetoothAdapter.state 111 | if (rawState == BluetoothAdapter.STATE_OFF || rawState == BluetoothAdapter.STATE_TURNING_OFF) { 112 | cancel() 113 | } 114 | } 115 | 116 | fun setScanPriority(priority: Pigeon.ScanPriority) { 117 | if(priority == Pigeon.ScanPriority.HIGH) 118 | { 119 | scanMode = ScanSettings.SCAN_MODE_LOW_LATENCY 120 | } 121 | else 122 | { 123 | scanMode = ScanSettings.SCAN_MODE_LOW_POWER 124 | } 125 | // if scan is running, restart with updated scanMode 126 | if(isScanning){ 127 | bluetoothAdapter.bluetoothLeScanner.stopScan(scanCallback) 128 | scan() 129 | } 130 | 131 | } 132 | 133 | @RequiresApi(Build.VERSION_CODES.O) 134 | fun isBtExtendedSupported(): Boolean 135 | { 136 | return bluetoothAdapter.isLeExtendedAdvertisingSupported; 137 | } 138 | 139 | @RequiresApi(Build.VERSION_CODES.O) 140 | fun maxAdvDataLen() : Int 141 | { 142 | return bluetoothAdapter.leMaximumAdvertisingDataLength; 143 | } 144 | 145 | fun getAdapterState(): Int { 146 | return when (bluetoothAdapter.state) { 147 | BluetoothAdapter.STATE_OFF -> 4 148 | BluetoothAdapter.STATE_ON -> 5 149 | BluetoothAdapter.STATE_TURNING_OFF -> 1 150 | BluetoothAdapter.STATE_TURNING_ON -> 1 151 | else -> 0 152 | } 153 | } 154 | 155 | private fun logAdapterInfo(bluetoothAdapter: BluetoothAdapter) { 156 | Log.i(TAG, "bluetooth LE extended supported: " + bluetoothAdapter.isLeExtendedAdvertisingSupported.toString()) 157 | Log.i(TAG, "bluetooth LE coded phy supported: " + bluetoothAdapter.isLeCodedPhySupported.toString()) 158 | Log.i(TAG, "bluetooth multiple advertisement supported: " + bluetoothAdapter.isMultipleAdvertisementSupported.toString()) 159 | Log.i(TAG, "bluetooth max adv data len:" + bluetoothAdapter.leMaximumAdvertisingDataLength.toString()) 160 | } 161 | 162 | private fun buildScanSettings() : ScanSettings { 163 | var scanSettings = ScanSettings.Builder() 164 | .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) 165 | .build() 166 | 167 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && 168 | bluetoothAdapter.isLeCodedPhySupported && 169 | bluetoothAdapter.isLeExtendedAdvertisingSupported 170 | ) { 171 | scanSettings = ScanSettings.Builder() 172 | .setScanMode(scanMode) 173 | .setLegacy(false) 174 | .setPhy(ScanSettings.PHY_LE_ALL_SUPPORTED) 175 | .build() 176 | } 177 | return scanSettings 178 | } 179 | 180 | fun phyFromInt(value: Int): Pigeon.BluetoothPhy? = 181 | Pigeon.BluetoothPhy.values().firstOrNull { it.index == value } 182 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/cz/dronetag/flutter_opendroneid/scanner/ODIDScanner.kt: -------------------------------------------------------------------------------- 1 | package cz.dronetag.flutter_opendroneid 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | 7 | /// Contains common functinality for ODID scanners. 8 | /// Derived scanners should use receiveData method that takes raw data and metadata 9 | /// and sends it to stream 10 | /// Creates [ODIDPayload] instances implementing Pigeon PayloadAPI 11 | abstract class ODIDScanner( 12 | private val odidPayloadStreamHandler: StreamHandler 13 | ) : Pigeon.PayloadApi { 14 | 15 | companion object { 16 | const val MAX_MESSAGE_SIZE = 25 17 | } 18 | 19 | var isScanning = false 20 | get() = field 21 | set(value) { 22 | field = value 23 | } 24 | 25 | val adapterStateReceiver: BroadcastReceiver = object : BroadcastReceiver() { 26 | override fun onReceive(context: Context?, intent: Intent?) { 27 | onAdapterStateReceived() 28 | } 29 | } 30 | 31 | abstract fun scan() 32 | 33 | abstract fun cancel() 34 | 35 | abstract fun onAdapterStateReceived() 36 | 37 | override fun buildPayload(rawData: ByteArray, receivedTimestamp: Long, metadata: Pigeon.ODIDMetadata): Pigeon.ODIDPayload { 38 | val builder = Pigeon.ODIDPayload.Builder().apply { 39 | setRawData(rawData) 40 | setReceivedTimestamp(receivedTimestamp) 41 | setMetadata(metadata) 42 | } 43 | 44 | return builder.build() 45 | } 46 | 47 | /// receive data and metadata, create [ODIDPayload] and sent to stream 48 | fun receiveData( 49 | data: ByteArray, metadata: Pigeon.ODIDMetadata, 50 | ) { 51 | val payload = buildPayload( 52 | data, System.currentTimeMillis(), metadata, 53 | ) 54 | 55 | odidPayloadStreamHandler.send(payload.toList() as Any) 56 | } 57 | 58 | /// returns ByteArray without first offset elements 59 | inline fun offsetData(data: ByteArray, offset: Int) : ByteArray = data.copyOfRange(offset, data.size) 60 | 61 | /// returns ByteArray with bytes from start to end 62 | inline fun getDataFromIndex(data: ByteArray, start: Int, end: Int) : ByteArray = data.copyOfRange(start, end) 63 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/cz/dronetag/flutter_opendroneid/scanner/WifiNaNScanner.kt: -------------------------------------------------------------------------------- 1 | package cz.dronetag.flutter_opendroneid 2 | 3 | import android.annotation.TargetApi 4 | import android.content.BroadcastReceiver 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.IntentFilter 8 | import android.net.wifi.aware.AttachCallback 9 | import android.net.wifi.aware.DiscoverySessionCallback 10 | import android.net.wifi.aware.IdentityChangedListener 11 | import android.net.wifi.aware.PeerHandle 12 | import android.net.wifi.aware.SubscribeConfig 13 | import android.net.wifi.aware.SubscribeDiscoverySession 14 | import android.net.wifi.aware.WifiAwareManager 15 | import android.net.wifi.aware.WifiAwareSession 16 | import android.os.Build 17 | import android.os.SystemClock 18 | import io.flutter.Log 19 | import androidx.annotation.NonNull 20 | import androidx.annotation.RequiresApi 21 | import android.content.pm.PackageManager; 22 | import java.lang.StringBuilder 23 | import java.nio.ByteBuffer 24 | import java.nio.ByteOrder 25 | import java.util.Arrays; 26 | import java.util.List; 27 | 28 | 29 | class WifiNaNScanner ( 30 | odidPayloadStreamHandler: StreamHandler, 31 | private val wifiStateHandler: StreamHandler, 32 | private val wifiAwareManager: WifiAwareManager?, 33 | private val context: Context 34 | ) : ODIDScanner(odidPayloadStreamHandler) { 35 | 36 | companion object { 37 | const val WIFI_NAN_OFFSET = 1 38 | } 39 | 40 | private val TAG: String = WifiNaNScanner::class.java.getSimpleName() 41 | private val wifiScanEnabled = true 42 | private var wifiAwareSupported = true 43 | 44 | private var wifiAwareSession: WifiAwareSession? = null 45 | 46 | // callback for Wi-Fi Aware session advertisement 47 | private val attachCallback: AttachCallback = @RequiresApi(Build.VERSION_CODES.O) 48 | object : AttachCallback() { 49 | override fun onAttached(session: WifiAwareSession) { 50 | if (!wifiAwareSupported) return 51 | wifiAwareSession = session 52 | val config = SubscribeConfig.Builder() 53 | .setServiceName("org.opendroneid.remoteid") 54 | .build() 55 | wifiAwareSession!!.subscribe(config, object : DiscoverySessionCallback() { 56 | override fun onServiceDiscovered( 57 | peerHandle: PeerHandle?, 58 | serviceSpecificInfo: ByteArray?, 59 | matchFilter: MutableList? 60 | ) { 61 | if (serviceSpecificInfo != null) { 62 | val metadataBuilder = Pigeon.ODIDMetadata.Builder().apply { 63 | setMacAddress(peerHandle.hashCode().toString()) 64 | setSource(Pigeon.MessageSource.WIFI_NAN) 65 | } 66 | receiveData( 67 | offsetData(serviceSpecificInfo, WIFI_NAN_OFFSET), 68 | metadataBuilder.build() 69 | ) 70 | } 71 | } 72 | }, null) 73 | } 74 | } 75 | 76 | @TargetApi(Build.VERSION_CODES.O) 77 | private val identityChangedListener: IdentityChangedListener = 78 | object : IdentityChangedListener() { 79 | override fun onIdentityChanged(mac: ByteArray) { 80 | val macAddress = arrayOfNulls(mac.size) 81 | var i = 0 82 | for (b in mac) macAddress[i++] = b 83 | } 84 | } 85 | 86 | init{ 87 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || 88 | !context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE)) { 89 | Log.i(TAG, "WiFi Aware is not supported."); 90 | wifiAwareSupported = false 91 | } 92 | else 93 | { 94 | wifiAwareSupported = true 95 | } 96 | } 97 | 98 | override fun scan() { 99 | if (!wifiAwareSupported) return 100 | 101 | isScanning = true 102 | context.registerReceiver(adapterStateReceiver, IntentFilter(WifiAwareManager.ACTION_WIFI_AWARE_STATE_CHANGED)) 103 | startScan(); 104 | } 105 | 106 | @RequiresApi(Build.VERSION_CODES.O) 107 | override fun cancel() { 108 | if (!wifiAwareSupported) return 109 | 110 | isScanning = false; 111 | context.unregisterReceiver(adapterStateReceiver) 112 | stopScan() 113 | } 114 | 115 | @RequiresApi(Build.VERSION_CODES.O) 116 | override fun onAdapterStateReceived() { 117 | if (wifiAwareManager!!.isAvailable) { 118 | Log.i(TAG, "WiFi Aware became available.") 119 | startScan() 120 | } 121 | } 122 | 123 | fun isWifiAwareSupported(): Boolean 124 | { 125 | return wifiAwareSupported; 126 | } 127 | 128 | @TargetApi(Build.VERSION_CODES.O) 129 | private fun startScan() { 130 | if (!wifiAwareSupported) return 131 | if (wifiAwareManager!!.isAvailable) 132 | { 133 | wifiAwareManager.attach( 134 | attachCallback, 135 | identityChangedListener, 136 | null 137 | ) 138 | wifiStateHandler.send(true) 139 | } 140 | } 141 | 142 | @TargetApi(Build.VERSION_CODES.O) 143 | private fun stopScan() { 144 | if (!wifiAwareSupported) return 145 | if (wifiAwareManager != null && wifiAwareManager!!.isAvailable && wifiAwareSession != null) 146 | { 147 | wifiAwareSession!!.close() 148 | wifiStateHandler.send(false) 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/cz/dronetag/flutter_opendroneid/scanner/WifiScanner.kt: -------------------------------------------------------------------------------- 1 | package cz.dronetag.flutter_opendroneid 2 | 3 | import android.net.wifi.WifiManager 4 | import android.net.wifi.ScanResult 5 | import android.os.Build 6 | import android.content.* 7 | import android.net.wifi.ScanResult.InformationElement 8 | import java.nio.ByteBuffer; 9 | import io.flutter.Log 10 | import kotlin.experimental.and 11 | import android.os.CountDownTimer 12 | import java.nio.ByteOrder 13 | import java.util.* 14 | 15 | /// Wi-Fi Beacons Scanner 16 | /// There are 2 ways to control WiFi scan: 17 | /// Continuous scan: Calls startScan() from scan completion callback 18 | /// Periodic scan: countdown timer triggers startScan after expiry of the timer. 19 | /// If phone is debug mode and scan throttling is off, scan is triggered from onReceive() callback. 20 | /// But if scan throttling is turned on on the phone (default setting on the phone), then scan throttling kick in. 21 | /// In case of throttling, startScan() fails. We need timer thread to periodically kick off scanning. 22 | class WifiScanner ( 23 | odidPayloadStreamHandler: StreamHandler, 24 | private val wifiStateHandler: StreamHandler, 25 | private val wifiManager: WifiManager?, 26 | private val context: Context 27 | ) : ODIDScanner(odidPayloadStreamHandler) { 28 | 29 | companion object { 30 | const val WIFI_BEACON_OFFSET = 5 31 | } 32 | 33 | private val TAG: String = WifiScanner::class.java.getSimpleName() 34 | private val CIDLen = 3 35 | private val DRICID = intArrayOf(0xFA, 0x0B, 0xBC) 36 | private val vendorTypeLen = 1 37 | private val vendorTypeValue = 0x0D 38 | private var scanSuccess = 0 39 | private var scanFailed = 0 40 | private val scanTimerInterval = 2 41 | 42 | private var countDownTimer: CountDownTimer? = null 43 | 44 | // callback for receiving Wi-Fi scan results 45 | private val broadcastReceiver = object : BroadcastReceiver() { 46 | override fun onReceive(contxt: Context?, intent: Intent?) { 47 | if (wifiManager == null) { 48 | return 49 | } 50 | val freshScanResult = intent!!.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false) 51 | val action = intent.action 52 | if (freshScanResult && WifiManager.SCAN_RESULTS_AVAILABLE_ACTION == action) { 53 | val wifiList = wifiManager.scanResults 54 | for (scanResult in wifiList) { 55 | try { 56 | handleResult(scanResult) 57 | } catch (e: NoSuchFieldException) { 58 | e.printStackTrace() 59 | } catch (e: IllegalAccessException) { 60 | e.printStackTrace() 61 | } 62 | } 63 | if(isScanning) 64 | scan() 65 | } 66 | } 67 | } 68 | 69 | override fun scan() { 70 | isScanning = true 71 | wifiStateHandler.send(true) 72 | context.registerReceiver(broadcastReceiver, IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) 73 | val ret = wifiManager!!.startScan() 74 | if (ret) { 75 | scanSuccess++ 76 | } else { 77 | scanFailed++ 78 | } 79 | } 80 | 81 | override fun cancel() { 82 | isScanning = false; 83 | context.unregisterReceiver(broadcastReceiver) 84 | wifiStateHandler.send(false) 85 | if (countDownTimer != null) { 86 | countDownTimer!!.cancel(); 87 | } 88 | } 89 | 90 | override fun onAdapterStateReceived() { 91 | if (wifiManager == null) { 92 | return 93 | } 94 | // wifi can be available even if it is turned of 95 | if(wifiManager.isScanAlwaysAvailable()) 96 | return 97 | val rawState = wifiManager.getWifiState() 98 | if (rawState == WifiManager.WIFI_STATE_DISABLED || rawState == WifiManager.WIFI_STATE_DISABLING) { 99 | cancel() 100 | } 101 | } 102 | 103 | fun getAdapterState(): Int { 104 | if (wifiManager == null) { 105 | return 1 106 | } 107 | // wifi can be available even if it is turned of 108 | if(wifiManager.isScanAlwaysAvailable()) 109 | return 3 110 | return when (wifiManager.getWifiState()) { 111 | WifiManager.WIFI_STATE_ENABLED -> 3 112 | else -> 1 113 | } 114 | } 115 | 116 | @Throws(NoSuchFieldException::class, IllegalAccessException::class) 117 | private fun handleResult(scanResult: ScanResult) { 118 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { 119 | // On earlier Android APIs, the information element field is hidden. 120 | // Use reflection to access it. 121 | val value = 122 | ScanResult::class.java.getField("informationElements")[scanResult] 123 | val elements = value as Array 124 | ?: return 125 | for (element in elements) { 126 | val valueId = element.javaClass.getField("id")[element] ?: continue 127 | val id = valueId as Int 128 | if (id == 221) { 129 | val bytes = element.javaClass.getField("bytes")[element] ?: continue 130 | receiveBeaconData(scanResult, bytes as ByteArray) 131 | } 132 | } 133 | } else { 134 | for (element in scanResult.informationElements) { 135 | if (element != null && element.id == 221) { 136 | val bytes = ByteArray(element.bytes.remaining()) 137 | element.bytes.get(bytes) 138 | receiveBeaconData(scanResult, bytes) 139 | } 140 | } 141 | } 142 | } 143 | 144 | private fun receiveBeaconData(scanResult: ScanResult, bytes: ByteArray) { 145 | if(checkDRICID(bytes)){ 146 | val channelWidth = when(scanResult.channelWidth) { 147 | ScanResult.CHANNEL_WIDTH_20MHZ -> 20 148 | ScanResult.CHANNEL_WIDTH_40MHZ -> 40 149 | ScanResult.CHANNEL_WIDTH_80MHZ -> 80 150 | ScanResult.CHANNEL_WIDTH_160MHZ -> 160 151 | ScanResult.CHANNEL_WIDTH_320MHZ -> 320 152 | else -> null 153 | } 154 | 155 | val metadataBuilder = Pigeon.ODIDMetadata.Builder().apply { 156 | setMacAddress(scanResult.BSSID) 157 | setSource(Pigeon.MessageSource.WIFI_BEACON) 158 | setRssi(scanResult.level.toLong()) 159 | setFrequency(scanResult.frequency.toLong()) 160 | setCenterFreq0(scanResult.centerFreq0.toLong()) 161 | setCenterFreq1(scanResult.centerFreq1.toLong()) 162 | setChannelWidthMhz(channelWidth?.toLong()) 163 | } 164 | 165 | receiveData( 166 | offsetData(bytes, WIFI_BEACON_OFFSET), 167 | metadataBuilder.build() 168 | ) 169 | } 170 | } 171 | 172 | private fun checkDRICID(bytes: ByteArray) : Boolean{ 173 | val buf = ByteBuffer.wrap(bytes as ByteArray).asReadOnlyBuffer() 174 | if (buf.remaining() < MAX_MESSAGE_SIZE + WIFI_BEACON_OFFSET){ 175 | return false 176 | } 177 | val dri_CID = ByteArray(CIDLen) 178 | buf.get(dri_CID, 0, CIDLen) 179 | 180 | val vendorType = ByteArray(vendorTypeLen) 181 | buf.get(vendorType, 0, vendorTypeLen) 182 | 183 | return (dri_CID[0] and 0xFF.toByte()) == DRICID.get(0).toByte() 184 | && ((dri_CID[1] and 0xFF.toByte()) == DRICID.get(1).toByte()) 185 | && ((dri_CID[2] and 0xFF.toByte()) == DRICID.get(2).toByte()) 186 | && vendorType[0] == vendorTypeValue.toByte() 187 | } 188 | 189 | private fun startCountDownTimer() { 190 | countDownTimer = object : CountDownTimer( 191 | Long.MAX_VALUE, 192 | (scanTimerInterval * 1000).toLong() 193 | ) { 194 | // This is called after every ScanTimerInterval sec. 195 | override fun onTick(millisUntilFinished: Long) { 196 | if(isScanning) 197 | scan() 198 | } 199 | 200 | override fun onFinish() {} 201 | }.start() 202 | } 203 | } -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "c23637390482d4cf9598c3ce3f2be31aa7332daf" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf 17 | base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf 18 | - platform: android 19 | create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf 20 | base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # flutter_opendroneid_example 2 | 3 | Simple demo application for flutter_opendroneid plugin. 4 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | .cxx/ 9 | 10 | # Remember to never publicly share your keystore. 11 | # See https://flutter.dev/to/reference-keystore 12 | key.properties 13 | **/*.keystore 14 | **/*.jks 15 | -------------------------------------------------------------------------------- /example/android/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("kotlin-android") 4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 | id("dev.flutter.flutter-gradle-plugin") 6 | } 7 | 8 | android { 9 | namespace = "com.example.flutter_opendroneid_example" 10 | compileSdk = flutter.compileSdkVersion 11 | ndkVersion = "27.0.12077973" 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_11 15 | targetCompatibility = JavaVersion.VERSION_11 16 | } 17 | 18 | kotlinOptions { 19 | jvmTarget = JavaVersion.VERSION_11.toString() 20 | } 21 | 22 | defaultConfig { 23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 24 | applicationId = "com.example.flutter_opendroneid_example" 25 | // You can update the following values to match your application needs. 26 | // For more information, see: https://flutter.dev/to/review-gradle-config. 27 | minSdk = flutter.minSdkVersion 28 | targetSdk = flutter.targetSdkVersion 29 | versionCode = flutter.versionCode 30 | versionName = flutter.versionName 31 | } 32 | 33 | buildTypes { 34 | release { 35 | // TODO: Add your own signing config for the release build. 36 | // Signing with the debug keys for now, so `flutter run --release` works. 37 | signingConfig = signingConfigs.getByName("debug") 38 | } 39 | } 40 | } 41 | 42 | flutter { 43 | source = "../.." 44 | } 45 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/flutter_opendroneid_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.flutter_opendroneid_example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity : FlutterActivity() 6 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() 9 | rootProject.layout.buildDirectory.value(newBuildDir) 10 | 11 | subprojects { 12 | val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) 13 | project.layout.buildDirectory.value(newSubprojectBuildDir) 14 | } 15 | subprojects { 16 | project.evaluationDependsOn(":app") 17 | } 18 | 19 | tasks.register("clean") { 20 | delete(rootProject.layout.buildDirectory) 21 | } 22 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip 6 | -------------------------------------------------------------------------------- /example/android/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | val flutterSdkPath = run { 3 | val properties = java.util.Properties() 4 | file("local.properties").inputStream().use { properties.load(it) } 5 | val flutterSdkPath = properties.getProperty("flutter.sdk") 6 | require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } 7 | flutterSdkPath 8 | } 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id("dev.flutter.flutter-plugin-loader") version "1.0.0" 21 | id("com.android.application") version "8.7.0" apply false 22 | id("org.jetbrains.kotlin.android") version "1.8.22" apply false 23 | } 24 | 25 | include(":app") 26 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | 33 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_ios_build_settings(target) 42 | target.build_configurations.each do |config| 43 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ 44 | '$(inherited)', 45 | 46 | 'PERMISSION_BLUETOOTH=1', 47 | ] 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - device_info_plus (0.0.1): 3 | - Flutter 4 | - Flutter (1.0.0) 5 | - flutter_opendroneid (1.0.0): 6 | - Flutter 7 | - permission_handler_apple (9.3.0): 8 | - Flutter 9 | 10 | DEPENDENCIES: 11 | - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) 12 | - Flutter (from `Flutter`) 13 | - flutter_opendroneid (from `.symlinks/plugins/flutter_opendroneid/ios`) 14 | - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) 15 | 16 | EXTERNAL SOURCES: 17 | device_info_plus: 18 | :path: ".symlinks/plugins/device_info_plus/ios" 19 | Flutter: 20 | :path: Flutter 21 | flutter_opendroneid: 22 | :path: ".symlinks/plugins/flutter_opendroneid/ios" 23 | permission_handler_apple: 24 | :path: ".symlinks/plugins/permission_handler_apple/ios" 25 | 26 | SPEC CHECKSUMS: 27 | device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89 28 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 29 | flutter_opendroneid: ee1b2725baecf01042167d32fa89b7267d031cd1 30 | permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d 31 | 32 | PODFILE CHECKSUM: ef626a226dd8eacd0cd4298f2011219ff17634a7 33 | 34 | COCOAPODS: 1.16.2 35 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 64 | 66 | 72 | 73 | 74 | 75 | 81 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | 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. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Flutter Opendroneid Example 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | flutter_opendroneid_example 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | UIApplicationSupportsIndirectInputEvents 30 | 31 | UILaunchStoryboardName 32 | LaunchScreen 33 | UIMainStoryboardFile 34 | Main 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | 46 | NSBluetoothAlwaysUsageDescription 47 | The application needs Bluetooth permission to acquire data from nearby aircraft. 48 | 49 | 50 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/lib/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_opendroneid/models/message_container.dart'; 6 | import 'package:flutter_opendroneid_example/message_container_view.dart'; 7 | import 'package:permission_handler/permission_handler.dart'; 8 | import 'package:flutter_opendroneid/flutter_opendroneid.dart'; 9 | import 'package:flutter_opendroneid/models/dri_source_type.dart'; 10 | 11 | import 'request_permission_button.dart'; 12 | import 'scan_button.dart'; 13 | 14 | class HomePage extends StatefulWidget { 15 | const HomePage({super.key, required this.title}); 16 | 17 | final String title; 18 | 19 | @override 20 | State createState() => _HomePageState(); 21 | } 22 | 23 | class _HomePageState extends State { 24 | int _btMessagesCounter = 0; 25 | int _wifiMessagesCounter = 0; 26 | 27 | MessageContainer? lastMessageContainer; 28 | 29 | StreamSubscription? btSubscription; 30 | StreamSubscription? wifiSubscription; 31 | 32 | @override 33 | void initState() { 34 | btSubscription = 35 | FlutterOpenDroneId.bluetoothMessages.listen((message) => setState(() { 36 | ++_btMessagesCounter; 37 | lastMessageContainer = message; 38 | })); 39 | wifiSubscription = 40 | FlutterOpenDroneId.wifiMessages.listen((message) => setState(() { 41 | ++_wifiMessagesCounter; 42 | lastMessageContainer = message; 43 | })); 44 | super.initState(); 45 | } 46 | 47 | @override 48 | void dispose() { 49 | btSubscription?.cancel(); 50 | wifiSubscription?.cancel(); 51 | super.dispose(); 52 | } 53 | 54 | @override 55 | Widget build(BuildContext context) { 56 | const padding = 8.0; 57 | final isAndroid = Platform.isAndroid; 58 | 59 | return Scaffold( 60 | appBar: AppBar( 61 | backgroundColor: Theme.of(context).colorScheme.inversePrimary, 62 | title: Text(widget.title), 63 | ), 64 | body: SafeArea( 65 | child: Padding( 66 | padding: const EdgeInsets.all(padding), 67 | child: Column( 68 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 69 | children: [ 70 | DefaultTextStyle( 71 | style: Theme.of(context).textTheme.titleMedium!, 72 | child: Column( 73 | children: [ 74 | Row( 75 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 76 | children: [ 77 | const Text('Received Bluetooth Messages:'), 78 | Text( 79 | '$_btMessagesCounter', 80 | ), 81 | ], 82 | ), 83 | if (isAndroid) 84 | Row( 85 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 86 | children: [ 87 | const Text('Received Wi-Fi Messages:'), 88 | Text( 89 | '$_wifiMessagesCounter', 90 | ), 91 | ], 92 | ), 93 | ], 94 | ), 95 | ), 96 | if (lastMessageContainer != null) ...[ 97 | Container( 98 | margin: EdgeInsets.only(top: 8.0), 99 | alignment: Alignment.centerLeft, 100 | child: Text( 101 | 'Last Message Container:', 102 | style: Theme.of(context).textTheme.titleMedium!, 103 | ), 104 | ), 105 | Expanded( 106 | child: MessageContainerView( 107 | messageContainer: lastMessageContainer!), 108 | ), 109 | ], 110 | Column( 111 | spacing: padding, 112 | crossAxisAlignment: CrossAxisAlignment.stretch, 113 | children: [ 114 | RequestPermissionButton( 115 | permissions: isAndroid 116 | ? [ 117 | Permission.bluetooth, 118 | Permission.bluetoothConnect, 119 | Permission.bluetoothScan 120 | ] 121 | : [Permission.bluetooth], 122 | name: 'Bluetooth Permission', 123 | ), 124 | if (isAndroid) 125 | RequestPermissionButton( 126 | permissions: [Permission.nearbyWifiDevices], 127 | name: 'Wi-Fi Permission', 128 | ), 129 | if (isAndroid) 130 | RequestPermissionButton( 131 | permissions: [Permission.location], 132 | name: 'Location Permission', 133 | ), 134 | ScanButton( 135 | sourceType: DriSourceType.Bluetooth, 136 | ), 137 | if (isAndroid) 138 | ScanButton( 139 | sourceType: DriSourceType.Wifi, 140 | ), 141 | ], 142 | ) 143 | ], 144 | ), 145 | ), 146 | ), 147 | ); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import 'home_page.dart'; 4 | 5 | void main() { 6 | runApp(const MyApp()); 7 | } 8 | 9 | class MyApp extends StatelessWidget { 10 | const MyApp({super.key}); 11 | 12 | // This widget is the root of your application. 13 | @override 14 | Widget build(BuildContext context) { 15 | return MaterialApp( 16 | title: 'flutter_opendroneid demo', 17 | theme: ThemeData( 18 | colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), 19 | ), 20 | home: const HomePage(title: 'flutter_opendroneid demo'), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/lib/message_container_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_opendroneid/flutter_opendroneid.dart'; 3 | import 'package:flutter_opendroneid/models/message_container.dart'; 4 | 5 | class MessageContainerView extends StatelessWidget { 6 | final MessageContainer messageContainer; 7 | 8 | const MessageContainerView({ 9 | super.key, 10 | required this.messageContainer, 11 | }); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return ListView( 16 | shrinkWrap: true, 17 | children: [ 18 | ...?messageContainer.basicIdMessages?.values.map(_buildMessage), 19 | _buildMessage(messageContainer.locationMessage), 20 | _buildMessage(messageContainer.operatorIdMessage), 21 | _buildMessage(messageContainer.selfIdMessage), 22 | _buildMessage(messageContainer.authenticationMessage), 23 | ], 24 | ); 25 | } 26 | 27 | Widget _buildMessage(ODIDMessage? message) { 28 | if (message != null) return Text('${message.toString()}\n'); 29 | 30 | return SizedBox.shrink(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/lib/request_permission_button.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:permission_handler/permission_handler.dart'; 5 | 6 | class RequestPermissionButton extends StatefulWidget { 7 | final List permissions; 8 | final String name; 9 | 10 | const RequestPermissionButton({ 11 | super.key, 12 | required this.permissions, 13 | required this.name, 14 | }); 15 | 16 | @override 17 | State createState() => 18 | _RequestPermissionButtonState(); 19 | } 20 | 21 | class _RequestPermissionButtonState extends State 22 | with WidgetsBindingObserver { 23 | List _statuses = []; 24 | 25 | @override 26 | void initState() { 27 | WidgetsBinding.instance.addObserver(this); 28 | _checkStatuses(); 29 | super.initState(); 30 | } 31 | 32 | @override 33 | void dispose() { 34 | WidgetsBinding.instance.removeObserver(this); 35 | super.dispose(); 36 | } 37 | 38 | @override 39 | void didChangeAppLifecycleState(AppLifecycleState state) { 40 | // check permission status when app is resumed 41 | if (state == AppLifecycleState.resumed) _checkStatuses(); 42 | } 43 | 44 | void _checkStatuses() async { 45 | final newStatuses = await widget.permissions.map((e) => e.status).wait; 46 | if (!mounted) return; 47 | setState(() { 48 | _statuses = newStatuses; 49 | }); 50 | } 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | final granted = _statuses.isNotEmpty && _statuses.every((e) => e.isGranted); 55 | 56 | return ElevatedButton( 57 | onPressed: granted 58 | ? null 59 | : () async { 60 | await widget.permissions.request(); 61 | if (!mounted) return; 62 | _checkStatuses(); 63 | }, 64 | child: 65 | Text(granted ? '${widget.name} Granted' : 'Request ${widget.name}'), 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example/lib/scan_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_opendroneid/flutter_opendroneid.dart'; 3 | import 'package:flutter_opendroneid/models/dri_source_type.dart'; 4 | 5 | class ScanButton extends StatefulWidget { 6 | final DriSourceType sourceType; 7 | 8 | const ScanButton({ 9 | super.key, 10 | required this.sourceType, 11 | }); 12 | 13 | @override 14 | State createState() => _ScanButtonState(); 15 | } 16 | 17 | class _ScanButtonState extends State { 18 | bool _isScanning = false; 19 | 20 | @override 21 | void initState() { 22 | _checkIsScanning(); 23 | super.initState(); 24 | } 25 | 26 | void _checkIsScanning() async { 27 | final isScanning = widget.sourceType == DriSourceType.Bluetooth 28 | ? await FlutterOpenDroneId.isScanningBluetooth 29 | : await FlutterOpenDroneId.isScanningWifi; 30 | 31 | if (!mounted) return; 32 | setState(() { 33 | _isScanning = isScanning; 34 | }); 35 | } 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return ElevatedButton( 40 | onPressed: () async { 41 | if (_isScanning) 42 | await FlutterOpenDroneId.stopScan(widget.sourceType); 43 | else 44 | await FlutterOpenDroneId.startScan(widget.sourceType); 45 | if (!mounted) return; 46 | _checkIsScanning(); 47 | }, 48 | child: Text('${_isScanning ? 'Stop' : 'Start'} $_sourceName Scan'), 49 | ); 50 | } 51 | 52 | String get _sourceName => switch (widget.sourceType) { 53 | DriSourceType.Bluetooth => 'Bluetooth', 54 | DriSourceType.Wifi => 'Wi-Fi', 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_opendroneid_example 2 | description: "A new Flutter project." 3 | publish_to: 'none' 4 | 5 | version: 1.0.0+1 6 | 7 | environment: 8 | sdk: ">=3.0.0 <4.0.0" 9 | flutter: ">=3.16.7" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | permission_handler: ^11.2.0 15 | flutter_opendroneid: 16 | path: ../ 17 | 18 | flutter: 19 | uses-material-design: true 20 | -------------------------------------------------------------------------------- /flutter_opendroneid_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/flutter_opendroneid_logo.png -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | /Flutter/ephemeral/ 38 | /Flutter/flutter_export_environment.sh -------------------------------------------------------------------------------- /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dronetag/flutter-opendroneid/866bb27cf63c6dbf865e08d19195a1d394d96858/ios/Assets/.gitkeep -------------------------------------------------------------------------------- /ios/Classes/FlutterOpendroneidPlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface FlutterOpendroneidPlugin : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /ios/Classes/FlutterOpendroneidPlugin.m: -------------------------------------------------------------------------------- 1 | #import "FlutterOpendroneidPlugin.h" 2 | #if __has_include() 3 | #import 4 | #else 5 | // Support project import fallback if the generated compatibility header 6 | // is not copied when this plugin is created as a library. 7 | // https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 8 | #import "flutter_opendroneid-Swift.h" 9 | #endif 10 | 11 | @implementation FlutterOpendroneidPlugin 12 | + (void)registerWithRegistrar:(NSObject*)registrar { 13 | [SwiftFlutterOpendroneidPlugin registerWithRegistrar:registrar]; 14 | } 15 | @end 16 | -------------------------------------------------------------------------------- /ios/Classes/Scanner/BluetoothScanner.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreBluetooth 3 | 4 | class BluetoothScanner: NSObject, CBCentralManagerDelegate, DTGPayloadApi { 5 | private let odidPayloadStreamHandler: StreamHandler 6 | private let scanStateHandler: StreamHandler 7 | 8 | private var centralManager: CBCentralManager! 9 | private var scanPriority: DTGScanPriority = .high 10 | private var restartTimer: Timer?; 11 | private let restartIntervalSec: TimeInterval = 120.0 12 | let dispatchQueue: DispatchQueue = DispatchQueue(label: "BluetoothScanner") 13 | 14 | static let serviceUUID = CBUUID(string: "0000fffa-0000-1000-8000-00805f9b34fb") 15 | static let odidAdCode: [UInt8] = [ 0x0D ] 16 | 17 | init(odidPayloadStreamHandler: StreamHandler, scanStateHandler: StreamHandler) { 18 | self.odidPayloadStreamHandler = odidPayloadStreamHandler 19 | self.scanStateHandler = scanStateHandler 20 | 21 | super.init() 22 | centralManager = CBCentralManager(delegate: self, queue: dispatchQueue) 23 | } 24 | 25 | func scan() { 26 | if centralManager.isScanning == true { 27 | updateScanState() 28 | return 29 | } 30 | guard centralManager.state == .poweredOn else { 31 | updateScanState() 32 | return 33 | } 34 | 35 | scanForPeripherals() 36 | if scanPriority == .high { 37 | restartTimer = Timer.scheduledTimer(withTimeInterval: restartIntervalSec, repeats: true) { timer in 38 | self.centralManager.stopScan() 39 | self.scanForPeripherals() 40 | } 41 | } 42 | updateScanState() 43 | } 44 | 45 | func isScanning() -> Bool { 46 | return centralManager.isScanning 47 | } 48 | 49 | func cancel() { 50 | centralManager.stopScan() 51 | if let timer = restartTimer { 52 | timer.invalidate() 53 | } 54 | updateScanState() 55 | } 56 | 57 | func setScanPriority(priority: DTGScanPriority) 58 | { 59 | scanPriority = priority 60 | // if scan is running when settting high prio, call scan to restart and set timer 61 | // if scan is running when setting low prio, just cancel restart timer 62 | if centralManager.isScanning { 63 | if scanPriority == .low { 64 | restartTimer?.invalidate() 65 | } 66 | else { 67 | centralManager.stopScan() 68 | scan() 69 | } 70 | } 71 | } 72 | 73 | func updateScanState() { 74 | scanStateHandler.send(centralManager.isScanning) 75 | } 76 | 77 | func managerState() -> Int{ 78 | return centralManager.state.rawValue 79 | } 80 | 81 | func centralManagerDidUpdateState(_ central: CBCentralManager) { 82 | updateScanState() 83 | } 84 | 85 | func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { 86 | handleOdidMessage(advertisementData: advertisementData, didDiscover: peripheral, rssi: RSSI, offset: 2) 87 | } 88 | 89 | private func scanForPeripherals(){ 90 | centralManager.scanForPeripherals( 91 | withServices: [BluetoothScanner.serviceUUID], 92 | options: [ 93 | CBCentralManagerScanOptionAllowDuplicatesKey: true, 94 | ] 95 | ) 96 | } 97 | 98 | func buildPayloadRawData(_ rawData: FlutterStandardTypedData, receivedTimestamp receivedTimestamp: NSNumber, metadata: DTGODIDMetadata, error: AutoreleasingUnsafeMutablePointer) -> DTGODIDPayload? { 99 | return DTGODIDPayload.make(withRawData: rawData, receivedTimestamp: receivedTimestamp ,metadata: metadata) 100 | } 101 | 102 | private func handleOdidMessage(advertisementData: [String : Any], didDiscover peripheral: CBPeripheral, rssi RSSI: NSNumber, offset: NSNumber){ 103 | guard let data = getOdidData(from: advertisementData, offset: offset) else { 104 | // This advertisement is not an ODID ad data 105 | return 106 | } 107 | var err: FlutterError? 108 | let systimestamp = Int(Date().timeIntervalSince1970 * 1000) 109 | let metadata = DTGODIDMetadata.make(withMacAddress: peripheral.identifier.uuidString, source: DTGMessageSource.bluetoothLegacy, rssi: RSSI.intValue as NSNumber, btName: peripheral.name, frequency: nil, centerFreq0: nil, centerFreq1: nil, channelWidthMhz: nil, primaryPhy: DTGBluetoothPhy.unknown, secondaryPhy: DTGBluetoothPhy.unknown) 110 | let payload = buildPayloadRawData(data, receivedTimestamp: systimestamp as NSNumber , metadata: metadata , error: &err) 111 | 112 | odidPayloadStreamHandler.send(payload!.toList() as Any) 113 | } 114 | 115 | private func getOdidData(from advertisementData: [String : Any], offset: NSNumber) -> FlutterStandardTypedData? { 116 | // Peripheral must have service data 117 | guard let serviceData = advertisementData["kCBAdvDataServiceData"] else { 118 | return nil 119 | } 120 | 121 | let serviceDataDict = serviceData as! Dictionary 122 | 123 | // Find the ODID service UUID 124 | guard serviceDataDict.keys.contains(BluetoothScanner.serviceUUID) else { 125 | return nil 126 | } 127 | 128 | let data = serviceDataDict[BluetoothScanner.serviceUUID] as! Data 129 | // offset data 130 | let dataF = FlutterStandardTypedData(bytes: data.dropFirst(offset.intValue)) 131 | // All data must start with 0x0D 132 | guard data.starts(with: BluetoothScanner.odidAdCode) else { 133 | return nil 134 | } 135 | return dataF 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /ios/Classes/SwiftFlutterOpendroneidPlugin.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @available(iOS 13.0.0, *) 5 | public class SwiftFlutterOpendroneidPlugin: NSObject, FlutterPlugin, DTGApi{ 6 | private static let dataEventChannelName = "flutter_odid_data_bt" 7 | private static let stateEventChannelName = "flutter_odid_state_bt" 8 | private static var eventChannels: [String: FlutterEventChannel] = [:] 9 | 10 | private var bluetoothScanner: BluetoothScanner! 11 | 12 | private let streamHandlers: [String: StreamHandler] = [ 13 | dataEventChannelName: StreamHandler(), 14 | stateEventChannelName: StreamHandler(), 15 | ] 16 | 17 | public static func register(with registrar: FlutterPluginRegistrar) { 18 | let messenger : FlutterBinaryMessenger = registrar.messenger() 19 | let instance : SwiftFlutterOpendroneidPlugin & DTGApi & NSObjectProtocol = SwiftFlutterOpendroneidPlugin.init() 20 | DTGApiSetup(messenger, instance); 21 | 22 | // Register event channels 23 | eventChannels = [ 24 | dataEventChannelName: FlutterEventChannel(name: dataEventChannelName, binaryMessenger: registrar.messenger()), 25 | stateEventChannelName: FlutterEventChannel(name: stateEventChannelName, binaryMessenger: registrar.messenger()), 26 | ] 27 | 28 | // Register stream handlers 29 | for entry in SwiftFlutterOpendroneidPlugin.eventChannels { 30 | entry.value.setStreamHandler( 31 | instance.streamHandlers[entry.key] 32 | ) 33 | } 34 | 35 | // Register bluetooth scanner 36 | instance.bluetoothScanner = BluetoothScanner( 37 | odidPayloadStreamHandler: instance.streamHandlers[dataEventChannelName]!, 38 | scanStateHandler: instance.streamHandlers[stateEventChannelName]! 39 | ) 40 | 41 | registrar.addApplicationDelegate(instance) 42 | } 43 | 44 | public func applicationWillTerminate(_ application: UIApplication) { 45 | for channel in SwiftFlutterOpendroneidPlugin.eventChannels.values { 46 | channel.setStreamHandler(nil) 47 | } 48 | SwiftFlutterOpendroneidPlugin.eventChannels.removeAll() 49 | for handler in streamHandlers.values { 50 | handler.onCancel(withArguments: nil) 51 | } 52 | } 53 | 54 | public func btMaxAdvDataLen() async -> (NSNumber?, FlutterError?) { 55 | return (0, nil) 56 | } 57 | 58 | public func btExtendedSupported() async -> (NSNumber?, FlutterError?) { 59 | return ((false) as NSNumber?, nil) 60 | } 61 | 62 | public func wifiNaNSupported() async -> (NSNumber?, FlutterError?) { 63 | return ((false) as NSNumber?, nil) 64 | } 65 | 66 | public func startScanBluetooth(completion: @escaping (FlutterError?) -> Void) { 67 | bluetoothScanner?.scan() 68 | completion(nil) 69 | } 70 | 71 | public func startScanWifi(completion: @escaping (FlutterError?) -> Void) { 72 | // wifi not used on ios so far 73 | completion(FlutterError.init(code: "unimplemented", message: "Wi-Fi is not available on iOS", details: nil)) 74 | } 75 | 76 | public func stopScanBluetooth(completion: @escaping (FlutterError?) -> Void) { 77 | bluetoothScanner?.cancel() 78 | completion(nil) 79 | } 80 | 81 | public func stopScanWifi(completion: @escaping (FlutterError?) -> Void) { 82 | // wifi not used on ios so far 83 | completion(FlutterError.init(code: "unimplemented", message: "Wi-Fi is not available on iOS", details: nil)) 84 | } 85 | 86 | public func setBtScanPriorityPriority(_ priority: DTGScanPriority) async -> FlutterError? { 87 | bluetoothScanner.setScanPriority(priority: priority) 88 | return nil 89 | } 90 | 91 | public func isScanningBluetooth() async -> (NSNumber?, FlutterError?) { 92 | return ((bluetoothScanner?.isScanning()) as NSNumber?, nil) 93 | } 94 | 95 | public func isScanningWifi() async -> (NSNumber?, FlutterError?) { 96 | return (false as NSNumber?, nil) 97 | } 98 | 99 | public func bluetoothState() async -> (NSNumber?, FlutterError?) { 100 | return ((bluetoothScanner?.managerState()) as NSNumber?, nil) 101 | } 102 | 103 | public func wifiState() async -> (NSNumber?, FlutterError?) { 104 | return ((DTGWifiState.disabled.rawValue) as NSNumber?, nil) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /ios/Classes/flutter_opendroneid.h: -------------------------------------------------------------------------------- 1 | #import "pigeon.h" 2 | -------------------------------------------------------------------------------- /ios/Classes/pigeon.h: -------------------------------------------------------------------------------- 1 | // Autogenerated from Pigeon (v10.1.6), do not edit directly. 2 | // See also: https://pub.dev/packages/pigeon 3 | 4 | #import 5 | 6 | @protocol FlutterBinaryMessenger; 7 | @protocol FlutterMessageCodec; 8 | @class FlutterError; 9 | @class FlutterStandardTypedData; 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /// Higher priority drains battery but receives more data 14 | typedef NS_ENUM(NSUInteger, DTGScanPriority) { 15 | DTGScanPriorityHigh = 0, 16 | DTGScanPriorityLow = 1, 17 | }; 18 | 19 | /// ODID Message Source 20 | typedef NS_ENUM(NSUInteger, DTGMessageSource) { 21 | DTGMessageSourceBluetoothLegacy = 0, 22 | DTGMessageSourceBluetoothLongRange = 1, 23 | DTGMessageSourceWifiNan = 2, 24 | DTGMessageSourceWifiBeacon = 3, 25 | DTGMessageSourceUnknown = 4, 26 | }; 27 | 28 | /// State of the Bluetooth adapter 29 | typedef NS_ENUM(NSUInteger, DTGBluetoothState) { 30 | DTGBluetoothStateUnknown = 0, 31 | DTGBluetoothStateResetting = 1, 32 | DTGBluetoothStateUnsupported = 2, 33 | DTGBluetoothStateUnauthorized = 3, 34 | DTGBluetoothStatePoweredOff = 4, 35 | DTGBluetoothStatePoweredOn = 5, 36 | }; 37 | 38 | /// State of the Wifi adapter 39 | typedef NS_ENUM(NSUInteger, DTGWifiState) { 40 | DTGWifiStateDisabling = 0, 41 | DTGWifiStateDisabled = 1, 42 | DTGWifiStateEnabling = 2, 43 | DTGWifiStateEnabled = 3, 44 | }; 45 | 46 | typedef NS_ENUM(NSUInteger, DTGBluetoothPhy) { 47 | DTGBluetoothPhyNone = 0, 48 | DTGBluetoothPhyPhy1M = 1, 49 | DTGBluetoothPhyPhy2M = 2, 50 | DTGBluetoothPhyPhyLECoded = 3, 51 | DTGBluetoothPhyUnknown = 4, 52 | }; 53 | 54 | @class DTGODIDMetadata; 55 | @class DTGODIDPayload; 56 | 57 | @interface DTGODIDMetadata : NSObject 58 | /// `init` unavailable to enforce nonnull fields, see the `make` class method. 59 | - (instancetype)init NS_UNAVAILABLE; 60 | + (instancetype)makeWithMacAddress:(NSString *)macAddress 61 | source:(DTGMessageSource)source 62 | rssi:(nullable NSNumber *)rssi 63 | btName:(nullable NSString *)btName 64 | frequency:(nullable NSNumber *)frequency 65 | centerFreq0:(nullable NSNumber *)centerFreq0 66 | centerFreq1:(nullable NSNumber *)centerFreq1 67 | channelWidthMhz:(nullable NSNumber *)channelWidthMhz 68 | primaryPhy:(DTGBluetoothPhy)primaryPhy 69 | secondaryPhy:(DTGBluetoothPhy)secondaryPhy; 70 | @property(nonatomic, copy) NSString * macAddress; 71 | @property(nonatomic, assign) DTGMessageSource source; 72 | @property(nonatomic, strong, nullable) NSNumber * rssi; 73 | @property(nonatomic, copy, nullable) NSString * btName; 74 | @property(nonatomic, strong, nullable) NSNumber * frequency; 75 | @property(nonatomic, strong, nullable) NSNumber * centerFreq0; 76 | @property(nonatomic, strong, nullable) NSNumber * centerFreq1; 77 | @property(nonatomic, strong, nullable) NSNumber * channelWidthMhz; 78 | @property(nonatomic, assign) DTGBluetoothPhy primaryPhy; 79 | @property(nonatomic, assign) DTGBluetoothPhy secondaryPhy; 80 | @end 81 | 82 | /// Payload send from native to dart contains raw data and metadata 83 | @interface DTGODIDPayload : NSObject 84 | /// `init` unavailable to enforce nonnull fields, see the `make` class method. 85 | - (NSArray *)toList; 86 | - (instancetype)init NS_UNAVAILABLE; 87 | + (instancetype)makeWithRawData:(FlutterStandardTypedData *)rawData 88 | receivedTimestamp:(NSNumber *)receivedTimestamp 89 | metadata:(DTGODIDMetadata *)metadata; 90 | @property(nonatomic, strong) FlutterStandardTypedData * rawData; 91 | @property(nonatomic, strong) NSNumber * receivedTimestamp; 92 | @property(nonatomic, strong) DTGODIDMetadata * metadata; 93 | @end 94 | 95 | /// The codec used by DTGApi. 96 | NSObject *DTGApiGetCodec(void); 97 | 98 | @protocol DTGApi 99 | - (void)startScanBluetoothWithCompletion:(void (^)(FlutterError *_Nullable))completion; 100 | - (void)startScanWifiWithCompletion:(void (^)(FlutterError *_Nullable))completion; 101 | - (void)stopScanBluetoothWithCompletion:(void (^)(FlutterError *_Nullable))completion; 102 | - (void)stopScanWifiWithCompletion:(void (^)(FlutterError *_Nullable))completion; 103 | - (void)setBtScanPriorityPriority:(DTGScanPriority)priority completion:(void (^)(FlutterError *_Nullable))completion; 104 | - (void)isScanningBluetoothWithCompletion:(void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion; 105 | - (void)isScanningWifiWithCompletion:(void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion; 106 | - (void)bluetoothStateWithCompletion:(void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion; 107 | - (void)wifiStateWithCompletion:(void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion; 108 | - (void)btExtendedSupportedWithCompletion:(void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion; 109 | - (void)btMaxAdvDataLenWithCompletion:(void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion; 110 | - (void)wifiNaNSupportedWithCompletion:(void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion; 111 | @end 112 | 113 | extern void DTGApiSetup(id binaryMessenger, NSObject *_Nullable api); 114 | 115 | /// The codec used by DTGPayloadApi. 116 | NSObject *DTGPayloadApiGetCodec(void); 117 | 118 | @protocol DTGPayloadApi 119 | /// @return `nil` only when `error != nil`. 120 | - (nullable DTGODIDPayload *)buildPayloadRawData:(FlutterStandardTypedData *)rawData receivedTimestamp:(NSNumber *)receivedTimestamp metadata:(DTGODIDMetadata *)metadata error:(FlutterError *_Nullable *_Nonnull)error; 121 | @end 122 | 123 | extern void DTGPayloadApiSetup(id binaryMessenger, NSObject *_Nullable api); 124 | 125 | NS_ASSUME_NONNULL_END 126 | -------------------------------------------------------------------------------- /ios/Classes/pigeon.m: -------------------------------------------------------------------------------- 1 | // Autogenerated from Pigeon (v10.1.6), do not edit directly. 2 | // See also: https://pub.dev/packages/pigeon 3 | 4 | #import "pigeon.h" 5 | 6 | #if TARGET_OS_OSX 7 | #import 8 | #else 9 | #import 10 | #endif 11 | 12 | #if !__has_feature(objc_arc) 13 | #error File requires ARC to be enabled. 14 | #endif 15 | 16 | static NSArray *wrapResult(id result, FlutterError *error) { 17 | if (error) { 18 | return @[ 19 | error.code ?: [NSNull null], error.message ?: [NSNull null], error.details ?: [NSNull null] 20 | ]; 21 | } 22 | return @[ result ?: [NSNull null] ]; 23 | } 24 | static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { 25 | id result = array[key]; 26 | return (result == [NSNull null]) ? nil : result; 27 | } 28 | 29 | @interface DTGODIDMetadata () 30 | + (DTGODIDMetadata *)fromList:(NSArray *)list; 31 | + (nullable DTGODIDMetadata *)nullableFromList:(NSArray *)list; 32 | - (NSArray *)toList; 33 | @end 34 | 35 | @interface DTGODIDPayload () 36 | + (DTGODIDPayload *)fromList:(NSArray *)list; 37 | + (nullable DTGODIDPayload *)nullableFromList:(NSArray *)list; 38 | - (NSArray *)toList; 39 | @end 40 | 41 | @implementation DTGODIDMetadata 42 | + (instancetype)makeWithMacAddress:(NSString *)macAddress 43 | source:(DTGMessageSource)source 44 | rssi:(nullable NSNumber *)rssi 45 | btName:(nullable NSString *)btName 46 | frequency:(nullable NSNumber *)frequency 47 | centerFreq0:(nullable NSNumber *)centerFreq0 48 | centerFreq1:(nullable NSNumber *)centerFreq1 49 | channelWidthMhz:(nullable NSNumber *)channelWidthMhz 50 | primaryPhy:(DTGBluetoothPhy)primaryPhy 51 | secondaryPhy:(DTGBluetoothPhy)secondaryPhy { 52 | DTGODIDMetadata* pigeonResult = [[DTGODIDMetadata alloc] init]; 53 | pigeonResult.macAddress = macAddress; 54 | pigeonResult.source = source; 55 | pigeonResult.rssi = rssi; 56 | pigeonResult.btName = btName; 57 | pigeonResult.frequency = frequency; 58 | pigeonResult.centerFreq0 = centerFreq0; 59 | pigeonResult.centerFreq1 = centerFreq1; 60 | pigeonResult.channelWidthMhz = channelWidthMhz; 61 | pigeonResult.primaryPhy = primaryPhy; 62 | pigeonResult.secondaryPhy = secondaryPhy; 63 | return pigeonResult; 64 | } 65 | + (DTGODIDMetadata *)fromList:(NSArray *)list { 66 | DTGODIDMetadata *pigeonResult = [[DTGODIDMetadata alloc] init]; 67 | pigeonResult.macAddress = GetNullableObjectAtIndex(list, 0); 68 | NSAssert(pigeonResult.macAddress != nil, @""); 69 | pigeonResult.source = [GetNullableObjectAtIndex(list, 1) integerValue]; 70 | pigeonResult.rssi = GetNullableObjectAtIndex(list, 2); 71 | pigeonResult.btName = GetNullableObjectAtIndex(list, 3); 72 | pigeonResult.frequency = GetNullableObjectAtIndex(list, 4); 73 | pigeonResult.centerFreq0 = GetNullableObjectAtIndex(list, 5); 74 | pigeonResult.centerFreq1 = GetNullableObjectAtIndex(list, 6); 75 | pigeonResult.channelWidthMhz = GetNullableObjectAtIndex(list, 7); 76 | pigeonResult.primaryPhy = [GetNullableObjectAtIndex(list, 8) integerValue]; 77 | pigeonResult.secondaryPhy = [GetNullableObjectAtIndex(list, 9) integerValue]; 78 | return pigeonResult; 79 | } 80 | + (nullable DTGODIDMetadata *)nullableFromList:(NSArray *)list { 81 | return (list) ? [DTGODIDMetadata fromList:list] : nil; 82 | } 83 | - (NSArray *)toList { 84 | return @[ 85 | (self.macAddress ?: [NSNull null]), 86 | @(self.source), 87 | (self.rssi ?: [NSNull null]), 88 | (self.btName ?: [NSNull null]), 89 | (self.frequency ?: [NSNull null]), 90 | (self.centerFreq0 ?: [NSNull null]), 91 | (self.centerFreq1 ?: [NSNull null]), 92 | (self.channelWidthMhz ?: [NSNull null]), 93 | @(self.primaryPhy), 94 | @(self.secondaryPhy), 95 | ]; 96 | } 97 | @end 98 | 99 | @implementation DTGODIDPayload 100 | + (instancetype)makeWithRawData:(FlutterStandardTypedData *)rawData 101 | receivedTimestamp:(NSNumber *)receivedTimestamp 102 | metadata:(DTGODIDMetadata *)metadata { 103 | DTGODIDPayload* pigeonResult = [[DTGODIDPayload alloc] init]; 104 | pigeonResult.rawData = rawData; 105 | pigeonResult.receivedTimestamp = receivedTimestamp; 106 | pigeonResult.metadata = metadata; 107 | return pigeonResult; 108 | } 109 | + (DTGODIDPayload *)fromList:(NSArray *)list { 110 | DTGODIDPayload *pigeonResult = [[DTGODIDPayload alloc] init]; 111 | pigeonResult.rawData = GetNullableObjectAtIndex(list, 0); 112 | NSAssert(pigeonResult.rawData != nil, @""); 113 | pigeonResult.receivedTimestamp = GetNullableObjectAtIndex(list, 1); 114 | NSAssert(pigeonResult.receivedTimestamp != nil, @""); 115 | pigeonResult.metadata = [DTGODIDMetadata nullableFromList:(GetNullableObjectAtIndex(list, 2))]; 116 | NSAssert(pigeonResult.metadata != nil, @""); 117 | return pigeonResult; 118 | } 119 | + (nullable DTGODIDPayload *)nullableFromList:(NSArray *)list { 120 | return (list) ? [DTGODIDPayload fromList:list] : nil; 121 | } 122 | - (NSArray *)toList { 123 | return @[ 124 | (self.rawData ?: [NSNull null]), 125 | (self.receivedTimestamp ?: [NSNull null]), 126 | (self.metadata ? [self.metadata toList] : [NSNull null]), 127 | ]; 128 | } 129 | @end 130 | 131 | NSObject *DTGApiGetCodec(void) { 132 | static FlutterStandardMessageCodec *sSharedObject = nil; 133 | sSharedObject = [FlutterStandardMessageCodec sharedInstance]; 134 | return sSharedObject; 135 | } 136 | 137 | void DTGApiSetup(id binaryMessenger, NSObject *api) { 138 | { 139 | FlutterBasicMessageChannel *channel = 140 | [[FlutterBasicMessageChannel alloc] 141 | initWithName:@"dev.flutter.pigeon.flutter_opendroneid.Api.startScanBluetooth" 142 | binaryMessenger:binaryMessenger 143 | codec:DTGApiGetCodec()]; 144 | if (api) { 145 | NSCAssert([api respondsToSelector:@selector(startScanBluetoothWithCompletion:)], @"DTGApi api (%@) doesn't respond to @selector(startScanBluetoothWithCompletion:)", api); 146 | [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { 147 | [api startScanBluetoothWithCompletion:^(FlutterError *_Nullable error) { 148 | callback(wrapResult(nil, error)); 149 | }]; 150 | }]; 151 | } else { 152 | [channel setMessageHandler:nil]; 153 | } 154 | } 155 | { 156 | FlutterBasicMessageChannel *channel = 157 | [[FlutterBasicMessageChannel alloc] 158 | initWithName:@"dev.flutter.pigeon.flutter_opendroneid.Api.startScanWifi" 159 | binaryMessenger:binaryMessenger 160 | codec:DTGApiGetCodec()]; 161 | if (api) { 162 | NSCAssert([api respondsToSelector:@selector(startScanWifiWithCompletion:)], @"DTGApi api (%@) doesn't respond to @selector(startScanWifiWithCompletion:)", api); 163 | [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { 164 | [api startScanWifiWithCompletion:^(FlutterError *_Nullable error) { 165 | callback(wrapResult(nil, error)); 166 | }]; 167 | }]; 168 | } else { 169 | [channel setMessageHandler:nil]; 170 | } 171 | } 172 | { 173 | FlutterBasicMessageChannel *channel = 174 | [[FlutterBasicMessageChannel alloc] 175 | initWithName:@"dev.flutter.pigeon.flutter_opendroneid.Api.stopScanBluetooth" 176 | binaryMessenger:binaryMessenger 177 | codec:DTGApiGetCodec()]; 178 | if (api) { 179 | NSCAssert([api respondsToSelector:@selector(stopScanBluetoothWithCompletion:)], @"DTGApi api (%@) doesn't respond to @selector(stopScanBluetoothWithCompletion:)", api); 180 | [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { 181 | [api stopScanBluetoothWithCompletion:^(FlutterError *_Nullable error) { 182 | callback(wrapResult(nil, error)); 183 | }]; 184 | }]; 185 | } else { 186 | [channel setMessageHandler:nil]; 187 | } 188 | } 189 | { 190 | FlutterBasicMessageChannel *channel = 191 | [[FlutterBasicMessageChannel alloc] 192 | initWithName:@"dev.flutter.pigeon.flutter_opendroneid.Api.stopScanWifi" 193 | binaryMessenger:binaryMessenger 194 | codec:DTGApiGetCodec()]; 195 | if (api) { 196 | NSCAssert([api respondsToSelector:@selector(stopScanWifiWithCompletion:)], @"DTGApi api (%@) doesn't respond to @selector(stopScanWifiWithCompletion:)", api); 197 | [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { 198 | [api stopScanWifiWithCompletion:^(FlutterError *_Nullable error) { 199 | callback(wrapResult(nil, error)); 200 | }]; 201 | }]; 202 | } else { 203 | [channel setMessageHandler:nil]; 204 | } 205 | } 206 | { 207 | FlutterBasicMessageChannel *channel = 208 | [[FlutterBasicMessageChannel alloc] 209 | initWithName:@"dev.flutter.pigeon.flutter_opendroneid.Api.setBtScanPriority" 210 | binaryMessenger:binaryMessenger 211 | codec:DTGApiGetCodec()]; 212 | if (api) { 213 | NSCAssert([api respondsToSelector:@selector(setBtScanPriorityPriority:completion:)], @"DTGApi api (%@) doesn't respond to @selector(setBtScanPriorityPriority:completion:)", api); 214 | [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { 215 | NSArray *args = message; 216 | DTGScanPriority arg_priority = [GetNullableObjectAtIndex(args, 0) integerValue]; 217 | [api setBtScanPriorityPriority:arg_priority completion:^(FlutterError *_Nullable error) { 218 | callback(wrapResult(nil, error)); 219 | }]; 220 | }]; 221 | } else { 222 | [channel setMessageHandler:nil]; 223 | } 224 | } 225 | { 226 | FlutterBasicMessageChannel *channel = 227 | [[FlutterBasicMessageChannel alloc] 228 | initWithName:@"dev.flutter.pigeon.flutter_opendroneid.Api.isScanningBluetooth" 229 | binaryMessenger:binaryMessenger 230 | codec:DTGApiGetCodec()]; 231 | if (api) { 232 | NSCAssert([api respondsToSelector:@selector(isScanningBluetoothWithCompletion:)], @"DTGApi api (%@) doesn't respond to @selector(isScanningBluetoothWithCompletion:)", api); 233 | [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { 234 | [api isScanningBluetoothWithCompletion:^(NSNumber *_Nullable output, FlutterError *_Nullable error) { 235 | callback(wrapResult(output, error)); 236 | }]; 237 | }]; 238 | } else { 239 | [channel setMessageHandler:nil]; 240 | } 241 | } 242 | { 243 | FlutterBasicMessageChannel *channel = 244 | [[FlutterBasicMessageChannel alloc] 245 | initWithName:@"dev.flutter.pigeon.flutter_opendroneid.Api.isScanningWifi" 246 | binaryMessenger:binaryMessenger 247 | codec:DTGApiGetCodec()]; 248 | if (api) { 249 | NSCAssert([api respondsToSelector:@selector(isScanningWifiWithCompletion:)], @"DTGApi api (%@) doesn't respond to @selector(isScanningWifiWithCompletion:)", api); 250 | [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { 251 | [api isScanningWifiWithCompletion:^(NSNumber *_Nullable output, FlutterError *_Nullable error) { 252 | callback(wrapResult(output, error)); 253 | }]; 254 | }]; 255 | } else { 256 | [channel setMessageHandler:nil]; 257 | } 258 | } 259 | { 260 | FlutterBasicMessageChannel *channel = 261 | [[FlutterBasicMessageChannel alloc] 262 | initWithName:@"dev.flutter.pigeon.flutter_opendroneid.Api.bluetoothState" 263 | binaryMessenger:binaryMessenger 264 | codec:DTGApiGetCodec()]; 265 | if (api) { 266 | NSCAssert([api respondsToSelector:@selector(bluetoothStateWithCompletion:)], @"DTGApi api (%@) doesn't respond to @selector(bluetoothStateWithCompletion:)", api); 267 | [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { 268 | [api bluetoothStateWithCompletion:^(NSNumber *_Nullable output, FlutterError *_Nullable error) { 269 | callback(wrapResult(output, error)); 270 | }]; 271 | }]; 272 | } else { 273 | [channel setMessageHandler:nil]; 274 | } 275 | } 276 | { 277 | FlutterBasicMessageChannel *channel = 278 | [[FlutterBasicMessageChannel alloc] 279 | initWithName:@"dev.flutter.pigeon.flutter_opendroneid.Api.wifiState" 280 | binaryMessenger:binaryMessenger 281 | codec:DTGApiGetCodec()]; 282 | if (api) { 283 | NSCAssert([api respondsToSelector:@selector(wifiStateWithCompletion:)], @"DTGApi api (%@) doesn't respond to @selector(wifiStateWithCompletion:)", api); 284 | [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { 285 | [api wifiStateWithCompletion:^(NSNumber *_Nullable output, FlutterError *_Nullable error) { 286 | callback(wrapResult(output, error)); 287 | }]; 288 | }]; 289 | } else { 290 | [channel setMessageHandler:nil]; 291 | } 292 | } 293 | { 294 | FlutterBasicMessageChannel *channel = 295 | [[FlutterBasicMessageChannel alloc] 296 | initWithName:@"dev.flutter.pigeon.flutter_opendroneid.Api.btExtendedSupported" 297 | binaryMessenger:binaryMessenger 298 | codec:DTGApiGetCodec()]; 299 | if (api) { 300 | NSCAssert([api respondsToSelector:@selector(btExtendedSupportedWithCompletion:)], @"DTGApi api (%@) doesn't respond to @selector(btExtendedSupportedWithCompletion:)", api); 301 | [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { 302 | [api btExtendedSupportedWithCompletion:^(NSNumber *_Nullable output, FlutterError *_Nullable error) { 303 | callback(wrapResult(output, error)); 304 | }]; 305 | }]; 306 | } else { 307 | [channel setMessageHandler:nil]; 308 | } 309 | } 310 | { 311 | FlutterBasicMessageChannel *channel = 312 | [[FlutterBasicMessageChannel alloc] 313 | initWithName:@"dev.flutter.pigeon.flutter_opendroneid.Api.btMaxAdvDataLen" 314 | binaryMessenger:binaryMessenger 315 | codec:DTGApiGetCodec()]; 316 | if (api) { 317 | NSCAssert([api respondsToSelector:@selector(btMaxAdvDataLenWithCompletion:)], @"DTGApi api (%@) doesn't respond to @selector(btMaxAdvDataLenWithCompletion:)", api); 318 | [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { 319 | [api btMaxAdvDataLenWithCompletion:^(NSNumber *_Nullable output, FlutterError *_Nullable error) { 320 | callback(wrapResult(output, error)); 321 | }]; 322 | }]; 323 | } else { 324 | [channel setMessageHandler:nil]; 325 | } 326 | } 327 | { 328 | FlutterBasicMessageChannel *channel = 329 | [[FlutterBasicMessageChannel alloc] 330 | initWithName:@"dev.flutter.pigeon.flutter_opendroneid.Api.wifiNaNSupported" 331 | binaryMessenger:binaryMessenger 332 | codec:DTGApiGetCodec()]; 333 | if (api) { 334 | NSCAssert([api respondsToSelector:@selector(wifiNaNSupportedWithCompletion:)], @"DTGApi api (%@) doesn't respond to @selector(wifiNaNSupportedWithCompletion:)", api); 335 | [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { 336 | [api wifiNaNSupportedWithCompletion:^(NSNumber *_Nullable output, FlutterError *_Nullable error) { 337 | callback(wrapResult(output, error)); 338 | }]; 339 | }]; 340 | } else { 341 | [channel setMessageHandler:nil]; 342 | } 343 | } 344 | } 345 | @interface DTGPayloadApiCodecReader : FlutterStandardReader 346 | @end 347 | @implementation DTGPayloadApiCodecReader 348 | - (nullable id)readValueOfType:(UInt8)type { 349 | switch (type) { 350 | case 128: 351 | return [DTGODIDMetadata fromList:[self readValue]]; 352 | case 129: 353 | return [DTGODIDPayload fromList:[self readValue]]; 354 | default: 355 | return [super readValueOfType:type]; 356 | } 357 | } 358 | @end 359 | 360 | @interface DTGPayloadApiCodecWriter : FlutterStandardWriter 361 | @end 362 | @implementation DTGPayloadApiCodecWriter 363 | - (void)writeValue:(id)value { 364 | if ([value isKindOfClass:[DTGODIDMetadata class]]) { 365 | [self writeByte:128]; 366 | [self writeValue:[value toList]]; 367 | } else if ([value isKindOfClass:[DTGODIDPayload class]]) { 368 | [self writeByte:129]; 369 | [self writeValue:[value toList]]; 370 | } else { 371 | [super writeValue:value]; 372 | } 373 | } 374 | @end 375 | 376 | @interface DTGPayloadApiCodecReaderWriter : FlutterStandardReaderWriter 377 | @end 378 | @implementation DTGPayloadApiCodecReaderWriter 379 | - (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { 380 | return [[DTGPayloadApiCodecWriter alloc] initWithData:data]; 381 | } 382 | - (FlutterStandardReader *)readerWithData:(NSData *)data { 383 | return [[DTGPayloadApiCodecReader alloc] initWithData:data]; 384 | } 385 | @end 386 | 387 | NSObject *DTGPayloadApiGetCodec(void) { 388 | static FlutterStandardMessageCodec *sSharedObject = nil; 389 | static dispatch_once_t sPred = 0; 390 | dispatch_once(&sPred, ^{ 391 | DTGPayloadApiCodecReaderWriter *readerWriter = [[DTGPayloadApiCodecReaderWriter alloc] init]; 392 | sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; 393 | }); 394 | return sSharedObject; 395 | } 396 | 397 | void DTGPayloadApiSetup(id binaryMessenger, NSObject *api) { 398 | { 399 | FlutterBasicMessageChannel *channel = 400 | [[FlutterBasicMessageChannel alloc] 401 | initWithName:@"dev.flutter.pigeon.flutter_opendroneid.PayloadApi.buildPayload" 402 | binaryMessenger:binaryMessenger 403 | codec:DTGPayloadApiGetCodec()]; 404 | if (api) { 405 | NSCAssert([api respondsToSelector:@selector(buildPayloadRawData:receivedTimestamp:metadata:error:)], @"DTGPayloadApi api (%@) doesn't respond to @selector(buildPayloadRawData:receivedTimestamp:metadata:error:)", api); 406 | [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { 407 | NSArray *args = message; 408 | FlutterStandardTypedData *arg_rawData = GetNullableObjectAtIndex(args, 0); 409 | NSNumber *arg_receivedTimestamp = GetNullableObjectAtIndex(args, 1); 410 | DTGODIDMetadata *arg_metadata = GetNullableObjectAtIndex(args, 2); 411 | FlutterError *error; 412 | DTGODIDPayload *output = [api buildPayloadRawData:arg_rawData receivedTimestamp:arg_receivedTimestamp metadata:arg_metadata error:&error]; 413 | callback(wrapResult(output, error)); 414 | }]; 415 | } else { 416 | [channel setMessageHandler:nil]; 417 | } 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /ios/Classes/utils/StreamHandler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Flutter 3 | 4 | class StreamHandler: NSObject, FlutterStreamHandler { 5 | var eventSink: FlutterEventSink? 6 | 7 | func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { 8 | self.eventSink = events 9 | return nil 10 | } 11 | 12 | func onCancel(withArguments arguments: Any?) -> FlutterError? { 13 | self.eventSink = nil 14 | return nil 15 | } 16 | 17 | func send(_ data: Any) { 18 | if let eventSink = self.eventSink { 19 | eventSink(data) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ios/Classes/utils/Utils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Data { 4 | var uint8: UInt8 { 5 | get { 6 | var number: UInt8 = 0 7 | self.copyBytes(to:&number, count: MemoryLayout.size) 8 | return number 9 | } 10 | } 11 | 12 | var uint16: UInt16 { 13 | get { 14 | let i16array = self.withUnsafeBytes { $0.load(as: UInt16.self) } 15 | return i16array 16 | } 17 | } 18 | 19 | var uint32: UInt32 { 20 | get { 21 | let i32array = self.withUnsafeBytes { $0.load(as: UInt32.self) } 22 | return i32array 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ios/flutter_opendroneid.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint flutter_opendroneid.podspec` to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'flutter_opendroneid' 7 | s.version = '1.0.0' 8 | s.summary = 'iOS implementation for reading Wi-Fi and Bluetooth Remote ID advertisements as Flutter plugin' 9 | s.authors = 'Dronetag s.r.o.' 10 | s.license = {} 11 | s.homepage = 'https://github.com/dronetag/flutter-opendroneid' 12 | s.source = { :path => '.' } 13 | s.source_files = 'Classes/**/*' 14 | s.platform = :ios, '8.0' 15 | 16 | s.dependency 'Flutter' 17 | 18 | # Flutter.framework does not contain a i386 slice. 19 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } 20 | s.swift_version = '5.0' 21 | end 22 | -------------------------------------------------------------------------------- /lib/exceptions/odid_message_parsing_exception.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_opendroneid/pigeon.dart'; 2 | 3 | class ODIDMessageParsingException implements Exception { 4 | final Object relatedException; 5 | final String macAddress; 6 | final String? btName; 7 | final MessageSource source; 8 | final int? rssi; 9 | final int receivedTimestamp; 10 | 11 | ODIDMessageParsingException({ 12 | required this.relatedException, 13 | required this.macAddress, 14 | required this.btName, 15 | required this.source, 16 | required this.receivedTimestamp, 17 | this.rssi, 18 | }); 19 | 20 | @override 21 | String toString() => 22 | 'ODIDMessageParsingException{ exception: $relatedException, ' 23 | 'mac: $macAddress, btName: $btName, source: $source, ' 24 | 'rssi: $rssi, receivedTimestamp: $receivedTimestamp }'; 25 | } 26 | -------------------------------------------------------------------------------- /lib/extensions/compare_extension.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_opendroneid/dart_opendroneid.dart'; 2 | 3 | // extensions to check whether messages have the same data 4 | // The messages may not necessarily be the same, timestamp, rssi are ignored 5 | // compares just relevant data fields 6 | extension LocationMessageCompareExtension on LocationMessage { 7 | // consider location with same timestamp equal 8 | bool containsEqualData(LocationMessage other) { 9 | return timestamp == other.timestamp && 10 | location?.latitude == other.location?.latitude && 11 | location?.longitude == other.location?.longitude; 12 | } 13 | } 14 | 15 | extension BasicIDCompareExtension on BasicIDMessage { 16 | bool containsEqualData(BasicIDMessage other) { 17 | return uasID == other.uasID && uaType == other.uaType; 18 | } 19 | } 20 | 21 | extension OperatorIDMessageCompareExtension on OperatorIDMessage { 22 | bool containsEqualData(OperatorIDMessage other) { 23 | return operatorID == other.operatorID; 24 | } 25 | } 26 | 27 | extension SystemMessageCompareExtension on SystemMessage { 28 | bool containsEqualData(SystemMessage other) { 29 | return areaCeiling == other.areaCeiling && 30 | areaCount == other.areaCount && 31 | areaFloor == other.areaFloor && 32 | areaRadius == other.areaRadius && 33 | uaClassification == other.uaClassification && 34 | timestamp == other.timestamp && 35 | operatorAltitude == other.operatorAltitude && 36 | operatorLocationType == other.operatorLocationType && 37 | operatorLocation == other.operatorLocation; 38 | } 39 | } 40 | 41 | extension AuthenticationCompareExtension on AuthMessage { 42 | bool containsEqualData(AuthMessage other) { 43 | return rawContent == other.rawContent; 44 | } 45 | } 46 | 47 | extension SelfIDCompareExtension on SelfIDMessage { 48 | bool containsEqualData(SelfIDMessage other) { 49 | return descriptionType == other.descriptionType && 50 | description == other.description; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/extensions/list_extension.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | extension HexStringExtension on Uint8List { 4 | String toHexString() => map((byte) => 5 | byte.toUnsigned(8).toRadixString(16).padLeft(2, '0').toUpperCase()) 6 | .join(); 7 | } 8 | -------------------------------------------------------------------------------- /lib/flutter_opendroneid.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:dart_opendroneid/dart_opendroneid.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:flutter_opendroneid/exceptions/odid_message_parsing_exception.dart'; 7 | import 'package:flutter_opendroneid/models/dri_source_type.dart'; 8 | import 'package:flutter_opendroneid/models/message_container.dart'; 9 | import 'package:flutter_opendroneid/models/permissions_missing_exception.dart'; 10 | 11 | import 'package:permission_handler/permission_handler.dart'; 12 | import 'package:device_info_plus/device_info_plus.dart'; 13 | import 'package:flutter_opendroneid/pigeon.dart' as pigeon; 14 | 15 | export 'package:dart_opendroneid/src/types.dart'; 16 | 17 | class FlutterOpenDroneId { 18 | static late pigeon.Api _api = pigeon.Api(); 19 | // event channels 20 | static const bluetoothOdidPayloadEventChannel = 21 | const EventChannel('flutter_odid_data_bt'); 22 | static const wifiOdidPayloadEventChannel = 23 | const EventChannel('flutter_odid_data_wifi'); 24 | static const _btStateEventChannel = 25 | const EventChannel('flutter_odid_state_bt'); 26 | static const _wifiStateEventChannel = 27 | const EventChannel('flutter_odid_state_wifi'); 28 | 29 | static StreamSubscription? _bluetoothOdidDataSubscription; 30 | static StreamSubscription? _wifiOdidDataSubscription; 31 | 32 | static final _wifiMessagesController = 33 | StreamController.broadcast(); 34 | static final _bluetoothMessagesController = 35 | StreamController.broadcast(); 36 | 37 | static Map _storedPacks = {}; 38 | 39 | static Stream get bluetoothState => _btStateEventChannel 40 | .receiveBroadcastStream() 41 | .map((event) => event as bool); 42 | 43 | static Stream get bluetoothMessages => 44 | _bluetoothMessagesController.stream; 45 | 46 | static Stream get wifiMessages => 47 | _wifiMessagesController.stream; 48 | 49 | static Stream get wifiState => _wifiStateEventChannel 50 | .receiveBroadcastStream() 51 | .map((event) => event as bool); 52 | 53 | static Future get btTurnedOn async => 54 | await _api.bluetoothState() == 55 | pigeon.BluetoothState.values.indexOf(pigeon.BluetoothState.PoweredOn); 56 | 57 | static Future get wifiTurnedOn async => 58 | await _api.wifiState() == 59 | pigeon.WifiState.values.indexOf(pigeon.WifiState.Enabled); 60 | 61 | /// Starts scanning for nearby traffic 62 | /// For Bluetooth scanning, bluetooth permissions are required on both platforms, 63 | /// Android requires Bluetooth scan permission location permission on ver. < 12 64 | /// 65 | /// For Wi-Fi scanning, location permission is required on Android 66 | /// 67 | /// Throws PermissionMissingException if permissions were not granted 68 | /// 69 | /// To further receive data, listen to 70 | /// streams. 71 | static Future startScan(DriSourceType sourceType) async { 72 | if (sourceType == DriSourceType.Bluetooth) { 73 | await _assertBluetoothPermissions(); 74 | _bluetoothOdidDataSubscription?.cancel(); 75 | _bluetoothOdidDataSubscription = bluetoothOdidPayloadEventChannel 76 | .receiveBroadcastStream() 77 | .listen((payload) => _updatePacks( 78 | pigeon.ODIDPayload.decode(payload), DriSourceType.Bluetooth)); 79 | await _api.startScanBluetooth(); 80 | } else if (sourceType == DriSourceType.Wifi) { 81 | await _assertWifiPermissions(); 82 | _wifiOdidDataSubscription?.cancel(); 83 | 84 | _wifiOdidDataSubscription = wifiOdidPayloadEventChannel 85 | .receiveBroadcastStream() 86 | .listen((payload) => _updatePacks( 87 | pigeon.ODIDPayload.decode(payload), DriSourceType.Wifi)); 88 | await _api.startScanWifi(); 89 | } 90 | } 91 | 92 | /// Stops any currently running scan 93 | static Future stopScan(DriSourceType sourceType) async { 94 | if (sourceType == DriSourceType.Bluetooth && 95 | (await _api.isScanningBluetooth())) { 96 | await _api.stopScanBluetooth(); 97 | _bluetoothOdidDataSubscription?.cancel(); 98 | } 99 | if (sourceType == DriSourceType.Wifi && await _api.isScanningWifi()) { 100 | await _api.stopScanWifi(); 101 | _wifiOdidDataSubscription?.cancel(); 102 | } 103 | } 104 | 105 | static Future setBtScanPriority(pigeon.ScanPriority priority) async { 106 | await _api.setBtScanPriority(priority); 107 | } 108 | 109 | static Future get isScanningBluetooth async { 110 | return _api.isScanningBluetooth(); 111 | } 112 | 113 | static Future get isBluetoothExtendedSupported async => 114 | await _api.btExtendedSupported(); 115 | 116 | static Future get isWifiNanSupported async => 117 | await _api.wifiNaNSupported(); 118 | 119 | static Future get btMaxAdvDataLen async => await _api.btMaxAdvDataLen(); 120 | 121 | static Future get isScanningWifi async => await _api.isScanningWifi(); 122 | 123 | static void _updatePacks( 124 | pigeon.ODIDPayload payload, DriSourceType sourceType) { 125 | final storedPack = _storedPacks[payload.metadata.macAddress] ?? 126 | MessageContainer( 127 | metadata: payload.metadata, 128 | lastUpdate: 129 | DateTime.fromMillisecondsSinceEpoch(payload.receivedTimestamp), 130 | ); 131 | ODIDMessage? message; 132 | try { 133 | message = parseODIDMessage(payload.rawData); 134 | } catch (e) { 135 | throw ODIDMessageParsingException( 136 | relatedException: e, 137 | macAddress: payload.metadata.macAddress, 138 | rssi: payload.metadata.rssi, 139 | receivedTimestamp: payload.receivedTimestamp, 140 | source: payload.metadata.source, 141 | btName: payload.metadata.btName, 142 | ); 143 | } 144 | 145 | if (message == null) return; 146 | 147 | final updatedPack = storedPack.update( 148 | message: message, 149 | receivedTimestamp: payload.receivedTimestamp, 150 | metadata: payload.metadata, 151 | ); 152 | // update was refused if updatedPack is null 153 | if (updatedPack != null) { 154 | _storedPacks[payload.metadata.macAddress] = updatedPack; 155 | return switch (sourceType) { 156 | DriSourceType.Bluetooth => 157 | _bluetoothMessagesController.add(updatedPack), 158 | DriSourceType.Wifi => _wifiMessagesController.add(updatedPack), 159 | }; 160 | } 161 | } 162 | 163 | /// Checks all required Bluetooth permissions and throws 164 | /// [PermissionsMissingException] if any of them are not granted. 165 | static Future _assertBluetoothPermissions() async { 166 | List missingPermissions = []; 167 | 168 | // Bluetooth permission is required on all platforms 169 | if (!await Permission.bluetooth.status.isGranted) 170 | missingPermissions.add(Permission.bluetooth); 171 | 172 | if (Platform.isAndroid) { 173 | // Bluetooth Scan permission is required on all Android phones 174 | if (!await Permission.bluetoothScan.status.isGranted) 175 | missingPermissions.add(Permission.bluetoothScan); 176 | 177 | // Android also requires location permission to scan BT devices 178 | 179 | if (!await Permission.location.status.isGranted) 180 | missingPermissions.add(Permission.location); 181 | } 182 | 183 | if (missingPermissions.isNotEmpty) 184 | throw PermissionsMissingException(missingPermissions); 185 | } 186 | 187 | /// Checks all required Wi-Fi permissions and throws 188 | /// [PermissionsMissingException] if any of them are not granted. 189 | static Future _assertWifiPermissions() async { 190 | // Android requires location permission to scan Wi-Fi devices 191 | if (Platform.isAndroid) { 192 | List missingPermissions = []; 193 | 194 | final androidVersionNumber = await _getAndroidVersionNumber(); 195 | if (androidVersionNumber == null) return; 196 | // Android < 12 also requires location permission 197 | // Android 13 has a new nearbyWifiDevicesPermission 198 | if (androidVersionNumber >= 13) { 199 | if (!await Permission.nearbyWifiDevices.status.isGranted) 200 | missingPermissions.add(Permission.nearbyWifiDevices); 201 | } else { 202 | if (!await Permission.location.status.isGranted) 203 | missingPermissions.add(Permission.location); 204 | } 205 | if (missingPermissions.isNotEmpty) 206 | throw PermissionsMissingException(missingPermissions); 207 | } 208 | } 209 | 210 | static Future _getAndroidVersionNumber() async { 211 | final deviceInfo = DeviceInfoPlugin(); 212 | final androidVersion = (await deviceInfo.androidInfo).version.release; 213 | return int.tryParse(androidVersion); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /lib/models/constants.dart: -------------------------------------------------------------------------------- 1 | const MAX_MESSAGE_SIZE = 25; 2 | const MAX_ID_BYTE_SIZE = 20; 3 | const MAX_STRING_BYTE_SIZE = 23; 4 | const MAX_AUTH_DATA_PAGES = 16; 5 | const MAX_AUTH_PAGE_ZERO_SIZE = 17; 6 | const MAX_AUTH_PAGE_NON_ZERO_SIZE = 23; 7 | const MAX_AUTH_DATA = MAX_AUTH_PAGE_ZERO_SIZE + 8 | (MAX_AUTH_DATA_PAGES - 1) * MAX_AUTH_PAGE_NON_ZERO_SIZE; 9 | const MAX_MESSAGES_IN_PACK = 9; 10 | const MAX_MESSAGE_PACK_SIZE = MAX_MESSAGE_SIZE * MAX_MESSAGES_IN_PACK; 11 | const LAT_LONG_MULTIPLIER = 1e-7; 12 | const MIN_DIR = 0; // Minimum direction 13 | const MAX_DIR = 360; // Maximum direction 14 | const INV_DIR = 361; // Invalid direction 15 | const MIN_SPEED_H = 0; // Minimum speed horizontal 16 | const MAX_SPEED_H = 254.25; // Maximum speed horizontal 17 | const INV_SPEED_H = 255; // Invalid speed horizontal 18 | const MIN_SPEED_V = (-62); // Minimum speed vertical 19 | const MAX_SPEED_V = 62; // Maximum speed vertical 20 | const INV_SPEED_V = 63; // Invalid speed vertical 21 | const MIN_LAT = -90; // Minimum latitude 22 | const MAX_LAT = 90; // Maximum latitude 23 | const INV_LAT = 0; // Invalid latitude 24 | const MIN_LON = -180; // Minimum longitude 25 | const MAX_LON = 180; // Maximum longitude 26 | const INV_LON = 0; // Invalid longitude 27 | const MIN_ALT = (-1000); // Minimum altitude 28 | const MAX_ALT = 31767.5; // Maximum altitude 29 | const INV_ALT = MIN_ALT; // Invalid altitude 30 | const MAX_TIMESTAMP = (60 * 60); 31 | const INV_TIMESTAMP = 0xFFFF; // Invalid, No Value or Unknown Timestamp 32 | const MAX_AREA_RADIUS = 2550; 33 | const OPERATOR_ID_NOT_SET = 'NULL'; 34 | -------------------------------------------------------------------------------- /lib/models/dri_source_type.dart: -------------------------------------------------------------------------------- 1 | enum DriSourceType { Wifi, Bluetooth } 2 | -------------------------------------------------------------------------------- /lib/models/message_container.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_opendroneid/dart_opendroneid.dart'; 2 | import 'package:flutter_opendroneid/extensions/compare_extension.dart'; 3 | import 'package:flutter_opendroneid/models/constants.dart'; 4 | import 'package:flutter_opendroneid/pigeon.dart' as pigeon; 5 | import 'package:flutter_opendroneid/utils/conversions.dart'; 6 | 7 | /// The [MessageContainer] groups together messages of different types 8 | /// from one device. It contains one instance of each message. The container is 9 | /// then sent using stream to client of the library. 10 | class MessageContainer { 11 | final pigeon.ODIDMetadata metadata; 12 | final DateTime lastUpdate; 13 | 14 | final Map? basicIdMessages; 15 | final LocationMessage? locationMessage; 16 | final OperatorIDMessage? operatorIdMessage; 17 | final SelfIDMessage? selfIdMessage; 18 | final AuthMessage? authenticationMessage; 19 | final SystemMessage? systemDataMessage; 20 | 21 | MessageContainer({ 22 | required this.metadata, 23 | required this.lastUpdate, 24 | this.basicIdMessages, 25 | this.locationMessage, 26 | this.operatorIdMessage, 27 | this.selfIdMessage, 28 | this.authenticationMessage, 29 | this.systemDataMessage, 30 | }); 31 | 32 | @override 33 | String toString() { 34 | final singleMessages = [ 35 | locationMessage, 36 | operatorIdMessage, 37 | selfIdMessage, 38 | authenticationMessage, 39 | systemDataMessage 40 | ]; 41 | 42 | final descriptionString = singleMessages 43 | .where((msg) => msg != null) 44 | .map((msg) => msg.runtimeType) 45 | .join(' + '); 46 | 47 | return 'MessageContainer { ' 48 | '$descriptionString, ' 49 | '${basicIdMessages?.length} basic ID messages, ' 50 | 'last update: $lastUpdate, ' 51 | 'metadata: $metadata }'; 52 | } 53 | 54 | MessageContainer copyWith({ 55 | DateTime? lastUpdate, 56 | pigeon.ODIDMetadata? metadata, 57 | Map? basicIdMessage, 58 | LocationMessage? locationMessage, 59 | OperatorIDMessage? operatorIdMessage, 60 | SelfIDMessage? selfIdMessage, 61 | AuthMessage? authenticationMessage, 62 | SystemMessage? systemDataMessage, 63 | }) => 64 | MessageContainer( 65 | metadata: metadata ?? this.metadata, 66 | lastUpdate: lastUpdate ?? DateTime.now(), 67 | basicIdMessages: basicIdMessage ?? this.basicIdMessages, 68 | locationMessage: locationMessage ?? this.locationMessage, 69 | operatorIdMessage: operatorIdMessage ?? this.operatorIdMessage, 70 | selfIdMessage: selfIdMessage ?? this.selfIdMessage, 71 | authenticationMessage: 72 | authenticationMessage ?? this.authenticationMessage, 73 | systemDataMessage: systemDataMessage ?? this.systemDataMessage, 74 | ); 75 | 76 | /// Returns new MessageContainer updated with message. 77 | /// Null is returned if update is refused, because it contains duplicate or 78 | /// corrupted data. 79 | MessageContainer? update({ 80 | required ODIDMessage message, 81 | required pigeon.ODIDMetadata metadata, 82 | required int receivedTimestamp, 83 | }) { 84 | if (message.runtimeType == MessagePack) { 85 | final messages = (message as MessagePack).messages; 86 | var result = this; 87 | for (var packMessage in messages) { 88 | final update = result.update( 89 | message: packMessage, 90 | metadata: metadata, 91 | receivedTimestamp: receivedTimestamp, 92 | ); 93 | if (update != null) result = update; 94 | } 95 | return result; 96 | } 97 | // update pack only if new data differ from saved ones 98 | return switch (message.runtimeType) { 99 | LocationMessage => locationMessage != null && 100 | locationMessage!.containsEqualData(message as LocationMessage) 101 | ? null 102 | : copyWith( 103 | locationMessage: message as LocationMessage, 104 | metadata: metadata, 105 | lastUpdate: 106 | DateTime.fromMillisecondsSinceEpoch(receivedTimestamp), 107 | ), 108 | BasicIDMessage => _updateBasicIDMessages( 109 | message: message as BasicIDMessage, 110 | receivedTimestamp: receivedTimestamp, 111 | metadata: metadata, 112 | ), 113 | SelfIDMessage => selfIdMessage != null && 114 | selfIdMessage!.containsEqualData(message as SelfIDMessage) 115 | ? null 116 | : copyWith( 117 | selfIdMessage: message as SelfIDMessage, 118 | lastUpdate: 119 | DateTime.fromMillisecondsSinceEpoch(receivedTimestamp), 120 | metadata: metadata, 121 | ), 122 | OperatorIDMessage => operatorIdMessage != null && 123 | operatorIdMessage!.containsEqualData(message as OperatorIDMessage) 124 | ? null 125 | : copyWith( 126 | operatorIdMessage: message as OperatorIDMessage, 127 | lastUpdate: 128 | DateTime.fromMillisecondsSinceEpoch(receivedTimestamp), 129 | metadata: metadata, 130 | ), 131 | AuthMessage => authenticationMessage != null && 132 | authenticationMessage!.containsEqualData(message as AuthMessage) 133 | ? null 134 | : copyWith( 135 | authenticationMessage: message as AuthMessage, 136 | lastUpdate: 137 | DateTime.fromMillisecondsSinceEpoch(receivedTimestamp), 138 | metadata: metadata, 139 | ), 140 | SystemMessage => systemDataMessage != null && 141 | systemDataMessage!.containsEqualData(message as SystemMessage) 142 | ? null 143 | : copyWith( 144 | systemDataMessage: message as SystemMessage, 145 | metadata: metadata, 146 | lastUpdate: 147 | DateTime.fromMillisecondsSinceEpoch(receivedTimestamp), 148 | ), 149 | _ => null 150 | }; 151 | } 152 | 153 | String get macAddress => metadata.macAddress; 154 | 155 | pigeon.MessageSource get packSource => metadata.source; 156 | 157 | bool get operatorIDSet => 158 | operatorIdMessage != null && 159 | operatorIdMessage!.operatorID != OPERATOR_ID_NOT_SET; 160 | 161 | bool get operatorIDValid { 162 | final validCharacters = RegExp(r'^[a-zA-Z0-9]+$'); 163 | return operatorIdMessage != null && 164 | operatorIdMessage!.operatorID.length == 16 && 165 | validCharacters.hasMatch(operatorIdMessage!.operatorID); 166 | } 167 | 168 | bool get systemDataValid => 169 | systemDataMessage != null && 170 | systemDataMessage?.operatorLocation != null && 171 | systemDataMessage!.operatorLocation!.latitude != INV_LAT && 172 | systemDataMessage?.operatorLocation!.longitude != INV_LON && 173 | systemDataMessage!.operatorLocation!.latitude <= MAX_LAT && 174 | systemDataMessage!.operatorLocation!.latitude >= MIN_LAT && 175 | systemDataMessage!.operatorLocation!.longitude <= MAX_LON && 176 | systemDataMessage!.operatorLocation!.longitude >= MIN_LON; 177 | 178 | bool get locationValid => 179 | locationMessage != null && 180 | locationMessage?.location != null && 181 | locationMessage!.location!.latitude != INV_LAT && 182 | locationMessage!.location!.longitude != INV_LON && 183 | locationMessage!.location!.latitude <= MAX_LAT && 184 | locationMessage!.location!.longitude <= MAX_LON && 185 | locationMessage!.location!.latitude >= MIN_LAT && 186 | locationMessage!.location!.longitude >= MIN_LON; 187 | 188 | /// Check if container contains basic id message with given uas id 189 | bool containsUasId(String uasId) => 190 | basicIdMessages?.values 191 | .any((element) => element.uasID.asString() == uasId) ?? 192 | false; 193 | 194 | // preferably return message with SerialNumber uas id, which is the default 195 | BasicIDMessage? get preferredBasicIdMessage { 196 | if (basicIdMessages == null || basicIdMessages!.isEmpty) return null; 197 | 198 | return basicIdMessages![IDType.serialNumber] ?? 199 | basicIdMessages!.values.first; 200 | } 201 | 202 | String? get serialNumberUasId => 203 | basicIdMessages?[IDType.serialNumber]?.uasID.asString(); 204 | 205 | MessageContainer? _updateBasicIDMessages({ 206 | required BasicIDMessage message, 207 | required pigeon.ODIDMetadata metadata, 208 | required int receivedTimestamp, 209 | }) { 210 | if (basicIdMessages != null && 211 | basicIdMessages![message.uasID.type] != null && 212 | basicIdMessages![message.uasID.type]!.containsEqualData(message)) 213 | return null; 214 | 215 | final newEntry = {message.uasID.type: message}; 216 | return copyWith( 217 | basicIdMessage: basicIdMessages == null ? newEntry : basicIdMessages! 218 | ..addAll(newEntry), 219 | metadata: metadata, 220 | lastUpdate: DateTime.fromMillisecondsSinceEpoch(receivedTimestamp), 221 | ); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /lib/models/permissions_missing_exception.dart: -------------------------------------------------------------------------------- 1 | import 'package:permission_handler/permission_handler.dart'; 2 | 3 | class PermissionsMissingException implements Exception { 4 | final List missingPermissions; 5 | PermissionsMissingException(this.missingPermissions); 6 | } 7 | -------------------------------------------------------------------------------- /lib/pigeon.dart: -------------------------------------------------------------------------------- 1 | // Autogenerated from Pigeon (v10.1.6), do not edit directly. 2 | // See also: https://pub.dev/packages/pigeon 3 | // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import 4 | 5 | import 'dart:async'; 6 | import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; 7 | 8 | import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; 9 | import 'package:flutter/services.dart'; 10 | 11 | /// Higher priority drains battery but receives more data 12 | enum ScanPriority { 13 | High, 14 | Low, 15 | } 16 | 17 | /// ODID Message Source 18 | enum MessageSource { 19 | BluetoothLegacy, 20 | BluetoothLongRange, 21 | WifiNan, 22 | WifiBeacon, 23 | Unknown, 24 | } 25 | 26 | /// State of the Bluetooth adapter 27 | enum BluetoothState { 28 | Unknown, 29 | Resetting, 30 | Unsupported, 31 | Unauthorized, 32 | PoweredOff, 33 | PoweredOn, 34 | } 35 | 36 | /// State of the Wifi adapter 37 | enum WifiState { 38 | Disabling, 39 | Disabled, 40 | Enabling, 41 | Enabled, 42 | } 43 | 44 | enum BluetoothPhy { 45 | None, 46 | Phy1M, 47 | Phy2M, 48 | PhyLECoded, 49 | Unknown, 50 | } 51 | 52 | class ODIDMetadata { 53 | ODIDMetadata({ 54 | required this.macAddress, 55 | required this.source, 56 | this.rssi, 57 | this.btName, 58 | this.frequency, 59 | this.centerFreq0, 60 | this.centerFreq1, 61 | this.channelWidthMhz, 62 | this.primaryPhy, 63 | this.secondaryPhy, 64 | }); 65 | 66 | String macAddress; 67 | 68 | MessageSource source; 69 | 70 | int? rssi; 71 | 72 | String? btName; 73 | 74 | int? frequency; 75 | 76 | int? centerFreq0; 77 | 78 | int? centerFreq1; 79 | 80 | int? channelWidthMhz; 81 | 82 | BluetoothPhy? primaryPhy; 83 | 84 | BluetoothPhy? secondaryPhy; 85 | 86 | Object encode() { 87 | return [ 88 | macAddress, 89 | source.index, 90 | rssi, 91 | btName, 92 | frequency, 93 | centerFreq0, 94 | centerFreq1, 95 | channelWidthMhz, 96 | primaryPhy?.index, 97 | secondaryPhy?.index, 98 | ]; 99 | } 100 | 101 | static ODIDMetadata decode(Object result) { 102 | result as List; 103 | return ODIDMetadata( 104 | macAddress: result[0]! as String, 105 | source: MessageSource.values[result[1]! as int], 106 | rssi: result[2] as int?, 107 | btName: result[3] as String?, 108 | frequency: result[4] as int?, 109 | centerFreq0: result[5] as int?, 110 | centerFreq1: result[6] as int?, 111 | channelWidthMhz: result[7] as int?, 112 | primaryPhy: result[8] != null 113 | ? BluetoothPhy.values[result[8]! as int] 114 | : null, 115 | secondaryPhy: result[9] != null 116 | ? BluetoothPhy.values[result[9]! as int] 117 | : null, 118 | ); 119 | } 120 | } 121 | 122 | /// Payload send from native to dart contains raw data and metadata 123 | class ODIDPayload { 124 | ODIDPayload({ 125 | required this.rawData, 126 | required this.receivedTimestamp, 127 | required this.metadata, 128 | }); 129 | 130 | Uint8List rawData; 131 | 132 | int receivedTimestamp; 133 | 134 | ODIDMetadata metadata; 135 | 136 | Object encode() { 137 | return [ 138 | rawData, 139 | receivedTimestamp, 140 | metadata.encode(), 141 | ]; 142 | } 143 | 144 | static ODIDPayload decode(Object result) { 145 | result as List; 146 | return ODIDPayload( 147 | rawData: result[0]! as Uint8List, 148 | receivedTimestamp: result[1]! as int, 149 | metadata: ODIDMetadata.decode(result[2]! as List), 150 | ); 151 | } 152 | } 153 | 154 | class Api { 155 | /// Constructor for [Api]. The [binaryMessenger] named argument is 156 | /// available for dependency injection. If it is left null, the default 157 | /// BinaryMessenger will be used which routes to the host platform. 158 | Api({BinaryMessenger? binaryMessenger}) 159 | : _binaryMessenger = binaryMessenger; 160 | final BinaryMessenger? _binaryMessenger; 161 | 162 | static const MessageCodec codec = StandardMessageCodec(); 163 | 164 | Future startScanBluetooth() async { 165 | final BasicMessageChannel channel = BasicMessageChannel( 166 | 'dev.flutter.pigeon.flutter_opendroneid.Api.startScanBluetooth', codec, 167 | binaryMessenger: _binaryMessenger); 168 | final List? replyList = 169 | await channel.send(null) as List?; 170 | if (replyList == null) { 171 | throw PlatformException( 172 | code: 'channel-error', 173 | message: 'Unable to establish connection on channel.', 174 | ); 175 | } else if (replyList.length > 1) { 176 | throw PlatformException( 177 | code: replyList[0]! as String, 178 | message: replyList[1] as String?, 179 | details: replyList[2], 180 | ); 181 | } else { 182 | return; 183 | } 184 | } 185 | 186 | Future startScanWifi() async { 187 | final BasicMessageChannel channel = BasicMessageChannel( 188 | 'dev.flutter.pigeon.flutter_opendroneid.Api.startScanWifi', codec, 189 | binaryMessenger: _binaryMessenger); 190 | final List? replyList = 191 | await channel.send(null) as List?; 192 | if (replyList == null) { 193 | throw PlatformException( 194 | code: 'channel-error', 195 | message: 'Unable to establish connection on channel.', 196 | ); 197 | } else if (replyList.length > 1) { 198 | throw PlatformException( 199 | code: replyList[0]! as String, 200 | message: replyList[1] as String?, 201 | details: replyList[2], 202 | ); 203 | } else { 204 | return; 205 | } 206 | } 207 | 208 | Future stopScanBluetooth() async { 209 | final BasicMessageChannel channel = BasicMessageChannel( 210 | 'dev.flutter.pigeon.flutter_opendroneid.Api.stopScanBluetooth', codec, 211 | binaryMessenger: _binaryMessenger); 212 | final List? replyList = 213 | await channel.send(null) as List?; 214 | if (replyList == null) { 215 | throw PlatformException( 216 | code: 'channel-error', 217 | message: 'Unable to establish connection on channel.', 218 | ); 219 | } else if (replyList.length > 1) { 220 | throw PlatformException( 221 | code: replyList[0]! as String, 222 | message: replyList[1] as String?, 223 | details: replyList[2], 224 | ); 225 | } else { 226 | return; 227 | } 228 | } 229 | 230 | Future stopScanWifi() async { 231 | final BasicMessageChannel channel = BasicMessageChannel( 232 | 'dev.flutter.pigeon.flutter_opendroneid.Api.stopScanWifi', codec, 233 | binaryMessenger: _binaryMessenger); 234 | final List? replyList = 235 | await channel.send(null) as List?; 236 | if (replyList == null) { 237 | throw PlatformException( 238 | code: 'channel-error', 239 | message: 'Unable to establish connection on channel.', 240 | ); 241 | } else if (replyList.length > 1) { 242 | throw PlatformException( 243 | code: replyList[0]! as String, 244 | message: replyList[1] as String?, 245 | details: replyList[2], 246 | ); 247 | } else { 248 | return; 249 | } 250 | } 251 | 252 | Future setBtScanPriority(ScanPriority arg_priority) async { 253 | final BasicMessageChannel channel = BasicMessageChannel( 254 | 'dev.flutter.pigeon.flutter_opendroneid.Api.setBtScanPriority', codec, 255 | binaryMessenger: _binaryMessenger); 256 | final List? replyList = 257 | await channel.send([arg_priority.index]) as List?; 258 | if (replyList == null) { 259 | throw PlatformException( 260 | code: 'channel-error', 261 | message: 'Unable to establish connection on channel.', 262 | ); 263 | } else if (replyList.length > 1) { 264 | throw PlatformException( 265 | code: replyList[0]! as String, 266 | message: replyList[1] as String?, 267 | details: replyList[2], 268 | ); 269 | } else { 270 | return; 271 | } 272 | } 273 | 274 | Future isScanningBluetooth() async { 275 | final BasicMessageChannel channel = BasicMessageChannel( 276 | 'dev.flutter.pigeon.flutter_opendroneid.Api.isScanningBluetooth', codec, 277 | binaryMessenger: _binaryMessenger); 278 | final List? replyList = 279 | await channel.send(null) as List?; 280 | if (replyList == null) { 281 | throw PlatformException( 282 | code: 'channel-error', 283 | message: 'Unable to establish connection on channel.', 284 | ); 285 | } else if (replyList.length > 1) { 286 | throw PlatformException( 287 | code: replyList[0]! as String, 288 | message: replyList[1] as String?, 289 | details: replyList[2], 290 | ); 291 | } else if (replyList[0] == null) { 292 | throw PlatformException( 293 | code: 'null-error', 294 | message: 'Host platform returned null value for non-null return value.', 295 | ); 296 | } else { 297 | return (replyList[0] as bool?)!; 298 | } 299 | } 300 | 301 | Future isScanningWifi() async { 302 | final BasicMessageChannel channel = BasicMessageChannel( 303 | 'dev.flutter.pigeon.flutter_opendroneid.Api.isScanningWifi', codec, 304 | binaryMessenger: _binaryMessenger); 305 | final List? replyList = 306 | await channel.send(null) as List?; 307 | if (replyList == null) { 308 | throw PlatformException( 309 | code: 'channel-error', 310 | message: 'Unable to establish connection on channel.', 311 | ); 312 | } else if (replyList.length > 1) { 313 | throw PlatformException( 314 | code: replyList[0]! as String, 315 | message: replyList[1] as String?, 316 | details: replyList[2], 317 | ); 318 | } else if (replyList[0] == null) { 319 | throw PlatformException( 320 | code: 'null-error', 321 | message: 'Host platform returned null value for non-null return value.', 322 | ); 323 | } else { 324 | return (replyList[0] as bool?)!; 325 | } 326 | } 327 | 328 | Future bluetoothState() async { 329 | final BasicMessageChannel channel = BasicMessageChannel( 330 | 'dev.flutter.pigeon.flutter_opendroneid.Api.bluetoothState', codec, 331 | binaryMessenger: _binaryMessenger); 332 | final List? replyList = 333 | await channel.send(null) as List?; 334 | if (replyList == null) { 335 | throw PlatformException( 336 | code: 'channel-error', 337 | message: 'Unable to establish connection on channel.', 338 | ); 339 | } else if (replyList.length > 1) { 340 | throw PlatformException( 341 | code: replyList[0]! as String, 342 | message: replyList[1] as String?, 343 | details: replyList[2], 344 | ); 345 | } else if (replyList[0] == null) { 346 | throw PlatformException( 347 | code: 'null-error', 348 | message: 'Host platform returned null value for non-null return value.', 349 | ); 350 | } else { 351 | return (replyList[0] as int?)!; 352 | } 353 | } 354 | 355 | Future wifiState() async { 356 | final BasicMessageChannel channel = BasicMessageChannel( 357 | 'dev.flutter.pigeon.flutter_opendroneid.Api.wifiState', codec, 358 | binaryMessenger: _binaryMessenger); 359 | final List? replyList = 360 | await channel.send(null) as List?; 361 | if (replyList == null) { 362 | throw PlatformException( 363 | code: 'channel-error', 364 | message: 'Unable to establish connection on channel.', 365 | ); 366 | } else if (replyList.length > 1) { 367 | throw PlatformException( 368 | code: replyList[0]! as String, 369 | message: replyList[1] as String?, 370 | details: replyList[2], 371 | ); 372 | } else if (replyList[0] == null) { 373 | throw PlatformException( 374 | code: 'null-error', 375 | message: 'Host platform returned null value for non-null return value.', 376 | ); 377 | } else { 378 | return (replyList[0] as int?)!; 379 | } 380 | } 381 | 382 | Future btExtendedSupported() async { 383 | final BasicMessageChannel channel = BasicMessageChannel( 384 | 'dev.flutter.pigeon.flutter_opendroneid.Api.btExtendedSupported', codec, 385 | binaryMessenger: _binaryMessenger); 386 | final List? replyList = 387 | await channel.send(null) as List?; 388 | if (replyList == null) { 389 | throw PlatformException( 390 | code: 'channel-error', 391 | message: 'Unable to establish connection on channel.', 392 | ); 393 | } else if (replyList.length > 1) { 394 | throw PlatformException( 395 | code: replyList[0]! as String, 396 | message: replyList[1] as String?, 397 | details: replyList[2], 398 | ); 399 | } else if (replyList[0] == null) { 400 | throw PlatformException( 401 | code: 'null-error', 402 | message: 'Host platform returned null value for non-null return value.', 403 | ); 404 | } else { 405 | return (replyList[0] as bool?)!; 406 | } 407 | } 408 | 409 | Future btMaxAdvDataLen() async { 410 | final BasicMessageChannel channel = BasicMessageChannel( 411 | 'dev.flutter.pigeon.flutter_opendroneid.Api.btMaxAdvDataLen', codec, 412 | binaryMessenger: _binaryMessenger); 413 | final List? replyList = 414 | await channel.send(null) as List?; 415 | if (replyList == null) { 416 | throw PlatformException( 417 | code: 'channel-error', 418 | message: 'Unable to establish connection on channel.', 419 | ); 420 | } else if (replyList.length > 1) { 421 | throw PlatformException( 422 | code: replyList[0]! as String, 423 | message: replyList[1] as String?, 424 | details: replyList[2], 425 | ); 426 | } else if (replyList[0] == null) { 427 | throw PlatformException( 428 | code: 'null-error', 429 | message: 'Host platform returned null value for non-null return value.', 430 | ); 431 | } else { 432 | return (replyList[0] as int?)!; 433 | } 434 | } 435 | 436 | Future wifiNaNSupported() async { 437 | final BasicMessageChannel channel = BasicMessageChannel( 438 | 'dev.flutter.pigeon.flutter_opendroneid.Api.wifiNaNSupported', codec, 439 | binaryMessenger: _binaryMessenger); 440 | final List? replyList = 441 | await channel.send(null) as List?; 442 | if (replyList == null) { 443 | throw PlatformException( 444 | code: 'channel-error', 445 | message: 'Unable to establish connection on channel.', 446 | ); 447 | } else if (replyList.length > 1) { 448 | throw PlatformException( 449 | code: replyList[0]! as String, 450 | message: replyList[1] as String?, 451 | details: replyList[2], 452 | ); 453 | } else if (replyList[0] == null) { 454 | throw PlatformException( 455 | code: 'null-error', 456 | message: 'Host platform returned null value for non-null return value.', 457 | ); 458 | } else { 459 | return (replyList[0] as bool?)!; 460 | } 461 | } 462 | } 463 | 464 | class _PayloadApiCodec extends StandardMessageCodec { 465 | const _PayloadApiCodec(); 466 | @override 467 | void writeValue(WriteBuffer buffer, Object? value) { 468 | if (value is ODIDMetadata) { 469 | buffer.putUint8(128); 470 | writeValue(buffer, value.encode()); 471 | } else if (value is ODIDPayload) { 472 | buffer.putUint8(129); 473 | writeValue(buffer, value.encode()); 474 | } else { 475 | super.writeValue(buffer, value); 476 | } 477 | } 478 | 479 | @override 480 | Object? readValueOfType(int type, ReadBuffer buffer) { 481 | switch (type) { 482 | case 128: 483 | return ODIDMetadata.decode(readValue(buffer)!); 484 | case 129: 485 | return ODIDPayload.decode(readValue(buffer)!); 486 | default: 487 | return super.readValueOfType(type, buffer); 488 | } 489 | } 490 | } 491 | 492 | class PayloadApi { 493 | /// Constructor for [PayloadApi]. The [binaryMessenger] named argument is 494 | /// available for dependency injection. If it is left null, the default 495 | /// BinaryMessenger will be used which routes to the host platform. 496 | PayloadApi({BinaryMessenger? binaryMessenger}) 497 | : _binaryMessenger = binaryMessenger; 498 | final BinaryMessenger? _binaryMessenger; 499 | 500 | static const MessageCodec codec = _PayloadApiCodec(); 501 | 502 | Future buildPayload(Uint8List arg_rawData, int arg_receivedTimestamp, ODIDMetadata arg_metadata) async { 503 | final BasicMessageChannel channel = BasicMessageChannel( 504 | 'dev.flutter.pigeon.flutter_opendroneid.PayloadApi.buildPayload', codec, 505 | binaryMessenger: _binaryMessenger); 506 | final List? replyList = 507 | await channel.send([arg_rawData, arg_receivedTimestamp, arg_metadata]) as List?; 508 | if (replyList == null) { 509 | throw PlatformException( 510 | code: 'channel-error', 511 | message: 'Unable to establish connection on channel.', 512 | ); 513 | } else if (replyList.length > 1) { 514 | throw PlatformException( 515 | code: replyList[0]! as String, 516 | message: replyList[1] as String?, 517 | details: replyList[2], 518 | ); 519 | } else if (replyList[0] == null) { 520 | throw PlatformException( 521 | code: 'null-error', 522 | message: 'Host platform returned null value for non-null return value.', 523 | ); 524 | } else { 525 | return (replyList[0] as ODIDPayload?)!; 526 | } 527 | } 528 | } 529 | -------------------------------------------------------------------------------- /lib/utils/conversions.dart: -------------------------------------------------------------------------------- 1 | import 'package:dart_opendroneid/dart_opendroneid.dart'; 2 | import 'package:flutter_opendroneid/extensions/list_extension.dart'; 3 | 4 | /// Conversions extensions 5 | // TODO move to dart-opendroneid in the future(?) 6 | const Map _horizontalAccuracyConversionMap = { 7 | HorizontalAccuracy.unknown: null, 8 | HorizontalAccuracy.kilometers_18_52: 18520, 9 | HorizontalAccuracy.kilometers_7_408: 7408, 10 | HorizontalAccuracy.kilometers_3_704: 3704, 11 | HorizontalAccuracy.kilometers_1_852: 1852, 12 | HorizontalAccuracy.meters_926: 926, 13 | HorizontalAccuracy.meters_555_6: 555.6, 14 | HorizontalAccuracy.meters_185_2: 185.2, 15 | HorizontalAccuracy.meters_92_6: 92.6, 16 | HorizontalAccuracy.meters_30: 30, 17 | HorizontalAccuracy.meters_10: 10, 18 | HorizontalAccuracy.meters_3: 3, 19 | HorizontalAccuracy.meters_1: 1, 20 | }; 21 | 22 | const Map _verticalAccuracyConversionMap = { 23 | VerticalAccuracy.unknown: null, 24 | VerticalAccuracy.meters_150: 150, 25 | VerticalAccuracy.meters_45: 45, 26 | VerticalAccuracy.meters_25: 25, 27 | VerticalAccuracy.meters_10: 10, 28 | VerticalAccuracy.meters_3: 3, 29 | VerticalAccuracy.meters_1: 1, 30 | }; 31 | 32 | const Map _speedAccuracyConversionMap = { 33 | SpeedAccuracy.unknown: null, 34 | SpeedAccuracy.meterPerSecond_10: 10, 35 | SpeedAccuracy.meterPerSecond_3: 3, 36 | SpeedAccuracy.meterPerSecond_1: 1, 37 | SpeedAccuracy.meterPerSecond_0_3: 0.3, 38 | }; 39 | 40 | const Map _idTypeConversionMap = { 41 | IDType.none: 'None', 42 | IDType.serialNumber: 'Serial Number', 43 | IDType.CAARegistrationID: 'CAA Registration ID', 44 | IDType.UTMAssignedID: 'UTM Assigned ID', 45 | IDType.specificSessionID: 'Specific Session ID', 46 | }; 47 | 48 | const Map _uaTypeConversionMap = { 49 | UAType.none: 'None', 50 | UAType.aeroplane: 'Aeroplane', 51 | UAType.helicopterOrMultirotor: 'Helicopter Or Multirotor', 52 | UAType.gyroplane: 'Gyroplane', 53 | UAType.hybridLift: 'Hybrid Lift', 54 | UAType.ornithopter: 'Ornithopter', 55 | UAType.glider: 'Glider', 56 | UAType.kite: 'Kite', 57 | UAType.freeBalloon: 'Free Balloon', 58 | UAType.captiveBalloon: 'Captive Balloon', 59 | UAType.airship: 'Airship', 60 | UAType.freeFallParachute: 'Free Fall Parachute', 61 | UAType.rocket: 'Rocket', 62 | UAType.tetheredPoweredAircraft: 'Tethered Powered Aircraft', 63 | UAType.groundObstacle: 'Ground Obstacle', 64 | UAType.other: 'Other', 65 | }; 66 | 67 | const Map _uaClassEuropeConversionMap = { 68 | UAClassEurope.undefined: 'Undefined', 69 | UAClassEurope.EUClass_0: 'EU Class 0', 70 | UAClassEurope.EUClass_1: 'EU Class 1', 71 | UAClassEurope.EUClass_2: 'EU Class 2', 72 | UAClassEurope.EUClass_3: 'EU Class 3', 73 | UAClassEurope.EUClass_4: 'EU Class 4', 74 | UAClassEurope.EUClass_5: 'EU Class 5', 75 | UAClassEurope.EUClass_6: 'EU Class 6', 76 | }; 77 | 78 | const Map _uaCategoryEuropeConversionMap = { 79 | UACategoryEurope.undefined: 'Undefined', 80 | UACategoryEurope.EUOpen: 'EU Open', 81 | UACategoryEurope.EUSpecific: 'EU Specific', 82 | UACategoryEurope.EUCertified: 'EU Certified', 83 | }; 84 | 85 | const Map _operatorLocTypeConversionMap = { 86 | OperatorLocationType.fixed: 'Fixed', 87 | OperatorLocationType.takeOff: 'Take Off', 88 | OperatorLocationType.dynamic: 'Dynamic', 89 | }; 90 | 91 | const Map _heightTypeConversionMap = { 92 | HeightType.aboveGroundLevel: 'Above Ground Level', 93 | HeightType.aboveTakeoff: 'Above Take Off', 94 | }; 95 | 96 | const Map _operationalStatusConversionMap = { 97 | OperationalStatus.ground: 'Grounded', 98 | OperationalStatus.airborne: 'Airborne', 99 | OperationalStatus.emergency: 'Emergency', 100 | OperationalStatus.none: 'Unknown', 101 | }; 102 | 103 | extension HorizontalAccuracyConversion on HorizontalAccuracy { 104 | double? toMeters() => _horizontalAccuracyConversionMap[this]; 105 | } 106 | 107 | extension VerticalAccuracyConversion on VerticalAccuracy { 108 | double? toMeters() => _verticalAccuracyConversionMap[this]; 109 | } 110 | 111 | extension SpeedAccuracyConversion on SpeedAccuracy { 112 | double? toMetersPerSecond() => _speedAccuracyConversionMap[this]; 113 | } 114 | 115 | extension UASIDConversion on UASID { 116 | String? asString() => switch (this) { 117 | IDNone() => null, 118 | SerialNumber(serialNumber: final sn) => sn, 119 | CAARegistrationID(registrationID: final regId) => regId, 120 | UTMAssignedID(id: final id) => id.toHexString(), 121 | SpecificSessionID(id: final id) => id.toHexString(), 122 | }; 123 | } 124 | 125 | extension IDTypeConversion on IDType { 126 | String? asString() => _idTypeConversionMap[this]; 127 | } 128 | 129 | extension UATypeConversion on UAType { 130 | String? asString() => _uaTypeConversionMap[this]; 131 | } 132 | 133 | extension UAClassificationConversion on UAClassification { 134 | bool isEuropeClassification() => this is UAClassificationEurope; 135 | String? uaClassEuropeString() => isEuropeClassification() 136 | ? (this as UAClassificationEurope).uaClassEurope.asString() 137 | : null; 138 | String? uaCategoryEuropeString() => isEuropeClassification() 139 | ? (this as UAClassificationEurope).uaCategoryEurope.asString() 140 | : null; 141 | } 142 | 143 | extension UACategoryEuropeConversion on UACategoryEurope { 144 | String? asString() => _uaCategoryEuropeConversionMap[this]; 145 | } 146 | 147 | extension UAClassEuropeConversion on UAClassEurope { 148 | String? asString() => _uaClassEuropeConversionMap[this]; 149 | } 150 | 151 | extension OperatorLocationTypeConversion on OperatorLocationType { 152 | String? asString() => _operatorLocTypeConversionMap[this]; 153 | } 154 | 155 | extension HeightTypeConversion on HeightType { 156 | String? asString() => _heightTypeConversionMap[this]; 157 | } 158 | 159 | extension OperationalStatusConversion on OperationalStatus { 160 | String? asString() => _operationalStatusConversionMap[this]; 161 | } 162 | -------------------------------------------------------------------------------- /pigeon/schema.dart: -------------------------------------------------------------------------------- 1 | import 'package:pigeon/pigeon.dart'; 2 | 3 | /// Higher priority drains battery but receives more data 4 | enum ScanPriority { 5 | High, 6 | Low, 7 | } 8 | 9 | /// ODID Message Source 10 | enum MessageSource { 11 | BluetoothLegacy, 12 | BluetoothLongRange, 13 | WifiNan, 14 | WifiBeacon, 15 | Unknown, 16 | } 17 | 18 | /// State of the Bluetooth adapter 19 | enum BluetoothState { 20 | Unknown, 21 | Resetting, 22 | Unsupported, 23 | Unauthorized, 24 | PoweredOff, 25 | PoweredOn, 26 | } 27 | 28 | /// State of the Wifi adapter 29 | enum WifiState { 30 | Disabling, 31 | Disabled, 32 | Enabling, 33 | Enabled, 34 | } 35 | 36 | enum BluetoothPhy { 37 | None, 38 | Phy1M, 39 | Phy2M, 40 | PhyLECoded, 41 | Unknown, 42 | } 43 | 44 | class ODIDMetadata { 45 | final String macAddress; 46 | 47 | final MessageSource source; 48 | 49 | final int? rssi; 50 | 51 | final String? btName; 52 | 53 | final int? frequency; 54 | 55 | final int? centerFreq0; 56 | 57 | final int? centerFreq1; 58 | 59 | final int? channelWidthMhz; 60 | 61 | final BluetoothPhy? primaryPhy; 62 | 63 | final BluetoothPhy? secondaryPhy; 64 | 65 | ODIDMetadata({ 66 | required this.macAddress, 67 | required this.source, 68 | this.rssi, 69 | this.btName, 70 | this.frequency, 71 | this.channelWidthMhz, 72 | this.centerFreq0, 73 | this.centerFreq1, 74 | this.primaryPhy, 75 | this.secondaryPhy, 76 | }); 77 | } 78 | 79 | /// Payload send from native to dart contains raw data and metadata 80 | class ODIDPayload { 81 | final Uint8List rawData; 82 | 83 | final int receivedTimestamp; 84 | 85 | final ODIDMetadata metadata; 86 | 87 | ODIDPayload( 88 | this.rawData, 89 | this.receivedTimestamp, 90 | this.metadata, 91 | ); 92 | } 93 | 94 | @HostApi() 95 | abstract class Api { 96 | @async 97 | void startScanBluetooth(); 98 | @async 99 | void startScanWifi(); 100 | @async 101 | void stopScanBluetooth(); 102 | @async 103 | void stopScanWifi(); 104 | @async 105 | void setBtScanPriority(ScanPriority priority); 106 | @async 107 | bool isScanningBluetooth(); 108 | @async 109 | bool isScanningWifi(); 110 | @async 111 | int bluetoothState(); 112 | @async 113 | int wifiState(); 114 | @async 115 | bool btExtendedSupported(); 116 | @async 117 | int btMaxAdvDataLen(); 118 | @async 119 | bool wifiNaNSupported(); 120 | } 121 | 122 | // ODIDPayload is not generated until used in API 123 | @HostApi() 124 | abstract class PayloadApi { 125 | ODIDPayload buildPayload( 126 | Uint8List rawData, int receivedTimestamp, ODIDMetadata metadata); 127 | } 128 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_opendroneid 2 | description: A flutter plugin for reading Wi-Fi and Bluetooth Remote ID advertisements using native Android and iOS platform-specific implementation. 3 | version: 0.23.0 4 | repository: https://github.com/dronetag/flutter-opendroneid 5 | environment: 6 | sdk: ">=3.0.0 <4.0.0" 7 | flutter: ">=3.16.7" 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | permission_handler: ^11.2.0 12 | device_info_plus: ^9.1.2 13 | dart_opendroneid: ^0.6.2 14 | dev_dependencies: 15 | pigeon: ^10.1.6 16 | flutter: 17 | plugin: 18 | platforms: 19 | android: 20 | package: cz.dronetag.flutter_opendroneid 21 | pluginClass: FlutterOpendroneidPlugin 22 | ios: 23 | pluginClass: FlutterOpendroneidPlugin 24 | -------------------------------------------------------------------------------- /scripts/pigeon_generate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script generates the Pigeon classes for structured data 4 | # exchange between the native code and Dart code 5 | 6 | flutter pub run pigeon \ 7 | --input pigeon/schema.dart \ 8 | --dart_out lib/pigeon.dart \ 9 | --objc_prefix DTG \ 10 | --objc_header_out ios/Classes/pigeon.h \ 11 | --objc_source_out ios/Classes/pigeon.m \ 12 | --java_out ./android/src/main/java/cz/dronetag/flutter_opendroneid/Pigeon.java \ 13 | --java_package "cz.dronetag.flutter_opendroneid" --------------------------------------------------------------------------------