Initial commit
@@ -0,0 +1,27 @@
|
|||||||
|
name: Code Review
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened, synchronize]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: anc95/ChatGPT-CodeReview@main
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
|
# Optional
|
||||||
|
OPENAI_API_ENDPOINT: https://api.openai.com/v1
|
||||||
|
MODEL: gpt-3.5-turbo-1106 # https://platform.openai.com/docs/models
|
||||||
|
PROMPT: "Please check if there are any bugs, confusions, or irregularities in the following code diff: "
|
||||||
|
top_p: 1 # https://platform.openai.com/docs/api-reference/chat/create#chat/create-top_p
|
||||||
|
temperature: 1 # https://platform.openai.com/docs/api-reference/chat/create#chat/create-temperature
|
||||||
|
max_tokens: 4096
|
||||||
|
MAX_PATCH_LENGTH: 10000 # if the patch/diff length is large than MAX_PATCH_LENGTH, will be ignored and won't review. By default, with no MAX_PATCH_LENGTH set, there is also no limit for the patch/diff length.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||||
|
/pubspec.lock
|
||||||
|
**/doc/api/
|
||||||
|
.dart_tool/
|
||||||
|
.packages
|
||||||
|
build/
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "d211f42860350d914a5ad8102f9ec32764dc6d06"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: plugin
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06
|
||||||
|
base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06
|
||||||
|
- platform: android
|
||||||
|
create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06
|
||||||
|
base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06
|
||||||
|
- platform: ios
|
||||||
|
create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06
|
||||||
|
base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06
|
||||||
|
- platform: linux
|
||||||
|
create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06
|
||||||
|
base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06
|
||||||
|
- platform: macos
|
||||||
|
create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06
|
||||||
|
base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06
|
||||||
|
- platform: windows
|
||||||
|
create_revision: d211f42860350d914a5ad8102f9ec32764dc6d06
|
||||||
|
base_revision: d211f42860350d914a5ad8102f9ec32764dc6d06
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Win32",
|
||||||
|
"includePath": [
|
||||||
|
"${workspaceFolder}/**"
|
||||||
|
],
|
||||||
|
"defines": [
|
||||||
|
"_DEBUG",
|
||||||
|
"UNICODE",
|
||||||
|
"_UNICODE"
|
||||||
|
],
|
||||||
|
"windowsSdkVersion": "10.0.20348.0",
|
||||||
|
"compilerPath": "cl.exe",
|
||||||
|
"cStandard": "c17",
|
||||||
|
"cppStandard": "c++20",
|
||||||
|
"intelliSenseMode": "windows-msvc-x64"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": 4
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "example",
|
||||||
|
"cwd": "example",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "example_auto_connect",
|
||||||
|
"cwd": "example",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"args": [
|
||||||
|
"--dart-define",
|
||||||
|
"AUTO_CONNECT=true",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"connnection",
|
||||||
|
"DSIMPLEBLE",
|
||||||
|
"Inspectable",
|
||||||
|
"microtask",
|
||||||
|
"modalias",
|
||||||
|
"NSUUID",
|
||||||
|
"pairable",
|
||||||
|
"rssi",
|
||||||
|
"simpleble",
|
||||||
|
"unawaited",
|
||||||
|
"Unpair",
|
||||||
|
"unwatch",
|
||||||
|
"uuidstr"
|
||||||
|
],
|
||||||
|
"cmake.configureOnOpen": false,
|
||||||
|
"files.associations": {
|
||||||
|
"xstring": "cpp",
|
||||||
|
"chrono": "cpp",
|
||||||
|
"xlocale": "cpp",
|
||||||
|
"optional": "cpp",
|
||||||
|
"memory": "cpp",
|
||||||
|
"algorithm": "cpp",
|
||||||
|
"any": "cpp",
|
||||||
|
"array": "cpp",
|
||||||
|
"atomic": "cpp",
|
||||||
|
"bit": "cpp",
|
||||||
|
"cctype": "cpp",
|
||||||
|
"charconv": "cpp",
|
||||||
|
"clocale": "cpp",
|
||||||
|
"cmath": "cpp",
|
||||||
|
"compare": "cpp",
|
||||||
|
"concepts": "cpp",
|
||||||
|
"condition_variable": "cpp",
|
||||||
|
"coroutine": "cpp",
|
||||||
|
"cstddef": "cpp",
|
||||||
|
"cstdint": "cpp",
|
||||||
|
"cstdio": "cpp",
|
||||||
|
"cstdlib": "cpp",
|
||||||
|
"cstring": "cpp",
|
||||||
|
"ctime": "cpp",
|
||||||
|
"cwchar": "cpp",
|
||||||
|
"exception": "cpp",
|
||||||
|
"format": "cpp",
|
||||||
|
"forward_list": "cpp",
|
||||||
|
"functional": "cpp",
|
||||||
|
"initializer_list": "cpp",
|
||||||
|
"iomanip": "cpp",
|
||||||
|
"ios": "cpp",
|
||||||
|
"iosfwd": "cpp",
|
||||||
|
"iostream": "cpp",
|
||||||
|
"istream": "cpp",
|
||||||
|
"iterator": "cpp",
|
||||||
|
"limits": "cpp",
|
||||||
|
"list": "cpp",
|
||||||
|
"locale": "cpp",
|
||||||
|
"map": "cpp",
|
||||||
|
"mutex": "cpp",
|
||||||
|
"new": "cpp",
|
||||||
|
"ostream": "cpp",
|
||||||
|
"ratio": "cpp",
|
||||||
|
"set": "cpp",
|
||||||
|
"shared_mutex": "cpp",
|
||||||
|
"sstream": "cpp",
|
||||||
|
"stdexcept": "cpp",
|
||||||
|
"stop_token": "cpp",
|
||||||
|
"streambuf": "cpp",
|
||||||
|
"string": "cpp",
|
||||||
|
"system_error": "cpp",
|
||||||
|
"thread": "cpp",
|
||||||
|
"tuple": "cpp",
|
||||||
|
"type_traits": "cpp",
|
||||||
|
"typeinfo": "cpp",
|
||||||
|
"unordered_map": "cpp",
|
||||||
|
"utility": "cpp",
|
||||||
|
"variant": "cpp",
|
||||||
|
"vector": "cpp",
|
||||||
|
"xfacet": "cpp",
|
||||||
|
"xhash": "cpp",
|
||||||
|
"xiosbase": "cpp",
|
||||||
|
"xlocbuf": "cpp",
|
||||||
|
"xlocinfo": "cpp",
|
||||||
|
"xlocmes": "cpp",
|
||||||
|
"xlocmon": "cpp",
|
||||||
|
"xlocnum": "cpp",
|
||||||
|
"xloctime": "cpp",
|
||||||
|
"xmemory": "cpp",
|
||||||
|
"xtr1common": "cpp",
|
||||||
|
"xtree": "cpp",
|
||||||
|
"xutility": "cpp",
|
||||||
|
"deque": "cpp",
|
||||||
|
"fstream": "cpp",
|
||||||
|
"unordered_set": "cpp",
|
||||||
|
"complex": "cpp",
|
||||||
|
"csignal": "cpp",
|
||||||
|
"cstdarg": "cpp",
|
||||||
|
"filesystem": "cpp",
|
||||||
|
"ranges": "cpp",
|
||||||
|
"span": "cpp",
|
||||||
|
"stack": "cpp",
|
||||||
|
"typeindex": "cpp",
|
||||||
|
"future": "cpp",
|
||||||
|
"regex": "cpp"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
## 0.8.0
|
||||||
|
|
||||||
|
* Initial release.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2024, Navideck Labs OÜ
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
Copyright (c) 2020, Woodemi Co,Ltd.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
# Universal Ble
|
||||||
|
|
||||||
|
A cross-platform (Android/iOS/MacOS/Windows/Linux/Web) BluetoothLE plugin for Flutter
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
- [Receive BLE availability changes](#receive-ble-availability-changes)
|
||||||
|
- [Scan BLE peripheral](#scan-ble-peripheral)
|
||||||
|
- [Connect BLE peripheral](#connect-ble-peripheral)
|
||||||
|
- [Discover services of BLE peripheral](#discover-services-of-ble-peripheral)
|
||||||
|
- [Transfer data between BLE central & peripheral](#transfer-data-between-ble-central--peripheral)
|
||||||
|
- [Pair BLE peripheral](#pair-ble-central--peripheral)
|
||||||
|
|
||||||
|
| API | Android | iOS | macOS | Windows (beta) | Linux (beta) | Web |
|
||||||
|
| :------------------- | :-----: | :-: | :---: | :-----: | :---: | :-: |
|
||||||
|
| startScan/stopScan | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||||
|
| connect/disconnect | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||||
|
| getConnectedDevices | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
|
||||||
|
| discoverServices | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||||
|
| readValue | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||||
|
| writeValue | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||||
|
| setNotifiable | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||||
|
| pair/unPair | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ |
|
||||||
|
| onPairStateChange | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ |
|
||||||
|
| enableBluetooth | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ |
|
||||||
|
| onAvailabilityChange | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||||
|
| requestMtu | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | ❌ |
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
You need to add the following permissions to your AndroidManifest.xml file:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30" />
|
||||||
|
```
|
||||||
|
|
||||||
|
If you use `BLUETOOTH_SCAN` to determine location, modify your AndroidManifest.xml file to include the following entry:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" tools:remove="android:usesPermissionFlags" tools:targetApi="s" />
|
||||||
|
```
|
||||||
|
|
||||||
|
If you use location services in your app, remove `android:maxSdkVersion="30"` from the location permission tags
|
||||||
|
|
||||||
|
### iOS / MacOS
|
||||||
|
|
||||||
|
Add `NSBluetoothPeripheralUsageDescription` and `NSBluetoothAlwaysUsageDescription` in Info.plist of your iOS and MacOS app,
|
||||||
|
|
||||||
|
Add `Bluetooth` capability to MacOS app from Xcode
|
||||||
|
|
||||||
|
## Scan BLE peripheral
|
||||||
|
|
||||||
|
Android/iOS/macOS/Windows/Linux
|
||||||
|
|
||||||
|
```dart
|
||||||
|
UniversalBle.onScanResult = (scanResult) {
|
||||||
|
// Handle scan result
|
||||||
|
}
|
||||||
|
|
||||||
|
UniversalBle.startScan();
|
||||||
|
|
||||||
|
// To scan on web, you can optionally add filters, and `optionalServices` required to discover those services after connection, for more info check [this](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice)
|
||||||
|
UniversalBle.startScan(
|
||||||
|
webRequestOptions: WebRequestOptionsBuilder.acceptAllDevices(
|
||||||
|
optionalServices: [
|
||||||
|
"SERVICE_UUID",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
UniversalBle.stopScan();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Connect to BLE peripheral
|
||||||
|
|
||||||
|
Connect to `deviceId`, received from `UniversalBle.onScanResult`
|
||||||
|
|
||||||
|
```dart
|
||||||
|
UniversalBle.onConnectionChanged = (String deviceId, BleConnectionState state) {
|
||||||
|
print('onConnectionChanged $deviceId, $state');
|
||||||
|
}
|
||||||
|
|
||||||
|
UniversalBle.connect(deviceId);
|
||||||
|
|
||||||
|
UniversalBle.disconnect(deviceId);
|
||||||
|
```
|
||||||
|
|
||||||
|
Get connected devices ( These devices might be connected through another applications, connect using `connect` method to use in your application ), use `withServices` to filter devices
|
||||||
|
|
||||||
|
```dart
|
||||||
|
await UniversalBle.getConnectedDevices(withServices: []);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Discover services of BLE peripheral
|
||||||
|
|
||||||
|
Discover services of `deviceId`
|
||||||
|
|
||||||
|
```dart
|
||||||
|
UniversalBle.discoverServices(deviceId);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transfer data between BLE central & peripheral
|
||||||
|
|
||||||
|
- Pull data from peripheral of `deviceId`
|
||||||
|
|
||||||
|
```dart
|
||||||
|
UniversalBle.readValue(deviceId, serviceId, characteristicId);
|
||||||
|
```
|
||||||
|
|
||||||
|
- Send data to peripheral of `deviceId`
|
||||||
|
|
||||||
|
```dart
|
||||||
|
UniversalBle.writeValue(deviceId, serviceId, characteristicId, value);
|
||||||
|
```
|
||||||
|
|
||||||
|
- Receive data from peripheral of `deviceId`
|
||||||
|
|
||||||
|
```dart
|
||||||
|
UniversalBle.onValueChanged = (String deviceId, String characteristicId, Uint8List value) {
|
||||||
|
print('onValueChanged $deviceId, $characteristicId, ${hex.encode(value)}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// To get characteristic updates in `onValueChanged`
|
||||||
|
UniversalBle.setNotifiable(deviceId, serviceId, characteristicId, BleInputProperty.notification);
|
||||||
|
|
||||||
|
// To stop updates
|
||||||
|
UniversalBle.setNotifiable(deviceId, serviceId, characteristicId, BleInputProperty.disabled);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pair BLE central & peripheral
|
||||||
|
|
||||||
|
- Pair apis
|
||||||
|
|
||||||
|
```dart
|
||||||
|
UniversalBle.onPairStateChange = (String deviceId, bool isPaired, String? error) {
|
||||||
|
// Handle Pairing state change
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get result in [onPairStateChange]
|
||||||
|
UniversalBle.pair(deviceId);
|
||||||
|
|
||||||
|
UniversalBle.unPair(deviceId);
|
||||||
|
|
||||||
|
UniversalBle.isPaired(deviceId);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Enable bluetooth programatically
|
||||||
|
|
||||||
|
```dart
|
||||||
|
UniversalBle.enableBluetooth();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Receive BLE availability changes
|
||||||
|
|
||||||
|
```dart
|
||||||
|
UniversalBle.onAvailabilityChange = (state) {
|
||||||
|
// Handle ble availability states
|
||||||
|
// e.g. poweredOff or poweredOn,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## To set your own implementation of UniversalBle for any specific platform
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Create a class which extends `UniversalBlePlatform`
|
||||||
|
class UniversalBleMock extends UniversalBlePlatform {
|
||||||
|
// Implement all methods
|
||||||
|
}
|
||||||
|
|
||||||
|
UniversalBle.setInstance(UniversalBleMock());
|
||||||
|
```
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/libraries
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.cxx
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
group 'com.navideck.universal_ble'
|
||||||
|
version '1.0-SNAPSHOT'
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
ext.kotlin_version = '1.7.10'
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:7.3.0'
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
android {
|
||||||
|
if (project.android.hasProperty("namespace")) {
|
||||||
|
namespace 'com.navideck.universal_ble'
|
||||||
|
}
|
||||||
|
|
||||||
|
compileSdkVersion 34
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '1.8'
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
|
test.java.srcDirs += 'src/test/kotlin'
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 21
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation 'org.jetbrains.kotlin:kotlin-test'
|
||||||
|
testImplementation 'org.mockito:mockito-core:5.0.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
testOptions {
|
||||||
|
unitTests.all {
|
||||||
|
useJUnitPlatform()
|
||||||
|
|
||||||
|
testLogging {
|
||||||
|
events "passed", "skipped", "failed", "standardOut", "standardError"
|
||||||
|
outputs.upToDateWhen {false}
|
||||||
|
showStandardStreams = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
rootProject.name = 'universal_ble'
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="com.navideck.universal_ble">
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.BLUETOOTH"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.BLUETOOTH_ADMIN"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,598 @@
|
|||||||
|
// Autogenerated from Pigeon (v13.0.0), do not edit directly.
|
||||||
|
// See also: https://pub.dev/packages/pigeon
|
||||||
|
|
||||||
|
package com.navideck.universal_ble
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import io.flutter.plugin.common.BasicMessageChannel
|
||||||
|
import io.flutter.plugin.common.BinaryMessenger
|
||||||
|
import io.flutter.plugin.common.MessageCodec
|
||||||
|
import io.flutter.plugin.common.StandardMessageCodec
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
|
private fun wrapResult(result: Any?): List<Any?> {
|
||||||
|
return listOf(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun wrapError(exception: Throwable): List<Any?> {
|
||||||
|
if (exception is FlutterError) {
|
||||||
|
return listOf(
|
||||||
|
exception.code,
|
||||||
|
exception.message,
|
||||||
|
exception.details
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return listOf(
|
||||||
|
exception.javaClass.simpleName,
|
||||||
|
exception.toString(),
|
||||||
|
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error class for passing custom error details to Flutter via a thrown PlatformException.
|
||||||
|
* @property code The error code.
|
||||||
|
* @property message The error message.
|
||||||
|
* @property details The error details. Must be a datatype supported by the api codec.
|
||||||
|
*/
|
||||||
|
class FlutterError (
|
||||||
|
val code: String,
|
||||||
|
override val message: String? = null,
|
||||||
|
val details: Any? = null
|
||||||
|
) : Throwable()
|
||||||
|
|
||||||
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
|
data class UniversalBleScanResult (
|
||||||
|
val deviceId: String,
|
||||||
|
val name: String? = null,
|
||||||
|
val isPaired: Boolean? = null,
|
||||||
|
val rssi: Long? = null,
|
||||||
|
val manufacturerData: ByteArray? = null,
|
||||||
|
val manufacturerDataHead: ByteArray? = null
|
||||||
|
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun fromList(list: List<Any?>): UniversalBleScanResult {
|
||||||
|
val deviceId = list[0] as String
|
||||||
|
val name = list[1] as String?
|
||||||
|
val isPaired = list[2] as Boolean?
|
||||||
|
val rssi = list[3].let { if (it is Int) it.toLong() else it as Long? }
|
||||||
|
val manufacturerData = list[4] as ByteArray?
|
||||||
|
val manufacturerDataHead = list[5] as ByteArray?
|
||||||
|
return UniversalBleScanResult(deviceId, name, isPaired, rssi, manufacturerData, manufacturerDataHead)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun toList(): List<Any?> {
|
||||||
|
return listOf<Any?>(
|
||||||
|
deviceId,
|
||||||
|
name,
|
||||||
|
isPaired,
|
||||||
|
rssi,
|
||||||
|
manufacturerData,
|
||||||
|
manufacturerDataHead,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
|
data class UniversalBleService (
|
||||||
|
val uuid: String,
|
||||||
|
val characteristics: List<UniversalBleCharacteristic?>? = null
|
||||||
|
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun fromList(list: List<Any?>): UniversalBleService {
|
||||||
|
val uuid = list[0] as String
|
||||||
|
val characteristics = list[1] as List<UniversalBleCharacteristic?>?
|
||||||
|
return UniversalBleService(uuid, characteristics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun toList(): List<Any?> {
|
||||||
|
return listOf<Any?>(
|
||||||
|
uuid,
|
||||||
|
characteristics,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
|
data class UniversalBleCharacteristic (
|
||||||
|
val uuid: String,
|
||||||
|
val properties: List<Long?>
|
||||||
|
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun fromList(list: List<Any?>): UniversalBleCharacteristic {
|
||||||
|
val uuid = list[0] as String
|
||||||
|
val properties = list[1] as List<Long?>
|
||||||
|
return UniversalBleCharacteristic(uuid, properties)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun toList(): List<Any?> {
|
||||||
|
return listOf<Any?>(
|
||||||
|
uuid,
|
||||||
|
properties,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private object UniversalBlePlatformChannelCodec : StandardMessageCodec() {
|
||||||
|
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||||
|
return when (type) {
|
||||||
|
128.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
UniversalBleCharacteristic.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
129.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
UniversalBleScanResult.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
130.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
UniversalBleService.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> super.readValueOfType(type, buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||||
|
when (value) {
|
||||||
|
is UniversalBleCharacteristic -> {
|
||||||
|
stream.write(128)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
|
is UniversalBleScanResult -> {
|
||||||
|
stream.write(129)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
|
is UniversalBleService -> {
|
||||||
|
stream.write(130)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
|
else -> super.writeValue(stream, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flutter -> Native
|
||||||
|
*
|
||||||
|
* Generated interface from Pigeon that represents a handler of messages from Flutter.
|
||||||
|
*/
|
||||||
|
interface UniversalBlePlatformChannel {
|
||||||
|
fun getBluetoothAvailabilityState(callback: (Result<Long>) -> Unit)
|
||||||
|
fun enableBluetooth(callback: (Result<Boolean>) -> Unit)
|
||||||
|
fun startScan()
|
||||||
|
fun stopScan()
|
||||||
|
fun connect(deviceId: String)
|
||||||
|
fun disconnect(deviceId: String)
|
||||||
|
fun setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: Long)
|
||||||
|
fun discoverServices(deviceId: String, callback: (Result<List<UniversalBleService>>) -> Unit)
|
||||||
|
fun readValue(deviceId: String, service: String, characteristic: String, callback: (Result<ByteArray>) -> Unit)
|
||||||
|
fun requestMtu(deviceId: String, expectedMtu: Long, callback: (Result<Long>) -> Unit)
|
||||||
|
fun writeValue(deviceId: String, service: String, characteristic: String, value: ByteArray, bleOutputProperty: Long, callback: (Result<Unit>) -> Unit)
|
||||||
|
fun isPaired(deviceId: String, callback: (Result<Boolean>) -> Unit)
|
||||||
|
fun pair(deviceId: String)
|
||||||
|
fun unPair(deviceId: String)
|
||||||
|
fun getConnectedDevices(withServices: List<String>, callback: (Result<List<UniversalBleScanResult>>) -> Unit)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** The codec used by UniversalBlePlatformChannel. */
|
||||||
|
val codec: MessageCodec<Any?> by lazy {
|
||||||
|
UniversalBlePlatformChannelCodec
|
||||||
|
}
|
||||||
|
/** Sets up an instance of `UniversalBlePlatformChannel` to handle messages through the `binaryMessenger`. */
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun setUp(binaryMessenger: BinaryMessenger, api: UniversalBlePlatformChannel?) {
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getBluetoothAvailabilityState", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
api.getBluetoothAvailabilityState() { result: Result<Long> ->
|
||||||
|
val error = result.exceptionOrNull()
|
||||||
|
if (error != null) {
|
||||||
|
reply.reply(wrapError(error))
|
||||||
|
} else {
|
||||||
|
val data = result.getOrNull()
|
||||||
|
reply.reply(wrapResult(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.enableBluetooth", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
api.enableBluetooth() { result: Result<Boolean> ->
|
||||||
|
val error = result.exceptionOrNull()
|
||||||
|
if (error != null) {
|
||||||
|
reply.reply(wrapError(error))
|
||||||
|
} else {
|
||||||
|
val data = result.getOrNull()
|
||||||
|
reply.reply(wrapResult(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
var wrapped: List<Any?>
|
||||||
|
try {
|
||||||
|
api.startScan()
|
||||||
|
wrapped = listOf<Any?>(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
wrapped = wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.stopScan", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
var wrapped: List<Any?>
|
||||||
|
try {
|
||||||
|
api.stopScan()
|
||||||
|
wrapped = listOf<Any?>(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
wrapped = wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val deviceIdArg = args[0] as String
|
||||||
|
var wrapped: List<Any?>
|
||||||
|
try {
|
||||||
|
api.connect(deviceIdArg)
|
||||||
|
wrapped = listOf<Any?>(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
wrapped = wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disconnect", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val deviceIdArg = args[0] as String
|
||||||
|
var wrapped: List<Any?>
|
||||||
|
try {
|
||||||
|
api.disconnect(deviceIdArg)
|
||||||
|
wrapped = listOf<Any?>(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
wrapped = wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setNotifiable", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val deviceIdArg = args[0] as String
|
||||||
|
val serviceArg = args[1] as String
|
||||||
|
val characteristicArg = args[2] as String
|
||||||
|
val bleInputPropertyArg = args[3].let { if (it is Int) it.toLong() else it as Long }
|
||||||
|
var wrapped: List<Any?>
|
||||||
|
try {
|
||||||
|
api.setNotifiable(deviceIdArg, serviceArg, characteristicArg, bleInputPropertyArg)
|
||||||
|
wrapped = listOf<Any?>(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
wrapped = wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.discoverServices", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val deviceIdArg = args[0] as String
|
||||||
|
api.discoverServices(deviceIdArg) { result: Result<List<UniversalBleService>> ->
|
||||||
|
val error = result.exceptionOrNull()
|
||||||
|
if (error != null) {
|
||||||
|
reply.reply(wrapError(error))
|
||||||
|
} else {
|
||||||
|
val data = result.getOrNull()
|
||||||
|
reply.reply(wrapResult(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.readValue", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val deviceIdArg = args[0] as String
|
||||||
|
val serviceArg = args[1] as String
|
||||||
|
val characteristicArg = args[2] as String
|
||||||
|
api.readValue(deviceIdArg, serviceArg, characteristicArg) { result: Result<ByteArray> ->
|
||||||
|
val error = result.exceptionOrNull()
|
||||||
|
if (error != null) {
|
||||||
|
reply.reply(wrapError(error))
|
||||||
|
} else {
|
||||||
|
val data = result.getOrNull()
|
||||||
|
reply.reply(wrapResult(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestMtu", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val deviceIdArg = args[0] as String
|
||||||
|
val expectedMtuArg = args[1].let { if (it is Int) it.toLong() else it as Long }
|
||||||
|
api.requestMtu(deviceIdArg, expectedMtuArg) { result: Result<Long> ->
|
||||||
|
val error = result.exceptionOrNull()
|
||||||
|
if (error != null) {
|
||||||
|
reply.reply(wrapError(error))
|
||||||
|
} else {
|
||||||
|
val data = result.getOrNull()
|
||||||
|
reply.reply(wrapResult(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.writeValue", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val deviceIdArg = args[0] as String
|
||||||
|
val serviceArg = args[1] as String
|
||||||
|
val characteristicArg = args[2] as String
|
||||||
|
val valueArg = args[3] as ByteArray
|
||||||
|
val bleOutputPropertyArg = args[4].let { if (it is Int) it.toLong() else it as Long }
|
||||||
|
api.writeValue(deviceIdArg, serviceArg, characteristicArg, valueArg, bleOutputPropertyArg) { result: Result<Unit> ->
|
||||||
|
val error = result.exceptionOrNull()
|
||||||
|
if (error != null) {
|
||||||
|
reply.reply(wrapError(error))
|
||||||
|
} else {
|
||||||
|
reply.reply(wrapResult(null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isPaired", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val deviceIdArg = args[0] as String
|
||||||
|
api.isPaired(deviceIdArg) { result: Result<Boolean> ->
|
||||||
|
val error = result.exceptionOrNull()
|
||||||
|
if (error != null) {
|
||||||
|
reply.reply(wrapError(error))
|
||||||
|
} else {
|
||||||
|
val data = result.getOrNull()
|
||||||
|
reply.reply(wrapResult(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.pair", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val deviceIdArg = args[0] as String
|
||||||
|
var wrapped: List<Any?>
|
||||||
|
try {
|
||||||
|
api.pair(deviceIdArg)
|
||||||
|
wrapped = listOf<Any?>(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
wrapped = wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.unPair", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val deviceIdArg = args[0] as String
|
||||||
|
var wrapped: List<Any?>
|
||||||
|
try {
|
||||||
|
api.unPair(deviceIdArg)
|
||||||
|
wrapped = listOf<Any?>(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
wrapped = wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getConnectedDevices", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val withServicesArg = args[0] as List<String>
|
||||||
|
api.getConnectedDevices(withServicesArg) { result: Result<List<UniversalBleScanResult>> ->
|
||||||
|
val error = result.exceptionOrNull()
|
||||||
|
if (error != null) {
|
||||||
|
reply.reply(wrapError(error))
|
||||||
|
} else {
|
||||||
|
val data = result.getOrNull()
|
||||||
|
reply.reply(wrapResult(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private object UniversalBleCallbackChannelCodec : StandardMessageCodec() {
|
||||||
|
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||||
|
return when (type) {
|
||||||
|
128.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
UniversalBleScanResult.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> super.readValueOfType(type, buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||||
|
when (value) {
|
||||||
|
is UniversalBleScanResult -> {
|
||||||
|
stream.write(128)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
|
else -> super.writeValue(stream, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Native -> Flutter
|
||||||
|
*
|
||||||
|
* Generated class from Pigeon that represents Flutter messages that can be called from Kotlin.
|
||||||
|
*/
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
class UniversalBleCallbackChannel(private val binaryMessenger: BinaryMessenger) {
|
||||||
|
companion object {
|
||||||
|
/** The codec used by UniversalBleCallbackChannel. */
|
||||||
|
val codec: MessageCodec<Any?> by lazy {
|
||||||
|
UniversalBleCallbackChannelCodec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun onAvailabilityChanged(stateArg: Long, callback: (Result<Unit>) -> Unit) {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged", codec)
|
||||||
|
channel.send(listOf(stateArg)) {
|
||||||
|
if (it is List<*>) {
|
||||||
|
if (it.size > 1) {
|
||||||
|
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)));
|
||||||
|
} else {
|
||||||
|
callback(Result.success(Unit));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback(Result.failure(FlutterError("channel-error", "Unable to establish connection on channel.", "")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun onPairStateChange(deviceIdArg: String, isPairedArg: Boolean, errorArg: String?, callback: (Result<Unit>) -> Unit) {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange", codec)
|
||||||
|
channel.send(listOf(deviceIdArg, isPairedArg, errorArg)) {
|
||||||
|
if (it is List<*>) {
|
||||||
|
if (it.size > 1) {
|
||||||
|
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)));
|
||||||
|
} else {
|
||||||
|
callback(Result.success(Unit));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback(Result.failure(FlutterError("channel-error", "Unable to establish connection on channel.", "")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun onScanResult(resultArg: UniversalBleScanResult, callback: (Result<Unit>) -> Unit) {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult", codec)
|
||||||
|
channel.send(listOf(resultArg)) {
|
||||||
|
if (it is List<*>) {
|
||||||
|
if (it.size > 1) {
|
||||||
|
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)));
|
||||||
|
} else {
|
||||||
|
callback(Result.success(Unit));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback(Result.failure(FlutterError("channel-error", "Unable to establish connection on channel.", "")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun onValueChanged(deviceIdArg: String, characteristicIdArg: String, valueArg: ByteArray, callback: (Result<Unit>) -> Unit) {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged", codec)
|
||||||
|
channel.send(listOf(deviceIdArg, characteristicIdArg, valueArg)) {
|
||||||
|
if (it is List<*>) {
|
||||||
|
if (it.size > 1) {
|
||||||
|
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)));
|
||||||
|
} else {
|
||||||
|
callback(Result.success(Unit));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback(Result.failure(FlutterError("channel-error", "Unable to establish connection on channel.", "")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun onConnectionChanged(deviceIdArg: String, stateArg: Long, callback: (Result<Unit>) -> Unit) {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged", codec)
|
||||||
|
channel.send(listOf(deviceIdArg, stateArg)) {
|
||||||
|
if (it is List<*>) {
|
||||||
|
if (it.size > 1) {
|
||||||
|
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)));
|
||||||
|
} else {
|
||||||
|
callback(Result.success(Unit));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback(Result.failure(FlutterError("channel-error", "Unable to establish connection on channel.", "")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
package com.navideck.universal_ble
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.bluetooth.BluetoothGatt
|
||||||
|
import android.bluetooth.BluetoothGattCharacteristic
|
||||||
|
import android.bluetooth.BluetoothGattDescriptor
|
||||||
|
import android.bluetooth.BluetoothStatusCodes
|
||||||
|
import android.bluetooth.le.ScanCallback.SCAN_FAILED_ALREADY_STARTED
|
||||||
|
import android.bluetooth.le.ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED
|
||||||
|
import android.bluetooth.le.ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED
|
||||||
|
import android.bluetooth.le.ScanCallback.SCAN_FAILED_INTERNAL_ERROR
|
||||||
|
import android.bluetooth.le.ScanCallback.SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES
|
||||||
|
import android.bluetooth.le.ScanCallback.SCAN_FAILED_SCANNING_TOO_FREQUENTLY
|
||||||
|
import android.bluetooth.le.ScanResult
|
||||||
|
import android.util.Log
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
private const val TAG = "UniversalBlePlugin"
|
||||||
|
|
||||||
|
val knownGatts = mutableListOf<BluetoothGatt>()
|
||||||
|
|
||||||
|
|
||||||
|
enum class BleConnectionState(val value: Long) {
|
||||||
|
Connected(0),
|
||||||
|
Disconnected(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class AvailabilityState(val value: Long) {
|
||||||
|
Unknown(0),
|
||||||
|
Resetting(1),
|
||||||
|
Unsupported(2),
|
||||||
|
Unauthorized(3),
|
||||||
|
PoweredOff(4),
|
||||||
|
PoweredOn(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class BleInputProperty(val value: Long) {
|
||||||
|
Disabled(0),
|
||||||
|
Notification(1),
|
||||||
|
Indication(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class BleOutputProperty(val value: Long) {
|
||||||
|
withResponse(0),
|
||||||
|
withoutResponse(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum class CharacteristicProperty(val value: Long) {
|
||||||
|
Broadcast(0),
|
||||||
|
Read(1),
|
||||||
|
WriteWithoutResponse(2),
|
||||||
|
Write(3),
|
||||||
|
Notify(4),
|
||||||
|
Indicate(5),
|
||||||
|
AuthenticatedSignedWrites(6),
|
||||||
|
ExtendedProperties(7)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.toBluetoothGatt(): BluetoothGatt {
|
||||||
|
return knownGatts.find { it.device.address == this }
|
||||||
|
?: throw FlutterError("IllegalArgument", "Unknown deviceId: $this", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.toAvailabilityState(): Long {
|
||||||
|
return when (this) {
|
||||||
|
BluetoothAdapter.STATE_OFF -> AvailabilityState.PoweredOff.value
|
||||||
|
BluetoothAdapter.STATE_ON -> AvailabilityState.PoweredOn.value
|
||||||
|
BluetoothAdapter.STATE_TURNING_ON -> AvailabilityState.Resetting.value
|
||||||
|
BluetoothAdapter.STATE_TURNING_OFF -> AvailabilityState.Resetting.value
|
||||||
|
else -> AvailabilityState.Unknown.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.parseScanErrorMessage(): String {
|
||||||
|
return when (this) {
|
||||||
|
SCAN_FAILED_ALREADY_STARTED -> "SCAN_FAILED_ALREADY_STARTED"
|
||||||
|
SCAN_FAILED_APPLICATION_REGISTRATION_FAILED -> "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED"
|
||||||
|
SCAN_FAILED_FEATURE_UNSUPPORTED -> "SCAN_FAILED_FEATURE_UNSUPPORTED"
|
||||||
|
SCAN_FAILED_INTERNAL_ERROR -> "SCAN_FAILED_INTERNAL_ERROR"
|
||||||
|
SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES -> "SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES"
|
||||||
|
SCAN_FAILED_SCANNING_TOO_FREQUENTLY -> "SCAN_FAILED_SCANNING_TOO_FREQUENTLY"
|
||||||
|
else -> "ErrorCode: $this"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.parseBluetoothStatusCodeError(): String? {
|
||||||
|
if (this == BluetoothStatusCodes.SUCCESS) return null
|
||||||
|
return when (this) {
|
||||||
|
BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED -> "ERROR_BLUETOOTH_NOT_ENABLED"
|
||||||
|
BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ALLOWED -> "ERROR_BLUETOOTH_NOT_ALLOWED"
|
||||||
|
BluetoothStatusCodes.ERROR_DEVICE_NOT_BONDED -> "ERROR_DEVICE_NOT_BONDED"
|
||||||
|
BluetoothStatusCodes.ERROR_GATT_WRITE_NOT_ALLOWED -> "ERROR_GATT_WRITE_NOT_ALLOWED"
|
||||||
|
BluetoothStatusCodes.ERROR_GATT_WRITE_REQUEST_BUSY -> "ERROR_GATT_WRITE_REQUEST_BUSY"
|
||||||
|
BluetoothStatusCodes.ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION -> "ERROR_MISSING_BLUETOOTH_CONNECT_PERMISSION"
|
||||||
|
BluetoothStatusCodes.ERROR_PROFILE_SERVICE_NOT_BOUND -> "ERROR_PROFILE_SERVICE_NOT_BOUND"
|
||||||
|
BluetoothStatusCodes.ERROR_UNKNOWN -> "ERROR_UNKNOWN"
|
||||||
|
BluetoothStatusCodes.FEATURE_NOT_CONFIGURED -> "FEATURE_NOT_CONFIGURED"
|
||||||
|
BluetoothStatusCodes.FEATURE_NOT_SUPPORTED -> "FEATURE_NOT_SUPPORTED"
|
||||||
|
BluetoothStatusCodes.FEATURE_SUPPORTED -> "FEATURE_SUPPORTED"
|
||||||
|
else -> "ErrorCode: $this"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val ScanResult.manufacturerDataHead: ByteArray?
|
||||||
|
get() {
|
||||||
|
val sparseArray = scanRecord?.manufacturerSpecificData ?: return null
|
||||||
|
if (sparseArray.size() == 0) return null
|
||||||
|
|
||||||
|
return sparseArray.keyAt(0).toShort().toByteArray() + sparseArray.valueAt(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BluetoothGatt.getCharacteristic(
|
||||||
|
service: String,
|
||||||
|
characteristic: String,
|
||||||
|
): BluetoothGattCharacteristic? =
|
||||||
|
getService(UUID.fromString(service)).getCharacteristic(UUID.fromString(characteristic))
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun BluetoothGatt.setNotifiable(
|
||||||
|
gattCharacteristic: BluetoothGattCharacteristic,
|
||||||
|
bleInputProperty: Long,
|
||||||
|
) {
|
||||||
|
val descClientCharConfirmation =
|
||||||
|
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
|
||||||
|
val descriptor = gattCharacteristic.getDescriptor(descClientCharConfirmation)
|
||||||
|
val bleInputPropertyEnum: BleInputProperty =
|
||||||
|
BleInputProperty.values().first { it.value == bleInputProperty }
|
||||||
|
val (value, enable) = when (bleInputPropertyEnum) {
|
||||||
|
BleInputProperty.Notification -> BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE to true
|
||||||
|
BleInputProperty.Indication -> BluetoothGattDescriptor.ENABLE_INDICATION_VALUE to true
|
||||||
|
else -> BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE to false
|
||||||
|
}
|
||||||
|
descriptor.value = value
|
||||||
|
setCharacteristicNotification(descriptor.characteristic, enable) && writeDescriptor(descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BluetoothDevice.removeBond() {
|
||||||
|
try {
|
||||||
|
javaClass.getMethod("removeBond").invoke(this)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Removing bond has been failed. ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun BluetoothGattCharacteristic.getPropertiesList(): ArrayList<Long> {
|
||||||
|
val propertiesList = arrayListOf<Long>()
|
||||||
|
if (properties and BluetoothGattCharacteristic.PROPERTY_BROADCAST > 0) {
|
||||||
|
propertiesList.add(CharacteristicProperty.Broadcast.value)
|
||||||
|
}
|
||||||
|
if (properties and BluetoothGattCharacteristic.PROPERTY_READ > 0) {
|
||||||
|
propertiesList.add(CharacteristicProperty.Read.value)
|
||||||
|
}
|
||||||
|
if (properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE > 0) {
|
||||||
|
propertiesList.add(CharacteristicProperty.WriteWithoutResponse.value)
|
||||||
|
}
|
||||||
|
if (properties and BluetoothGattCharacteristic.PROPERTY_WRITE > 0) {
|
||||||
|
propertiesList.add(CharacteristicProperty.Write.value)
|
||||||
|
}
|
||||||
|
if (properties and BluetoothGattCharacteristic.PROPERTY_NOTIFY > 0) {
|
||||||
|
propertiesList.add(CharacteristicProperty.Notify.value)
|
||||||
|
}
|
||||||
|
if (properties and BluetoothGattCharacteristic.PROPERTY_INDICATE > 0) {
|
||||||
|
propertiesList.add(CharacteristicProperty.Indicate.value)
|
||||||
|
}
|
||||||
|
if (properties and BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE > 0) {
|
||||||
|
propertiesList.add(CharacteristicProperty.AuthenticatedSignedWrites.value)
|
||||||
|
}
|
||||||
|
if (properties and BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS > 0) {
|
||||||
|
propertiesList.add(CharacteristicProperty.ExtendedProperties.value)
|
||||||
|
}
|
||||||
|
return propertiesList
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Short.toByteArray(byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN): ByteArray =
|
||||||
|
ByteBuffer.allocate(2 /*Short.SIZE_BYTES*/).order(byteOrder).putShort(this).array()
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
fun unknownCharacteristicError(char: String) =
|
||||||
|
FlutterError("IllegalArgument", "Unknown error", null)
|
||||||
|
|
||||||
|
// Future result classes
|
||||||
|
class BleCharacteristicFuture(
|
||||||
|
val deviceId: String,
|
||||||
|
val characteristicId: String,
|
||||||
|
val serviceId: String,
|
||||||
|
val result: (Result<ByteArray>) -> Unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
class DiscoverServicesFuture(
|
||||||
|
val deviceId: String,
|
||||||
|
val result: (Result<List<UniversalBleService>>) -> Unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
class MtuResultFuture(
|
||||||
|
val deviceId: String,
|
||||||
|
val result: (Result<Long>) -> Unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
class WriteResultFuture(
|
||||||
|
val deviceId: String,
|
||||||
|
val characteristicId: String,
|
||||||
|
val serviceId: String,
|
||||||
|
val result: (Result<Unit>) -> Unit,
|
||||||
|
)
|
||||||
@@ -0,0 +1,743 @@
|
|||||||
|
package com.navideck.universal_ble
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.bluetooth.*
|
||||||
|
import android.bluetooth.BluetoothDevice.BOND_BONDED
|
||||||
|
import android.bluetooth.BluetoothDevice.BOND_NONE
|
||||||
|
import android.bluetooth.le.ScanCallback
|
||||||
|
import android.bluetooth.le.ScanResult
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Context.RECEIVER_EXPORTED
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
|
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||||
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||||
|
import io.flutter.plugin.common.*
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
private const val TAG = "UniversalBlePlugin"
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), FlutterPlugin,
|
||||||
|
ActivityAware, PluginRegistry.ActivityResultListener {
|
||||||
|
private val bluetoothEnableRequestCode = 2342313
|
||||||
|
private var callbackChannel: UniversalBleCallbackChannel? = null
|
||||||
|
private var mainThreadHandler: Handler? = null
|
||||||
|
private lateinit var context: Context
|
||||||
|
private var activity: Activity? = null
|
||||||
|
private lateinit var bluetoothManager: BluetoothManager
|
||||||
|
private val mtuResultFutureList = mutableListOf<MtuResultFuture>()
|
||||||
|
private val bleCharacteristicFutureList = mutableListOf<BleCharacteristicFuture>()
|
||||||
|
private val discoverServicesFutureList = mutableListOf<DiscoverServicesFuture>()
|
||||||
|
private val writeResultFutureList = mutableListOf<WriteResultFuture>()
|
||||||
|
private val cachedServicesMap = mutableMapOf<String, List<String>>()
|
||||||
|
private val devicesStateMap = mutableMapOf<String, Int>()
|
||||||
|
private var bluetoothEnableRequestFuture: ((Result<Boolean>) -> Unit)? = null
|
||||||
|
|
||||||
|
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
|
UniversalBlePlatformChannel.setUp(flutterPluginBinding.binaryMessenger, this)
|
||||||
|
callbackChannel = UniversalBleCallbackChannel(flutterPluginBinding.binaryMessenger)
|
||||||
|
context = flutterPluginBinding.applicationContext
|
||||||
|
mainThreadHandler = Handler(Looper.getMainLooper())
|
||||||
|
bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||||
|
|
||||||
|
val intentFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
|
||||||
|
intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
context.registerReceiver(broadcastReceiver, intentFilter, RECEIVER_EXPORTED)
|
||||||
|
} else {
|
||||||
|
context.registerReceiver(broadcastReceiver, intentFilter)
|
||||||
|
}
|
||||||
|
cachedServicesMap.putAll(getCachedServicesMap())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
|
bluetoothManager.adapter.bluetoothLeScanner?.stopScan(scanCallback)
|
||||||
|
context.unregisterReceiver(broadcastReceiver)
|
||||||
|
callbackChannel = null
|
||||||
|
mainThreadHandler = null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun getBluetoothAvailabilityState(callback: (Result<Long>) -> Unit) {
|
||||||
|
callback(
|
||||||
|
Result.success(
|
||||||
|
bluetoothManager.adapter?.state?.toAvailabilityState()
|
||||||
|
?: AvailabilityState.Unknown.value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun enableBluetooth(callback: (Result<Boolean>) -> Unit) {
|
||||||
|
if (bluetoothManager.adapter.isEnabled) {
|
||||||
|
callback(Result.success(true))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (bluetoothEnableRequestFuture != null) {
|
||||||
|
callback(
|
||||||
|
Result.failure(
|
||||||
|
FlutterError("Failed", "Bluetooth enable request in progress", null)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
|
||||||
|
activity?.startActivityForResult(enableBtIntent, bluetoothEnableRequestCode)
|
||||||
|
bluetoothEnableRequestFuture = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startScan() {
|
||||||
|
if (!isBluetoothAvailable()) throw FlutterError(
|
||||||
|
"BluetoothNotEnabled",
|
||||||
|
"Bluetooth not enabled",
|
||||||
|
)
|
||||||
|
bluetoothManager.adapter.bluetoothLeScanner?.startScan(scanCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopScan() {
|
||||||
|
if (!isBluetoothAvailable()) throw FlutterError(
|
||||||
|
"BluetoothNotEnabled",
|
||||||
|
"Bluetooth not enabled",
|
||||||
|
)
|
||||||
|
// check if already scanning
|
||||||
|
bluetoothManager.adapter.bluetoothLeScanner?.stopScan(scanCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun connect(deviceId: String) {
|
||||||
|
val currentState = devicesStateMap[deviceId]
|
||||||
|
|
||||||
|
// If already connected, send connected message,
|
||||||
|
// if connecting, do nothing
|
||||||
|
knownGatts.find { it.device.address == deviceId }?.let {
|
||||||
|
if (currentState == BluetoothGatt.STATE_CONNECTED) {
|
||||||
|
Log.e(TAG, "$deviceId Already connected")
|
||||||
|
mainThreadHandler?.post {
|
||||||
|
callbackChannel?.onConnectionChanged(
|
||||||
|
deviceId,
|
||||||
|
BleConnectionState.Connected.value
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if (currentState == BluetoothGatt.STATE_CONNECTING) {
|
||||||
|
throw FlutterError("Connecting", "Connection already in progress", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val remoteDevice = bluetoothManager.adapter.getRemoteDevice(deviceId)
|
||||||
|
val gatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
remoteDevice.connectGatt(
|
||||||
|
context,
|
||||||
|
false,
|
||||||
|
this,
|
||||||
|
BluetoothDevice.TRANSPORT_LE
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
remoteDevice.connectGatt(context, false, this)
|
||||||
|
}
|
||||||
|
knownGatts.add(gatt)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun disconnect(deviceId: String) {
|
||||||
|
// val currentState = devicesStateMap[deviceId]
|
||||||
|
// If `disconnect` is called before BluetoothGatt.STATE_CONNECTED or BluetoothGatt.STATE_CONNECTING
|
||||||
|
// there will be no `disconnected` message any more from callback
|
||||||
|
// if (currentState != null && currentState != BluetoothGatt.STATE_CONNECTED) {
|
||||||
|
// Log.e(TAG, "$deviceId Already disconnected or disconnecting")
|
||||||
|
// mainThreadHandler?.post {
|
||||||
|
// callbackChannel?.onConnectionChanged(
|
||||||
|
// deviceId,
|
||||||
|
// BleConnectionState.Disconnected.value
|
||||||
|
// ) {}
|
||||||
|
// }
|
||||||
|
// return
|
||||||
|
//}
|
||||||
|
cleanConnection(deviceId.toBluetoothGatt())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun discoverServices(
|
||||||
|
deviceId: String,
|
||||||
|
callback: (Result<List<UniversalBleService>>) -> Unit,
|
||||||
|
) {
|
||||||
|
if (!deviceId.toBluetoothGatt().discoverServices()) {
|
||||||
|
callback(Result.failure(FlutterError("Failed", "Failed to discover services", null)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
discoverServicesFutureList.add(DiscoverServicesFuture(deviceId, callback))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
|
||||||
|
if (status != BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
discoverServicesFutureList.filter { it.deviceId == gatt.device.address }.forEach {
|
||||||
|
discoverServicesFutureList.remove(it)
|
||||||
|
it.result(
|
||||||
|
Result.failure(FlutterError("Failed", "Failed to discover services", null))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setCachedServices(gatt.device.address, gatt.services.map { it.uuid.toString() })
|
||||||
|
val universalBleServices = gatt.services.map { service ->
|
||||||
|
UniversalBleService(
|
||||||
|
uuid = service.uuid.toString(),
|
||||||
|
characteristics = service.characteristics.map {
|
||||||
|
UniversalBleCharacteristic(
|
||||||
|
uuid = it.uuid.toString(),
|
||||||
|
properties = it.getPropertiesList()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
discoverServicesFutureList.filter { it.deviceId == gatt.device.address }.forEach {
|
||||||
|
discoverServicesFutureList.remove(it)
|
||||||
|
it.result(Result.success(universalBleServices))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun setNotifiable(
|
||||||
|
deviceId: String,
|
||||||
|
service: String,
|
||||||
|
characteristic: String,
|
||||||
|
bleInputProperty: Long,
|
||||||
|
) {
|
||||||
|
val gatt = deviceId.toBluetoothGatt()
|
||||||
|
val gattCharacteristic = gatt.getCharacteristic(service, characteristic)
|
||||||
|
?: throw FlutterError(
|
||||||
|
"IllegalArgument",
|
||||||
|
"Unknown characteristic: $characteristic",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
gatt.setNotifiable(gattCharacteristic, bleInputProperty)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun readValue(
|
||||||
|
deviceId: String,
|
||||||
|
service: String,
|
||||||
|
characteristic: String,
|
||||||
|
callback: (Result<ByteArray>) -> Unit,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
val gatt = deviceId.toBluetoothGatt()
|
||||||
|
val gattCharacteristic = gatt.getCharacteristic(service, characteristic)
|
||||||
|
if (gattCharacteristic == null) {
|
||||||
|
callback(
|
||||||
|
Result.failure(FlutterError("IllegalArgument", "Unknown characteristic", null))
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!gatt.readCharacteristic(gattCharacteristic)) {
|
||||||
|
callback(
|
||||||
|
Result.failure(unknownCharacteristicError(characteristic))
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bleCharacteristicFutureList.add(
|
||||||
|
BleCharacteristicFuture(
|
||||||
|
gatt.device.address,
|
||||||
|
gattCharacteristic.uuid.toString(),
|
||||||
|
gattCharacteristic.service.uuid.toString(),
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: FlutterError) {
|
||||||
|
callback(Result.failure(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCharacteristicRead(
|
||||||
|
gatt: BluetoothGatt,
|
||||||
|
characteristic: BluetoothGattCharacteristic,
|
||||||
|
value: ByteArray,
|
||||||
|
status: Int,
|
||||||
|
) {
|
||||||
|
bleCharacteristicFutureList.filter {
|
||||||
|
it.deviceId == gatt.device.address &&
|
||||||
|
it.characteristicId == characteristic.uuid.toString() &&
|
||||||
|
it.serviceId == characteristic.service.uuid.toString()
|
||||||
|
}.forEach {
|
||||||
|
bleCharacteristicFutureList.remove(it)
|
||||||
|
it.result(Result.success(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCharacteristicRead(
|
||||||
|
gatt: BluetoothGatt,
|
||||||
|
characteristic: BluetoothGattCharacteristic,
|
||||||
|
status: Int,
|
||||||
|
) {
|
||||||
|
// Pass data to new api
|
||||||
|
onCharacteristicRead(gatt, characteristic, characteristic.value, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun writeValue(
|
||||||
|
deviceId: String,
|
||||||
|
service: String,
|
||||||
|
characteristic: String,
|
||||||
|
value: ByteArray,
|
||||||
|
bleOutputProperty: Long,
|
||||||
|
callback: (Result<Unit>) -> Unit,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
val gatt = deviceId.toBluetoothGatt()
|
||||||
|
val gattCharacteristic = gatt.getCharacteristic(service, characteristic)
|
||||||
|
if (gattCharacteristic == null) {
|
||||||
|
callback(Result.failure(unknownCharacteristicError(characteristic)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
||||||
|
if (bleOutputProperty == BleOutputProperty.withResponse.value) {
|
||||||
|
if (gattCharacteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE == 0) {
|
||||||
|
callback(
|
||||||
|
Result.failure(
|
||||||
|
FlutterError(
|
||||||
|
"IllegalArgument",
|
||||||
|
"Characteristic does not support write withResponse",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
||||||
|
} else if (bleOutputProperty == BleOutputProperty.withoutResponse.value) {
|
||||||
|
if (gattCharacteristic.properties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE == 0) {
|
||||||
|
callback(
|
||||||
|
Result.failure(
|
||||||
|
FlutterError(
|
||||||
|
"IllegalArgument",
|
||||||
|
"Characteristic does not support write withoutResponse",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
|
||||||
|
}
|
||||||
|
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
// val result = gatt.writeCharacteristic(gattCharacteristic, value, writeType)
|
||||||
|
// Log.v(TAG,"writeResult $characteristic: $result => ${result.parseBluetoothStatusCodeError()}")
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
gattCharacteristic.value = value
|
||||||
|
gattCharacteristic.writeType = writeType
|
||||||
|
val result = gatt.writeCharacteristic(gattCharacteristic)
|
||||||
|
if (!result) {
|
||||||
|
callback(Result.failure(FlutterError("Failed", "Failed to write", null)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (writeType == BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) {
|
||||||
|
// wait for the result
|
||||||
|
writeResultFutureList.add(
|
||||||
|
WriteResultFuture(
|
||||||
|
gatt.device.address,
|
||||||
|
gattCharacteristic.uuid.toString(),
|
||||||
|
gattCharacteristic.service.uuid.toString(),
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
callback(Result.success(Unit))
|
||||||
|
}
|
||||||
|
} catch (e: FlutterError) {
|
||||||
|
callback(Result.failure(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCharacteristicWrite(
|
||||||
|
gatt: BluetoothGatt?,
|
||||||
|
characteristic: BluetoothGattCharacteristic,
|
||||||
|
status: Int,
|
||||||
|
) {
|
||||||
|
writeResultFutureList.filter {
|
||||||
|
it.deviceId == gatt?.device?.address &&
|
||||||
|
it.characteristicId == characteristic.uuid.toString() &&
|
||||||
|
it.serviceId == characteristic.service.uuid.toString()
|
||||||
|
}.forEach {
|
||||||
|
writeResultFutureList.remove(it)
|
||||||
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
it.result(Result.success(Unit))
|
||||||
|
} else {
|
||||||
|
it.result(
|
||||||
|
Result.failure(
|
||||||
|
FlutterError(
|
||||||
|
"Failed",
|
||||||
|
"Failed to write ($status)",
|
||||||
|
"status $status"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun requestMtu(deviceId: String, expectedMtu: Long, callback: (Result<Long>) -> Unit) {
|
||||||
|
val gatt = deviceId.toBluetoothGatt()
|
||||||
|
gatt.requestMtu(expectedMtu.toInt())
|
||||||
|
mtuResultFutureList.add(MtuResultFuture(deviceId, callback))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
|
||||||
|
val deviceId = gatt?.device?.address ?: return
|
||||||
|
mtuResultFutureList.filter { it.deviceId == deviceId }.forEach {
|
||||||
|
mtuResultFutureList.remove(it)
|
||||||
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
it.result(Result.success(mtu.toLong()))
|
||||||
|
} else {
|
||||||
|
it.result(Result.failure(FlutterError("Failed to change MTU", null, null)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isPaired(deviceId: String, callback: (Result<Boolean>) -> Unit) {
|
||||||
|
val remoteDevice: BluetoothDevice =
|
||||||
|
bluetoothManager.adapter.getRemoteDevice(deviceId)
|
||||||
|
callback(Result.success(remoteDevice.bondState == BOND_BONDED))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pair(deviceId: String) {
|
||||||
|
val remoteDevice =
|
||||||
|
bluetoothManager.adapter.getRemoteDevice(deviceId)
|
||||||
|
if (remoteDevice.bondState == BOND_NONE) {
|
||||||
|
if (!remoteDevice.createBond()) {
|
||||||
|
throw FlutterError("Failed", "Failed to pair", null)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw FlutterError("AlreadyPair", "Already paired", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun unPair(deviceId: String) {
|
||||||
|
val remoteDevice: BluetoothDevice =
|
||||||
|
bluetoothManager.adapter.getRemoteDevice(deviceId)
|
||||||
|
if (remoteDevice.bondState == BOND_BONDED) {
|
||||||
|
remoteDevice.removeBond()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getConnectedDevices(
|
||||||
|
withServices: List<String>,
|
||||||
|
callback: (Result<List<UniversalBleScanResult>>) -> Unit,
|
||||||
|
) {
|
||||||
|
var devices: List<BluetoothDevice> =
|
||||||
|
bluetoothManager.getConnectedDevices(BluetoothProfile.GATT)
|
||||||
|
if (withServices.isNotEmpty()) {
|
||||||
|
devices = filterDevicesByServices(devices, withServices)
|
||||||
|
}
|
||||||
|
callback(
|
||||||
|
Result.success(
|
||||||
|
devices.map {
|
||||||
|
UniversalBleScanResult(
|
||||||
|
name = it.name,
|
||||||
|
deviceId = it.address,
|
||||||
|
isPaired = it.bondState == BOND_BONDED,
|
||||||
|
manufacturerDataHead = null,
|
||||||
|
manufacturerData = null,
|
||||||
|
rssi = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterDevicesByServices(
|
||||||
|
devices: List<BluetoothDevice>,
|
||||||
|
withServices: List<String>,
|
||||||
|
): List<BluetoothDevice> {
|
||||||
|
// If all devices have cached services
|
||||||
|
if (devices.all { cachedServicesMap[it.address] != null }) {
|
||||||
|
return devices.filter { device ->
|
||||||
|
cachedServicesMap[device.address]?.any { uuid -> withServices.contains(uuid) } == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else discover services off already connected devices
|
||||||
|
val latch = CountDownLatch(devices.size)
|
||||||
|
val resultMap = mutableMapOf<String, Boolean>()
|
||||||
|
|
||||||
|
devices.forEach { device ->
|
||||||
|
discoverServicesOffAlreadyConnectedDevice(device) { uuids ->
|
||||||
|
resultMap[device.address] =
|
||||||
|
uuids?.any { uuid -> withServices.contains(uuid) } == true
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val timeout = (devices.size * 2).toLong()
|
||||||
|
latch.await(timeout, TimeUnit.SECONDS)
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
Thread.currentThread().interrupt()
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices.filter { resultMap[it.address] == true }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun discoverServicesOffAlreadyConnectedDevice(
|
||||||
|
device: BluetoothDevice,
|
||||||
|
callback: (List<String>?) -> Unit,
|
||||||
|
) {
|
||||||
|
// Check if already cached
|
||||||
|
cachedServicesMap[device.address]?.let {
|
||||||
|
callback(it)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// To avoid duplicate callback
|
||||||
|
var isUpdated = false
|
||||||
|
fun updateCallback(uuids: List<String>?) {
|
||||||
|
if (isUpdated) return
|
||||||
|
callback(uuids)
|
||||||
|
isUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If its a known gatt, just discover services
|
||||||
|
knownGatts.find { it.device.address == device.address }?.let {
|
||||||
|
it.services?.let { services ->
|
||||||
|
updateCallback(services.map { service -> service.uuid.toString() })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it.discoverServices()) {
|
||||||
|
discoverServicesFutureList.add(
|
||||||
|
DiscoverServicesFuture(device.address) { uuids: Result<List<UniversalBleService>> ->
|
||||||
|
if (uuids.isSuccess) {
|
||||||
|
updateCallback(uuids.getOrNull()?.map { it.uuid })
|
||||||
|
} else {
|
||||||
|
updateCallback(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else connect to Gatt, discover services, then disconnect
|
||||||
|
val callbackHandler = object : BluetoothGattCallback() {
|
||||||
|
override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
|
||||||
|
if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothGatt.STATE_CONNECTED) {
|
||||||
|
if (gatt?.discoverServices() != true) {
|
||||||
|
updateCallback(null)
|
||||||
|
if (!knownGatts.any { it.device.address == device.address }) {
|
||||||
|
gatt?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateCallback(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
|
||||||
|
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
val uuids = gatt?.services?.map { it.uuid.toString() }
|
||||||
|
if (uuids != null) {
|
||||||
|
setCachedServices(device.address, uuids)
|
||||||
|
}
|
||||||
|
updateCallback(uuids)
|
||||||
|
}
|
||||||
|
if (!knownGatts.any { it.device.address == device.address }) {
|
||||||
|
gatt?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
device.connectGatt(context, false, callbackHandler, BluetoothDevice.TRANSPORT_LE)
|
||||||
|
} else {
|
||||||
|
device.connectGatt(context, false, callbackHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanConnection(gatt: BluetoothGatt) {
|
||||||
|
knownGatts.remove(gatt)
|
||||||
|
gatt.disconnect()
|
||||||
|
bleCharacteristicFutureList.removeAll {
|
||||||
|
it.deviceId == gatt.device.address
|
||||||
|
}
|
||||||
|
mtuResultFutureList.removeAll {
|
||||||
|
it.deviceId == gatt.device.address
|
||||||
|
}
|
||||||
|
discoverServicesFutureList.removeAll {
|
||||||
|
it.deviceId == gatt.device.address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val broadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
if (intent.action == BluetoothAdapter.ACTION_STATE_CHANGED) {
|
||||||
|
mainThreadHandler?.post {
|
||||||
|
callbackChannel?.onAvailabilityChanged(
|
||||||
|
bluetoothManager.adapter?.state?.toAvailabilityState()
|
||||||
|
?: AvailabilityState.Unknown.value
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
} else if (intent.action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) {
|
||||||
|
val device: BluetoothDevice =
|
||||||
|
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) ?: return
|
||||||
|
// get pairing failed error
|
||||||
|
when (intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR)) {
|
||||||
|
BluetoothDevice.BOND_BONDING -> {
|
||||||
|
Log.v(TAG, "${device.address} BOND_BONDING")
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothDevice.ERROR -> {
|
||||||
|
mainThreadHandler?.post {
|
||||||
|
callbackChannel?.onPairStateChange(
|
||||||
|
device.address,
|
||||||
|
false,
|
||||||
|
"No pairing state received"
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BOND_BONDED -> {
|
||||||
|
mainThreadHandler?.post {
|
||||||
|
callbackChannel?.onPairStateChange(
|
||||||
|
device.address,
|
||||||
|
true,
|
||||||
|
null
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
BOND_NONE -> {
|
||||||
|
mainThreadHandler?.post {
|
||||||
|
callbackChannel?.onPairStateChange(
|
||||||
|
device.address,
|
||||||
|
false,
|
||||||
|
null
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val scanCallback = object : ScanCallback() {
|
||||||
|
override fun onScanFailed(errorCode: Int) {
|
||||||
|
Log.e(TAG, "OnScanFailed: ${errorCode.parseScanErrorMessage()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||||
|
// Log.v(TAG, "onScanResult: $result")
|
||||||
|
mainThreadHandler?.post {
|
||||||
|
callbackChannel?.onScanResult(
|
||||||
|
UniversalBleScanResult(
|
||||||
|
name = result.device.name,
|
||||||
|
deviceId = result.device.address,
|
||||||
|
isPaired = result.device.bondState == BOND_BONDED,
|
||||||
|
manufacturerDataHead = result.manufacturerDataHead,
|
||||||
|
rssi = result.rssi.toLong()
|
||||||
|
)
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBatchScanResults(results: MutableList<ScanResult>?) {
|
||||||
|
Log.v(TAG, "onBatchScanResults: $results")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
|
||||||
|
devicesStateMap[gatt.device.address] = newState
|
||||||
|
if (newState == BluetoothGatt.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) {
|
||||||
|
mainThreadHandler?.post {
|
||||||
|
callbackChannel?.onConnectionChanged(
|
||||||
|
gatt.device.address,
|
||||||
|
BleConnectionState.Connected.value
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cleanConnection(gatt)
|
||||||
|
mainThreadHandler?.post {
|
||||||
|
callbackChannel?.onConnectionChanged(
|
||||||
|
gatt.device.address,
|
||||||
|
BleConnectionState.Disconnected.value
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCharacteristicChanged(
|
||||||
|
gatt: BluetoothGatt,
|
||||||
|
characteristic: BluetoothGattCharacteristic,
|
||||||
|
) {
|
||||||
|
mainThreadHandler?.post {
|
||||||
|
callbackChannel?.onValueChanged(
|
||||||
|
deviceIdArg = gatt.device.address,
|
||||||
|
characteristicIdArg = characteristic.uuid.toString(),
|
||||||
|
valueArg = characteristic.value
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isBluetoothAvailable(): Boolean {
|
||||||
|
return bluetoothManager.adapter.isEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setCachedServices(deviceId: String, services: List<String>) {
|
||||||
|
val cachedServicesSharedPref = context.getSharedPreferences(
|
||||||
|
"com.navideck.universal_ble.services",
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
cachedServicesSharedPref.edit().putStringSet(deviceId, services.toSet()).apply()
|
||||||
|
cachedServicesMap[deviceId] = services
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCachedServicesMap(): Map<String, List<String>> {
|
||||||
|
val cachedServicesSharedPref = context.getSharedPreferences(
|
||||||
|
"com.navideck.universal_ble.services",
|
||||||
|
Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
return cachedServicesSharedPref.all.mapValues { (_, value) ->
|
||||||
|
(value as? Set<*>)?.map { it.toString() } ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||||
|
if (requestCode == bluetoothEnableRequestCode) {
|
||||||
|
val future = bluetoothEnableRequestFuture ?: return false
|
||||||
|
future(Result.success(resultCode == Activity.RESULT_OK))
|
||||||
|
bluetoothEnableRequestFuture = null
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||||
|
activity = binding.activity
|
||||||
|
binding.addActivityResultListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromActivity() {
|
||||||
|
activity = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromActivityForConfigChanges() {}
|
||||||
|
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.navideck.universal_ble
|
||||||
|
|
||||||
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import kotlin.test.Test
|
||||||
|
import org.mockito.Mockito
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation.
|
||||||
|
*
|
||||||
|
* Once you have built the plugin's example app, you can run these tests from the command
|
||||||
|
* line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or
|
||||||
|
* you can run them directly from IDEs that support JUnit such as Android Studio.
|
||||||
|
*/
|
||||||
|
|
||||||
|
internal class UniversalBlePluginTest {
|
||||||
|
@Test
|
||||||
|
fun onMethodCall_getPlatformVersion_returnsExpectedValue() {
|
||||||
|
val plugin = UniversalBlePlugin()
|
||||||
|
|
||||||
|
val call = MethodCall("getPlatformVersion", null)
|
||||||
|
val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java)
|
||||||
|
plugin.onMethodCall(call, mockResult)
|
||||||
|
|
||||||
|
Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
.idea/
|
||||||
|
.vagrant/
|
||||||
|
.sconsign.dblite
|
||||||
|
.svn/
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
profile
|
||||||
|
|
||||||
|
DerivedData/
|
||||||
|
build/
|
||||||
|
GeneratedPluginRegistrant.h
|
||||||
|
GeneratedPluginRegistrant.m
|
||||||
|
|
||||||
|
.generated/
|
||||||
|
|
||||||
|
*.pbxuser
|
||||||
|
*.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
*.perspectivev3
|
||||||
|
|
||||||
|
!default.pbxuser
|
||||||
|
!default.mode1v3
|
||||||
|
!default.mode2v3
|
||||||
|
!default.perspectivev3
|
||||||
|
|
||||||
|
xcuserdata
|
||||||
|
|
||||||
|
*.moved-aside
|
||||||
|
|
||||||
|
*.pyc
|
||||||
|
*sync/
|
||||||
|
Icon?
|
||||||
|
.tags*
|
||||||
|
|
||||||
|
/Flutter/Generated.xcconfig
|
||||||
|
/Flutter/ephemeral/
|
||||||
|
/Flutter/flutter_export_environment.sh
|
||||||
@@ -0,0 +1,525 @@
|
|||||||
|
// Autogenerated from Pigeon (v13.0.0), do not edit directly.
|
||||||
|
// See also: https://pub.dev/packages/pigeon
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
#if os(iOS)
|
||||||
|
import Flutter
|
||||||
|
#elseif os(macOS)
|
||||||
|
import FlutterMacOS
|
||||||
|
#else
|
||||||
|
#error("Unsupported platform.")
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private func isNullish(_ value: Any?) -> Bool {
|
||||||
|
return value is NSNull || value == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func wrapResult(_ result: Any?) -> [Any?] {
|
||||||
|
return [result]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func wrapError(_ error: Any) -> [Any?] {
|
||||||
|
if let flutterError = error as? FlutterError {
|
||||||
|
return [
|
||||||
|
flutterError.code,
|
||||||
|
flutterError.message,
|
||||||
|
flutterError.details
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
"\(error)",
|
||||||
|
"\(type(of: error))",
|
||||||
|
"Stacktrace: \(Thread.callStackSymbols)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func nilOrValue<T>(_ value: Any?) -> T? {
|
||||||
|
if value is NSNull { return nil }
|
||||||
|
return value as! T?
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generated class from Pigeon that represents data sent in messages.
|
||||||
|
struct UniversalBleScanResult {
|
||||||
|
var deviceId: String
|
||||||
|
var name: String? = nil
|
||||||
|
var isPaired: Bool? = nil
|
||||||
|
var rssi: Int64? = nil
|
||||||
|
var manufacturerData: FlutterStandardTypedData? = nil
|
||||||
|
var manufacturerDataHead: FlutterStandardTypedData? = nil
|
||||||
|
|
||||||
|
static func fromList(_ list: [Any?]) -> UniversalBleScanResult? {
|
||||||
|
let deviceId = list[0] as! String
|
||||||
|
let name: String? = nilOrValue(list[1])
|
||||||
|
let isPaired: Bool? = nilOrValue(list[2])
|
||||||
|
let rssi: Int64? = isNullish(list[3]) ? nil : (list[3] is Int64? ? list[3] as! Int64? : Int64(list[3] as! Int32))
|
||||||
|
let manufacturerData: FlutterStandardTypedData? = nilOrValue(list[4])
|
||||||
|
let manufacturerDataHead: FlutterStandardTypedData? = nilOrValue(list[5])
|
||||||
|
|
||||||
|
return UniversalBleScanResult(
|
||||||
|
deviceId: deviceId,
|
||||||
|
name: name,
|
||||||
|
isPaired: isPaired,
|
||||||
|
rssi: rssi,
|
||||||
|
manufacturerData: manufacturerData,
|
||||||
|
manufacturerDataHead: manufacturerDataHead
|
||||||
|
)
|
||||||
|
}
|
||||||
|
func toList() -> [Any?] {
|
||||||
|
return [
|
||||||
|
deviceId,
|
||||||
|
name,
|
||||||
|
isPaired,
|
||||||
|
rssi,
|
||||||
|
manufacturerData,
|
||||||
|
manufacturerDataHead,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generated class from Pigeon that represents data sent in messages.
|
||||||
|
struct UniversalBleService {
|
||||||
|
var uuid: String
|
||||||
|
var characteristics: [UniversalBleCharacteristic?]? = nil
|
||||||
|
|
||||||
|
static func fromList(_ list: [Any?]) -> UniversalBleService? {
|
||||||
|
let uuid = list[0] as! String
|
||||||
|
let characteristics: [UniversalBleCharacteristic?]? = nilOrValue(list[1])
|
||||||
|
|
||||||
|
return UniversalBleService(
|
||||||
|
uuid: uuid,
|
||||||
|
characteristics: characteristics
|
||||||
|
)
|
||||||
|
}
|
||||||
|
func toList() -> [Any?] {
|
||||||
|
return [
|
||||||
|
uuid,
|
||||||
|
characteristics,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generated class from Pigeon that represents data sent in messages.
|
||||||
|
struct UniversalBleCharacteristic {
|
||||||
|
var uuid: String
|
||||||
|
var properties: [Int64?]
|
||||||
|
|
||||||
|
static func fromList(_ list: [Any?]) -> UniversalBleCharacteristic? {
|
||||||
|
let uuid = list[0] as! String
|
||||||
|
let properties = list[1] as! [Int64?]
|
||||||
|
|
||||||
|
return UniversalBleCharacteristic(
|
||||||
|
uuid: uuid,
|
||||||
|
properties: properties
|
||||||
|
)
|
||||||
|
}
|
||||||
|
func toList() -> [Any?] {
|
||||||
|
return [
|
||||||
|
uuid,
|
||||||
|
properties,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class UniversalBlePlatformChannelCodecReader: FlutterStandardReader {
|
||||||
|
override func readValue(ofType type: UInt8) -> Any? {
|
||||||
|
switch type {
|
||||||
|
case 128:
|
||||||
|
return UniversalBleCharacteristic.fromList(self.readValue() as! [Any?])
|
||||||
|
case 129:
|
||||||
|
return UniversalBleScanResult.fromList(self.readValue() as! [Any?])
|
||||||
|
case 130:
|
||||||
|
return UniversalBleService.fromList(self.readValue() as! [Any?])
|
||||||
|
default:
|
||||||
|
return super.readValue(ofType: type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class UniversalBlePlatformChannelCodecWriter: FlutterStandardWriter {
|
||||||
|
override func writeValue(_ value: Any) {
|
||||||
|
if let value = value as? UniversalBleCharacteristic {
|
||||||
|
super.writeByte(128)
|
||||||
|
super.writeValue(value.toList())
|
||||||
|
} else if let value = value as? UniversalBleScanResult {
|
||||||
|
super.writeByte(129)
|
||||||
|
super.writeValue(value.toList())
|
||||||
|
} else if let value = value as? UniversalBleService {
|
||||||
|
super.writeByte(130)
|
||||||
|
super.writeValue(value.toList())
|
||||||
|
} else {
|
||||||
|
super.writeValue(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class UniversalBlePlatformChannelCodecReaderWriter: FlutterStandardReaderWriter {
|
||||||
|
override func reader(with data: Data) -> FlutterStandardReader {
|
||||||
|
return UniversalBlePlatformChannelCodecReader(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
|
||||||
|
return UniversalBlePlatformChannelCodecWriter(data: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UniversalBlePlatformChannelCodec: FlutterStandardMessageCodec {
|
||||||
|
static let shared = UniversalBlePlatformChannelCodec(readerWriter: UniversalBlePlatformChannelCodecReaderWriter())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flutter -> Native
|
||||||
|
///
|
||||||
|
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||||
|
protocol UniversalBlePlatformChannel {
|
||||||
|
func getBluetoothAvailabilityState(completion: @escaping (Result<Int64, Error>) -> Void)
|
||||||
|
func enableBluetooth(completion: @escaping (Result<Bool, Error>) -> Void)
|
||||||
|
func startScan() throws
|
||||||
|
func stopScan() throws
|
||||||
|
func connect(deviceId: String) throws
|
||||||
|
func disconnect(deviceId: String) throws
|
||||||
|
func setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: Int64) throws
|
||||||
|
func discoverServices(deviceId: String, completion: @escaping (Result<[UniversalBleService], Error>) -> Void)
|
||||||
|
func readValue(deviceId: String, service: String, characteristic: String, completion: @escaping (Result<FlutterStandardTypedData, Error>) -> Void)
|
||||||
|
func requestMtu(deviceId: String, expectedMtu: Int64, completion: @escaping (Result<Int64, Error>) -> Void)
|
||||||
|
func writeValue(deviceId: String, service: String, characteristic: String, value: FlutterStandardTypedData, bleOutputProperty: Int64, completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
func isPaired(deviceId: String, completion: @escaping (Result<Bool, Error>) -> Void)
|
||||||
|
func pair(deviceId: String) throws
|
||||||
|
func unPair(deviceId: String) throws
|
||||||
|
func getConnectedDevices(withServices: [String], completion: @escaping (Result<[UniversalBleScanResult], Error>) -> Void)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
|
class UniversalBlePlatformChannelSetup {
|
||||||
|
/// The codec used by UniversalBlePlatformChannel.
|
||||||
|
static var codec: FlutterStandardMessageCodec { UniversalBlePlatformChannelCodec.shared }
|
||||||
|
/// Sets up an instance of `UniversalBlePlatformChannel` to handle messages through the `binaryMessenger`.
|
||||||
|
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: UniversalBlePlatformChannel?) {
|
||||||
|
let getBluetoothAvailabilityStateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getBluetoothAvailabilityState", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
getBluetoothAvailabilityStateChannel.setMessageHandler { _, reply in
|
||||||
|
api.getBluetoothAvailabilityState() { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let res):
|
||||||
|
reply(wrapResult(res))
|
||||||
|
case .failure(let error):
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getBluetoothAvailabilityStateChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let enableBluetoothChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.enableBluetooth", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
enableBluetoothChannel.setMessageHandler { _, reply in
|
||||||
|
api.enableBluetooth() { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let res):
|
||||||
|
reply(wrapResult(res))
|
||||||
|
case .failure(let error):
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
enableBluetoothChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let startScanChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.startScan", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
startScanChannel.setMessageHandler { _, reply in
|
||||||
|
do {
|
||||||
|
try api.startScan()
|
||||||
|
reply(wrapResult(nil))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startScanChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let stopScanChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.stopScan", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
stopScanChannel.setMessageHandler { _, reply in
|
||||||
|
do {
|
||||||
|
try api.stopScan()
|
||||||
|
reply(wrapResult(nil))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stopScanChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let connectChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.connect", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
connectChannel.setMessageHandler { message, reply in
|
||||||
|
let args = message as! [Any?]
|
||||||
|
let deviceIdArg = args[0] as! String
|
||||||
|
do {
|
||||||
|
try api.connect(deviceId: deviceIdArg)
|
||||||
|
reply(wrapResult(nil))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
connectChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let disconnectChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.disconnect", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
disconnectChannel.setMessageHandler { message, reply in
|
||||||
|
let args = message as! [Any?]
|
||||||
|
let deviceIdArg = args[0] as! String
|
||||||
|
do {
|
||||||
|
try api.disconnect(deviceId: deviceIdArg)
|
||||||
|
reply(wrapResult(nil))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
disconnectChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let setNotifiableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.setNotifiable", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
setNotifiableChannel.setMessageHandler { message, reply in
|
||||||
|
let args = message as! [Any?]
|
||||||
|
let deviceIdArg = args[0] as! String
|
||||||
|
let serviceArg = args[1] as! String
|
||||||
|
let characteristicArg = args[2] as! String
|
||||||
|
let bleInputPropertyArg = args[3] is Int64 ? args[3] as! Int64 : Int64(args[3] as! Int32)
|
||||||
|
do {
|
||||||
|
try api.setNotifiable(deviceId: deviceIdArg, service: serviceArg, characteristic: characteristicArg, bleInputProperty: bleInputPropertyArg)
|
||||||
|
reply(wrapResult(nil))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setNotifiableChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let discoverServicesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.discoverServices", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
discoverServicesChannel.setMessageHandler { message, reply in
|
||||||
|
let args = message as! [Any?]
|
||||||
|
let deviceIdArg = args[0] as! String
|
||||||
|
api.discoverServices(deviceId: deviceIdArg) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let res):
|
||||||
|
reply(wrapResult(res))
|
||||||
|
case .failure(let error):
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
discoverServicesChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let readValueChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.readValue", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
readValueChannel.setMessageHandler { message, reply in
|
||||||
|
let args = message as! [Any?]
|
||||||
|
let deviceIdArg = args[0] as! String
|
||||||
|
let serviceArg = args[1] as! String
|
||||||
|
let characteristicArg = args[2] as! String
|
||||||
|
api.readValue(deviceId: deviceIdArg, service: serviceArg, characteristic: characteristicArg) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let res):
|
||||||
|
reply(wrapResult(res))
|
||||||
|
case .failure(let error):
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
readValueChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let requestMtuChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.requestMtu", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
requestMtuChannel.setMessageHandler { message, reply in
|
||||||
|
let args = message as! [Any?]
|
||||||
|
let deviceIdArg = args[0] as! String
|
||||||
|
let expectedMtuArg = args[1] is Int64 ? args[1] as! Int64 : Int64(args[1] as! Int32)
|
||||||
|
api.requestMtu(deviceId: deviceIdArg, expectedMtu: expectedMtuArg) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let res):
|
||||||
|
reply(wrapResult(res))
|
||||||
|
case .failure(let error):
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
requestMtuChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let writeValueChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.writeValue", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
writeValueChannel.setMessageHandler { message, reply in
|
||||||
|
let args = message as! [Any?]
|
||||||
|
let deviceIdArg = args[0] as! String
|
||||||
|
let serviceArg = args[1] as! String
|
||||||
|
let characteristicArg = args[2] as! String
|
||||||
|
let valueArg = args[3] as! FlutterStandardTypedData
|
||||||
|
let bleOutputPropertyArg = args[4] is Int64 ? args[4] as! Int64 : Int64(args[4] as! Int32)
|
||||||
|
api.writeValue(deviceId: deviceIdArg, service: serviceArg, characteristic: characteristicArg, value: valueArg, bleOutputProperty: bleOutputPropertyArg) { result in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
reply(wrapResult(nil))
|
||||||
|
case .failure(let error):
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
writeValueChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let isPairedChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.isPaired", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
isPairedChannel.setMessageHandler { message, reply in
|
||||||
|
let args = message as! [Any?]
|
||||||
|
let deviceIdArg = args[0] as! String
|
||||||
|
api.isPaired(deviceId: deviceIdArg) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let res):
|
||||||
|
reply(wrapResult(res))
|
||||||
|
case .failure(let error):
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isPairedChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let pairChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.pair", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
pairChannel.setMessageHandler { message, reply in
|
||||||
|
let args = message as! [Any?]
|
||||||
|
let deviceIdArg = args[0] as! String
|
||||||
|
do {
|
||||||
|
try api.pair(deviceId: deviceIdArg)
|
||||||
|
reply(wrapResult(nil))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pairChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let unPairChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.unPair", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
unPairChannel.setMessageHandler { message, reply in
|
||||||
|
let args = message as! [Any?]
|
||||||
|
let deviceIdArg = args[0] as! String
|
||||||
|
do {
|
||||||
|
try api.unPair(deviceId: deviceIdArg)
|
||||||
|
reply(wrapResult(nil))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unPairChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let getConnectedDevicesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBlePlatformChannel.getConnectedDevices", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
getConnectedDevicesChannel.setMessageHandler { message, reply in
|
||||||
|
let args = message as! [Any?]
|
||||||
|
let withServicesArg = args[0] as! [String]
|
||||||
|
api.getConnectedDevices(withServices: withServicesArg) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let res):
|
||||||
|
reply(wrapResult(res))
|
||||||
|
case .failure(let error):
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getConnectedDevicesChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private class UniversalBleCallbackChannelCodecReader: FlutterStandardReader {
|
||||||
|
override func readValue(ofType type: UInt8) -> Any? {
|
||||||
|
switch type {
|
||||||
|
case 128:
|
||||||
|
return UniversalBleScanResult.fromList(self.readValue() as! [Any?])
|
||||||
|
default:
|
||||||
|
return super.readValue(ofType: type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class UniversalBleCallbackChannelCodecWriter: FlutterStandardWriter {
|
||||||
|
override func writeValue(_ value: Any) {
|
||||||
|
if let value = value as? UniversalBleScanResult {
|
||||||
|
super.writeByte(128)
|
||||||
|
super.writeValue(value.toList())
|
||||||
|
} else {
|
||||||
|
super.writeValue(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class UniversalBleCallbackChannelCodecReaderWriter: FlutterStandardReaderWriter {
|
||||||
|
override func reader(with data: Data) -> FlutterStandardReader {
|
||||||
|
return UniversalBleCallbackChannelCodecReader(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
|
||||||
|
return UniversalBleCallbackChannelCodecWriter(data: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UniversalBleCallbackChannelCodec: FlutterStandardMessageCodec {
|
||||||
|
static let shared = UniversalBleCallbackChannelCodec(readerWriter: UniversalBleCallbackChannelCodecReaderWriter())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Native -> Flutter
|
||||||
|
///
|
||||||
|
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
|
||||||
|
protocol UniversalBleCallbackChannelProtocol {
|
||||||
|
func onAvailabilityChanged(state stateArg: Int64, completion: @escaping (Result<Void, FlutterError>) -> Void)
|
||||||
|
func onPairStateChange(deviceId deviceIdArg: String, isPaired isPairedArg: Bool, error errorArg: String?, completion: @escaping (Result<Void, FlutterError>) -> Void)
|
||||||
|
func onScanResult(result resultArg: UniversalBleScanResult, completion: @escaping (Result<Void, FlutterError>) -> Void)
|
||||||
|
func onValueChanged(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, value valueArg: FlutterStandardTypedData, completion: @escaping (Result<Void, FlutterError>) -> Void)
|
||||||
|
func onConnectionChanged(deviceId deviceIdArg: String, state stateArg: Int64, completion: @escaping (Result<Void, FlutterError>) -> Void)
|
||||||
|
}
|
||||||
|
class UniversalBleCallbackChannel: UniversalBleCallbackChannelProtocol {
|
||||||
|
private let binaryMessenger: FlutterBinaryMessenger
|
||||||
|
init(binaryMessenger: FlutterBinaryMessenger){
|
||||||
|
self.binaryMessenger = binaryMessenger
|
||||||
|
}
|
||||||
|
var codec: FlutterStandardMessageCodec {
|
||||||
|
return UniversalBleCallbackChannelCodec.shared
|
||||||
|
}
|
||||||
|
func onAvailabilityChanged(state stateArg: Int64, completion: @escaping (Result<Void, FlutterError>) -> Void) {
|
||||||
|
let channel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onAvailabilityChanged", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
channel.sendMessage([stateArg] as [Any?]) { _ in
|
||||||
|
completion(.success(Void()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func onPairStateChange(deviceId deviceIdArg: String, isPaired isPairedArg: Bool, error errorArg: String?, completion: @escaping (Result<Void, FlutterError>) -> Void) {
|
||||||
|
let channel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onPairStateChange", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
channel.sendMessage([deviceIdArg, isPairedArg, errorArg] as [Any?]) { _ in
|
||||||
|
completion(.success(Void()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func onScanResult(result resultArg: UniversalBleScanResult, completion: @escaping (Result<Void, FlutterError>) -> Void) {
|
||||||
|
let channel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onScanResult", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
channel.sendMessage([resultArg] as [Any?]) { _ in
|
||||||
|
completion(.success(Void()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func onValueChanged(deviceId deviceIdArg: String, characteristicId characteristicIdArg: String, value valueArg: FlutterStandardTypedData, completion: @escaping (Result<Void, FlutterError>) -> Void) {
|
||||||
|
let channel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onValueChanged", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
channel.sendMessage([deviceIdArg, characteristicIdArg, valueArg] as [Any?]) { _ in
|
||||||
|
completion(.success(Void()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func onConnectionChanged(deviceId deviceIdArg: String, state stateArg: Int64, completion: @escaping (Result<Void, FlutterError>) -> Void) {
|
||||||
|
let channel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.universal_ble.UniversalBleCallbackChannel.onConnectionChanged", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
channel.sendMessage([deviceIdArg, stateArg] as [Any?]) { _ in
|
||||||
|
completion(.success(Void()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
//
|
||||||
|
// UniversalBleHelper.swift
|
||||||
|
// universal_ble
|
||||||
|
//
|
||||||
|
// Created by Rohit Sangwan on 25/10/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreBluetooth
|
||||||
|
import Foundation
|
||||||
|
#if os(iOS)
|
||||||
|
import Flutter
|
||||||
|
#elseif os(OSX)
|
||||||
|
import FlutterMacOS
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum BleInputProperty: Int {
|
||||||
|
case disabled = 0
|
||||||
|
case notification = 1
|
||||||
|
case indication = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BleOutputProperty: Int {
|
||||||
|
case withResponse = 0
|
||||||
|
case withoutResponse = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BlueConnectionState: Int64 {
|
||||||
|
case connected = 0
|
||||||
|
case disconnected = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AvailabilityState: Int64 {
|
||||||
|
case unknown = 0
|
||||||
|
case resetting = 1
|
||||||
|
case unsupported = 2
|
||||||
|
case unauthorized = 3
|
||||||
|
case poweredOff = 4
|
||||||
|
case poweredOn = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CharacteristicProperty: Int64 {
|
||||||
|
case broadcast = 0
|
||||||
|
case read = 1
|
||||||
|
case writeWithoutResponse = 2
|
||||||
|
case write = 3
|
||||||
|
case notify = 4
|
||||||
|
case indicate = 5
|
||||||
|
case authenticatedSignedWrites = 6
|
||||||
|
case extendedProperties = 7
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CBCharacteristicProperties {
|
||||||
|
var toCharacteristicProperty: [Int64] {
|
||||||
|
var properties = [Int64]()
|
||||||
|
if contains(.broadcast) {
|
||||||
|
properties.append(CharacteristicProperty.broadcast.rawValue)
|
||||||
|
}
|
||||||
|
if contains(.read) {
|
||||||
|
properties.append(CharacteristicProperty.read.rawValue)
|
||||||
|
}
|
||||||
|
if contains(.writeWithoutResponse) {
|
||||||
|
properties.append(CharacteristicProperty.writeWithoutResponse.rawValue)
|
||||||
|
}
|
||||||
|
if contains(.write) {
|
||||||
|
properties.append(CharacteristicProperty.write.rawValue)
|
||||||
|
}
|
||||||
|
if contains(.notify) {
|
||||||
|
properties.append(CharacteristicProperty.notify.rawValue)
|
||||||
|
}
|
||||||
|
if contains(.indicate) {
|
||||||
|
properties.append(CharacteristicProperty.indicate.rawValue)
|
||||||
|
}
|
||||||
|
if contains(.authenticatedSignedWrites) {
|
||||||
|
properties.append(CharacteristicProperty.authenticatedSignedWrites.rawValue)
|
||||||
|
}
|
||||||
|
if contains(.extendedProperties) {
|
||||||
|
properties.append(CharacteristicProperty.extendedProperties.rawValue)
|
||||||
|
}
|
||||||
|
return properties
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CBManagerState {
|
||||||
|
func toAvailabilityState() -> AvailabilityState {
|
||||||
|
switch self {
|
||||||
|
case .unknown:
|
||||||
|
return AvailabilityState.unknown
|
||||||
|
case .resetting:
|
||||||
|
return AvailabilityState.resetting
|
||||||
|
case .unsupported:
|
||||||
|
return AvailabilityState.unsupported
|
||||||
|
case .unauthorized:
|
||||||
|
return AvailabilityState.unauthorized
|
||||||
|
case .poweredOff:
|
||||||
|
return AvailabilityState.poweredOff
|
||||||
|
case .poweredOn:
|
||||||
|
return AvailabilityState.poweredOn
|
||||||
|
@unknown default:
|
||||||
|
return AvailabilityState.unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension CBUUID {
|
||||||
|
var uuidStr: String {
|
||||||
|
uuidString.lowercased()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension CBPeripheral {
|
||||||
|
// FIXME: https://forums.developer.apple.com/thread/84375
|
||||||
|
var uuid: UUID {
|
||||||
|
value(forKey: "identifier") as! NSUUID as UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCharacteristic(_ characteristic: String, of service: String) -> CBCharacteristic? {
|
||||||
|
let GSS_SUFFIX = "0000-1000-8000-00805f9b34fb"
|
||||||
|
let s = services?.first {
|
||||||
|
$0.uuid.uuidStr.lowercased() == service.lowercased() || service.lowercased() == "0000\($0.uuid.uuidStr)-\(GSS_SUFFIX)".lowercased()
|
||||||
|
}
|
||||||
|
let c = s?.characteristics?.first {
|
||||||
|
$0.uuid.uuidStr.lowercased() == characteristic.lowercased() || characteristic.lowercased() == "0000\($0.uuid.uuidStr)-\(GSS_SUFFIX)".lowercased()
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func setNotifiable(_ bleInputProperty: String, for characteristic: String, of service: String) {
|
||||||
|
guard let characteristic = getCharacteristic(characteristic, of: service) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setNotifyValue(bleInputProperty != "disabled", for: characteristic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Future classes
|
||||||
|
class CharacteristicReadFuture {
|
||||||
|
let deviceId: String
|
||||||
|
let characteristicId: String
|
||||||
|
let serviceId: String?
|
||||||
|
let result: (Result<FlutterStandardTypedData, Error>) -> Void
|
||||||
|
|
||||||
|
init(deviceId: String, characteristicId: String, serviceId: String?, result: @escaping (Result<FlutterStandardTypedData, Error>) -> Void) {
|
||||||
|
self.deviceId = deviceId
|
||||||
|
self.characteristicId = characteristicId
|
||||||
|
self.serviceId = serviceId
|
||||||
|
self.result = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CharacteristicWriteFuture {
|
||||||
|
let deviceId: String
|
||||||
|
let characteristicId: String
|
||||||
|
let serviceId: String?
|
||||||
|
let result: (Result<Void, Error>) -> Void
|
||||||
|
|
||||||
|
init(deviceId: String, characteristicId: String, serviceId: String?, result: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
self.deviceId = deviceId
|
||||||
|
self.characteristicId = characteristicId
|
||||||
|
self.serviceId = serviceId
|
||||||
|
self.result = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiscoverServicesFuture {
|
||||||
|
let deviceId: String
|
||||||
|
let result: (Result<[UniversalBleService], Error>) -> Void
|
||||||
|
|
||||||
|
init(deviceId: String, result: @escaping (Result<[UniversalBleService], Error>) -> Void) {
|
||||||
|
self.deviceId = deviceId
|
||||||
|
self.result = result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
import CoreBluetooth
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
#elseif os(OSX)
|
||||||
|
import Cocoa
|
||||||
|
import FlutterMacOS
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public class UniversalBlePlugin: NSObject, FlutterPlugin {
|
||||||
|
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||||
|
var messenger: FlutterBinaryMessenger
|
||||||
|
#if os(iOS)
|
||||||
|
messenger = registrar.messenger()
|
||||||
|
#elseif os(macOS)
|
||||||
|
messenger = registrar.messenger
|
||||||
|
#endif
|
||||||
|
let callbackChannel = UniversalBleCallbackChannel(binaryMessenger: messenger)
|
||||||
|
let api = BleCentralDarwin(callbackChannel: callbackChannel)
|
||||||
|
UniversalBlePlatformChannelSetup.setUp(binaryMessenger: messenger, api: api)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var discoveredPeripherals = [String: CBPeripheral]()
|
||||||
|
|
||||||
|
private class BleCentralDarwin: NSObject, UniversalBlePlatformChannel, CBCentralManagerDelegate, CBPeripheralDelegate {
|
||||||
|
var callbackChannel: UniversalBleCallbackChannel
|
||||||
|
private lazy var manager: CBCentralManager = .init(delegate: self, queue: nil)
|
||||||
|
private var discoveredServicesProgressMap: [String: [UniversalBleService]] = [:]
|
||||||
|
private var characteristicReadFutures = [CharacteristicReadFuture]()
|
||||||
|
private var characteristicWriteFutures = [CharacteristicWriteFuture]()
|
||||||
|
private var discoverServicesFutures = [DiscoverServicesFuture]()
|
||||||
|
private var bluetoothAvailabilityStateCallback: ((Result<Int64, Error>) -> Void)? {
|
||||||
|
didSet {
|
||||||
|
oldValue?(Result.success(AvailabilityState.unknown.rawValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(callbackChannel: UniversalBleCallbackChannel) {
|
||||||
|
self.callbackChannel = callbackChannel
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBluetoothAvailabilityState(completion: @escaping (Result<Int64, Error>) -> Void) {
|
||||||
|
let managerState = manager.state
|
||||||
|
|
||||||
|
if managerState == .unknown {
|
||||||
|
bluetoothAvailabilityStateCallback = completion
|
||||||
|
} else {
|
||||||
|
completion(.success(managerState.toAvailabilityState().rawValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func enableBluetooth(completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||||
|
completion(Result.failure(FlutterError(code: "NotSupported", message: nil, details: nil)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func startScan() throws {
|
||||||
|
manager.scanForPeripherals(withServices: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopScan() throws {
|
||||||
|
manager.stopScan()
|
||||||
|
}
|
||||||
|
|
||||||
|
func connect(deviceId: String) throws {
|
||||||
|
let peripheral = try deviceId.getPeripheral()
|
||||||
|
peripheral.delegate = self
|
||||||
|
manager.connect(peripheral)
|
||||||
|
}
|
||||||
|
|
||||||
|
func disconnect(deviceId: String) throws {
|
||||||
|
let peripheral = try deviceId.getPeripheral()
|
||||||
|
if peripheral.state != CBPeripheralState.disconnected {
|
||||||
|
manager.cancelPeripheralConnection(peripheral)
|
||||||
|
}
|
||||||
|
cleanUpConnection(deviceId: deviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanUpConnection(deviceId: String) {
|
||||||
|
characteristicReadFutures.removeAll { future in
|
||||||
|
future.deviceId == deviceId
|
||||||
|
}
|
||||||
|
discoverServicesFutures.removeAll { future in
|
||||||
|
future.deviceId == deviceId
|
||||||
|
}
|
||||||
|
discoveredServicesProgressMap[deviceId] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoverServices(deviceId: String, completion: @escaping (Result<[UniversalBleService], Error>) -> Void) {
|
||||||
|
guard let peripheral = discoveredPeripherals[deviceId] else {
|
||||||
|
completion(Result.failure(
|
||||||
|
FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(self)", details: nil)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if discoveredServicesProgressMap[deviceId] != nil {
|
||||||
|
completion(Result.failure(FlutterError(code: "AlreadyInProgress", message: "Services discovery already in progress for :\(deviceId)", details: nil)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
peripheral.discoverServices(nil)
|
||||||
|
discoverServicesFutures.append(DiscoverServicesFuture(deviceId: deviceId, result: completion))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func onServicesDiscovered(deviceId: String, services: [UniversalBleService]) {
|
||||||
|
discoverServicesFutures.removeAll { future in
|
||||||
|
if future.deviceId == deviceId {
|
||||||
|
future.result(Result.success(services))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setNotifiable(deviceId: String, service: String, characteristic: String, bleInputProperty: Int64) throws {
|
||||||
|
let peripheral = try deviceId.getPeripheral()
|
||||||
|
|
||||||
|
guard let c = peripheral.getCharacteristic(characteristic, of: service) else {
|
||||||
|
throw FlutterError(code: "IllegalArgument", message: "Unknown characteristic:\(characteristic)", details: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bleInputProperty == BleInputProperty.notification.rawValue && !c.properties.contains(.notify) {
|
||||||
|
throw FlutterError(code: "InvalidAction", message: "Characteristic does not support notify", details: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bleInputProperty == BleInputProperty.indication.rawValue && !c.properties.contains(.indicate) {
|
||||||
|
throw FlutterError(code: "InvalidAction", message: "Characteristic does not support indicate", details: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
let shouldNotify = bleInputProperty != BleInputProperty.disabled.rawValue
|
||||||
|
peripheral.setNotifyValue(shouldNotify, for: c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readValue(deviceId: String, service: String, characteristic: String, completion: @escaping (Result<FlutterStandardTypedData, Error>) -> Void) {
|
||||||
|
guard let peripheral = discoveredPeripherals[deviceId] else {
|
||||||
|
completion(Result.failure(FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(self)", details: nil)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let gattCharacteristic = peripheral.getCharacteristic(characteristic, of: service) else {
|
||||||
|
completion(Result.failure(FlutterError(code: "IllegalArgument", message: "Unknown characteristic:\(characteristic)", details: nil)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !gattCharacteristic.properties.contains(.read) {
|
||||||
|
completion(Result.failure(FlutterError(code: "InvalidAction", message: "Characteristic does not support read", details: nil)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
peripheral.readValue(for: gattCharacteristic)
|
||||||
|
characteristicReadFutures.append(CharacteristicReadFuture(deviceId: deviceId, characteristicId: gattCharacteristic.uuid.uuidStr, serviceId: gattCharacteristic.service?.uuid.uuidStr, result: completion))
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeValue(deviceId: String, service: String, characteristic: String, value: FlutterStandardTypedData, bleOutputProperty: Int64, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
guard let peripheral = discoveredPeripherals[deviceId] else {
|
||||||
|
completion(Result.failure(FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(self)", details: nil)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let gattCharacteristic = peripheral.getCharacteristic(characteristic, of: service) else {
|
||||||
|
completion(Result.failure(FlutterError(code: "IllegalArgument", message: "Unknown characteristic:\(characteristic)", details: nil)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let type = bleOutputProperty == BleOutputProperty.withoutResponse.rawValue ? CBCharacteristicWriteType.withoutResponse : CBCharacteristicWriteType.withResponse
|
||||||
|
|
||||||
|
if type == CBCharacteristicWriteType.withResponse {
|
||||||
|
if !gattCharacteristic.properties.contains(.write) {
|
||||||
|
completion(Result.failure(FlutterError(code: "InvalidAction", message: "Characteristic does not support write withResponse", details: nil)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if type == CBCharacteristicWriteType.withoutResponse {
|
||||||
|
if !gattCharacteristic.properties.contains(.writeWithoutResponse) {
|
||||||
|
completion(Result.failure(FlutterError(code: "InvalidAction", message: "Characteristic does not support write withoutResponse", details: nil)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
peripheral.writeValue(value.data, for: gattCharacteristic, type: type)
|
||||||
|
|
||||||
|
if type == CBCharacteristicWriteType.withResponse {
|
||||||
|
// Wait for future response
|
||||||
|
characteristicWriteFutures.append(CharacteristicWriteFuture(deviceId: deviceId, characteristicId: gattCharacteristic.uuid.uuidStr, serviceId: gattCharacteristic.service?.uuid.uuidStr, result: completion))
|
||||||
|
} else {
|
||||||
|
completion(Result.success({}()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestMtu(deviceId: String, expectedMtu _: Int64, completion: @escaping (Result<Int64, Error>) -> Void) {
|
||||||
|
guard let peripheral = discoveredPeripherals[deviceId] else {
|
||||||
|
completion(Result.failure(FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(self)", details: nil)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let mtu = peripheral.maximumWriteValueLength(for: CBCharacteristicWriteType.withoutResponse)
|
||||||
|
let GATT_HEADER_LENGTH = 3
|
||||||
|
let mtuResult = Int64(mtu + GATT_HEADER_LENGTH)
|
||||||
|
completion(Result.success(mtuResult))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPaired(deviceId _: String, completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||||
|
completion(Result.failure(FlutterError(code: "NotSupported", message: nil, details: nil)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func pair(deviceId _: String) throws {
|
||||||
|
throw FlutterError(code: "NotSupported", message: nil, details: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unPair(deviceId _: String) throws {
|
||||||
|
throw FlutterError(code: "NotSupported", message: nil, details: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConnectedDevices(withServices: [String], completion: @escaping (Result<[UniversalBleScanResult], Error>) -> Void) {
|
||||||
|
var filterCBUUID = withServices.map { CBUUID(string: $0) }
|
||||||
|
// We can't keep this filter empty, so adding a default filter
|
||||||
|
if filterCBUUID.isEmpty { filterCBUUID.append(CBUUID(string: "1800")) }
|
||||||
|
let bleDevices = manager.retrieveConnectedPeripherals(withServices: filterCBUUID)
|
||||||
|
bleDevices.forEach { discoveredPeripherals[$0.uuid.uuidString] = $0 }
|
||||||
|
completion(Result.success(bleDevices.map {
|
||||||
|
UniversalBleScanResult(
|
||||||
|
deviceId: $0.uuid.uuidString,
|
||||||
|
name: $0.name ?? ""
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||||
|
let state = central.state.toAvailabilityState().rawValue
|
||||||
|
callbackChannel.onAvailabilityChanged(state: state) { _ in }
|
||||||
|
|
||||||
|
bluetoothAvailabilityStateCallback?(Result.success(state))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func centralManager(_: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
|
||||||
|
discoveredPeripherals[peripheral.uuid.uuidString] = peripheral
|
||||||
|
let manufacturerData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data
|
||||||
|
callbackChannel.onScanResult(result: UniversalBleScanResult(
|
||||||
|
deviceId: peripheral.uuid.uuidString,
|
||||||
|
name: peripheral.name,
|
||||||
|
isPaired: nil,
|
||||||
|
rssi: RSSI as? Int64,
|
||||||
|
manufacturerData: FlutterStandardTypedData(bytes: manufacturerData ?? Data())
|
||||||
|
)) { _ in }
|
||||||
|
}
|
||||||
|
|
||||||
|
public func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||||||
|
callbackChannel.onConnectionChanged(deviceId: peripheral.uuid.uuidString, state: BlueConnectionState.connected.rawValue) { _ in }
|
||||||
|
}
|
||||||
|
|
||||||
|
public func centralManager(_: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error _: Error?) {
|
||||||
|
callbackChannel.onConnectionChanged(deviceId: peripheral.uuid.uuidString, state: BlueConnectionState.disconnected.rawValue) { _ in }
|
||||||
|
}
|
||||||
|
|
||||||
|
public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices _: Error?) {
|
||||||
|
let deviceId = peripheral.identifier.uuidString
|
||||||
|
guard let services = peripheral.services else {
|
||||||
|
onServicesDiscovered(deviceId: deviceId, services: [])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
discoveredServicesProgressMap[deviceId] = services.map { UniversalBleService(uuid: $0.uuid.uuidString, characteristics: nil) }
|
||||||
|
for service in services {
|
||||||
|
peripheral.discoverCharacteristics(nil, for: service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error _: Error?) {
|
||||||
|
let deviceId = peripheral.identifier.uuidString
|
||||||
|
var universalBleCharacteristicsList: [UniversalBleCharacteristic] = []
|
||||||
|
for characteristic in service.characteristics ?? [] {
|
||||||
|
universalBleCharacteristicsList.append(
|
||||||
|
UniversalBleCharacteristic(uuid: characteristic.uuid.uuidString, properties: characteristic.properties.toCharacteristicProperty))
|
||||||
|
}
|
||||||
|
// Update discoveredServicesProgressMap
|
||||||
|
if let index = discoveredServicesProgressMap[deviceId]?.firstIndex(where: { $0.uuid == service.uuid.uuidString }) {
|
||||||
|
discoveredServicesProgressMap[deviceId]?[index] = UniversalBleService(uuid: service.uuid.uuidString, characteristics: universalBleCharacteristicsList)
|
||||||
|
}
|
||||||
|
// Check if all services and their characteristics have been discovered
|
||||||
|
if discoveredServicesProgressMap[deviceId]?.allSatisfy({ $0.characteristics != nil }) ?? false {
|
||||||
|
onServicesDiscovered(deviceId: deviceId, services: discoveredServicesProgressMap[deviceId] ?? [])
|
||||||
|
discoveredServicesProgressMap[deviceId] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||||
|
print("peripheral:didWriteValueForCharacteristic \(characteristic.uuid.uuidStr) error: \(String(describing: error))")
|
||||||
|
// Update futures for writeValue
|
||||||
|
characteristicWriteFutures.removeAll { future in
|
||||||
|
if future.deviceId == peripheral.uuid.uuidString && future.characteristicId == characteristic.uuid.uuidStr && future.serviceId == characteristic.service?.uuid.uuidStr {
|
||||||
|
if error != nil {
|
||||||
|
future.result(Result.failure(FlutterError(code: "WriteFailed", message: String(describing: error), details: nil)))
|
||||||
|
} else {
|
||||||
|
future.result(Result.success({}()))
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||||
|
// Update callbackChannel if notifying
|
||||||
|
if characteristic.isNotifying {
|
||||||
|
if let characteristicValue = characteristic.value {
|
||||||
|
callbackChannel.onValueChanged(deviceId: peripheral.uuid.uuidString, characteristicId: characteristic.uuid.uuidStr, value: FlutterStandardTypedData(bytes: characteristicValue)) { _ in }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if characteristicReadFutures.count == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update futures for readValue
|
||||||
|
characteristicReadFutures.removeAll { future in
|
||||||
|
if future.deviceId == peripheral.uuid.uuidString && future.characteristicId == characteristic.uuid.uuidStr && future.serviceId == characteristic.service?.uuid.uuidStr {
|
||||||
|
if error != nil {
|
||||||
|
future.result(Result.failure(FlutterError(code: "ReadFailed", message: String(describing: error), details: nil)))
|
||||||
|
} else {
|
||||||
|
if let characteristicValue = characteristic.value {
|
||||||
|
future.result(Result.success(FlutterStandardTypedData(bytes: characteristicValue)))
|
||||||
|
} else {
|
||||||
|
future.result(Result.failure(FlutterError(code: "ReadFailed", message: "No value", details: nil)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
func getPeripheral() throws -> CBPeripheral {
|
||||||
|
guard let peripheral = discoveredPeripherals[self] else {
|
||||||
|
throw FlutterError(code: "IllegalArgument", message: "Unknown deviceId:\(self)", details: nil)
|
||||||
|
}
|
||||||
|
return peripheral
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FlutterError: Error {}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#
|
||||||
|
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
|
||||||
|
# Run `pod lib lint universal_ble.podspec` to validate before publishing.
|
||||||
|
#
|
||||||
|
Pod::Spec.new do |s|
|
||||||
|
s.name = 'universal_ble'
|
||||||
|
s.version = '0.0.1'
|
||||||
|
s.summary = 'A new Flutter plugin project.'
|
||||||
|
s.description = <<-DESC
|
||||||
|
A new Flutter plugin project.
|
||||||
|
DESC
|
||||||
|
s.homepage = 'http://example.com'
|
||||||
|
s.license = { :file => '../LICENSE' }
|
||||||
|
s.author = { 'Your Company' => 'email@example.com' }
|
||||||
|
s.source = { :path => '.' }
|
||||||
|
s.source_files = 'Classes/**/*'
|
||||||
|
s.ios.dependency 'Flutter'
|
||||||
|
s.osx.dependency 'FlutterMacOS'
|
||||||
|
s.ios.deployment_target = '9.0'
|
||||||
|
s.osx.deployment_target = '10.12'
|
||||||
|
|
||||||
|
# Flutter.framework does not contain a i386 slice.
|
||||||
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
||||||
|
s.swift_version = '5.0'
|
||||||
|
end
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.packages
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "ead455963c12b453cdb2358cad34969c76daf180"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: ead455963c12b453cdb2358cad34969c76daf180
|
||||||
|
base_revision: ead455963c12b453cdb2358cad34969c76daf180
|
||||||
|
- platform: web
|
||||||
|
create_revision: ead455963c12b453cdb2358cad34969c76daf180
|
||||||
|
base_revision: ead455963c12b453cdb2358cad34969c76daf180
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# universal_ble_example
|
||||||
|
|
||||||
|
Demonstrates how to use the universal_ble plugin.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This project is a starting point for a Flutter application.
|
||||||
|
|
||||||
|
A few resources to get you started if this is your first Flutter project:
|
||||||
|
|
||||||
|
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||||
|
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||||
|
|
||||||
|
For help getting started with Flutter development, view the
|
||||||
|
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||||
|
samples, guidance on mobile development, and a full API reference.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
plugins {
|
||||||
|
id "com.android.application"
|
||||||
|
id "kotlin-android"
|
||||||
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
|
}
|
||||||
|
|
||||||
|
def localProperties = new Properties()
|
||||||
|
def localPropertiesFile = rootProject.file('local.properties')
|
||||||
|
if (localPropertiesFile.exists()) {
|
||||||
|
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||||
|
localProperties.load(reader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||||
|
if (flutterVersionCode == null) {
|
||||||
|
flutterVersionCode = '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||||
|
if (flutterVersionName == null) {
|
||||||
|
flutterVersionName = '1.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace "com.navideck.universal_ble_example"
|
||||||
|
compileSdkVersion 34
|
||||||
|
ndkVersion flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '1.8'
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId "com.navideck.universal_ble_example"
|
||||||
|
// You can update the following values to match your application needs.
|
||||||
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||||
|
minSdkVersion 21
|
||||||
|
targetSdkVersion 34
|
||||||
|
versionCode flutterVersionCode.toInteger()
|
||||||
|
versionName flutterVersionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source '../..'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:label="universal_ble_example"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
|
the Android process has started. This theme is visible to the user
|
||||||
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
to determine the Window background behind the Flutter UI. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<!-- Don't delete the meta-data below.
|
||||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.navideck.universal_ble_example
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity: FlutterActivity() {
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?android:colorBackground" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@android:color/white" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
buildscript {
|
||||||
|
ext.kotlin_version = '1.7.10'
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:7.3.0'
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.buildDir = '../build'
|
||||||
|
subprojects {
|
||||||
|
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(':app')
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("clean", Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
pluginManagement {
|
||||||
|
def flutterSdkPath = {
|
||||||
|
def properties = new Properties()
|
||||||
|
file("local.properties").withInputStream { properties.load(it) }
|
||||||
|
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||||
|
return flutterSdkPath
|
||||||
|
}
|
||||||
|
settings.ext.flutterSdkPath = flutterSdkPath()
|
||||||
|
|
||||||
|
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
include ":app"
|
||||||
|
|
||||||
|
apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
**/dgph
|
||||||
|
*.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
*.moved-aside
|
||||||
|
*.pbxuser
|
||||||
|
*.perspectivev3
|
||||||
|
**/*sync/
|
||||||
|
.sconsign.dblite
|
||||||
|
.tags*
|
||||||
|
**/.vagrant/
|
||||||
|
**/DerivedData/
|
||||||
|
Icon?
|
||||||
|
**/Pods/
|
||||||
|
**/.symlinks/
|
||||||
|
profile
|
||||||
|
xcuserdata
|
||||||
|
**/.generated/
|
||||||
|
Flutter/App.framework
|
||||||
|
Flutter/Flutter.framework
|
||||||
|
Flutter/Flutter.podspec
|
||||||
|
Flutter/Generated.xcconfig
|
||||||
|
Flutter/ephemeral/
|
||||||
|
Flutter/app.flx
|
||||||
|
Flutter/app.zip
|
||||||
|
Flutter/flutter_assets/
|
||||||
|
Flutter/flutter_export_environment.sh
|
||||||
|
ServiceDefinitions.json
|
||||||
|
Runner/GeneratedPluginRegistrant.*
|
||||||
|
|
||||||
|
# Exceptions to above rules.
|
||||||
|
!default.mode1v3
|
||||||
|
!default.mode2v3
|
||||||
|
!default.pbxuser
|
||||||
|
!default.perspectivev3
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>io.flutter.flutter.app</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>FMWK</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>MinimumOSVersion</key>
|
||||||
|
<string>11.0</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||||
|
#include "Generated.xcconfig"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
|
#include "Generated.xcconfig"
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Uncomment this line to define a global platform for your project
|
||||||
|
# platform :ios, '11.0'
|
||||||
|
|
||||||
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
||||||
|
project 'Runner', {
|
||||||
|
'Debug' => :debug,
|
||||||
|
'Profile' => :release,
|
||||||
|
'Release' => :release,
|
||||||
|
}
|
||||||
|
|
||||||
|
def flutter_root
|
||||||
|
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||||
|
unless File.exist?(generated_xcode_build_settings_path)
|
||||||
|
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||||
|
end
|
||||||
|
|
||||||
|
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||||
|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||||
|
return matches[1].strip if matches
|
||||||
|
end
|
||||||
|
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||||
|
end
|
||||||
|
|
||||||
|
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||||
|
|
||||||
|
flutter_ios_podfile_setup
|
||||||
|
|
||||||
|
target 'Runner' do
|
||||||
|
use_frameworks!
|
||||||
|
use_modular_headers!
|
||||||
|
|
||||||
|
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||||
|
target 'RunnerTests' do
|
||||||
|
inherit! :search_paths
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post_install do |installer|
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
flutter_additional_ios_build_settings(target)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
PODS:
|
||||||
|
- device_info_plus (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- Flutter (1.0.0)
|
||||||
|
- integration_test (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- permission_handler_apple (9.1.1):
|
||||||
|
- Flutter
|
||||||
|
- universal_ble (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
|
||||||
|
DEPENDENCIES:
|
||||||
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
|
- Flutter (from `Flutter`)
|
||||||
|
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||||
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
|
- universal_ble (from `.symlinks/plugins/universal_ble/darwin`)
|
||||||
|
|
||||||
|
EXTERNAL SOURCES:
|
||||||
|
device_info_plus:
|
||||||
|
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||||
|
Flutter:
|
||||||
|
:path: Flutter
|
||||||
|
integration_test:
|
||||||
|
:path: ".symlinks/plugins/integration_test/ios"
|
||||||
|
permission_handler_apple:
|
||||||
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
|
universal_ble:
|
||||||
|
:path: ".symlinks/plugins/universal_ble/darwin"
|
||||||
|
|
||||||
|
SPEC CHECKSUMS:
|
||||||
|
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||||
|
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||||
|
integration_test: 13825b8a9334a850581300559b8839134b124670
|
||||||
|
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
|
||||||
|
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
|
||||||
|
|
||||||
|
PODFILE CHECKSUM: 70d9d25280d0dd177a5f637cdb0f0b0b12c6a189
|
||||||
|
|
||||||
|
COCOAPODS: 1.14.3
|
||||||
@@ -0,0 +1,724 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 54;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
|
3C020DDC565FF8D37F3235C5 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E92536821956EA00892DD639 /* Pods_Runner.framework */; };
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
|
B2742FE9E6C1C51A78D26865 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9AB88605F389EEED340E5872 /* Pods_RunnerTests.framework */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||||
|
remoteInfo = Runner;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 10;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
name = "Embed Frameworks";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
0D9A3CA727A1375BDCCC70CF /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
|
30B9BF32CD25D1ECB8BB5DCA /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
|
411AF23EC127806AE083685B /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
4414252E2351EBDB7761F83A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
6CF659084054FE2807275A3A /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
9AB88605F389EEED340E5872 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
A11EED44BE0324AD23055698 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
E92536821956EA00892DD639 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
8EE4584064C335A1F66BB6D9 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
B2742FE9E6C1C51A78D26865 /* Pods_RunnerTests.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
3C020DDC565FF8D37F3235C5 /* Pods_Runner.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||||
|
);
|
||||||
|
path = RunnerTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
68033A1F47BF85C19A1ED513 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E92536821956EA00892DD639 /* Pods_Runner.framework */,
|
||||||
|
9AB88605F389EEED340E5872 /* Pods_RunnerTests.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Flutter;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146E51CF9000F007C117D = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */,
|
||||||
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
|
FAF8A3758B0770B81319B3AE /* Pods */,
|
||||||
|
68033A1F47BF85C19A1ED513 /* Frameworks */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146EF1CF9000F007C117D /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */,
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||||
|
);
|
||||||
|
path = Runner;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
FAF8A3758B0770B81319B3AE /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
30B9BF32CD25D1ECB8BB5DCA /* Pods-Runner.debug.xcconfig */,
|
||||||
|
4414252E2351EBDB7761F83A /* Pods-Runner.release.xcconfig */,
|
||||||
|
A11EED44BE0324AD23055698 /* Pods-Runner.profile.xcconfig */,
|
||||||
|
411AF23EC127806AE083685B /* Pods-RunnerTests.debug.xcconfig */,
|
||||||
|
0D9A3CA727A1375BDCCC70CF /* Pods-RunnerTests.release.xcconfig */,
|
||||||
|
6CF659084054FE2807275A3A /* Pods-RunnerTests.profile.xcconfig */,
|
||||||
|
);
|
||||||
|
path = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
7E79F47357B64513A488AC3E /* [CP] Check Pods Manifest.lock */,
|
||||||
|
331C807D294A63A400263BE5 /* Sources */,
|
||||||
|
331C807F294A63A400263BE5 /* Resources */,
|
||||||
|
8EE4584064C335A1F66BB6D9 /* Frameworks */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = RunnerTests;
|
||||||
|
productName = RunnerTests;
|
||||||
|
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
|
buildPhases = (
|
||||||
|
3CE2E408302E9BF272A3BE07 /* [CP] Check Pods Manifest.lock */,
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
|
2F5EDC3CC74E7431E4CB95DF /* [CP] Embed Pods Frameworks */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = Runner;
|
||||||
|
productName = Runner;
|
||||||
|
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
BuildIndependentTargetsInParallel = YES;
|
||||||
|
LastUpgradeCheck = 1430;
|
||||||
|
ORGANIZATIONNAME = "";
|
||||||
|
TargetAttributes = {
|
||||||
|
331C8080294A63A400263BE5 = {
|
||||||
|
CreatedOnToolsVersion = 14.0;
|
||||||
|
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D = {
|
||||||
|
CreatedOnToolsVersion = 7.3.1;
|
||||||
|
LastSwiftMigration = 1100;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||||
|
compatibilityVersion = "Xcode 9.3";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */,
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
331C807F294A63A400263BE5 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
2F5EDC3CC74E7431E4CB95DF /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||||
|
);
|
||||||
|
name = "Thin Binary";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
|
};
|
||||||
|
3CE2E408302E9BF272A3BE07 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
7E79F47357B64513A488AC3E /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Run Script";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
331C807D294A63A400263BE5 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||||
|
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C146FB1CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = Main.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C147001CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = LaunchScreen.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = QQAZR43YM7;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
331C8088294A63A400263BE5 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 411AF23EC127806AE083685B /* Pods-RunnerTests.debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
331C8089294A63A400263BE5 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 0D9A3CA727A1375BDCCC70CF /* Pods-RunnerTests.release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
331C808A294A63A400263BE5 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 6CF659084054FE2807275A3A /* Pods-RunnerTests.profile.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample.RunnerTests;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
97C147031CF9000F007C117D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
97C147041CF9000F007C117D /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
97C147061CF9000F007C117D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = QQAZR43YM7;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
97C147071CF9000F007C117D /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = QQAZR43YM7;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.navideck.universalBleExample;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
331C8088294A63A400263BE5 /* Debug */,
|
||||||
|
331C8089294A63A400263BE5 /* Release */,
|
||||||
|
331C808A294A63A400263BE5 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
97C147031CF9000F007C117D /* Debug */,
|
||||||
|
97C147041CF9000F007C117D /* Release */,
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
97C147061CF9000F007C117D /* Debug */,
|
||||||
|
97C147071CF9000F007C117D /* Release */,
|
||||||
|
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1430"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||||
|
BuildableName = "RunnerTests.xctest"
|
||||||
|
BlueprintName = "RunnerTests"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Profile"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
<FileRef
|
||||||
|
location = "group:Pods/Pods.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import UIKit
|
||||||
|
import Flutter
|
||||||
|
|
||||||
|
@UIApplicationMain
|
||||||
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
|
override func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
|
) -> Bool {
|
||||||
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "83.5x83.5",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "1024x1024",
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"filename" : "Icon-App-1024x1024@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
@@ -0,0 +1,5 @@
|
|||||||
|
# Launch Screen Assets
|
||||||
|
|
||||||
|
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||||
|
|
||||||
|
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<image name="LaunchImage" width="168" height="185"/>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--Flutter View Controller-->
|
||||||
|
<scene sceneID="tne-QT-ifu">
|
||||||
|
<objects>
|
||||||
|
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Universal Ble</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>universal_ble_example</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIMainStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||||
|
<string>example</string>
|
||||||
|
<key>NSBluetoothPeripheralUsageDescription</key>
|
||||||
|
<string>example</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#import "GeneratedPluginRegistrant.h"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
@testable import universal_ble
|
||||||
|
|
||||||
|
// This demonstrates a simple unit test of the Swift portion of this plugin's implementation.
|
||||||
|
//
|
||||||
|
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||||
|
|
||||||
|
class RunnerTests: XCTestCase {
|
||||||
|
|
||||||
|
func testGetPlatformVersion() {
|
||||||
|
let plugin = UniversalBlePlugin()
|
||||||
|
|
||||||
|
let call = FlutterMethodCall(methodName: "getPlatformVersion", arguments: [])
|
||||||
|
|
||||||
|
let resultExpectation = expectation(description: "result block must be called.")
|
||||||
|
plugin.handle(call) { result in
|
||||||
|
XCTAssertEqual(result as! String, "iOS " + UIDevice.current.systemVersion)
|
||||||
|
resultExpectation.fulfill()
|
||||||
|
}
|
||||||
|
waitForExpectations(timeout: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
// ignore_for_file: use_build_context_synchronously, avoid_print
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:universal_ble/universal_ble.dart';
|
||||||
|
import 'package:universal_ble_example/peripheral_detail_page.dart';
|
||||||
|
import 'package:universal_ble_example/permission_validator.dart';
|
||||||
|
|
||||||
|
// Run with auto connect to : auto Scan , FindDevice, Connect, DiscoverServices
|
||||||
|
const autoConnect = bool.fromEnvironment('AUTO_CONNECT', defaultValue: false);
|
||||||
|
const deviceIdToConnect = "50:e9:f1:c2:5e:2e";
|
||||||
|
const deviceNameFilterForAutoConnect = "MyDevice";
|
||||||
|
const primaryColor = Colors.blue;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(
|
||||||
|
const MaterialApp(
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
home: MyApp(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp extends StatefulWidget {
|
||||||
|
const MyApp({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State createState() => _MyAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyAppState extends State<MyApp> {
|
||||||
|
StreamSubscription<BleScanResult>? _subscription;
|
||||||
|
final _scanResults = <BleScanResult>[];
|
||||||
|
AvailabilityState? bleAvailabilityState;
|
||||||
|
|
||||||
|
List<String> services = [
|
||||||
|
"00001800-0000-1000-8000-00805f9b34fb",
|
||||||
|
"00002a00-0000-1000-8000-00805f9b34fb",
|
||||||
|
"00002a01-0000-1000-8000-00805f9b34fb",
|
||||||
|
"00002a19-0000-1000-8000-00805f9b34fb",
|
||||||
|
"8000cc00-cc00-ffff-ffff-ffffffffffff",
|
||||||
|
"8000dd00-dd00-ffff-ffff-ffffffffffff",
|
||||||
|
];
|
||||||
|
|
||||||
|
// Set the web request options with the required services
|
||||||
|
get requestOptions =>
|
||||||
|
WebRequestOptionsBuilder.acceptAllDevices(optionalServices: services);
|
||||||
|
|
||||||
|
void initialize() {
|
||||||
|
UniversalBle.getBluetoothAvailabilityState().then((value) {
|
||||||
|
print("GetBluetoothAvailabilityState: ${value.name}");
|
||||||
|
setState(() {
|
||||||
|
bleAvailabilityState = value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
UniversalBle.onAvailabilityChange = (state) {
|
||||||
|
print("OnAvailabilityChange: ${state.name}");
|
||||||
|
setState(() {
|
||||||
|
bleAvailabilityState = state;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (autoConnect) {
|
||||||
|
UniversalBle.startScan(webRequestOptions: requestOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
UniversalBle.onScanResult = (result) {
|
||||||
|
if (!_scanResults.any((r) => r.deviceId == result.deviceId)) {
|
||||||
|
setState(() => _scanResults.add(result));
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
int index = _scanResults
|
||||||
|
.indexWhere((element) => element.deviceId == result.deviceId);
|
||||||
|
if (result.name == null && _scanResults[index].name != null) {
|
||||||
|
result.name = _scanResults[index].name;
|
||||||
|
}
|
||||||
|
_scanResults[index] = result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (autoConnect &&
|
||||||
|
(result.deviceId == deviceIdToConnect ||
|
||||||
|
(result.name?.contains(deviceNameFilterForAutoConnect) ==
|
||||||
|
true))) {
|
||||||
|
print("Auto connecting to ${result.deviceId}");
|
||||||
|
UniversalBle.stopScan();
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PeripheralDetailPage(result.deviceId, true),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_subscription?.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Universal Ble'),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
_buildButtons(),
|
||||||
|
const Divider(
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
_scanResults.isEmpty
|
||||||
|
? const Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.all(8.0),
|
||||||
|
child: Icon(
|
||||||
|
Icons.bluetooth,
|
||||||
|
color: Colors.grey,
|
||||||
|
size: 100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Scan For Devices',
|
||||||
|
style: TextStyle(color: Colors.grey, fontSize: 22),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: _buildListView(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildButtons() {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
|
const tileWidth = 150;
|
||||||
|
const tileHeight = 500;
|
||||||
|
final count = constraints.maxWidth ~/ tileWidth;
|
||||||
|
return GridView.count(
|
||||||
|
crossAxisCount: count,
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
childAspectRatio: tileHeight / tileWidth,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
children: [
|
||||||
|
ElevatedButton(
|
||||||
|
child: const Text('Enable Bluetooth'),
|
||||||
|
onPressed: () async {
|
||||||
|
bool isEnabled = await UniversalBle.enableBluetooth();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text("BluetoothEnabled: $isEnabled"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
child: const Text('Permissions'),
|
||||||
|
onPressed: () async {
|
||||||
|
bool hasPermissions =
|
||||||
|
await Validator.arePermissionsGranted();
|
||||||
|
if (hasPermissions) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text("Permissions already granted"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
child: const Text('Start Scan'),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_scanResults.clear();
|
||||||
|
});
|
||||||
|
UniversalBle.startScan(webRequestOptions: requestOptions);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
child: const Text('Stop Scan'),
|
||||||
|
onPressed: () {
|
||||||
|
UniversalBle.stopScan();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
child: const Text('Connected Devices'),
|
||||||
|
onPressed: () async {
|
||||||
|
var devices = await UniversalBle.getConnectedDevices(
|
||||||
|
withServices: services,
|
||||||
|
);
|
||||||
|
if (devices.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text("No Connected Devices Found"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// print(devices.map((e) => e.toJson()));
|
||||||
|
setState(() {
|
||||||
|
_scanResults.clear();
|
||||||
|
_scanResults.addAll(devices);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
'Ble Availability : ${bleAvailabilityState?.name}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_scanResults.clear();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.close_rounded)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildListView() {
|
||||||
|
return Expanded(
|
||||||
|
child: ListView.separated(
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
var scanResult = _scanResults[index];
|
||||||
|
String? name = scanResult.name;
|
||||||
|
if (name == null || name.isEmpty) {
|
||||||
|
name = 'NA';
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: Card(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(
|
||||||
|
'$name (${scanResult.rssi})',
|
||||||
|
),
|
||||||
|
subtitle: scanResult.isPaired != null
|
||||||
|
? Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("${scanResult.deviceId} "),
|
||||||
|
scanResult.isPaired == true
|
||||||
|
? const Text(
|
||||||
|
"Paired",
|
||||||
|
style: TextStyle(color: Colors.green),
|
||||||
|
)
|
||||||
|
: const Text(
|
||||||
|
"Not Paired",
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Text(scanResult.deviceId),
|
||||||
|
trailing: const Icon(Icons.arrow_forward_ios),
|
||||||
|
onTap: () {
|
||||||
|
print("$name: ${scanResult.deviceId}");
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
PeripheralDetailPage(scanResult.deviceId, false),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (context, index) => const Divider(),
|
||||||
|
itemCount: _scanResults.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,662 @@
|
|||||||
|
// ignore_for_file: avoid_print, depend_on_referenced_packages
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:convert/convert.dart';
|
||||||
|
import 'package:expandable/expandable.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:universal_ble/universal_ble.dart';
|
||||||
|
import 'package:universal_ble_example/main.dart';
|
||||||
|
|
||||||
|
String gssUuid(String code) => '0000$code-0000-1000-8000-00805f9b34fb';
|
||||||
|
|
||||||
|
class PeripheralDetailPage extends StatefulWidget {
|
||||||
|
final String deviceId;
|
||||||
|
final bool autoConnect;
|
||||||
|
const PeripheralDetailPage(this.deviceId, this.autoConnect, {Key? key})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() {
|
||||||
|
return _PeripheralDetailPageState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
|
||||||
|
bool isConnected = false;
|
||||||
|
bool parseValue = true;
|
||||||
|
GlobalKey<FormState> uuidFormKey = GlobalKey<FormState>();
|
||||||
|
GlobalKey<FormState> valueFormKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
List<BleService> discoveredServices = [];
|
||||||
|
ExpandableController expandableController =
|
||||||
|
ExpandableController(initialExpanded: autoConnect);
|
||||||
|
List<String> results = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
UniversalBle.onConnectionChanged = _handleConnectionChange;
|
||||||
|
UniversalBle.onValueChanged = _handleValueChange;
|
||||||
|
UniversalBle.onPairStateChange =
|
||||||
|
(String deviceId, bool isPaired, String? error) {
|
||||||
|
print('OnPairStateChange $deviceId, $isPaired');
|
||||||
|
setState(() {
|
||||||
|
if (error != null) {
|
||||||
|
results.add("PairStateChangeError (Paired: $isPaired): $error ");
|
||||||
|
} else {
|
||||||
|
results.add('PairStateChange: $isPaired');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (autoConnect) {
|
||||||
|
UniversalBle.connect(widget.deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
UniversalBle.onConnectionChanged = null;
|
||||||
|
UniversalBle.onValueChanged = null;
|
||||||
|
|
||||||
|
// Disconnect when leaving the page
|
||||||
|
if (isConnected) UniversalBle.disconnect(widget.deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleConnectionChange(String deviceId, BleConnectionState state) {
|
||||||
|
print('_handleConnectionChange $deviceId, ${state.name}');
|
||||||
|
setState(() {
|
||||||
|
if (deviceId == widget.deviceId) {
|
||||||
|
isConnected = (state == BleConnectionState.connected);
|
||||||
|
}
|
||||||
|
results.add('Connection: ${state.name.toUpperCase()}');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto Discover Services
|
||||||
|
if (isConnected) {
|
||||||
|
discoverServices();
|
||||||
|
} else if (autoConnect && !isConnected) {
|
||||||
|
print("Trying to reconnect");
|
||||||
|
UniversalBle.connect(widget.deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void discoverServices() async {
|
||||||
|
var services = await UniversalBle.discoverServices(widget.deviceId);
|
||||||
|
print('${services.length} services discovered');
|
||||||
|
discoveredServices.clear();
|
||||||
|
setState(() {
|
||||||
|
discoveredServices = services;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleValueChange(
|
||||||
|
String deviceId, String characteristicId, Uint8List value) {
|
||||||
|
String s = String.fromCharCodes(value);
|
||||||
|
String data =
|
||||||
|
parseValue ? '$s\nraw : ${value.toString()}' : value.toString();
|
||||||
|
print('_handleValueChange $deviceId, $characteristicId, $s');
|
||||||
|
setState(() {
|
||||||
|
results.add("Value: $data");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List? _getWriteValue() {
|
||||||
|
if (!uuidFormKey.currentState!.validate() ||
|
||||||
|
!valueFormKey.currentState!.validate()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (binaryCode.text.isEmpty) {
|
||||||
|
print("Error: No value to write");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<int>? value;
|
||||||
|
String text = binaryCode.text;
|
||||||
|
if (text.contains(",") || text.contains("[") || text.contains("]")) {
|
||||||
|
text = text.replaceAll("[", "").replaceAll("]", "").trim();
|
||||||
|
value = text.split(',').map(int.parse).toList();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
value = hex.decode(text);
|
||||||
|
} catch (e) {
|
||||||
|
print("Error parsing hex $e");
|
||||||
|
print("Trying utf8 encoding");
|
||||||
|
value = utf8.encode(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Uint8List.fromList(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
final serviceUUID = TextEditingController();
|
||||||
|
final characteristicUUID = TextEditingController();
|
||||||
|
final binaryCode = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Peripheral Details'),
|
||||||
|
actions: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Icon(
|
||||||
|
Icons.circle,
|
||||||
|
color: isConnected ? Colors.greenAccent : Colors.red,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: LayoutBuilder(builder: (context, constraints) {
|
||||||
|
bool isDesktop = constraints.maxWidth > 1000;
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
if (isDesktop)
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Container(
|
||||||
|
color: Colors.grey[300],
|
||||||
|
child: discoveredServices.isEmpty
|
||||||
|
? const Center(
|
||||||
|
child: Text('No Services Discovered'),
|
||||||
|
)
|
||||||
|
: ServicesView(
|
||||||
|
discoveredServices: discoveredServices,
|
||||||
|
scrollable: true,
|
||||||
|
onTap: (BleService service,
|
||||||
|
BleCharacteristic characteristic) {
|
||||||
|
serviceUUID.text = service.uuid;
|
||||||
|
characteristicUUID.text = characteristic.uuid;
|
||||||
|
print(
|
||||||
|
characteristic.properties.map((e) => e.name),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Form(
|
||||||
|
key: uuidFormKey,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Top buttons
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: <Widget>[
|
||||||
|
PlatformButton(
|
||||||
|
text: 'Connect',
|
||||||
|
enabled: !isConnected,
|
||||||
|
onPressed: () async {
|
||||||
|
try {
|
||||||
|
await UniversalBle.connect(widget.deviceId);
|
||||||
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
|
setState(() {
|
||||||
|
results.add('ConnectError : $e');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
PlatformButton(
|
||||||
|
text: 'Disconnect',
|
||||||
|
enabled: isConnected,
|
||||||
|
onPressed: () {
|
||||||
|
UniversalBle.disconnect(widget.deviceId);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
|
||||||
|
// Text Fields
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: TextFormField(
|
||||||
|
controller: serviceUUID,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Please enter a service uuid';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'ServiceUUID',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: TextFormField(
|
||||||
|
controller: characteristicUUID,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Please enter a Characteristic uuid';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'CharacteristicUUID',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Form(
|
||||||
|
key: valueFormKey,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 5,
|
||||||
|
child: TextFormField(
|
||||||
|
controller: binaryCode,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Please enter a value';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Enter Value ',
|
||||||
|
hintText:
|
||||||
|
"Enter Value (String, Hex or List<int>)",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Switch(
|
||||||
|
value: parseValue,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
parseValue = value;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const Divider(),
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: LayoutBuilder(builder: (context, constraints) {
|
||||||
|
const tileWidth = 150;
|
||||||
|
const tileHeight = 500;
|
||||||
|
final count = constraints.maxWidth ~/ tileWidth;
|
||||||
|
return GridView.count(
|
||||||
|
crossAxisCount: count,
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
childAspectRatio: tileHeight / tileWidth,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
children: [
|
||||||
|
PlatformButton(
|
||||||
|
enabled: isConnected,
|
||||||
|
onPressed: () {
|
||||||
|
Uint8List? value = _getWriteValue();
|
||||||
|
if (value == null) return;
|
||||||
|
print("Writing $value");
|
||||||
|
UniversalBle.writeValue(
|
||||||
|
widget.deviceId,
|
||||||
|
serviceUUID.text,
|
||||||
|
characteristicUUID.text,
|
||||||
|
value,
|
||||||
|
BleOutputProperty.withResponse);
|
||||||
|
},
|
||||||
|
text: 'Write',
|
||||||
|
),
|
||||||
|
PlatformButton(
|
||||||
|
enabled: isConnected,
|
||||||
|
onPressed: () async {
|
||||||
|
if (!uuidFormKey.currentState!.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Uint8List value =
|
||||||
|
await UniversalBle.readValue(
|
||||||
|
widget.deviceId,
|
||||||
|
serviceUUID.text,
|
||||||
|
characteristicUUID.text);
|
||||||
|
String s = String.fromCharCodes(value);
|
||||||
|
String data = parseValue
|
||||||
|
? '$s\nraw : ${value.toString()}'
|
||||||
|
: value.toString();
|
||||||
|
setState(() {
|
||||||
|
results.add('Read : $data');
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
|
setState(() {
|
||||||
|
results.add('ReadError : $e');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text: 'Read',
|
||||||
|
),
|
||||||
|
PlatformButton(
|
||||||
|
enabled: isConnected,
|
||||||
|
onPressed: () async {
|
||||||
|
int mtu = await UniversalBle.requestMtu(
|
||||||
|
widget.deviceId, 247);
|
||||||
|
setState(() {
|
||||||
|
results.add('MTU : $mtu');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
text: 'Request Mtu',
|
||||||
|
),
|
||||||
|
PlatformButton(
|
||||||
|
enabled: isConnected,
|
||||||
|
onPressed: () {
|
||||||
|
if (!uuidFormKey.currentState!.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BleCharacteristic? characteristic;
|
||||||
|
|
||||||
|
for (BleService service
|
||||||
|
in discoveredServices) {
|
||||||
|
for (BleCharacteristic c
|
||||||
|
in service.characteristics) {
|
||||||
|
if (c.uuid == characteristicUUID.text) {
|
||||||
|
characteristic = c;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BleInputProperty inputProperty =
|
||||||
|
BleInputProperty.notification;
|
||||||
|
if (characteristic != null) {
|
||||||
|
if (characteristic.properties.contains(
|
||||||
|
CharacteristicProperty.indicate)) {
|
||||||
|
inputProperty =
|
||||||
|
BleInputProperty.indication;
|
||||||
|
} else if (characteristic.properties
|
||||||
|
.contains(
|
||||||
|
CharacteristicProperty.notify)) {
|
||||||
|
inputProperty =
|
||||||
|
BleInputProperty.notification;
|
||||||
|
} else {
|
||||||
|
print('No notify or indicate property');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print(inputProperty);
|
||||||
|
UniversalBle.setNotifiable(
|
||||||
|
widget.deviceId,
|
||||||
|
serviceUUID.text,
|
||||||
|
characteristicUUID.text,
|
||||||
|
inputProperty,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
text: 'Notify/Indicate',
|
||||||
|
),
|
||||||
|
PlatformButton(
|
||||||
|
enabled: isConnected,
|
||||||
|
onPressed: () {
|
||||||
|
if (!uuidFormKey.currentState!.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UniversalBle.setNotifiable(
|
||||||
|
widget.deviceId,
|
||||||
|
serviceUUID.text,
|
||||||
|
characteristicUUID.text,
|
||||||
|
BleInputProperty.disabled,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
text: 'Cancel Notify',
|
||||||
|
),
|
||||||
|
PlatformButton(
|
||||||
|
onPressed: () async {
|
||||||
|
discoverServices();
|
||||||
|
},
|
||||||
|
enabled: isConnected,
|
||||||
|
text: 'Discover Services',
|
||||||
|
),
|
||||||
|
PlatformButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await UniversalBle.pair(widget.deviceId);
|
||||||
|
},
|
||||||
|
text: 'Pair',
|
||||||
|
),
|
||||||
|
PlatformButton(
|
||||||
|
onPressed: () async {
|
||||||
|
bool isPaired = await UniversalBle.isPaired(
|
||||||
|
widget.deviceId);
|
||||||
|
setState(() {
|
||||||
|
results.add('IsPaired : $isPaired');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
text: 'IsPaired',
|
||||||
|
),
|
||||||
|
PlatformButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await UniversalBle.unPair(widget.deviceId);
|
||||||
|
},
|
||||||
|
text: 'UnPair',
|
||||||
|
),
|
||||||
|
PlatformButton(
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() {
|
||||||
|
results.clear();
|
||||||
|
discoveredServices.clear();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
text: 'Clear',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Results , Services
|
||||||
|
if (!isDesktop)
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
ResultView(
|
||||||
|
results: results,
|
||||||
|
onClearTap: (int? index) {
|
||||||
|
setState(() {
|
||||||
|
if (index != null) {
|
||||||
|
results.removeAt(index);
|
||||||
|
} else {
|
||||||
|
results.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
const Divider(),
|
||||||
|
ServicesView(
|
||||||
|
discoveredServices: discoveredServices,
|
||||||
|
onTap: (BleService service,
|
||||||
|
BleCharacteristic characteristic) {
|
||||||
|
serviceUUID.text = service.uuid;
|
||||||
|
characteristicUUID.text = characteristic.uuid;
|
||||||
|
print(
|
||||||
|
characteristic.properties
|
||||||
|
.map((e) => e.name),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// For Desktop
|
||||||
|
if (isDesktop)
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Container(
|
||||||
|
color: Colors.grey[300],
|
||||||
|
child: results.isEmpty
|
||||||
|
? const Center(
|
||||||
|
child: Text('No results'),
|
||||||
|
)
|
||||||
|
: ResultView(
|
||||||
|
results: results,
|
||||||
|
scrollable: true,
|
||||||
|
onClearTap: (int? index) {
|
||||||
|
setState(() {
|
||||||
|
if (index != null) {
|
||||||
|
results.removeAt(index);
|
||||||
|
} else {
|
||||||
|
results.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResultView extends StatelessWidget {
|
||||||
|
final List<String> results;
|
||||||
|
final bool scrollable;
|
||||||
|
final Function(int? index) onClearTap;
|
||||||
|
const ResultView({
|
||||||
|
required this.results,
|
||||||
|
required this.onClearTap,
|
||||||
|
this.scrollable = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: !scrollable,
|
||||||
|
physics: scrollable ? null : const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: results.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Card(
|
||||||
|
child: ListTile(
|
||||||
|
onTap: () {
|
||||||
|
onClearTap(index);
|
||||||
|
},
|
||||||
|
title: Text(results[index]),
|
||||||
|
trailing: const Icon(Icons.clear),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServicesView extends StatelessWidget {
|
||||||
|
final List<BleService> discoveredServices;
|
||||||
|
final bool scrollable;
|
||||||
|
|
||||||
|
final Function(BleService service, BleCharacteristic characteristic)? onTap;
|
||||||
|
const ServicesView({
|
||||||
|
super.key,
|
||||||
|
required this.discoveredServices,
|
||||||
|
this.onTap,
|
||||||
|
this.scrollable = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: !scrollable,
|
||||||
|
physics: scrollable ? null : const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: discoveredServices.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Card(
|
||||||
|
child: ExpandablePanel(
|
||||||
|
header: Container(
|
||||||
|
color: scrollable ? Colors.lime : Colors.grey[300],
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.arrow_forward_ios),
|
||||||
|
Expanded(child: Text(discoveredServices[index].uuid)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
collapsed: const SizedBox(),
|
||||||
|
expanded: Column(
|
||||||
|
children: discoveredServices[index]
|
||||||
|
.characteristics
|
||||||
|
.map((e) => Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
onTap?.call(discoveredServices[index], e);
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.arrow_right_outlined),
|
||||||
|
Expanded(child: Text(e.uuid)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Properties: ${e.properties.map((e) => e.name)}",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlatformButton extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
final Function()? onPressed;
|
||||||
|
final bool enabled;
|
||||||
|
const PlatformButton({
|
||||||
|
required this.text,
|
||||||
|
required this.onPressed,
|
||||||
|
this.enabled = true,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ElevatedButton(
|
||||||
|
onPressed: enabled ? onPressed : null,
|
||||||
|
child: Text(text),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
// ignore_for_file: avoid_print
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
/*
|
||||||
|
Required Permissions :
|
||||||
|
<----------->
|
||||||
|
IOS :
|
||||||
|
- Bluetooth
|
||||||
|
<----------->
|
||||||
|
Android :
|
||||||
|
if AndroidVersions < 12
|
||||||
|
- Location
|
||||||
|
- Bluetooth
|
||||||
|
else
|
||||||
|
- Bluetooth Scan
|
||||||
|
- Bluetooth Connect
|
||||||
|
<----------->
|
||||||
|
Macos :
|
||||||
|
<----------->
|
||||||
|
Windows : None
|
||||||
|
<----------->
|
||||||
|
Linux : None
|
||||||
|
<----------->
|
||||||
|
Web :
|
||||||
|
Check if Browser Supports Bluetooth
|
||||||
|
*/
|
||||||
|
|
||||||
|
static Future<bool> arePermissionsGranted() async {
|
||||||
|
if (!isMobilePlatform) return true;
|
||||||
|
|
||||||
|
var status = await _permissionStatus;
|
||||||
|
bool blePermissionGranted = status[0];
|
||||||
|
bool locationPermissionGranted = status[1];
|
||||||
|
|
||||||
|
if (locationPermissionGranted && blePermissionGranted) return true;
|
||||||
|
|
||||||
|
if (!blePermissionGranted) {
|
||||||
|
PermissionStatus blePermissionCheck =
|
||||||
|
await Permission.bluetooth.request();
|
||||||
|
if (blePermissionCheck.isPermanentlyDenied) {
|
||||||
|
print("Bluetooth Permission Permanently Denied");
|
||||||
|
openAppSettings();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!locationPermissionGranted) {
|
||||||
|
PermissionStatus locationPermissionCheck =
|
||||||
|
await Permission.location.request();
|
||||||
|
if (locationPermissionCheck.isPermanentlyDenied) {
|
||||||
|
print("Location Permission Permanently Denied");
|
||||||
|
openAppSettings();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<bool>> get _permissionStatus async {
|
||||||
|
bool blePermissionGranted = false;
|
||||||
|
bool locationPermissionGranted = false;
|
||||||
|
|
||||||
|
if (await requiresExplicitAndroidBluetoothPermissions) {
|
||||||
|
bool bleConnectPermission =
|
||||||
|
(await Permission.bluetoothConnect.request()).isGranted;
|
||||||
|
bool bleScanPermission =
|
||||||
|
(await Permission.bluetoothScan.request()).isGranted;
|
||||||
|
|
||||||
|
blePermissionGranted = bleConnectPermission && bleScanPermission;
|
||||||
|
locationPermissionGranted = true;
|
||||||
|
} else {
|
||||||
|
PermissionStatus permissionStatus = await Permission.bluetooth.request();
|
||||||
|
blePermissionGranted = permissionStatus.isGranted;
|
||||||
|
locationPermissionGranted = await requiresLocationPermission
|
||||||
|
? (await Permission.locationWhenInUse.request()).isGranted
|
||||||
|
: true;
|
||||||
|
}
|
||||||
|
return [locationPermissionGranted, blePermissionGranted];
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool get isMobilePlatform =>
|
||||||
|
!kIsWeb && (Platform.isAndroid || Platform.isIOS);
|
||||||
|
|
||||||
|
static Future<bool> get requiresLocationPermission async =>
|
||||||
|
!kIsWeb &&
|
||||||
|
Platform.isAndroid &&
|
||||||
|
(!await requiresExplicitAndroidBluetoothPermissions);
|
||||||
|
|
||||||
|
static Future<bool> get requiresExplicitAndroidBluetoothPermissions async {
|
||||||
|
if (kIsWeb || !Platform.isAndroid) return false;
|
||||||
|
AndroidDeviceInfo androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||||
|
return androidInfo.version.sdkInt >= 31;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
flutter/ephemeral
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# Project-level configuration.
|
||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
project(runner LANGUAGES CXX)
|
||||||
|
|
||||||
|
# The name of the executable created for the application. Change this to change
|
||||||
|
# the on-disk name of your application.
|
||||||
|
set(BINARY_NAME "universal_ble_example")
|
||||||
|
# The unique GTK application identifier for this application. See:
|
||||||
|
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||||
|
set(APPLICATION_ID "com.navideck.universal_ble")
|
||||||
|
|
||||||
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
|
# versions of CMake.
|
||||||
|
cmake_policy(SET CMP0063 NEW)
|
||||||
|
|
||||||
|
# Load bundled libraries from the lib/ directory relative to the binary.
|
||||||
|
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
|
||||||
|
|
||||||
|
# Root filesystem for cross-building.
|
||||||
|
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
|
||||||
|
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
|
||||||
|
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Define build configuration options.
|
||||||
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||||
|
STRING "Flutter build mode" FORCE)
|
||||||
|
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||||
|
"Debug" "Profile" "Release")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Compilation settings that should be applied to most targets.
|
||||||
|
#
|
||||||
|
# Be cautious about adding new options here, as plugins use this function by
|
||||||
|
# default. In most cases, you should add new options to specific targets instead
|
||||||
|
# of modifying this function.
|
||||||
|
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||||
|
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||||
|
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||||
|
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||||
|
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Flutter library and tool build rules.
|
||||||
|
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||||
|
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||||
|
|
||||||
|
# System-level dependencies.
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||||
|
|
||||||
|
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||||
|
|
||||||
|
# Define the application target. To change its name, change BINARY_NAME above,
|
||||||
|
# not the value here, or `flutter run` will no longer work.
|
||||||
|
#
|
||||||
|
# Any new source files that you add to the application should be added here.
|
||||||
|
add_executable(${BINARY_NAME}
|
||||||
|
"main.cc"
|
||||||
|
"my_application.cc"
|
||||||
|
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply the standard set of build settings. This can be removed for applications
|
||||||
|
# that need different build settings.
|
||||||
|
apply_standard_settings(${BINARY_NAME})
|
||||||
|
|
||||||
|
# Add dependency libraries. Add any application-specific dependencies here.
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
|
||||||
|
|
||||||
|
# Run the Flutter tool portions of the build. This must not be removed.
|
||||||
|
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||||
|
|
||||||
|
# Only the install-generated bundle's copy of the executable will launch
|
||||||
|
# correctly, since the resources must in the right relative locations. To avoid
|
||||||
|
# people trying to run the unbundled copy, put it in a subdirectory instead of
|
||||||
|
# the default top-level location.
|
||||||
|
set_target_properties(${BINARY_NAME}
|
||||||
|
PROPERTIES
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Enable the test target.
|
||||||
|
set(include_universal_ble_tests TRUE)
|
||||||
|
|
||||||
|
# Generated plugin build rules, which manage building the plugins and adding
|
||||||
|
# them to the application.
|
||||||
|
include(flutter/generated_plugins.cmake)
|
||||||
|
|
||||||
|
|
||||||
|
# === Installation ===
|
||||||
|
# By default, "installing" just makes a relocatable bundle in the build
|
||||||
|
# directory.
|
||||||
|
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
||||||
|
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||||
|
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Start with a clean build bundle directory every time.
|
||||||
|
install(CODE "
|
||||||
|
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
|
||||||
|
" COMPONENT Runtime)
|
||||||
|
|
||||||
|
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||||
|
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
|
||||||
|
|
||||||
|
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
|
||||||
|
install(FILES "${bundled_library}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
endforeach(bundled_library)
|
||||||
|
|
||||||
|
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||||
|
# from a previous install.
|
||||||
|
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||||
|
install(CODE "
|
||||||
|
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||||
|
" COMPONENT Runtime)
|
||||||
|
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||||
|
|
||||||
|
# Install the AOT library on non-Debug builds only.
|
||||||
|
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||||
|
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# This file controls Flutter-level build steps. It should not be edited.
|
||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
|
||||||
|
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||||
|
|
||||||
|
# Configuration provided via flutter tool.
|
||||||
|
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||||
|
|
||||||
|
# TODO: Move the rest of this into files in ephemeral. See
|
||||||
|
# https://github.com/flutter/flutter/issues/57146.
|
||||||
|
|
||||||
|
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
|
||||||
|
# which isn't available in 3.10.
|
||||||
|
function(list_prepend LIST_NAME PREFIX)
|
||||||
|
set(NEW_LIST "")
|
||||||
|
foreach(element ${${LIST_NAME}})
|
||||||
|
list(APPEND NEW_LIST "${PREFIX}${element}")
|
||||||
|
endforeach(element)
|
||||||
|
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# === Flutter Library ===
|
||||||
|
# System-level dependencies.
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||||
|
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
|
||||||
|
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
|
||||||
|
|
||||||
|
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
|
||||||
|
|
||||||
|
# Published to parent scope for install step.
|
||||||
|
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||||
|
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||||
|
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||||
|
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||||
|
"fl_basic_message_channel.h"
|
||||||
|
"fl_binary_codec.h"
|
||||||
|
"fl_binary_messenger.h"
|
||||||
|
"fl_dart_project.h"
|
||||||
|
"fl_engine.h"
|
||||||
|
"fl_json_message_codec.h"
|
||||||
|
"fl_json_method_codec.h"
|
||||||
|
"fl_message_codec.h"
|
||||||
|
"fl_method_call.h"
|
||||||
|
"fl_method_channel.h"
|
||||||
|
"fl_method_codec.h"
|
||||||
|
"fl_method_response.h"
|
||||||
|
"fl_plugin_registrar.h"
|
||||||
|
"fl_plugin_registry.h"
|
||||||
|
"fl_standard_message_codec.h"
|
||||||
|
"fl_standard_method_codec.h"
|
||||||
|
"fl_string_codec.h"
|
||||||
|
"fl_value.h"
|
||||||
|
"fl_view.h"
|
||||||
|
"flutter_linux.h"
|
||||||
|
)
|
||||||
|
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
|
||||||
|
add_library(flutter INTERFACE)
|
||||||
|
target_include_directories(flutter INTERFACE
|
||||||
|
"${EPHEMERAL_DIR}"
|
||||||
|
)
|
||||||
|
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
|
||||||
|
target_link_libraries(flutter INTERFACE
|
||||||
|
PkgConfig::GTK
|
||||||
|
PkgConfig::GLIB
|
||||||
|
PkgConfig::GIO
|
||||||
|
)
|
||||||
|
add_dependencies(flutter flutter_assemble)
|
||||||
|
|
||||||
|
# === Flutter tool backend ===
|
||||||
|
# _phony_ is a non-existent file to force this command to run every time,
|
||||||
|
# since currently there's no way to get a full input/output list from the
|
||||||
|
# flutter tool.
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/_phony_
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env
|
||||||
|
${FLUTTER_TOOL_ENVIRONMENT}
|
||||||
|
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
|
||||||
|
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_target(flutter_assemble DEPENDS
|
||||||
|
"${FLUTTER_LIBRARY}"
|
||||||
|
${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
|
||||||
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||||
|
#define GENERATED_PLUGIN_REGISTRANT_
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
|
||||||
|
// Registers Flutter plugins.
|
||||||
|
void fl_register_plugins(FlPluginRegistry* registry);
|
||||||
|
|
||||||
|
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#
|
||||||
|
# Generated file, do not edit.
|
||||||
|
#
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
)
|
||||||
|
|
||||||
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|
||||||
|
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||||
|
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||||
|
endforeach(plugin)
|
||||||
|
|
||||||
|
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||||
|
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||||
|
endforeach(ffi_plugin)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#include "my_application.h"
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
g_autoptr(MyApplication) app = my_application_new();
|
||||||
|
return g_application_run(G_APPLICATION(app), argc, argv);
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
#include "my_application.h"
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
#ifdef GDK_WINDOWING_X11
|
||||||
|
#include <gdk/gdkx.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "flutter/generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
struct _MyApplication {
|
||||||
|
GtkApplication parent_instance;
|
||||||
|
char** dart_entrypoint_arguments;
|
||||||
|
};
|
||||||
|
|
||||||
|
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||||
|
|
||||||
|
// Implements GApplication::activate.
|
||||||
|
static void my_application_activate(GApplication* application) {
|
||||||
|
MyApplication* self = MY_APPLICATION(application);
|
||||||
|
GtkWindow* window =
|
||||||
|
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||||
|
|
||||||
|
// Use a header bar when running in GNOME as this is the common style used
|
||||||
|
// by applications and is the setup most users will be using (e.g. Ubuntu
|
||||||
|
// desktop).
|
||||||
|
// If running on X and not using GNOME then just use a traditional title bar
|
||||||
|
// in case the window manager does more exotic layout, e.g. tiling.
|
||||||
|
// If running on Wayland assume the header bar will work (may need changing
|
||||||
|
// if future cases occur).
|
||||||
|
gboolean use_header_bar = TRUE;
|
||||||
|
#ifdef GDK_WINDOWING_X11
|
||||||
|
GdkScreen* screen = gtk_window_get_screen(window);
|
||||||
|
if (GDK_IS_X11_SCREEN(screen)) {
|
||||||
|
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||||
|
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
||||||
|
use_header_bar = FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (use_header_bar) {
|
||||||
|
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||||
|
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||||
|
gtk_header_bar_set_title(header_bar, "universal_ble_example");
|
||||||
|
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||||
|
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||||
|
} else {
|
||||||
|
gtk_window_set_title(window, "universal_ble_example");
|
||||||
|
}
|
||||||
|
|
||||||
|
gtk_window_set_default_size(window, 1280, 720);
|
||||||
|
gtk_widget_show(GTK_WIDGET(window));
|
||||||
|
|
||||||
|
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||||
|
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
|
||||||
|
|
||||||
|
FlView* view = fl_view_new(project);
|
||||||
|
gtk_widget_show(GTK_WIDGET(view));
|
||||||
|
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||||
|
|
||||||
|
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||||
|
|
||||||
|
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements GApplication::local_command_line.
|
||||||
|
static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
|
||||||
|
MyApplication* self = MY_APPLICATION(application);
|
||||||
|
// Strip out the first argument as it is the binary name.
|
||||||
|
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
||||||
|
|
||||||
|
g_autoptr(GError) error = nullptr;
|
||||||
|
if (!g_application_register(application, nullptr, &error)) {
|
||||||
|
g_warning("Failed to register: %s", error->message);
|
||||||
|
*exit_status = 1;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_application_activate(application);
|
||||||
|
*exit_status = 0;
|
||||||
|
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements GObject::dispose.
|
||||||
|
static void my_application_dispose(GObject* object) {
|
||||||
|
MyApplication* self = MY_APPLICATION(object);
|
||||||
|
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||||
|
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void my_application_class_init(MyApplicationClass* klass) {
|
||||||
|
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||||
|
G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
|
||||||
|
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void my_application_init(MyApplication* self) {}
|
||||||
|
|
||||||
|
MyApplication* my_application_new() {
|
||||||
|
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||||
|
"application-id", APPLICATION_ID,
|
||||||
|
"flags", G_APPLICATION_NON_UNIQUE,
|
||||||
|
nullptr));
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#ifndef FLUTTER_MY_APPLICATION_H_
|
||||||
|
#define FLUTTER_MY_APPLICATION_H_
|
||||||
|
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
|
||||||
|
GtkApplication)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* my_application_new:
|
||||||
|
*
|
||||||
|
* Creates a new Flutter-based application.
|
||||||
|
*
|
||||||
|
* Returns: a new #MyApplication.
|
||||||
|
*/
|
||||||
|
MyApplication* my_application_new();
|
||||||
|
|
||||||
|
#endif // FLUTTER_MY_APPLICATION_H_
|
||||||