ESP32 | BLUETOOTH CLASSIC | Flutter - Let's build BT Serial based on the examples. (Ft. Chat App)

This commit is contained in:
Eric
2020-05-03 20:14:14 -07:00
parent d6c37fce33
commit 10a1b6f404
78 changed files with 3782 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
//https://github.com/espressif/arduino-esp32/blob/master/libraries/BluetoothSerial/examples/SerialToSerialBT/SerialToSerialBT.ino
//This example code is in the Public Domain (or CC0 licensed, at your option.)
//By Evandro Copercini - 2018
//
//This example creates a bridge between Serial and Classical Bluetooth (SPP)
//and also demonstrate that SerialBT have the same functionalities of a normal Serial
#include "BluetoothSerial.h"
#if !defined(CONFIG_BT_ENABLED) || !defined(CONFIG_BLUEDROID_ENABLED)
#error Bluetooth is not enabled! Please run `make menuconfig` to and enable it
#endif
BluetoothSerial SerialBT;
void setup() {
Serial.begin(115200);
SerialBT.begin("ESP32_CLASSIC_BT"); //Bluetooth device name
Serial.println("The device started, now you can pair it with bluetooth!");
}
void loop() {
if (Serial.available()) {
SerialBT.write(Serial.read());
}
if (SerialBT.available()) {
Serial.write(SerialBT.read());
}
delay(20);
}

View File

@@ -0,0 +1,44 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Exceptions to above rules.
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages

View File

@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 45da0f63af4fbc60b920c3a13d58201408c3c8f4
channel: master
project_type: app

View File

@@ -0,0 +1,16 @@
# androidbluetoothserialapp
A new Flutter application.
## 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://flutter.dev/docs/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
For help getting started with Flutter, view our
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@@ -0,0 +1,7 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java

View File

@@ -0,0 +1,54 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 28
lintOptions {
disable 'InvalidPackage'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.thatproject.androidbluetoothserialapp"
minSdkVersion 18
targetSdkVersion 28
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 '../..'
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.thatproject.androidbluetoothserialapp">
<!-- Flutter 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>

View File

@@ -0,0 +1,47 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.thatproject.androidbluetoothserialapp">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:name="io.flutter.app.FlutterApplication"
android:label="androidbluetoothserialapp"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
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"
/>
<!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of
Flutter's first frame. -->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background"
/>
<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>

View File

@@ -0,0 +1,6 @@
package com.thatproject.androidbluetoothserialapp;
import io.flutter.embedding.android.FlutterActivity;
public class MainActivity extends FlutterActivity {
}

View File

@@ -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>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter 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:color/white</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.thatproject.androidbluetoothserialapp">
<!-- Flutter 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>

View File

@@ -0,0 +1,29 @@
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
}
}
allprojects {
repositories {
google()
jcenter()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip

View File

@@ -0,0 +1,15 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
include ':app'
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"

View File

@@ -0,0 +1,32 @@
*.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/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

View File

@@ -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>$(DEVELOPMENT_LANGUAGE)</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>8.0</string>
</dict>
</plist>

View File

@@ -0,0 +1,2 @@
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@@ -0,0 +1,2 @@
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

View File

@@ -0,0 +1,84 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '9.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 parse_KV_file(file, separator='=')
file_abs_path = File.expand_path(file)
if !File.exists? file_abs_path
return [];
end
generated_key_values = {}
skip_line_start_symbols = ["#", "/"]
File.foreach(file_abs_path) do |line|
next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
plugin = line.split(pattern=separator)
if plugin.length == 2
podname = plugin[0].strip()
path = plugin[1].strip()
podpath = File.expand_path("#{path}", file_abs_path)
generated_key_values[podname] = podpath
else
puts "Invalid plugin specification: #{line}"
end
end
generated_key_values
end
target 'Runner' do
# Flutter Pod
copied_flutter_dir = File.join(__dir__, 'Flutter')
copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework')
copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec')
unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path)
# Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet.
# That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration.
# CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.
generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig')
unless File.exist?(generated_xcode_build_settings_path)
raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path)
cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR'];
unless File.exist?(copied_framework_path)
FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir)
end
unless File.exist?(copied_podspec_path)
FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir)
end
end
# Keep pod path relative so it can be checked into Podfile.lock.
pod 'Flutter', :path => 'Flutter'
# Plugin Pods
# Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
# referring to absolute paths on developers' machines.
system('rm -rf .symlinks')
system('mkdir -p .symlinks/plugins')
plugin_pods = parse_KV_file('../.flutter-plugins')
plugin_pods.each do |name, path|
symlink = File.join('.symlinks', 'plugins', name)
File.symlink(path, symlink)
pod name, :path => File.join(symlink, 'ios')
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'NO'
end
end
end

View File

@@ -0,0 +1,496 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
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 */; };
/* End PBXBuildFile 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 */
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>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; 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; };
97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
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>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
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 */,
CF3B75C9A7D2FA2A4C99F110 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */,
7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
97C146F11CF9000F007C117D /* Supporting Files */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
);
path = Runner;
sourceTree = "<group>";
};
97C146F11CF9000F007C117D /* Supporting Files */ = {
isa = PBXGroup;
children = (
97C146F21CF9000F007C117D /* main.m */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
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 = {
LastUpgradeCheck = 1020;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
};
};
};
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 */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
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 */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
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 */
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
97C146F31CF9000F007C117D /* main.m in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase 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 = 8.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;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.thatproject.androidbluetoothserialapp;
PRODUCT_NAME = "$(TARGET_NAME)";
VERSIONING_SYSTEM = "apple-generic";
};
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 = 8.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 = 8.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.thatproject.androidbluetoothserialapp;
PRODUCT_NAME = "$(TARGET_NAME)";
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.thatproject.androidbluetoothserialapp;
PRODUCT_NAME = "$(TARGET_NAME)";
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
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 */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
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">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</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>
<AdditionalOptions>
</AdditionalOptions>
</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>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,6 @@
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
@interface AppDelegate : FlutterAppDelegate
@end

View File

@@ -0,0 +1,13 @@
#import "AppDelegate.h"
#import "GeneratedPluginRegistrant.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self];
// Override point for customization after application launch.
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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.

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,45 @@
<?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>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>androidbluetoothserialapp</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>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,9 @@
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char* argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

View File

@@ -0,0 +1,145 @@
import 'package:flutter/material.dart';
import './BackgroundCollectingTask.dart';
import './helpers/LineChart.dart';
import './helpers/PaintStyle.dart';
class BackgroundCollectedPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final BackgroundCollectingTask task =
BackgroundCollectingTask.of(context, rebuildOnChange: true);
// Arguments shift is needed for timestamps as miliseconds in double could loose precision.
final int argumentsShift =
task.samples.first.timestamp.millisecondsSinceEpoch;
final Duration showDuration =
Duration(hours: 2); // @TODO . show duration should be configurable
final Iterable<DataSample> lastSamples = task.getLastOf(showDuration);
final Iterable<double> arguments = lastSamples.map((sample) {
return (sample.timestamp.millisecondsSinceEpoch - argumentsShift)
.toDouble();
});
// Step for argument labels
final Duration argumentsStep =
Duration(minutes: 15); // @TODO . step duration should be configurable
// Find first timestamp floored to step before
final DateTime beginningArguments = lastSamples.first.timestamp;
DateTime beginningArgumentsStep = DateTime(beginningArguments.year,
beginningArguments.month, beginningArguments.day);
while (beginningArgumentsStep.isBefore(beginningArguments)) {
beginningArgumentsStep = beginningArgumentsStep.add(argumentsStep);
}
beginningArgumentsStep = beginningArgumentsStep.subtract(argumentsStep);
final DateTime endingArguments = lastSamples.last.timestamp;
// Generate list of timestamps of labels
final Iterable<DateTime> argumentsLabelsTimestamps = () sync* {
DateTime timestamp = beginningArgumentsStep;
yield timestamp;
while (timestamp.isBefore(endingArguments)) {
timestamp = timestamp.add(argumentsStep);
yield timestamp;
}
}();
// Map strings for labels
final Iterable<LabelEntry> argumentsLabels =
argumentsLabelsTimestamps.map((timestamp) {
return LabelEntry(
(timestamp.millisecondsSinceEpoch - argumentsShift).toDouble(),
((timestamp.hour <= 9 ? '0' : '') +
timestamp.hour.toString() +
':' +
(timestamp.minute <= 9 ? '0' : '') +
timestamp.minute.toString()));
});
return Scaffold(
appBar: AppBar(
title: Text('Collected data'),
actions: <Widget>[
// Progress circle
(task.inProgress
? FittedBox(
child: Container(
margin: new EdgeInsets.all(16.0),
child: CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation<Color>(Colors.white))))
: Container(/* Dummy */)),
// Start/stop buttons
(task.inProgress
? IconButton(icon: Icon(Icons.pause), onPressed: task.pause)
: IconButton(
icon: Icon(Icons.play_arrow), onPressed: task.reasume)),
],
),
body: ListView(
children: <Widget>[
Divider(),
ListTile(
leading: const Icon(Icons.brightness_7),
title: const Text('Temperatures'),
subtitle: const Text('In Celsius'),
),
LineChart(
constraints: const BoxConstraints.expand(height: 350),
arguments: arguments,
argumentsLabels: argumentsLabels,
values: [
lastSamples.map((sample) => sample.temperature1),
lastSamples.map((sample) => sample.temperature2),
],
verticalLinesStyle: const PaintStyle(color: Colors.grey),
additionalMinimalHorizontalLabelsInterval: 0,
additionalMinimalVerticalLablesInterval: 0,
seriesPointsStyles: [
null,
null,
//const PaintStyle(style: PaintingStyle.stroke, strokeWidth: 1.7*3, color: Colors.indigo, strokeCap: StrokeCap.round),
],
seriesLinesStyles: [
const PaintStyle(
style: PaintingStyle.stroke,
strokeWidth: 1.7,
color: Colors.indigoAccent),
const PaintStyle(
style: PaintingStyle.stroke,
strokeWidth: 1.7,
color: Colors.redAccent),
],
),
Divider(),
ListTile(
leading: const Icon(Icons.filter_vintage),
title: const Text('Water pH level'),
),
LineChart(
constraints: const BoxConstraints.expand(height: 200),
arguments: arguments,
argumentsLabels: argumentsLabels,
values: [
lastSamples.map((sample) => sample.waterpHlevel),
],
verticalLinesStyle: const PaintStyle(color: Colors.grey),
additionalMinimalHorizontalLabelsInterval: 0,
additionalMinimalVerticalLablesInterval: 0,
seriesPointsStyles: [
null,
],
seriesLinesStyles: [
const PaintStyle(
style: PaintingStyle.stroke,
strokeWidth: 1.7,
color: Colors.greenAccent),
],
),
],
));
}
}

View File

@@ -0,0 +1,123 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
import 'package:scoped_model/scoped_model.dart';
class DataSample {
double temperature1;
double temperature2;
double waterpHlevel;
DateTime timestamp;
DataSample({
this.temperature1,
this.temperature2,
this.waterpHlevel,
this.timestamp,
});
}
class BackgroundCollectingTask extends Model {
static BackgroundCollectingTask of(
BuildContext context, {
bool rebuildOnChange = false,
}) =>
ScopedModel.of<BackgroundCollectingTask>(
context,
rebuildOnChange: rebuildOnChange,
);
final BluetoothConnection _connection;
List<int> _buffer = List<int>();
// @TODO , Such sample collection in real code should be delegated
// (via `Stream<DataSample>` preferably) and then saved for later
// displaying on chart (or even stright prepare for displaying).
// @TODO ? should be shrinked at some point, endless colleting data would cause memory shortage.
List<DataSample> samples = List<DataSample>();
bool inProgress;
BackgroundCollectingTask._fromConnection(this._connection) {
_connection.input.listen((data) {
_buffer += data;
while (true) {
// If there is a sample, and it is full sent
int index = _buffer.indexOf('t'.codeUnitAt(0));
if (index >= 0 && _buffer.length - index >= 7) {
final DataSample sample = DataSample(
temperature1: (_buffer[index + 1] + _buffer[index + 2] / 100),
temperature2: (_buffer[index + 3] + _buffer[index + 4] / 100),
waterpHlevel: (_buffer[index + 5] + _buffer[index + 6] / 100),
timestamp: DateTime.now());
_buffer.removeRange(0, index + 7);
samples.add(sample);
notifyListeners(); // Note: It shouldn't be invoked very often - in this example data comes at every second, but if there would be more data, it should update (including repaint of graphs) in some fixed interval instead of after every sample.
//print("${sample.timestamp.toString()} -> ${sample.temperature1} / ${sample.temperature2}");
}
// Otherwise break
else {
break;
}
}
}).onDone(() {
inProgress = false;
notifyListeners();
});
}
static Future<BackgroundCollectingTask> connect(
BluetoothDevice server) async {
final BluetoothConnection connection =
await BluetoothConnection.toAddress(server.address);
return BackgroundCollectingTask._fromConnection(connection);
}
void dispose() {
_connection.dispose();
}
Future<void> start() async {
inProgress = true;
_buffer.clear();
samples.clear();
notifyListeners();
_connection.output.add(ascii.encode('start'));
await _connection.output.allSent;
}
Future<void> cancel() async {
inProgress = false;
notifyListeners();
_connection.output.add(ascii.encode('stop'));
await _connection.finish();
}
Future<void> pause() async {
inProgress = false;
notifyListeners();
_connection.output.add(ascii.encode('stop'));
await _connection.output.allSent;
}
Future<void> reasume() async {
inProgress = true;
notifyListeners();
_connection.output.add(ascii.encode('start'));
await _connection.output.allSent;
}
Iterable<DataSample> getLastOf(Duration duration) {
DateTime startingTime = DateTime.now().subtract(duration);
int i = samples.length;
do {
i -= 1;
if (i <= 0) {
break;
}
} while (samples[i].timestamp.isAfter(startingTime));
return samples.getRange(i, samples.length);
}
}

View File

@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
class BluetoothDeviceListEntry extends ListTile {
BluetoothDeviceListEntry({
@required BluetoothDevice device,
int rssi,
GestureTapCallback onTap,
GestureLongPressCallback onLongPress,
bool enabled = true,
}) : super(
onTap: onTap,
onLongPress: onLongPress,
enabled: enabled,
leading:
Icon(Icons.devices), // @TODO . !BluetoothClass! class aware icon
title: Text(device.name ?? "Unknown device"),
subtitle: Text(device.address.toString()),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
rssi != null
? Container(
margin: new EdgeInsets.all(8.0),
child: DefaultTextStyle(
style: _computeTextStyle(rssi),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(rssi.toString()),
Text('dBm'),
],
),
),
)
: Container(width: 0, height: 0),
device.isConnected
? Icon(Icons.import_export)
: Container(width: 0, height: 0),
device.isBonded
? Icon(Icons.link)
: Container(width: 0, height: 0),
],
),
);
static TextStyle _computeTextStyle(int rssi) {
/**/ if (rssi >= -35)
return TextStyle(color: Colors.greenAccent[700]);
else if (rssi >= -45)
return TextStyle(
color: Color.lerp(
Colors.greenAccent[700], Colors.lightGreen, -(rssi + 35) / 10));
else if (rssi >= -55)
return TextStyle(
color: Color.lerp(
Colors.lightGreen, Colors.lime[600], -(rssi + 45) / 10));
else if (rssi >= -65)
return TextStyle(
color: Color.lerp(Colors.lime[600], Colors.amber, -(rssi + 55) / 10));
else if (rssi >= -75)
return TextStyle(
color: Color.lerp(
Colors.amber, Colors.deepOrangeAccent, -(rssi + 65) / 10));
else if (rssi >= -85)
return TextStyle(
color: Color.lerp(
Colors.deepOrangeAccent, Colors.redAccent, -(rssi + 75) / 10));
else
/*code symetry*/
return TextStyle(color: Colors.redAccent);
}
}

View File

@@ -0,0 +1,237 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
class ChatPage extends StatefulWidget {
final BluetoothDevice server;
const ChatPage({this.server});
@override
_ChatPage createState() => new _ChatPage();
}
class _Message {
int whom;
String text;
_Message(this.whom, this.text);
}
class _ChatPage extends State<ChatPage> {
static final clientID = 0;
BluetoothConnection connection;
List<_Message> messages = List<_Message>();
String _messageBuffer = '';
final TextEditingController textEditingController =
new TextEditingController();
final ScrollController listScrollController = new ScrollController();
bool isConnecting = true;
bool get isConnected => connection != null && connection.isConnected;
bool isDisconnecting = false;
@override
void initState() {
super.initState();
BluetoothConnection.toAddress(widget.server.address).then((_connection) {
print('Connected to the device');
connection = _connection;
setState(() {
isConnecting = false;
isDisconnecting = false;
});
connection.input.listen(_onDataReceived).onDone(() {
// Example: Detect which side closed the connection
// There should be `isDisconnecting` flag to show are we are (locally)
// in middle of disconnecting process, should be set before calling
// `dispose`, `finish` or `close`, which all causes to disconnect.
// If we except the disconnection, `onDone` should be fired as result.
// If we didn't except this (no flag set), it means closing by remote.
if (isDisconnecting) {
print('Disconnecting locally!');
} else {
print('Disconnected remotely!');
}
if (this.mounted) {
setState(() {});
}
});
}).catchError((error) {
print('Cannot connect, exception occured');
print(error);
});
}
@override
void dispose() {
// Avoid memory leak (`setState` after dispose) and disconnect
if (isConnected) {
isDisconnecting = true;
connection.dispose();
connection = null;
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final List<Row> list = messages.map((_message) {
return Row(
children: <Widget>[
Container(
child: Text(
(text) {
return text == '/shrug' ? '¯\\_(ツ)_/¯' : text;
}(_message.text.trim()),
style: TextStyle(color: Colors.white)),
padding: EdgeInsets.all(12.0),
margin: EdgeInsets.only(bottom: 8.0, left: 8.0, right: 8.0),
width: 222.0,
decoration: BoxDecoration(
color:
_message.whom == clientID ? Colors.blueAccent : Colors.grey,
borderRadius: BorderRadius.circular(7.0)),
),
],
mainAxisAlignment: _message.whom == clientID
? MainAxisAlignment.end
: MainAxisAlignment.start,
);
}).toList();
return Scaffold(
appBar: AppBar(
title: (isConnecting
? Text('Connecting chat to ' + widget.server.name + '...')
: isConnected
? Text('Live chat with ' + widget.server.name)
: Text('Chat log with ' + widget.server.name))),
body: SafeArea(
child: Column(
children: <Widget>[
Flexible(
child: ListView(
padding: const EdgeInsets.all(12.0),
controller: listScrollController,
children: list),
),
Row(
children: <Widget>[
Flexible(
child: Container(
margin: const EdgeInsets.only(left: 16.0),
child: TextField(
style: const TextStyle(fontSize: 15.0),
controller: textEditingController,
decoration: InputDecoration.collapsed(
hintText: isConnecting
? 'Wait until connected...'
: isConnected
? 'Type your message...'
: 'Chat got disconnected',
hintStyle: const TextStyle(color: Colors.grey),
),
enabled: isConnected,
),
),
),
Container(
margin: const EdgeInsets.all(8.0),
child: IconButton(
icon: const Icon(Icons.send),
onPressed: isConnected
? () => _sendMessage(textEditingController.text)
: null),
),
],
)
],
),
),
);
}
void _onDataReceived(Uint8List data) {
// Allocate buffer for parsed data
int backspacesCounter = 0;
data.forEach((byte) {
if (byte == 8 || byte == 127) {
backspacesCounter++;
}
});
Uint8List buffer = Uint8List(data.length - backspacesCounter);
int bufferIndex = buffer.length;
// Apply backspace control character
backspacesCounter = 0;
for (int i = data.length - 1; i >= 0; i--) {
if (data[i] == 8 || data[i] == 127) {
backspacesCounter++;
} else {
if (backspacesCounter > 0) {
backspacesCounter--;
} else {
buffer[--bufferIndex] = data[i];
}
}
}
// Create message if there is new line character
String dataString = String.fromCharCodes(buffer);
int index = buffer.indexOf(13);
if (~index != 0) {
setState(() {
messages.add(
_Message(
1,
backspacesCounter > 0
? _messageBuffer.substring(
0, _messageBuffer.length - backspacesCounter)
: _messageBuffer + dataString.substring(0, index),
),
);
_messageBuffer = dataString.substring(index);
});
} else {
_messageBuffer = (backspacesCounter > 0
? _messageBuffer.substring(
0, _messageBuffer.length - backspacesCounter)
: _messageBuffer + dataString);
}
}
void _sendMessage(String text) async {
text = text.trim();
textEditingController.clear();
if (text.length > 0) {
try {
connection.output.add(utf8.encode(text + "\r\n"));
await connection.output.allSent;
setState(() {
messages.add(_Message(clientID, text));
});
Future.delayed(Duration(milliseconds: 333)).then((_) {
listScrollController.animateTo(
listScrollController.position.maxScrollExtent,
duration: Duration(milliseconds: 333),
curve: Curves.easeOut);
});
} catch (e) {
// Ignore error, but notify state
setState(() {});
}
}
}
}

View File

@@ -0,0 +1,153 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
import './BluetoothDeviceListEntry.dart';
class DiscoveryPage extends StatefulWidget {
/// If true, discovery starts on page start, otherwise user must press action button.
final bool start;
const DiscoveryPage({this.start = true});
@override
_DiscoveryPage createState() => new _DiscoveryPage();
}
class _DiscoveryPage extends State<DiscoveryPage> {
StreamSubscription<BluetoothDiscoveryResult> _streamSubscription;
List<BluetoothDiscoveryResult> results = List<BluetoothDiscoveryResult>();
bool isDiscovering;
_DiscoveryPage();
@override
void initState() {
super.initState();
isDiscovering = widget.start;
if (isDiscovering) {
_startDiscovery();
}
}
void _restartDiscovery() {
setState(() {
results.clear();
isDiscovering = true;
});
_startDiscovery();
}
void _startDiscovery() {
_streamSubscription =
FlutterBluetoothSerial.instance.startDiscovery().listen((r) {
setState(() {
results.add(r);
});
});
_streamSubscription.onDone(() {
setState(() {
isDiscovering = false;
});
});
}
// @TODO . One day there should be `_pairDevice` on long tap on something... ;)
@override
void dispose() {
// Avoid memory leak (`setState` after dispose) and cancel discovery
_streamSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: isDiscovering
? Text('Discovering devices')
: Text('Discovered devices'),
actions: <Widget>[
isDiscovering
? FittedBox(
child: Container(
margin: new EdgeInsets.all(16.0),
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
)
: IconButton(
icon: Icon(Icons.replay),
onPressed: _restartDiscovery,
)
],
),
body: ListView.builder(
itemCount: results.length,
itemBuilder: (BuildContext context, index) {
BluetoothDiscoveryResult result = results[index];
return BluetoothDeviceListEntry(
device: result.device,
rssi: result.rssi,
onTap: () {
Navigator.of(context).pop(result.device);
},
onLongPress: () async {
try {
bool bonded = false;
if (result.device.isBonded) {
print('Unbonding from ${result.device.address}...');
await FlutterBluetoothSerial.instance
.removeDeviceBondWithAddress(result.device.address);
print('Unbonding from ${result.device.address} has succed');
} else {
print('Bonding with ${result.device.address}...');
bonded = await FlutterBluetoothSerial.instance
.bondDeviceAtAddress(result.device.address);
print(
'Bonding with ${result.device.address} has ${bonded ? 'succed' : 'failed'}.');
}
setState(() {
results[results.indexOf(result)] = BluetoothDiscoveryResult(
device: BluetoothDevice(
name: result.device.name ?? '',
address: result.device.address,
type: result.device.type,
bondState: bonded
? BluetoothBondState.bonded
: BluetoothBondState.none,
),
rssi: result.rssi);
});
} catch (ex) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Error occured while bonding'),
content: Text("${ex.toString()}"),
actions: <Widget>[
new FlatButton(
child: new Text("Close"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
},
);
},
),
);
}
}

View File

@@ -0,0 +1,358 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
import 'package:scoped_model/scoped_model.dart';
import './DiscoveryPage.dart';
import './SelectBondedDevicePage.dart';
import './ChatPage.dart';
import './BackgroundCollectingTask.dart';
import './BackgroundCollectedPage.dart';
// import './helpers/LineChart.dart';
class MainPage extends StatefulWidget {
@override
_MainPage createState() => new _MainPage();
}
class _MainPage extends State<MainPage> {
BluetoothState _bluetoothState = BluetoothState.UNKNOWN;
String _address = "...";
String _name = "...";
Timer _discoverableTimeoutTimer;
int _discoverableTimeoutSecondsLeft = 0;
BackgroundCollectingTask _collectingTask;
bool _autoAcceptPairingRequests = false;
@override
void initState() {
super.initState();
// Get current state
FlutterBluetoothSerial.instance.state.then((state) {
setState(() {
_bluetoothState = state;
});
});
Future.doWhile(() async {
// Wait if adapter not enabled
if (await FlutterBluetoothSerial.instance.isEnabled) {
return false;
}
await Future.delayed(Duration(milliseconds: 0xDD));
return true;
}).then((_) {
// Update the address field
FlutterBluetoothSerial.instance.address.then((address) {
setState(() {
_address = address;
});
});
});
FlutterBluetoothSerial.instance.name.then((name) {
setState(() {
_name = name;
});
});
// Listen for futher state changes
FlutterBluetoothSerial.instance
.onStateChanged()
.listen((BluetoothState state) {
setState(() {
_bluetoothState = state;
// Discoverable mode is disabled when Bluetooth gets disabled
_discoverableTimeoutTimer = null;
_discoverableTimeoutSecondsLeft = 0;
});
});
}
@override
void dispose() {
FlutterBluetoothSerial.instance.setPairingRequestHandler(null);
_collectingTask?.dispose();
_discoverableTimeoutTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Bluetooth Serial'),
),
body: Container(
child: ListView(
children: <Widget>[
Divider(),
ListTile(title: const Text('General')),
SwitchListTile(
title: const Text('Enable Bluetooth'),
value: _bluetoothState.isEnabled,
onChanged: (bool value) {
// Do the request and update with the true value then
future() async {
// async lambda seems to not working
if (value)
await FlutterBluetoothSerial.instance.requestEnable();
else
await FlutterBluetoothSerial.instance.requestDisable();
}
future().then((_) {
setState(() {});
});
},
),
ListTile(
title: const Text('Bluetooth status'),
subtitle: Text(_bluetoothState.toString()),
trailing: RaisedButton(
child: const Text('Settings'),
onPressed: () {
FlutterBluetoothSerial.instance.openSettings();
},
),
),
ListTile(
title: const Text('Local adapter address'),
subtitle: Text(_address),
),
ListTile(
title: const Text('Local adapter name'),
subtitle: Text(_name),
onLongPress: null,
),
ListTile(
title: _discoverableTimeoutSecondsLeft == 0
? const Text("Discoverable")
: Text(
"Discoverable for ${_discoverableTimeoutSecondsLeft}s"),
subtitle: const Text("PsychoX-Luna"),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Checkbox(
value: _discoverableTimeoutSecondsLeft != 0,
onChanged: null,
),
IconButton(
icon: const Icon(Icons.edit),
onPressed: null,
),
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () async {
print('Discoverable requested');
final int timeout = await FlutterBluetoothSerial.instance
.requestDiscoverable(60);
if (timeout < 0) {
print('Discoverable mode denied');
} else {
print(
'Discoverable mode acquired for $timeout seconds');
}
setState(() {
_discoverableTimeoutTimer?.cancel();
_discoverableTimeoutSecondsLeft = timeout;
_discoverableTimeoutTimer =
Timer.periodic(Duration(seconds: 1), (Timer timer) {
setState(() {
if (_discoverableTimeoutSecondsLeft < 0) {
FlutterBluetoothSerial.instance.isDiscoverable
.then((isDiscoverable) {
if (isDiscoverable) {
print(
"Discoverable after timeout... might be infinity timeout :F");
_discoverableTimeoutSecondsLeft += 1;
}
});
timer.cancel();
_discoverableTimeoutSecondsLeft = 0;
} else {
_discoverableTimeoutSecondsLeft -= 1;
}
});
});
});
},
)
],
),
),
Divider(),
ListTile(title: const Text('Devices discovery and connection')),
SwitchListTile(
title: const Text('Auto-try specific pin when pairing'),
subtitle: const Text('Pin 1234'),
value: _autoAcceptPairingRequests,
onChanged: (bool value) {
setState(() {
_autoAcceptPairingRequests = value;
});
if (value) {
FlutterBluetoothSerial.instance.setPairingRequestHandler(
(BluetoothPairingRequest request) {
print("Trying to auto-pair with Pin 1234");
if (request.pairingVariant == PairingVariant.Pin) {
return Future.value("1234");
}
return null;
});
} else {
FlutterBluetoothSerial.instance
.setPairingRequestHandler(null);
}
},
),
ListTile(
title: RaisedButton(
child: const Text('Explore discovered devices'),
onPressed: () async {
final BluetoothDevice selectedDevice =
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return DiscoveryPage();
},
),
);
if (selectedDevice != null) {
print('Discovery -> selected ' + selectedDevice.address);
} else {
print('Discovery -> no device selected');
}
}),
),
ListTile(
title: RaisedButton(
child: const Text('Connect to paired device to chat'),
onPressed: () async {
final BluetoothDevice selectedDevice =
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return SelectBondedDevicePage(checkAvailability: false);
},
),
);
if (selectedDevice != null) {
print('Connect -> selected ' + selectedDevice.address);
_startChat(context, selectedDevice);
} else {
print('Connect -> no device selected');
}
},
),
),
Divider(),
ListTile(title: const Text('Multiple connections example')),
ListTile(
title: RaisedButton(
child: ((_collectingTask != null && _collectingTask.inProgress)
? const Text('Disconnect and stop background collecting')
: const Text('Connect to start background collecting')),
onPressed: () async {
if (_collectingTask != null && _collectingTask.inProgress) {
await _collectingTask.cancel();
setState(() {
/* Update for `_collectingTask.inProgress` */
});
} else {
final BluetoothDevice selectedDevice =
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return SelectBondedDevicePage(
checkAvailability: false);
},
),
);
if (selectedDevice != null) {
await _startBackgroundTask(context, selectedDevice);
setState(() {
/* Update for `_collectingTask.inProgress` */
});
}
}
},
),
),
ListTile(
title: RaisedButton(
child: const Text('View background collected data'),
onPressed: (_collectingTask != null)
? () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return ScopedModel<BackgroundCollectingTask>(
model: _collectingTask,
child: BackgroundCollectedPage(),
);
},
),
);
}
: null,
),
),
],
),
),
);
}
void _startChat(BuildContext context, BluetoothDevice server) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return ChatPage(server: server);
},
),
);
}
Future<void> _startBackgroundTask(
BuildContext context,
BluetoothDevice server,
) async {
try {
_collectingTask = await BackgroundCollectingTask.connect(server);
await _collectingTask.start();
} catch (ex) {
if (_collectingTask != null) {
_collectingTask.cancel();
}
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Error occured while connecting'),
content: Text("${ex.toString()}"),
actions: <Widget>[
new FlatButton(
child: new Text("Close"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
}
}

View File

@@ -0,0 +1,144 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
import './BluetoothDeviceListEntry.dart';
class SelectBondedDevicePage extends StatefulWidget {
/// If true, on page start there is performed discovery upon the bonded devices.
/// Then, if they are not avaliable, they would be disabled from the selection.
final bool checkAvailability;
const SelectBondedDevicePage({this.checkAvailability = true});
@override
_SelectBondedDevicePage createState() => new _SelectBondedDevicePage();
}
enum _DeviceAvailability {
no,
maybe,
yes,
}
class _DeviceWithAvailability extends BluetoothDevice {
BluetoothDevice device;
_DeviceAvailability availability;
int rssi;
_DeviceWithAvailability(this.device, this.availability, [this.rssi]);
}
class _SelectBondedDevicePage extends State<SelectBondedDevicePage> {
List<_DeviceWithAvailability> devices = List<_DeviceWithAvailability>();
// Availability
StreamSubscription<BluetoothDiscoveryResult> _discoveryStreamSubscription;
bool _isDiscovering;
_SelectBondedDevicePage();
@override
void initState() {
super.initState();
_isDiscovering = widget.checkAvailability;
if (_isDiscovering) {
_startDiscovery();
}
// Setup a list of the bonded devices
FlutterBluetoothSerial.instance
.getBondedDevices()
.then((List<BluetoothDevice> bondedDevices) {
setState(() {
devices = bondedDevices
.map(
(device) => _DeviceWithAvailability(
device,
widget.checkAvailability
? _DeviceAvailability.maybe
: _DeviceAvailability.yes,
),
)
.toList();
});
});
}
void _restartDiscovery() {
setState(() {
_isDiscovering = true;
});
_startDiscovery();
}
void _startDiscovery() {
_discoveryStreamSubscription =
FlutterBluetoothSerial.instance.startDiscovery().listen((r) {
setState(() {
Iterator i = devices.iterator;
while (i.moveNext()) {
var _device = i.current;
if (_device.device == r.device) {
_device.availability = _DeviceAvailability.yes;
_device.rssi = r.rssi;
}
}
});
});
_discoveryStreamSubscription.onDone(() {
setState(() {
_isDiscovering = false;
});
});
}
@override
void dispose() {
// Avoid memory leak (`setState` after dispose) and cancel discovery
_discoveryStreamSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
List<BluetoothDeviceListEntry> list = devices
.map((_device) => BluetoothDeviceListEntry(
device: _device.device,
rssi: _device.rssi,
enabled: _device.availability == _DeviceAvailability.yes,
onTap: () {
Navigator.of(context).pop(_device.device);
},
))
.toList();
return Scaffold(
appBar: AppBar(
title: Text('Select device'),
actions: <Widget>[
_isDiscovering
? FittedBox(
child: Container(
margin: new EdgeInsets.all(16.0),
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
),
)
: IconButton(
icon: Icon(Icons.replay),
onPressed: _restartDiscovery,
)
],
),
body: ListView(children: list),
);
}
}

View File

@@ -0,0 +1,600 @@
/// @name LineChart
/// @version 0.0.5
/// @description Simple line chart widget
/// @author Patryk "PsychoX" Ludwikowski <patryk.ludwikowski.7+dart@gmail.com>
/// @license MIT License (see https://mit-license.org/)
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import 'dart:math' as math show min, max;
import './PaintStyle.dart';
class LabelEntry {
final double value;
final String label;
LabelEntry(this.value, this.label);
}
/// Widget that allows to show data on line chart.
///
/// All arguments, values and labels data should be sorted!
/// Since both the arguments and the values must be `double` type,
/// be aware of the precision.
class LineChart extends StatelessWidget {
/// Constraints for the line chart.
final BoxConstraints constraints;
// @TODO ? Both `_LineChartPainter` and `LineChart` have most the same fields.
// `LineChart` is just mainly passing them to the painter. Shouldn't there be
// only one class containing these data? Some `LineChartData` forged inside here
// and then passed and used by the painter? :thinking:
/// Padding around main drawng area. Necessary for displaying labels (around the chart).
final EdgeInsets padding;
/* Arguments */
/// Collection of doubles as arguments.
final Iterable<double> arguments;
/// Mappings of strings for doubles arguments, which allow to specify custom
/// strings as labels for certain arguments.
final Iterable<LabelEntry> argumentsLabels;
/* Values */
/// Collection of data series as collections of next values on corresponding arguments.
final Iterable<Iterable<double>> values;
/// Mappings of string for doubles values, which allow to specify custom
/// string as labels for certain values.
final Iterable<LabelEntry> valuesLabels;
/* Labels & lines styles */
/// Style of horizontal lines labels
final TextStyle horizontalLabelsTextStyle;
/// Style of vertical lines labels
final TextStyle verticalLabelsTextStyle;
/// Defines style of horizontal lines. Might be null in order to prevent lines from drawing.
final Paint horizontalLinesPaint;
/// Defines style of vertical lines. Might be null in order to prevent lines from drawing.
final Paint verticalLinesPaint;
// @TODO . expose it
final bool snapToLeftLabel = false;
final bool snapToTopLabel = true;
final bool snapToRightLabel = false;
final bool snapToBottomLabel = true;
/* Series points & lines styles */
/// List of paint styles for series values points.
///
/// On whole list null would use predefined set of styles.
/// On list entry null there will be no points for certain series.
final List<Paint> seriesPointsPaints;
/// List of paint styles for lines between next series points.
///
/// On null there will be no lines.
final List<Paint> seriesLinesPaints;
final double additionalMinimalHorizontalLabelsInterval;
final double additionalMinimalVerticalLablesInterval;
LineChart({
@required this.constraints,
this.padding = const EdgeInsets.fromLTRB(32, 12, 20, 28),
this.arguments,
this.argumentsLabels,
this.values,
this.valuesLabels,
this.horizontalLabelsTextStyle,
this.verticalLabelsTextStyle,
PaintStyle horizontalLinesStyle = const PaintStyle(color: Colors.grey),
PaintStyle verticalLinesStyle, // null for default
this.additionalMinimalHorizontalLabelsInterval = 8,
this.additionalMinimalVerticalLablesInterval = 8,
Iterable<PaintStyle>
seriesPointsStyles, // null would use predefined set of styles
Iterable<PaintStyle> seriesLinesStyles, // null for default
}) : horizontalLinesPaint = horizontalLinesStyle?.toPaint(),
verticalLinesPaint = verticalLinesStyle?.toPaint(),
seriesPointsPaints = _prepareSeriesPointsPaints(seriesPointsStyles),
seriesLinesPaints = _prepareSeriesLinesPaints(seriesLinesStyles) {
if (seriesPointsStyles.length < values.length &&
12 /* default paints */ < values.length) {
throw "Too few `seriesPointsPaintStyle`s! Try define more or limit number of displayed series";
}
if (seriesLinesStyles.length < values.length) {
throw "Too few `seriesLinesStyles`s! Try define more or limit number of displayed series";
}
}
static Iterable<Paint> _prepareSeriesPointsPaints(
Iterable<PaintStyle> seriesPointsStyles) {
if (seriesPointsStyles == null) {
// Default paint for points
return List<Paint>.unmodifiable(<Paint>[
PaintStyle(strokeWidth: 1.7, color: Colors.blue).toPaint(),
PaintStyle(strokeWidth: 1.7, color: Colors.red).toPaint(),
PaintStyle(strokeWidth: 1.7, color: Colors.yellow).toPaint(),
PaintStyle(strokeWidth: 1.7, color: Colors.green).toPaint(),
PaintStyle(strokeWidth: 1.7, color: Colors.purple).toPaint(),
PaintStyle(strokeWidth: 1.7, color: Colors.deepOrange).toPaint(),
PaintStyle(strokeWidth: 1.7, color: Colors.brown).toPaint(),
PaintStyle(strokeWidth: 1.7, color: Colors.lime).toPaint(),
PaintStyle(strokeWidth: 1.7, color: Colors.indigo).toPaint(),
PaintStyle(strokeWidth: 1.7, color: Colors.pink).toPaint(),
PaintStyle(strokeWidth: 1.7, color: Colors.amber).toPaint(),
PaintStyle(strokeWidth: 1.7, color: Colors.teal).toPaint(),
// For more, user should specify them :F
]);
} else {
return seriesPointsStyles.map((style) => style?.toPaint()).toList();
}
}
static Iterable<Paint> _prepareSeriesLinesPaints(
Iterable<PaintStyle> seriesLinesStyles) {
if (seriesLinesStyles == null) {
return null;
} else {
return seriesLinesStyles.map((style) => style.toPaint()).toList();
}
}
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: this.constraints,
child: CustomPaint(
painter: _LineChartPainter(
padding: padding,
arguments: arguments,
argumentsLabels: argumentsLabels,
values: values,
valuesLabels: valuesLabels,
horizontalLabelsTextStyle:
horizontalLabelsTextStyle ?? Theme.of(context).textTheme.caption,
verticalLabelsTextStyle:
verticalLabelsTextStyle ?? Theme.of(context).textTheme.caption,
horizontalLinesPaint: horizontalLinesPaint,
verticalLinesPaint: verticalLinesPaint,
additionalMinimalHorizontalLabelsInterval:
additionalMinimalHorizontalLabelsInterval,
additionalMinimalVerticalLablesInterval:
additionalMinimalVerticalLablesInterval,
seriesPointsPaints: seriesPointsPaints,
seriesLinesPaints: seriesLinesPaints,
)));
}
}
class _LineChartPainter extends CustomPainter {
/// Padding around main drawng area. Necessary for displaying labels (around the chart).
final EdgeInsets padding;
/* Arguments */
/// Collection of doubles as arguments.
final Iterable<double> arguments;
/// Mappings of strings for doubles arguments, which allow to specify custom
/// strings as labels for certain arguments.
final Iterable<LabelEntry> argumentsLabels;
/* Values */
/// Collection of data series as collections of next values on corresponding arguments.
final Iterable<Iterable<double>> values;
/// Mappings of string for doubles values, which allow to specify custom
/// string as labels for certain values.
final Iterable<LabelEntry> valuesLabels;
/* Labels & lines styles */
/// Style of horizontal lines labels
final TextStyle horizontalLabelsTextStyle;
/// Style of vertical lines labels
final TextStyle verticalLabelsTextStyle;
/// Defines style of horizontal lines. Might be null in order to prevent lines from drawing.
final Paint horizontalLinesPaint;
/// Defines style of vertical lines. Might be null in order to prevent lines from drawing.
final Paint verticalLinesPaint;
// @TODO . expose it
final bool snapToLeftLabel = false;
final bool snapToTopLabel = true;
final bool snapToRightLabel = false;
final bool snapToBottomLabel = true;
/* Series points & lines styles */
/// Collection of paint styles for series values points.
///
/// On whole argument null would use predefined set of styles.
/// On collection entry null there will be no points for certain series.
final Iterable<Paint> seriesPointsPaints;
/// Collection of paint styles for lines between next series points.
///
/// On null there will be no lines.
final Iterable<Paint> seriesLinesPaints;
/* Runtime */
/// Minimal allowed interval between horizontal lines. Calculated from font size.
final double minimalHorizontalLabelsInterval;
/// Maximal value of all data series values
double maxValue = -double.maxFinite;
/// Minimal value of all data series values
double minValue = double.maxFinite;
double _minimalHorizontalRatio = 0;
double _minimalVerticalRatio = 0;
/// Creates `_LineChartPainter` (`CustomPainter`) with given data and styling.
_LineChartPainter({
this.padding = const EdgeInsets.fromLTRB(40, 8, 8, 32),
this.arguments,
this.argumentsLabels,
this.values,
this.valuesLabels,
@required this.horizontalLabelsTextStyle,
@required this.verticalLabelsTextStyle,
@required this.horizontalLinesPaint,
@required this.verticalLinesPaint,
double additionalMinimalHorizontalLabelsInterval = 8,
double additionalMinimalVerticalLablesInterval = 8,
@required this.seriesPointsPaints,
@required this.seriesLinesPaints,
}) : this.minimalHorizontalLabelsInterval =
horizontalLabelsTextStyle.fontSize +
additionalMinimalHorizontalLabelsInterval {
// Find max & min values of data to be show
for (Iterable<double> series in values) {
for (double value in series) {
if (value > maxValue) {
maxValue = value;
} else if (value < minValue) {
minValue = value;
}
}
}
if (valuesLabels != null) {
// Find minimal vertical ratio to fit all provided values labels
Iterator<LabelEntry> entry = valuesLabels.iterator;
entry.moveNext();
double lastValue = entry.current.value;
while (entry.moveNext()) {
final double goodRatio =
minimalHorizontalLabelsInterval / (entry.current.value - lastValue);
if (goodRatio > _minimalVerticalRatio) {
_minimalVerticalRatio = goodRatio;
}
lastValue = entry.current.value;
}
}
if (argumentsLabels != null) {
// Find minimal horizontal ratio to fit all provided arguments labels
Iterator<LabelEntry> entry = argumentsLabels.iterator;
entry.moveNext();
double lastValue = entry.current.value;
double lastWidth =
_getLabelTextPainter(entry.current.label, verticalLabelsTextStyle)
.width;
while (entry.moveNext()) {
final double nextValue = entry.current.value;
final double nextWidth =
_getLabelTextPainter(entry.current.label, verticalLabelsTextStyle)
.width;
final double goodRatio = ((lastWidth + nextWidth) / 2 +
additionalMinimalVerticalLablesInterval) /
(nextValue - lastValue);
if (goodRatio > _minimalHorizontalRatio) {
_minimalHorizontalRatio = goodRatio;
}
lastValue = nextValue;
lastWidth = nextWidth;
}
}
}
@override
void paint(Canvas canvas, Size size) {
final double width = size.width - padding.left - padding.right;
final double height = size.height - padding.top - padding.bottom;
/* Horizontal lines with labels */
double valuesOffset = 0; // @TODO ? could be used in future for scrolling
double verticalRatio;
{
Iterable<LabelEntry> labels;
// If no labels provided - generate them!
if (valuesLabels == null) {
final double optimalStepValue =
_calculateOptimalStepValue(maxValue - minValue, height);
int stepsNumber = 1;
// Find bottom line value
double bottomValue = 0;
if (minValue > 0) {
while (bottomValue < minValue) {
bottomValue += optimalStepValue;
}
bottomValue -= optimalStepValue;
} else {
while (bottomValue > minValue) {
bottomValue -= optimalStepValue;
}
}
valuesOffset = bottomValue;
// Find top line value
double topValue = bottomValue;
while (topValue < maxValue) {
topValue += optimalStepValue;
stepsNumber += 1;
}
// Set labels iterable from prepared generator
Iterable<LabelEntry> generator(double optimalStepValue, int stepsNumber,
[double value = 0.0]) sync* {
//double value = _bottomValue;
for (int i = 0; i < stepsNumber; i++) {
yield LabelEntry(
value,
value
.toString()); // @TODO , choose better precision based on optimal step value while parsing to string
value += optimalStepValue;
}
}
labels = generator(optimalStepValue, stepsNumber, bottomValue);
if (!snapToTopLabel) {
topValue = maxValue;
}
if (!snapToBottomLabel) {
bottomValue = valuesOffset = minValue;
}
// Calculate vertical ratio of pixels per value
// Note: There is no empty space already
verticalRatio = height / (topValue - bottomValue);
}
// If labels provided - use them
else {
// Set labels iterable as the provided list
labels = valuesLabels;
// Use minimal visible value as offset.
// Note: `minValue` is calculated in constructor and includes miniaml labels values.
valuesOffset = minValue;
// Calculate vertical ratio of pixels per value
// Note: `_minimalVerticalRatio` is calculated in constructor
final double topValue = snapToTopLabel
? math.max(maxValue, valuesLabels.last.value)
: maxValue;
final double bottomValue = snapToBottomLabel
? math.min(minValue, valuesLabels.first.value)
: minValue;
final double noEmptySpaceRatio = height / (topValue - bottomValue);
verticalRatio = math.max(_minimalVerticalRatio, noEmptySpaceRatio);
}
// Draw the horizontal lines and labels
for (LabelEntry tuple in labels) {
if (tuple.value < valuesOffset) continue;
final double yOffset = (size.height -
padding.bottom -
(tuple.value - valuesOffset) * verticalRatio);
if (yOffset < padding.top) break;
// Draw line
if (horizontalLinesPaint != null) {
canvas.drawLine(
Offset(padding.left, yOffset),
Offset(size.width - padding.right, yOffset),
horizontalLinesPaint);
}
// Draw label
TextPainter(
text: TextSpan(text: tuple.label, style: horizontalLabelsTextStyle),
textAlign: TextAlign.right,
textDirection: TextDirection.ltr)
..layout(minWidth: padding.left - 4)
..paint(canvas,
Offset(0, yOffset - horizontalLabelsTextStyle.fontSize / 2 - 1));
}
}
/* Vertical lines with labels */
double argumentsOffset = 0;
final double xOffsetLimit = size.width - padding.right;
double horizontalRatio;
{
Iterable<LabelEntry> labels;
// If no labels provided - generate them!
if (argumentsLabels == null) {
throw "not implemented";
// @TODO . after few hot days of thinking about the problem for 1-2 hour a day, I just gave up.
// The hardest in the problem is that there must be trade-off between space for labels and max lines,
// but keep in mind that the label values should be in some human-readable steps (0.5, 10, 0.02...).
}
// If labels provided - use them
else {
// Set labels iterable as the provided list
labels = argumentsLabels;
// Use first visible argument as arguments offset
argumentsOffset = argumentsLabels.first.value;
if (!snapToLeftLabel) {
argumentsOffset = arguments.first;
}
// Calculate vertical ratio of pixels per value
// Note: `_minimalHorizontalRatio` is calculated in constructor
final double leftMost = snapToLeftLabel
? math.min(arguments.first, argumentsLabels.first.value)
: arguments.first;
final double rightMost = snapToRightLabel
? math.max(arguments.last, argumentsLabels.last.value)
: arguments.last;
final double noEmptySpaceRatio = width / (rightMost - leftMost);
horizontalRatio = math.max(_minimalHorizontalRatio, noEmptySpaceRatio);
}
// Draw the vertical lines and labels
for (LabelEntry tuple in labels) {
if (tuple.value < argumentsOffset) continue;
final double xOffset =
padding.left + (tuple.value - argumentsOffset) * horizontalRatio;
if (xOffset > xOffsetLimit) break;
// Draw line
if (verticalLinesPaint != null) {
canvas.drawLine(
Offset(xOffset, padding.top),
Offset(xOffset, size.height - padding.bottom),
verticalLinesPaint);
}
// Draw label
final TextPainter textPainter = TextPainter(
text: TextSpan(text: tuple.label, style: verticalLabelsTextStyle),
textDirection: TextDirection.ltr)
..layout();
textPainter.paint(
canvas,
Offset(xOffset - textPainter.width / 2,
size.height - verticalLabelsTextStyle.fontSize - 8));
}
}
/* Points and lines between subsequent */
Iterator<Iterable<double>> series = values.iterator;
Iterator<Paint> linesPaints = seriesLinesPaints == null
? <Paint>[].iterator
: seriesLinesPaints.iterator;
Iterator<Paint> pointsPaints = seriesPointsPaints.iterator;
while (series.moveNext()) {
List<Offset> points = [];
Iterator<double> value = series.current.iterator;
Iterator<double> argument = arguments.iterator;
while (value.moveNext()) {
argument.moveNext();
if (value.current == null || value.current == double.nan) continue;
if (argument.current < argumentsOffset) continue;
final double xOffset = padding.left +
(argument.current - argumentsOffset) * horizontalRatio;
if (xOffset > xOffsetLimit) break;
if (value.current < valuesOffset) continue;
final double yOffset = size.height -
padding.bottom -
(value.current - valuesOffset) * verticalRatio;
if (yOffset < padding.top) continue;
points.add(Offset(xOffset, yOffset));
}
// Lines
if (linesPaints.moveNext() && linesPaints.current != null) {
canvas.drawPath(Path()..addPolygon(points, false), linesPaints.current);
}
// Points
if (pointsPaints.moveNext() && pointsPaints.current != null) {
canvas.drawPoints(ui.PointMode.points, points, pointsPaints.current);
}
}
}
@override
bool shouldRepaint(_LineChartPainter old) =>
(this.arguments != old.arguments ||
this.values != old.values ||
this.argumentsLabels != old.argumentsLabels ||
this.valuesLabels != old.valuesLabels ||
this.seriesPointsPaints != old.seriesPointsPaints ||
this.seriesLinesPaints != old.seriesLinesPaints ||
this.horizontalLabelsTextStyle != old.horizontalLabelsTextStyle ||
this.verticalLabelsTextStyle != old.verticalLabelsTextStyle ||
this.padding != old.padding //
);
// ..., 0.01, 0.02, 0.05, 0.1, [0.125], 0.2, [0.25], 0.5, 1, 2, 5, 10, 20, 50, 100, 200, 500, ...
double _calculateOptimalStepValue(double valueRange, double height) {
final int maxSteps = height ~/ minimalHorizontalLabelsInterval;
if (maxSteps <= 0) {
throw "invalid max lines!";
}
double interval = valueRange / maxSteps;
if (interval > 1) {
int zeros = 0;
while (interval >= 10) {
interval = interval / 10;
zeros += 1;
}
/**/ if (interval <= 1) {
interval = 1;
} else if (interval <= 2) {
interval = 2;
} else if (interval <= 5) {
interval = 5;
}
for (; zeros-- != 0;) {
interval *= 10;
}
} else {
// @TODO ! not working at all for lower :C
int zeros = 0;
while (interval < 0) {
interval = interval * 10;
zeros += 1;
}
/**/ if (interval <= 1) {
interval = 1;
} else if (interval <= 2) {
interval = 2;
} else if (interval <= 5) {
interval = 5;
}
for (; zeros-- != 0;) {
interval /= 10;
}
}
return interval;
}
TextPainter _getLabelTextPainter(String text, TextStyle style) {
return TextPainter(
text: TextSpan(text: text, style: style),
textDirection: TextDirection.ltr)
..layout();
}
}

View File

@@ -0,0 +1,263 @@
import 'dart:ui';
/// A description of the style to use when drawing on a [Canvas].
///
/// Most APIs on [Canvas] take a [Paint] object to describe the style
/// to use for that operation. [PaintStyle] allows to be const
/// constructed and later in runtime forged into the [Paint] object.
class PaintStyle {
/// Whether to apply anti-aliasing to lines and images drawn on the
/// canvas.
///
/// Defaults to true.
final bool isAntiAlias;
// Must be kept in sync with the default in paint.cc.
static const int _kColorDefault = 0xFF000000;
/// The color to use when stroking or filling a shape.
///
/// Defaults to opaque black.
///
/// See also:
///
/// * [style], which controls whether to stroke or fill (or both).
/// * [colorFilter], which overrides [color].
/// * [shader], which overrides [color] with more elaborate effects.
///
/// This color is not used when compositing. To colorize a layer, use
/// [colorFilter].
final Color color;
// Must be kept in sync with the default in paint.cc.
static final int _kBlendModeDefault = BlendMode.srcOver.index;
/// A blend mode to apply when a shape is drawn or a layer is composited.
///
/// The source colors are from the shape being drawn (e.g. from
/// [Canvas.drawPath]) or layer being composited (the graphics that were drawn
/// between the [Canvas.saveLayer] and [Canvas.restore] calls), after applying
/// the [colorFilter], if any.
///
/// The destination colors are from the background onto which the shape or
/// layer is being composited.
///
/// Defaults to [BlendMode.srcOver].
///
/// See also:
///
/// * [Canvas.saveLayer], which uses its [Paint]'s [blendMode] to composite
/// the layer when [restore] is called.
/// * [BlendMode], which discusses the user of [saveLayer] with [blendMode].
final BlendMode blendMode;
/// Whether to paint inside shapes, the edges of shapes, or both.
///
/// Defaults to [PaintingStyle.fill].
final PaintingStyle style;
/// How wide to make edges drawn when [style] is set to
/// [PaintingStyle.stroke]. The width is given in logical pixels measured in
/// the direction orthogonal to the direction of the path.
///
/// Defaults to 0.0, which correspond to a hairline width.
final double strokeWidth;
/// The kind of finish to place on the end of lines drawn when
/// [style] is set to [PaintingStyle.stroke].
///
/// Defaults to [StrokeCap.butt], i.e. no caps.
final StrokeCap strokeCap;
/// The kind of finish to place on the joins between segments.
///
/// This applies to paths drawn when [style] is set to [PaintingStyle.stroke],
/// It does not apply to points drawn as lines with [Canvas.drawPoints].
///
/// Defaults to [StrokeJoin.miter], i.e. sharp corners.
///
/// Some examples of joins:
///
/// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/miter_4_join.mp4}
///
/// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/round_join.mp4}
///
/// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/bevel_join.mp4}
///
/// The centers of the line segments are colored in the diagrams above to
/// highlight the joins, but in normal usage the join is the same color as the
/// line.
///
/// See also:
///
/// * [strokeMiterLimit] to control when miters are replaced by bevels when
/// this is set to [StrokeJoin.miter].
/// * [strokeCap] to control what is drawn at the ends of the stroke.
/// * [StrokeJoin] for the definitive list of stroke joins.
final StrokeJoin strokeJoin;
// Must be kept in sync with the default in paint.cc.
static const double _kStrokeMiterLimitDefault = 4.0;
/// The limit for miters to be drawn on segments when the join is set to
/// [StrokeJoin.miter] and the [style] is set to [PaintingStyle.stroke]. If
/// this limit is exceeded, then a [StrokeJoin.bevel] join will be drawn
/// instead. This may cause some 'popping' of the corners of a path if the
/// angle between line segments is animated, as seen in the diagrams below.
///
/// This limit is expressed as a limit on the length of the miter.
///
/// Defaults to 4.0. Using zero as a limit will cause a [StrokeJoin.bevel]
/// join to be used all the time.
///
/// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/miter_0_join.mp4}
///
/// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/miter_4_join.mp4}
///
/// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/miter_6_join.mp4}
///
/// The centers of the line segments are colored in the diagrams above to
/// highlight the joins, but in normal usage the join is the same color as the
/// line.
///
/// See also:
///
/// * [strokeJoin] to control the kind of finish to place on the joins
/// between segments.
/// * [strokeCap] to control what is drawn at the ends of the stroke.
final double strokeMiterLimit;
/// A mask filter (for example, a blur) to apply to a shape after it has been
/// drawn but before it has been composited into the image.
///
/// See [MaskFilter] for details.
final MaskFilter maskFilter;
/// Controls the performance vs quality trade-off to use when applying
/// filters, such as [maskFilter], or when drawing images, as with
/// [Canvas.drawImageRect] or [Canvas.drawImageNine].
///
/// Defaults to [FilterQuality.none].
// TODO(ianh): verify that the image drawing methods actually respect this
final FilterQuality filterQuality;
/// The shader to use when stroking or filling a shape.
///
/// When this is null, the [color] is used instead.
///
/// See also:
///
/// * [Gradient], a shader that paints a color gradient.
/// * [ImageShader], a shader that tiles an [Image].
/// * [colorFilter], which overrides [shader].
/// * [color], which is used if [shader] and [colorFilter] are null.
final Shader shader;
/// A color filter to apply when a shape is drawn or when a layer is
/// composited.
///
/// See [ColorFilter] for details.
///
/// When a shape is being drawn, [colorFilter] overrides [color] and [shader].
final ColorFilter colorFilter;
/// Whether the colors of the image are inverted when drawn.
///
/// Inverting the colors of an image applies a new color filter that will
/// be composed with any user provided color filters. This is primarily
/// used for implementing smart invert on iOS.
final bool invertColors;
const PaintStyle({
this.isAntiAlias = true,
this.color = const Color(_kColorDefault),
this.blendMode = BlendMode.srcOver,
this.style = PaintingStyle.fill,
this.strokeWidth = 0.0,
this.strokeCap = StrokeCap.butt,
this.strokeJoin = StrokeJoin.miter,
this.strokeMiterLimit = 4.0,
this.maskFilter, // null
this.filterQuality = FilterQuality.none,
this.shader, // null
this.colorFilter, // null
this.invertColors = false,
});
@override
String toString() {
final StringBuffer result = StringBuffer();
String semicolon = '';
result.write('PaintStyle(');
if (style == PaintingStyle.stroke) {
result.write('$style');
if (strokeWidth != 0.0)
result.write(' ${strokeWidth.toStringAsFixed(1)}');
else
result.write(' hairline');
if (strokeCap != StrokeCap.butt) result.write(' $strokeCap');
if (strokeJoin == StrokeJoin.miter) {
if (strokeMiterLimit != _kStrokeMiterLimitDefault)
result.write(
' $strokeJoin up to ${strokeMiterLimit.toStringAsFixed(1)}');
} else {
result.write(' $strokeJoin');
}
semicolon = '; ';
}
if (isAntiAlias != true) {
result.write('${semicolon}antialias off');
semicolon = '; ';
}
if (color != const Color(_kColorDefault)) {
if (color != null)
result.write('$semicolon$color');
else
result.write('${semicolon}no color');
semicolon = '; ';
}
if (blendMode.index != _kBlendModeDefault) {
result.write('$semicolon$blendMode');
semicolon = '; ';
}
if (colorFilter != null) {
result.write('${semicolon}colorFilter: $colorFilter');
semicolon = '; ';
}
if (maskFilter != null) {
result.write('${semicolon}maskFilter: $maskFilter');
semicolon = '; ';
}
if (filterQuality != FilterQuality.none) {
result.write('${semicolon}filterQuality: $filterQuality');
semicolon = '; ';
}
if (shader != null) {
result.write('${semicolon}shader: $shader');
semicolon = '; ';
}
if (invertColors) result.write('${semicolon}invert: $invertColors');
result.write(')');
return result.toString();
}
Paint toPaint() {
Paint paint = Paint();
if (this.isAntiAlias != true) paint.isAntiAlias = this.isAntiAlias;
if (this.color != const Color(_kColorDefault)) paint.color = this.color;
if (this.blendMode != BlendMode.srcOver) paint.blendMode = this.blendMode;
if (this.style != PaintingStyle.fill) paint.style = this.style;
if (this.strokeWidth != 0.0) paint.strokeWidth = this.strokeWidth;
if (this.strokeCap != StrokeCap.butt) paint.strokeCap = this.strokeCap;
if (this.strokeJoin != StrokeJoin.miter) paint.strokeJoin = this.strokeJoin;
if (this.strokeMiterLimit != 4.0)
paint.strokeMiterLimit = this.strokeMiterLimit;
if (this.maskFilter != null) paint.maskFilter = this.maskFilter;
if (this.filterQuality != FilterQuality.none)
paint.filterQuality = this.filterQuality;
if (this.shader != null) paint.shader = this.shader;
if (this.colorFilter != null) paint.colorFilter = this.colorFilter;
if (this.invertColors != false) paint.invertColors = this.invertColors;
return paint;
}
}

View File

@@ -0,0 +1,15 @@
//flutter_bluetooth_serial_example
//https://github.com/edufolly/flutter_bluetooth_serial/tree/master/example
import 'package:flutter/material.dart';
import './MainPage.dart';
void main() => runApp(new ExampleApplication());
class ExampleApplication extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: MainPage());
}
}

View File

@@ -0,0 +1,160 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.3"
clock:
dependency: transitive
description:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.14.12"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
fake_async:
dependency: transitive
description:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_bluetooth_serial:
dependency: "direct main"
description:
name: flutter_bluetooth_serial
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.2"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.6"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.8"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
scoped_model:
dependency: "direct main"
description:
name: scoped_model
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.3"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.15"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.6"
vector_math:
dependency: transitive
description:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
sdks:
dart: ">=2.7.0 <3.0.0"

View File

@@ -0,0 +1,78 @@
name: androidbluetoothserialapp
description: A new Flutter application.
# The following line prevents the package from being accidentally published to
# pub.dev using `pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.0.0+1
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.3
flutter_bluetooth_serial: ^0.2.2
scoped_model: ^1.0.1
dev_dependencies:
flutter_test:
sdk: flutter
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware.
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter application.">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="androidbluetoothserialapp">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="shortcut icon" type="image/png" href="favicon.png"/>
<title>androidbluetoothserialapp</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<!-- This script installs service_worker.js to provide PWA functionality to
application. For more information, see:
https://developers.google.com/web/fundamentals/primers/service-workers -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('flutter_service_worker.js');
});
}
</script>
<script src="main.dart.js" type="application/javascript"></script>
</body>
</html>

View File

@@ -0,0 +1,23 @@
{
"name": "androidbluetoothserialapp",
"short_name": "androidbluetoothserialapp",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter application.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}