Browse Source

测试串口和蓝牙通信

master-new-炒锅
liup 2 months ago
parent
commit
8c1a004ad6
45 changed files with 3988 additions and 5 deletions
  1. +2
    -2
      .idea/deploymentTargetSelector.xml
  2. +329
    -0
      .idea/other.xml
  3. +15
    -1
      app/build.gradle
  4. BIN
     
  5. BIN
     
  6. BIN
     
  7. BIN
     
  8. BIN
     
  9. +1
    -1
      app/release/output-metadata.json
  10. +25
    -0
      app/src/main/AndroidManifest.xml
  11. +46
    -0
      app/src/main/java/com/bonait/bnframework/api/Api.java
  12. +117
    -0
      app/src/main/java/com/bonait/bnframework/api/CommonResponse.java
  13. +70
    -0
      app/src/main/java/com/bonait/bnframework/api/HttpUtil.java
  14. +29
    -0
      app/src/main/java/com/bonait/bnframework/api/IHttpCallBack.java
  15. +56
    -0
      app/src/main/java/com/bonait/bnframework/api/ServerManager.java
  16. +51
    -0
      app/src/main/java/com/bonait/bnframework/api/interceptor/AccountSNInterceptor.kt
  17. +71
    -0
      app/src/main/java/com/bonait/bnframework/api/interceptor/ConnectRetryInterceptor.java
  18. +21
    -0
      app/src/main/java/com/bonait/bnframework/api/interceptor/RequestInterceptor.java
  19. +73
    -0
      app/src/main/java/com/bonait/bnframework/api/interceptor/StbInfoInterceptor.java
  20. +430
    -0
      app/src/main/java/com/bonait/bnframework/ble/BleClientActivity.java
  21. +186
    -0
      app/src/main/java/com/bonait/bnframework/ble/BleDevAdapter.java
  22. +297
    -0
      app/src/main/java/com/bonait/bnframework/ble/BleServiceActivity.java
  23. +6
    -1
      app/src/main/java/com/bonait/bnframework/modules/welcome/activity/WelcomeActivity.java
  24. +111
    -0
      app/src/main/java/com/bonait/bnframework/serial/AssistBean.java
  25. +737
    -0
      app/src/main/java/com/bonait/bnframework/serial/ComAssistantActivity.java
  26. +23
    -0
      app/src/main/java/com/bonait/bnframework/serial/ComBean.java
  27. +83
    -0
      app/src/main/java/com/bonait/bnframework/serial/MyFunc.java
  28. +243
    -0
      app/src/main/java/com/bonait/bnframework/serial/SerialHelper.java
  29. +86
    -0
      app/src/main/java/com/bonait/bnframework/serial/SerialPort.java
  30. +124
    -0
      app/src/main/java/com/bonait/bnframework/serial/SerialPortFinder.java
  31. +2
    -0
      app/src/main/java/com/bonait/bnframework/ui/viewmodel/HomeGoodsViewModel.java
  32. BIN
     
  33. BIN
     
  34. BIN
     
  35. BIN
     
  36. BIN
     
  37. +7
    -0
      app/src/main/res/drawable/divider.xml
  38. +14
    -0
      app/src/main/res/drawable/sel_item.xml
  39. +6
    -0
      app/src/main/res/drawable/stroke.xml
  40. +122
    -0
      app/src/main/res/layout/activity_bleclient.xml
  41. +451
    -0
      app/src/main/res/layout/activity_serial_test.xml
  42. +49
    -0
      app/src/main/res/layout/activity_service.xml
  43. +22
    -0
      app/src/main/res/layout/item/layout/item_dev.xml
  44. +69
    -0
      app/src/main/res/values/baudrates.xml
  45. +14
    -0
      app/src/main/res/values/strings.xml

+ 2
- 2
.idea/deploymentTargetSelector.xml View File

@@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2024-09-14T07:16:28.864494800Z">
<DropdownSelection timestamp="2024-09-29T02:30:16.584496100Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="Default" identifier="serial=192.168.1.108:5555;connection=54d7fd03" />
<DeviceId pluginId="Default" identifier="serial=192.168.1.6:5555;connection=26244323" />
</handle>
</Target>
</DropdownSelection>


+ 329
- 0
.idea/other.xml View File

@@ -0,0 +1,329 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="direct_access_persist.xml">
<option name="deviceSelectionList">
<list>
<PersistentDeviceSelectionData>
<option name="api" value="27" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="F01L" />
<option name="id" value="F01L" />
<option name="manufacturer" value="FUJITSU" />
<option name="name" value="F-01L" />
<option name="screenDensity" value="360" />
<option name="screenX" value="720" />
<option name="screenY" value="1280" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="28" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="SH-01L" />
<option name="id" value="SH-01L" />
<option name="manufacturer" value="SHARP" />
<option name="name" value="AQUOS sense2 SH-01L" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="Lenovo" />
<option name="codename" value="TB370FU" />
<option name="id" value="TB370FU" />
<option name="manufacturer" value="Lenovo" />
<option name="name" value="Tab P12" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1840" />
<option name="screenY" value="2944" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
<option name="codename" value="a51" />
<option name="id" value="a51" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy A51" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="akita" />
<option name="id" value="akita" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="b0q" />
<option name="id" value="b0q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S22 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="32" />
<option name="brand" value="google" />
<option name="codename" value="bluejay" />
<option name="id" value="bluejay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="caiman" />
<option name="id" value="caiman" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro" />
<option name="screenDensity" value="360" />
<option name="screenX" value="960" />
<option name="screenY" value="2142" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="comet" />
<option name="id" value="comet" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro Fold" />
<option name="screenDensity" value="390" />
<option name="screenX" value="2076" />
<option name="screenY" value="2152" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="29" />
<option name="brand" value="samsung" />
<option name="codename" value="crownqlteue" />
<option name="id" value="crownqlteue" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Note9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2220" />
<option name="screenY" value="1080" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="dm3q" />
<option name="id" value="dm3q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S23 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="e1q" />
<option name="id" value="e1q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S24" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix_camera" />
<option name="id" value="felix_camera" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold (Camera-enabled)" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="gts8uwifi" />
<option name="id" value="gts8uwifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S8 Ultra" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1848" />
<option name="screenY" value="2960" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="husky" />
<option name="id" value="husky" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8 Pro" />
<option name="screenDensity" value="390" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="motorola" />
<option name="codename" value="java" />
<option name="id" value="java" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="G20" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="komodo" />
<option name="id" value="komodo" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro XL" />
<option name="screenDensity" value="360" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="lynx" />
<option name="id" value="lynx" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="google" />
<option name="codename" value="oriole" />
<option name="id" value="oriole" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="panther" />
<option name="id" value="panther" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q5q" />
<option name="id" value="q5q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold5" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1812" />
<option name="screenY" value="2176" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="r11" />
<option name="id" value="r11" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Watch" />
<option name="screenDensity" value="320" />
<option name="screenX" value="384" />
<option name="screenY" value="384" />
<option name="type" value="WEAR_OS" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="redfin" />
<option name="id" value="redfin" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 5" />
<option name="screenDensity" value="440" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="shiba" />
<option name="id" value="shiba" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="tangorpro" />
<option name="id" value="tangorpro" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Tablet" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="tokay" />
<option name="id" value="tokay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="29" />
<option name="brand" value="samsung" />
<option name="codename" value="x1q" />
<option name="id" value="x1q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S20" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1440" />
<option name="screenY" value="3200" />
</PersistentDeviceSelectionData>
</list>
</option>
</component>
</project>

+ 15
- 1
app/build.gradle View File

@@ -19,6 +19,9 @@ android {
versionName rootProject.ext.versionName
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
multiDexEnabled true
ndk {
abiFilters 'armeabi','armeabi-v7a', 'x86', 'arm64-v8a','mips', 'x86_64'
}
}
signingConfigs {
release {
@@ -59,6 +62,8 @@ android {

sourceSets {
main {
jni.srcDirs = []//禁止gradle 自动编译,使用已经编译好的So库
jniLibs.srcDirs = ['src/main/jniLibs', 'libs']//指向要使用的库文件//的路径,前边的是自己项目的,后边的是第三方的so
res.srcDirs = [
'src/main/res/layout/datatab',
'src/main/res/layout/item',
@@ -74,6 +79,7 @@ android {
buildFeatures {
viewBinding = true
}
ndkVersion '28.0.12433566 rc1'
applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = "boluo-xiaochao-v${defaultConfig.versionCode}-${releaseTime()}"+"-${variant.name}.apk"
@@ -83,7 +89,6 @@ android {

dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')

//测试相关
testImplementation 'junit:junit:4.13-beta-2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
@@ -194,4 +199,13 @@ dependencies {

//日志工具 可定位代码行数
api 'com.apkfuns.logutils:library:1.7.5'

//retrofit2网络请求
api 'com.squareup.retrofit2:converter-gson:2.5.0'
api "com.squareup.retrofit2:retrofit:2.5.0"
api "com.squareup.retrofit2:adapter-rxjava2:2.5.0"
api "com.squareup.retrofit2:converter-scalars:2.5.0"
api 'com.squareup.okhttp3:logging-interceptor:3.10.0'
//调试okhttp请求(建议debug使用)
api 'com.localebro:okhttpprofiler:1.0.8'
}

BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


+ 1
- 1
app/release/output-metadata.json View File

@@ -13,7 +13,7 @@
"attributes": [],
"versionCode": 107,
"versionName": "1.0.7",
"outputFile": "boluo-xiaochao-v107-202409251528-release.apk"
"outputFile": "boluo-xiaochao-v107-202410071053-release.apk"
}
],
"elementType": "File"

+ 25
- 0
app/src/main/AndroidManifest.xml View File

@@ -5,6 +5,7 @@

<uses-sdk tools:overrideLibrary="com.aliyun.iot.breeze.biz,com.aliyun.iot.breeze.ota, com.aliyun.iot.breeze.sdk,com.aliyun.iot.ble,com.aliyun.alink.linksdk.tmp" />
<!-- 网络权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.INTERNET" /> <!-- 网络连接 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- 获取网络连接状态 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- 写存储的权限 -->
@@ -21,6 +22,20 @@
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>

<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="true" />
<uses-feature android:name="android.hardware.location.gps" />

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

<!--Android 6.0及后续版本扫描蓝牙,需要定位权限(进入GPS设置,可以看到蓝牙定位)-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="com.android.example.USB_PERMISSION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

<application
android:vmSafeMode="true"
android:name=".MainApplication"
@@ -33,11 +48,21 @@
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ico"
android:supportsRtl="true"
android:extractNativeLibs="true"
android:theme="@style/AppTheme"
android:debuggable="true"
tools:ignore="GoogleAppIndexingWarning"
tools:node="merge"
tools:replace="android:icon">
<activity
android:name=".serial.ComAssistantActivity"
android:exported="true" />
<activity
android:name=".ble.BleServiceActivity"
android:exported="true" />
<activity
android:name=".ble.BleClientActivity"
android:exported="true" />
<activity
android:name=".modules.home.fragment.from.Cpxz1Activity"
android:exported="false" />


+ 46
- 0
app/src/main/java/com/bonait/bnframework/api/Api.java View File

@@ -0,0 +1,46 @@
package com.bonait.bnframework.api;


import com.bonait.bnframework.common.API.APIResultT;
import com.bonait.bnframework.common.model.GoodsData;

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Url;

/**
* 接口服务
*/
public interface Api {

// /**
// * 上报日志文件
// *
// * @param url
// * @param account 账号
// * @param device 设备
// * @param faultCode 故障代码
// * @param sceneCode 场景code
// * @param serialNo 序列号
// * @param file 文件
// * @return
// */
// @Multipart
// @POST
// Call<BaseResp<String>> upLoadLogFile(@Url String url,
// @Query("account") String account,
// @Query("device") String device,
// @Query("faultCode") String faultCode,
// @Query("sceneCode") String sceneCode,
// @Query("serialNo") String serialNo,
// @Part MultipartBody.Part file);
/**
* 获取搜索关键字
*
* @param url
* @return
*/
@GET
Call<APIResultT<GoodsData>> getGoodsData(@Url String url);

}

+ 117
- 0
app/src/main/java/com/bonait/bnframework/api/CommonResponse.java View File

@@ -0,0 +1,117 @@
package com.bonait.bnframework.api;

import java.net.SocketTimeoutException;

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

/**
* @author Nov
* @date 19.11.25
*/
public class CommonResponse<T> implements Callback<T> {
/**
* 服务器请求失败
*/
public static final int APP_NET_ERROR = 99999;

/**
* 服务器请求成功,但是数据返回为空
*/
public static final int APP_DATA_NULL = 99998;

/**
* 服务器请求成功,但是数据返回错误
*/
public static final int APP_DATA_ERROR = 99997;
/**
* 回调
*/
private IHttpCallBack callback;


public CommonResponse(IHttpCallBack callback) {
this.callback = callback;
}


@Override
public void onResponse(Call<T> call, Response<T> response) {
T data = response.body();
if (null == response) {
callError("response=null", APP_DATA_ERROR, "response =null");
return;
}
if (null == data) {
callError("data=null", APP_DATA_ERROR, response.message());
return;
}
callSuccess(data);
}

@Override
public void onFailure(Call<T> call, Throwable t) {
//LogUtils.d("接口出错....."+t.getMessage() + " "+call.request().url());
callFail(APP_NET_ERROR, t);

}

/**
* 回调失败
*
* @param code
* @param e
*/
private void callFail(final int code, final Throwable e) {
if (null == callback) {
return;
}
if (e instanceof SocketTimeoutException) {
callback.onFailed(code, e, "请求超时");
} else {
callback.onFailed(code, e, "接口请求错误");
}

e.printStackTrace();
reportErrorInfo(code + "", e.getMessage());

}

/**
* 回调错误
*
* @param code
*/
private void callError(String type, final int code, String msg) {
if (null == callback) {
return;
}
//LogUtils.d("请求数据处理错误.....");
callback.onError(code);
reportErrorInfo(code + "", msg);
}

/**
* 回调成功
*
* @param t
*/
private void callSuccess(final Object t) {
if (null == callback) {
return;
}
callback.onSuccess(t);
}

/**
* 上报错误信息
*
* @param code
* @param description
*/
private void reportErrorInfo(String code, String description) {

}

}

+ 70
- 0
app/src/main/java/com/bonait/bnframework/api/HttpUtil.java View File

@@ -0,0 +1,70 @@
package com.bonait.bnframework.api;

import com.bonait.bnframework.BuildConfig;
import com.bonait.bnframework.api.interceptor.ConnectRetryInterceptor;
import com.bonait.bnframework.api.interceptor.RequestInterceptor;
import com.bonait.bnframework.api.interceptor.StbInfoInterceptor;
import com.google.gson.GsonBuilder;
import com.localebro.okhttpprofiler.OkHttpProfilerInterceptor;

import java.lang.reflect.Modifier;
import java.util.concurrent.TimeUnit;

import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

/**
* Http网络请求
*/
public class HttpUtil {
private static final String JSON_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";

private static Retrofit mRetrofit;

private HttpUtil() {
throw new AssertionError();
}

public static void init(String baseUrl) {
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create(new GsonBuilder()
.excludeFieldsWithModifiers(Modifier.PROTECTED | Modifier.STATIC)
.setDateFormat(JSON_DATE_FORMAT)
.setPrettyPrinting()
.create()));

OkHttpClient.Builder builderOk = new OkHttpClient.Builder();
builderOk.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS);

//过多的打印会导致盒子io异常
builderOk.addInterceptor(new RequestInterceptor());
// if (Config.isShowHttpLog) {
builderOk.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY));
// } else {
// builderOk.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC));

// }
if (BuildConfig.DEBUG) {
builderOk.addInterceptor(new OkHttpProfilerInterceptor());
}
builderOk.addInterceptor(new ConnectRetryInterceptor(3));
// builderOk.addInterceptor(new StbInfoInterceptor());
// builderOk.addInterceptor(new AccountSNInterceptor());
OkHttpClient okHttpClient = builderOk.connectionPool(new ConnectionPool()).build();
builder.client(okHttpClient);
mRetrofit = builder.build();
}

public static <T> T getService(Class<T> t) {
if (mRetrofit == null) {
throw new IllegalStateException("HttpUtil is not init.");
}
return mRetrofit.create(t);
}
}

+ 29
- 0
app/src/main/java/com/bonait/bnframework/api/IHttpCallBack.java View File

@@ -0,0 +1,29 @@
package com.bonait.bnframework.api;

/**
* @author Nov
* @date 19.11.25
*/
public interface IHttpCallBack<T> {
/**
* 请求成功
*
* @param t 成功返回参数
*/
public void onSuccess(T t);

/**
* 请求成功,但是返回数据处理中发现异常
*
* @param failCode 失败原因Code 非法请求:ServerUtil.CODE_FAIL_ILLEGAL;其它失败:ServerUtil.CODE_FAIL; 等
*/
public void onError(int failCode);

/**
* Http请求失败
*
* @param throwable 异常,可能为空
* @param message 其它错误信息
*/
public void onFailed(int failCode, Throwable throwable, String message);
}

+ 56
- 0
app/src/main/java/com/bonait/bnframework/api/ServerManager.java View File

@@ -0,0 +1,56 @@
package com.bonait.bnframework.api;

import com.bonait.bnframework.common.constant.ConfigName;
import com.bonait.bnframework.common.model.GoodsData;
import com.bonait.bnframework.common.model.upload.UploadRes;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import retrofit2.Call;

public class ServerManager {

/**
* 热搜
*
* @param callback
*/
public static void getGoodsData(IHttpCallBack<GoodsData> callback) {
String url = "https://cfv.black-pa.com/kitchbase"+"/api/goods/Getdevicegoods?deviceId="+ ConfigName.getInstance().DeviceAutoKey;
Call call = HttpUtil.getService(Api.class).getGoodsData(url);
comEnqueue(call, callback);
}
/**
* 获取消息数据
*
* @param device 设备
* @param faultCode 故障代码
* @param sceneCode 场景code
* @param file 文件
* @param callback
*/
public static void upLoadLogFile(String device, String faultCode, String sceneCode, MultipartBody.Part file, IHttpCallBack<UploadRes<String>> callback) {
// String url = "/logfile/upload";
// Call<UploadRes<String>> call = HttpUtil.getService(Api.class).upLoadLogFile(url, itvName, device, faultCode, sceneCode, serialNo, file);
// CommonResponse<UploadRes<String>> response = new CommonResponse<>(callback);
// call.enqueue(response);
}

/**
* >>>>>>> .merge_file_a00276
* cms同步请求
*
* @param call
* @param callBack
*/
private static void comEnqueue(Call call, IHttpCallBack callBack) {
CommonResponse hrh = new CommonResponse(callBack);
call.enqueue(hrh);
}

}

+ 51
- 0
app/src/main/java/com/bonait/bnframework/api/interceptor/AccountSNInterceptor.kt View File

@@ -0,0 +1,51 @@
//package com.amt.module_common.net.interceptor
//
//import okhttp3.*
//
///**
// * @author Nov
// * @date 23.11.23
// */
//class AccountSNInterceptor : Interceptor {
// override fun intercept(chain: Interceptor.Chain): Response {
// val request = addAccountSN(chain.request())
// return chain.proceed(request)
// }
//
// /**
// * 添加公共字段
// */
// private fun addAccountSN(request: Request): Request {
// val oldRequest: Request = request //旧连接LoginActivity
// var newRequest: Request //添加公共参数后的新连接
// val method = oldRequest.method()
// if (method.equals("GET")) {
// var httpUrl = oldRequest.url()
// httpUrl = httpUrl.newBuilder()
//// .addEncodedQueryParameter("publicParamsSN", LocalDataManager.sn())
// .build()
// newRequest = oldRequest.newBuilder().url(httpUrl).build()
// } else if (method.equals("POST")) {
// val oldBody = oldRequest.body()
// newRequest = oldRequest
// oldBody?.let {
// newRequest = if (it is FormBody) {
// val newBody = FormBody.Builder()
//// newBody.addEncoded("publicParamsAccount", LocalDataManager.userId())
// if (it.contentLength() > 0) {
// val formBody1 = it
// for (i in 0 until formBody1.size()) {
// newBody.addEncoded(formBody1.encodedName(i), formBody1.encodedValue(i))
// }
// }
// oldRequest.newBuilder().post(newBody.build()).build()
// } else {
// oldRequest
// }
// }
// } else {
// newRequest = oldRequest
// }
// return newRequest
// }
//}

+ 71
- 0
app/src/main/java/com/bonait/bnframework/api/interceptor/ConnectRetryInterceptor.java View File

@@ -0,0 +1,71 @@
package com.bonait.bnframework.api.interceptor;


import org.greenrobot.eventbus.EventBus;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

/**
* 网络请求重试拦截器
*
* @author: wx
* @date: 2022/12/21
*/
public class ConnectRetryInterceptor implements Interceptor {
// 最大重试次数
private int maxRetry;
//失败连续请求时间
private List<Long> failTimeList = new ArrayList<>();

public ConnectRetryInterceptor(int maxRetry) {
this.maxRetry = maxRetry;
}

@NotNull
@Override
public Response intercept(@NotNull Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
int retryNum = 0;
if (!response.isSuccessful()) {
dealFail();
while (retryNum < maxRetry) {
retryNum++;
response = chain.proceed(request);
}
} else {
if (failTimeList.size() > 0) {
failTimeList.clear();
}
}
return response;
}

private void dealFail() {
long curTimeMillis = System.currentTimeMillis();
if (failTimeList.size() > 0) {
Iterator<Long> iterator = failTimeList.iterator();
while (iterator.hasNext()) {
Long timeMillis = iterator.next();
//超过一分钟的移除
if (curTimeMillis - timeMillis >= 60 * 1000) {
iterator.remove();
}
}
//连续3次请求错误
// if(failTimeList.size() >=3){
// EventBus.getDefault().post(new EventBusMessage(EventBusType.FETCH_FAULT_DETECT,0));
// }
} else {
failTimeList.add(curTimeMillis);
}
}
}

+ 21
- 0
app/src/main/java/com/bonait/bnframework/api/interceptor/RequestInterceptor.java View File

@@ -0,0 +1,21 @@
package com.bonait.bnframework.api.interceptor;

import java.io.IOException;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

/**
* 请求拦截器,修改请求header
*
* @author: wx
* @date: 2022/12/21
*/
public class RequestInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request().newBuilder().addHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8").build();
return chain.proceed(request);
}
}

+ 73
- 0
app/src/main/java/com/bonait/bnframework/api/interceptor/StbInfoInterceptor.java View File

@@ -0,0 +1,73 @@
package com.bonait.bnframework.api.interceptor;

import android.text.TextUtils;
import android.util.Base64;

import com.google.gson.JsonObject;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

/**
* 系统属性
*
* @author: wx
* @date: 2023/2/23
*/
public class StbInfoInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
JsonObject jb = new JsonObject();
// try {
// if (DataSaveUtil.Companion.getInstance().decodeBoolean(SpConfig.SP_KEY_USER_IS_LOGIN)) {
// jb.addProperty("account", LocalDataManager.INSTANCE.userId());
// }else {
// jb.addProperty("account", "");
// }
// jb.addProperty("areaCode", LocalDataManager.INSTANCE.areaId());
// jb.addProperty("groupCode", LocalDataManager.INSTANCE.userGroup());
// jb.addProperty("liveGroup", LocalDataManager.INSTANCE.liveGroup());
// jb.addProperty("manufacturer", LocalDataManager.INSTANCE.manufacturer());
// jb.addProperty("model", LocalDataManager.INSTANCE.model());
// jb.addProperty("mac", LocalDataManager.INSTANCE.mac());
// jb.addProperty("softVersion", LocalDataManager.INSTANCE.softVersion());
// jb.addProperty("hardVersion", LocalDataManager.INSTANCE.hardware());
// jb.addProperty("apkVersion", AppUtil.INSTANCE.getVersionCode());
// LogUtil.d("stbInfo:" + jb);
// } catch (Exception e) {
// e.printStackTrace();
// }
byte[] datas = jb.toString().getBytes("UTF-8");
String base64String = Base64.encodeToString(datas, Base64.DEFAULT);
String base64String1 = encodeHeadInfo(base64String);
Request request = chain.request().newBuilder()
// .addHeader("stbInfo", base64String1)
.build();
return chain.proceed(request);
}

/**
* 过滤特殊符号
*
* @param headInfo
* @return
*/
private String encodeHeadInfo(String headInfo) {
StringBuffer stringBuffer = new StringBuffer();
for (int i = 0, length = headInfo.length(); i < length; i++) {
char c = headInfo.charAt(i);
if (c <= '\u001f' || c >= '\u007f') {
stringBuffer.append(String.format("", (int) c));
} else {
stringBuffer.append(c);
}
}
return stringBuffer.toString();
}

}

+ 430
- 0
app/src/main/java/com/bonait/bnframework/ble/BleClientActivity.java View File

@@ -0,0 +1,430 @@
package com.bonait.bnframework.ble;

import android.Manifest;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothProfile;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Base64;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.bonait.bnframework.R;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
import java.util.UUID;

/**
* BLE客户端
*/
@SuppressLint("MissingPermission")
public class BleClientActivity extends AppCompatActivity {

private static final String TAG = BleClientActivity.class.getSimpleName();

private EditText mWriteET;
private TextView mTips;
private BleDevAdapter mBleDevAdapter;
private BluetoothGatt mBluetoothGatt;
private boolean isConnected = false;

private static final int bleSize = 20;
private static Queue<String> queue = new LinkedList<String>();
public static final UUID UUID_SERVICE = UUID.fromString("7377647A-0000-1000-8000-00805F9B34FB"); //全部自定义.
public static final UUID UUID_DESC_NOTITY = UUID.fromString("7377647A-0000-1000-adc5-89df72c789ec");
public static final UUID UUID_CHAR_WRITE = UUID.fromString("7377647A-0000-1000-7264-00805F9B34FB");
public static final UUID UUID_CHAR_READ_NOTIFY = UUID.fromString("7377647A-0000-1000-6e66-00805F9B34FB");
// 与服务端连接的Callback
public BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
BluetoothDevice dev = gatt.getDevice();
Log.i(TAG, String.format("onConnectionStateChange:%s,%s,%s,%s", dev.getName(), dev.getAddress(), status, newState));
if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) {
isConnected = true;
gatt.discoverServices(); //启动服务发现
} else {
isConnected = false;
closeConn();
}
logTv(String.format(status == 0 ? (newState == 2 ? "与[%s]连接成功" : "与[%s]连接断开") : ("与[%s]连接出错,错误码:" + status), dev));
}

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
Log.i(TAG, String.format("onServicesDiscovered:%s,%s,%s", gatt.getDevice().getName(), gatt.getDevice().getAddress(), status));
if (status == BluetoothGatt.GATT_SUCCESS) { //BLE服务发现成功
// 遍历获取BLE服务Services/Characteristics/Descriptors的全部UUID
for (BluetoothGattService service : gatt.getServices()) {
StringBuilder allUUIDs = new StringBuilder("UUIDs={\nS=" + service.getUuid().toString());
for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
allUUIDs.append(",\nC=").append(characteristic.getUuid());
for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors())
allUUIDs.append(",\nD=").append(descriptor.getUuid());
}
allUUIDs.append("}");
Log.i(TAG, "onServicesDiscovered:" + allUUIDs.toString());
logTv("发现服务" + allUUIDs);
}
}
}

@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
UUID uuid = characteristic.getUuid();
String valueStr = new String(characteristic.getValue());
Log.e(TAG, String.format("onCharacteristicRead:%s,%s,%s,%s,%s", gatt.getDevice().getName(), gatt.getDevice().getAddress(), uuid, valueStr, status));
logTv("读取Characteristic[" + uuid + "]:\n" + valueStr);
}

@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
UUID uuid = characteristic.getUuid();
String valueStr = new String(characteristic.getValue());
Log.e(TAG, String.format("onCharacteristicWrite:%s,%s,%s,%s,%s", gatt.getDevice().getName(), gatt.getDevice().getAddress(), uuid, valueStr, status));
logTv("写入Characteristic[" + uuid + "]:\n" + valueStr);

if(queue.size()>0){
bleNotifyDevice(Base64.decode(queue.poll(), Base64.NO_WRAP));
}
}

@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
UUID uuid = characteristic.getUuid();
String valueStr = new String(characteristic.getValue());
Log.e(TAG, String.format("onCharacteristicChanged:%s,%s,%s,%s", gatt.getDevice().getName(), gatt.getDevice().getAddress(), uuid, valueStr));
logTv("通知Characteristic[" + uuid + "]:\n" + valueStr);
}

@Override
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
UUID uuid = descriptor.getUuid();
String valueStr = Arrays.toString(descriptor.getValue());
Log.i(TAG, String.format("onDescriptorRead:%s,%s,%s,%s,%s", gatt.getDevice().getName(), gatt.getDevice().getAddress(), uuid, valueStr, status));
logTv("读取Descriptor[" + uuid + "]:\n" + valueStr);
}

@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
UUID uuid = descriptor.getUuid();
String valueStr = Arrays.toString(descriptor.getValue());
Log.e(TAG, String.format("onDescriptorWrite:%s,%s,%s,%s,%s", gatt.getDevice().getName(), gatt.getDevice().getAddress(), uuid, valueStr, status));
logTv("写入Descriptor[" + uuid + "]:\n" + valueStr);
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_bleclient);
RecyclerView rv = findViewById(R.id.rv_ble);
mWriteET = findViewById(R.id.et_write);
mTips = findViewById(R.id.tv_tips);
rv.setLayoutManager(new LinearLayoutManager(this));
mBleDevAdapter = new BleDevAdapter(new BleDevAdapter.Listener() {

@SuppressLint("NewApi")
@Override
public void onItemClick(BluetoothDevice dev) {
closeConn();

mBluetoothGatt = dev.connectGatt(BleClientActivity.this, false, mBluetoothGattCallback,BluetoothDevice.TRANSPORT_LE);

//autoConnect 这意味着 是否直接连接到远程设备(false)或在远程设备可用时立即自动连接(true)
//以下方案还是无效
//mBluetoothGatt = dev.connectGatt(BleClientActivity.this, true, mBluetoothGattCallback,TRANSPORT_LE); // 连接蓝牙设备
//bluetoothDevice.connectGatt(MainActivity.this, true, gattCallback);总是连接失败,提示status返回133,用了各种方法都不行,
// 后台一查才发现6.0及以上系统的手机要使用bluetoothDevice.connectGatt(MainActivity.this,true, gattCallback, TRANSPORT_LE),
// 其中TRANSPORT_LE参数是设置传输层模式。传输层模式有三种TRANSPORT_AUTO 、TRANSPORT_BREDR 和TRANSPORT_LE。
// 如果不传默认TRANSPORT_AUTO,6.0系统及以上需要使用TRANSPORT_LE这种传输模式,
// 具体为啥,我也不知道,我猜是因为Android6.0及以上系统重新定义了蓝牙BLE的传输模式必须使用TRANSPORT_LE这种方式吧。

//发起蓝牙Gatt连接 BluetoothDevice.connectGatt(Context context, boolean autoConnect,
//BluetoothGattCallback callback),这里有一个参数autoConnect,如果为 true
//的话,系统就会发起一个后台连接,等到系统发现了一个设备,就会自动连上,通常这个过程是非常慢的。为
//false 的话,就会直接连接,通常会比较快。同样,BluetoothGatt.connect()只能发起一个后台连接,不是直
//接连接,所以连接时设置autoConnect参数设置为false,如果想实现重连功能的话,自己去手动实现吧,实在不
//想手动写

logTv(String.format("与[%s]开始连接............", dev));
}
});
rv.setAdapter(mBleDevAdapter);
checkPermissions();
}

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
checkPermissions();
}

private void checkPermissions(){
boolean isPermission = true;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
String[] permissions = {
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION};
for (String str : permissions) {
if (checkSelfPermission(str) != PackageManager.PERMISSION_GRANTED) {
Log.i("tag"," requestPermissions "+str);
ActivityCompat.requestPermissions(this,permissions, 111);
isPermission = false;
break;
}else {
Log.i("tag"," permissions true "+str);
}
}
}
if(isPermission){

}
}


@Override
protected void onDestroy() {
super.onDestroy();
closeConn();
}

/**
* 清理本地的BluetoothGatt 的缓存,以保证在蓝牙连接设备的时候,设备的服务、特征是最新的
* @param gatt
* @return
*/
public boolean refreshDeviceCache(BluetoothGatt gatt) {
if(null != gatt){
try {
BluetoothGatt localBluetoothGatt = gatt;
Method localMethod = localBluetoothGatt.getClass().getMethod( "refresh", new Class[0]);
if (localMethod != null) {
boolean bool = ((Boolean) localMethod.invoke(
localBluetoothGatt, new Object[0])).booleanValue();
return bool;
}
} catch (Exception localException) {
localException.printStackTrace();
}
}
return false;
}

// BLE中心设备连接外围设备的数量有限(大概2~7个),在建立新连接之前必须释放旧连接资源,否则容易出现连接错误133
private void closeConn() {
if (mBluetoothGatt != null) {
refreshDeviceCache(mBluetoothGatt);
mBluetoothGatt.disconnect();
mBluetoothGatt.close();
}
}

// 扫描BLE
public void reScan(View view) {
if (mBleDevAdapter.isScanning)
Toast.makeText(this, "正在扫描", Toast.LENGTH_SHORT).show();
else
mBleDevAdapter.reScan();
}

// 清除Log
public void cleanLog(View view) {
mTips.setText("");
}

// 注意:连续频繁读写数据容易失败,读写操作间隔最好200ms以上,或等待上次回调完成后再进行下次读写操作!
// 读取数据成功会回调->onCharacteristicChanged()
public void read(View view) {
BluetoothGattService service = getGattService(UUID_SERVICE);
if (service != null) {
BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID_CHAR_READ_NOTIFY);//通过UUID获取可读的Characteristic
mBluetoothGatt.readCharacteristic(characteristic);
}
}

// 注意:连续频繁读写数据容易失败,读写操作间隔最好200ms以上,或等待上次回调完成后再进行下次读写操作!
// 写入数据成功会回调->onCharacteristicWrite()
public void write(View view) {
BluetoothGattService service = getGattService(UUID_SERVICE);
if (service != null) {
String text = mWriteET.getText().toString();
BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID_CHAR_WRITE);//通过UUID获取可写的Characteristic
characteristic.setValue(text.getBytes()); //单次最多20个字节
mBluetoothGatt.writeCharacteristic(characteristic);
}
}

// 设置通知Characteristic变化会回调->onCharacteristicChanged()
public void setNotify(View view) {
BluetoothGattService service = getGattService(UUID_SERVICE);
if (service != null) {
// 设置Characteristic通知
BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID_CHAR_READ_NOTIFY);//通过UUID获取可通知的Characteristic
mBluetoothGatt.setCharacteristicNotification(characteristic, true);

// 向Characteristic的Descriptor属性写入通知开关,使蓝牙设备主动向手机发送数据
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUID_DESC_NOTITY);
// descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);//和通知类似,但服务端不主动发数据,只指示客户端读取数据
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);
}
}

// 获取Gatt服务
private BluetoothGattService getGattService(UUID uuid) {
if (!isConnected) {
Toast.makeText(this, "没有连接", Toast.LENGTH_SHORT).show();
return null;
}
BluetoothGattService service = mBluetoothGatt.getService(uuid);
if (service == null)
Toast.makeText(this, "没有找到服务UUID", Toast.LENGTH_SHORT).show();
return service;
}

// 输出日志
private void logTv(final String msg) {
if (isDestroyed())
return;
runOnUiThread(new Runnable() {
@Override
public void run() {
mTips.append(msg + "\n\n");
}
});
}


public void writeCmd(View view){
BluetoothGattService service = getGattService(UUID_SERVICE);
if (service != null) {

String text = "01010100002D22222222222222222222222222";
byte[] write_cmd = text.getBytes();
//byte[] write_cmd = new byte[] {(byte)0x01,0x01, 0x01, 0x00,0x00,(byte)0x2D};

BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID_CHAR_WRITE);//通过UUID获取可写的Characteristic
characteristic.setValue(write_cmd); //单次最多20个字节
mBluetoothGatt.writeCharacteristic(characteristic);
}
}

public static byte[] hexStrToByteArray(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 void writeLongCmd(View view){

//String text = "AA010101010000";
String text = "AA01010101000F0102030405060708090A0B0C0D0E0F";
String crc = "123";
String message = text + crc;

byte[] write_cmd = hexStrToByteArray(message);
//byte[] write_cmd = new byte[] {(byte)0xaa,0x01,0x01,0x01,0x01,0x00,0x00,(byte)0x2D};

// 1、定义通讯协议,如下(这里只是个举例,可以项目扩展)
// 协议号(2个字节) 消息号(1个字节) 功能(1个字节) 子功能(1个字节) 数据长度(2个字节) Data(N个字节) CRC校验(1个字节)
// AA01 01 01 01 0000 2D

// 消息号(1个字节) 功能(1个字节) 子功能(1个字节) 数据长度(2个字节) 数据内容(N个字节) CRC校验(1个字节)
// 01 01 01 0000 -- 2D

//2、封装通用发送数据接口(拆包)
//该接口根据会发送数据内容按最大字节数拆分(一般20字节)放入队列,拆分完后,依次从队列里取出发送

//3、封装通用接收数据接口(组包)
//该接口根据从接收的数据按协议里的定义解析数据长度判读是否完整包,不是的话把每条消息累加起来

//4、解析完整的数据包,进行业务逻辑处理

//5、协议还可以加密解密,需要注意的选算法参数的时候,加密后的长度最好跟原数据长度一致,这样不会影响拆包组包

logTv("写入长指令(大于20字节):\n" + message);

bleNotifyDeviceByQueue(false,write_cmd);
}


private void bleNotifyDeviceByQueue(boolean isNeedEncrypt, byte[] data){

Log.e(TAG, "bleNotifyAppByQueue isNeedEncrypt=" + isNeedEncrypt);

boolean isNeedSend = true;
if (queue.size() > 0) {
isNeedSend = false;
}

int section = data.length / bleSize;
int remain = data.length % bleSize;

//Log.e(TAG, "queue.size=" + queue.size() + ",data.length=" + data.length + ",section=" + section + ",remain=" + remain);

if (section > 1 || (section == 1 && remain > 0)) {
for (int i = 0; i < section; i++) {
byte b_data[] = new byte[bleSize];
System.arraycopy(data, i * bleSize, b_data, 0, bleSize);

queue.add(Base64.encodeToString(b_data, Base64.NO_WRAP));
}

if (remain > 0) {
byte b_data[] = new byte[remain];
System.arraycopy(data, section * bleSize, b_data, 0, remain);

queue.add(Base64.encodeToString(b_data, Base64.NO_WRAP));
}
} else {
byte b_data[] = new byte[data.length];
System.arraycopy(data, 0, b_data, 0, data.length);

queue.add(Base64.encodeToString(b_data, Base64.NO_WRAP));
}

if (isNeedSend) {
bleNotifyDevice(Base64.decode(queue.poll(), Base64.NO_WRAP));
}
}

private void bleNotifyDevice(byte[] data) {
BluetoothGattService service = getGattService(UUID_SERVICE);
if (service != null) {
BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID_CHAR_WRITE);//通过UUID获取可写的Characteristic
characteristic.setValue(data); //单次最多20个字节
mBluetoothGatt.writeCharacteristic(characteristic);
}
}
}

+ 186
- 0
app/src/main/java/com/bonait/bnframework/ble/BleDevAdapter.java View File

@@ -0,0 +1,186 @@
package com.bonait.bnframework.ble;

import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanResult;
import android.graphics.Color;
import android.os.Handler;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import com.bonait.bnframework.R;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@SuppressLint("MissingPermission")
public class BleDevAdapter extends RecyclerView.Adapter<BleDevAdapter.VH> {

private static final String TAG = BleDevAdapter.class.getSimpleName();

private static final String DeviceName = "62F";//要搜索的设备蓝牙名称高亮显示

private final Listener mListener;
private final Handler mHandler = new Handler();
private final List<BleDev> mDevices = new ArrayList<>();
public boolean isScanning;

@SuppressLint("NewApi")
private final ScanCallback mScanCallback = new ScanCallback() {// 扫描Callback
@Override
public void onScanResult(int callbackType, ScanResult result) {
BluetoothDevice device = result.getDevice();
BleDev dev = new BleDev(device, result);

Log.i(TAG, "onScan: device=" + device+";dev="+dev);
if (!mDevices.contains(dev) &&device!=null&& device.getName()!=null) {//&& device.getName().contains("62F") && device.getName()!=null
String deviceName = device.getName();// 获取蓝牙设备名字
String deviceAddress = device.getAddress();// 获取蓝牙设备mac地址
int deviceBondState = device.getBondState();// 蓝牙绑定状态

if(device.getName()!=null){
mDevices.add(0,dev);
}else {
mDevices.add(dev);
}

/*if(deviceName!=null && !deviceName.toLowerCase().contains("mi")){
mDevices.add(dev);
}*/

BluetoothClass bluetoothClass = device.getBluetoothClass();
final int deviceClass = bluetoothClass.getDeviceClass(); //设备类型(音频、手机、电脑、音箱等等)
final int majorDeviceClass = bluetoothClass.getMajorDeviceClass();//具体的设备类型(例如音频设备又分为音箱、耳机、麦克风等等)

Log.e(TAG,"onScanResult ======= deviceName:"+deviceName+",deviceAddress="+deviceAddress+",deviceBondState="+deviceBondState
+",deviceClass="+Integer.toHexString(deviceClass)+",majorDeviceClass="+Integer.toHexString(majorDeviceClass));
notifyDataSetChanged();

Log.i(TAG, "onScanResult: " + result); // result.getScanRecord() 获取BLE广播数据
}
}
};

BleDevAdapter(Listener listener) {
mListener = listener;
scanBle();
}

// 重新扫描
public void reScan() {
mDevices.clear();
notifyDataSetChanged();
scanBle();
}

// 扫描BLE蓝牙(不会扫描经典蓝牙)
@SuppressLint("NewApi")
private void scanBle() {
isScanning = true;

Log.e(TAG,"scanBle");

// BluetoothAdapter bluetoothAdapter = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE).getDefaultAdapter();
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
final BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
// Android5.0新增的扫描API,扫描返回的结果更友好,比如BLE广播数据以前是byte[] scanRecord,而新API帮我们解析成ScanRecord类

bluetoothLeScanner.startScan(mScanCallback);

mHandler.postDelayed(new Runnable() {
@Override
public void run() {
bluetoothLeScanner.stopScan(mScanCallback); //停止扫描
isScanning = false;
}
}, 15000);
}

@NonNull
@Override
public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_dev, parent, false);
return new VH(view);
}

@SuppressLint("NewApi")
@Override
public void onBindViewHolder(@NonNull final VH holder, int position) {
BleDev dev = mDevices.get(position);
String name = dev.dev.getName();
String address = dev.dev.getAddress();
holder.name.setText(String.format("%s, %s, Rssi=%s", name, address, dev.scanResult.getRssi()));
holder.address.setText(String.format("广播数据{%s}", dev.scanResult.getScanRecord()));

if(holder.name.getText().toString().toLowerCase().contains(DeviceName)){
Log.e(TAG,"c="+name);
holder.name.setBackgroundColor(Color.RED);
}else {
holder.name.setBackgroundColor(0);
}

}

@Override
public int getItemCount() {
return mDevices.size();
}

class VH extends RecyclerView.ViewHolder implements View.OnClickListener {
final TextView name;
final TextView address;

VH(final View itemView) {
super(itemView);
itemView.setOnClickListener(this);
name = itemView.findViewById(R.id.name);
address = itemView.findViewById(R.id.address);
}

@Override
public void onClick(View v) {
int pos = getAdapterPosition();
Log.d(TAG, "onClick, getAdapterPosition=" + pos);
if (pos >= 0 && pos < mDevices.size())
mListener.onItemClick(mDevices.get(pos).dev);
}
}

public interface Listener {
void onItemClick(BluetoothDevice dev);
}

public static class BleDev {
public BluetoothDevice dev;
ScanResult scanResult;

BleDev(BluetoothDevice device, ScanResult result) {
dev = device;
scanResult = result;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BleDev bleDev = (BleDev) o;
return Objects.equals(dev, bleDev.dev);
}

@Override
public int hashCode() {
return Objects.hash(dev);
}
}
}

+ 297
- 0
app/src/main/java/com/bonait/bnframework/ble/BleServiceActivity.java View File

@@ -0,0 +1,297 @@
package com.bonait.bnframework.ble;

import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattServer;
import android.bluetooth.BluetoothGattServerCallback;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
import android.bluetooth.le.BluetoothLeAdvertiser;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelUuid;
import android.util.Log;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;


import com.bonait.bnframework.databinding.ActivityServiceBinding;

import java.util.UUID;

/**
* @author liupeng
* @description:
* @date :2024/2/5 10:02
*/
@SuppressLint("MissingPermission")
public class BleServiceActivity extends AppCompatActivity {
private final String TAG = "MainActivity==>";
private ActivityServiceBinding bindingView;

private Handler handler = new Handler(Looper.getMainLooper()){
@SuppressLint("HandlerLeak")
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
bindingView = ActivityServiceBinding.inflate(getLayoutInflater());
setContentView(bindingView.getRoot());


bindingView.btn1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(!open){
openBlueTooth();
startBLEServer();
open = true;
}
}
});
}

@Override
protected void onPause() {
super.onPause();
finish();
}

@Override
protected void onDestroy() {
super.onDestroy();
closeBlueTooth();
stopBleServer();
}

private boolean open = false;
@SuppressLint("MissingPermission")
public void openBlueTooth(){
// 打开蓝牙(提示对话框)
// Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
// discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
// startActivity(discoverableIntent);

// 打开蓝牙(静默,无提示)
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
bluetoothAdapter.enable();//需要BLUETOOTH_ADMIN权限

}
@SuppressLint("MissingPermission")
public void closeBlueTooth(){
// 关闭蓝牙
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
bluetoothAdapter.disable();
}

public static final UUID UUID_SERVICE = UUID.fromString("7377647A-0000-1000-8000-00805F9B34FB"); //全部自定义.
public static final UUID UUID_DESC_NOTITY = UUID.fromString("7377647A-0000-1000-adc5-89df72c789ec");
public static final UUID UUID_CHAR_WRITE = UUID.fromString("7377647A-0000-1000-7264-00805F9B34FB");
public static final UUID UUID_CHAR_READ_NOTIFY = UUID.fromString("7377647A-0000-1000-6e66-00805F9B34FB");
private BluetoothLeAdvertiser mBluetoothLeAdvertiser; // BLE广播
private BluetoothGattServer mBluetoothGattServer; // BLE服务端
private BluetoothGatt bluetoothGatt;
// BLE广播Callback
@SuppressLint("NewApi")
private final AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() {
@Override
public void onStartSuccess(AdvertiseSettings settingsInEffect) {
Log.d(TAG,"BLE广播开启成功");
handler.post(new Runnable() {
@Override
public void run() {
bindingView.tvState.setText("蓝牙已开启");
}
});
}
@Override
public void onStartFailure(int errorCode) {
Log.e(TAG,"BLE广播开启失败,错误码:" + errorCode);
handler.post(new Runnable() {
@Override
public void run() {
bindingView.tvState.setText("蓝牙开启失败");
}
});
}
};
// private final BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() {
// @Override
// public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
// super.onMtuChanged(gatt, mtu, status);
// Log.d("onMtuChanged"," onMtuChanged");
// if (BluetoothGatt.GATT_SUCCESS == status) {
// Log.d("BleService", "onMtuChanged success MTU = " + mtu);
// }else {
// Log.d("BleService", "onMtuChanged fail ");
// }
// }
// };
// private boolean requestMtu2(){
// if (bluetoothGatt != null) {
// // 25 *6 +3 =153
// return bluetoothGatt.requestMtu(517);
// }
// return false;
// }
/*
* 蓝牙连接,通信
* */
// BLE服务端Callback
private final BluetoothGattServerCallback mBluetoothGattServerCallback = new BluetoothGattServerCallback() {
@SuppressLint("MissingPermission")
@Override
public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {

// bluetoothGatt = device.connectGatt(getApplication(), true, mBluetoothGattCallback);
// Log.i("requestMTUU=",bluetoothGatt.requestMtu(25)+"");
// Log.i("requestMtu2=",requestMtu2()+"");
// onMtuChanged(device,517);
Log.i(TAG, String.format("onConnectionStateChange:%s,%s,%s,%s", device.getName(), device.getAddress(), status, newState));
String stateStr = String.format(status == 0 ? (newState == 2 ? "与[%s]连接成功" : "与[%s]连接断开") : ("与[%s]连接出错,错误码:" + status), device);
Log.d(TAG,stateStr);
handler.post(new Runnable() {
@Override
public void run() {
bindingView.tvState.setText(stateStr+"");
}
});
}
@Override
public void onServiceAdded(int status, BluetoothGattService service) {
Log.i(TAG, String.format("onServiceAdded:%s,%s", status, service.getUuid()));
Log.d(TAG,String.format(status == 0 ? "添加服务[%s]成功" : "添加服务[%s]失败,错误码:" + status, service.getUuid()));
}
@SuppressLint("MissingPermission")
@Override
public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
Log.i(TAG, String.format("onCharacteristicReadRequest:%s,%s,%s,%s,%s", device.getName(), device.getAddress(), requestId, offset, characteristic.getUuid()));
// mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS,offset,new byte[]{1, 2,3,4} );// 响应客户端

String test = " 盒子信息 ******* /// 99";
byte[] responseBytes = test.getBytes();
Log.i(TAG,"responseBytes.length=="+responseBytes.length);
if(responseBytes.length>offset){
int restLen = responseBytes.length-offset;
byte[] bytesCopy = new byte[restLen>22?22:restLen];
System.arraycopy(responseBytes,offset,bytesCopy,0,restLen>22?22:restLen);
mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS,offset,bytesCopy );// 响应客户端
Log.d(TAG,"客户端读取Characteristic[" + characteristic.getUuid() + "]:\n" + bytesCopy.toString());
}else{
mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS,offset,responseBytes );// 响应客户端
}
}
@SuppressLint("MissingPermission")
@Override
public void onCharacteristicWriteRequest(final BluetoothDevice device, int requestId,
final BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] requestBytes) {
// 获取客户端发过来的数据
String requestStr = new String(requestBytes);
Log.i(TAG, String.format("onCharacteristicWriteRequest:%s,%s,%s,%s,%s,%s,%s,%s", device.getName(), device.getAddress(), requestId, characteristic.getUuid(),
preparedWrite, responseNeeded, offset, "requestStr="+requestStr));
mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, new byte[]{1,2,1});// 响应客户端
handler.post(new Runnable() {
@Override
public void run() {
bindingView.tv.setText("收到信息="+requestStr);
}
});
}

@SuppressLint("MissingPermission")
@Override
public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
Log.i(TAG, String.format("onExecuteWrite:%s,%s,%s,%s", device.getName(), device.getAddress(), requestId, execute));
}
@SuppressLint("MissingPermission")
@Override
public void onNotificationSent(BluetoothDevice device, int status) {
Log.i(TAG, String.format("onNotificationSent:%s,%s,%s", device.getName(), device.getAddress(), status));
}



@SuppressLint("MissingPermission")
@Override
public void onMtuChanged(BluetoothDevice device, int mtu) {
super.onMtuChanged(device, 512);
Log.i(TAG, String.format("onMtuChanged:%s,%s,%s", device.getName(), device.getAddress(), mtu));
}


};
BluetoothGattCharacteristic characteristicRead;
@SuppressLint({"NewApi", "MissingPermission"})
public void startBLEServer(){
BluetoothManager bluetoothManager = (BluetoothManager) getApplication().getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
bluetoothAdapter.setName("62F");
// ============启动BLE蓝牙广播(广告) =================================================================================
Log.i(TAG,"启动BLE蓝牙广播(广告)");
//广播设置(必须)
AdvertiseSettings settings = new AdvertiseSettings.Builder()
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_POWER) //广播模式: 低功耗,平衡,低延迟
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) //发射功率级别: 极低,低,中,高
.setConnectable(true) //能否连接,广播分为可连接广播和不可连接广播
.setTimeout(0)
.build();
//广播数据(必须,广播启动就会发送)
AdvertiseData advertiseData = new AdvertiseData.Builder()
.setIncludeDeviceName(true) //包含蓝牙名称
.setIncludeTxPowerLevel(true) //包含发射功率级别
.addManufacturerData(0x09, "sw".getBytes()) //设备厂商数据,自定义
.build();
//扫描响应数据(可选,当客户端扫描时才发送)
AdvertiseData scanResponse = new AdvertiseData.Builder()
.addManufacturerData(2, new byte[]{66, 66,31,45, (byte) 0xac}) //设备厂商数据,自定义
.addServiceUuid(new ParcelUuid(UUID_SERVICE)) //服务UUID
// .addServiceData(new ParcelUuid(UUID_SERVICE), new byte[]{2}) //服务数据,自定义
.build();
mBluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser();
mBluetoothLeAdvertiser.startAdvertising(settings, advertiseData, scanResponse, mAdvertiseCallback);

// 注意:必须要开启可连接的BLE广播,其它设备才能发现并连接BLE服务端!
// =============启动BLE蓝牙服务端=====================================================================================
BluetoothGattService service = new BluetoothGattService(UUID_SERVICE, BluetoothGattService.SERVICE_TYPE_PRIMARY);
//添加可读+通知characteristic
characteristicRead = new BluetoothGattCharacteristic(UUID_CHAR_READ_NOTIFY,
BluetoothGattCharacteristic.PROPERTY_NOTIFY|BluetoothGattCharacteristic.PROPERTY_READ|BluetoothGattCharacteristic.PROPERTY_WRITE,
BluetoothGattCharacteristic.PERMISSION_READ|BluetoothGattCharacteristic.PERMISSION_WRITE);
characteristicRead.addDescriptor(new BluetoothGattDescriptor(UUID_DESC_NOTITY, BluetoothGattCharacteristic.PERMISSION_WRITE | BluetoothGattCharacteristic.PERMISSION_READ));
service.addCharacteristic(characteristicRead);
//添加可写characteristic
BluetoothGattCharacteristic characteristicWrite = new BluetoothGattCharacteristic(UUID_CHAR_WRITE,
BluetoothGattCharacteristic.PROPERTY_NOTIFY|BluetoothGattCharacteristic.PROPERTY_READ|BluetoothGattCharacteristic.PROPERTY_WRITE
, BluetoothGattCharacteristic.PERMISSION_WRITE|BluetoothGattCharacteristic.PERMISSION_READ);
service.addCharacteristic(characteristicWrite);
if (bluetoothManager != null)
mBluetoothGattServer = bluetoothManager.openGattServer(getApplication(), mBluetoothGattServerCallback);
mBluetoothGattServer.addService(service);
}

@SuppressLint("NewApi")
public void stopBleServer(){
if (mBluetoothLeAdvertiser != null)
mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback);
if (mBluetoothGattServer != null)
mBluetoothGattServer.close();
}

}

+ 6
- 1
app/src/main/java/com/bonait/bnframework/modules/welcome/activity/WelcomeActivity.java View File

@@ -18,6 +18,7 @@ import com.apkfuns.logutils.LogUtils;
import com.bonait.bnframework.BuildConfig;
import com.bonait.bnframework.MainApplication;
import com.bonait.bnframework.R;
import com.bonait.bnframework.ble.BleClientActivity;
import com.bonait.bnframework.business.MainInit;
import com.bonait.bnframework.common.base.BaseActivity;
import com.bonait.bnframework.common.constant.ConfigName;
@@ -26,6 +27,7 @@ import com.bonait.bnframework.common.helper.CrashHandler;
import com.bonait.bnframework.common.utils.ScreenUtils;
import com.bonait.bnframework.manager.ActivityLifecycleManager;
import com.bonait.bnframework.modules.home.activity.BottomNavigation2Activity;
import com.bonait.bnframework.serial.ComAssistantActivity;
import com.bonait.bnframework.test.TestActivity;
import com.bonait.bnframework.ui.util.DisplayManager;
import com.lzy.okgo.OkGo;
@@ -147,7 +149,9 @@ public class WelcomeActivity extends BaseActivity {
*/
public void PermissionCheck() {

String[] params = {Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA};
String[] params = {Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.CAMERA};
//String[] params = {Manifest.permission.WRITE_EXTERNAL_STORAGE};
//判断是否获取权限
if (EasyPermissions.hasPermissions(this, params)) {
@@ -254,6 +258,7 @@ public class WelcomeActivity extends BaseActivity {
ConfigName.getInstance().IsPortraitScreen=ScreenUtils.IsPortraitScreen(this);

// 跳转到登录页面
// Intent intent = new Intent(WelcomeActivity.this, ComAssistantActivity.class);
Intent intent = new Intent(WelcomeActivity.this, LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);


+ 111
- 0
app/src/main/java/com/bonait/bnframework/serial/AssistBean.java View File

@@ -0,0 +1,111 @@
package com.bonait.bnframework.serial;

import java.io.Serializable;

/**
* @author benjaminwan
*用于保存界面数据
*/
public class AssistBean implements Serializable{
private static final long serialVersionUID = -5620661009186692227L;
private boolean isTxt=true;
private String SendTxtA="COMA",SendTxtB="COMB",SendTxtC="COMC",SendTxtD="COMD";
private String SendHexA="AA",SendHexB="BB",SendHexC="CC",SendHexD="DD";
public String sTimeA="500";
public String sTimeB="500";
public String sTimeC="500";
public String sTimeD="500";
public boolean isTxt()
{
return isTxt;
}
public void setTxtMode(boolean isTxt)
{
this.isTxt = isTxt;
}

public String getSendA()
{
if (isTxt)
{
return SendTxtA;
} else
{
return SendHexA;
}
}
public String getSendB()
{
if (isTxt)
{
return SendTxtB;
} else
{
return SendHexB;
}
}
public String getSendC()
{
if (isTxt)
{
return SendTxtC;
} else
{
return SendHexC;
}
}
public String getSendD()
{
if (isTxt)
{
return SendTxtD;
} else
{
return SendHexD;
}
}

public void setSendA(String sendA)
{
if (isTxt)
{
SendTxtA = sendA;
} else
{
SendHexA = sendA;
}
}

public void setSendB(String sendB)
{
if (isTxt)
{
SendTxtB = sendB;
} else
{
SendHexB = sendB;
}
}

public void setSendC(String sendC)
{
if (isTxt)
{
SendTxtC = sendC;
} else
{
SendHexC = sendC;
}
}

public void setSendD(String sendD)
{
if (isTxt)
{
SendTxtD = sendD;
} else
{
SendHexD = sendD;
}
}
}

+ 737
- 0
app/src/main/java/com/bonait/bnframework/serial/ComAssistantActivity.java View File

@@ -0,0 +1,737 @@
package com.bonait.bnframework.serial;

import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Configuration;
import android.os.Bundle;
import android.text.InputType;
import android.text.method.KeyListener;
import android.text.method.NumberKeyListener;
import android.text.method.TextKeyListener;
import android.text.method.TextKeyListener.Capitalize;
import android.util.Base64;
import android.view.KeyEvent;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.RadioButton;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;

import androidx.appcompat.app.AppCompatActivity;

import com.apkfuns.logutils.LogUtils;
import com.bonait.bnframework.R;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

/**
* serialport api和jni取自http://code.google.com/p/android-serialport-api/
*
* @author benjaminwan
* 串口助手,支持4串口同时读写
* 程序载入时自动搜索串口设备
* n,8,1,没得选
*
* 本页面是参考页面
* http://code1.okbase.net/codefile/ComAssistantActivity.java_2014121928629_9.htm
*/
public class ComAssistantActivity extends AppCompatActivity {
EditText editTextRecDisp, editTextLines, editTextCOMA, editTextCOMB, editTextCOMC, editTextCOMD;
EditText editTextTimeCOMA, editTextTimeCOMB, editTextTimeCOMC, editTextTimeCOMD;
CheckBox checkBoxAutoClear, checkBoxAutoCOMA, checkBoxAutoCOMB, checkBoxAutoCOMC, checkBoxAutoCOMD;
Button ButtonClear, ButtonSendCOMA, ButtonSendCOMB, ButtonSendCOMC, ButtonSendCOMD;
ToggleButton toggleButtonCOMA, toggleButtonCOMB, toggleButtonCOMC, toggleButtonCOMD;
Spinner SpinnerCOMA, SpinnerCOMB, SpinnerCOMC, SpinnerCOMD;
Spinner SpinnerBaudRateCOMA, SpinnerBaudRateCOMB, SpinnerBaudRateCOMC, SpinnerBaudRateCOMD;
RadioButton radioButtonTxt, radioButtonHex;
SerialControl ComA, ComB, ComC, ComD;//4个串口
DispQueueThread DispQueue;//刷新显示线程
SerialPortFinder mSerialPortFinder;//串口设备搜索
AssistBean AssistData;//用于界面数据序列化和反序列化
int iRecLines = 0;//接收区行数

/**
* Called when the activity is first created.
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_serial_test);
ComA = new SerialControl();
ComB = new SerialControl();
ComC = new SerialControl();
ComD = new SerialControl();
DispQueue = new DispQueueThread();
DispQueue.start();
AssistData = getAssistData();
setControls();
}

@Override
public void onDestroy() {
saveAssistData(AssistData);
CloseComPort(ComA);
CloseComPort(ComB);
CloseComPort(ComC);
CloseComPort(ComD);
super.onDestroy();
}

@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
CloseComPort(ComA);
CloseComPort(ComB);
CloseComPort(ComC);
CloseComPort(ComD);
setContentView(R.layout.activity_serial_test);
setControls();
}

//----------------------------------------------------
private void setControls() {
String appName = getString(R.string.app_name);
try {
PackageInfo pinfo = getPackageManager().getPackageInfo("com.mstxx.kaoqin", PackageManager.GET_CONFIGURATIONS);
String versionName = pinfo.versionName;
// String versionCode = String.valueOf(pinfo.versionCode);
setTitle(appName + " V" + versionName);
} catch (NameNotFoundException e) {
e.printStackTrace();
}
editTextRecDisp = (EditText) findViewById(R.id.editTextRecDisp);
editTextLines = (EditText) findViewById(R.id.editTextLines);
editTextCOMA = (EditText) findViewById(R.id.editTextCOMA);
editTextCOMB = (EditText) findViewById(R.id.editTextCOMB);
editTextCOMC = (EditText) findViewById(R.id.editTextCOMC);
editTextCOMD = (EditText) findViewById(R.id.editTextCOMD);
editTextTimeCOMA = (EditText) findViewById(R.id.editTextTimeCOMA);
editTextTimeCOMB = (EditText) findViewById(R.id.editTextTimeCOMB);
editTextTimeCOMC = (EditText) findViewById(R.id.editTextTimeCOMC);
editTextTimeCOMD = (EditText) findViewById(R.id.editTextTimeCOMD);

checkBoxAutoClear = (CheckBox) findViewById(R.id.checkBoxAutoClear);
checkBoxAutoCOMA = (CheckBox) findViewById(R.id.checkBoxAutoCOMA);
checkBoxAutoCOMB = (CheckBox) findViewById(R.id.checkBoxAutoCOMB);
checkBoxAutoCOMC = (CheckBox) findViewById(R.id.checkBoxAutoCOMC);
checkBoxAutoCOMD = (CheckBox) findViewById(R.id.checkBoxAutoCOMD);
ButtonClear = (Button) findViewById(R.id.ButtonClear);
ButtonSendCOMA = (Button) findViewById(R.id.ButtonSendCOMA);
ButtonSendCOMB = (Button) findViewById(R.id.ButtonSendCOMB);
ButtonSendCOMC = (Button) findViewById(R.id.ButtonSendCOMC);
ButtonSendCOMD = (Button) findViewById(R.id.ButtonSendCOMD);
toggleButtonCOMA = (ToggleButton) findViewById(R.id.toggleButtonCOMA);
toggleButtonCOMB = (ToggleButton) findViewById(R.id.ToggleButtonCOMB);
toggleButtonCOMC = (ToggleButton) findViewById(R.id.ToggleButtonCOMC);
toggleButtonCOMD = (ToggleButton) findViewById(R.id.ToggleButtonCOMD);
SpinnerCOMA = (Spinner) findViewById(R.id.SpinnerCOMA);
SpinnerCOMB = (Spinner) findViewById(R.id.SpinnerCOMB);
SpinnerCOMC = (Spinner) findViewById(R.id.SpinnerCOMC);
SpinnerCOMD = (Spinner) findViewById(R.id.SpinnerCOMD);
SpinnerBaudRateCOMA = (Spinner) findViewById(R.id.SpinnerBaudRateCOMA);
SpinnerBaudRateCOMB = (Spinner) findViewById(R.id.SpinnerBaudRateCOMB);
SpinnerBaudRateCOMC = (Spinner) findViewById(R.id.SpinnerBaudRateCOMC);
SpinnerBaudRateCOMD = (Spinner) findViewById(R.id.SpinnerBaudRateCOMD);
radioButtonTxt = (RadioButton) findViewById(R.id.radioButtonTxt);
radioButtonHex = (RadioButton) findViewById(R.id.radioButtonHex);

editTextCOMA.setOnEditorActionListener(new EditorActionEvent());
editTextCOMB.setOnEditorActionListener(new EditorActionEvent());
editTextCOMC.setOnEditorActionListener(new EditorActionEvent());
editTextCOMD.setOnEditorActionListener(new EditorActionEvent());
editTextTimeCOMA.setOnEditorActionListener(new EditorActionEvent());
editTextTimeCOMB.setOnEditorActionListener(new EditorActionEvent());
editTextTimeCOMC.setOnEditorActionListener(new EditorActionEvent());
editTextTimeCOMD.setOnEditorActionListener(new EditorActionEvent());
editTextCOMA.setOnFocusChangeListener(new FocusChangeEvent());
editTextCOMB.setOnFocusChangeListener(new FocusChangeEvent());
editTextCOMC.setOnFocusChangeListener(new FocusChangeEvent());
editTextCOMD.setOnFocusChangeListener(new FocusChangeEvent());
editTextTimeCOMA.setOnFocusChangeListener(new FocusChangeEvent());
editTextTimeCOMB.setOnFocusChangeListener(new FocusChangeEvent());
editTextTimeCOMC.setOnFocusChangeListener(new FocusChangeEvent());
editTextTimeCOMD.setOnFocusChangeListener(new FocusChangeEvent());

radioButtonTxt.setOnClickListener(new radioButtonClickEvent());
radioButtonHex.setOnClickListener(new radioButtonClickEvent());
ButtonClear.setOnClickListener(new ButtonClickEvent());
ButtonSendCOMA.setOnClickListener(new ButtonClickEvent());
ButtonSendCOMB.setOnClickListener(new ButtonClickEvent());
ButtonSendCOMC.setOnClickListener(new ButtonClickEvent());
ButtonSendCOMD.setOnClickListener(new ButtonClickEvent());
toggleButtonCOMA.setOnCheckedChangeListener(new ToggleButtonCheckedChangeEvent());
toggleButtonCOMB.setOnCheckedChangeListener(new ToggleButtonCheckedChangeEvent());
toggleButtonCOMC.setOnCheckedChangeListener(new ToggleButtonCheckedChangeEvent());
toggleButtonCOMD.setOnCheckedChangeListener(new ToggleButtonCheckedChangeEvent());
checkBoxAutoCOMA.setOnCheckedChangeListener(new CheckBoxChangeEvent());
checkBoxAutoCOMB.setOnCheckedChangeListener(new CheckBoxChangeEvent());
checkBoxAutoCOMC.setOnCheckedChangeListener(new CheckBoxChangeEvent());
checkBoxAutoCOMD.setOnCheckedChangeListener(new CheckBoxChangeEvent());

ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
R.array.baudrates_value, android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
SpinnerBaudRateCOMA.setAdapter(adapter);
SpinnerBaudRateCOMB.setAdapter(adapter);
SpinnerBaudRateCOMC.setAdapter(adapter);
SpinnerBaudRateCOMD.setAdapter(adapter);
SpinnerBaudRateCOMA.setSelection(12);
SpinnerBaudRateCOMB.setSelection(12);
SpinnerBaudRateCOMC.setSelection(12);
SpinnerBaudRateCOMD.setSelection(12);

mSerialPortFinder = new SerialPortFinder();
String[] entryValues = mSerialPortFinder.getAllDevicesPath();
List<String> allDevices = new ArrayList<String>();
for (int i = 0; i < entryValues.length; i++) {
allDevices.add(entryValues[i]);
}
allDevices.add("/dev/ttyCOM0");
ArrayAdapter<String> aspnDevices = new ArrayAdapter<String>(this,
android.R.layout.simple_spinner_item, allDevices);
aspnDevices.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
SpinnerCOMA.setAdapter(aspnDevices);
SpinnerCOMB.setAdapter(aspnDevices);
SpinnerCOMC.setAdapter(aspnDevices);
SpinnerCOMD.setAdapter(aspnDevices);
if (allDevices.size() > 0) {
SpinnerCOMA.setSelection(0);
}
if (allDevices.size() > 1) {
SpinnerCOMB.setSelection(1);
}
if (allDevices.size() > 2) {
SpinnerCOMC.setSelection(2);
}
if (allDevices.size() > 3) {
SpinnerCOMD.setSelection(3);
}
SpinnerCOMA.setOnItemSelectedListener(new ItemSelectedEvent());
SpinnerCOMB.setOnItemSelectedListener(new ItemSelectedEvent());
SpinnerCOMC.setOnItemSelectedListener(new ItemSelectedEvent());
SpinnerCOMD.setOnItemSelectedListener(new ItemSelectedEvent());
SpinnerBaudRateCOMA.setOnItemSelectedListener(new ItemSelectedEvent());
SpinnerBaudRateCOMB.setOnItemSelectedListener(new ItemSelectedEvent());
SpinnerBaudRateCOMC.setOnItemSelectedListener(new ItemSelectedEvent());
SpinnerBaudRateCOMD.setOnItemSelectedListener(new ItemSelectedEvent());
DispAssistData(AssistData);
}

//----------------------------------------------------串口号或波特率变化时,关闭打开的串口
class ItemSelectedEvent implements Spinner.OnItemSelectedListener {
public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
if ((arg0 == SpinnerCOMA) || (arg0 == SpinnerBaudRateCOMA)) {
CloseComPort(ComA);
checkBoxAutoCOMA.setChecked(false);
toggleButtonCOMA.setChecked(false);
} else if ((arg0 == SpinnerCOMB) || (arg0 == SpinnerBaudRateCOMB)) {
CloseComPort(ComB);
checkBoxAutoCOMA.setChecked(false);
toggleButtonCOMB.setChecked(false);
} else if ((arg0 == SpinnerCOMC) || (arg0 == SpinnerBaudRateCOMC)) {
CloseComPort(ComC);
checkBoxAutoCOMA.setChecked(false);
toggleButtonCOMC.setChecked(false);
} else if ((arg0 == SpinnerCOMD) || (arg0 == SpinnerBaudRateCOMD)) {
CloseComPort(ComD);
checkBoxAutoCOMA.setChecked(false);
toggleButtonCOMD.setChecked(false);
}
}

public void onNothingSelected(AdapterView<?> arg0) {
}

}

//----------------------------------------------------编辑框焦点转移事件
class FocusChangeEvent implements EditText.OnFocusChangeListener {
public void onFocusChange(View v, boolean hasFocus) {
if (v == editTextCOMA) {
setSendData(editTextCOMA);
} else if (v == editTextCOMB) {
setSendData(editTextCOMB);
} else if (v == editTextCOMC) {
setSendData(editTextCOMC);
} else if (v == editTextCOMD) {
setSendData(editTextCOMD);
} else if (v == editTextTimeCOMA) {
setDelayTime(editTextTimeCOMA);
} else if (v == editTextTimeCOMB) {
setDelayTime(editTextTimeCOMB);
} else if (v == editTextTimeCOMC) {
setDelayTime(editTextTimeCOMC);
} else if (v == editTextTimeCOMD) {
setDelayTime(editTextTimeCOMD);
}
}
}

//----------------------------------------------------编辑框完成事件
class EditorActionEvent implements EditText.OnEditorActionListener {
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (v == editTextCOMA) {
setSendData(editTextCOMA);
} else if (v == editTextCOMB) {
setSendData(editTextCOMB);
} else if (v == editTextCOMC) {
setSendData(editTextCOMC);
} else if (v == editTextCOMD) {
setSendData(editTextCOMD);
} else if (v == editTextTimeCOMA) {
setDelayTime(editTextTimeCOMA);
} else if (v == editTextTimeCOMB) {
setDelayTime(editTextTimeCOMB);
} else if (v == editTextTimeCOMC) {
setDelayTime(editTextTimeCOMC);
} else if (v == editTextTimeCOMD) {
setDelayTime(editTextTimeCOMD);
}
return false;
}
}

//----------------------------------------------------Txt、Hex模式选择
class radioButtonClickEvent implements RadioButton.OnClickListener {
public void onClick(View v) {
if (v == radioButtonTxt) {
KeyListener TxtkeyListener = new TextKeyListener(Capitalize.NONE, false);
editTextCOMA.setKeyListener(TxtkeyListener);
editTextCOMB.setKeyListener(TxtkeyListener);
editTextCOMC.setKeyListener(TxtkeyListener);
editTextCOMD.setKeyListener(TxtkeyListener);
AssistData.setTxtMode(true);
} else if (v == radioButtonHex) {
KeyListener HexkeyListener = new NumberKeyListener() {
public int getInputType() {
return InputType.TYPE_CLASS_TEXT;
}

@Override
protected char[] getAcceptedChars() {
return new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F'};
}
};
editTextCOMA.setKeyListener(HexkeyListener);
editTextCOMB.setKeyListener(HexkeyListener);
editTextCOMC.setKeyListener(HexkeyListener);
editTextCOMD.setKeyListener(HexkeyListener);
AssistData.setTxtMode(false);
}
editTextCOMA.setText(AssistData.getSendA());
editTextCOMB.setText(AssistData.getSendB());
editTextCOMC.setText(AssistData.getSendC());
editTextCOMD.setText(AssistData.getSendD());
setSendData(editTextCOMA);
setSendData(editTextCOMB);
setSendData(editTextCOMC);
setSendData(editTextCOMD);
}
}

//----------------------------------------------------自动发送
class CheckBoxChangeEvent implements CheckBox.OnCheckedChangeListener {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (buttonView == checkBoxAutoCOMA) {
if (!toggleButtonCOMA.isChecked() && isChecked) {
buttonView.setChecked(false);
return;
}
SetLoopData(ComA, editTextCOMA.getText().toString());
SetAutoSend(ComA, isChecked);
} else if (buttonView == checkBoxAutoCOMB) {
if (!toggleButtonCOMB.isChecked() && isChecked) {
buttonView.setChecked(false);
return;
}
SetLoopData(ComB, editTextCOMB.getText().toString());
SetAutoSend(ComB, isChecked);
} else if (buttonView == checkBoxAutoCOMC) {
if (!toggleButtonCOMC.isChecked() && isChecked) {
buttonView.setChecked(false);
return;
}
SetLoopData(ComC, editTextCOMC.getText().toString());
SetAutoSend(ComC, isChecked);
} else if (buttonView == checkBoxAutoCOMD) {
if (!toggleButtonCOMD.isChecked() && isChecked) {
buttonView.setChecked(false);
return;
}
SetLoopData(ComD, editTextCOMD.getText().toString());
SetAutoSend(ComD, isChecked);
}
}
}

//----------------------------------------------------清除按钮、发送按钮
class ButtonClickEvent implements View.OnClickListener {
public void onClick(View v) {
if (v == ButtonClear) {
editTextRecDisp.setText("");
} else if (v == ButtonSendCOMA) {
sendPortData(ComA, editTextCOMA.getText().toString());
} else if (v == ButtonSendCOMB) {
sendPortData(ComB, editTextCOMB.getText().toString());
} else if (v == ButtonSendCOMC) {
sendPortData(ComC, editTextCOMC.getText().toString());
} else if (v == ButtonSendCOMD) {
sendPortData(ComD, editTextCOMD.getText().toString());
}
}
}

//----------------------------------------------------打开关闭串口
class ToggleButtonCheckedChangeEvent implements ToggleButton.OnCheckedChangeListener {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (buttonView == toggleButtonCOMA) {
if (isChecked) {
if (toggleButtonCOMB.isChecked() && SpinnerCOMA.getSelectedItemPosition() == SpinnerCOMB.getSelectedItemPosition()) {
ShowMessage("串口" + SpinnerCOMA.getSelectedItem().toString() + "已打开");
buttonView.setChecked(false);
} else if (toggleButtonCOMC.isChecked() && SpinnerCOMA.getSelectedItemPosition() == SpinnerCOMC.getSelectedItemPosition()) {
ShowMessage("串口" + SpinnerCOMA.getSelectedItem().toString() + "已打开");
buttonView.setChecked(false);
} else if (toggleButtonCOMD.isChecked() && SpinnerCOMA.getSelectedItemPosition() == SpinnerCOMD.getSelectedItemPosition()) {
ShowMessage("串口" + SpinnerCOMA.getSelectedItem().toString() + "已打开");
buttonView.setChecked(false);
} else {
// ComA=new SerialControl("/dev/s3c2410_serial0", "9600");
ComA.setPort(SpinnerCOMA.getSelectedItem().toString());
LogUtils.d("COMA = " + SpinnerCOMA.getSelectedItem().toString());
ComA.setBaudRate(SpinnerBaudRateCOMA.getSelectedItem().toString());
LogUtils.d("COMA = " + SpinnerBaudRateCOMA.getSelectedItem().toString());
OpenComPort(ComA);
}
} else {
CloseComPort(ComA);
checkBoxAutoCOMA.setChecked(false);
}
} else if (buttonView == toggleButtonCOMB) {
if (isChecked) {
if (toggleButtonCOMA.isChecked() && SpinnerCOMB.getSelectedItemPosition() == SpinnerCOMA.getSelectedItemPosition()) {
ShowMessage("串口" + SpinnerCOMB.getSelectedItem().toString() + "已打开");
buttonView.setChecked(false);
} else if (toggleButtonCOMC.isChecked() && SpinnerCOMB.getSelectedItemPosition() == SpinnerCOMC.getSelectedItemPosition()) {
ShowMessage("串口" + SpinnerCOMB.getSelectedItem().toString() + "已打开");
buttonView.setChecked(false);
} else if (toggleButtonCOMD.isChecked() && SpinnerCOMB.getSelectedItemPosition() == SpinnerCOMD.getSelectedItemPosition()) {
ShowMessage("串口" + SpinnerCOMB.getSelectedItem().toString() + "已打开");
buttonView.setChecked(false);
} else {
// ComB=new SerialControl("/dev/s3c2410_serial1", "9600");
ComB.setPort(SpinnerCOMB.getSelectedItem().toString());
ComB.setBaudRate(SpinnerBaudRateCOMB.getSelectedItem().toString());
OpenComPort(ComB);
}
} else {
CloseComPort(ComB);
checkBoxAutoCOMB.setChecked(false);
}
} else if (buttonView == toggleButtonCOMC) {
if (isChecked) {
if (toggleButtonCOMA.isChecked() && SpinnerCOMC.getSelectedItemPosition() == SpinnerCOMA.getSelectedItemPosition()) {
ShowMessage("串口" + SpinnerCOMC.getSelectedItem().toString() + "已打开");
buttonView.setChecked(false);
} else if (toggleButtonCOMB.isChecked() && SpinnerCOMC.getSelectedItemPosition() == SpinnerCOMB.getSelectedItemPosition()) {
ShowMessage("串口" + SpinnerCOMC.getSelectedItem().toString() + "已打开");
buttonView.setChecked(false);
} else if (toggleButtonCOMD.isChecked() && SpinnerCOMC.getSelectedItemPosition() == SpinnerCOMD.getSelectedItemPosition()) {
ShowMessage("串口" + SpinnerCOMC.getSelectedItem().toString() + "已打开");
buttonView.setChecked(false);
} else {
// ComC=new SerialControl("/dev/s3c2410_serial2", "9600");
ComC.setPort(SpinnerCOMC.getSelectedItem().toString());
ComC.setBaudRate(SpinnerBaudRateCOMC.getSelectedItem().toString());
OpenComPort(ComC);
}
} else {
CloseComPort(ComC);
checkBoxAutoCOMC.setChecked(false);
}
} else if (buttonView == toggleButtonCOMD) {
if (isChecked) {
if (toggleButtonCOMA.isChecked() && SpinnerCOMD.getSelectedItemPosition() == SpinnerCOMA.getSelectedItemPosition()) {
ShowMessage("串口" + SpinnerCOMD.getSelectedItem().toString() + "已打开");
buttonView.setChecked(false);
} else if (toggleButtonCOMB.isChecked() && SpinnerCOMD.getSelectedItemPosition() == SpinnerCOMB.getSelectedItemPosition()) {
ShowMessage("串口" + SpinnerCOMD.getSelectedItem().toString() + "已打开");
buttonView.setChecked(false);
} else if (toggleButtonCOMC.isChecked() && SpinnerCOMD.getSelectedItemPosition() == SpinnerCOMC.getSelectedItemPosition()) {
ShowMessage("串口" + SpinnerCOMD.getSelectedItem().toString() + "已打开");
buttonView.setChecked(false);
} else {
// ComD=new SerialControl("/dev/s3c2410_serial3", "9600");
ComD.setPort(SpinnerCOMD.getSelectedItem().toString());
ComD.setBaudRate(SpinnerBaudRateCOMD.getSelectedItem().toString());
OpenComPort(ComD);
}
} else {
CloseComPort(ComD);
checkBoxAutoCOMD.setChecked(false);
}
}
}
}

//----------------------------------------------------串口控制类
private class SerialControl extends SerialHelper {

// public SerialControl(String sPort, String sBaudRate){
// super(sPort, sBaudRate);
// }
public SerialControl() {
}

@Override
protected void onDataReceived(final ComBean ComRecData) {
//数据接收量大或接收时弹出软键盘,界面会卡顿,可能和6410的显示性能有关
//直接刷新显示,接收数据量大时,卡顿明显,但接收与显示同步。
//用线程定时刷新显示可以获得较流畅的显示效果,但是接收数据速度快于显示速度时,显示会滞后。
//最终效果差不多-_-,线程定时刷新稍好一些。
DispQueue.AddQueue(ComRecData);//线程定时刷新显示(推荐)
/*
runOnUiThread(new Runnable()//直接刷新显示
{
public void run()
{
DispRecData(ComRecData);
}
});*/
}
}

//----------------------------------------------------刷新显示线程
private class DispQueueThread extends Thread {
private Queue<ComBean> QueueList = new LinkedList<ComBean>();

@Override
public void run() {
super.run();
while (!isInterrupted()) {
final ComBean ComData;
while ((ComData = QueueList.poll()) != null) {
runOnUiThread(new Runnable() {
public void run() {
DispRecData(ComData);
}
});
try {
Thread.sleep(100);//显示性能高的话,可以把此数值调小。
} catch (Exception e) {
e.printStackTrace();
}
break;
}
}
}

public synchronized void AddQueue(ComBean ComData) {
QueueList.add(ComData);
}
}

//----------------------------------------------------刷新界面数据
private void DispAssistData(AssistBean AssistData) {
editTextCOMA.setText(AssistData.getSendA());
editTextCOMB.setText(AssistData.getSendB());
editTextCOMC.setText(AssistData.getSendC());
editTextCOMD.setText(AssistData.getSendD());
setSendData(editTextCOMA);
setSendData(editTextCOMB);
setSendData(editTextCOMC);
setSendData(editTextCOMD);
if (AssistData.isTxt()) {
radioButtonTxt.setChecked(true);
} else {
radioButtonHex.setChecked(true);
}
editTextTimeCOMA.setText(AssistData.sTimeA);
editTextTimeCOMB.setText(AssistData.sTimeB);
editTextTimeCOMC.setText(AssistData.sTimeC);
editTextTimeCOMD.setText(AssistData.sTimeD);
setDelayTime(editTextTimeCOMA);
setDelayTime(editTextTimeCOMB);
setDelayTime(editTextTimeCOMC);
setDelayTime(editTextTimeCOMD);
}

//----------------------------------------------------保存、获取界面数据
private void saveAssistData(AssistBean AssistData) {
AssistData.sTimeA = editTextTimeCOMA.getText().toString();
AssistData.sTimeB = editTextTimeCOMB.getText().toString();
AssistData.sTimeC = editTextTimeCOMC.getText().toString();
AssistData.sTimeD = editTextTimeCOMD.getText().toString();
SharedPreferences msharedPreferences = getSharedPreferences("ComAssistant", Context.MODE_PRIVATE);
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(AssistData);
String sBase64 = new String(Base64.encode(baos.toByteArray(), 0));
SharedPreferences.Editor editor = msharedPreferences.edit();
editor.putString("AssistData", sBase64);
editor.commit();
} catch (IOException e) {
e.printStackTrace();
}
}

//----------------------------------------------------
private AssistBean getAssistData() {
SharedPreferences msharedPreferences = getSharedPreferences("ComAssistant", Context.MODE_PRIVATE);
AssistBean AssistData = new AssistBean();
try {
String personBase64 = msharedPreferences.getString("AssistData", "");
byte[] base64Bytes = Base64.decode(personBase64.getBytes(), 0);
ByteArrayInputStream bais = new ByteArrayInputStream(base64Bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
AssistData = (AssistBean) ois.readObject();
return AssistData;
} catch (Exception e) {
e.printStackTrace();
}
return AssistData;
}

//----------------------------------------------------设置自动发送延时
private void setDelayTime(TextView v) {
if (v == editTextTimeCOMA) {
AssistData.sTimeA = v.getText().toString();
SetiDelayTime(ComA, v.getText().toString());
} else if (v == editTextTimeCOMB) {
AssistData.sTimeB = v.getText().toString();
SetiDelayTime(ComB, v.getText().toString());
} else if (v == editTextTimeCOMC) {
AssistData.sTimeC = v.getText().toString();
SetiDelayTime(ComC, v.getText().toString());
} else if (v == editTextTimeCOMD) {
AssistData.sTimeD = v.getText().toString();
SetiDelayTime(ComD, v.getText().toString());
}
}

//----------------------------------------------------设置自动发送数据
private void setSendData(TextView v) {
if (v == editTextCOMA) {
AssistData.setSendA(v.getText().toString());
SetLoopData(ComA, v.getText().toString());
} else if (v == editTextCOMB) {
AssistData.setSendB(v.getText().toString());
SetLoopData(ComB, v.getText().toString());
} else if (v == editTextCOMC) {
AssistData.setSendC(v.getText().toString());
SetLoopData(ComC, v.getText().toString());
} else if (v == editTextCOMD) {
AssistData.setSendD(v.getText().toString());
SetLoopData(ComD, v.getText().toString());
}
}

//----------------------------------------------------设置自动发送延时
private void SetiDelayTime(SerialHelper ComPort, String sTime) {
ComPort.setiDelay(Integer.parseInt(sTime));
}

//----------------------------------------------------设置自动发送数据
private void SetLoopData(SerialHelper ComPort, String sLoopData) {
if (radioButtonTxt.isChecked()) {
ComPort.setTxtLoopData(sLoopData);
} else if (radioButtonHex.isChecked()) {
ComPort.setHexLoopData(sLoopData);
}
}

//----------------------------------------------------显示接收数据
private void DispRecData(ComBean ComRecData) {
StringBuilder sMsg = new StringBuilder();
sMsg.append(ComRecData.sRecTime);
sMsg.append("[");
sMsg.append(ComRecData.sComPort);
sMsg.append("]");
if (radioButtonTxt.isChecked()) {
sMsg.append("[Txt] ");
sMsg.append(new String(ComRecData.bRec));
} else if (radioButtonHex.isChecked()) {
sMsg.append("[Hex] ");
sMsg.append(MyFunc.ByteArrToHex(ComRecData.bRec));
}
sMsg.append("\r\n");
editTextRecDisp.append(sMsg);
LogUtils.d("sMsg = " + sMsg);
iRecLines++;
editTextLines.setText(String.valueOf(iRecLines));
if ((iRecLines > 500) && (checkBoxAutoClear.isChecked()))//达到500项自动清除
{
editTextRecDisp.setText("");
editTextLines.setText("0");
iRecLines = 0;
}
}

//----------------------------------------------------设置自动发送模式开关
private void SetAutoSend(SerialHelper ComPort, boolean isAutoSend) {
if (isAutoSend) {
ComPort.startSend();
} else {
ComPort.stopSend();
}
}

//----------------------------------------------------串口发送
private void sendPortData(SerialHelper ComPort, String sOut) {
if (ComPort != null && ComPort.isOpen()) {
if (radioButtonTxt.isChecked()) {
ComPort.sendTxt(sOut);
} else if (radioButtonHex.isChecked()) {
ComPort.sendHex(sOut);
}
}
}

//----------------------------------------------------关闭串口
private void CloseComPort(SerialHelper ComPort) {
if (ComPort != null) {
ComPort.stopSend();
ComPort.close();
}
}

//----------------------------------------------------打开串口
private void OpenComPort(SerialHelper ComPort) {
try {
ComPort.open();
} catch (SecurityException e) {
ShowMessage("打开串口失败:没有串口读/写权限!");
} catch (IOException e) {
ShowMessage("打开串口失败:未知错误!");
} catch (InvalidParameterException e) {
ShowMessage("打开串口失败:参数错误!");
}
}

//------------------------------------------显示消息
private void ShowMessage(String sMsg) {
Toast.makeText(this, sMsg, Toast.LENGTH_SHORT).show();
}
}

+ 23
- 0
app/src/main/java/com/bonait/bnframework/serial/ComBean.java View File

@@ -0,0 +1,23 @@
package com.bonait.bnframework.serial;

import java.text.SimpleDateFormat;

/**
* @author benjaminwan
*串口数据
*/
public class ComBean {
public byte[] bRec=null;
public String sRecTime="";
public String sComPort="";
public ComBean(String sPort, byte[] buffer, int size){
sComPort=sPort;
bRec=new byte[size];
for (int i = 0; i < size; i++)
{
bRec[i]=buffer[i];
}
SimpleDateFormat sDateFormat = new SimpleDateFormat("hh:mm:ss");
sRecTime = sDateFormat.format(new java.util.Date());
}
}

+ 83
- 0
app/src/main/java/com/bonait/bnframework/serial/MyFunc.java View File

@@ -0,0 +1,83 @@
package com.bonait.bnframework.serial;


import com.apkfuns.logutils.LogUtils;

/**
* @author benjaminwan
*数据转换工具
*/
public class MyFunc {
//-------------------------------------------------------
// 判断奇数或偶数,位运算,最后一位是1则为奇数,为0是偶数
static public int isOdd(int num)
{
return num & 0x1;
}
//-------------------------------------------------------
static public int HexToInt(String inHex)//Hex字符串转int
{
return Integer.parseInt(inHex, 16);
}
//-------------------------------------------------------
static public byte HexToByte(String inHex)//Hex字符串转byte
{
return (byte)Integer.parseInt(inHex,16);
}
//-------------------------------------------------------
static public String Byte2Hex(Byte inByte)//1字节转2个Hex字符
{
return String.format("%02x", inByte).toUpperCase();
}
//-------------------------------------------------------
static public String ByteArrToHex(byte[] inBytArr)//字节数组转转hex字符串
{
StringBuilder strBuilder=new StringBuilder();
int j=inBytArr.length;
for (int i = 0; i < j; i++)
{
strBuilder.append(Byte2Hex(inBytArr[i]));
strBuilder.append("");
}
String result = strBuilder.toString();
for (int i = 0; i < 10; i++) {
result = result.replace("3"+i,i+"");
}
LogUtils.d("srt = "+strBuilder.toString());
LogUtils.d("result = "+result);
return result.replace("0D0A","");
}
//-------------------------------------------------------
static public String ByteArrToHex(byte[] inBytArr,int offset,int byteCount)//字节数组转转hex字符串,可选长度
{
StringBuilder strBuilder=new StringBuilder();
int j=byteCount;
for (int i = offset; i < j; i++)
{
strBuilder.append(Byte2Hex(inBytArr[i]));
}
return strBuilder.toString();
}
//-------------------------------------------------------
//转hex字符串转字节数组
static public byte[] HexToByteArr(String inHex)//hex字符串转字节数组
{
int hexlen = inHex.length();
byte[] result;
if (isOdd(hexlen)==1)
{//奇数
hexlen++;
result = new byte[(hexlen/2)];
inHex="0"+inHex;
}else {//偶数
result = new byte[(hexlen/2)];
}
int j=0;
for (int i = 0; i < hexlen; i+=2)
{
result[j]=HexToByte(inHex.substring(i,i+2));
j++;
}
return result;
}
}

+ 243
- 0
app/src/main/java/com/bonait/bnframework/serial/SerialHelper.java View File

@@ -0,0 +1,243 @@
package com.bonait.bnframework.serial;

import com.apkfuns.logutils.LogUtils;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidParameterException;

/**
* @author benjaminwan
*串口辅助工具类
*/
public abstract class SerialHelper {
private SerialPort mSerialPort;
private OutputStream mOutputStream;
private InputStream mInputStream;
private ReadThread mReadThread;
private SendThread mSendThread;
private String sPort="/dev/s3c2410_serial0";
private int iBaudRate=9600;
private boolean _isOpen=false;
private byte[] _bLoopData=new byte[]{0x30};
private int iDelay=500;
//----------------------------------------------------
public SerialHelper(String sPort, int iBaudRate){
this.sPort = sPort;
this.iBaudRate=iBaudRate;
}
public SerialHelper(){
this("/dev/ttyCOM4",9600);
}
public SerialHelper(String sPort){
this(sPort,9600);
}
public SerialHelper(String sPort, String sBaudRate){
this(sPort,Integer.parseInt(sBaudRate));
}
//----------------------------------------------------
public void open() throws SecurityException, IOException,InvalidParameterException{
mSerialPort = new SerialPort(new File(sPort), iBaudRate, 0);
mOutputStream = mSerialPort.getOutputStream();
mInputStream = mSerialPort.getInputStream();
mReadThread = new ReadThread();
mReadThread.start();
mSendThread = new SendThread();
mSendThread.setSuspendFlag();
mSendThread.start();
_isOpen=true;
}
//----------------------------------------------------
public void close(){
if (mReadThread != null)
mReadThread.interrupt();
if (mSerialPort != null) {
mSerialPort.close();
mSerialPort = null;
}
_isOpen=false;
}
//----------------------------------------------------
public void send(byte[] bOutArray){
try
{
mOutputStream.write(bOutArray);
} catch (IOException e)
{
e.printStackTrace();
}
}
//----------------------------------------------------
public void sendHex(String sHex){
byte[] bOutArray = MyFunc.HexToByteArr(sHex);
send(bOutArray);
}
//----------------------------------------------------
public void sendTxt(String sTxt){
byte[] bOutArray =sTxt.getBytes();
send(bOutArray);
LogUtils.d("sTXT= "+sTxt);
}
//----------------------------------------------------
private class ReadThread extends Thread {
@Override
public void run() {
super.run();
while(!isInterrupted()) {
try
{
if (mInputStream == null) return;
byte[] buffer=new byte[512];
int size = mInputStream.read(buffer);
if (size > 0){
ComBean ComRecData = new ComBean(sPort,buffer,size);
onDataReceived(ComRecData);
}
try
{
Thread.sleep(50);//延时50ms
} catch (InterruptedException e)
{
e.printStackTrace();
}
} catch (Throwable e)
{
e.printStackTrace();
return;
}
}
}
}
//----------------------------------------------------
private class SendThread extends Thread{
public boolean suspendFlag = true;// 控制线程的执行
@Override
public void run() {
super.run();
while(!isInterrupted()) {
synchronized (this)
{
while (suspendFlag)
{
try
{
wait();
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
send(getbLoopData());
try
{
Thread.sleep(iDelay);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}

//线程暂停
public void setSuspendFlag() {
this.suspendFlag = true;
}

//唤醒线程
public synchronized void setResume() {
this.suspendFlag = false;
notify();
}
}
//----------------------------------------------------
public int getBaudRate()
{
return iBaudRate;
}
public boolean setBaudRate(int iBaud)
{
if (_isOpen)
{
return false;
} else
{
iBaudRate = iBaud;
return true;
}
}
public boolean setBaudRate(String sBaud)
{
int iBaud = Integer.parseInt(sBaud);
return setBaudRate(iBaud);
}
//----------------------------------------------------
public String getPort()
{
return sPort;
}
public boolean setPort(String sPort)
{
if (_isOpen)
{
return false;
} else
{
this.sPort = sPort;
return true;
}
}
//----------------------------------------------------
public boolean isOpen()
{
return _isOpen;
}
//----------------------------------------------------
public byte[] getbLoopData()
{
return _bLoopData;
}
//----------------------------------------------------
public void setbLoopData(byte[] bLoopData)
{
this._bLoopData = bLoopData;
}
//----------------------------------------------------
public void setTxtLoopData(String sTxt){
this._bLoopData = sTxt.getBytes();
}
//----------------------------------------------------
public void setHexLoopData(String sHex){
this._bLoopData = MyFunc.HexToByteArr(sHex);
}
//----------------------------------------------------
public int getiDelay()
{
return iDelay;
}
//----------------------------------------------------
public void setiDelay(int iDelay)
{
this.iDelay = iDelay;
}
//----------------------------------------------------
public void startSend()
{
if (mSendThread != null)
{
mSendThread.setResume();
}
}
//----------------------------------------------------
public void stopSend()
{
if (mSendThread != null)
{
mSendThread.setSuspendFlag();
}
}
//----------------------------------------------------
protected abstract void onDataReceived(ComBean ComRecData);
}

+ 86
- 0
app/src/main/java/com/bonait/bnframework/serial/SerialPort.java View File

@@ -0,0 +1,86 @@
/*
* Copyright 2009 Cedric Priscal
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.bonait.bnframework.serial;


import com.apkfuns.logutils.LogUtils;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class SerialPort {

private static final String TAG = "SerialPort";

/*
* Do not remove or rename the field mFd: it is used by native method close();
*/
private FileDescriptor mFd;
private FileInputStream mFileInputStream;
private FileOutputStream mFileOutputStream;

public SerialPort(File device, int baudrate, int flags) throws SecurityException, IOException {

/* Check access permission */
if (!device.canRead() || !device.canWrite()) {
try {
/* Missing read/write permission, trying to chmod the file */
Process su;
su = Runtime.getRuntime().exec("/system/xbin/su");
String cmd = "chmod 777 " + device.getAbsolutePath() + "\n"
+ "exit\n";
su.getOutputStream().write(cmd.getBytes());
if ((su.waitFor() != 0) || !device.canRead()
|| !device.canWrite()) {
throw new SecurityException();
}
} catch (Exception e) {
e.printStackTrace();
throw new SecurityException();
}
}

mFd = open(device.getAbsolutePath(), baudrate, flags);
if (mFd == null) {
LogUtils.d(TAG+ "native open returns null");
throw new IOException();
}
mFileInputStream = new FileInputStream(mFd);
mFileOutputStream = new FileOutputStream(mFd);
}

// Getters and setters
public InputStream getInputStream() {
return mFileInputStream;
}

public OutputStream getOutputStream() {
return mFileOutputStream;
}

// JNI
private native static FileDescriptor open(String path, int baudrate, int flags);
public native void close();
static {
System.loadLibrary("serial_port");
}
}

+ 124
- 0
app/src/main/java/com/bonait/bnframework/serial/SerialPortFinder.java View File

@@ -0,0 +1,124 @@
/*
* Copyright 2009 Cedric Priscal
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.bonait.bnframework.serial;

import android.util.Log;

import com.apkfuns.logutils.LogUtils;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.util.Iterator;
import java.util.Vector;

public class SerialPortFinder {

public class Driver {
public Driver(String name, String root) {
mDriverName = name;
mDeviceRoot = root;
}
private String mDriverName;
private String mDeviceRoot;
Vector<File> mDevices = null;
public Vector<File> getDevices() {
if (mDevices == null) {
mDevices = new Vector<File>();
File dev = new File("/dev");
File[] files = dev.listFiles();
int i;
for (i=0; i<files.length; i++) {
if (files[i].getAbsolutePath().startsWith(mDeviceRoot)) {
Log.d(TAG, "Found new device: " + files[i]);
mDevices.add(files[i]);
}
}
}
return mDevices;
}
public String getName() {
return mDriverName;
}
}

private static final String TAG = "SerialPort";

private Vector<Driver> mDrivers = null;

Vector<Driver> getDrivers() throws IOException {
if (mDrivers == null) {
mDrivers = new Vector<Driver>();
LineNumberReader r = new LineNumberReader(new FileReader("/proc/tty/drivers"));
String l;
while((l = r.readLine()) != null) {
// Issue 3:
// Since driver name may contain spaces, we do not extract driver name with split()
String drivername = l.substring(0, 0x15).trim();
String[] w = l.split(" +");
if ((w.length >= 5) && (w[w.length-1].equals("serial"))) {
LogUtils.d(TAG+ "Found new driver " + drivername + " on " + w[w.length-4]);
mDrivers.add(new Driver(drivername, w[w.length-4]));
}
}
r.close();
}
return mDrivers;
}

public String[] getAllDevices() {
Vector<String> devices = new Vector<String>();
// Parse each driver
Iterator<Driver> itdriv;
try {
itdriv = getDrivers().iterator();
while(itdriv.hasNext()) {
Driver driver = itdriv.next();
Iterator<File> itdev = driver.getDevices().iterator();
while(itdev.hasNext()) {
String device = itdev.next().getName();
String value = String.format("%s (%s)", device, driver.getName());
devices.add(value);
}
}
} catch (IOException e) {
e.printStackTrace();
}
return devices.toArray(new String[devices.size()]);
}

public String[] getAllDevicesPath() {
Vector<String> devices = new Vector<String>();
// Parse each driver
Iterator<Driver> itdriv;
try {
itdriv = getDrivers().iterator();
while(itdriv.hasNext()) {
Driver driver = itdriv.next();
Iterator<File> itdev = driver.getDevices().iterator();
while(itdev.hasNext()) {
String device = itdev.next().getAbsolutePath();
devices.add(device);
}
}
} catch (IOException e) {
e.printStackTrace();
}
return devices.toArray(new String[devices.size()]);
}
}

+ 2
- 0
app/src/main/java/com/bonait/bnframework/ui/viewmodel/HomeGoodsViewModel.java View File

@@ -10,6 +10,8 @@ import androidx.lifecycle.ViewModel;
import com.alibaba.fastjson.TypeReference;
import com.apkfuns.logutils.LogUtils;

import com.bonait.bnframework.api.IHttpCallBack;
import com.bonait.bnframework.api.ServerManager;
import com.bonait.bnframework.business.RecordManager;
import com.bonait.bnframework.common.API.APIHelper;
import com.bonait.bnframework.common.API.APIResultT;


BIN
View File


BIN
View File


BIN
View File


BIN
View File


BIN
View File


+ 7
- 0
app/src/main/res/drawable/divider.xml View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#666" />
<size
android:width="0.1dp"
android:height="0.1dp" />
</shape>

+ 14
- 0
app/src/main/res/drawable/sel_item.xml View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!--<item android:state_focused="true">
<color android:color="#600f" />
</item>-->

<!--<item android:state_selected="true">
<color android:color="#6666" />
</item>-->

<item android:state_pressed="true">
<color android:color="@color/colorAccent" />
</item>
</selector>

+ 6
- 0
app/src/main/res/drawable/stroke.xml View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke
android:width="1dp"
android:color="@color/colorPrimaryDark" />
</shape>

+ 122
- 0
app/src/main/res/layout/activity_bleclient.xml View File

@@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="2dp"
>

<Button
android:id="@+id/btn_scan"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:onClick="reScan"
android:text="@string/reScan"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btn_clean_log"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/rv_ble"/>

<Button
android:id="@+id/btn_clean_log"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:onClick="cleanLog"
android:text="@string/clean_log"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/btn_scan"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/rv_ble" />

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_ble"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@drawable/stroke"
android:padding="4dp"
app:layout_constraintHeight_percent="0.4"
app:layout_constraintTop_toBottomOf="@+id/btn_scan" />

<Button
android:id="@+id/btn_read"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:onClick="read"
android:text="@string/read"
app:layout_constraintEnd_toStartOf="@+id/btn_notify"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/rv_ble" />

<Button
android:id="@+id/btn_notify"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:onClick="setNotify"
android:text="@string/setNotify"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/btn_read"
app:layout_constraintTop_toBottomOf="@+id/rv_ble" />

<EditText
android:id="@+id/et_write"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/max_byte"
android:inputType="none"
android:maxLength="20"
app:layout_constraintEnd_toStartOf="@+id/btn_write"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn_read"/>

<Button
android:id="@+id/btn_write"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:onClick="write"
android:text="@string/write"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/et_write"
app:layout_constraintTop_toBottomOf="@+id/btn_notify" />


<Button
android:id="@+id/btn_write_cmd"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:onClick="writeCmd"
android:text="@string/write_cmd"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btn_write_long_cmd"
app:layout_constraintTop_toBottomOf="@+id/btn_write" />

<Button
android:id="@+id/btn_write_long_cmd"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:onClick="writeLongCmd"
android:textSize="10sp"
android:text="@string/write_long_cmd"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/btn_write_cmd"
app:layout_constraintTop_toBottomOf="@+id/btn_write" />

<ScrollView
android:id="@+id/sv"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/stroke"
android:padding="2dp"
android:scrollbars="none"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn_write_cmd">

<TextView
android:id="@+id/tv_tips"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

+ 451
- 0
app/src/main/res/layout/activity_serial_test.xml View File

@@ -0,0 +1,451 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >


<LinearLayout
android:id="@+id/LinearLayoutRecDisp"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_weight="1" >

<EditText
android:id="@+id/editTextRecDisp"
android:layout_width="fill_parent"
android:layout_height="match_parent"
android:clickable="false"
android:gravity="top"
android:longClickable="false" android:layout_weight="1" android:textSize="14sp" android:editable="false"/>


<LinearLayout
android:id="@+id/LinearLayoutRecTool"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical" >


<Button
android:id="@+id/ButtonClear"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="清除"/>


<RadioGroup
android:id="@+id/radioGroupOption"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >

<RadioButton
android:id="@+id/radioButtonTxt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="Txt" />

<RadioButton
android:id="@+id/radioButtonHex"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hex" />
</RadioGroup>




<EditText
android:id="@+id/editTextLines"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="false"
android:editable="false"
android:enabled="false"
android:imeOptions="actionNone"
android:inputType="number"
android:longClickable="false"
android:text="0" >

<requestFocus />
</EditText>


<CheckBox
android:id="@+id/checkBoxAutoClear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:text="自清" />

</LinearLayout>

</LinearLayout>

<LinearLayout
android:id="@+id/LinearLayoutCOMAB"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_weight="1" >

<LinearLayout
android:id="@+id/LinearLayoutCOMA"
android:layout_width="fill_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical" >



<EditText
android:id="@+id/editTextCOMA"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:gravity="top"
android:imeOptions="actionDone" android:layout_weight="1" android:singleLine="true"/>




<LinearLayout
android:id="@+id/LinearLayoutTooLCOMA"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">




<Spinner
android:id="@+id/SpinnerCOMA"
android:layout_width="match_parent"
android:layout_height="wrap_content" />



<LinearLayout
android:id="@+id/LinearLayoutOption1COMA"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical" >

<Spinner
android:id="@+id/SpinnerBaudRateCOMA"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1" />

<ToggleButton
android:id="@+id/toggleButtonCOMA"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ToggleButton" />

</LinearLayout>




<LinearLayout
android:id="@+id/LinearLayoutOption2COMA"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical" >

<EditText
android:id="@+id/editTextTimeCOMA"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="number" android:text="500" android:layout_weight="1"/>

<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ms" />

<CheckBox
android:id="@+id/checkBoxAutoCOMA"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="自动" />




<Button
android:id="@+id/ButtonSendCOMA"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发送"/>

</LinearLayout>

</LinearLayout>

</LinearLayout>



<LinearLayout
android:id="@+id/LinearLayoutCOMB"
android:layout_width="fill_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical" >


<EditText
android:id="@+id/editTextCOMB"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_weight="1" android:gravity="top" android:imeOptions="actionDone" android:singleLine="true"/>


<LinearLayout
android:id="@+id/LinearLayoutTooLCOMB"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >


<Spinner
android:id="@+id/SpinnerCOMB"
android:layout_width="match_parent"
android:layout_height="wrap_content" />



<LinearLayout
android:id="@+id/LinearLayoutOption1COMB"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical" >

<Spinner
android:id="@+id/SpinnerBaudRateCOMB"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1" />

<ToggleButton
android:id="@+id/ToggleButtonCOMB"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ToggleButton" />

</LinearLayout>



<LinearLayout
android:id="@+id/LinearLayoutOption2COMB"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical" >

<EditText
android:id="@+id/editTextTimeCOMB"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:inputType="number" android:text="500"/>

<TextView
android:id="@+id/TextView01"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ms" />

<CheckBox
android:id="@+id/checkBoxAutoCOMB"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="自动" />



<Button
android:id="@+id/ButtonSendCOMB"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发送"/>

</LinearLayout>
</LinearLayout>

</LinearLayout>

</LinearLayout>



<LinearLayout
android:id="@+id/LinearLayoutCOMCD"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_weight="1" >

<LinearLayout
android:id="@+id/LinearLayoutCOMC"
android:layout_width="fill_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical" >

<EditText
android:id="@+id/editTextCOMC"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:gravity="top"
android:imeOptions="actionDone" android:layout_weight="1" android:singleLine="true"/>

<LinearLayout
android:id="@+id/LinearLayoutTooLCOMC"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >

<Spinner
android:id="@+id/SpinnerCOMC"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<LinearLayout
android:id="@+id/LinearLayoutOption1COMC"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical" >

<Spinner
android:id="@+id/SpinnerBaudRateCOMC"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1" />

<ToggleButton
android:id="@+id/ToggleButtonCOMC"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ToggleButton" />
</LinearLayout>

<LinearLayout
android:id="@+id/LinearLayoutOption2COMC"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical" >

<EditText
android:id="@+id/editTextTimeCOMC"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:inputType="number"
android:text="500" />

<TextView
android:id="@+id/TextView02"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ms" />

<CheckBox
android:id="@+id/checkBoxAutoCOMC"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="自动" />

<Button
android:id="@+id/ButtonSendCOMC"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发送" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

<LinearLayout
android:id="@+id/LinearLayoutCOMD"
android:layout_width="fill_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical" >

<EditText
android:id="@+id/editTextCOMD"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_weight="1"
android:gravity="top"
android:imeOptions="actionDone" android:singleLine="true"/>

<LinearLayout
android:id="@+id/LinearLayoutTooLCOMD"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >

<Spinner
android:id="@+id/SpinnerCOMD"
android:layout_width="match_parent"
android:layout_height="wrap_content" />


<LinearLayout
android:id="@+id/LinearLayoutOption1COMD"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical" >

<Spinner
android:id="@+id/SpinnerBaudRateCOMD"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1" />

<ToggleButton
android:id="@+id/ToggleButtonCOMD"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ToggleButton" />
</LinearLayout>

<LinearLayout
android:id="@+id/LinearLayoutOption2COMD"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical" >

<EditText
android:id="@+id/editTextTimeCOMD"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:inputType="number"
android:text="500" />

<TextView
android:id="@+id/TextView03"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ms" />

<CheckBox
android:id="@+id/checkBoxAutoCOMD"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="自动" />

<Button
android:id="@+id/ButtonSendCOMD"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发送" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>

</LinearLayout>

+ 49
- 0
app/src/main/res/layout/activity_service.xml View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">

<Button
android:id="@+id/btn1"
android:layout_width="200dp"
android:layout_height="100dp"
android:layout_gravity="center_horizontal"
android:background="@drawable/bg_round15_yellow_btn"
android:text="开启蓝牙"
android:layout_marginTop="150dp"
android:textSize="36sp"
android:textColor="@color/white"
>
<requestFocus/>
</Button>
<TextView
android:id="@+id/tv_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:textColor="@color/black"
tools:text="开启"
android:layout_marginLeft="300dp"
android:textStyle="bold"
android:textSize="36sp"
/>
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="@color/black"
android:textSize="36sp"
tools:text="asasasaaaaaaaaaaaaaa"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="@color/black"
android:textSize="36sp"
android:text="a"
/>

</FrameLayout>

+ 22
- 0
app/src/main/res/layout/item/layout/item_dev.xml View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="2dp"
android:background="@drawable/sel_item"
android:divider="@drawable/divider"
android:orientation="vertical"
android:showDividers="end">

<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

<TextView
android:id="@+id/address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@+id/name" />
</LinearLayout>

+ 69
- 0
app/src/main/res/values/baudrates.xml View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>

<string-array name="baudrates_name">
<item>50</item>
<item>75</item>
<item>110</item>
<item>134</item>
<item>150</item>
<item>200</item>
<item>300</item>
<item>600</item>
<item>1200</item>
<item>1800</item>
<item>2400</item>
<item>4800</item>
<item>9600</item>
<item>19200</item>
<item>38400</item>
<item>57600</item>
<item>115200</item>
<item>230400</item>
<item>460800</item>
<item>500000</item>
<item>576000</item>
<item>921600</item>
<item>1000000</item>
<item>1152000</item>
<item>1500000</item>
<item>2000000</item>
<item>2500000</item>
<item>3000000</item>
<item>3500000</item>
<item>4000000</item>
</string-array>
<string-array name="baudrates_value">
<item>50</item>
<item>75</item>
<item>110</item>
<item>134</item>
<item>150</item>
<item>200</item>
<item>300</item>
<item>600</item>
<item>1200</item>
<item>1800</item>
<item>2400</item>
<item>4800</item>
<item>9600</item>
<item>19200</item>
<item>38400</item>
<item>57600</item>
<item>115200</item>
<item>230400</item>
<item>460800</item>
<item>500000</item>
<item>576000</item>
<item>921600</item>
<item>1000000</item>
<item>1152000</item>
<item>1500000</item>
<item>2000000</item>
<item>2500000</item>
<item>3000000</item>
<item>3500000</item>
<item>4000000</item>
</string-array>

</resources>

+ 14
- 0
app/src/main/res/values/strings.xml View File

@@ -198,4 +198,18 @@
<string name="top_cleaninfo">在此输入您的用量</string>
<string name="text_calibrationinfo">标定前需要排空管道</string>
<string name="top_autotest">模块自动测试</string>

<string name="ble_client">BLE客户端</string>
<string name="ble_server">BLE服务端</string>
<string name="ble">低功耗蓝牙(BLE)</string>
<string name="reScan">重新扫描</string>
<string name="clean_log">清空日志</string>
<string name="read">读取</string>
<string name="setNotify">设置通知</string>
<string name="max_byte">最多20字节</string>
<string name="write">写入</string>
<string name="re_conn">重连</string>
<string name="serial_port">串口调试</string>
<string name="write_cmd">写入协议内容</string>
<string name="write_long_cmd">写入长(大于20字节)内容</string>
</resources>

Loading…
Cancel
Save