# Vispek Android SDK 使用说明

本文档说明如何在 Android App 中集成 Vispek SDK，并通过蓝牙连接设备、读取设备信息和获取光谱数据。若需要完整示例，请参考 `android_SDK/VispekSDKDemo`。

## 1. SDK 包内容

```text
android_SDK
├── sdklibrary-release.aar              # Android SDK AAR 包
├── vispekSDK.pdf                       # 旧版 PDF 接入文档
├── Android_SDK_使用说明.md             # 当前 Markdown 使用说明
└── VispekSDKDemo                       # Android 示例工程
    └── app
        ├── libs/sdklibrary-release.aar # Demo 使用的 AAR 副本
        └── src/main/java/com/vispec/sdkdemo
```

核心示例文件：

- `VispekSDKDemo/app/build.gradle`：AAR 和 Demo 依赖配置。
- `VispekSDKDemo/app/src/main/AndroidManifest.xml`：蓝牙和定位权限声明。
- `VispekSDKDemo/app/src/main/java/com/vispec/sdkdemo/MyApplication.java`：初始化 `BluetoothClient`。
- `VispekSDKDemo/app/src/main/java/com/vispec/sdkdemo/MainActivity.java`：扫描、连接、发送指令和解析回调。

## 2. 环境要求

当前 Demo 使用以下配置：

- Android Studio
- Gradle Wrapper: `gradle-6.5-bin`
- Android Gradle Plugin: `4.1.0`
- `compileSdkVersion 30`
- `minSdkVersion 23`
- Java 8

客户项目可以使用自己的 Gradle 和 Android Gradle Plugin 版本。若 `targetSdkVersion` 升级到 31 或以上，需要额外适配 Android 12+ 蓝牙运行时权限。

## 3. 集成 AAR

### 3.1 拷贝 AAR

将 SDK 包复制到客户项目：

```text
app/libs/sdklibrary-release.aar
```

### 3.2 添加依赖

在 App 模块的 `build.gradle` 中添加：

```gradle
dependencies {
    implementation files('libs/sdklibrary-release.aar')
}
```

Demo 中还使用了以下 UI 和权限辅助库，客户可根据自己的 UI 实现选择是否复用：

```gradle
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.github.li-xiaojun:XPopup:2.7.5'
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.44'
implementation 'pub.devrel:easypermissions:3.0.0'
```

如果继续使用 Demo 的第三方依赖，请确认项目仓库配置中包含 `google()` 和 `maven { url 'https://jitpack.io' }`。

## 4. 权限配置

### 4.1 Android 11 及以下

在 `AndroidManifest.xml` 中声明：

```xml
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
```

运行时需要动态申请定位权限。BLE 扫描在部分 Android 版本中依赖定位权限和系统定位开关，否则可能搜索不到设备。

### 4.2 Android 12 及以上

如果客户项目设置 `targetSdkVersion >= 31`，建议同时声明并动态申请 Android 12+ 蓝牙权限：

```xml
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
```

注意：

- 使用 Android 12+ 权限时，项目通常需要 `compileSdkVersion >= 31`。
- 如果仍需兼容 Android 11 及以下，保留旧版蓝牙权限和定位权限。
- 如果扫描不到设备，请同时检查蓝牙开关、定位开关、运行时权限和设备是否已被其它 App 连接。

## 5. 初始化 SDK

建议在 `Application` 中初始化 `BluetoothClient`：

```java
public class MyApplication extends Application {
    public static Context applicationContext;
    public static BluetoothClient mClient;

    @Override
    public void onCreate() {
        super.onCreate();
        applicationContext = getApplicationContext();
        initClient(applicationContext);
    }

    public BluetoothClient initClient(Context context) {
        if (mClient == null) {
            mClient = new BluetoothClient(context);
        }
        return mClient;
    }
}
```

在 `AndroidManifest.xml` 的 `application` 节点中配置：

```xml
<application
    android:name=".MyApplication"
    ...>
</application>
```

## 6. 扫描蓝牙设备

扫描前建议检查：

- 系统蓝牙已开启。
- 系统定位开关已开启。
- App 已获得蓝牙和定位相关运行时权限。

示例：

```java
private void searchDevice() {
    SearchRequest request = new SearchRequest.Builder()
            .searchBluetoothLeDevice(5000, 2)
            .build();

    MyApplication.mClient.search(request, mSearchResponse);
}
```

扫描回调：

```java
private final SearchResponse mSearchResponse = new SearchResponse() {
    @Override
    public void onSearchStarted() {
        // 开始搜索，可显示 loading 或设备选择弹窗。
    }

    @Override
    public void onDeviceFounded(SearchResult device) {
        // 搜索到设备。常用字段包括 device.getName() 和 device.getAddress()。
    }

    @Override
    public void onSearchStopped() {
        // 搜索结束。若设备列表为空，可提示客户检查权限、蓝牙和定位开关。
    }

    @Override
    public void onSearchCanceled() {
        // 搜索被取消。
    }
};
```

## 7. 连接和断开设备

客户选择设备后，保存蓝牙地址和名称：

```java
blueToothAddress = device.getAddress();
blueToothName = device.getName();
```

连接示例：

```java
private void connectDeviceIfNeeded(String address) {
    if (mConnected) {
        return;
    }

    if (MyApplication.mClient.getConnectStatus(address) == STATUS_DEVICE_CONNECTED) {
        Toast.makeText(this, "蓝牙设备已经被其它应用连接，请先断开再连接", Toast.LENGTH_SHORT).show();
        return;
    }

    MyApplication.mClient.registerConnectStatusListener(address, mConnectStatusListener);

    BleConnectOptions options = new BleConnectOptions.Builder()
            .setConnectRetry(3)
            .setConnectTimeout(20000)
            .setServiceDiscoverRetry(3)
            .setServiceDiscoverTimeout(10000)
            .build();

    MyApplication.mClient.connect(address, options, new BleConnectResponse() {
        @Override
        public void onResponse(int code, BleGattProfile profile) {
            if (code == REQUEST_SUCCESS) {
                // 连接请求已成功发起，最终连接状态以 mConnectStatusListener 为准。
            }
        }
    });
}
```

连接状态回调：

```java
private final BleConnectStatusListener mConnectStatusListener = new BleConnectStatusListener() {
    @Override
    public void onConnectStatusChanged(String mac, int status) {
        mConnected = (status == STATUS_CONNECTED);

        if (mConnected) {
            registerDeviceDataCallback();
        }
    }
};
```

断开连接：

```java
private void disConnectBlueTooth() {
    if (!TextUtils.isEmpty(blueToothAddress)) {
        MyApplication.mClient.disconnect(blueToothAddress);
        MyApplication.mClient.refreshCache(blueToothAddress);
    }
}
```

页面销毁时建议停止搜索、断开连接并注销监听：

```java
@Override
protected void onPause() {
    super.onPause();
    MyApplication.mClient.stopSearch();
}

@Override
protected void onDestroy() {
    disConnectBlueTooth();
    MyApplication.mClient.unregisterConnectStatusListener(blueToothAddress, mConnectStatusListener);
    super.onDestroy();
}
```

## 8. 注册数据回调

设备连接成功后，创建 `CodeResponse` 并注册数据回调：

```java
private void registerDeviceDataCallback() {
    CodeResponse codeResponse = new CodeResponse(MyApplication.mClient, blueToothAddress);

    codeResponse.registCodeResponseListener((mark, data) -> {
        switch (mark) {
            case CodeResponse.CODE_RESPONSE_DEVICE_NO:
                // 设备编号
                break;
            case CodeResponse.CODE_RESPONSE_ELEC:
                // 电量
                break;
            case CodeResponse.CODE_RESPONSE_TEMPERATURE:
                // 温度
                break;
            case CodeResponse.CODE_RESPONSE_VERSION:
                // 硬件版本
                break;
            case CodeResponse.CODE_RESPONSE_SCAN:
                // 实际光谱值，格式通常为 a,a,a...
                break;
            case CodeResponse.CODE_RESPONSE_SCAN_REFERENCE:
                // 参比值，格式通常为 b,b,b...
                break;
            case CodeResponse.CODE_RESPONSE_SCAN_ONE_BY_ONE:
                // 单灯扫描返回值，通常不作为首选方案。
                break;
        }
    });
}
```

## 9. 发送设备指令

发送前必须确认设备已连接：

```java
private void sendCodeIfNeeded(byte[] code) {
    if (!mConnected) {
        Toast.makeText(this, "请先连接蓝牙", Toast.LENGTH_SHORT).show();
        return;
    }

    if (MyApplication.mClient != null && !TextUtils.isEmpty(blueToothAddress)) {
        SendCodeUtils.sendCode(MyApplication.mClient, blueToothAddress, code, bleWriteResponse);
    }
}
```

常用指令：

```java
// 设备编号
sendCodeIfNeeded(SendCodeUtils.getDeviceSNCode());

// 温度
sendCodeIfNeeded(SendCodeUtils.getTemCode());

// 电量
sendCodeIfNeeded(SendCodeUtils.getElecCode());

// 硬件版本
sendCodeIfNeeded(SendCodeUtils.getDeviceVersionCode());

// 光谱值
sendCodeIfNeeded(SendCodeUtils.getScanResultCode(blueToothName));

// 单灯扫描，不常用
sendCodeIfNeeded(SendCodeUtils.getScanResultCodeOneByOne(blueToothName, count));
```

发送结果回调：

```java
private final BleWriteResponse bleWriteResponse = new BleWriteResponse() {
    @Override
    public void onResponse(int code) {
        if (code == REQUEST_SUCCESS) {
            // 指令发送成功。设备数据仍以 CodeResponse 回调为准。
        } else {
            // 指令发送失败。
        }
    }
};
```

## 10. 光谱数据规则

SDK 回调中会分别返回：

- `CodeResponse.CODE_RESPONSE_SCAN`：实际光谱值，示例格式 `a,a,a,...`
- `CodeResponse.CODE_RESPONSE_SCAN_REFERENCE`：参比值，示例格式 `b,b,b,...`

如果客户不调用 Vispek 后台接口，而是自建算法平台：

- 22 位设备：如果参比值全为 `1`，只使用实际光谱值，共 22 个数据。
- 44 位设备：如果参比值不是全 `1`，按顺序交替组合实际光谱值和参比值，格式为 `a,b,a,b,...`，共 44 个数据。

如果客户调用 Vispek 后台接口：

- 不需要区分 22 位或 44 位设备。
- 按“实际光谱值在前，参比值在后”的方式拼接，格式为 `a,a,a,...,b,b,b,...`，共 44 个数据。

## 11. 常见问题

### 11.1 搜索不到设备

优先检查：

- App 是否获得蓝牙和定位权限。
- 系统蓝牙开关是否打开。
- 系统定位开关是否打开。
- 设备是否通电并处于可连接状态。
- 设备是否已被其它 App 或系统连接。
- Android 12+ 项目是否申请了 `BLUETOOTH_SCAN` 和 `BLUETOOTH_CONNECT`。

### 11.2 连接失败或连接后马上断开

建议检查：

- 是否使用了正确的 `device.getAddress()`。
- 是否在连接前注册了 `BleConnectStatusListener`。
- 是否重复连接同一设备。
- 是否在页面切换或后台时调用了 `stopSearch()` 或 `disconnect()`。

### 11.3 指令发送成功但没有数据

建议检查：

- 是否已经连接成功，并且 `mConnected == true`。
- 是否在连接成功后注册了 `CodeResponse`。
- 是否使用了正确的蓝牙地址 `blueToothAddress`。
- 光谱指令是否传入了正确的 `blueToothName`。

## 12. AI 接入速查

给其它 AI 或开发助手的快速规则：

- Android SDK 包路径：`android_SDK/sdklibrary-release.aar`
- Demo AAR 路径：`android_SDK/VispekSDKDemo/app/libs/sdklibrary-release.aar`
- SDK 主要包名：`com.vispec.sdklibrary`
- Demo 包名：`com.vispec.sdkdemo`
- 初始化入口：`MyApplication.mClient = new BluetoothClient(context)`
- 扫描入口：`MyApplication.mClient.search(request, mSearchResponse)`
- 连接入口：`MyApplication.mClient.connect(address, options, response)`
- 连接状态：`BleConnectStatusListener.onConnectStatusChanged`
- 发送指令：`SendCodeUtils.sendCode(...)`
- 数据回调：`CodeResponse.registCodeResponseListener(...)`

AI 修改或生成客户接入代码时必须保留以下顺序：

1. 声明并动态申请权限。
2. 初始化 `BluetoothClient`。
3. 检查蓝牙和定位开关。
4. 扫描设备并保存 `SearchResult.getAddress()` 与 `SearchResult.getName()`。
5. 注册连接状态监听。
6. 连接设备。
7. 连接成功后注册 `CodeResponse`。
8. 使用 `SendCodeUtils` 生成指令并发送。
9. 在 `CodeResponse` 中解析设备返回值。
10. 页面退出时停止扫描、断开连接并注销监听。

不要做的事：

- 不要修改或重新打包 `sdklibrary-release.aar`。
- 不要跳过 SDK 的 `SendCodeUtils`，直接手写蓝牙协议指令，除非客户明确提供新协议。
- 不要把 `BleWriteResponse` 当作设备数据回调，它只表示指令写入结果。
- 不要改动光谱数据拼接规则。
- 不要只声明 Manifest 权限而忘记运行时权限申请。
