├── .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