commit a0f0cd5f484394534f9939391840e08729cbae2f Author: ayub Date: Sat Sep 6 18:38:40 2025 +0700 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..496ee2c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2bd425a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,27 @@ +## 0.1.7 + +- Fix null Exception at _$BluetoothDeviceFromJson and BluetoothManager.state (PR https://github.com/andrey-ushakov/flutter_bluetooth_basic/pull/26). + +## 0.1.6 + +- Null-Safety + +## 0.1.5 + +- Updated pubspec.yaml + +## 0.1.4 + +- Android: start scan bug fixed (error handling) + +## 0.1.3 + +- Updated README.md + +## 0.1.2 + +- Added Android support + +## 0.0.1 + +- iOS only support diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c8a5043 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2020 Andrey U. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..00f2639 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# flutter_bluetooth_basic + +Flutter plugin that allows to find bluetooth devices & send raw bytes data. +Supports both Android and iOS. + +Inspired by [bluetooth_print](https://github.com/thon-ju/bluetooth_print). + + +## Main Features +* Android and iOS support +* Scan for bluetooth devices +* Send raw `List bytes` data to a device + + +## Getting Started + +For a full example please check */example* folder. Here are only the most important parts of the code to illustrate how to use the library. + +```dart +BluetoothManager bluetoothManager = BluetoothManager.instance; +BluetoothDevice _device; + +bluetoothManager.startScan(timeout: Duration(seconds: 4)); +bluetoothManager.state.listen((state) { + switch (state) { + case BluetoothManager.CONNECTED: + // ... + break; + case BluetoothManager.DISCONNECTED: + // ... + break; + default: + break; + } +}); +// bluetoothManager.scanResults is a Stream> sending the found devices. + +// _device = + +await bluetoothManager.connect(_device); + +List bytes = latin1.encode('Hello world!\n').toList(); +await bluetoothManager.writeData(bytes); + +await bluetoothManager.disconnect(); +``` + +## See also +* Example of usage in a project: [esc_pos_printer](https://github.com/andrey-ushakov/esc_pos_printer) diff --git a/android/.DS_Store b/android/.DS_Store new file mode 100644 index 0000000..a0041d5 Binary files /dev/null and b/android/.DS_Store differ diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..484bcbf --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,39 @@ +group 'com.tablemi.flutter_bluetooth_basic' +version '1.0' + +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.5.0' + } +} + +rootProject.allprojects { + repositories { + google() + jcenter() + } +} + +apply plugin: 'com.android.library' + +android { + namespace 'com.tablemi.flutter_bluetooth_basic' + compileSdkVersion 28 + + defaultConfig { + minSdkVersion 21 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + } +} + +dependencies { + implementation files('libs/gprintersdkv2.jar') +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..38c8d45 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..019065d --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/android/libs/gprintersdkv2.jar b/android/libs/gprintersdkv2.jar new file mode 100644 index 0000000..4228d74 Binary files /dev/null and b/android/libs/gprintersdkv2.jar differ diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..f9dda6b --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'flutter_bluetooth_basic' diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5866eb1 --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/android/src/main/java/com/tablemi/flutter_bluetooth_basic/Constant.java b/android/src/main/java/com/tablemi/flutter_bluetooth_basic/Constant.java new file mode 100644 index 0000000..4c6af40 --- /dev/null +++ b/android/src/main/java/com/tablemi/flutter_bluetooth_basic/Constant.java @@ -0,0 +1,5 @@ +package com.tablemi.flutter_bluetooth_basic; + +public class Constant { + public static final int abnormal_Disconnection = 0x011; // Abnormal disconnection +} \ No newline at end of file diff --git a/android/src/main/java/com/tablemi/flutter_bluetooth_basic/DeviceConnFactoryManager.java b/android/src/main/java/com/tablemi/flutter_bluetooth_basic/DeviceConnFactoryManager.java new file mode 100644 index 0000000..393c2d5 --- /dev/null +++ b/android/src/main/java/com/tablemi/flutter_bluetooth_basic/DeviceConnFactoryManager.java @@ -0,0 +1,643 @@ +package com.tablemi.flutter_bluetooth_basic; + +import android.content.Context; +import android.content.Intent; +import android.hardware.usb.UsbDevice; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import com.gprinter.io.*; + +import java.io.IOException; +import java.util.Vector; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class DeviceConnFactoryManager { + + public PortManager mPort; + + private static final String TAG = DeviceConnFactoryManager.class.getSimpleName(); + + public CONN_METHOD connMethod; + + private String ip; + + private int port; + + private String macAddress; + + private UsbDevice mUsbDevice; + + private Context mContext; + + private String serialPortPath; + + private int baudrate; + + private int id; + + private static DeviceConnFactoryManager[] deviceConnFactoryManagers = new DeviceConnFactoryManager[4]; + + private boolean isOpenPort; + /** + * ESC查询打印机实时状态指令 // ESC query printer real-time status instruction + */ + private byte[] esc = {0x10, 0x04, 0x02}; + + /** + * ESC查询打印机实时状态 缺纸状态 + */ + private static final int ESC_STATE_PAPER_ERR = 0x20; + + /** + * ESC指令查询打印机实时状态 打印机开盖状态 + */ + private static final int ESC_STATE_COVER_OPEN = 0x04; + + /** + * ESC指令查询打印机实时状态 打印机报错状态 + */ + private static final int ESC_STATE_ERR_OCCURS = 0x40; + + /** + * TSC查询打印机状态指令 + */ + private byte[] tsc = {0x1b, '!', '?'}; + + /** + * TSC指令查询打印机实时状态 打印机缺纸状态 + */ + private static final int TSC_STATE_PAPER_ERR = 0x04; + + /** + * TSC指令查询打印机实时状态 打印机开盖状态 + */ + private static final int TSC_STATE_COVER_OPEN = 0x01; + + /** + * TSC指令查询打印机实时状态 打印机出错状态 + */ + private static final int TSC_STATE_ERR_OCCURS = 0x80; + + private byte[] cpcl={0x1b,0x68}; + + /** + * CPCL指令查询打印机实时状态 打印机缺纸状态 + */ + private static final int CPCL_STATE_PAPER_ERR = 0x01; + /** + * CPCL指令查询打印机实时状态 打印机开盖状态 + */ + private static final int CPCL_STATE_COVER_OPEN = 0x02; + + private byte[] sendCommand; + /** + * 判断打印机所使用指令是否是ESC指令 + */ + private PrinterCommand currentPrinterCommand; + public static final byte FLAG = 0x10; + private static final int READ_DATA = 10000; + private static final int DEFAUIT_COMMAND=20000; + private static final String READ_DATA_CNT = "read_data_cnt"; + private static final String READ_BUFFER_ARRAY = "read_buffer_array"; + public static final String ACTION_CONN_STATE = "action_connect_state"; + public static final String ACTION_QUERY_PRINTER_STATE = "action_query_printer_state"; + public static final String STATE = "state"; + public static final String DEVICE_ID = "id"; + public static final int CONN_STATE_DISCONNECT = 0x90; + public static final int CONN_STATE_CONNECTING = CONN_STATE_DISCONNECT << 1; + public static final int CONN_STATE_FAILED = CONN_STATE_DISCONNECT << 2; + public static final int CONN_STATE_CONNECTED = CONN_STATE_DISCONNECT << 3; + public PrinterReader reader; + private int queryPrinterCommandFlag; + private final int ESC = 1; + private final int TSC = 3; + private final int CPCL = 2; + public enum CONN_METHOD { + //蓝牙连接 + BLUETOOTH("BLUETOOTH"), + //USB连接 + USB("USB"), + //wifi连接 + WIFI("WIFI"), + //串口连接 + SERIAL_PORT("SERIAL_PORT"); + + private String name; + + private CONN_METHOD(String name) { + this.name = name; + } + + @Override + public String toString() { + return this.name; + } + } + + public static DeviceConnFactoryManager[] getDeviceConnFactoryManagers() { + return deviceConnFactoryManagers; + } + + /** + * 打开端口 + * + * @return + */ + public void openPort() { + deviceConnFactoryManagers[id].isOpenPort = false; + + switch (deviceConnFactoryManagers[id].connMethod) { + case BLUETOOTH: + mPort = new BluetoothPort(macAddress); + isOpenPort = deviceConnFactoryManagers[id].mPort.openPort(); + break; + case USB: + mPort = new UsbPort(mContext, mUsbDevice); + isOpenPort = mPort.openPort(); + break; + case WIFI: + mPort = new EthernetPort(ip, port); + isOpenPort = mPort.openPort(); + break; + case SERIAL_PORT: + mPort = new SerialPort(serialPortPath, baudrate, 0); + isOpenPort = mPort.openPort(); + break; + default: + break; + } + + //端口打开成功后,检查连接打印机所使用的打印机指令ESC、TSC + if (isOpenPort) { + queryCommand(); + } else { + if (this.mPort != null) { + this.mPort=null; + } + + } + } + + /** + * 查询当前连接打印机所使用打印机指令(ESC(EscCommand.java)、TSC(LabelCommand.java)) + */ + private void queryCommand() { + //开启读取打印机返回数据线程 + reader = new PrinterReader(); + reader.start(); //读取数据线程 + //查询打印机所使用指令 + queryPrinterCommand(); //小票机连接不上 注释这行,添加下面那三行代码。使用ESC指令 + + } + + /** + * 获取端口连接方式 + * + * @return + */ + public CONN_METHOD getConnMethod() { + return connMethod; + } + + /** + * Get port open status (true open, false not open) + * + * @return + */ + public boolean getConnState() { + return isOpenPort; + } + + /** + * 获取连接蓝牙的物理地址 + * + * @return + */ + public String getMacAddress() { + return macAddress; + } + + /** + * 获取连接网口端口号 + * + * @return + */ + public int getPort() { + return port; + } + + /** + * 获取连接网口的IP + * + * @return + */ + public String getIp() { + return ip; + } + + /** + * 获取连接的USB设备信息 + * + * @return + */ + public UsbDevice usbDevice() { + return mUsbDevice; + } + + /** + * 关闭端口 + */ + public void closePort(int id) { + if (this.mPort != null) { + if(reader!=null) { + reader.cancel(); + reader = null; + } + boolean b= this.mPort.closePort(); + if(b) { + this.mPort=null; + isOpenPort = false; + currentPrinterCommand = null; + } + } + + } + + /** + * 获取串口号 + * + * @return + */ + public String getSerialPortPath() { + return serialPortPath; + } + + /** + * 获取波特率 + * + * @return + */ + public int getBaudrate() { + return baudrate; + } + + public static void closeAllPort() { + for (DeviceConnFactoryManager deviceConnFactoryManager : deviceConnFactoryManagers) { + if (deviceConnFactoryManager != null) { + Log.e(TAG, "cloaseAllPort() id -> " + deviceConnFactoryManager.id); + deviceConnFactoryManager.closePort(deviceConnFactoryManager.id); + deviceConnFactoryManagers[deviceConnFactoryManager.id] = null; + } + } + } + + private DeviceConnFactoryManager(Build build) { + this.connMethod = build.connMethod; + this.macAddress = build.macAddress; + this.port = build.port; + this.ip = build.ip; + this.mUsbDevice = build.usbDevice; + this.mContext = build.context; + this.serialPortPath = build.serialPortPath; + this.baudrate = build.baudrate; + this.id = build.id; + deviceConnFactoryManagers[id] = this; + } + + /** + * 获取当前打印机指令 + * + * @return PrinterCommand + */ + public PrinterCommand getCurrentPrinterCommand() { + return deviceConnFactoryManagers[id].currentPrinterCommand; + } + + public static final class Build { + private String ip; + private String macAddress; + private UsbDevice usbDevice; + private int port; + private CONN_METHOD connMethod; + private Context context; + private String serialPortPath; + private int baudrate; + private int id; + + public DeviceConnFactoryManager.Build setIp(String ip) { + this.ip = ip; + return this; + } + + public DeviceConnFactoryManager.Build setMacAddress(String macAddress) { + this.macAddress = macAddress; + return this; + } + + public DeviceConnFactoryManager.Build setUsbDevice(UsbDevice usbDevice) { + this.usbDevice = usbDevice; + return this; + } + + public DeviceConnFactoryManager.Build setPort(int port) { + this.port = port; + return this; + } + + public DeviceConnFactoryManager.Build setConnMethod(CONN_METHOD connMethod) { + this.connMethod = connMethod; + return this; + } + + public DeviceConnFactoryManager.Build setContext(Context context) { + this.context = context; + return this; + } + + public DeviceConnFactoryManager.Build setId(int id) { + this.id = id; + return this; + } + + public DeviceConnFactoryManager.Build setSerialPort(String serialPortPath) { + this.serialPortPath = serialPortPath; + return this; + } + + public DeviceConnFactoryManager.Build setBaudrate(int baudrate) { + this.baudrate = baudrate; + return this; + } + + public DeviceConnFactoryManager build() { + return new DeviceConnFactoryManager(this); + } + } + + public void sendDataImmediately(final Vector data) { + if (this.mPort == null) { + return; + } + try { + this.mPort.writeDataImmediately(data, 0, data.size()); + } catch (Exception e) { + // Abort Send + mHandler.obtainMessage(Constant.abnormal_Disconnection).sendToTarget(); + e.printStackTrace(); + + } + } + public void sendByteDataImmediately(final byte [] data) { + if (this.mPort == null) { + return; + }else { + Vector datas = new Vector<>(); + for(int i = 0; i < data.length; ++i) { + datas.add(Byte.valueOf(data[i])); + } + try { + this.mPort.writeDataImmediately(datas, 0, datas.size()); + } catch (IOException e) { + // Abort Send + e.printStackTrace(); + mHandler.obtainMessage(Constant.abnormal_Disconnection).sendToTarget(); + } + } + } + public int readDataImmediately(byte[] buffer){ + int r = 0; + if (this.mPort == null) { + return r; + } + + try { + r = this.mPort.readData(buffer); + } catch (IOException e) { + + } + + return r; + } + + /** + * 查询打印机当前使用的指令(ESC、CPCL、TSC、) + */ + private void queryPrinterCommand() { + queryPrinterCommandFlag = ESC; + ThreadPool.getInstantiation().addSerialTask(new Runnable() { + @Override + public void run() { + //开启计时器,隔2000毫秒没有没返回值时发送查询打印机状态指令,先发票据,面单,标签 + final ThreadFactoryBuilder threadFactoryBuilder = new ThreadFactoryBuilder("Timer"); + final ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1, threadFactoryBuilder); + scheduledExecutorService.scheduleAtFixedRate(threadFactoryBuilder.newThread(new Runnable() { + @Override + public void run() { + if (currentPrinterCommand == null && queryPrinterCommandFlag > TSC) { + if (reader != null) {//三种状态,查询无返回值,发送连接失败广播 + reader.cancel(); + mPort.closePort(); + isOpenPort = false; + + scheduledExecutorService.shutdown(); + } + } + if (currentPrinterCommand != null) { + if (scheduledExecutorService != null && !scheduledExecutorService.isShutdown()) { + scheduledExecutorService.shutdown(); + } + return; + } + switch (queryPrinterCommandFlag) { + case ESC: + //发送ESC查询打印机状态指令 + sendCommand = esc; + break; + case TSC: + //发送ESC查询打印机状态指令 + sendCommand = tsc; + break; + case CPCL: + //发送CPCL查询打印机状态指令 + sendCommand = cpcl; + break; + default: + break; + } + Vector data = new Vector<>(sendCommand.length); + for (int i = 0; i < sendCommand.length; i++) { + data.add(sendCommand[i]); + } + sendDataImmediately(data); + queryPrinterCommandFlag++; + } + }), 1500, 1500, TimeUnit.MILLISECONDS); + } + }); + } + + class PrinterReader extends Thread { + private boolean isRun = false; + + private byte[] buffer = new byte[100]; + + public PrinterReader() { + isRun = true; + } + + @Override + public void run() { + try { + while (isRun) { + //读取打印机返回信息,打印机没有返回纸返回-1 + Log.e(TAG,"wait read "); + int len = readDataImmediately(buffer); + Log.e(TAG," read "+len); + if (len > 0) { + Message message = Message.obtain(); + message.what = READ_DATA; + Bundle bundle = new Bundle(); + bundle.putInt(READ_DATA_CNT, len); //数据长度 + bundle.putByteArray(READ_BUFFER_ARRAY, buffer); //数据 + message.setData(bundle); + mHandler.sendMessage(message); + } + } + } catch (Exception e) {//异常断开 + if (deviceConnFactoryManagers[id] != null) { + closePort(id); + mHandler.obtainMessage(Constant.abnormal_Disconnection).sendToTarget(); + } + } + } + + public void cancel() { + isRun = false; + } + } + + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case Constant.abnormal_Disconnection://异常断开连接 + Log.d(TAG, "abnormal disconnection"); + sendStateBroadcast(Constant.abnormal_Disconnection); + break; + case DEFAUIT_COMMAND://默认模式 + + break; + case READ_DATA: + int cnt = msg.getData().getInt(READ_DATA_CNT); //数据长度 >0; + byte[] buffer = msg.getData().getByteArray(READ_BUFFER_ARRAY); //数据 + //这里只对查询状态返回值做处理,其它返回值可参考编程手册来解析 + if (buffer == null) { + return; + } + int result = judgeResponseType(buffer[0]); //数据右移 + String status = ""; + if (sendCommand == esc) { + //设置当前打印机模式为ESC模式 + if (currentPrinterCommand == null) { + currentPrinterCommand = PrinterCommand.ESC; + sendStateBroadcast(CONN_STATE_CONNECTED); + } else {//查询打印机状态 + if (result == 0) {//打印机状态查询 + Intent intent = new Intent(ACTION_QUERY_PRINTER_STATE); + intent.putExtra(DEVICE_ID, id); + if(mContext!=null){ + mContext.sendBroadcast(intent); + } + } else if (result == 1) {//查询打印机实时状态 + if ((buffer[0] & ESC_STATE_PAPER_ERR) > 0) { + status += " Printer out of paper"; + } + if ((buffer[0] & ESC_STATE_COVER_OPEN) > 0) { + status += " Printer open cover"; + } + if ((buffer[0] & ESC_STATE_ERR_OCCURS) > 0) { + status += " Printer error"; + } + Log.d(TAG, status); + } + } + }else if (sendCommand == tsc) { + //设置当前打印机模式为TSC模式 + if (currentPrinterCommand == null) { + currentPrinterCommand = PrinterCommand.TSC; + sendStateBroadcast(CONN_STATE_CONNECTED); + } else { + if (cnt == 1) {//查询打印机实时状态 + if ((buffer[0] & TSC_STATE_PAPER_ERR) > 0) {//缺纸 + status += " Printer out of paper"; + } + if ((buffer[0] & TSC_STATE_COVER_OPEN) > 0) {//开盖 + status += " Printer open cover"; + } + if ((buffer[0] & TSC_STATE_ERR_OCCURS) > 0) {//打印机报错 + status += " Printer error"; + } + Log.d(TAG, status); + } else {//打印机状态查询 + Intent intent = new Intent(ACTION_QUERY_PRINTER_STATE); + intent.putExtra(DEVICE_ID, id); + if(mContext!=null){ + mContext.sendBroadcast(intent); + } + } + } + }else if(sendCommand==cpcl){ + if (currentPrinterCommand == null) { + currentPrinterCommand = PrinterCommand.CPCL; + sendStateBroadcast(CONN_STATE_CONNECTED); + }else { + if (cnt == 1) { + + if ((buffer[0] ==CPCL_STATE_PAPER_ERR)) {//缺纸 + status += " Printer out of paper"; + } + if ((buffer[0] ==CPCL_STATE_COVER_OPEN)) {//开盖 + status += " Printer open cover"; + } + Log.d(TAG, status); + } else {//打印机状态查询 + Intent intent = new Intent(ACTION_QUERY_PRINTER_STATE); + intent.putExtra(DEVICE_ID, id); + if(mContext!=null){ + mContext.sendBroadcast(intent); + } + } + } + } + break; + default: + break; + } + } + }; + + /** + * 发送广播 + * @param state + */ + private void sendStateBroadcast(int state) { + Intent intent = new Intent(ACTION_CONN_STATE); + intent.putExtra(STATE, state); + intent.putExtra(DEVICE_ID, id); + if(mContext != null){ + mContext.sendBroadcast(intent);//此处若报空指针错误,需要在清单文件application标签里注册此类,参考demo + } + } + + /** + * 判断是实时状态(10 04 02)还是查询状态(1D 72 01) + */ + private int judgeResponseType(byte r) { + return (byte) ((r & FLAG) >> 4); + } + +} \ No newline at end of file diff --git a/android/src/main/java/com/tablemi/flutter_bluetooth_basic/FlutterBluetoothBasicPlugin.java b/android/src/main/java/com/tablemi/flutter_bluetooth_basic/FlutterBluetoothBasicPlugin.java new file mode 100644 index 0000000..3cf46fa --- /dev/null +++ b/android/src/main/java/com/tablemi/flutter_bluetooth_basic/FlutterBluetoothBasicPlugin.java @@ -0,0 +1,318 @@ +package com.tablemi.flutter_bluetooth_basic; + +import android.Manifest; +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothManager; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanResult; +import android.bluetooth.le.ScanSettings; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.util.Log; + +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Vector; + +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.PluginRegistry; + +public class FlutterBluetoothBasicPlugin implements + FlutterPlugin, + MethodChannel.MethodCallHandler, + PluginRegistry.RequestPermissionsResultListener, + ActivityAware { + + private static final String TAG = "BluetoothBasicPlugin"; + private static final String NAMESPACE = "flutter_bluetooth_basic"; + private static final int REQUEST_COARSE_LOCATION_PERMISSIONS = 1451; + + private MethodChannel channel; + private EventChannel stateChannel; + + private Context context; + private Activity activity; + + private BluetoothManager mBluetoothManager; + private BluetoothAdapter mBluetoothAdapter; + private ThreadPool threadPool; + + private MethodCall pendingCall; + private MethodChannel.Result pendingResult; + + private int id = 0; + + @Override + public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) { + context = flutterPluginBinding.getApplicationContext(); + channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), NAMESPACE + "/methods"); + stateChannel = new EventChannel(flutterPluginBinding.getBinaryMessenger(), NAMESPACE + "/state"); + channel.setMethodCallHandler(this); + } + + @Override + public void onDetachedFromEngine(FlutterPluginBinding binding) { + channel.setMethodCallHandler(null); + channel = null; + stateChannel.setStreamHandler(null); + stateChannel = null; + } + + @Override + public void onAttachedToActivity(ActivityPluginBinding binding) { + this.activity = binding.getActivity(); + binding.addRequestPermissionsResultListener(this); + + mBluetoothManager = (BluetoothManager) activity.getSystemService(Context.BLUETOOTH_SERVICE); + mBluetoothAdapter = mBluetoothManager.getAdapter(); + + if (stateChannel != null) { + stateChannel.setStreamHandler(stateStreamHandler); + } + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + activity = null; + } + + @Override + public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { + onAttachedToActivity(binding); + } + + @Override + public void onDetachedFromActivity() { + activity = null; + } + + @Override + public void onMethodCall(MethodCall call, MethodChannel.Result result) { + if (mBluetoothAdapter == null && !"isAvailable".equals(call.method)) { + result.error("bluetooth_unavailable", "Bluetooth is unavailable", null); + return; + } + + final Map args = call.arguments(); + + switch (call.method) { + case "state": + state(result); + break; + case "isAvailable": + result.success(mBluetoothAdapter != null); + break; + case "isOn": + result.success(mBluetoothAdapter.isEnabled()); + break; + case "isConnected": + result.success(threadPool != null); + break; + case "startScan": + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_COARSE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions( + activity, + new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, + REQUEST_COARSE_LOCATION_PERMISSIONS); + pendingCall = call; + pendingResult = result; + break; + } + startScan(call, result); + break; + case "stopScan": + stopScan(); + result.success(null); + break; + case "connect": + connect(result, args); + break; + case "disconnect": + result.success(disconnect()); + break; + case "destroy": + result.success(destroy()); + break; + case "writeData": + writeData(result, args); + break; + default: + result.notImplemented(); + break; + } + } + + private void startScan(MethodCall call, MethodChannel.Result result) { + try { + startScan(); + result.success(null); + } catch (Exception e) { + result.error("startScan", e.getMessage(), null); + } + } + + private void state(MethodChannel.Result result) { + try { + result.success(mBluetoothAdapter.getState()); + } catch (SecurityException e) { + result.error("invalid_argument", "Argument 'address' not found", null); + } + } + + private void startScan() throws IllegalStateException { + BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner(); + if (scanner == null) + throw new IllegalStateException("getBluetoothLeScanner() is null. Is the Adapter on?"); + + ScanSettings settings = new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(); + scanner.startScan(null, settings, mScanCallback); + } + + private void stopScan() { + BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner(); + if (scanner != null) scanner.stopScan(mScanCallback); + } + + private final ScanCallback mScanCallback = new ScanCallback() { + @Override + public void onScanResult(int callbackType, ScanResult result) { + BluetoothDevice device = result.getDevice(); + if (device != null && device.getName() != null) { + invokeMethodUIThread("ScanResult", device); + } + } + }; + + private void invokeMethodUIThread(final String name, final BluetoothDevice device) { + final Map ret = new HashMap<>(); + ret.put("address", device.getAddress()); + ret.put("name", device.getName()); + ret.put("type", device.getType()); + + activity.runOnUiThread(() -> channel.invokeMethod(name, ret)); + } + + private void connect(MethodChannel.Result result, Map args) { + if (args.containsKey("address")) { + String address = (String) args.get("address"); + disconnect(); + + new DeviceConnFactoryManager.Build() + .setId(id) + .setConnMethod(DeviceConnFactoryManager.CONN_METHOD.BLUETOOTH) + .setMacAddress(address) + .build(); + + threadPool = ThreadPool.getInstantiation(); + threadPool.addSerialTask(() -> DeviceConnFactoryManager.getDeviceConnFactoryManagers()[id].openPort()); + + result.success(true); + } else { + result.error("invalid_argument", "Argument 'address' not found", null); + } + } + + private boolean disconnect() { + if (DeviceConnFactoryManager.getDeviceConnFactoryManagers()[id] != null && + DeviceConnFactoryManager.getDeviceConnFactoryManagers()[id].mPort != null) { + DeviceConnFactoryManager.getDeviceConnFactoryManagers()[id].reader.cancel(); + DeviceConnFactoryManager.getDeviceConnFactoryManagers()[id].mPort.closePort(); + DeviceConnFactoryManager.getDeviceConnFactoryManagers()[id].mPort = null; + } + return true; + } + + private boolean destroy() { + DeviceConnFactoryManager.closeAllPort(); + if (threadPool != null) { + threadPool.stopThreadPool(); + } + return true; + } + + private void writeData(MethodChannel.Result result, Map args) { + if (args.containsKey("bytes")) { + final ArrayList bytes = (ArrayList) args.get("bytes"); + + threadPool = ThreadPool.getInstantiation(); + threadPool.addSerialTask(() -> { + Vector vectorData = new Vector<>(); + for (Integer val : bytes) { + vectorData.add(Byte.valueOf(Integer.toString(val > 127 ? val - 256 : val))); + } + DeviceConnFactoryManager.getDeviceConnFactoryManagers()[id].sendDataImmediately(vectorData); + }); + } else { + result.error("bytes_empty", "Bytes param is empty", null); + } + } + + @Override + public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == REQUEST_COARSE_LOCATION_PERMISSIONS) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + startScan(pendingCall, pendingResult); + } else { + pendingResult.error("no_permissions", "This app requires location permissions for scanning", null); + pendingResult = null; + } + return true; + } + return false; + } + + private final EventChannel.StreamHandler stateStreamHandler = new EventChannel.StreamHandler() { + private EventChannel.EventSink sink; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + Log.d(TAG, "stateStreamHandler, current action: " + action); + + if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) { + threadPool = null; + sink.success(intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)); + } else if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action)) { + sink.success(1); + } else if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(action)) { + threadPool = null; + sink.success(0); + } + } + }; + + @Override + public void onListen(Object o, EventChannel.EventSink eventSink) { + sink = eventSink; + IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); + filter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED); + filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); + filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); + activity.registerReceiver(mReceiver, filter); + } + + @Override + public void onCancel(Object o) { + sink = null; + activity.unregisterReceiver(mReceiver); + } + }; +} diff --git a/android/src/main/java/com/tablemi/flutter_bluetooth_basic/PrinterCommand.java b/android/src/main/java/com/tablemi/flutter_bluetooth_basic/PrinterCommand.java new file mode 100644 index 0000000..e65d61f --- /dev/null +++ b/android/src/main/java/com/tablemi/flutter_bluetooth_basic/PrinterCommand.java @@ -0,0 +1,7 @@ +package com.tablemi.flutter_bluetooth_basic; + +public enum PrinterCommand { + ESC, + TSC, + CPCL +} diff --git a/android/src/main/java/com/tablemi/flutter_bluetooth_basic/ThreadFactoryBuilder.java b/android/src/main/java/com/tablemi/flutter_bluetooth_basic/ThreadFactoryBuilder.java new file mode 100644 index 0000000..a5e95e2 --- /dev/null +++ b/android/src/main/java/com/tablemi/flutter_bluetooth_basic/ThreadFactoryBuilder.java @@ -0,0 +1,21 @@ +package com.tablemi.flutter_bluetooth_basic; + +import java.util.concurrent.ThreadFactory; + +public class ThreadFactoryBuilder implements ThreadFactory { + + private String name; + private int counter; + + public ThreadFactoryBuilder(String name) { + this.name = name; + counter = 1; + } + + @Override + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable, name); + thread.setName("ThreadFactoryBuilder_" + name + "_" + counter); + return thread; + } +} diff --git a/android/src/main/java/com/tablemi/flutter_bluetooth_basic/ThreadPool.java b/android/src/main/java/com/tablemi/flutter_bluetooth_basic/ThreadPool.java new file mode 100644 index 0000000..45eb2fb --- /dev/null +++ b/android/src/main/java/com/tablemi/flutter_bluetooth_basic/ThreadPool.java @@ -0,0 +1,102 @@ +package com.tablemi.flutter_bluetooth_basic; + +import android.util.Log; + +import java.util.ArrayDeque; +import java.util.concurrent.*; + + +public class ThreadPool { + + private Runnable mActive; + + private static ThreadPool threadPool; + /** + * java线程池 + */ + private ThreadPoolExecutor threadPoolExecutor; + + /** + * 系统最大可用线程 + */ + private final static int CPU_AVAILABLE = Runtime.getRuntime().availableProcessors(); + + /** + * 最大线程数 + */ + private final static int MAX_POOL_COUNTS = CPU_AVAILABLE * 2 + 1; + + /** + * 线程存活时间 + */ + private final static long AVAILABLE = 1L; + + /** + * 核心线程数 + */ + private final static int CORE_POOL_SIZE = CPU_AVAILABLE + 1; + + /** + * 线程池缓存队列 + */ + private BlockingQueue mWorkQueue = new ArrayBlockingQueue<>(CORE_POOL_SIZE); + + private ArrayDeque mArrayDeque = new ArrayDeque<>(); + + private ThreadFactory threadFactory = new ThreadFactoryBuilder("ThreadPool"); + + private ThreadPool() { + threadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_COUNTS, AVAILABLE, TimeUnit.SECONDS, mWorkQueue,threadFactory); + } + + public static ThreadPool getInstantiation() { + if (threadPool == null) { + threadPool = new ThreadPool(); + } + return threadPool; + } + + public void addParallelTask(Runnable runnable) { //并行线程 + if (runnable == null) { + throw new NullPointerException("addTask(Runnable runnable)传入参数为空"); + } + if (threadPoolExecutor.getActiveCount() + 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.tablemi.flutter_bluetooth_basic_example" + minSdkVersion 21 + targetSdkVersion 28 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..8feb5af --- /dev/null +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c32e045 --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/example/android/app/src/main/java/com/tablemi/flutter_bluetooth_basic_example/MainActivity.java b/example/android/app/src/main/java/com/tablemi/flutter_bluetooth_basic_example/MainActivity.java new file mode 100644 index 0000000..87aa346 --- /dev/null +++ b/example/android/app/src/main/java/com/tablemi/flutter_bluetooth_basic_example/MainActivity.java @@ -0,0 +1,13 @@ +package com.tablemi.flutter_bluetooth_basic_example; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.plugins.GeneratedPluginRegistrant; + +public class MainActivity extends FlutterActivity { + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + GeneratedPluginRegistrant.registerWith(flutterEngine); + } +} diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..00fa441 --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..8feb5af --- /dev/null +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle new file mode 100644 index 0000000..6de3728 --- /dev/null +++ b/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.5.3' + } +} + +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 +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties new file mode 100644 index 0000000..38c8d45 --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true +android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..296b146 --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/example/android/local.properties b/example/android/local.properties new file mode 100644 index 0000000..37da947 --- /dev/null +++ b/example/android/local.properties @@ -0,0 +1,2 @@ +sdk.dir=/Users/andrey/Library/Android/sdk +flutter.sdk=/Users/andrey/development/flutter \ No newline at end of file diff --git a/example/android/settings.gradle b/example/android/settings.gradle new file mode 100644 index 0000000..5a2f14f --- /dev/null +++ b/example/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..6b4c0f7 --- /dev/null +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 8.0 + + diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..e8efba1 --- /dev/null +++ b/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Flutter/Generated.xcconfig b/example/ios/Flutter/Generated.xcconfig new file mode 100644 index 0000000..1d09309 --- /dev/null +++ b/example/ios/Flutter/Generated.xcconfig @@ -0,0 +1,13 @@ +// This is a generated file; do not edit or check into version control. +FLUTTER_ROOT=/Users/andrey/development/flutter +FLUTTER_APPLICATION_PATH=/Users/andrey/code/projects/flutter/flutter_bluetooth_basic/example +COCOAPODS_PARALLEL_CODE_SIGN=true +FLUTTER_TARGET=lib/main.dart +FLUTTER_BUILD_DIR=build +FLUTTER_BUILD_NAME=1.0.0 +FLUTTER_BUILD_NUMBER=1 +EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 +DART_OBFUSCATION=false +TRACK_WIDGET_CREATION=false +TREE_SHAKE_ICONS=false +PACKAGE_CONFIG=.packages diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..399e934 --- /dev/null +++ b/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Flutter/flutter_export_environment.sh b/example/ios/Flutter/flutter_export_environment.sh new file mode 100755 index 0000000..2cdb2e3 --- /dev/null +++ b/example/ios/Flutter/flutter_export_environment.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# This is a generated file; do not edit or check into version control. +export "FLUTTER_ROOT=/Users/andrey/development/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/andrey/code/projects/flutter/flutter_bluetooth_basic/example" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" +export "FLUTTER_TARGET=lib/main.dart" +export "FLUTTER_BUILD_DIR=build" +export "FLUTTER_BUILD_NAME=1.0.0" +export "FLUTTER_BUILD_NUMBER=1" +export "DART_OBFUSCATION=false" +export "TRACK_WIDGET_CREATION=false" +export "TREE_SHAKE_ICONS=false" +export "PACKAGE_CONFIG=.packages" diff --git a/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 0000000..98a90b8 --- /dev/null +++ b/example/ios/Podfile @@ -0,0 +1,87 @@ +# 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 + +# Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. +install! 'cocoapods', :disable_input_output_paths => true + +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 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock new file mode 100644 index 0000000..7c40d67 --- /dev/null +++ b/example/ios/Podfile.lock @@ -0,0 +1,28 @@ +PODS: + - Flutter (1.0.0) + - flutter_bluetooth_basic (0.0.1): + - Flutter + - path_provider (0.0.1): + - Flutter + +DEPENDENCIES: + - Flutter (from `Flutter`) + - flutter_bluetooth_basic (from `.symlinks/plugins/flutter_bluetooth_basic/ios`) + - path_provider (from `.symlinks/plugins/path_provider/ios`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + flutter_bluetooth_basic: + :path: ".symlinks/plugins/flutter_bluetooth_basic/ios" + path_provider: + :path: ".symlinks/plugins/path_provider/ios" + +SPEC CHECKSUMS: + Flutter: 0e3d915762c693b495b44d77113d4970485de6ec + flutter_bluetooth_basic: 0e4e27e22b50b3a25cc1d1e131953feb4af414f4 + path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d + +PODFILE CHECKSUM: 3dbe063e9c90a5d7c9e4e76e70a821b9e2c1d271 + +COCOAPODS: 1.8.4 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..eea1368 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,579 @@ +// !$*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 */; }; + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 92AC2DEF0FE1AD4381FF46F0 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EBFC51A13E362D761AD92AB2 /* libPods-Runner.a */; }; + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 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 = ( + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 89FC27FEE05299E830AC2A76 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; + 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 = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B365B4D6871B56C59378C336 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + EBFC51A13E362D761AD92AB2 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F3316B1877E9B2F711FD6F98 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, + 92AC2DEF0FE1AD4381FF46F0 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 230AA67EDFEF05D8734536AD /* Pods */ = { + isa = PBXGroup; + children = ( + F3316B1877E9B2F711FD6F98 /* Pods-Runner.debug.xcconfig */, + 89FC27FEE05299E830AC2A76 /* Pods-Runner.release.xcconfig */, + B365B4D6871B56C59378C336 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 4BC1EA12D25798CAE0ED6C20 /* Frameworks */ = { + isa = PBXGroup; + children = ( + EBFC51A13E362D761AD92AB2 /* libPods-Runner.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B80C3931E831B6300D905FE /* App.framework */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEBA1CF902C7004384FC /* Flutter.framework */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 230AA67EDFEF05D8734536AD /* Pods */, + 4BC1EA12D25798CAE0ED6C20 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 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 = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + D8BFF256A8E2C306C228A524 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 224C8CE2EFE4315A68DB0D58 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = "The Chromium Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = 8YA3WTV6AF; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + 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 */ + 224C8CE2EFE4315A68DB0D58 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + 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\" 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"; + }; + D8BFF256A8E2C306C228A524 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* 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 = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + 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)"; + DEVELOPMENT_TEAM = 8YA3WTV6AF; + 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.tablemi.flutterBluetoothBasicExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + 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; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + 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)"; + DEVELOPMENT_TEAM = 8YA3WTV6AF; + 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.tablemi.flutterBluetoothBasicExample; + 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)"; + DEVELOPMENT_TEAM = 8YA3WTV6AF; + 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.tablemi.flutterBluetoothBasicExample; + 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 */; +} diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..ad6250a --- /dev/null +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner/AppDelegate.h b/example/ios/Runner/AppDelegate.h new file mode 100644 index 0000000..36e21bb --- /dev/null +++ b/example/ios/Runner/AppDelegate.h @@ -0,0 +1,6 @@ +#import +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/example/ios/Runner/AppDelegate.m b/example/ios/Runner/AppDelegate.m new file mode 100644 index 0000000..70e8393 --- /dev/null +++ b/example/ios/Runner/AppDelegate.m @@ -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 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -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" + } +} diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -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. \ No newline at end of file diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/GeneratedPluginRegistrant.h b/example/ios/Runner/GeneratedPluginRegistrant.h new file mode 100644 index 0000000..7a89092 --- /dev/null +++ b/example/ios/Runner/GeneratedPluginRegistrant.h @@ -0,0 +1,19 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GeneratedPluginRegistrant_h +#define GeneratedPluginRegistrant_h + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface GeneratedPluginRegistrant : NSObject ++ (void)registerWithRegistry:(NSObject*)registry; +@end + +NS_ASSUME_NONNULL_END +#endif /* GeneratedPluginRegistrant_h */ diff --git a/example/ios/Runner/GeneratedPluginRegistrant.m b/example/ios/Runner/GeneratedPluginRegistrant.m new file mode 100644 index 0000000..18b7c60 --- /dev/null +++ b/example/ios/Runner/GeneratedPluginRegistrant.m @@ -0,0 +1,28 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#import "GeneratedPluginRegistrant.h" + +#if __has_include() +#import +#else +@import flutter_bluetooth_basic; +#endif + +#if __has_include() +#import +#else +@import path_provider; +#endif + +@implementation GeneratedPluginRegistrant + ++ (void)registerWithRegistry:(NSObject*)registry { + [FlutterBluetoothBasicPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterBluetoothBasicPlugin"]]; + [FLTPathProviderPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTPathProviderPlugin"]]; +} + +@end diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist new file mode 100644 index 0000000..85786a8 --- /dev/null +++ b/example/ios/Runner/Info.plist @@ -0,0 +1,54 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + flutter_bluetooth_basic_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + NSBluetoothAlwaysUsageDescription + Allow App use bluetooth? + NSBluetoothPeripheralUsageDescription + Allow App use bluetooth? + UIBackgroundModes + + bluetooth-central + bluetooth-peripheral + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/example/ios/Runner/main.m b/example/ios/Runner/main.m new file mode 100644 index 0000000..dff6597 --- /dev/null +++ b/example/ios/Runner/main.m @@ -0,0 +1,9 @@ +#import +#import +#import "AppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..36e2042 --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,202 @@ +import 'dart:convert'; +// import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bluetooth_basic/flutter_bluetooth_basic.dart'; + +void main() => runApp(MyApp()); + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Bluetooth scanner', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: MyHomePage(title: 'Bluetooth Scanner'), + ); + } +} + +class MyHomePage extends StatefulWidget { + MyHomePage({Key key, this.title}) : super(key: key); + final String title; + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + BluetoothManager bluetoothManager = BluetoothManager.instance; + + bool _connected = false; + BluetoothDevice _device; + String tips = 'no device connect'; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) => initBluetooth()); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future initBluetooth() async { + bluetoothManager.startScan(timeout: Duration(seconds: 4)); + + bool isConnected = await bluetoothManager.isConnected; + + bluetoothManager.state.listen((state) { + print('cur device status: $state'); + + switch (state) { + case BluetoothManager.CONNECTED: + setState(() { + _connected = true; + tips = 'connect success'; + }); + break; + case BluetoothManager.DISCONNECTED: + setState(() { + _connected = false; + tips = 'disconnect success'; + }); + break; + default: + break; + } + }); + + if (!mounted) return; + + if (isConnected) { + setState(() { + _connected = true; + }); + } + } + + void _onConnect() async { + if (_device != null && _device.address != null) { + await bluetoothManager.connect(_device); + } else { + setState(() { + tips = 'please select device'; + }); + print('please select device'); + } + } + + void _onDisconnect() async { + await bluetoothManager.disconnect(); + } + + void _sendData() async { + List bytes = latin1.encode('Hello world!\n\n\n').toList(); + + // Set codetable west. Add import 'dart:typed_data'; + // List bytes = Uint8List.fromList(List.from('\x1Bt'.codeUnits)..add(6)); + // Text with special characters + // bytes += latin1.encode('blåbærgrød\n\n\n'); + + await bluetoothManager.writeData(bytes); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: RefreshIndicator( + onRefresh: () => + bluetoothManager.startScan(timeout: Duration(seconds: 4)), + child: SingleChildScrollView( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10), + child: Text(tips), + ), + ], + ), + Divider(), + StreamBuilder>( + stream: bluetoothManager.scanResults, + initialData: [], + builder: (c, snapshot) => Column( + children: snapshot.data + .map((d) => ListTile( + title: Text(d.name ?? ''), + subtitle: Text(d.address), + onTap: () async { + setState(() { + _device = d; + }); + }, + trailing: + _device != null && _device.address == d.address + ? Icon( + Icons.check, + color: Colors.green, + ) + : null, + )) + .toList(), + ), + ), + Divider(), + Container( + padding: EdgeInsets.fromLTRB(20, 5, 20, 10), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + OutlineButton( + child: Text('connect'), + onPressed: _connected ? null : _onConnect, + ), + SizedBox(width: 10.0), + OutlineButton( + child: Text('disconnect'), + onPressed: _connected ? _onDisconnect : null, + ), + ], + ), + OutlineButton( + child: Text('Send test data'), + onPressed: _connected ? _sendData : null, + ), + ], + ), + ) + ], + ), + ), + ), + floatingActionButton: StreamBuilder( + stream: bluetoothManager.isScanning, + initialData: false, + builder: (c, snapshot) { + if (snapshot.data) { + return FloatingActionButton( + child: Icon(Icons.stop), + onPressed: () => bluetoothManager.stopScan(), + backgroundColor: Colors.red, + ); + } else { + return FloatingActionButton( + child: Icon(Icons.search), + onPressed: () => + bluetoothManager.startScan(timeout: Duration(seconds: 4))); + } + }, + ), + ); + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..56bef48 --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,22 @@ +name: flutter_bluetooth_basic_example +description: Demonstrates how to use the flutter_bluetooth_basic plugin. +publish_to: 'none' + +environment: + sdk: ">=2.1.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^0.1.2 + path_provider: ^1.4.5 + +dev_dependencies: + flutter_test: + sdk: flutter + + flutter_bluetooth_basic: + path: ../ + +flutter: + uses-material-design: true diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart new file mode 100644 index 0000000..500222c --- /dev/null +++ b/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:flutter_bluetooth_basic_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && + widget.data.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} diff --git a/flutter_bluetooth_basic.iml b/flutter_bluetooth_basic.iml new file mode 100644 index 0000000..429df7d --- /dev/null +++ b/flutter_bluetooth_basic.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ios/Classes/BLEConnecter.h b/ios/Classes/BLEConnecter.h new file mode 100644 index 0000000..82111f4 --- /dev/null +++ b/ios/Classes/BLEConnecter.h @@ -0,0 +1,102 @@ +// +// Connecter.h +// GSDK +// + +#import "Connecter.h" +#import + +@interface BLEConnecter :Connecter + +@property(nonatomic,strong)CBCharacteristic *airPatchChar; +@property(nonatomic,strong)CBCharacteristic *transparentDataWriteChar; +@property(nonatomic,strong)CBCharacteristic *transparentDataReadOrNotifyChar; +@property(nonatomic,strong)CBCharacteristic *connectionParameterChar; + +@property(nonatomic,strong)CBUUID *transServiceUUID; +@property(nonatomic,strong)CBUUID *transTxUUID; +@property(nonatomic,strong)CBUUID *transRxUUID; +@property(nonatomic,strong)CBUUID *disUUID1; +@property(nonatomic,strong)CBUUID *disUUID2; +@property(nonatomic,strong)NSArray *serviceUUID; + +@property(nonatomic,copy)DiscoverDevice discover; +@property(nonatomic,copy)UpdateState updateState; +@property(nonatomic,copy)WriteProgress writeProgress; + +/**数据包大小,默认130个字节*/ +@property(nonatomic,assign)NSUInteger datagramSize; + +@property(nonatomic,strong)CBPeripheral *connPeripheral; + +//+(instancetype)sharedInstance; + +/** + * 方法说明:设置特定的Service UUID,以及Service对应的具有读、写特征值 + * @param serviceUUID 蓝牙模块的service uuid + * @param txUUID 具有写入权限特征值 + * @param rxUUID 具有读取权限特征值 + */ +- (void)configureTransparentServiceUUID: (NSString *)serviceUUID txUUID:(NSString *)txUUID rxUUID:(NSString *)rxUUID; + +/** + * 方法说明:扫描外设 + * @param serviceUUIDs 需要连接的外设UUID + * @param options 其它可选操作 + * @param discover 发现设备 + * peripheral 发现的外设 + * advertisementData + * RSSI 外设信号强度 + */ +-(void)scanForPeripheralsWithServices:(nullable NSArray *)serviceUUIDs options:(nullable NSDictionary *)options discover:(void(^_Nullable)(CBPeripheral *_Nullable peripheral,NSDictionary *_Nullable advertisementData,NSNumber *_Nullable RSSI))discover; + +/** + * 方法说明:停止扫描蓝牙外设 + */ +-(void)stopScan; + +/** + * 方法说明:更新蓝牙状态 + * @param state 更新蓝牙状态 + */ +-(void)didUpdateState:(void(^_Nullable)(NSInteger state))state; + +/** + * 方法说明:连接外设 + * @param peripheral 需要连接的外设 + * @param options 其它可选操作 + * @param timeout 连接超时 + * @param connectState 连接状态 + */ +-(void)connectPeripheral:(CBPeripheral *_Nullable)peripheral options:(nullable NSDictionary *)options timeout:(NSUInteger)timeout connectBlack:(void(^_Nullable)(ConnectState state)) connectState; + +/** + * 方法说明:连接外设 + * @param peripheral 需要连接的外设 + * @param options 其它可选操作 + */ +-(void)connectPeripheral:(CBPeripheral * _Nullable)peripheral options:(nullable NSDictionary *)options; + +/** + * 方法说明:断开连接 + * @param peripheral 需要断开连接的外设 + */ +-(void)closePeripheral:(nonnull CBPeripheral *)peripheral; + +/** + * 方法说明: 往蓝牙模块中写入数据 // Method description: write data to Bluetooth module + * @param data 往蓝牙模块中写入的数据 // Data written to the Bluetooth module + * @param progress 写入数据的进度 // Progress of writing data + * @param callBack 读取蓝牙模块返回数据 // Read Bluetooth module data + */ +-(void)write:(NSData *_Nullable)data progress:(void(^_Nullable)(NSUInteger total,NSUInteger progress))progress receCallBack:(void (^_Nullable)(NSData *_Nullable))callBack; + +/** + * 方法说明: 往蓝牙模块中写入数据 // Method description: write data to Bluetooth module + * @param characteristic 特征值 + * @param data 往蓝牙模块中写入的数据 // Data written to the Bluetooth module + * @param type 写入方式CBCharacteristicWriteWithResponse写入方式是带流控写入方式。CBCharacteristicWriteWithoutResponse不带流控写入方式

@see CBCharacteristicWriteType

+ * Writing method CBCharacteristicWriteWithResponse The writing method is a writing method with flow control. CBCharacteristicWriteWithoutResponseWithout flow control write method

@see CBCharacteristicWriteType

+ */ +-(void)writeValue:(NSData *)data forCharacteristic:(nonnull CBCharacteristic *)characteristic type:(CBCharacteristicWriteType)type; +@end diff --git a/ios/Classes/Connecter.h b/ios/Classes/Connecter.h new file mode 100644 index 0000000..c1fd08f --- /dev/null +++ b/ios/Classes/Connecter.h @@ -0,0 +1,44 @@ +// +// Connecter.h +// GSDK +// +#import +#import "ConnecterBlock.h" + +@interface Connecter:NSObject + +//读取数据 +@property(nonatomic,copy)ReadData readData; +//连接状态 +@property(nonatomic,copy)ConnectDeviceState state; + +/** + * 方法说明: 连接 // Method description: connect + */ +-(void)connect; + +/** + * 方法说明: 连接到指定设备 // Method description: connect to the specified device + * @param connectState 连接状态 + */ +-(void)connect:(void(^)(ConnectState state))connectState; + +/** + * 方法说明: 关闭连接 + */ +-(void)close; + +/** + * 发送数据 // send data + * 向输出流中写入数据 // Write data to the output stream + */ +-(void)write:(NSData *)data receCallBack:(void(^)(NSData *data))callBack; +-(void)write:(NSData *)data; + +/** + * 读取数据 + * @parma data 读取到的数据 + */ +-(void)read:(void(^)(NSData *data))data; + +@end diff --git a/ios/Classes/ConnecterBlock.h b/ios/Classes/ConnecterBlock.h new file mode 100644 index 0000000..2ade47d --- /dev/null +++ b/ios/Classes/ConnecterBlock.h @@ -0,0 +1,40 @@ +// +// ConnecterBlock.h +// GSDK +// + +#ifndef ConnecterBlock_h +#define ConnecterBlock_h +#import + +/** + * @enum ConnectState + * @discussion 连接状态 + * @constant CONNECT_STATE_DISCONNECT ConnectDeviceState返回state为该状态是表示已断开连接 + * @constant CONNECT_STATE_CONNECTING ConnectDeviceState返回state为该状态是表示正在连接中 + * @constant CONNECT_STATE_CONNECTED ConnectDeviceState返回state为该状态是表示连接成功 + * @constant CONNECT_STATE_TIMEOUT ConnectDeviceState返回state为该状态是表示连接超时 + * @constant CONNECT_STATE_FAILT ConnectDeviceState返回state为该状态是表示连接失败 + */ +typedef enum : NSUInteger { + NOT_FOUND_DEVICE,//未找到设备 + CONNECT_STATE_DISCONNECT,//断开连接 + CONNECT_STATE_CONNECTING,//连接中 + CONNECT_STATE_CONNECTED,//连接上 + CONNECT_STATE_TIMEOUT,//连接超时 + CONNECT_STATE_FAILT//连接失败 +}ConnectState; + +/**发现设备 Discover devices*/ +typedef void(^DiscoverDevice)(CBPeripheral *_Nullable peripheral,NSDictionary * _Nullable advertisementData,NSNumber * _Nullable RSSI); +/**蓝牙状态更新 Bluetooth status update*/ +typedef void(^UpdateState)(NSInteger state); +/**连接状态 Connection Status*/ +typedef void(^ConnectDeviceState)(ConnectState state); +/**读取数据 Read data*/ +typedef void(^ReadData)(NSData * _Nullable data); +/**发送数据进度 只适用于蓝牙发送数据 Sending data progress ** Only for Bluetooth sending data*/ +typedef void(^WriteProgress)(NSUInteger total,NSUInteger progress); +typedef void (^Error)(id error); + +#endif /* ConnecterBlock_h */ diff --git a/ios/Classes/ConnecterManager.h b/ios/Classes/ConnecterManager.h new file mode 100644 index 0000000..5149876 --- /dev/null +++ b/ios/Classes/ConnecterManager.h @@ -0,0 +1,93 @@ +// +// ConnecterManager.h +// GSDK +// + +#import +#import "BLEConnecter.h" +#import "EthernetConnecter.h" +#import "Connecter.h" + +/** + * @enum ConnectMethod + * @discussion 连接方式 + * @constant BLUETOOTH 蓝牙连接 + * @constant ETHERNET 网口连接(wifi连接) + */ +typedef enum : NSUInteger{ + BLUETOOTH, + ETHERNET +}ConnectMethod; + +#define Manager [ConnecterManager sharedInstance] + +@interface ConnecterManager : NSObject +@property(nonatomic,strong)BLEConnecter *bleConnecter; +@property(nonatomic,strong)Connecter *connecter; + ++(instancetype)sharedInstance; + +/** + * 方法说明:关闭连接 + */ +-(void)close; + +/** + * 方法说明: 向输出流中写入数据(只适用于蓝牙) // Method description: write data to the output stream (only for Bluetooth) + * @param data 需要写入的数据 + * @param progress 写入数据进度 + * @param callBack 读取输入流中的数据 + */ +-(void)write:(NSData *_Nullable)data progress:(void(^_Nullable)(NSUInteger total,NSUInteger progress))progress receCallBack:(void (^_Nullable)(NSData *_Nullable))callBack; + +/** + * 方法说明:向输出流中写入数据 // Method description: writing data to the output stream + * @param callBack 读取数据接口 + */ +-(void)write:(NSData *)data receCallBack:(void (^)(NSData *))callBack; + +/** + * 方法说明:向输出流中写入数据 // Method description: writing data to the output stream + * @param data 需要写入的数据 + */ +-(void)write:(NSData *)data; + +/** + * 方法说明:停止扫描 + */ +-(void)stopScan; + +/** + * 方法说明:更新蓝牙状态 + * @param state 蓝牙状态 + */ +-(void)didUpdateState:(void(^)(NSInteger state))state; + +/** + * 方法说明:连接外设 + * @param peripheral 需连接的外设 + * @param options 其它可选操作 + * @param timeout 连接时间 + * @param connectState 连接状态 + */ +-(void)connectPeripheral:(CBPeripheral *)peripheral options:(nullable NSDictionary *)options timeout:(NSUInteger)timeout connectBlack:(void(^_Nullable)(ConnectState state)) connectState; + +/** + * 方法说明:连接外设 + * @param peripheral 需连接的外设 + * @param options 其它可选操作 + */ +-(void)connectPeripheral:(CBPeripheral * _Nullable)peripheral options:(nullable NSDictionary *)options; + +/** + * 方法说明:扫描外设 + * @param serviceUUIDs 需要发现外设的UUID,设置为nil则发现周围所有外设 + * @param options 其它可选操作 + * @param discover 发现的设备 + */ +-(void)scanForPeripheralsWithServices:(nullable NSArray *)serviceUUIDs options:(nullable NSDictionary *)options discover:(void(^_Nullable)(CBPeripheral *_Nullable peripheral,NSDictionary *_Nullable advertisementData,NSNumber *_Nullable RSSI))discover; + + + + +@end diff --git a/ios/Classes/ConnecterManager.m b/ios/Classes/ConnecterManager.m new file mode 100644 index 0000000..d50e31b --- /dev/null +++ b/ios/Classes/ConnecterManager.m @@ -0,0 +1,108 @@ +// +// ConnecterManager.m +// GSDK +// +// + +#import "ConnecterManager.h" + +@interface ConnecterManager(){ + ConnectMethod currentConnMethod; +} +@end + +@implementation ConnecterManager + +static ConnecterManager *manager; +static dispatch_once_t once; + ++(instancetype)sharedInstance { + dispatch_once(&once, ^{ + manager = [[ConnecterManager alloc]init]; + }); + return manager; +} + +/** + * 方法说明:扫描外设 + * @param serviceUUIDs 需要发现外设的UUID,设置为nil则发现周围所有外设 + * @param options 其它可选操作 + * @param discover 发现的设备 + */ +-(void)scanForPeripheralsWithServices:(nullable NSArray *)serviceUUIDs options:(nullable NSDictionary *)options discover:(void(^_Nullable)(CBPeripheral *_Nullable peripheral,NSDictionary *_Nullable advertisementData,NSNumber *_Nullable RSSI))discover{ + [_bleConnecter scanForPeripheralsWithServices:serviceUUIDs options:options discover:discover]; +} + +/** + * 方法说明:更新蓝牙状态 + * @param state 蓝牙状态 + */ +-(void)didUpdateState:(void(^)(NSInteger state))state { + if (_bleConnecter == nil) { + currentConnMethod = BLUETOOTH; + [self initConnecter:currentConnMethod]; + } + [_bleConnecter didUpdateState:state]; +} + +-(void)initConnecter:(ConnectMethod)connectMethod { + switch (connectMethod) { + case BLUETOOTH: + _bleConnecter = [BLEConnecter new]; + _connecter = _bleConnecter; + break; + default: + break; + } +} + +/** + * 方法说明:停止扫描 + */ +-(void)stopScan { + [_bleConnecter stopScan]; +} + +/** + * 连接 + */ +-(void)connectPeripheral:(CBPeripheral *)peripheral options:(nullable NSDictionary *)options timeout:(NSUInteger)timeout connectBlack:(void(^_Nullable)(ConnectState state)) connectState{ + [_bleConnecter connectPeripheral:peripheral options:options timeout:timeout connectBlack:connectState]; +} + +-(void)connectPeripheral:(CBPeripheral * _Nullable)peripheral options:(nullable NSDictionary *)options { + [_bleConnecter connectPeripheral:peripheral options:options]; +} + +-(void)write:(NSData *_Nullable)data progress:(void(^_Nullable)(NSUInteger total,NSUInteger progress))progress receCallBack:(void (^_Nullable)(NSData *_Nullable))callBack { + [_bleConnecter write:data progress:progress receCallBack:callBack]; +} + +-(void)write:(NSData *)data receCallBack:(void (^)(NSData *))callBack { +#ifdef DEBUG + NSLog(@"[ConnecterManager] write:receCallBack:"); +#endif + _bleConnecter.writeProgress = nil; + [_connecter write:data receCallBack:callBack]; +} + +-(void)write:(NSData *)data { +#ifdef DEBUG + NSLog(@"[ConnecterManager] write:"); +#endif + _bleConnecter.writeProgress = nil; + [_connecter write:data]; +} + +-(void)close { + if (_connecter) { + [_connecter close]; + } + switch (currentConnMethod) { + case BLUETOOTH: + _bleConnecter = nil; + break; + } +} + +@end diff --git a/ios/Classes/EthernetConnecter.h b/ios/Classes/EthernetConnecter.h new file mode 100644 index 0000000..4c6a57b --- /dev/null +++ b/ios/Classes/EthernetConnecter.h @@ -0,0 +1,27 @@ +// +// EthernetConnecter.h +// GSDK +// + +#import "Connecter.h" + +@interface EthernetConnecter :Connecter +/**连接设备的ip地址*/ +@property(nonatomic,strong)NSString *ip; +/**连接设备的端口号*/ +@property(nonatomic,assign)int port; + +//+(instancetype)sharedInstance; + +/** + * 方法说明: 连接设备 + * @param ip 连接设备的ip地址 + * @param port 连接设备的端口号 + * @param connectState 连接状态 @see ConnectState + * @param callback 输入流数据回调 + */ +-(void)connectIP:(NSString *)ip port:(int)port connectState:(void (^)(ConnectState state))connectState callback:(void(^)(NSData *data))callback; + +-(void)connectIP:(NSString *)ip port:(int)port connectState:(void (^)(ConnectState))connectState; + +@end diff --git a/ios/Classes/FlutterBluetoothBasicPlugin.h b/ios/Classes/FlutterBluetoothBasicPlugin.h new file mode 100644 index 0000000..4586f53 --- /dev/null +++ b/ios/Classes/FlutterBluetoothBasicPlugin.h @@ -0,0 +1,13 @@ +#import +#import +#import "ConnecterManager.h" + +#define NAMESPACE @"flutter_bluetooth_basic" + +@interface FlutterBluetoothBasicPlugin : NSObject +@property(nonatomic,copy)ConnectDeviceState state; +@end + +@interface BluetoothPrintStreamHandler : NSObject +@property FlutterEventSink sink; +@end diff --git a/ios/Classes/FlutterBluetoothBasicPlugin.m b/ios/Classes/FlutterBluetoothBasicPlugin.m new file mode 100644 index 0000000..de60571 --- /dev/null +++ b/ios/Classes/FlutterBluetoothBasicPlugin.m @@ -0,0 +1,183 @@ +#import "FlutterBluetoothBasicPlugin.h" +#import "ConnecterManager.h" + +@interface FlutterBluetoothBasicPlugin () +@property(nonatomic, retain) NSObject *registrar; +@property(nonatomic, retain) FlutterMethodChannel *channel; +@property(nonatomic, retain) BluetoothPrintStreamHandler *stateStreamHandler; +@property(nonatomic) NSMutableDictionary *scannedPeripherals; +@end + +@implementation FlutterBluetoothBasicPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + FlutterMethodChannel* channel = [FlutterMethodChannel + methodChannelWithName:NAMESPACE @"/methods" + binaryMessenger:[registrar messenger]]; + FlutterEventChannel* stateChannel = [FlutterEventChannel eventChannelWithName:NAMESPACE @"/state" binaryMessenger:[registrar messenger]]; + FlutterBluetoothBasicPlugin* instance = [[FlutterBluetoothBasicPlugin alloc] init]; + + instance.channel = channel; + instance.scannedPeripherals = [NSMutableDictionary new]; + + // STATE + BluetoothPrintStreamHandler* stateStreamHandler = [[BluetoothPrintStreamHandler alloc] init]; + [stateChannel setStreamHandler:stateStreamHandler]; + instance.stateStreamHandler = stateStreamHandler; + + [registrar addMethodCallDelegate:instance channel:channel]; +} + +- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { + NSLog(@"call method -> %@", call.method); + + if ([@"state" isEqualToString:call.method]) { + result(nil); + } else if([@"isAvailable" isEqualToString:call.method]) { + + result(@(YES)); + } else if([@"isConnected" isEqualToString:call.method]) { + + result(@(NO)); + } else if([@"isOn" isEqualToString:call.method]) { + result(@(YES)); + }else if([@"startScan" isEqualToString:call.method]) { + NSLog(@"getDevices method -> %@", call.method); + [self.scannedPeripherals removeAllObjects]; + + if (Manager.bleConnecter == nil) { + [Manager didUpdateState:^(NSInteger state) { + switch (state) { + case CBCentralManagerStateUnsupported: + NSLog(@"The platform/hardware doesn't support Bluetooth Low Energy."); + break; + case CBCentralManagerStateUnauthorized: + NSLog(@"The app is not authorized to use Bluetooth Low Energy."); + break; + case CBCentralManagerStatePoweredOff: + NSLog(@"Bluetooth is currently powered off."); + break; + case CBCentralManagerStatePoweredOn: + [self startScan]; + NSLog(@"Bluetooth power on"); + break; + case CBCentralManagerStateUnknown: + default: + break; + } + }]; + } else { + [self startScan]; + } + + result(nil); + } else if([@"stopScan" isEqualToString:call.method]) { + [Manager stopScan]; + result(nil); + } else if([@"connect" isEqualToString:call.method]) { + NSDictionary *device = [call arguments]; + @try { + NSLog(@"connect device begin -> %@", [device objectForKey:@"name"]); + CBPeripheral *peripheral = [_scannedPeripherals objectForKey:[device objectForKey:@"address"]]; + + self.state = ^(ConnectState state) { + [self updateConnectState:state]; + }; + [Manager connectPeripheral:peripheral options:nil timeout:2 connectBlack: self.state]; + + result(nil); + } @catch(FlutterError *e) { + result(e); + } + } else if([@"disconnect" isEqualToString:call.method]) { + @try { + [Manager close]; + result(nil); + } @catch(FlutterError *e) { + result(e); + } + } else if([@"writeData" isEqualToString:call.method]) { + @try { + NSDictionary *args = [call arguments]; + + NSMutableArray *bytes = [args objectForKey:@"bytes"]; + + NSNumber* lenBuf = [args objectForKey:@"length"]; + int len = [lenBuf intValue]; + char cArray[len]; + + for (int i = 0; i < len; ++i) { +// NSLog(@"** ind_%d (d): %@, %d", i, bytes[i], [bytes[i] charValue]); + cArray[i] = [bytes[i] charValue]; + } + NSData *data2 = [NSData dataWithBytes:cArray length:sizeof(cArray)]; +// NSLog(@"bytes in hex: %@", [data2 description]); + [Manager write:data2]; + result(nil); + } @catch(FlutterError *e) { + result(e); + } + } +} + +-(void)startScan { + [Manager scanForPeripheralsWithServices:nil options:nil discover:^(CBPeripheral * _Nullable peripheral, NSDictionary * _Nullable advertisementData, NSNumber * _Nullable RSSI) { + if (peripheral.name != nil) { + + NSLog(@"find device -> %@", peripheral.name); + [self.scannedPeripherals setObject:peripheral forKey:[[peripheral identifier] UUIDString]]; + + NSDictionary *device = [NSDictionary dictionaryWithObjectsAndKeys:peripheral.identifier.UUIDString,@"address",peripheral.name,@"name",nil,@"type",nil]; + [_channel invokeMethod:@"ScanResult" arguments:device]; + } + }]; + +} + +-(void)updateConnectState:(ConnectState)state { + dispatch_async(dispatch_get_main_queue(), ^{ + NSNumber *ret = @0; + switch (state) { + case CONNECT_STATE_CONNECTING: + NSLog(@"status -> %@", @"Connecting ..."); + ret = @0; + break; + case CONNECT_STATE_CONNECTED: + NSLog(@"status -> %@", @"Connection success"); + ret = @1; + break; + case CONNECT_STATE_FAILT: + NSLog(@"status -> %@", @"Connection failed"); + ret = @0; + break; + case CONNECT_STATE_DISCONNECT: + NSLog(@"status -> %@", @"Disconnected"); + ret = @0; + break; + default: + NSLog(@"status -> %@", @"Connection timed out"); + ret = @0; + break; + } + + NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:ret,@"id",nil]; + if(_stateStreamHandler.sink != nil) { + self.stateStreamHandler.sink([dict objectForKey:@"id"]); + } + }); +} + +@end + +@implementation BluetoothPrintStreamHandler + +- (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink { + self.sink = eventSink; + return nil; +} + +- (FlutterError*)onCancelWithArguments:(id)arguments { + self.sink = nil; + return nil; +} + +@end diff --git a/ios/flutter_bluetooth_basic.podspec b/ios/flutter_bluetooth_basic.podspec new file mode 100644 index 0000000..00c844f --- /dev/null +++ b/ios/flutter_bluetooth_basic.podspec @@ -0,0 +1,28 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint flutter_bluetooth_basic.podspec' to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'flutter_bluetooth_basic' + s.version = '0.0.1' + s.summary = 'A new flutter plugin project.' + s.description = <<-DESC +A new flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.static_framework = true + s.dependency 'Flutter' + # s.platform = :ios, '8.0' + + # Import all * .a libraries in the Classes folder + s.frameworks = ["SystemConfiguration", "CoreTelephony","WebKit"] + s.vendored_libraries = '**/*.a' + + # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. + # s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } +end diff --git a/ios/libGSDK.a b/ios/libGSDK.a new file mode 100644 index 0000000..f867658 Binary files /dev/null and b/ios/libGSDK.a differ diff --git a/lib/flutter_bluetooth_basic.dart b/lib/flutter_bluetooth_basic.dart new file mode 100644 index 0000000..92b4017 --- /dev/null +++ b/lib/flutter_bluetooth_basic.dart @@ -0,0 +1,10 @@ +/* + * flutter_bluetooth_basic + * Created by Andrey U. + * + * See LICENSE for distribution and usage details. + */ +library flutter_bluetooth_basic; + +export './src/bluetooth_manager.dart'; +export './src/bluetooth_device.dart'; diff --git a/lib/src/bluetooth_device.dart b/lib/src/bluetooth_device.dart new file mode 100644 index 0000000..de92e0d --- /dev/null +++ b/lib/src/bluetooth_device.dart @@ -0,0 +1,17 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'bluetooth_device.g.dart'; + +@JsonSerializable(includeIfNull: false) +class BluetoothDevice { + BluetoothDevice(); + + String? name; + String? address; + int? type = 0; + bool? connected = false; + + factory BluetoothDevice.fromJson(Map json) => + _$BluetoothDeviceFromJson(json); + Map toJson() => _$BluetoothDeviceToJson(this); +} diff --git a/lib/src/bluetooth_device.g.dart b/lib/src/bluetooth_device.g.dart new file mode 100644 index 0000000..4d930f6 --- /dev/null +++ b/lib/src/bluetooth_device.g.dart @@ -0,0 +1,31 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'bluetooth_device.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +BluetoothDevice _$BluetoothDeviceFromJson(Map json) { + return BluetoothDevice() + ..name = json['name'] as String? + ..address = json['address'] as String? + ..type = json['type'] as int? + ..connected = json['connected'] as bool?; +} + +Map _$BluetoothDeviceToJson(BluetoothDevice instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('name', instance.name); + writeNotNull('address', instance.address); + writeNotNull('type', instance.type); + writeNotNull('connected', instance.connected); + return val; +} diff --git a/lib/src/bluetooth_manager.dart b/lib/src/bluetooth_manager.dart new file mode 100644 index 0000000..e67bc24 --- /dev/null +++ b/lib/src/bluetooth_manager.dart @@ -0,0 +1,143 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:rxdart/rxdart.dart'; + +import 'bluetooth_device.dart'; + +/// A BluetoothManager. +class BluetoothManager { + static const String NAMESPACE = 'flutter_bluetooth_basic'; + static const int CONNECTED = 1; + static const int DISCONNECTED = 0; + + static const MethodChannel _channel = + const MethodChannel('$NAMESPACE/methods'); + static const EventChannel _stateChannel = + const EventChannel('$NAMESPACE/state'); + Stream get _methodStream => _methodStreamController.stream; + final StreamController _methodStreamController = + StreamController.broadcast(); + + BluetoothManager._() { + _channel.setMethodCallHandler((MethodCall call) { + _methodStreamController.add(call); + return Future(() => null); + }); + } + + static BluetoothManager _instance = BluetoothManager._(); + + static BluetoothManager get instance => _instance; + + // Future get isAvailable async => + // await _channel.invokeMethod('isAvailable').then((d) => d); + + // Future get isOn async => + // await _channel.invokeMethod('isOn').then((d) => d); + + Future get isConnected async => + await _channel.invokeMethod('isConnected'); + + BehaviorSubject _isScanning = BehaviorSubject.seeded(false); + Stream get isScanning => _isScanning.stream; + + BehaviorSubject> _scanResults = + BehaviorSubject.seeded([]); + Stream> get scanResults => _scanResults.stream; + + PublishSubject _stopScanPill = new PublishSubject(); + + /// Gets the current state of the Bluetooth module + Stream get state async* { + yield await _channel.invokeMethod('state').then((s) => s); + + yield* _stateChannel.receiveBroadcastStream().map((s) => s); + } + + /// Starts a scan for Bluetooth Low Energy devices + /// Timeout closes the stream after a specified [Duration] + Stream scan({ + Duration? timeout, + }) async* { + if (_isScanning.value == true) { + throw Exception('Another scan is already in progress.'); + } + + // Emit to isScanning + _isScanning.add(true); + + final killStreams = []; + killStreams.add(_stopScanPill); + if (timeout != null) { + killStreams.add(Rx.timer(null, timeout)); + } + + // Clear scan results list + _scanResults.add([]); + + try { + await _channel.invokeMethod('startScan'); + } catch (e) { + print('Error starting scan.'); + _stopScanPill.add(null); + _isScanning.add(false); + throw e; + } + + yield* BluetoothManager.instance._methodStream + .where((m) => m.method == "ScanResult") + .map((m) => m.arguments) + .takeUntil(Rx.merge(killStreams)) + .doOnDone(stopScan) + .map((map) { + final device = BluetoothDevice.fromJson(Map.from(map)); + final List? list = _scanResults.value; + int newIndex = -1; + list!.asMap().forEach((index, e) { + if (e.address == device.address) { + newIndex = index; + } + }); + + if (newIndex != -1) { + list[newIndex] = device; + } else { + list.add(device); + } + _scanResults.add(list); + return device; + }); + } + + Future startScan({ + Duration? timeout, + }) async { + await scan(timeout: timeout).drain(); + return _scanResults.value; + } + + /// Stops a scan for Bluetooth Low Energy devices + Future stopScan() async { + await _channel.invokeMethod('stopScan'); + _stopScanPill.add(null); + _isScanning.add(false); + } + + Future connect(BluetoothDevice device) => + _channel.invokeMethod('connect', device.toJson()); + + Future disconnect() => _channel.invokeMethod('disconnect'); + + Future destroy() => _channel.invokeMethod('destroy'); + + Future writeData(List bytes) { + Map args = Map(); + args['bytes'] = bytes; + args['length'] = bytes.length; + + _channel.invokeMethod('writeData', args); + + return Future.value(true); + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..2ac5afa --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,29 @@ +name: flutter_bluetooth_basic +description: Flutter plugin that allows to find bluetooth devices & send raw bytes data +version: 0.1.7 +homepage: https://github.com/andrey-ushakov/flutter_bluetooth_basic + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.12.0" + +dependencies: + flutter: + sdk: flutter + json_annotation: ^4.1.0 + rxdart: ^0.26.0 + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^1.0.0 + json_serializable: ^3.2.2 + +flutter: + plugin: + platforms: + android: + package: com.tablemi.flutter_bluetooth_basic + pluginClass: FlutterBluetoothBasicPlugin + ios: + pluginClass: FlutterBluetoothBasicPlugin diff --git a/test/flutter_bluetooth_basic_test.dart b/test/flutter_bluetooth_basic_test.dart new file mode 100644 index 0000000..f5871df --- /dev/null +++ b/test/flutter_bluetooth_basic_test.dart @@ -0,0 +1,19 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_bluetooth_basic/flutter_bluetooth_basic.dart'; + +void main() { + const MethodChannel channel = MethodChannel('flutter_bluetooth_basic'); + + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return '42'; + }); + }); + + tearDown(() { + channel.setMockMethodCallHandler(null); + }); +}