Browse Source

device connect--ECG

zzf 1 year ago
parent
commit
9da0ade570
55 changed files with 2896 additions and 159 deletions
  1. BIN
      .gradle/7.0.2/fileHashes/resourceHashesCache.bin
  2. 5 1
      build.gradle
  3. 1 0
      ecg/.gitignore
  4. 31 0
      ecg/build.gradle
  5. 0 0
      ecg/consumer-rules.pro
  6. 21 0
      ecg/proguard-rules.pro
  7. 27 0
      ecg/src/androidTest/java/com/yuanxu/ecg/ExampleInstrumentedTest.java
  8. 7 0
      ecg/src/main/AndroidManifest.xml
  9. 33 0
      ecg/src/main/java/com/yuanxu/ecg/CmdFactory.java
  10. 602 0
      ecg/src/main/java/com/yuanxu/ecg/ECGManager.java
  11. 290 0
      ecg/src/main/java/com/yuanxu/ecg/HandlerManager.java
  12. 82 0
      ecg/src/main/java/com/yuanxu/ecg/L.java
  13. 155 0
      ecg/src/main/java/com/yuanxu/ecg/MsgCenter.java
  14. 47 0
      ecg/src/main/java/com/yuanxu/ecg/bean/CmdType.java
  15. 69 0
      ecg/src/main/java/com/yuanxu/ecg/bean/UserInfo.java
  16. 11 0
      ecg/src/main/java/com/yuanxu/ecg/callback/BaseCallback.java
  17. 8 0
      ecg/src/main/java/com/yuanxu/ecg/callback/DeviceTimeCallback.java
  18. 8 0
      ecg/src/main/java/com/yuanxu/ecg/callback/DeviceTypeCallback.java
  19. 53 0
      ecg/src/main/java/com/yuanxu/ecg/callback/ExecuteCallback.java
  20. 58 0
      ecg/src/main/java/com/yuanxu/ecg/cmd/BaseCmd.java
  21. 139 0
      ecg/src/main/java/com/yuanxu/ecg/cmd/BindUserIdCmd.java
  22. 20 0
      ecg/src/main/java/com/yuanxu/ecg/cmd/QueryDeviceTimeCmd.java
  23. 20 0
      ecg/src/main/java/com/yuanxu/ecg/cmd/QueryDeviceTypeCmd.java
  24. 20 0
      ecg/src/main/java/com/yuanxu/ecg/cmd/QueryStatusCmd.java
  25. 52 0
      ecg/src/main/java/com/yuanxu/ecg/cmd/SetDeviceTimeCmd.java
  26. 51 0
      ecg/src/main/java/com/yuanxu/ecg/cmd/StartCollectingCmd.java
  27. 20 0
      ecg/src/main/java/com/yuanxu/ecg/cmd/StartTransferringCmd.java
  28. 20 0
      ecg/src/main/java/com/yuanxu/ecg/cmd/StopCollectingCmd.java
  29. 20 0
      ecg/src/main/java/com/yuanxu/ecg/cmd/StopTransferringCmd.java
  30. 14 0
      ecg/src/main/java/com/yuanxu/ecg/exception/InvalidAddressException.java
  31. 15 0
      ecg/src/main/java/com/yuanxu/ecg/exception/PermissionException.java
  32. 31 0
      ecg/src/main/java/com/yuanxu/ecg/handle/BaseHandler.java
  33. 28 0
      ecg/src/main/java/com/yuanxu/ecg/handle/HeartDataHandler.java
  34. 118 0
      ecg/src/main/java/com/yuanxu/ecg/handle/cmdhandler/BaseCmdHandler.java
  35. 45 0
      ecg/src/main/java/com/yuanxu/ecg/handle/cmdhandler/BindUserIdCmdHandler.java
  36. 40 0
      ecg/src/main/java/com/yuanxu/ecg/handle/cmdhandler/QueryDeviceTimeCmdHandler.java
  37. 39 0
      ecg/src/main/java/com/yuanxu/ecg/handle/cmdhandler/QueryDeviceTypeCmdHandler.java
  38. 60 0
      ecg/src/main/java/com/yuanxu/ecg/handle/cmdhandler/QueryStatusCmdHandler.java
  39. 44 0
      ecg/src/main/java/com/yuanxu/ecg/handle/cmdhandler/SetDeviceTimeCmdHandler.java
  40. 45 0
      ecg/src/main/java/com/yuanxu/ecg/handle/cmdhandler/StartCollectingCmdHandler.java
  41. 25 0
      ecg/src/main/java/com/yuanxu/ecg/handle/cmdhandler/StartTransferringCmdHandler.java
  42. 40 0
      ecg/src/main/java/com/yuanxu/ecg/handle/cmdhandler/StopCollectingCmdHandler.java
  43. 49 0
      ecg/src/main/java/com/yuanxu/ecg/handle/cmdhandler/StopTransferringCmdHandler.java
  44. 43 0
      ecg/src/main/java/com/yuanxu/ecg/utils/ByteUtils.java
  45. 43 0
      ecg/src/main/java/com/yuanxu/ecg/utils/DeviceInfoParser.java
  46. 3 0
      ecg/src/main/res/values/strings.xml
  47. 17 0
      ecg/src/test/java/com/yuanxu/ecg/ExampleUnitTest.java
  48. 3 0
      sample/build.gradle
  49. 3 1
      sample/src/main/java/com/yanzhenjie/andserver/sample/MainActivity.java
  50. 140 157
      sample/src/main/java/com/yanzhenjie/andserver/sample/controller/CommonController.java
  51. 107 0
      sample/src/main/java/com/yanzhenjie/andserver/sample/model/ConnectBluetoothRequestParam.java
  52. 70 0
      sample/src/main/java/com/yanzhenjie/andserver/sample/model/EcgUserInfo.java
  53. 2 0
      settings.gradle
  54. 2 0
      signalproc-release/build.gradle
  55. BIN
      signalproc-release/signalproc-release.aar

BIN
.gradle/7.0.2/fileHashes/resourceHashesCache.bin


+ 5 - 1
build.gradle

@@ -2,13 +2,16 @@ apply from: 'config.gradle'
 
 buildscript {
     repositories {
+//        maven { url 'https://repository.mulesoft.org/nexus/content/repositories/public/' }
         mavenLocal()
         google()
         mavenCentral()
+
     }
 
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.6.3'
+//        classpath 'com.android.tools.build:gradle:3.6.3'
+        classpath 'com.android.tools.build:gradle:7.0.4'
         classpath 'com.yanzhenjie.andserver:plugin:2.1.11'
     }
 }
@@ -18,6 +21,7 @@ allprojects {
         mavenLocal()
         google()
         mavenCentral()
+        maven { url 'https://jitpack.io' }
     }
 }
 

+ 1 - 0
ecg/.gitignore

@@ -0,0 +1 @@
+/build

+ 31 - 0
ecg/build.gradle

@@ -0,0 +1,31 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 30
+    buildToolsVersion "30.0.2"
+
+
+    defaultConfig {
+        minSdkVersion 19
+        targetSdkVersion 30
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles 'consumer-rules.pro'
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+
+}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+    api 'com.github.Ficat:EasyBle:v2.0.1'
+    implementation fileTree(dir: 'C:\\Users\\Administrator\\.gradle\\caches\\modules-2\\files-2.1\\com.github.Ficat\\EasyBle\\v2.0.1\\c55e54b9e3942cba23966e84c6e4b42e726d338f', include: ['*.aar', '*.jar'], exclude: [])
+//    implementation files('libs\\EasyBle-v2.0.1-sources.jar')
+}

+ 0 - 0
ecg/consumer-rules.pro


+ 21 - 0
ecg/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 27 - 0
ecg/src/androidTest/java/com/yuanxu/ecg/ExampleInstrumentedTest.java

@@ -0,0 +1,27 @@
+package com.yuanxu.ecg;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+        assertEquals("com.yuanxu.ecg.test", appContext.getPackageName());
+    }
+}

+ 7 - 0
ecg/src/main/AndroidManifest.xml

@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.yuanxu.ecg">
+    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+    <uses-permission android:name="android.permission.BLUETOOTH" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+
+</manifest>

+ 33 - 0
ecg/src/main/java/com/yuanxu/ecg/CmdFactory.java

@@ -0,0 +1,33 @@
+package com.yuanxu.ecg;
+
+import com.yuanxu.ecg.bean.CmdType;
+import com.yuanxu.ecg.cmd.BaseCmd;
+
+import java.lang.reflect.Constructor;
+
+/**
+ * 指令工厂类,对外提供
+ */
+public class CmdFactory {
+    private CmdFactory() {
+
+    }
+
+    public static CmdFactory newInstance() {
+        return new CmdFactory();
+    }
+
+    public BaseCmd createCmd(CmdType cmdType) {
+        String absoluteClassName = cmdType.getCmdAbsoluteClassName();
+        try {
+            Class<?> clasz = Class.forName(absoluteClassName);
+            Constructor<?> constructor = clasz.getConstructor();
+            BaseCmd d = (BaseCmd) constructor.newInstance();
+            return d;
+        } catch (Exception e) {
+            L.e("CmdFactory生成" + cmdType.getCmdClassName() + "对象出错:" + e.getMessage());
+            return null;
+        }
+    }
+
+}

+ 602 - 0
ecg/src/main/java/com/yuanxu/ecg/ECGManager.java

@@ -0,0 +1,602 @@
+package com.yuanxu.ecg;
+
+import android.app.Application;
+import android.bluetooth.BluetoothAdapter;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+
+import com.ficat.easyble.BleDevice;
+import com.ficat.easyble.BleManager;
+import com.ficat.easyble.gatt.bean.CharacteristicInfo;
+import com.ficat.easyble.gatt.bean.ServiceInfo;
+import com.ficat.easyble.gatt.callback.BleConnectCallback;
+import com.ficat.easyble.gatt.callback.BleNotifyCallback;
+import com.ficat.easyble.gatt.callback.BleWriteCallback;
+import com.yuanxu.ecg.bean.UserInfo;
+import com.yuanxu.ecg.callback.BaseCallback;
+import com.yuanxu.ecg.callback.DeviceTimeCallback;
+import com.yuanxu.ecg.callback.DeviceTypeCallback;
+import com.yuanxu.ecg.callback.ExecuteCallback;
+import com.yuanxu.ecg.cmd.BindUserIdCmd;
+import com.yuanxu.ecg.cmd.SetDeviceTimeCmd;
+import com.yuanxu.ecg.cmd.StartCollectingCmd;
+import com.yuanxu.ecg.cmd.StopCollectingCmd;
+import com.yuanxu.ecg.handle.cmdhandler.QueryDeviceTimeCmdHandler;
+import com.yuanxu.ecg.handle.cmdhandler.QueryDeviceTypeCmdHandler;
+import com.yuanxu.ecg.handle.cmdhandler.StopCollectingCmdHandler;
+import com.yuanxu.ecg.handle.cmdhandler.StopTransferringCmdHandler;
+import com.yuanxu.ecg.utils.ByteUtils;
+import com.yuanxu.ecg.utils.DeviceInfoParser;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ECGManager {
+    private static final String SERVICE_UUID = "0000fff0-0000-1000-8000-00805f9b34fb";
+    private static final String CHARACTERISTIC_UUID_V1 = "0000fff1-0000-1000-8000-00805f9b34fb";
+    private static final String CHARACTERISTIC_UUID_V2 = "0000fff2-0000-1000-8000-00805f9b34fb";
+    private  final String TAG = "ECGManager";
+    private BleManager manager;
+    private ExecuteCallback executeCallback;
+    private DeviceTimeCallback deviceTimeCallback;
+    private DeviceTypeCallback deviceTypeCallback;
+    private BleDevice curConnectedDevice;//当前已连接的设备
+    private int currentProcess = ExecuteCallback.PROCESS_IDLE;//当前进度,默认空闲
+    private boolean autoReconnect = true;//当连接非用户主动断开时是否自动重连,默认为true
+    private boolean disconnectByUser;//是否由用户主动断开
+
+
+    /**
+     * 硬件版本map,key为address,value表示是否为旧版本心电贴设备
+     * <p>
+     * 注:旧版本的通知特征通道uuid为CHARACTERISTIC_UUID_V1为notify,写入特征
+     * 通道uuid为CHARACTERISTIC_UUID_V2;新版本反之
+     */
+    private Map<String, Boolean> versionMap;
+
+    /**
+     * 消息中心MsgCenter的监听器回调
+     */
+    private MsgCenter.MsgListener msgListener = new MsgCenter.MsgListener() {
+        @Override
+        public void onSendCmd(String hexCmd) {
+            if (curConnectedDevice == null) {
+                //本SDK目前仅支持单连接,故直接使用连接的第一个设备即可
+                List<BleDevice> list = manager.getConnectedDevices();
+                if (list.size() > 0) {
+                    curConnectedDevice = list.get(0);
+                }
+            }
+            write(curConnectedDevice, ByteUtils.hexStr2Bytes(hexCmd));
+        }
+
+        @Override
+        public void onCmdError() {
+            //指令有误导致任务异常结束,重置进度为空闲,防止下次执行任务时execute()时
+            //进度判断失效
+            updateProcess(ExecuteCallback.PROCESS_IDLE, "指令有误,请检查后重试");
+            if (executeCallback != null) {
+                executeCallback.onFailure(BaseCallback.FAIL_OTHER, "指令有误(长度不足、命令字不存在、逻辑错误)等");
+            }
+            //任务结束,清理所有callback
+            clearAllCallback();
+        }
+
+        @Override
+        public void onCmdExecuteFail(String hexStrResponse) {
+            if (!TextUtils.isEmpty(hexStrResponse)) {
+                String cmPrefix = hexStrResponse.substring(0, 4);
+                if (cmPrefix.equalsIgnoreCase(StopCollectingCmd.CMD_PREFIX)) {
+                    return;
+                }
+            }
+            //指令执行失败导致任务异常结束,重置进度为空闲,防止下次执行任务时execute()
+            //时进度判断失效
+            updateProcess(ExecuteCallback.PROCESS_IDLE, "指令执行失败,错误指令:" + hexStrResponse);
+            if (executeCallback != null) {
+                executeCallback.onFailure(BaseCallback.FAIL_OTHER, "指令(" + hexStrResponse +
+                        ")执行失败(索要数据不存在、当前时刻不适合执行此命令、设备错误)");
+            }
+            //任务结束,清理所有callback
+            clearAllCallback();
+        }
+
+        @Override
+        public void onStopTransferringSuccess() {
+            //只有在当前状态为数据接收中时,停止传输成功才代表数据已经采集完成,因
+            //开发者可主动调用stop()或在设备状态非空闲时会自动先stop()再重试
+            if (currentProcess == ExecuteCallback.PROCESS_DATA_RECEIVING) {
+                if (executeCallback != null) {
+                    executeCallback.onSuccess();
+                }
+                //任务成功结束,更新进度为空闲
+                updateProcess(ExecuteCallback.PROCESS_IDLE, "停止传输命令发送成功,数据传输终止,达到空闲状态");
+            }
+        }
+
+        @Override
+        public void onDeviceNotIdle(String status) {
+            //硬件非空闲状态下停止传输与采集
+            stop();
+            //延时T秒后重新执行命令。若在发送停止传输和停止采集指令成功后调用重新执行命令,
+            //则需相应判断(如是否为设备非空闲才调用stop指令),此处简化为仅做相应延时即可
+            new Handler(Looper.getMainLooper())
+                    .postDelayed(new Runnable() {
+                        @Override
+                        public void run() {
+                            retryExecuteCmd();
+                        }
+                    }, 1200);
+        }
+
+        @Override
+        public void onDeviceType(String hexDeviceType) {
+            if (deviceTypeCallback != null) {
+                String type = DeviceInfoParser.parseDeviceType(hexDeviceType);
+                deviceTypeCallback.onDeviceType(type);
+            }
+        }
+
+        @Override
+        public void onDeviceTime(String hexDeviceTime) {
+            if (deviceTimeCallback != null) {
+                String result = DeviceInfoParser.parseDeviceTime(hexDeviceTime);
+                deviceTimeCallback.onDeviceTime(result);
+            }
+        }
+
+        @Override
+        public void onReceivedHeartData(String hexData) {
+            //当前进度不为空闲且不为正在接收时更改进度。防止顺序错乱(如发送停止cmd且currentProcess已更新
+            //为空闲,但可能出现onReceivedHeartData因次数较多随后再次被回调造成进度被修改为接收中)
+            if (currentProcess != ExecuteCallback.PROCESS_IDLE && currentProcess != ExecuteCallback.PROCESS_DATA_RECEIVING) {
+                updateProcess(ExecuteCallback.PROCESS_DATA_RECEIVING, "接收硬件数据中");
+            }
+            if (executeCallback != null) {
+                executeCallback.onReceivedOriginalData(ByteUtils.hexStr2Bytes(hexData));
+            }
+        }
+    };
+
+    private ECGManager() {
+
+    }
+
+    public static ECGManager getInstance() {
+        return ECGManagerHolder.sManager;
+    }
+
+    private static final class ECGManagerHolder {
+        static final ECGManager sManager = new ECGManager();
+    }
+
+    public ECGManager init(Application application) {
+        if (manager != null) {
+            L.d("ECGManager已初始化,无需重复调用");
+            return this;
+        }
+        checkNotNull(application, Application.class);
+        //准备BleManager并初始化
+        manager = BleManager
+                .getInstance()
+                .setConnectionOptions(BleManager.ConnectOptions
+                        .newInstance()
+                        .connectTimeout(12000))//扫描连接超时时间默认12秒
+                .init(application);
+        //handler管理器初始化
+        HandlerManager.getInstance().init();
+        //初始化消息中心
+        MsgCenter.getInstance().init();
+        MsgCenter.getInstance().addMsgListener(msgListener);
+        versionMap = new HashMap<>();
+        return this;
+    }
+
+    public ECGManager setLog(boolean enable, String tag) {
+        L.SHOW_LOG = enable;
+        if (!TextUtils.isEmpty(tag)) {
+            L.TAG = tag;
+        }
+        return this;
+    }
+
+    public ECGManager setAutoReconnect(boolean autoReconnect) {
+        this.autoReconnect = autoReconnect;
+        return this;
+    }
+
+    public void execute(final String address, UserInfo userInfo, ExecuteCallback callback) {
+        if (manager == null) {
+            throw new IllegalStateException("You should call init() first");
+        }
+        if (!BluetoothAdapter.checkBluetoothAddress(address)) {
+            throw new IllegalArgumentException("Invalid address:" + address);
+        }
+        checkNotNull(userInfo, UserInfo.class);
+        checkNotNull(callback, ExecuteCallback.class);
+        if (!BleManager.isBluetoothOn()) {
+            callback.onFailure(BaseCallback.FAIL_BLUETOOTH_NOT_AVAILABLE, "蓝牙尚未打开,请打开蓝牙后重试");
+            return;
+        }
+        if (hasConnectedToOtherDevice(address)) {
+            callback.onFailure(ExecuteCallback.FAIL_CONNECTION_ALREADY_ESTABLISHED, "本机已连接到其他硬件设备,请先断开连接后重试");
+            return;
+        }
+        if (manager.isConnecting(address)) {
+            System.out.println("---------------------------------------------------------------------------------------");
+            return;
+        }
+        if (manager.isConnected(address)) {
+            //当前设备处于连接且空闲状态,直接执行新的检测任务
+            if (currentProcess == ExecuteCallback.PROCESS_IDLE) {
+                disconnectByUser = false;
+                executeCallback = callback;
+                executeCmd(userInfo);
+            } else {
+                L.w("当前存在其他尚未结束的检测任务");
+            }
+        } else {
+            disconnectByUser = false;
+            executeCallback = callback;
+            //1.未连接状态下
+            //2.上轮检测任务时出现连接断开后且无法成功重连(如用户禁止重连或用户手动关闭
+            //了蓝牙)时,任务已异常结束。
+            //以上2中情况下当前任务进度是非空闲的(即状态为已断开),重置任务进度
+            currentProcess = ExecuteCallback.PROCESS_IDLE;
+            //未连接状态,连接并执行任务
+            connect(address, userInfo);
+        }
+    }
+
+    /**
+     * 查询设备时间
+     */
+    public void queryDeviceTime(DeviceTimeCallback callback) {
+        checkNotNull(callback, DeviceTimeCallback.class);
+        if (manager == null) {
+            throw new IllegalStateException("You should call init() first");
+        }
+        if (!BleManager.isBluetoothOn()) {
+            callback.onFailure(BaseCallback.FAIL_BLUETOOTH_NOT_AVAILABLE, "蓝牙尚未打开,请打开蓝牙并连接成功后重试");
+            return;
+        }
+        if (curConnectedDevice == null) {
+            callback.onFailure(BaseCallback.FAIL_OTHER, "无已连接的硬件设备,请确保有连接成功的设备后重试");
+            return;
+        }
+        deviceTimeCallback = callback;
+        HandlerManager
+                .getInstance()
+                .getHandler(QueryDeviceTimeCmdHandler.class)
+                .sendCmdToDevice();
+    }
+
+    /**
+     * 查询设备类型
+     */
+    public void queryDeviceType(DeviceTypeCallback callback) {
+        checkNotNull(callback, DeviceTypeCallback.class);
+        if (manager == null) {
+            throw new IllegalStateException("You should call init() first");
+        }
+        if (!BleManager.isBluetoothOn()) {
+            callback.onFailure(BaseCallback.FAIL_BLUETOOTH_NOT_AVAILABLE, "蓝牙尚未打开,请打开蓝牙并连接成功后重试");
+            return;
+        }
+        if (curConnectedDevice == null) {
+            callback.onFailure(BaseCallback.FAIL_OTHER, "无已连接的硬件设备,请确保有连接成功的设备后重试");
+            return;
+        }
+        deviceTypeCallback = callback;
+        HandlerManager
+                .getInstance()
+                .getHandler(QueryDeviceTypeCmdHandler.class)
+                .sendCmdToDevice();
+    }
+
+    public void setCustomCmdExecuteFlow(HandlerManager.CmdExecuteFlow flow) {
+        if (manager == null) {
+            throw new IllegalStateException("You should call init() first");
+        }
+        if (flow == null) {
+            return;
+        }
+        HandlerManager
+                .getInstance()
+                .setCustomCmdExecuteFlow(flow);
+    }
+
+    /**
+     * 停止本轮测试采集
+     * <p>
+     * 注意:并不断开连接
+     */
+    public void stop() {
+        if ( manager == null || manager.getConnectedDevices().size() <= 0 ) {
+            return;
+        }
+        //若停止传输指令handler的next不是停止采集指令handler,则
+        //直接发送停止采集指令;否则先发送停止传输指令后再让停止传
+        //输指令handler收到硬件回复后自动发送停止采集指令
+        StopTransferringCmdHandler handler = HandlerManager
+                .getInstance()
+                .getHandler(StopTransferringCmdHandler.class);
+        if (handler.getNext() instanceof StopCollectingCmdHandler) {
+            handler.setNextHandlerSendCmdWhenCanHandle(true);
+            handler.sendCmdToDevice();
+        } else {
+            HandlerManager
+                    .getInstance()
+                    .getHandler(StopCollectingCmdHandler.class)
+                    .sendCmdToDevice();
+        }
+    }
+
+    /**
+     * 断开当前所有已连接设备
+     */
+    public void disconnect() {
+        if (manager != null) {
+            manager.disconnectAll();
+        }
+        disconnectByUser = true;
+        curConnectedDevice = null;
+        //确保断开正在进行中的连接时callback也会被正常clear
+        clearAllCallback();
+    }
+
+    /**
+     * 释放资源,当不再使用该库或因其他原因暂不使用(如内存吃紧且暂不使用该SDK)时调用,若
+     * 调用该方法后要继续使用该库,则需重新调用{@link #init(Application)}
+     */
+    public void release() {
+        if (manager == null) {
+            return;
+        }
+        disconnect();
+        manager.destroy();
+        MsgCenter.getInstance().release();
+        HandlerManager.getInstance().release();
+        versionMap.clear();
+        versionMap = null;
+        manager = null;
+        curConnectedDevice = null;
+        clearAllCallback();
+        currentProcess = ExecuteCallback.PROCESS_IDLE;
+        autoReconnect = true;
+    }
+
+    /**
+     * 获取BleManager,通过获取该BleManager,开发者可配置蓝牙相关参数(如扫描、连接参数)
+     * 或其他功能
+     * <p>
+     * 注意:若尚未调用{@link #init(Application)},则会返回null
+     */
+    public BleManager getBleManager() {
+        return manager;
+    }
+
+    private void connect(final String address, final UserInfo userInfo) {
+        manager.connect(address, new BleConnectCallback() {
+            @Override
+            public void onStart(boolean startConnectSuccess, String info, BleDevice device) {
+                L.d("开始连接=" + startConnectSuccess + "    info=" + info);
+                if (startConnectSuccess) {
+                    updateProcess(ExecuteCallback.PROCESS_CONNECT_START, "连接开始," + info);
+                } else {
+                    if (executeCallback != null) {
+                        executeCallback.onFailure(ExecuteCallback.FAIL_CONNECTION_START_FAIL, "连接无法正常开始," + info);
+                        clearAllCallback();
+                    }
+                }
+            }
+
+            @Override
+            public void onConnected(BleDevice device) {
+                L.d("已连接");
+                updateProcess(ExecuteCallback.PROCESS_CONNECTED, "已连接至设备(" + device.address + ")");
+                setNotify(device, userInfo);
+                curConnectedDevice = device;
+            }
+
+            @Override
+            public void onDisconnected(String info, int status, BleDevice device) {
+                L.d("连接断开    info=" + info + "    status=" + status);
+                curConnectedDevice = null;
+                int preProcess = currentProcess;
+                updateProcess(ExecuteCallback.PROCESS_DISCONNECTED, "连接断开," + info);
+                //主动断开或空闲状态下断开后不再执行重连
+                if (disconnectByUser || preProcess == ExecuteCallback.PROCESS_IDLE) {
+                    L.i(disconnectByUser ? "已主动断开连接" : "设备空闲状态下连接断开,不再执行重连");
+                    //清理callback
+                    clearAllCallback();
+                    return;
+                }
+                if (autoReconnect) {//非空闲状态(如数据尚未接收完成等)且允许重连时
+                    //1.清空原接收到的数据(当前版本SDK暂无)
+                    //2.重连重新执行检测任务
+                    manager.connect(address, this);
+                } else {//非空闲状态且不允许重连时
+                    if (executeCallback != null) {
+                        executeCallback.onFailure(BaseCallback.FAIL_OTHER, "任务执行过程中连接异常断开,且您不允许自动重连,本次检测任务失败");
+                    }
+                    clearAllCallback();
+                }
+            }
+
+            @Override
+            public void onFailure(int failCode, String info, BleDevice device) {
+                L.d(failCode == BleConnectCallback.FAIL_CONNECT_TIMEOUT ? "连接超时" : "连接失败");
+                updateProcess(ExecuteCallback.PROCESS_CONNECT_FAIL, info);
+                curConnectedDevice = null;
+                if (autoReconnect && !disconnectByUser) {
+                    manager.connect(address, this);
+                }
+            }
+        });
+    }
+
+    private void setNotify(BleDevice device, final UserInfo userInfo) {
+        distinguishVersion(device);
+        Boolean b = versionMap.get(device.address);
+        boolean oldVersion = b == null ? false : b;
+        updateProcess(ExecuteCallback.PROCESS_NOTIFY_START, "notify开始");
+        manager.notify(device, SERVICE_UUID, oldVersion ? CHARACTERISTIC_UUID_V1 : CHARACTERISTIC_UUID_V2,
+                new BleNotifyCallback() {
+                    @Override
+                    public void onCharacteristicChanged(byte[] data, BleDevice device) {
+                        HandlerManager
+                                .getInstance()
+                                .getHandlerHead()
+                                .handleResponse(ByteUtils.bytes2HexStr(data));
+                    }
+
+                    @Override
+                    public void onNotifySuccess(String notifySuccessUuid, BleDevice device) {
+                        L.d("notify成功  notifyUuid=" + notifySuccessUuid);
+                        updateProcess(ExecuteCallback.PROCESS_NOTIFY_SUCCESS, "notify成功");
+                        executeCmd(userInfo);
+                    }
+
+                    @Override
+                    public void onFailure(int failCode, String info, BleDevice device) {
+                        L.d("notify失败  info=" + info);
+                        updateProcess(ExecuteCallback.PROCESS_NOTIFY_FAIL, info);
+                    }
+                });
+    }
+
+    private void executeCmd(UserInfo userInfo) {
+        HandlerManager.CmdExecuteFlow flow = HandlerManager
+                .getInstance()
+                .getCmdExecuteFlow();
+        //设置某些指令所需信息
+        BindUserIdCmd bindUserId = flow.getCmd(BindUserIdCmd.class);
+        SetDeviceTimeCmd setDeviceTime = flow.getCmd(SetDeviceTimeCmd.class);
+        StartCollectingCmd collecting = flow.getCmd(StartCollectingCmd.class);
+        if (bindUserId != null) {
+            bindUserId.setName(userInfo.getName());
+            bindUserId.setAge(userInfo.getAge());
+            bindUserId.setGender(userInfo.isFemale());
+            bindUserId.setHeight(userInfo.getHeight());
+            bindUserId.setWeight(userInfo.getWeight());
+        }
+        long timestamp = System.currentTimeMillis();
+        if (setDeviceTime != null) {
+            setDeviceTime.setDeviceTimestamp(timestamp);
+        }
+        if (collecting != null) {
+            collecting.setTimestamp(timestamp);
+        }
+        L.d("开始依命令执行流发送命令");
+        //根据指令执行流开始发送命令
+        flow.getHead().sendCmdToDevice();
+        updateProcess(ExecuteCallback.PROCESS_CMD_SENDING, "命令发送中");
+    }
+
+    /**
+     * 当检查设备状态时发现设备不为空闲(待机)状态,则会重新发送指令流
+     */
+    private void retryExecuteCmd() {
+        HandlerManager
+                .getInstance()
+                .getCmdExecuteFlow()
+                .getHead()
+                .sendCmdToDevice();
+        L.d("重新依命令执行流发送命令");
+        updateProcess(ExecuteCallback.PROCESS_CMD_SENDING, "命令发送中");
+    }
+
+    private void write(BleDevice device, final byte[] data) {
+        if (manager == null || device == null || data == null
+                || !manager.isConnected(device.address)) {
+            return;
+        }
+        distinguishVersion(device);
+        if (!versionMap.containsKey(device.address) || versionMap.get(device.address) == null) {
+            L.d("写入时无法判断当前设备型号进而无法区分notify和write特征uuid");
+            return;
+        }
+        Boolean b = versionMap.get(device.address);
+        boolean oldVersion = b == null ? false : b;
+        manager.write(device, SERVICE_UUID, oldVersion ? CHARACTERISTIC_UUID_V2 : CHARACTERISTIC_UUID_V1,
+                data, new BleWriteCallback() {
+                    @Override
+                    public void onWriteSuccess(byte[] data, BleDevice device) {
+                    }
+
+                    @Override
+                    public void onFailure(int failCode, String info, BleDevice device) {
+                        L.d("写入失败:" + ByteUtils.bytes2HexStr(data) + "   detail=" + info);
+                    }
+                });
+    }
+
+    private void updateProcess(int process, String info) {
+        currentProcess = process;
+        if (executeCallback != null) {
+            executeCallback.onProcess(process, info);
+        }
+    }
+
+    /**
+     * 通过Characteristic的属性区分新旧设备
+     * <p>
+     * 注:
+     * 1.旧版本的通知特征通道uuid为CHARACTERISTIC_UUID_V1为notify,写入特征通道uuid为
+     * CHARACTERISTIC_UUID_V2;新版本反之
+     * 2.目前使用该方式主要因两款设备已进入市场使用,而硬件即为提供型号区分且暂无法修改
+     */
+    private void distinguishVersion(BleDevice device) {
+        if (manager == null || !manager.isConnected(device.address)
+                || versionMap.containsKey(device.address)) {
+            return;
+        }
+        Map<ServiceInfo, List<CharacteristicInfo>> info = manager.getDeviceServices(device);
+        for (Map.Entry<ServiceInfo, List<CharacteristicInfo>> e : info.entrySet()) {
+            if (!e.getKey().uuid.equals(SERVICE_UUID)) {
+                continue;
+            }
+            List<CharacteristicInfo> list = e.getValue();
+            for (CharacteristicInfo i : list) {
+                if (i.uuid.equals(CHARACTERISTIC_UUID_V1)) {
+                    boolean oldVersion = i.notify;
+                    versionMap.put(device.address, oldVersion);
+                    return;
+                }
+            }
+        }
+    }
+
+    private void clearAllCallback() {
+        executeCallback = null;
+        deviceTimeCallback = null;
+        deviceTypeCallback = null;
+    }
+
+    /**
+     * 当前设备是否已连接除targetAddress外的其他设备
+     *
+     * @param targetAddress 目标设备
+     */
+    private boolean hasConnectedToOtherDevice(String targetAddress) {
+        if (manager == null || manager.getConnectedDevices().size() <= 0) {
+            return false;
+        }
+        for (BleDevice d : manager.getConnectedDevices()) {
+            if (!d.address.equals(targetAddress)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void checkNotNull(Object object, Class<?> clasz) {
+        if (object == null) {
+            String claszSimpleName = clasz.getSimpleName();
+            throw new IllegalArgumentException(claszSimpleName + " is null");
+        }
+    }
+
+}

+ 290 - 0
ecg/src/main/java/com/yuanxu/ecg/HandlerManager.java

@@ -0,0 +1,290 @@
+package com.yuanxu.ecg;
+
+import com.yuanxu.ecg.cmd.BaseCmd;
+import com.yuanxu.ecg.cmd.BindUserIdCmd;
+import com.yuanxu.ecg.cmd.QueryDeviceTimeCmd;
+import com.yuanxu.ecg.cmd.QueryDeviceTypeCmd;
+import com.yuanxu.ecg.cmd.QueryStatusCmd;
+import com.yuanxu.ecg.cmd.SetDeviceTimeCmd;
+import com.yuanxu.ecg.cmd.StartCollectingCmd;
+import com.yuanxu.ecg.cmd.StartTransferringCmd;
+import com.yuanxu.ecg.cmd.StopCollectingCmd;
+import com.yuanxu.ecg.cmd.StopTransferringCmd;
+import com.yuanxu.ecg.handle.BaseHandler;
+import com.yuanxu.ecg.handle.HeartDataHandler;
+import com.yuanxu.ecg.handle.cmdhandler.BaseCmdHandler;
+import com.yuanxu.ecg.handle.cmdhandler.BindUserIdCmdHandler;
+import com.yuanxu.ecg.handle.cmdhandler.QueryDeviceTimeCmdHandler;
+import com.yuanxu.ecg.handle.cmdhandler.QueryDeviceTypeCmdHandler;
+import com.yuanxu.ecg.handle.cmdhandler.QueryStatusCmdHandler;
+import com.yuanxu.ecg.handle.cmdhandler.SetDeviceTimeCmdHandler;
+import com.yuanxu.ecg.handle.cmdhandler.StartCollectingCmdHandler;
+import com.yuanxu.ecg.handle.cmdhandler.StartTransferringCmdHandler;
+import com.yuanxu.ecg.handle.cmdhandler.StopCollectingCmdHandler;
+import com.yuanxu.ecg.handle.cmdhandler.StopTransferringCmdHandler;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class HandlerManager {
+    private BaseHandler handlerHead, handlerMiddle;
+    private CmdExecuteFlow cmdExecuteFlow;
+
+    private HandlerManager() {
+
+    }
+
+    static HandlerManager getInstance() {
+        return HandlerManagerHolder.sHandlerManager;
+    }
+
+    private static final class HandlerManagerHolder {
+        static final HandlerManager sHandlerManager = new HandlerManager();
+    }
+
+    public void init() {
+        if (handlerHead != null) {
+            return;
+        }
+        initHandlerLink();
+    }
+
+    public void release() {
+        handlerHead = null;
+        handlerMiddle = null;
+        cmdExecuteFlow = null;
+    }
+
+    /**
+     * 获取handler链头结点
+     */
+    public BaseHandler getHandlerHead() {
+        return handlerHead;
+    }
+
+    /**
+     * 获取handler链中{@link #cmdExecuteFlow}链的上一节点。详
+     * 见{@link #initHandlerLink()}代码
+     */
+    public BaseHandler getHandlerEnd() {
+        return handlerMiddle;
+    }
+
+    /**
+     * 获取handler链中指令处理链头结点
+     */
+    public BaseCmdHandler getCmdHandlerHead() {
+        if (cmdExecuteFlow == null) {
+            return getDefaultCmdExecutorFlow().getHead();
+        }
+        return cmdExecuteFlow.getHead();
+    }
+
+    public <T extends BaseHandler> T getHandler(Class<T> tClass) {
+        try {
+            BaseHandler current = handlerHead;
+            while (current != null) {
+                if (current.getClass().getSimpleName().equals(tClass.getSimpleName())) {
+                    return (T) current;
+                }
+                current = current.getNext();
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            L.e("HandlerManager#getHandler异常:" + e.getMessage());
+        }
+        return null;
+    }
+
+    /**
+     * 设置自定义指令执行链
+     */
+    public void setCustomCmdExecuteFlow(CmdExecuteFlow flow) {
+        if (flow == null || flow.getHead() == null) {
+            throw new IllegalArgumentException("CmdExecuteFlow is null");
+        }
+        if (handlerHead == null || handlerMiddle == null) {
+            throw new IllegalStateException("You should call init() first");
+        }
+        if (!(flow.getHead() instanceof QueryStatusCmdHandler)) {
+            L.w("自定义CmdExecuteFlow头结点不为QueryStatusCmdHandler,某些条件下执行该指令执行流可能出现问题");
+        }
+        this.cmdExecuteFlow = flow;
+        handlerMiddle.setNext(cmdExecuteFlow.getHead());
+        //检查handler链表中是否含有相同类型的handler
+        checkHandlerLink(handlerHead);
+    }
+
+    /**
+     * 获取当前的命令执行链
+     */
+    public CmdExecuteFlow getCmdExecuteFlow() {
+        return cmdExecuteFlow;
+    }
+
+
+    private void initHandlerLink() {
+        HeartDataHandler h0 = new HeartDataHandler();//数据处理handler
+        StopTransferringCmdHandler h1 = new StopTransferringCmdHandler(new StopTransferringCmd());//停止实时传输handler
+        StopCollectingCmdHandler h2 = new StopCollectingCmdHandler(new StopCollectingCmd());//停止采集handler
+        QueryDeviceTimeCmdHandler h3 = new QueryDeviceTimeCmdHandler(new QueryDeviceTimeCmd());//查询设备时间handler
+        QueryDeviceTypeCmdHandler h4 = new QueryDeviceTypeCmdHandler(new QueryDeviceTypeCmd());//查询设备类型handler
+
+        h0.setNext(h1);
+        h1.setNext(h2);//停止采集handler作停止传输handler的next,以便停止传输后停止采集
+        h2.setNext(h3);
+        h3.setNext(h4);
+
+        if (cmdExecuteFlow == null) {
+            cmdExecuteFlow = getDefaultCmdExecutorFlow();
+        }
+        h4.setNext(cmdExecuteFlow.getHead());
+
+        handlerHead = h0;
+        handlerMiddle = h4;
+    }
+
+    private CmdExecuteFlow getDefaultCmdExecutorFlow() {
+        QueryStatusCmdHandler h1 = new QueryStatusCmdHandler(new QueryStatusCmd());
+        SetDeviceTimeCmdHandler h2 = new SetDeviceTimeCmdHandler(new SetDeviceTimeCmd());
+        BindUserIdCmdHandler h3 = new BindUserIdCmdHandler(new BindUserIdCmd());
+        StartCollectingCmdHandler h4 = new StartCollectingCmdHandler(new StartCollectingCmd());
+        StartTransferringCmdHandler h5 = new StartTransferringCmdHandler(new StartTransferringCmd());
+
+        h1.setNextHandlerSendCmdWhenCanHandle(true);
+        h2.setNextHandlerSendCmdWhenCanHandle(true);
+        h3.setNextHandlerSendCmdWhenCanHandle(true);
+        h4.setNextHandlerSendCmdWhenCanHandle(true);
+
+        return CmdExecuteFlow
+                .newInstance()
+                .next(h1)
+                .next(h2)
+                .next(h3)
+                .next(h4)
+                .next(h5);
+    }
+
+    /**
+     * 指令执行流
+     * <p>
+     * 注:本类只能添加的一些指定类型的指令handler,一般情况下我们不希望开发者
+     * 改变类型限制,但若开发者用特殊需求或其他极其特殊原因需要添加其他类型指令
+     * handler而又希望该类型能被CmdExecuteFlow接受,则使用反射调
+     * 用{@link CmdExecuteFlow#addPermitType(Class)}添加对应类型即可,详见该
+     * 方法注释
+     */
+    public static final class CmdExecuteFlow {
+        private BaseCmdHandler head, current;
+        private Set<String> typeSet;
+
+        private CmdExecuteFlow() {
+            restrictType();
+        }
+
+        public static CmdExecuteFlow newInstance() {
+            return new CmdExecuteFlow();
+        }
+
+        public CmdExecuteFlow next(BaseCmdHandler cmdHandler) {
+            if (cmdHandler == null) {
+                throw new IllegalArgumentException("BaseCmdHandler is null");
+            }
+            String className = cmdHandler.getClass().getSimpleName();
+            if (!typeSet.contains(className)) {
+                throw new IllegalArgumentException("This " + className + " is not permitted here");
+            }
+            if (head == null) {
+                head = cmdHandler;
+                current = head;
+            } else {
+                current.setNext(cmdHandler);
+                current = cmdHandler;
+            }
+            return this;
+        }
+
+        public BaseCmdHandler getHead() {
+            return head;
+        }
+
+        public BaseCmdHandler getCurrent() {
+            return current;
+        }
+
+        public <T extends BaseCmd> T getCmd(Class<T> tClass) {
+            try {
+                BaseCmdHandler current = head;
+                while (current != null) {
+                    String name = current.getCmd().getClass().getSimpleName();
+                    if (name.equals(tClass.getSimpleName())) {
+                        return (T) current.getCmd();
+                    }
+                    current = (BaseCmdHandler) current.getNext();
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+                L.e("CmdExecuteLink中getCmd异常:" + e.getMessage());
+            }
+            return null;
+        }
+
+        /**
+         * 查询该指令handler是否是被允许的类型
+         */
+        public boolean isPermitted(BaseCmdHandler cmdHandler) {
+            return typeSet.contains(cmdHandler.getClass().getSimpleName());
+        }
+
+        /**
+         * 获取所有允许类型handler集合
+         */
+        public List<String> getAllPermitCmdHandler() {
+            List<String> list = new ArrayList<>();
+            list.addAll(typeSet);
+            return list;
+        }
+
+        private void restrictType() {
+            addPermitType(QueryStatusCmdHandler.class);
+            addPermitType(SetDeviceTimeCmdHandler.class);
+            addPermitType(BindUserIdCmdHandler.class);
+            addPermitType(StartCollectingCmdHandler.class);
+            addPermitType(StartTransferringCmdHandler.class);
+        }
+
+        /**
+         * 添加允许的类型
+         * <p>
+         * 注:一般情况下不希望开发者改变类型限制,但若开发者用特殊需求或其他极其
+         * 特殊原因需要添加其他类型指令handler而又希望该类型能被CmdExecuteFlow接受,
+         * 则使用反射调用该方法添加对应类型即可
+         */
+        private <H extends BaseCmdHandler> void addPermitType(Class<H> tClass) {
+            if (tClass == null) return;
+            if (typeSet == null) {
+                typeSet = new HashSet<>();
+            }
+            typeSet.add(tClass.getSimpleName());
+        }
+
+    }
+
+    /**
+     * 检查handler链表中是否含有相同类型的handler
+     */
+    private static void checkHandlerLink(BaseHandler head) {
+        List<String> nameList = new ArrayList<>();
+        BaseHandler current = head;
+        while (current != null) {
+            String curLinkName = current.getClass().getSimpleName();
+            if (nameList.contains(curLinkName)) {
+                throw new IllegalStateException("The handler link has contained this handler node: " + curLinkName);
+            }
+            nameList.add(curLinkName);
+            current = current.getNext();
+        }
+    }
+}

+ 82 - 0
ecg/src/main/java/com/yuanxu/ecg/L.java

@@ -0,0 +1,82 @@
+package com.yuanxu.ecg;
+
+import android.util.Log;
+
+public class L {
+    static boolean SHOW_LOG = true;
+    static String TAG = "TAG";
+
+    public static void d(String info) {
+        if (SHOW_LOG) {
+            Log.d(TAG, info);
+        }
+    }
+
+    public static void e(String info) {
+        if (SHOW_LOG) {
+            Log.e(TAG, info);
+        }
+    }
+
+    public static void w(String info) {
+        if (SHOW_LOG) {
+            Log.w(TAG, info);
+        }
+    }
+
+    public static void v(String info) {
+        if (SHOW_LOG) {
+            Log.v(TAG, info);
+        }
+    }
+
+    public static void i(String info) {
+        if (SHOW_LOG) {
+            Log.i(TAG, info);
+        }
+    }
+
+    public static void d(Object obj, String info) {
+        if (SHOW_LOG) {
+            Log.d(tag(obj), info);
+        }
+    }
+
+    public static void e(Object obj, String info) {
+        if (SHOW_LOG) {
+            Log.e(tag(obj), info);
+        }
+    }
+
+    public static void v(Object obj, String info) {
+        if (SHOW_LOG) {
+            Log.v(tag(obj), info);
+        }
+    }
+
+    public static void w(Object obj, String info) {
+        if (SHOW_LOG) {
+            Log.w(tag(obj), info);
+        }
+    }
+
+    public static void i(Object obj, String info) {
+        if (SHOW_LOG) {
+            Log.i(tag(obj), info);
+        }
+    }
+
+    private static String tag(Object obj) {
+        if (obj == null) {
+            return TAG;
+        }
+        if (obj instanceof String) {
+            return (String) obj;
+        }
+        if (obj instanceof Number) {
+            return String.valueOf(obj);
+        }
+        return obj.getClass().getSimpleName();
+    }
+
+}

+ 155 - 0
ecg/src/main/java/com/yuanxu/ecg/MsgCenter.java

@@ -0,0 +1,155 @@
+package com.yuanxu.ecg;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import com.yuanxu.ecg.handle.cmdhandler.QueryStatusCmdHandler;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MsgCenter {
+    public static final int MSG_WHAT_CMD_SEND = 100;//发送指令
+    public static final int MSG_WHAT_CMD_ERROR = 101;//指令有误(长度不足、命令字不存在、逻辑错误)
+    public static final int MSG_WHAT_CMD_EXECUTE_FAIL = 102;//指令执行失败(索要数据不存在、当前时刻不适合执行此命令、设备错误)
+    public static final int MSG_WHAT_DEVICE_TIME = 103;//设备时间
+    public static final int MSG_WHAT_DEVICE_TYPE = 104;//设备类型
+    public static final int MSG_WHAT_DEVICE_NOT_IDLE = 105;//设备非待机状态
+    public static final int MSG_WHAT_DATA = 106;//数据
+    public static final int MSG_WHAT_STOP_TRANSFERRING_SUCCESS = 107;//停止传输发送成功
+
+
+    private Handler handler;
+    private List<MsgListener> msgListeners;
+
+
+    private MsgCenter() {
+
+    }
+
+    private static class MsgCenterHolder {
+        static final MsgCenter sMsgCenter = new MsgCenter();
+    }
+
+    public static MsgCenter getInstance() {
+        return MsgCenterHolder.sMsgCenter;
+    }
+
+    public void init() {
+        handler = new Handler(Looper.getMainLooper()) {
+            @Override
+            public void handleMessage(Message msg) {
+                super.handleMessage(msg);
+                int what = msg.what;
+                String info = msg.obj == null ? "" : (String) msg.obj;
+                notifyMsgListener(what, info);
+            }
+        };
+    }
+
+    public void release() {
+        clearAllMsgListener();
+        msgListeners = null;
+        handler = null;
+    }
+
+    private void notifyMsgListener(int type, String hexInfo) {
+        if (msgListeners == null) return;
+        for (MsgListener l : msgListeners) {
+            switch (type) {
+                case MSG_WHAT_CMD_SEND:
+                    l.onSendCmd(hexInfo);
+                    break;
+                case MSG_WHAT_CMD_ERROR:
+                    l.onCmdError();
+                    break;
+                case MSG_WHAT_CMD_EXECUTE_FAIL:
+                    l.onCmdExecuteFail(hexInfo);
+                    break;
+                case MSG_WHAT_DEVICE_TIME:
+                    l.onDeviceTime(hexInfo);
+                    break;
+                case MSG_WHAT_DEVICE_TYPE:
+                    l.onDeviceType(hexInfo);
+                    break;
+                case MSG_WHAT_DATA:
+                    l.onReceivedHeartData(hexInfo);
+                    break;
+                case MSG_WHAT_STOP_TRANSFERRING_SUCCESS:
+                    l.onStopTransferringSuccess();
+                    break;
+                case MSG_WHAT_DEVICE_NOT_IDLE:
+                    l.onDeviceNotIdle(hexInfo);
+                default:
+                    break;
+            }
+        }
+    }
+
+    public synchronized void addMsgListener(MsgListener l) {
+        if (l == null) {
+            return;
+        }
+        if (msgListeners == null) {
+            msgListeners = new ArrayList<>();
+        }
+        msgListeners.add(l);
+    }
+
+    public synchronized void removeMsgListener(MsgListener l) {
+        if (l == null || msgListeners == null) {
+            return;
+        }
+        msgListeners.remove(l);
+    }
+
+    public synchronized void clearAllMsgListener() {
+        if (msgListeners == null) return;
+        msgListeners.clear();
+    }
+
+    public void sendMsg(Message msg) {
+        handler.sendMessage(msg);
+    }
+
+    public void sendDelayMsg(Message msg, long delayMills) {
+        if (delayMills < 0) {
+            delayMills = 0;
+        }
+        handler.sendMessageDelayed(msg, delayMills);
+    }
+
+    public interface MsgListener {
+        void onSendCmd(String hexCmd);
+
+        void onDeviceType(String hexDeviceType);
+
+        void onDeviceTime(String hexDeviceTime);
+
+        /**
+         * 设备非空闲(待机)状态
+         *
+         * @param status 具体状态,详见
+         *               {@link QueryStatusCmdHandler#STATUS_COLLECTING_SYNC}
+         *               {@link QueryStatusCmdHandler#STATUS_COLLECTING_SINGLE}
+         *               {@link QueryStatusCmdHandler#STATUS_COLLECTING_REAL_TIME}
+         */
+        void onDeviceNotIdle(String status);
+
+        /**
+         * 指令有误(长度不足、命令字不存在、逻辑错误)
+         */
+        void onCmdError();
+
+        /**
+         * 指令执行失败(索要数据不存在、当前时刻不适合执行此命令、设备错误)
+         */
+        void onCmdExecuteFail(String hexStrResponse);
+
+        void onReceivedHeartData(String hexData);
+
+        void onStopTransferringSuccess();
+    }
+
+}

+ 47 - 0
ecg/src/main/java/com/yuanxu/ecg/bean/CmdType.java

@@ -0,0 +1,47 @@
+package com.yuanxu.ecg.bean;
+
+import com.yuanxu.ecg.cmd.BindUserIdCmd;
+import com.yuanxu.ecg.cmd.QueryDeviceTimeCmd;
+import com.yuanxu.ecg.cmd.QueryDeviceTypeCmd;
+import com.yuanxu.ecg.cmd.QueryStatusCmd;
+import com.yuanxu.ecg.cmd.SetDeviceTimeCmd;
+import com.yuanxu.ecg.cmd.StartCollectingCmd;
+import com.yuanxu.ecg.cmd.StartTransferringCmd;
+import com.yuanxu.ecg.cmd.StopCollectingCmd;
+import com.yuanxu.ecg.cmd.StopTransferringCmd;
+
+public enum CmdType {
+    QUERY_STATUS(QueryStatusCmd.class, "查询设备状态"),
+    QUERY_DEVICE_TIME(QueryDeviceTimeCmd.class, "查询设备时间"),
+    QUERY_DEVICE_TYPE(QueryDeviceTypeCmd.class, "查询设备类型"),
+    BIND_USER_ID(BindUserIdCmd.class, "绑定用户"),
+    SET_DEVICE_TIME(SetDeviceTimeCmd.class, "设置设备时间"),
+    START_COLLECTING(StartCollectingCmd.class, "硬件开始采集数据"),
+    START_TRANSFERRING(StartTransferringCmd.class, "硬件开始传输数据"),
+    STOP_COLLECTING(StopCollectingCmd.class, "硬件停止采集数据"),
+    STOP_TRANSFERRING(StopTransferringCmd.class, "硬件停止传输数据");
+
+
+    private String cmdClassName;//指令类名
+    private String cmdAbsoluteClassName;//指令类全路径名
+    private String cmdDetail;//指令介绍
+
+    CmdType(Class<?> cmdClass, String cmdDetail) {
+        this.cmdDetail = cmdDetail;
+        this.cmdClassName = cmdClass.getSimpleName();
+        String packageName = cmdClass.getPackage() == null ? "com.yuanxu.ecg.cmd" : cmdClass.getPackage().getName();
+        this.cmdAbsoluteClassName = packageName + "." + cmdClass.getSimpleName();
+    }
+
+    public String getCmdClassName() {
+        return cmdClassName == null ? "" : cmdClassName;
+    }
+
+    public String getCmdAbsoluteClassName() {
+        return cmdAbsoluteClassName == null ? "" : cmdAbsoluteClassName;
+    }
+
+    public String getCmdDetail() {
+        return cmdDetail == null ? "" : cmdDetail;
+    }
+}

+ 69 - 0
ecg/src/main/java/com/yuanxu/ecg/bean/UserInfo.java

@@ -0,0 +1,69 @@
+package com.yuanxu.ecg.bean;
+
+import android.text.TextUtils;
+
+import com.yuanxu.ecg.cmd.BindUserIdCmd;
+
+import java.io.Serializable;
+
+public class UserInfo implements Serializable {
+    private String name;//姓名
+    private boolean female;//是否为女性
+    private int age;//年龄
+    private int height;//身高(cm)
+    private int weight;//体重(kg)
+
+    public UserInfo(String name, boolean female, int age, int height, int weight) {
+        if (TextUtils.isEmpty(name)) {
+            throw new IllegalArgumentException("name is null");
+        }
+        if (!BindUserIdCmd.isValidNameBytesLength(name)) {
+            throw new IllegalArgumentException("the length of this name(" + name + ") is greater than max byte length(" + BindUserIdCmd.MAX_NAME_BYTE_LENGTH + ")");
+        }
+        this.name = name;
+        this.female = female;
+        this.age = age;
+        this.height = height;
+        this.weight = weight;
+    }
+
+    public String getName() {
+        return name == null ? "" : name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public boolean isFemale() {
+        return female;
+    }
+
+    public void setFemale(boolean female) {
+        this.female = female;
+    }
+
+    public int getAge() {
+        return age;
+    }
+
+    public void setAge(int age) {
+        this.age = age;
+    }
+
+    public int getHeight() {
+        return height;
+    }
+
+    public void setHeight(int height) {
+        this.height = height;
+    }
+
+    public int getWeight() {
+        return weight;
+    }
+
+    public void setWeight(int weight) {
+        this.weight = weight;
+    }
+}

+ 11 - 0
ecg/src/main/java/com/yuanxu/ecg/callback/BaseCallback.java

@@ -0,0 +1,11 @@
+package com.yuanxu.ecg.callback;
+
+public interface BaseCallback {
+    int FAIL_BLUETOOTH_NOT_AVAILABLE = 100;//蓝牙不可用
+    int FAIL_OTHER = 101;//其他原因
+
+    /**
+     * 失败
+     */
+    void onFailure(int failCode, String info);
+}

+ 8 - 0
ecg/src/main/java/com/yuanxu/ecg/callback/DeviceTimeCallback.java

@@ -0,0 +1,8 @@
+package com.yuanxu.ecg.callback;
+
+public interface DeviceTimeCallback extends BaseCallback {
+    /**
+     * 设备时间,格式yy-MM--dd HH:mm:ss
+     */
+    void onDeviceTime(String deviceTime);
+}

+ 8 - 0
ecg/src/main/java/com/yuanxu/ecg/callback/DeviceTypeCallback.java

@@ -0,0 +1,8 @@
+package com.yuanxu.ecg.callback;
+
+public interface DeviceTypeCallback extends BaseCallback {
+    /**
+     * 设备型号
+     */
+    void onDeviceType(String type);
+}

+ 53 - 0
ecg/src/main/java/com/yuanxu/ecg/callback/ExecuteCallback.java

@@ -0,0 +1,53 @@
+package com.yuanxu.ecg.callback;
+
+public interface ExecuteCallback extends BaseCallback {
+    /**
+     * 进度相关常量
+     */
+    //连接相关
+    int PROCESS_CONNECT_START = 201;//连接开始
+    int PROCESS_CONNECT_FAIL = 202;//连接失败
+    int PROCESS_CONNECTED = 203;//已连接
+    int PROCESS_DISCONNECTED = 204;//连接断开
+
+    //notify相关
+    int PROCESS_NOTIFY_START = 205;//开始notify
+    int PROCESS_NOTIFY_SUCCESS = 206;//notify成功
+    int PROCESS_NOTIFY_FAIL = 207;//notify失败
+
+    //其他
+    int PROCESS_CMD_SENDING = 208;//正在发送相关指令
+    int PROCESS_DATA_RECEIVING = 209;//正在接收硬件发回的心电数据
+    int PROCESS_IDLE = 300;//任务结束,达到空闲状态
+
+
+    //注:当前版本暂不提供数据分析、报告生成功能,故暂时注释掉
+//    int PROCESS_DATA_RECEIVE_FINISH = 210;//心电数据接收结束
+//    int PROCESS_DATA_ANALYSIS = 211;//数据分析中
+//    int PROCESS_DATA_REPORT_GENERATING = 212;//报告生成中
+
+
+    /**
+     * 失败常量
+     */
+    int FAIL_CONNECTION_ALREADY_ESTABLISHED = 301;//连接被占用(连接早已建立)
+    int FAIL_CONNECTION_START_FAIL = 302;//连接开始失败
+
+
+    /**
+     * 成功
+     */
+    void onSuccess();
+
+    /**
+     * 进度
+     *
+     * @param process 进度
+     */
+    void onProcess(int process, String info);
+
+    /**
+     * 原始心电数据
+     */
+    void onReceivedOriginalData(byte[] data);
+}

+ 58 - 0
ecg/src/main/java/com/yuanxu/ecg/cmd/BaseCmd.java

@@ -0,0 +1,58 @@
+package com.yuanxu.ecg.cmd;
+
+import android.text.TextUtils;
+
+import com.yuanxu.ecg.utils.ByteUtils;
+import com.yuanxu.ecg.L;
+
+public abstract class BaseCmd {
+    /**
+     * 获取命令
+     */
+    public abstract byte[] getCmd();
+
+    /**
+     * 获取指令头(十六进制字符串)
+     */
+    public abstract String getHexStrCmdPrefix();
+
+    /**
+     * 获取十六进制字符串cmd
+     */
+    public String getHexStringCmd() {
+        if (getCmd() == null || getCmd().length <= 0) {
+            return "";
+        }
+        return ByteUtils.bytes2HexStr(getCmd());
+    }
+
+
+    protected String generateHexStringCmdPlaceholder(int count) {
+        return generateHexStringCmdPlaceholder(count, "0");
+    }
+
+    /**
+     * 生成十六进制字符串形式的命令占位符
+     * <p>
+     * 注意:两个十六进制字符代表一个字节,生成占位符时需注意此点
+     *
+     * @param count 占位符个数
+     */
+    protected String generateHexStringCmdPlaceholder(int count, String placeHolder) {
+        if (count <= 0) {
+            return "";
+        }
+        if (count % 2 != 0) {
+            L.d("生成指令占位符个数为奇数");
+        }
+        if (TextUtils.isEmpty(placeHolder) || placeHolder.length() != 1 ||
+                !placeHolder.matches("[0-9a-fA-F]")) {
+            placeHolder = "f";
+        }
+        StringBuilder builder = new StringBuilder();
+        for (int i = 0; i < count; i++) {
+            builder.append(placeHolder);
+        }
+        return builder.toString();
+    }
+}

+ 139 - 0
ecg/src/main/java/com/yuanxu/ecg/cmd/BindUserIdCmd.java

@@ -0,0 +1,139 @@
+package com.yuanxu.ecg.cmd;
+
+import android.text.TextUtils;
+
+import com.yuanxu.ecg.utils.ByteUtils;
+
+import java.nio.charset.StandardCharsets;
+
+public class BindUserIdCmd extends BaseCmd {
+    /**
+     * 指令头(十六进制字符串形式)
+     */
+    public static final String CMD_PREFIX = "E841";
+
+    /**
+     * 用户信息部分的指令前缀(十六进制字符串形式)
+     */
+    public static final String USER_INFO_PREFIX = "AA";
+
+    /**
+     * 最大姓名字节长度
+     */
+    public static final int MAX_NAME_BYTE_LENGTH = 12;
+
+
+    private String hexName;//姓名(最多12字节)
+    private String hexGender;//性别 0x00表示女,0x01表示男
+    private String hexAge;//年龄(1个字节)
+    private String hexHeight;//身高/cm(1个字节)
+    private String hexWeight;//体重/kg(1个字节)
+
+    @Override
+    public byte[] getCmd() {
+        checkCmd();
+        StringBuilder builder = new StringBuilder();
+        //指令头
+        builder.append(CMD_PREFIX);
+        //user信息前缀
+        builder.append(USER_INFO_PREFIX);
+        //姓名(姓名字节长度 + 姓名 + 姓名不足12字节时占位符)
+        builder.append(getHexStrLengthOfNameBytes());
+        builder.append(hexName);
+        builder.append(generateHexStringCmdPlaceholder(MAX_NAME_BYTE_LENGTH * 2 - hexName.length()));
+        //性别、年龄、身高、体重等
+        builder.append(hexGender);
+        builder.append(hexAge);
+        builder.append(hexHeight);
+        builder.append(hexWeight);
+        return ByteUtils.hexStr2Bytes(builder.toString());
+    }
+
+    @Override
+    public String getHexStrCmdPrefix() {
+        return CMD_PREFIX;
+    }
+
+    /**
+     * 是否为合法的名字字节长度,名字的字节长度不超过{@link #MAX_NAME_BYTE_LENGTH}
+     */
+    public static boolean isValidNameBytesLength(String name) {
+        if (TextUtils.isEmpty(name)) {
+            return false;
+        }
+        return name.getBytes(StandardCharsets.UTF_8).length <= BindUserIdCmd.MAX_NAME_BYTE_LENGTH;
+    }
+
+    public BindUserIdCmd setName(String name) {
+        if (TextUtils.isEmpty(name)) {
+            throw new IllegalArgumentException("name is null");
+        }
+        byte[] bytes = name.getBytes(StandardCharsets.UTF_8);
+        if (bytes.length > MAX_NAME_BYTE_LENGTH) {
+            throw new IllegalArgumentException("the length of this name(" + name + ") is greater than max byte length(" + MAX_NAME_BYTE_LENGTH + ")");
+        }
+        hexName = ByteUtils.bytes2HexStr(bytes);
+        return this;
+    }
+
+    public BindUserIdCmd setGender(boolean female) {
+        hexGender = female ? "00" : "01";
+        return this;
+    }
+
+    public BindUserIdCmd setAge(int age) {
+        hexAge = getHexStrOfInt(age);
+        return this;
+    }
+
+    public BindUserIdCmd setHeight(int height) {
+        hexHeight = getHexStrOfInt(height);
+        return this;
+    }
+
+    public BindUserIdCmd setWeight(int weight) {
+        hexWeight = getHexStrOfInt(weight);
+        return this;
+    }
+
+    public String getHexGender() {
+        return hexGender == null ? "" : hexGender;
+    }
+
+    public String getHexName() {
+        return hexName == null ? "" : hexName;
+    }
+
+    public String getHexAge() {
+        return hexAge == null ? "" : hexAge;
+    }
+
+    public String getHexHeight() {
+        return hexHeight == null ? "" : hexHeight;
+    }
+
+    public String getHexWeight() {
+        return hexWeight == null ? "" : hexWeight;
+    }
+
+    private String getHexStrLengthOfNameBytes() {
+        return getHexStrOfInt(hexName.length() / 2);
+    }
+
+    /**
+     * 获取int型数据的最后一字节所标识的十六进制字符串
+     */
+    private String getHexStrOfInt(int value) {
+        byte[] bytes = ByteUtils.int2Bytes(value);
+        String str = ByteUtils.bytes2HexStr(bytes);
+        return str.substring(str.length() - 2);
+    }
+
+    private void checkCmd() {
+        if (TextUtils.isEmpty(hexName) || TextUtils.isEmpty(hexGender) ||
+                TextUtils.isEmpty(hexAge) || TextUtils.isEmpty(hexHeight) ||
+                TextUtils.isEmpty(hexWeight)) {
+            throw new IllegalStateException("Please make sure that you have set all user info(name,gender,age,height,weight)");
+        }
+    }
+}

+ 20 - 0
ecg/src/main/java/com/yuanxu/ecg/cmd/QueryDeviceTimeCmd.java

@@ -0,0 +1,20 @@
+package com.yuanxu.ecg.cmd;
+
+import com.yuanxu.ecg.utils.ByteUtils;
+
+public class QueryDeviceTimeCmd extends BaseCmd {
+    /**
+     * 指令头(十六进制字符串形式)
+     */
+    public static final String CMD_PREFIX = "E81F";
+
+    @Override
+    public byte[] getCmd() {
+        return ByteUtils.hexStr2Bytes(CMD_PREFIX + generateHexStringCmdPlaceholder(36));
+    }
+
+    @Override
+    public String getHexStrCmdPrefix() {
+        return CMD_PREFIX;
+    }
+}

+ 20 - 0
ecg/src/main/java/com/yuanxu/ecg/cmd/QueryDeviceTypeCmd.java

@@ -0,0 +1,20 @@
+package com.yuanxu.ecg.cmd;
+
+import com.yuanxu.ecg.utils.ByteUtils;
+
+public class QueryDeviceTypeCmd extends BaseCmd {
+    /**
+     * 指令头(十六进制字符串形式)
+     */
+    public static final String CMD_PREFIX = "E813";
+
+    @Override
+    public byte[] getCmd() {
+        return ByteUtils.hexStr2Bytes(CMD_PREFIX + generateHexStringCmdPlaceholder(36));
+    }
+
+    @Override
+    public String getHexStrCmdPrefix() {
+        return CMD_PREFIX;
+    }
+}

+ 20 - 0
ecg/src/main/java/com/yuanxu/ecg/cmd/QueryStatusCmd.java

@@ -0,0 +1,20 @@
+package com.yuanxu.ecg.cmd;
+
+import com.yuanxu.ecg.utils.ByteUtils;
+
+public class QueryStatusCmd extends BaseCmd {
+    /**
+     * 指令头(十六进制字符串形式)
+     */
+    public static final String CMD_PREFIX = "E810";
+
+    @Override
+    public byte[] getCmd() {
+        return ByteUtils.hexStr2Bytes(CMD_PREFIX + generateHexStringCmdPlaceholder(36));
+    }
+
+    @Override
+    public String getHexStrCmdPrefix() {
+        return CMD_PREFIX;
+    }
+}

+ 52 - 0
ecg/src/main/java/com/yuanxu/ecg/cmd/SetDeviceTimeCmd.java

@@ -0,0 +1,52 @@
+package com.yuanxu.ecg.cmd;
+
+import com.yuanxu.ecg.utils.ByteUtils;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public class SetDeviceTimeCmd extends BaseCmd {
+    /**
+     * 指令头(十六进制字符串形式)
+     */
+    public static final String CMD_PREFIX = "E840";
+
+    private long timestamp = Long.MIN_VALUE;
+
+    @Override
+    public byte[] getCmd() {
+        return ByteUtils.hexStr2Bytes(CMD_PREFIX + getHexStrDeviceTime());
+    }
+
+    @Override
+    public String getHexStrCmdPrefix() {
+        return CMD_PREFIX;
+    }
+
+    protected String getHexStrDeviceTime() {
+        if (timestamp <= Long.MIN_VALUE) {
+            return generateHexStringCmdPlaceholder(12);
+        }
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        String timeStr = sdf.format(new Date(timestamp));
+        String[] arr = timeStr.split(" ");
+        String[] date = arr[0].split("-");
+        String[] time = arr[1].split(":");
+
+        byte year = Byte.valueOf(date[0].substring(date[0].length() - 2));
+        byte month = Byte.valueOf(date[1]);
+        byte day = Byte.valueOf(date[2]);
+        byte hour = Byte.valueOf(time[0]);
+        byte minute = Byte.valueOf(time[1]);
+        byte second = Byte.valueOf(time[2]);
+
+        return ByteUtils.byte2HexStr(year) + ByteUtils.byte2HexStr(month) + ByteUtils.byte2HexStr(day)
+                + ByteUtils.byte2HexStr(hour) + ByteUtils.byte2HexStr(minute) + ByteUtils.byte2HexStr(second);
+    }
+
+    public SetDeviceTimeCmd setDeviceTimestamp(long mills) {
+        this.timestamp = mills;
+        return this;
+    }
+
+}

+ 51 - 0
ecg/src/main/java/com/yuanxu/ecg/cmd/StartCollectingCmd.java

@@ -0,0 +1,51 @@
+package com.yuanxu.ecg.cmd;
+
+import com.yuanxu.ecg.utils.ByteUtils;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public class StartCollectingCmd extends BaseCmd {
+    /**
+     * 指令头(十六进制字符串形式)
+     */
+    public static final String CMD_PREFIX = "E823";
+
+    private long timestamp = Long.MIN_VALUE;
+
+    @Override
+    public byte[] getCmd() {
+        return ByteUtils.hexStr2Bytes(CMD_PREFIX + getHexStrDeviceTime());
+    }
+
+    @Override
+    public String getHexStrCmdPrefix() {
+        return CMD_PREFIX;
+    }
+
+    protected String getHexStrDeviceTime() {
+        if (timestamp <= Long.MIN_VALUE) {
+            return generateHexStringCmdPlaceholder(12);
+        }
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        String timeStr = sdf.format(new Date(timestamp));
+        String[] arr = timeStr.split(" ");
+        String[] date = arr[0].split("-");
+        String[] time = arr[1].split(":");
+
+        byte year = Byte.valueOf(date[0].substring(date[0].length() - 2));
+        byte month = Byte.valueOf(date[1]);
+        byte day = Byte.valueOf(date[2]);
+        byte hour = Byte.valueOf(time[0]);
+        byte minute = Byte.valueOf(time[1]);
+        byte second = Byte.valueOf(time[2]);
+
+        return ByteUtils.byte2HexStr(year) + ByteUtils.byte2HexStr(month) + ByteUtils.byte2HexStr(day)
+                + ByteUtils.byte2HexStr(hour) + ByteUtils.byte2HexStr(minute) + ByteUtils.byte2HexStr(second);
+    }
+
+    public StartCollectingCmd setTimestamp(long mills) {
+        this.timestamp = mills;
+        return this;
+    }
+}

+ 20 - 0
ecg/src/main/java/com/yuanxu/ecg/cmd/StartTransferringCmd.java

@@ -0,0 +1,20 @@
+package com.yuanxu.ecg.cmd;
+
+import com.yuanxu.ecg.utils.ByteUtils;
+
+public class StartTransferringCmd extends BaseCmd {
+    /**
+     * 指令头(十六进制字符串形式)
+     */
+    public static final String CMD_PREFIX = "E820";
+
+    @Override
+    public byte[] getCmd() {
+        return ByteUtils.hexStr2Bytes(CMD_PREFIX + "0136EE80");
+    }
+
+    @Override
+    public String getHexStrCmdPrefix() {
+        return CMD_PREFIX;
+    }
+}

+ 20 - 0
ecg/src/main/java/com/yuanxu/ecg/cmd/StopCollectingCmd.java

@@ -0,0 +1,20 @@
+package com.yuanxu.ecg.cmd;
+
+import com.yuanxu.ecg.utils.ByteUtils;
+
+public class StopCollectingCmd extends BaseCmd {
+    /**
+     * 指令头(十六进制字符串形式)
+     */
+    public static final String CMD_PREFIX = "E822";
+
+    @Override
+    public byte[] getCmd() {
+        return ByteUtils.hexStr2Bytes(CMD_PREFIX + generateHexStringCmdPlaceholder(36));
+    }
+
+    @Override
+    public String getHexStrCmdPrefix() {
+        return CMD_PREFIX;
+    }
+}

+ 20 - 0
ecg/src/main/java/com/yuanxu/ecg/cmd/StopTransferringCmd.java

@@ -0,0 +1,20 @@
+package com.yuanxu.ecg.cmd;
+
+import com.yuanxu.ecg.utils.ByteUtils;
+
+public class StopTransferringCmd extends BaseCmd {
+    /**
+     * 指令头(十六进制字符串形式)
+     */
+    public static final String CMD_PREFIX = "E826";
+
+    @Override
+    public byte[] getCmd() {
+        return ByteUtils.hexStr2Bytes(CMD_PREFIX + generateHexStringCmdPlaceholder(36));
+    }
+
+    @Override
+    public String getHexStrCmdPrefix() {
+        return CMD_PREFIX;
+    }
+}

+ 14 - 0
ecg/src/main/java/com/yuanxu/ecg/exception/InvalidAddressException.java

@@ -0,0 +1,14 @@
+package com.yuanxu.ecg.exception;
+
+public class InvalidAddressException extends Exception {
+    private String invalidAddress;
+
+    public InvalidAddressException(String address) {
+        super("Invalid address:" + address);
+        invalidAddress = address;
+    }
+
+    public String getInvalidAddress() {
+        return invalidAddress;
+    }
+}

+ 15 - 0
ecg/src/main/java/com/yuanxu/ecg/exception/PermissionException.java

@@ -0,0 +1,15 @@
+package com.yuanxu.ecg.exception;
+
+public class PermissionException extends Exception {
+
+    private String missingPermission;
+
+    public PermissionException(String missingPermission) {
+        super("no permission:" + missingPermission);
+        this.missingPermission = missingPermission;
+    }
+
+    public String getMissingPermission() {
+        return missingPermission;
+    }
+}

+ 31 - 0
ecg/src/main/java/com/yuanxu/ecg/handle/BaseHandler.java

@@ -0,0 +1,31 @@
+package com.yuanxu.ecg.handle;
+
+public abstract class BaseHandler {
+    protected BaseHandler next;
+
+    public BaseHandler setNext(BaseHandler next) {
+        this.next = next;
+        return this;
+    }
+
+    public BaseHandler getNext(){
+        return next;
+    }
+
+    public void handleResponse(String hexResponse) {
+        if (handle(hexResponse)) {
+            return;
+        }
+        if (next != null) {
+            next.handleResponse(hexResponse);
+        }
+    }
+
+    /**
+     * 处理下位机响应信息
+     *
+     * @param hexResponse 十六进制字符串形式回复msg
+     * @return 是否处理,true表示能处理,false反之
+     */
+    protected abstract boolean handle(String hexResponse);
+}

+ 28 - 0
ecg/src/main/java/com/yuanxu/ecg/handle/HeartDataHandler.java

@@ -0,0 +1,28 @@
+package com.yuanxu.ecg.handle;
+
+import android.os.Message;
+
+import com.yuanxu.ecg.MsgCenter;
+
+public class HeartDataHandler extends BaseHandler {
+    /**
+     * 数据信息头(十六进制字符串)
+     */
+    public static final String DATA_HEAD_HEX_STR = "FDFE";
+
+    @Override
+    protected boolean handle(String hexResponse) {
+        if (!hexResponse.startsWith(DATA_HEAD_HEX_STR) && !hexResponse.startsWith(DATA_HEAD_HEX_STR.toLowerCase())) {
+            return false;
+        }
+        Message msg = Message.obtain();
+        msg.what = MsgCenter.MSG_WHAT_DATA;
+        msg.obj = hexResponse.substring(4);
+        MsgCenter.getInstance().sendMsg(msg);
+        return true;
+    }
+
+    private String getDataHeadHexStr() {
+        return DATA_HEAD_HEX_STR;
+    }
+}

+ 118 - 0
ecg/src/main/java/com/yuanxu/ecg/handle/cmdhandler/BaseCmdHandler.java

@@ -0,0 +1,118 @@
+package com.yuanxu.ecg.handle.cmdhandler;
+
+import android.os.Message;
+
+import com.yuanxu.ecg.MsgCenter;
+import com.yuanxu.ecg.cmd.BaseCmd;
+import com.yuanxu.ecg.handle.BaseHandler;
+import com.yuanxu.ecg.L;
+
+public abstract class BaseCmdHandler<T extends BaseCmd> extends BaseHandler {
+    protected static String sHexStrCmdErrorResponse = "E8FF00000000";
+
+    protected T cmd;
+
+    /**
+     * 当本级指令handler能够处理且处理完硬件返回msg后,是否让下一级指令Handler发送命令
+     * <p>
+     * 应用场景:当发送状态指令后,收到硬件返回的状态msg,若硬件处于待机状态,则继续发送
+     * 如绑定用户或单机采集等指令进行绑定或采集流程
+     * <p>
+     * 注意:该值设置为true应谨慎,例如若开始单机采集handler的next为停止采集handler,此
+     * 时若单机采集handler的nextHandlerSendCmdWhenCanHandle设置为true,则会出现在收到
+     * 单机采集指令的回复后立刻发送停止采集指令
+     */
+    protected boolean nextHandlerSendCmdWhenCanHandle;
+
+    public BaseCmdHandler(T cmd) {
+        if (cmd == null) {
+            throw new IllegalStateException("cmd is null");
+        }
+        this.cmd = cmd;
+    }
+
+    /**
+     * 设置当本级指令handler能够处理且处理完硬件返回msg后,是否让下一级指令Handler发送
+     * 命令,详细见{@link #nextHandlerSendCmdWhenCanHandle}注释说明
+     */
+    public BaseCmdHandler setNextHandlerSendCmdWhenCanHandle(boolean nextHandlerSendCmdWhenCanHandle) {
+        this.nextHandlerSendCmdWhenCanHandle = nextHandlerSendCmdWhenCanHandle;
+        return this;
+    }
+
+    public boolean isNextHandlerSendCmdWhenCanHandle() {
+        return nextHandlerSendCmdWhenCanHandle;
+    }
+
+    /**
+     * 获取命令
+     */
+    public T getCmd() {
+        return cmd;
+    }
+
+    /**
+     * 发送指令至设备
+     */
+    public void sendCmdToDevice() {
+        Message msg = Message.obtain();
+        msg.what = MsgCenter.MSG_WHAT_CMD_SEND;
+        msg.obj = cmd.getHexStringCmd();
+        MsgCenter.getInstance().sendMsg(msg);
+    }
+
+    @Override
+    protected boolean handle(String hexResponse) {
+        //指令有误(长度不足、命令字不存在、逻辑错误)
+        if (hexResponse.equalsIgnoreCase(sHexStrCmdErrorResponse) || hexResponse.startsWith(sHexStrCmdErrorResponse)) {
+            L.e("指令有误");
+            handleCmdError();
+            return true;
+        }
+        //指令执行失败(索要数据不存在、当前时刻不适合执行此命令、设备错误)
+        if (hexResponse.equalsIgnoreCase(getCmdExecuteFailResponse())) {
+            L.e(cmd.getClass().getSimpleName() + "指令执行失败");
+            handleCmdExecuteFail(hexResponse);
+            return true;
+        }
+        return handleCmdResponse(hexResponse);
+    }
+
+    /**
+     * 处理指令回复msg
+     *
+     * @param hexResponse 下位机(硬件)返回的十六进制字符串形式的指令回复msg
+     * @return 是否为本指令的回复msg(即是否能够处理),true表示能处理,false反之
+     */
+    protected abstract boolean handleCmdResponse(String hexResponse);
+
+
+    /**
+     * 处理指令有误(长度不足、命令字不存在、逻辑错误)
+     */
+    protected void handleCmdError() {
+        Message msg = Message.obtain();
+        msg.what = MsgCenter.MSG_WHAT_CMD_ERROR;
+        MsgCenter.getInstance().sendMsg(msg);
+    }
+
+    /**
+     * 指令执行失败(索要数据不存在、当前时刻不适合执行此命令、设备错误)
+     */
+    protected void handleCmdExecuteFail(String originalHexResponse) {
+        Message msg = Message.obtain();
+        msg.what = MsgCenter.MSG_WHAT_CMD_EXECUTE_FAIL;
+        msg.obj = originalHexResponse;
+        MsgCenter.getInstance().sendMsg(msg);
+    }
+
+    /**
+     * 获取指令发送失败回复(索要数据不存在、当前时刻不适合执行此命令、设备错误)
+     * <p>
+     * 若硬件改变了协议回复内容,则重写该方法即可
+     */
+    protected String getCmdExecuteFailResponse() {
+        return cmd.getHexStrCmdPrefix() + "00000000";
+    }
+
+}

+ 45 - 0
ecg/src/main/java/com/yuanxu/ecg/handle/cmdhandler/BindUserIdCmdHandler.java

@@ -0,0 +1,45 @@
+package com.yuanxu.ecg.handle.cmdhandler;
+
+import android.text.TextUtils;
+
+import com.yuanxu.ecg.cmd.BindUserIdCmd;
+import com.yuanxu.ecg.L;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class BindUserIdCmdHandler extends BaseCmdHandler<BindUserIdCmd> {
+
+    private Pattern responsePatter = Pattern.compile(BindUserIdCmd.CMD_PREFIX + "000000([0-9a-fA-F]{2})", Pattern.CASE_INSENSITIVE);
+
+    public BindUserIdCmdHandler(BindUserIdCmd cmd) {
+        super(cmd);
+    }
+
+    @Override
+    public void sendCmdToDevice() {
+        super.sendCmdToDevice();
+        L.d("发送绑定用户指令:" + cmd.getHexStringCmd());
+    }
+
+    @Override
+    protected boolean handleCmdResponse(String hexResponse) {
+        L.d(getClass().getSimpleName() + "处理====" + hexResponse);
+        Matcher matcher = responsePatter.matcher(hexResponse);
+        if (matcher.matches()) {
+            String result = matcher.group(1);
+            boolean success = !TextUtils.isEmpty(result) && result.equals("01");
+            L.d("绑定用户" + (success ? "成功" : "失败"));
+            if (success) {
+                if (next != null && nextHandlerSendCmdWhenCanHandle && next instanceof BaseCmdHandler) {
+                    ((BaseCmdHandler) next).sendCmdToDevice();
+                }
+            } else {
+                //绑定失败,重新发送
+                sendCmdToDevice();
+            }
+            return true;
+        }
+        return false;
+    }
+}

+ 40 - 0
ecg/src/main/java/com/yuanxu/ecg/handle/cmdhandler/QueryDeviceTimeCmdHandler.java

@@ -0,0 +1,40 @@
+package com.yuanxu.ecg.handle.cmdhandler;
+
+import android.os.Message;
+
+import com.yuanxu.ecg.MsgCenter;
+import com.yuanxu.ecg.cmd.QueryDeviceTimeCmd;
+import com.yuanxu.ecg.L;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class QueryDeviceTimeCmdHandler extends BaseCmdHandler<QueryDeviceTimeCmd> {
+    private Pattern responsePatter = Pattern.compile(QueryDeviceTimeCmd.CMD_PREFIX + "(\\w{12})", Pattern.CASE_INSENSITIVE);
+
+    public QueryDeviceTimeCmdHandler(QueryDeviceTimeCmd cmd) {
+        super(cmd);
+    }
+
+    @Override
+    public void sendCmdToDevice() {
+        super.sendCmdToDevice();
+        L.d("发送查询设备时间指令:" + cmd.getHexStringCmd());
+    }
+
+    @Override
+    protected boolean handleCmdResponse(String hexResponse) {
+        L.d(getClass().getSimpleName() + "处理====" + hexResponse);
+        Matcher matcher = responsePatter.matcher(hexResponse);
+        if (matcher.matches()) {
+            String result = matcher.group(1);
+            Message msg = Message.obtain();
+            msg.what = MsgCenter.MSG_WHAT_DEVICE_TIME;
+            msg.obj = result;
+            MsgCenter.getInstance().sendMsg(msg);
+            return true;
+        }
+        return false;
+    }
+
+}

+ 39 - 0
ecg/src/main/java/com/yuanxu/ecg/handle/cmdhandler/QueryDeviceTypeCmdHandler.java

@@ -0,0 +1,39 @@
+package com.yuanxu.ecg.handle.cmdhandler;
+
+import android.os.Message;
+
+import com.yuanxu.ecg.MsgCenter;
+import com.yuanxu.ecg.cmd.QueryDeviceTypeCmd;
+import com.yuanxu.ecg.L;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class QueryDeviceTypeCmdHandler extends BaseCmdHandler<QueryDeviceTypeCmd> {
+    private Pattern responsePatter = Pattern.compile(QueryDeviceTypeCmd.CMD_PREFIX + "(\\w{28})", Pattern.CASE_INSENSITIVE);
+
+    public QueryDeviceTypeCmdHandler(QueryDeviceTypeCmd cmd) {
+        super(cmd);
+    }
+
+    @Override
+    public void sendCmdToDevice() {
+        super.sendCmdToDevice();
+        L.d("发送查询设备类型指令:" + cmd.getHexStringCmd());
+    }
+
+    @Override
+    protected boolean handleCmdResponse(String hexResponse) {
+        L.d(getClass().getSimpleName() + "处理====" + hexResponse);
+        Matcher matcher = responsePatter.matcher(hexResponse);
+        if (matcher.matches()) {
+            String result = matcher.group(1);
+            Message msg = Message.obtain();
+            msg.what = MsgCenter.MSG_WHAT_DEVICE_TYPE;
+            msg.obj = result;
+            MsgCenter.getInstance().sendMsg(msg);
+            return true;
+        }
+        return false;
+    }
+}

+ 60 - 0
ecg/src/main/java/com/yuanxu/ecg/handle/cmdhandler/QueryStatusCmdHandler.java

@@ -0,0 +1,60 @@
+package com.yuanxu.ecg.handle.cmdhandler;
+
+import android.os.Message;
+
+import com.yuanxu.ecg.MsgCenter;
+import com.yuanxu.ecg.cmd.QueryStatusCmd;
+import com.yuanxu.ecg.L;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class QueryStatusCmdHandler extends BaseCmdHandler<QueryStatusCmd> {
+    /**
+     * 状态类型
+     */
+    public static final String STATUS_IDLE = "00";//待机
+    public static final String STATUS_COLLECTING_REAL_TIME = "01";//实时采集
+    public static final String STATUS_COLLECTING_SYNC = "02";//同步采集
+    public static final String STATUS_COLLECTING_SINGLE = "03";//单机采集
+
+    private Pattern responsePatter = Pattern.compile(QueryStatusCmd.CMD_PREFIX + "([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})",
+            Pattern.CASE_INSENSITIVE);
+
+    public QueryStatusCmdHandler(QueryStatusCmd cmd) {
+        super(cmd);
+    }
+
+    @Override
+    public void sendCmdToDevice() {
+        super.sendCmdToDevice();
+        L.d("发送查询设备状态指令:" + cmd.getHexStringCmd());
+    }
+
+    @Override
+    protected boolean handleCmdResponse(String hexResponse) {
+        L.d(getClass().getSimpleName() + "处理====" + hexResponse);
+        Matcher matcher = responsePatter.matcher(hexResponse);
+        if (matcher.matches()) {
+            String batteryCode = matcher.group(1);
+            String status = matcher.group(2);
+            String batteryX = matcher.group(3);
+            String batteryY = matcher.group(4);
+            boolean idle = STATUS_IDLE.equals(status);
+            L.d("设备状态为" + (idle ? "待机" : "非待机状态") + " status=" + status);
+            if (idle) { //待机状态,可以执行采集等其他指令
+                if (next != null && nextHandlerSendCmdWhenCanHandle &&
+                        next instanceof BaseCmdHandler) {
+                    ((BaseCmdHandler) next).sendCmdToDevice();
+                }
+            } else {//非待机状态
+                Message msg = Message.obtain();
+                msg.what = MsgCenter.MSG_WHAT_DEVICE_NOT_IDLE;
+                msg.obj = status;
+                MsgCenter.getInstance().sendMsg(msg);
+            }
+            return true;
+        }
+        return false;
+    }
+}

+ 44 - 0
ecg/src/main/java/com/yuanxu/ecg/handle/cmdhandler/SetDeviceTimeCmdHandler.java

@@ -0,0 +1,44 @@
+package com.yuanxu.ecg.handle.cmdhandler;
+
+import android.text.TextUtils;
+
+import com.yuanxu.ecg.cmd.SetDeviceTimeCmd;
+import com.yuanxu.ecg.L;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class SetDeviceTimeCmdHandler extends BaseCmdHandler<SetDeviceTimeCmd> {
+    private Pattern responsePatter = Pattern.compile(SetDeviceTimeCmd.CMD_PREFIX + "000000([0-9a-fA-F]{2})", Pattern.CASE_INSENSITIVE);
+
+    public SetDeviceTimeCmdHandler(SetDeviceTimeCmd cmd) {
+        super(cmd);
+    }
+
+    @Override
+    public void sendCmdToDevice() {
+        super.sendCmdToDevice();
+        L.d("发送设置设备时间指令:" + cmd.getHexStringCmd());
+    }
+
+    @Override
+    protected boolean handleCmdResponse(String hexResponse) {
+        L.d(getClass().getSimpleName() + "处理====" + hexResponse);
+        Matcher matcher = responsePatter.matcher(hexResponse);
+        if (matcher.matches()) {
+            String result = matcher.group(1);
+            boolean success = !TextUtils.isEmpty(result) && result.equals("01");
+            L.d("设置系统时间命令" + (success ? "成功" : "失败"));
+            if (success) {
+                if (next != null && nextHandlerSendCmdWhenCanHandle && next instanceof BaseCmdHandler) {
+                    ((BaseCmdHandler) next).sendCmdToDevice();
+                }
+            } else {
+                //发送失败,重新发送
+                sendCmdToDevice();
+            }
+            return true;
+        }
+        return false;
+    }
+}

+ 45 - 0
ecg/src/main/java/com/yuanxu/ecg/handle/cmdhandler/StartCollectingCmdHandler.java

@@ -0,0 +1,45 @@
+package com.yuanxu.ecg.handle.cmdhandler;
+
+import android.text.TextUtils;
+
+import com.yuanxu.ecg.cmd.StartCollectingCmd;
+import com.yuanxu.ecg.L;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class StartCollectingCmdHandler extends BaseCmdHandler<StartCollectingCmd> {
+
+    private Pattern responsePatter = Pattern.compile(StartCollectingCmd.CMD_PREFIX + "000000([0-9a-fA-F]{2})", Pattern.CASE_INSENSITIVE);
+
+    public StartCollectingCmdHandler(StartCollectingCmd cmd) {
+        super(cmd);
+    }
+
+    @Override
+    public void sendCmdToDevice() {
+        super.sendCmdToDevice();
+        L.d("发送开始采集指令:" + cmd.getHexStringCmd());
+    }
+
+    @Override
+    protected boolean handleCmdResponse(String hexResponse) {
+        L.d(getClass().getSimpleName() + "处理====" + hexResponse);
+        Matcher matcher = responsePatter.matcher(hexResponse);
+        if (matcher.matches()) {
+            String result = matcher.group(1);
+            boolean success = !TextUtils.isEmpty(result) && result.equals("01");
+            L.d("发送单机采集命令" + (success ? "成功" : "失败"));
+            if (success) {
+                if (next != null && nextHandlerSendCmdWhenCanHandle && next instanceof BaseCmdHandler) {
+                    ((BaseCmdHandler) next).sendCmdToDevice();
+                }
+            } else {
+                //发送失败,重新发送
+                sendCmdToDevice();
+            }
+            return true;
+        }
+        return false;
+    }
+}

+ 25 - 0
ecg/src/main/java/com/yuanxu/ecg/handle/cmdhandler/StartTransferringCmdHandler.java

@@ -0,0 +1,25 @@
+package com.yuanxu.ecg.handle.cmdhandler;
+
+import com.yuanxu.ecg.cmd.StartTransferringCmd;
+import com.yuanxu.ecg.L;
+
+public class StartTransferringCmdHandler extends BaseCmdHandler<StartTransferringCmd> {
+
+    public StartTransferringCmdHandler(StartTransferringCmd cmd) {
+        super(cmd);
+    }
+
+    @Override
+    public void sendCmdToDevice() {
+        super.sendCmdToDevice();
+        L.d("发送开始传输指令:" + cmd.getHexStringCmd());
+    }
+
+    @Override
+    protected boolean handleCmdResponse(String hexResponse) {
+        L.d(getClass().getSimpleName() + "处理====" + hexResponse);
+        //传输指令发送后并无指令回应信息,硬件会直接传输心电数据至本设备,
+        //而心电数据处理单独在HeartDataHandler中处理,故此处直接返回false
+        return false;
+    }
+}

+ 40 - 0
ecg/src/main/java/com/yuanxu/ecg/handle/cmdhandler/StopCollectingCmdHandler.java

@@ -0,0 +1,40 @@
+package com.yuanxu.ecg.handle.cmdhandler;
+
+import android.text.TextUtils;
+
+import com.yuanxu.ecg.cmd.StopCollectingCmd;
+import com.yuanxu.ecg.L;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class StopCollectingCmdHandler extends BaseCmdHandler<StopCollectingCmd> {
+    private Pattern responsePatter = Pattern.compile(StopCollectingCmd.CMD_PREFIX + "000000([0-9a-fA-F]{2})", Pattern.CASE_INSENSITIVE);
+
+    public StopCollectingCmdHandler(StopCollectingCmd cmd) {
+        super(cmd);
+    }
+
+    @Override
+    public void sendCmdToDevice() {
+        super.sendCmdToDevice();
+        L.d("发送停止采集指令:" + cmd.getHexStringCmd());
+    }
+
+    @Override
+    protected boolean handleCmdResponse(String hexResponse) {
+        L.d(getClass().getSimpleName() + "处理====" + hexResponse);
+        Matcher matcher = responsePatter.matcher(hexResponse);
+        if (matcher.matches()) {
+            String result = matcher.group(1);
+            boolean success = !TextUtils.isEmpty(result) && result.equals("01");
+            L.d("停止采集命令" + (success ? "成功" : "失败"));
+            if (!success) {
+                //发送失败,重新发送
+                sendCmdToDevice();
+            }
+            return true;
+        }
+        return false;
+    }
+}

+ 49 - 0
ecg/src/main/java/com/yuanxu/ecg/handle/cmdhandler/StopTransferringCmdHandler.java

@@ -0,0 +1,49 @@
+package com.yuanxu.ecg.handle.cmdhandler;
+
+import android.os.Message;
+import android.text.TextUtils;
+
+import com.yuanxu.ecg.MsgCenter;
+import com.yuanxu.ecg.cmd.StopTransferringCmd;
+import com.yuanxu.ecg.L;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class StopTransferringCmdHandler extends BaseCmdHandler<StopTransferringCmd> {
+    private Pattern responsePatter = Pattern.compile(StopTransferringCmd.CMD_PREFIX + "000000([0-9a-fA-F]{2})", Pattern.CASE_INSENSITIVE);
+
+    public StopTransferringCmdHandler(StopTransferringCmd cmd) {
+        super(cmd);
+    }
+
+    @Override
+    public void sendCmdToDevice() {
+        super.sendCmdToDevice();
+        L.d("发送停止传输指令:" + cmd.getHexStringCmd());
+    }
+
+    @Override
+    protected boolean handleCmdResponse(String hexResponse) {
+        L.d(getClass().getSimpleName() + "处理====" + hexResponse);
+        Matcher matcher = responsePatter.matcher(hexResponse);
+        if (matcher.matches()) {
+            String result = matcher.group(1);
+            boolean success = !TextUtils.isEmpty(result) && result.equals("01");
+            L.d("停止实时传输命令" + (success ? "成功" : "失败"));
+            if (success) {
+                if (next != null && nextHandlerSendCmdWhenCanHandle && next instanceof BaseCmdHandler) {
+                    ((BaseCmdHandler) next).sendCmdToDevice();
+                }
+                Message msg = Message.obtain();
+                msg.what = MsgCenter.MSG_WHAT_STOP_TRANSFERRING_SUCCESS;
+                MsgCenter.getInstance().sendMsg(msg);
+            } else {
+                //发送失败,重新发送
+                sendCmdToDevice();
+            }
+            return true;
+        }
+        return false;
+    }
+}

+ 43 - 0
ecg/src/main/java/com/yuanxu/ecg/utils/ByteUtils.java

@@ -0,0 +1,43 @@
+package com.yuanxu.ecg.utils;
+
+public class ByteUtils {
+    public static String bytes2HexStr(byte[] bytes) {
+        if (bytes == null) {
+            return null;
+        }
+        StringBuilder b = new StringBuilder();
+        for (int i = 0; i < bytes.length; i++) {
+            b.append(String.format("%02x", bytes[i] & 0xFF));
+        }
+        return b.toString();
+    }
+
+    public static String byte2HexStr(byte b) {
+        return String.format("%02x", b & 0xFF);
+    }
+
+    public static byte[] hexStr2Bytes(String str) {
+        if (str == null) {
+            return null;
+        }
+        if (str.length() == 0) {
+            return new byte[0];
+        }
+        byte[] byteArray = new byte[str.length() / 2];
+        for (int i = 0; i < byteArray.length; i++) {
+            String subStr = str.substring(2 * i, 2 * i + 2);
+            byteArray[i] = ((byte) Integer.parseInt(subStr, 16));
+        }
+        return byteArray;
+    }
+
+    public static byte[] int2Bytes(int i) {
+        byte[] result = new byte[4];
+        //由高位到低位
+        result[0] = (byte) ((i >> 24) & 0xFF);
+        result[1] = (byte) ((i >> 16) & 0xFF);
+        result[2] = (byte) ((i >> 8) & 0xFF);
+        result[3] = (byte) (i & 0xFF);
+        return result;
+    }
+}

+ 43 - 0
ecg/src/main/java/com/yuanxu/ecg/utils/DeviceInfoParser.java

@@ -0,0 +1,43 @@
+package com.yuanxu.ecg.utils;
+
+import java.nio.charset.StandardCharsets;
+
+public class DeviceInfoParser {
+    public static String parseDeviceTime(String hexDeviceTime) {
+        return parseDeviceTime(ByteUtils.hexStr2Bytes(hexDeviceTime));
+    }
+
+    public static String parseDeviceTime(byte[] bytes) {
+        String str = "";
+        try {
+            if (bytes != null) {
+                int year = bytes[0] + 2000;
+                int month = bytes[1];
+                int day = bytes[2];
+                int hour = bytes[3];
+                int minute = bytes[4];
+                int second = bytes[5];
+                str = year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second;
+            }
+            return str;
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return "";
+    }
+
+    public static String parseDeviceType(String hexDeviceType) {
+        return parseDeviceType(ByteUtils.hexStr2Bytes(hexDeviceType));
+    }
+
+    public static String parseDeviceType(byte[] bytes) {
+        try {
+            if (bytes != null) {
+                return new String(bytes, StandardCharsets.UTF_8);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return "";
+    }
+}

+ 3 - 0
ecg/src/main/res/values/strings.xml

@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">ecg</string>
+</resources>

+ 17 - 0
ecg/src/test/java/com/yuanxu/ecg/ExampleUnitTest.java

@@ -0,0 +1,17 @@
+package com.yuanxu.ecg;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() {
+        assertEquals(4, 2 + 2);
+    }
+}

+ 3 - 0
sample/build.gradle

@@ -32,6 +32,8 @@ dependencies {
     implementation deps.json
 
     implementation project(path :':megablelibopen')
+    implementation project(path:':ecg')
+    implementation project(path:':signalproc-release')
 
     // https://mvnrepository.com/artifact/io.reactivex.rxjava2/rxjava
     implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
@@ -44,4 +46,5 @@ dependencies {
     // https://mvnrepository.com/artifact/org.java-websocket/Java-WebSocket
     implementation 'org.java-websocket:Java-WebSocket:1.5.3'
     implementation 'com.github.hijesse:android-logger:2.5.0'
+
 }

+ 3 - 1
sample/src/main/java/com/yanzhenjie/andserver/sample/MainActivity.java

@@ -16,6 +16,7 @@
 package com.yanzhenjie.andserver.sample;
 
 import android.Manifest;
+import android.app.Application;
 import android.app.PendingIntent;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothManager;
@@ -69,7 +70,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
     SocketServer socketServer;
 
     public static Context context;
-
+    public static Application application;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -106,6 +107,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
         BluetoothManager bluetoothManager = (BluetoothManager) this.getSystemService(Context.BLUETOOTH_SERVICE);
         mBluetoothAdapter = bluetoothManager.getAdapter();
         context = this.getApplicationContext();
+        application = getApplication();
         new Thread(() -> {
             String address = HostUtil.getHostIp();
             InetSocketAddress inetSocketAddress = new InetSocketAddress(address, Constant.WEB_SOCKET_PORT);

+ 140 - 157
sample/src/main/java/com/yanzhenjie/andserver/sample/controller/CommonController.java

@@ -11,18 +11,25 @@ import androidx.annotation.RequiresApi;
 import com.yanzhenjie.andserver.annotation.GetMapping;
 import com.yanzhenjie.andserver.annotation.PathVariable;
 import com.yanzhenjie.andserver.annotation.PostMapping;
+import com.yanzhenjie.andserver.annotation.RequestBody;
 import com.yanzhenjie.andserver.annotation.RequestMapping;
+import com.yanzhenjie.andserver.annotation.RequestMethod;
 import com.yanzhenjie.andserver.annotation.RequestParam;
 import com.yanzhenjie.andserver.annotation.RestController;
 import com.yanzhenjie.andserver.sample.MainActivity;
+import com.yanzhenjie.andserver.sample.model.ConnectBluetoothRequestParam;
+import com.yanzhenjie.andserver.sample.model.EcgUserInfo;
 import com.yanzhenjie.andserver.sample.model.ScannedDevice;
+import com.yanzhenjie.andserver.sample.model.UserInfo;
 import com.yanzhenjie.andserver.sample.util.Constant;
 import com.yanzhenjie.andserver.sample.util.FileUtils;
 import com.yanzhenjie.andserver.sample.util.Result;
 import com.yanzhenjie.andserver.sample.util.UtilsSharedPreference;
 import com.yanzhenjie.andserver.util.MediaType;
+import com.yuanxu.ecg.ECGManager;
 
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.time.DateFormatUtils;
 import org.slf4j.helpers.NOPLogger;
 
 import java.util.Collection;
@@ -176,12 +183,6 @@ public class CommonController extends BaseController{
                     }
                     ScannedDevice scannedDevice = new ScannedDevice(device.getName(), device.getAddress(), rssi);
                     Log.d(TAG, "device: "+scannedDevice);
-                /*int index = scannedDeviceList.indexOf(scannedDevice);
-                if(index == -1){
-                    scannedDeviceList.add(scannedDevice);
-                }else {
-                    scannedDeviceList.add(index,scannedDevice);
-                }*/
                     scannedDeviceList.put(scannedDevice.getAddress(),scannedDevice);
                 }else if(model.equals(Constant.MODEL_ECG)){
                     if (!device.getName().toLowerCase().contains("bw-ecg")) return;
@@ -214,35 +215,6 @@ public class CommonController extends BaseController{
             Log.d(TAG,aLong.toString()+"scanning...");
 
         });
-        /*switch (model){
-
-            case Constant.MODEL_ECG:
-
-                break ;
-            case Constant.MODEL_PULSE:
-                Log.d(TAG,"model=PULSE");
-                //scan PULSE device
-                Log.d(TAG,"size---"+scannedDeviceList.size());
-                if(scannedDeviceList.size()>0){
-                    scannedDeviceList.clear();
-                }
-                Log.d(TAG,"size---"+scannedDeviceList.size());
-                //scan ecg device
-                Observable.timer(SCAN_PERIOD,TimeUnit.SECONDS).subscribe( aLong -> {
-                    mBluetoothAdapter.stopLeScan(mLeScanCallback);
-                    scanStatus.set(false);
-                    Log.d(TAG,"scannedDeviceList.size---"+scannedDeviceList.size());
-                });
-                Log.d(TAG,"mLeScanCallback---"+mLeScanCallback);
-                Log.d(TAG,"mBluetoothAdapter---"+mBluetoothAdapter);
-                Log.i(TAG,"开始扫描");
-                mBluetoothAdapter.startLeScan(mLeScanCallback);
-                Observable.interval(1,TimeUnit.SECONDS).take(SCAN_PERIOD).observeOn(AndroidSchedulers.mainThread()).subscribe(aLong ->{
-                    Log.d(TAG,aLong.toString()+"scanning...");
-
-                });
-                break;
-        }*/
         while (scanStatus.get()){
             Thread.sleep(1000);
             Log.d(TAG, scanStatus+"---"+System.currentTimeMillis()+"----");
@@ -256,67 +228,76 @@ public class CommonController extends BaseController{
      * connect bluetooth
      * @return
      */
-    @PostMapping(value = "/connect/bluetooth/{model}",produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
-    public Result connectBluetooth(@Valid @RequestParam(name = "mac") String mac,@Valid @RequestParam(name = "name") String name){
-        Log.i(TAG,mac);
+
+    @PostMapping(value ="/connect/bluetooth/{model}",produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+    public Result<Object> connectBluetooth( @PathVariable("model") String model,@RequestBody ConnectBluetoothRequestParam param){
+
+        Log.i(TAG,param.getMac());
         fileName = "测试文件名称";
-        MegaBleCallback megaBleCallback = new MegaBleCallback() {
-            @Override
-            public void onConnectionStateChange(boolean connected, MegaBleDevice device) {
-                super.onConnectionStateChange(connected, device);
-                //状态变更 :通知前台服务
-                if(connected){
-                    Log.d(TAG, "已连接:"+device);
-                }else {
-                    Log.d(TAG, "断开连接");
+
+        if(model.equals(Constant.MODEL_PULSE)){
+            fileName = "脉诊-"+ param.getUserName() +"-"+ param.getPhoneNumber()+"-"+DateFormatUtils.format(System.currentTimeMillis(), "yyyyMMddHHmmss")+".txt";
+            if(StringUtils.isEmpty(param.getName()))
+                return fail(null, "null param exception: param name is null");
+            MegaBleCallback megaBleCallback = new MegaBleCallback() {
+                @Override
+                public void onConnectionStateChange(boolean connected, MegaBleDevice device) {
+                    super.onConnectionStateChange(connected, device);
+                    //状态变更 :通知前台服务
+                    if(connected){
+                        Log.d(TAG, "已连接:"+device);
+                    }else {
+                        Log.d(TAG, "断开连接");
+                    }
+                }
+                @Override
+                public void onError(int code) {
+                    NLogger.e(TAG,"error code :{}",code);
                 }
-            }
-            @Override
-            public void onError(int code) {
-                NLogger.e(TAG,"error code :{}",code);
-            }
 
-            @Override
-            public void onStart() {
-                String userId = "5837288dc59e0d00577c5f9a";
-                String token = UtilsSharedPreference.get(MainActivity.context,UtilsSharedPreference.KEY_TOKEN);
-                if(StringUtils.isEmpty(token)){
-                    megaBleClient.startWithToken(userId, "0,0,0,0,0,0");
-                }else {
-                    megaBleClient.startWithToken(userId, token);
+                @Override
+                public void onStart() {
+                    Log.d(TAG, "onStart:--");
+                    String userId = "5837288dc59e0d00577c5f9a";
+                    String token = UtilsSharedPreference.get(MainActivity.context,UtilsSharedPreference.KEY_TOKEN);
+                    if(StringUtils.isEmpty(token)){
+                        megaBleClient.startWithToken(userId, "0,0,0,0,0,0");
+                    }else {
+                        megaBleClient.startWithToken(userId, token);
+                    }
                 }
-            }
 
-            @Override
-            public void onDeviceInfoReceived(MegaBleDevice device) {
-                megaBleClient.getV2Batt();
-                megaBleClient.getV2Mode();
-                megaBleClient.enableV2ModeSpoMonitor(true);
-            }
-            @Override
-            public void onSetUserInfo() {
-                megaBleClient.setUserInfo((byte) 25, (byte) 1, (byte) 170, (byte) 60, (byte) 0);
-            }
+                @Override
+                public void onDeviceInfoReceived(MegaBleDevice device) {
+                    Log.d(TAG, "onDeviceInfoReceived----");
+                    megaBleClient.getV2Batt();
+                    megaBleClient.getV2Mode();
+                    megaBleClient.enableV2ModeSpoMonitor(true);
+                }
+                @Override
+                public void onSetUserInfo() {
+                    megaBleClient.setUserInfo((byte) param.getAge(), (byte) param.getGender(), (byte) param.getHeight(), (byte) param.getWeight(), (byte) 0);
+                }
 
-            @Override
-            public void onIdle() {
-                // 设备闲时,可开启实时、开启长时监控、收监控数据。
-                // 长时监控数据会被记录到戒指内部,实时数据不会。
-                // 长时监控开启后,可断开蓝牙连接,戒指将自动保存心率血氧数据,以便后续手机连上收取。默认每次连上会同步过来。
-                // 绑定token有变动时,用户信息,监测数据将被清空。
-                // 建议默认开启全局实时通道,无需关闭,重复开启无影响
-                // suggested setting, repeated call is ok.
-                Log.d(TAG, "Important: the remote device is idle.");
-                megaBleClient.toggleLive(true);
-            }
+                @Override
+                public void onIdle() {
+                    // 设备闲时,可开启实时、开启长时监控、收监控数据。
+                    // 长时监控数据会被记录到戒指内部,实时数据不会。
+                    // 长时监控开启后,可断开蓝牙连接,戒指将自动保存心率血氧数据,以便后续手机连上收取。默认每次连上会同步过来。
+                    // 绑定token有变动时,用户信息,监测数据将被清空。
+                    // 建议默认开启全局实时通道,无需关闭,重复开启无影响
+                    // suggested setting, repeated call is ok.
+                    Log.d(TAG, "Important: the remote device is idle.");
+                    megaBleClient.toggleLive(true);
+                }
 
-            @Override
-            public void onV2LiveSpoLive(MegaV2LiveSpoLive live) {
+                @Override
+                public void onV2LiveSpoLive(MegaV2LiveSpoLive live) {
 
-            }
+                }
 
-            @Override
-            public void onV2LiveSpoMonitor(MegaV2LiveSpoMonitor live) {
+                @Override
+                public void onV2LiveSpoMonitor(MegaV2LiveSpoMonitor live) {
                /* switch (live.getStatus()){
                     case MegaBleConst.STATUS_LIVE_VALID:
                         updateV2Live(live);
@@ -329,93 +310,95 @@ public class CommonController extends BaseController{
                     break;
 
                 }*/
-            }
+                }
 
-            //notice user to shaking ,in order to bind the ring
-            @Override
-            public void onKnockDevice() {
+                //notice user to shaking ,in order to bind the ring
+                @Override
+                public void onKnockDevice() {
 
-            }
+                }
 
-            @Override
-            public void onTokenReceived(String token) {
-                UtilsSharedPreference.put(MainActivity.context, UtilsSharedPreference.KEY_TOKEN, token);
-            }
+                @Override
+                public void onTokenReceived(String token) {
+                    UtilsSharedPreference.put(MainActivity.context, UtilsSharedPreference.KEY_TOKEN, token);
+                }
 
-            //notice UI modify rssi
-            @Override
-            public void onRssiReceived(int rssi) {
+                //notice UI modify rssi
+                @Override
+                public void onRssiReceived(int rssi) {
 
-            }
+                }
 
-            /**
-             * notify front to change battery value & battery status
-             *
-             * @param value 电池电量
-             * @param status 电池状态  normal(0, "normal"), charging(1, "charging"),full(2, "full"),lowPower(3, "lowPower");error(4, "error");shutdown(5, "shutdown");
-             */
-            @Override
-            public void onBatteryChanged(int value, int status) {
+                /**
+                 * notify front to change battery value & battery status
+                 *
+                 * @param value 电池电量
+                 * @param status 电池状态  normal(0, "normal"), charging(1, "charging"),full(2, "full"),lowPower(3, "lowPower");error(4, "error");shutdown(5, "shutdown");
+                 */
+                @Override
+                public void onBatteryChanged(int value, int status) {
 //                MegaBleBattery.getDescription(status);
-            }
+                }
 
-            /**
-             * notify front active mode
-             * @param mode
-             */
-            @Override
-            public void onV2ModeReceived(MegaV2Mode mode) {
+                /**
+                 * notify front active mode
+                 * @param mode
+                 */
+                @Override
+                public void onV2ModeReceived(MegaV2Mode mode) {
 
-            }
+                }
 
-            /**
-             * 心跳检测,检测到心跳就开启脉搏
-             * @param heartBeat
-             */
-            @Override
-            public void onHeartBeatReceived(MegaBleHeartBeat heartBeat) {
+                /**
+                 * 心跳检测,检测到心跳就开启脉搏
+                 * @param heartBeat
+                 */
+                @Override
+                public void onHeartBeatReceived(MegaBleHeartBeat heartBeat) {
 
-            }
+                }
 
-            @Override
-            public void onRawdataParsed(int[][] a) {
-                if(a != null && a.length > 0){
-                    for (int[] ints : a) {
-                        FileUtils.addFile(MainActivity.context.getFilesDir().getPath() + "/脉诊'", String.valueOf(ints[0]));
+                @Override
+                public void onRawdataParsed(int[][] a) {
+                    if(a != null && a.length > 0){
+                        for (int[] ints : a) {
+                            Log.d(TAG,"rawDataParsed:"+ints[0]);
+                            Log.d(TAG, "filePath:"+MainActivity.context.getFilesDir().getPath()+"/PULSE");
+                            FileUtils.addFile(MainActivity.context.getFilesDir().getPath() + "/PULSE/"+fileName, String.valueOf(ints[0]));
+                        }
                     }
                 }
+            };
+            // mock id, key,use yours
+            MegaBleBuilder builder = new MegaBleBuilder();
+            try{
+                megaBleClient = builder
+                        .withSecretId("D4CE5DD515F81247")
+                        .withSecretKey("uedQ2MgVEFlsGIWSgofHYHNdZSyHmmJ5")
+                        .withContext(MainActivity.context)
+                        .withCallback(megaBleCallback)
+                        .build();
+            }catch (Exception e ){
+                e.printStackTrace();
             }
-        };
-        // mock id, key,use yours
-        MegaBleBuilder builder = new MegaBleBuilder();
-        try{
-            megaBleClient = builder
-                    .withSecretId("D4CE5DD515F81247")
-                    .withSecretKey("uedQ2MgVEFlsGIWSgofHYHNdZSyHmmJ5")
-                    .withContext(MainActivity.context)
-                    .withCallback(megaBleCallback)
-                    .build();
-        }catch (Exception e ){
-            e.printStackTrace();
+            // 开发测试时,可以开启debug
+            megaBleClient.setDebugEnable(true);
+            megaBleClient.connect(param.getMac(), param.getName());
+        }else if(model.equals(Constant.MODEL_ECG)){
+            fileName =  "心电-"+param.getUserName()+"-"+param.getPhoneNumber()+"-"+DateFormatUtils.format(System.currentTimeMillis(), "yyyyMMddHHmmss");
+            //connect  ECG device
+            ECGManager
+                    .getInstance()
+                    .setLog(true, "ECG")
+                    .setAutoReconnect(true)
+                    .init(MainActivity.application);
+            EcgUserInfo ecgUserInfo = new EcgUserInfo(param.getName(), param.getGender() == 0, param.getAge(), param.getHeight(), param.getWeight());
+
+
+        }else{
+            return fail(null, "wrong model");
         }
-        // 开发测试时,可以开启debug
-        megaBleClient.setDebugEnable(true);
-        megaBleClient.connect(mac, name);
 
         return success();
     }
-
-
- /*   //初始化蓝牙广播
-    private void initBroadcast(){
-        broadcastReceiver = new CustomBroadcastReceiver();
-        IntentFilter intentFilter = new IntentFilter();
-        intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED); //开始扫描
-        intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);//扫描结束
-        intentFilter.addAction(BluetoothDevice.ACTION_FOUND);//搜索到设备
-        intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); //配对状态监听
-        registerReceiver(broadcastReceiver,intentFilter);
-    }*/
-
-
 }

+ 107 - 0
sample/src/main/java/com/yanzhenjie/andserver/sample/model/ConnectBluetoothRequestParam.java

@@ -0,0 +1,107 @@
+package com.yanzhenjie.andserver.sample.model;
+
+// Created by Develoven on 2023/4/27.
+// Copyright (c) 2023 redflow. All rights reserved.
+//
+public class ConnectBluetoothRequestParam {
+    /**
+     * bluetooth  device address
+     */
+    private String mac;
+    /**
+     * bluetooth device name
+     */
+    private String name;
+
+    /**
+     * gender 1 man 0 woman
+     */
+    private int gender;
+
+    /**
+     * user name
+     */
+    private String userName;
+
+    /**
+     * user phone number
+     */
+    private String phoneNumber;
+
+    /**
+     * height
+     */
+    private int height;
+
+    /**
+     * age
+     */
+    private int age;
+
+    /**
+     * weight
+     */
+    private int weight ;
+    public String getMac() {
+        return mac;
+    }
+
+    public int getGender(){
+        return gender;
+    }
+    public String getName() {
+        return name;
+    }
+
+    public String getUserName() {
+        return userName;
+    }
+
+    public String getPhoneNumber() {
+        return phoneNumber;
+    }
+
+    public int getAge() {
+        return age;
+    }
+
+    public int getHeight() {
+        return height;
+    }
+
+    public int getWeight() {
+        return weight;
+    }
+
+    public void setMac(String mac) {
+        this.mac = mac;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public void setUserName(String userName) {
+        this.userName = userName;
+    }
+
+    public void setPhoneNumber(String phoneNumber) {
+        this.phoneNumber = phoneNumber;
+    }
+
+    public void setGender(int gender) {
+        this.gender = gender;
+    }
+
+    public void setAge(int age) {
+        this.age = age;
+    }
+
+    public void setHeight(int height) {
+        this.height = height;
+    }
+
+    public void setWeight(int weight) {
+        this.weight = weight;
+    }
+}

+ 70 - 0
sample/src/main/java/com/yanzhenjie/andserver/sample/model/EcgUserInfo.java

@@ -0,0 +1,70 @@
+package com.yanzhenjie.andserver.sample.model;
+
+import android.text.TextUtils;
+
+import com.yuanxu.ecg.cmd.BindUserIdCmd;
+
+import java.io.Serializable;
+
+public class EcgUserInfo implements Serializable {
+
+    private String name;//姓名
+    private boolean gender;//是否为女性
+    private int age;//年龄
+    private int height;//身高(cm)
+    private int weight;//体重(kg)
+
+    public EcgUserInfo(String name, boolean gender, int age, int height, int weight) {
+        if (TextUtils.isEmpty(name)) {
+            throw new IllegalArgumentException("name is null");
+        }
+        if (!BindUserIdCmd.isValidNameBytesLength(name)) {
+            throw new IllegalArgumentException("the length of this name(" + name + ") is greater than max byte length(" + BindUserIdCmd.MAX_NAME_BYTE_LENGTH + ")");
+        }
+        this.name = name;
+        this.gender = gender;
+        this.age = age;
+        this.height = height;
+        this.weight = weight;
+    }
+
+    public String getName() {
+        return name == null ? "" : name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public boolean getGender() {
+        return gender;
+    }
+
+    public void setGender(boolean female) {
+        this.gender = female;
+    }
+
+    public int getAge() {
+        return age;
+    }
+
+    public void setAge(int age) {
+        this.age = age;
+    }
+
+    public int getHeight() {
+        return height;
+    }
+
+    public void setHeight(int height) {
+        this.height = height;
+    }
+
+    public int getWeight() {
+        return weight;
+    }
+
+    public void setWeight(int weight) {
+        this.weight = weight;
+    }
+}

+ 2 - 0
settings.gradle

@@ -4,3 +4,5 @@ include ':processor'
 include ':plugin'
 include ':sample'
 include ':megablelibopen'
+include ':signalproc-release'
+include ':ecg'

+ 2 - 0
signalproc-release/build.gradle

@@ -0,0 +1,2 @@
+configurations.maybeCreate("default")
+artifacts.add("default", file('signalproc-release.aar'))

BIN
signalproc-release/signalproc-release.aar