Browse Source

初始提交

Frankensteinly 1 year ago
commit
b0a077c607
99 changed files with 6048 additions and 0 deletions
  1. 15 0
      .gitignore
  2. 3 0
      .idea/.gitignore
  3. 123 0
      .idea/codeStyles/Project.xml
  4. 5 0
      .idea/codeStyles/codeStyleConfig.xml
  5. 6 0
      .idea/compiler.xml
  6. 18 0
      .idea/deploymentTargetSelector.xml
  7. 19 0
      .idea/gradle.xml
  8. 6 0
      .idea/kotlinc.xml
  9. 10 0
      .idea/migrations.xml
  10. 9 0
      .idea/misc.xml
  11. 1 0
      app/.gitignore
  12. 81 0
      app/build.gradle
  13. BIN
      app/libs/adh_series_sdk.jar
  14. 21 0
      app/proguard-rules.pro
  15. 24 0
      app/src/androidTest/java/com/example/whatever/ExampleInstrumentedTest.kt
  16. 57 0
      app/src/main/AndroidManifest.xml
  17. BIN
      app/src/main/assets/testpng.png
  18. 7 0
      app/src/main/java/com/example/whatever/CustomBleGattCallback.kt
  19. 9 0
      app/src/main/java/com/example/whatever/CustomBleIndicateCallback.kt
  20. 7 0
      app/src/main/java/com/example/whatever/CustomBleScanCallback.kt
  21. 9 0
      app/src/main/java/com/example/whatever/CustomBleWriteCallback.kt
  22. 27 0
      app/src/main/java/com/example/whatever/MyApplication.kt
  23. 3 0
      app/src/main/java/com/example/whatever/RealResponse.kt
  24. 64 0
      app/src/main/java/com/example/whatever/Token.kt
  25. 36 0
      app/src/main/java/com/example/whatever/activity/BaseBindingActivity.kt
  26. 409 0
      app/src/main/java/com/example/whatever/activity/BleActivity.kt
  27. 128 0
      app/src/main/java/com/example/whatever/activity/CmdActivity.kt
  28. 44 0
      app/src/main/java/com/example/whatever/activity/MainActivity.kt
  29. 22 0
      app/src/main/java/com/example/whatever/activity/ModbusActivity.kt
  30. 20 0
      app/src/main/java/com/example/whatever/activity/ScreenAdaptActivity.kt
  31. 57 0
      app/src/main/java/com/example/whatever/activity/WebActivity.kt
  32. 419 0
      app/src/main/java/com/example/whatever/ble/BleCmdManager.kt
  33. 59 0
      app/src/main/java/com/example/whatever/ble/BleConst.kt
  34. 194 0
      app/src/main/java/com/example/whatever/ble/BleUtil.kt
  35. 50 0
      app/src/main/java/com/example/whatever/ble/KeyGenerator.java
  36. 97 0
      app/src/main/java/com/example/whatever/extentions/ByteArray.kt
  37. 18 0
      app/src/main/java/com/example/whatever/extentions/Int.kt
  38. 10 0
      app/src/main/java/com/example/whatever/extentions/Long.kt
  39. 19 0
      app/src/main/java/com/example/whatever/extentions/String.kt
  40. 493 0
      app/src/main/java/com/example/whatever/modbus/ModBusController.kt
  41. 730 0
      app/src/main/java/com/example/whatever/modbus/ModBusManager.kt
  42. 155 0
      app/src/main/java/com/example/whatever/modbus/PortManager.kt
  43. 12 0
      app/src/main/java/com/example/whatever/model/BleBean.kt
  44. 6 0
      app/src/main/java/com/example/whatever/model/Constants.kt
  45. 3 0
      app/src/main/java/com/example/whatever/model/FileBean.kt
  46. 10 0
      app/src/main/java/com/example/whatever/research/UnixTime.java
  47. 77 0
      app/src/main/java/com/example/whatever/research/WorkTicketBean.kt
  48. 122 0
      app/src/main/java/com/example/whatever/util/CRC16.java
  49. 16 0
      app/src/main/java/com/example/whatever/util/DownloadCallBack.kt
  50. 133 0
      app/src/main/java/com/example/whatever/util/Executor.kt
  51. 118 0
      app/src/main/java/com/example/whatever/util/FileUtil.kt
  52. 55 0
      app/src/main/java/com/example/whatever/util/Funcs.kt
  53. 271 0
      app/src/main/java/com/example/whatever/util/NetHttpManager.kt
  54. 22 0
      app/src/main/java/com/example/whatever/util/ToastUtils.kt
  55. 41 0
      app/src/main/java/com/example/whatever/util/log/ILog.kt
  56. 16 0
      app/src/main/java/com/example/whatever/util/log/LogDiskStrategy.kt
  57. 64 0
      app/src/main/java/com/example/whatever/util/log/LogHandle.kt
  58. 178 0
      app/src/main/java/com/example/whatever/util/log/LogUtil.kt
  59. 81 0
      app/src/main/java/com/example/whatever/util/log/LogWriteHandler.kt
  60. 147 0
      app/src/main/java/com/example/whatever/util/log/MyCsvFormatStrategy.kt
  61. 105 0
      app/src/main/java/com/example/whatever/util/log/MyDiskLogStrategy.kt
  62. BIN
      app/src/main/jniLibs/armeabi-v7a/libserial_port.so
  63. 170 0
      app/src/main/res/drawable/ic_launcher_background.xml
  64. 30 0
      app/src/main/res/drawable/ic_launcher_foreground.xml
  65. 36 0
      app/src/main/res/layout/activity_ble.xml
  66. 79 0
      app/src/main/res/layout/activity_cmd.xml
  67. 30 0
      app/src/main/res/layout/activity_main.xml
  68. 25 0
      app/src/main/res/layout/activity_modbus.xml
  69. 23 0
      app/src/main/res/layout/activity_screen_adapt.xml
  70. 15 0
      app/src/main/res/layout/activity_web.xml
  71. 214 0
      app/src/main/res/layout/item_ble.xml
  72. 6 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  73. 6 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  74. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher.webp
  75. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
  76. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher.webp
  77. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
  78. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher.webp
  79. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
  80. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
  81. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
  82. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
  83. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
  84. 7 0
      app/src/main/res/values-night/themes.xml
  85. 6 0
      app/src/main/res/values/colors.xml
  86. 3 0
      app/src/main/res/values/strings.xml
  87. 9 0
      app/src/main/res/values/themes.xml
  88. 13 0
      app/src/main/res/xml/backup_rules.xml
  89. 4 0
      app/src/main/res/xml/common_network_config.xml
  90. 19 0
      app/src/main/res/xml/data_extraction_rules.xml
  91. 17 0
      app/src/test/java/com/example/whatever/ExampleUnitTest.kt
  92. 5 0
      build.gradle
  93. 24 0
      gradle.properties
  94. 36 0
      gradle/libs.versions.toml
  95. BIN
      gradle/wrapper/gradle-wrapper.jar
  96. 6 0
      gradle/wrapper/gradle-wrapper.properties
  97. 185 0
      gradlew
  98. 89 0
      gradlew.bat
  99. 30 0
      settings.gradle

+ 15 - 0
.gitignore

@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties

+ 3 - 0
.idea/.gitignore

@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml

+ 123 - 0
.idea/codeStyles/Project.xml

@@ -0,0 +1,123 @@
+<component name="ProjectCodeStyleConfiguration">
+  <code_scheme name="Project" version="173">
+    <JetCodeStyleSettings>
+      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+    </JetCodeStyleSettings>
+    <codeStyleSettings language="XML">
+      <option name="FORCE_REARRANGE_MODE" value="1" />
+      <indentOptions>
+        <option name="CONTINUATION_INDENT_SIZE" value="4" />
+      </indentOptions>
+      <arrangement>
+        <rules>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>xmlns:android</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>xmlns:.*</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+              <order>BY_NAME</order>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*:id</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*:name</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>name</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>style</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+              <order>BY_NAME</order>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+                </AND>
+              </match>
+              <order>ANDROID_ATTRIBUTE_ORDER</order>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>.*</XML_NAMESPACE>
+                </AND>
+              </match>
+              <order>BY_NAME</order>
+            </rule>
+          </section>
+        </rules>
+      </arrangement>
+    </codeStyleSettings>
+    <codeStyleSettings language="kotlin">
+      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+    </codeStyleSettings>
+  </code_scheme>
+</component>

+ 5 - 0
.idea/codeStyles/codeStyleConfig.xml

@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+  <state>
+    <option name="USE_PER_PROJECT_SETTINGS" value="true" />
+  </state>
+</component>

+ 6 - 0
.idea/compiler.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="CompilerConfiguration">
+    <bytecodeTargetLevel target="17" />
+  </component>
+</project>

+ 18 - 0
.idea/deploymentTargetSelector.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="deploymentTargetSelector">
+    <selectionStates>
+      <SelectionState runConfigName="app">
+        <option name="selectionMode" value="DROPDOWN" />
+        <DropdownSelection timestamp="2024-09-06T03:12:23.017429800Z">
+          <Target type="DEFAULT_BOOT">
+            <handle>
+              <DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\Frankensteinly\.android\avd\Pixel_6_Pro_API_31.avd" />
+            </handle>
+          </Target>
+        </DropdownSelection>
+        <DialogSelection />
+      </SelectionState>
+    </selectionStates>
+  </component>
+</project>

+ 19 - 0
.idea/gradle.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="GradleMigrationSettings" migrationVersion="1" />
+  <component name="GradleSettings">
+    <option name="linkedExternalProjectsSettings">
+      <GradleProjectSettings>
+        <option name="externalProjectPath" value="$PROJECT_DIR$" />
+        <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
+        <option name="modules">
+          <set>
+            <option value="$PROJECT_DIR$" />
+            <option value="$PROJECT_DIR$/app" />
+          </set>
+        </option>
+        <option name="resolveExternalAnnotations" value="false" />
+      </GradleProjectSettings>
+    </option>
+  </component>
+</project>

+ 6 - 0
.idea/kotlinc.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="KotlinJpsPluginSettings">
+    <option name="version" value="1.9.0" />
+  </component>
+</project>

+ 10 - 0
.idea/migrations.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectMigrations">
+    <option name="MigrateToGradleLocalJavaHome">
+      <set>
+        <option value="$PROJECT_DIR$" />
+      </set>
+    </option>
+  </component>
+</project>

+ 9 - 0
.idea/misc.xml

@@ -0,0 +1,9 @@
+<project version="4">
+  <component name="ExternalStorageConfigurationManager" enabled="true" />
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
+    <output url="file://$PROJECT_DIR$/build/classes" />
+  </component>
+  <component name="ProjectType">
+    <option name="id" value="Android" />
+  </component>
+</project>

+ 1 - 0
app/.gitignore

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

+ 81 - 0
app/build.gradle

@@ -0,0 +1,81 @@
+plugins {
+    alias(libs.plugins.android.application)
+    alias(libs.plugins.jetbrains.kotlin.android)
+}
+
+android {
+    namespace 'com.example.whatever'
+    compileSdk 34
+    viewBinding.enabled = true
+
+    dataBinding {
+        enabled true
+    }
+
+    defaultConfig {
+        applicationId "com.example.whatever"
+        minSdk 24
+        targetSdk 34
+        versionCode 1
+        versionName "1.0"
+
+        multiDexEnabled true
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+    kotlinOptions {
+        jvmTarget = '1.8'
+    }
+}
+
+dependencies {
+
+    implementation libs.androidx.core.ktx
+    implementation libs.androidx.appcompat
+    implementation libs.material
+    implementation libs.androidx.activity
+    implementation libs.androidx.constraintlayout
+    testImplementation libs.junit
+    androidTestImplementation libs.androidx.junit
+    androidTestImplementation libs.androidx.espresso.core
+
+//    implementation libs.mvvmhabit
+    implementation libs.okhttps
+    implementation libs.log.interceptor
+    implementation libs.autosize
+    implementation libs.fastble
+
+    implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
+
+    // RV通用Adapter  https://github.com/hongyangAndroid/base-adapter
+    implementation 'com.zhy:base-rvadapter:3.0.3'
+    // 日志工具 https://github.com/orhanobut/logger
+    implementation 'com.orhanobut:logger:2.2.0'
+
+
+    implementation 'androidx.multidex:multidex:2.0.1'
+
+    //https://github.com/googlesamples/easypermissions
+    implementation 'pub.devrel:easypermissions:3.0.0'
+
+
+    // https://github.com/scwang90/SmartRefreshLayout
+    implementation 'androidx.appcompat:appcompat:1.7.0'                 //必须 1.0.0 以上
+    implementation 'io.github.scwang90:refresh-layout-kernel:2.1.0'      //核心必须依赖
+    implementation 'io.github.scwang90:refresh-header-classics:2.1.0'    //经典刷新头
+
+    implementation 'androidx.recyclerview:recyclerview:1.3.0'
+
+    implementation 'com.leon:lfilepickerlibrary:1.8.0'
+}

BIN
app/libs/adh_series_sdk.jar


+ 21 - 0
app/proguard-rules.pro

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

+ 24 - 0
app/src/androidTest/java/com/example/whatever/ExampleInstrumentedTest.kt

@@ -0,0 +1,24 @@
+package com.example.whatever
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+    @Test
+    fun useAppContext() {
+        // Context of the app under test.
+        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+        assertEquals("com.example.whatever", appContext.packageName)
+    }
+}

+ 57 - 0
app/src/main/AndroidManifest.xml

@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools" >
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.BLUETOOTH" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+
+    <application
+        android:name=".MyApplication"
+        android:allowBackup="true"
+        android:dataExtractionRules="@xml/data_extraction_rules"
+        android:fullBackupContent="@xml/backup_rules"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:networkSecurityConfig="@xml/common_network_config"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/Theme.Whatever"
+        tools:targetApi="31" >
+        <activity
+            android:name=".activity.CmdActivity"
+            android:exported="false" />
+        <activity
+            android:name=".activity.ModbusActivity"
+            android:exported="false" />
+        <activity
+            android:name=".activity.BleActivity"
+            android:exported="false" />
+        <activity
+            android:name=".activity.WebActivity"
+            android:exported="false" />
+        <activity
+            android:name=".activity.ScreenAdaptActivity"
+            android:exported="false" />
+
+        <meta-data
+            android:name="design_width_in_dp"
+            android:value="640" />
+        <meta-data
+            android:name="design_height_in_dp"
+            android:value="360" />
+
+        <activity
+            android:name=".activity.MainActivity"
+            android:exported="true" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>

BIN
app/src/main/assets/testpng.png


+ 7 - 0
app/src/main/java/com/example/whatever/CustomBleGattCallback.kt

@@ -0,0 +1,7 @@
+package com.example.whatever
+
+import com.clj.fastble.callback.BleGattCallback
+
+abstract class CustomBleGattCallback : BleGattCallback() {
+    abstract fun onPrompt(promptStr: String?)
+}

+ 9 - 0
app/src/main/java/com/example/whatever/CustomBleIndicateCallback.kt

@@ -0,0 +1,9 @@
+package com.example.whatever
+
+import com.clj.fastble.callback.BleIndicateCallback
+
+abstract class CustomBleIndicateCallback : BleIndicateCallback() {
+    abstract fun onPrompt(promptStr: String?)
+    abstract fun onConnectPrompt(promptStr: String?)
+    abstract fun onDisConnectPrompt(promptStr: String?)
+}

+ 7 - 0
app/src/main/java/com/example/whatever/CustomBleScanCallback.kt

@@ -0,0 +1,7 @@
+package com.example.whatever
+
+import com.clj.fastble.callback.BleScanCallback
+
+abstract class CustomBleScanCallback : BleScanCallback() {
+    abstract fun onPrompt(promptStr: String?)
+}

+ 9 - 0
app/src/main/java/com/example/whatever/CustomBleWriteCallback.kt

@@ -0,0 +1,9 @@
+package com.example.whatever
+
+import com.clj.fastble.callback.BleWriteCallback
+
+abstract class CustomBleWriteCallback : BleWriteCallback() {
+    abstract fun onPrompt(promptStr: String?)
+    abstract fun onConnectPrompt(promptStr: String?)
+    abstract fun onDisConnectPrompt(promptStr: String?)
+}

+ 27 - 0
app/src/main/java/com/example/whatever/MyApplication.kt

@@ -0,0 +1,27 @@
+package com.example.whatever
+
+import android.app.Application
+import android.content.Context
+import androidx.multidex.MultiDex
+import com.example.whatever.ble.BleUtil
+import com.example.whatever.util.FileUtil
+import com.example.whatever.util.log.LogUtil
+
+class MyApplication : Application() {
+
+    companion object {
+        var instance: MyApplication? = null
+    }
+
+    override fun onCreate() {
+        super.onCreate()
+        instance = this
+        LogUtil.init(instance!!, FileUtil.ROOT_APP + FileUtil.LOG_DIR)
+        BleUtil.instance?.initBle(this)
+    }
+
+    override fun attachBaseContext(base: Context?) {
+        super.attachBaseContext(base)
+        MultiDex.install(base)
+    }
+}

+ 3 - 0
app/src/main/java/com/example/whatever/RealResponse.kt

@@ -0,0 +1,3 @@
+package com.example.whatever
+
+data class RealResponse(val code: Int, val msg: String, val data: Any?)

+ 64 - 0
app/src/main/java/com/example/whatever/Token.kt

@@ -0,0 +1,64 @@
+package com.example.whatever
+
+import android.content.Context
+import android.content.SharedPreferences
+import java.util.concurrent.TimeUnit
+
+class Token(
+    val accessToken: String,
+    val refreshToken: String,
+    val expiresIn: Int = 0,
+    val expiresAt: Long
+
+) {
+
+    fun saveToSp(context:Context): Boolean {
+        val now = System.currentTimeMillis() / 1000
+        return tokenSp(context).edit()
+            .putString("accessToken", accessToken)
+            .putString("refreshToken", refreshToken)
+            .putLong("expiresAt", expiresIn + now - 60)
+            .putLong("refreshExpiresAt", TimeUnit.DAYS.toSeconds(7) + now - 60)
+            .commit()
+    }
+
+    fun isValid(): Boolean {
+        return expiresAt < 0 || expiresAt > System.currentTimeMillis() / 1000
+    }
+
+
+    companion object {
+
+        fun fromSp(context:Context): Token? {
+            val sp = tokenSp(context)
+            val accessToken = sp.getString("accessToken", null)
+            val refreshToken = sp.getString("refreshToken", null)
+            if (accessToken == null || refreshToken == null) {
+                return null
+            }
+            val refreshExpiresAt = sp.getLong("refreshExpiresAt", 0)
+            val now = System.currentTimeMillis() / 1000
+            if (refreshExpiresAt < now) {
+                return null
+            }
+            val expiresAt = sp.getLong("expiresAt", 0)
+            return Token(accessToken, refreshToken, expiresAt = expiresAt)
+        }
+
+        fun clear(context: Context) : Boolean {
+            return tokenSp(context).edit()
+                .putString("accessToken", null)
+                .putString("refreshToken", null)
+                .putLong("expiresAt", 0)
+                .putLong("refreshExpiresAt", 0)
+                .commit()
+        }
+
+        private fun tokenSp(context: Context): SharedPreferences {
+            return context.getSharedPreferences("token", Context.MODE_PRIVATE)
+        }
+
+    }
+
+
+}

+ 36 - 0
app/src/main/java/com/example/whatever/activity/BaseBindingActivity.kt

@@ -0,0 +1,36 @@
+package com.example.whatever.activity
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.viewbinding.ViewBinding
+import pub.devrel.easypermissions.EasyPermissions
+
+abstract class BaseBindingActivity<T : ViewBinding?> : AppCompatActivity() {
+    protected var mBinding: T? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        mBinding = viewBinding
+        setContentView(mBinding!!.root)
+
+        initView()
+    }
+
+    abstract val viewBinding: T
+
+    abstract fun initView()
+
+    override fun onDestroy() {
+        super.onDestroy()
+        mBinding = null
+    }
+
+    fun handleLoading(isShow: Boolean) {
+
+    }
+
+    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this)
+    }
+}

+ 409 - 0
app/src/main/java/com/example/whatever/activity/BleActivity.kt

@@ -0,0 +1,409 @@
+package com.example.whatever.activity
+
+import android.Manifest.permission.ACCESS_COARSE_LOCATION
+import android.Manifest.permission.ACCESS_FINE_LOCATION
+import android.Manifest.permission.BLUETOOTH_ADVERTISE
+import android.Manifest.permission.BLUETOOTH_CONNECT
+import android.Manifest.permission.BLUETOOTH_SCAN
+import android.app.Activity
+import android.bluetooth.BluetoothGatt
+import android.content.Intent
+import android.os.Build
+import android.util.Log
+import com.clj.fastble.BleManager
+import com.clj.fastble.data.BleDevice
+import com.clj.fastble.exception.BleException
+import com.example.whatever.CustomBleGattCallback
+import com.example.whatever.CustomBleIndicateCallback
+import com.example.whatever.CustomBleScanCallback
+import com.example.whatever.CustomBleWriteCallback
+import com.example.whatever.R
+import com.example.whatever.databinding.ActivityBleBinding
+import com.example.whatever.model.BleBean
+import com.example.whatever.model.Constants.PERMISSION_REQUEST_CODE
+import com.example.whatever.ble.BleConst.INDICATE_UUID
+import com.example.whatever.ble.BleConst.SERVICE_UUID
+import com.example.whatever.ble.BleCmdManager
+import com.example.whatever.extentions.toHexString
+import com.example.whatever.util.ToastUtils
+import com.example.whatever.ble.BleUtil
+import com.example.whatever.util.log.LogUtil
+import com.leon.lfilepickerlibrary.LFilePicker
+import com.zhy.adapter.recyclerview.CommonAdapter
+import com.zhy.adapter.recyclerview.base.ViewHolder
+import pub.devrel.easypermissions.AfterPermissionGranted
+import pub.devrel.easypermissions.EasyPermissions
+import java.io.File
+
+class BleActivity : BaseBindingActivity<ActivityBleBinding>() {
+
+    private val FILE_CODE = 1000
+    private var fileMac: String? = null
+
+    override val viewBinding: ActivityBleBinding
+        get() = ActivityBleBinding.inflate(layoutInflater)
+
+    override fun initView() {
+
+        mBinding?.srl?.setEnableRefresh(true)
+        mBinding?.srl?.setEnableLoadMore(false)
+        mBinding?.srl?.autoRefresh()
+        mBinding?.srl?.setOnRefreshListener { checkPermission() }
+
+        mBinding?.rvBle?.adapter =
+            object : CommonAdapter<BleBean>(this, R.layout.item_ble, BleUtil.instance?.deviceList) {
+                override fun convert(holder: ViewHolder, item: BleBean?, position: Int) {
+                    holder.setText(R.id.ble_name, "Name:${item?.bleDevice?.name}")
+                    holder.setText(R.id.ble_mac, "Mac:${item?.bleDevice?.mac}")
+                    holder.setText(R.id.blc_connection, if (BleManager.getInstance().isConnected(item?.bleDevice)) "已连接" else "未连接")
+                    holder.setOnClickListener(R.id.connect) { doConnect(position) }
+                    holder.setOnClickListener(R.id.disconnect) {
+                        BleUtil.instance?.indicate(BleUtil.instance?.deviceList?.get(position)?.bleDevice!!, isStart = false, indicateCallback = null)
+                        mBinding?.rvBle?.adapter?.notifyItemChanged(position)
+                    }
+
+                    holder.setOnClickListener(R.id.mtu) {
+                        BleUtil.instance?.setMtu(BleUtil.instance?.deviceList?.get(position)?.bleDevice!!)
+                    }
+
+                    holder.setOnClickListener(R.id.indicate) {
+                        item?.let {
+                            BleUtil.instance?.indicate(it.bleDevice, indicateCallback = object : CustomBleIndicateCallback() {
+                                override fun onPrompt(promptStr: String?) {
+                                    println("监听onPrompt : $promptStr")
+                                }
+
+                                override fun onConnectPrompt(promptStr: String?) {
+                                    println("监听onConnectPrompt : $promptStr")
+                                }
+
+                                override fun onDisConnectPrompt(promptStr: String?) {
+                                    println("监听onDisConnectPrompt : $promptStr")
+                                }
+
+                                override fun onIndicateSuccess() {
+                                    println("监听成功")
+                                }
+
+                                override fun onIndicateFailure(exception: BleException?) {
+                                    println("监听失败")
+                                }
+
+                                override fun onCharacteristicChanged(data: ByteArray?) {
+                                    println("******************* : ${data?.toHexString()}")
+                                    data?.let { itData ->
+                                        BleCmdManager.handleRsp(it, itData)
+                                    }
+                                }
+                            })
+                        }
+                    }
+
+                    holder.setOnClickListener(R.id.stopIndicate) {
+                        item?.let {
+                            BleUtil.instance?.indicate(it.bleDevice, isStart = false, indicateCallback = null)
+                        }
+                    }
+
+                    holder.setOnClickListener(R.id.exit) { finish() }
+
+                    holder.setOnClickListener(R.id.token) {
+                        BleCmdManager.getToken(item?.bleDevice?.mac, object : CustomBleWriteCallback() {
+                            override fun onPrompt(promptStr: String?) {}
+
+                            override fun onConnectPrompt(promptStr: String?) {}
+
+                            override fun onDisConnectPrompt(promptStr: String?) {}
+
+                            override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                                println("getToken success")
+                            }
+
+                            override fun onWriteFailure(exception: BleException?) {
+                                println("getToken fail")
+                            }
+                        })
+                    }
+
+                    holder.setOnClickListener(R.id.version) {
+                        BleCmdManager.getVersion(item?.bleDevice?.mac, object : CustomBleWriteCallback() {
+                            override fun onPrompt(promptStr: String?) {}
+
+                            override fun onConnectPrompt(promptStr: String?) {}
+
+                            override fun onDisConnectPrompt(promptStr: String?) {}
+
+                            override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                                println("getVersion success")
+                            }
+
+                            override fun onWriteFailure(exception: BleException?) {
+                                println("getVersion fail")
+                            }
+                        })
+                    }
+
+                    holder.setOnClickListener(R.id.power) {
+                        BleCmdManager.getPower(item?.bleDevice?.mac, object : CustomBleWriteCallback() {
+                            override fun onPrompt(promptStr: String?) {}
+
+                            override fun onConnectPrompt(promptStr: String?) {}
+
+                            override fun onDisConnectPrompt(promptStr: String?) {}
+
+                            override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                                println("getPower success")
+                            }
+
+                            override fun onWriteFailure(exception: BleException?) {
+                                println("getPower fail")
+                            }
+                        })
+                    }
+
+                    holder.setOnClickListener(R.id.device) {
+                        BleCmdManager.getCurrentStatus(item?.bleDevice!!, object : CustomBleWriteCallback() {
+                            override fun onPrompt(promptStr: String?) {}
+
+                            override fun onConnectPrompt(promptStr: String?) {}
+
+                            override fun onDisConnectPrompt(promptStr: String?) {}
+
+                            override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                                println("getCurrentStatus success")
+                            }
+                            override fun onWriteFailure(exception: BleException?) {
+                                println("getCurrentStatus fail")}
+                        })
+                    }
+
+                    holder.setOnClickListener(R.id.mode1) {
+                        BleCmdManager.switchMode(byteArrayOf(0x01), item?.bleDevice!!, object : CustomBleWriteCallback() {
+                            override fun onPrompt(promptStr: String?) {}
+
+                            override fun onConnectPrompt(promptStr: String?) {}
+
+                            override fun onDisConnectPrompt(promptStr: String?) {}
+
+                            override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                                println("switch mode 1 success")}
+                            override fun onWriteFailure(exception: BleException?) {
+                                println("switch mode 1 fail")}
+                        })
+                    }
+
+                    holder.setOnClickListener(R.id.mode2) {
+                        BleCmdManager.switchMode(byteArrayOf(0x02), item?.bleDevice!!, object : CustomBleWriteCallback() {
+                            override fun onPrompt(promptStr: String?) {}
+
+                            override fun onConnectPrompt(promptStr: String?) {}
+
+                            override fun onDisConnectPrompt(promptStr: String?) {}
+
+                            override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                                println("switch mode 2 success")}
+                            override fun onWriteFailure(exception: BleException?) {
+                                println("switch mode 2 fail")}
+                        })
+                    }
+
+                    holder.setOnClickListener(R.id.ticketStatus) {
+                        BleCmdManager.getTicketStatus(item?.bleDevice!!, object : CustomBleWriteCallback() {
+                            override fun onPrompt(promptStr: String?) {}
+
+                            override fun onConnectPrompt(promptStr: String?) {}
+
+                            override fun onDisConnectPrompt(promptStr: String?) {}
+
+                            override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                                println("getTicketStatus success")}
+                            override fun onWriteFailure(exception: BleException?) {
+                                println("getTicketStatus fail")}
+                        })
+                    }
+
+                    holder.setOnClickListener(R.id.sendTicket) {
+//                        val testStr = "{\"cardNo\":\"ACB13E0B\",\"data\":[{\"taskCode\":\"87654321110\",\"taskId\":\"587aa6cfcce940778836e512a10c330e\",\"dataList\":[{\"dataId\":1,\"target\":0,\"status\":0,\"closed\":1,\"equipRfidNo\":\"1DA0C66D031080\",\"infoRfidNo\":\"206BD295\"},{\"dataId\":2,\"target\":0,\"status\":0,\"closed\":1,\"equipRfidNo\":\"1DCAD26D031080\",\"infoRfidNo\":\"D074D295\"}],\"closed\":1}]}"
+                        val testStr = "{\"cardNo\":\"80A8C0F4EA\",\"password\":\"12345678\",\"effectiveTime\":24,\"data\":[{\"taskCode\":\"202401020001\",\"taskId\":\"71b49baa49b343bc84d7e6b829ac1bdc\",\"codeId\":1,\"dataList\":[{\"dataId\":1,\"equipRfidNo\":\"049648B2E31690\",\"infoRfidNo\":\"04E3BCCA201290\",\"target\":1},{\"dataId\":2,\"equipRfidNo\":\"0405982414C563\",\"target\":0,\"prevId\":1}]},{\"taskCode\":\"202401020002\",\"taskId\":\"145b5a4cc38c41e19943f4c8b48d12b0\",\"codeId\":2,\"dataList\":[{\"dataId\":1,\"equipRfidNo\":\"045460F7F4F438\",\"infoRfidNo\":\"04BC6584C65009\",\"target\":1},{\"dataId\":2,\"equipRfidNo\":\"042B99E449E795\",\"target\":0,\"prevId\":1},{\"dataId\":3,\"equipRfidNo\":\"04A312EE848B62\",\"infoRfidNo\":\"04220E86831289\",\"target\":1,\"prevId\":2}]}],\"lockList\":[{\"lockId\":\"1\",\"rfid\":\"040E21443010E9\"},{\"lockId\":\"2\",\"rfid\":\"0457505E5861C2\"}]}"
+                        BleCmdManager.sendWorkTicket(testStr, bleDevice = item?.bleDevice!!, callback = object : CustomBleWriteCallback() {
+                            override fun onPrompt(promptStr: String?) {}
+
+                            override fun onConnectPrompt(promptStr: String?) {}
+
+                            override fun onDisConnectPrompt(promptStr: String?) {}
+
+                            override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                                println("sendTicket success")
+                            }
+
+                            override fun onWriteFailure(exception: BleException?) {
+                                println("sendTicket fail")
+                            }
+                        })
+                    }
+
+                    holder.setOnClickListener(R.id.sendFile) {
+                        fileMac = item?.bleDevice?.mac
+
+                        LFilePicker()
+                            .withActivity(this@BleActivity)
+                            .withRequestCode(FILE_CODE)
+//                            .withStartPath("${FileUtil.getRootFolder(this)?.absolutePath}")
+                            .start()
+
+                    }
+                }
+            }
+
+        BleManager.getInstance().init(application)
+    }
+
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        super.onActivityResult(requestCode, resultCode, data)
+        when (requestCode) {
+            FILE_CODE -> {
+                if (resultCode == Activity.RESULT_OK && data != null) {
+                    val list: List<String>? = data.getStringArrayListExtra("paths")
+                    LogUtil.d("选择的文件为: $list")
+                    if (list.isNullOrEmpty()) {
+                        ToastUtils.tip("请选择文件!")
+                        return
+                    }
+                    BleCmdManager.sendFile(2, File(list[0]), mac = fileMac, callback = object : CustomBleWriteCallback() {
+                        override fun onPrompt(promptStr: String?) {}
+
+                        override fun onConnectPrompt(promptStr: String?) {}
+
+                        override fun onDisConnectPrompt(promptStr: String?) {}
+
+                        override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                            println("sendFile success")
+                        }
+
+                        override fun onWriteFailure(exception: BleException?) {
+                            println("sendFile fail")
+                        }
+                    })
+                }
+            }
+        }
+    }
+
+    private fun checkPermission() {
+        val permissions: Array<String> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            arrayOf(
+                BLUETOOTH_SCAN,
+                BLUETOOTH_ADVERTISE,
+                BLUETOOTH_CONNECT,
+                ACCESS_FINE_LOCATION,
+                ACCESS_COARSE_LOCATION
+            )
+        } else {
+            arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION)
+        }
+        val isPermission = EasyPermissions.hasPermissions(this, *permissions)
+        if (isPermission) {
+            doScan()
+        } else {
+            EasyPermissions.requestPermissions(this, "", PERMISSION_REQUEST_CODE, *permissions)
+        }
+    }
+
+    @AfterPermissionGranted(PERMISSION_REQUEST_CODE)
+    fun doScan() {
+        Log.d("doScanBle", "扫描开始:$")
+        BleUtil.instance?.scan(object : CustomBleScanCallback() {
+            override fun onPrompt(promptStr: String?) {
+                ToastUtils.tip(promptStr)
+                mBinding?.srl?.finishRefresh()
+                BleManager.getInstance().enableBluetooth()
+            }
+
+            override fun onScanStarted(success: Boolean) {
+                Log.d("doScanBle", "扫描开始:${success}")
+                BleUtil.instance?.deviceList?.clear()
+                mBinding?.rvBle?.adapter?.notifyDataSetChanged()
+            }
+
+            override fun onScanning(bleDevice: BleDevice?) {
+                bleDevice?.let {
+                    Log.d("doScanBle", "扫描到的设备:${it.mac}")
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                        if (!it.name.isNullOrBlank()) {
+                            BleUtil.instance?.deviceList?.add(BleBean(it))
+                            mBinding?.rvBle?.adapter?.notifyDataSetChanged()
+                        } else {
+
+                        }
+                    } else {
+                        BleUtil.instance?.deviceList?.add(BleBean(it))
+                        mBinding?.rvBle?.adapter?.notifyDataSetChanged()
+                    }
+                }
+            }
+
+            override fun onScanFinished(scanResultList: MutableList<BleDevice>?) {
+                mBinding?.srl?.finishRefresh()
+                if (BleUtil.instance?.deviceList?.isEmpty() == true) {
+                    ToastUtils.tip("没有扫描到设备,请下拉刷新重试!")
+                }
+            }
+        })
+    }
+
+    fun doConnect(position: Int) {
+        handleLoading(true)
+        BleManager.getInstance().disconnect(BleUtil.instance?.deviceList?.get(position)?.bleDevice)
+        BleUtil.instance?.connectBySelect(BleUtil.instance?.deviceList?.get(position)?.bleDevice,
+            object : CustomBleGattCallback() {
+                override fun onPrompt(promptStr: String?) {
+                    handleLoading(false)
+                    ToastUtils.tip(promptStr)
+                }
+
+                override fun onStartConnect() {
+                }
+
+                override fun onConnectFail(bleDevice: BleDevice?, exception: BleException?) {
+                    handleLoading(false)
+                    ToastUtils.tip("连接失败,请重试!")
+                }
+
+                override fun onConnectSuccess(
+                    bleDevice: BleDevice?,
+                    gatt: BluetoothGatt?,
+                    status: Int
+                ) {
+                    LogUtil.i("onConnectSuccess : $bleDevice")
+                    bleDevice?.let {
+                        mBinding?.rvBle?.adapter?.notifyItemChanged(position)
+//                        BleUtil.instance?.deviceList?.add(BleBean(it))
+                        LogUtil.i("Add BleBean : ${BleUtil.instance?.deviceList}")
+                    }
+                }
+
+                override fun onDisConnected(
+                    isActiveDisConnected: Boolean,
+                    device: BleDevice?,
+                    gatt: BluetoothGatt?,
+                    status: Int
+                ) {
+                    handleLoading(false)
+                    ToastUtils.tip("连接断开了,请重试!")
+                    BleUtil.instance?.getBleDeviceByMac(device?.mac)?.let {
+                        BleUtil.instance?.deviceList?.remove(it)
+                    }
+                }
+            })
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+
+        BleUtil.instance?.deviceList?.forEach {
+            BleManager.getInstance().stopIndicate(it.bleDevice, SERVICE_UUID, INDICATE_UUID)
+        }
+        BleUtil.instance?.deviceList?.clear()
+        BleManager.getInstance().disconnectAllDevice()
+    }
+}

+ 128 - 0
app/src/main/java/com/example/whatever/activity/CmdActivity.kt

@@ -0,0 +1,128 @@
+package com.example.whatever.activity
+
+import com.clj.fastble.data.BleDevice
+import com.clj.fastble.exception.BleException
+import com.example.whatever.CustomBleIndicateCallback
+import com.example.whatever.CustomBleWriteCallback
+import com.example.whatever.databinding.ActivityCmdBinding
+import com.example.whatever.ble.BleCmdManager
+import com.example.whatever.ble.BleUtil
+
+class CmdActivity : BaseBindingActivity<ActivityCmdBinding>() {
+
+    private var bleDevice: BleDevice? = null
+
+    override val viewBinding: ActivityCmdBinding
+        get() = ActivityCmdBinding.inflate(layoutInflater)
+
+    override fun initView() {
+        bleDevice = intent.getParcelableExtra("device")
+
+        mBinding?.exit?.setOnClickListener { finish() }
+
+        mBinding?.indicate?.setOnClickListener {
+            BleUtil.instance?.getBleDeviceByMac(bleDevice?.mac)?.let {
+                BleUtil.instance?.indicate(it.bleDevice, indicateCallback = object : CustomBleIndicateCallback() {
+                    override fun onPrompt(promptStr: String?) {}
+
+                    override fun onConnectPrompt(promptStr: String?) {}
+
+                    override fun onDisConnectPrompt(promptStr: String?) {}
+
+                    override fun onIndicateSuccess() {
+                        println("监听成功")
+                    }
+
+                    override fun onIndicateFailure(exception: BleException?) {
+                        println("监听失败")
+                    }
+
+                    override fun onCharacteristicChanged(data: ByteArray?) {
+                        println("******************* : ${data?.toList()}")
+                        data?.let { itData ->
+                            BleCmdManager.handleRsp(it, itData)
+                        }
+                    }
+
+                })
+            }
+
+        }
+        mBinding?.token?.setOnClickListener {
+            BleCmdManager.getToken(bleDevice?.mac, object : CustomBleWriteCallback() {
+                override fun onPrompt(promptStr: String?) {}
+
+                override fun onConnectPrompt(promptStr: String?) {}
+
+                override fun onDisConnectPrompt(promptStr: String?) {}
+
+                override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                    println("getToken success")
+                }
+
+                override fun onWriteFailure(exception: BleException?) {
+                    println("getToken fail")
+                }
+            })
+        }
+        mBinding?.version?.setOnClickListener {
+            BleCmdManager.getVersion(bleDevice?.mac, object : CustomBleWriteCallback() {
+                override fun onPrompt(promptStr: String?) {}
+
+                override fun onConnectPrompt(promptStr: String?) {}
+
+                override fun onDisConnectPrompt(promptStr: String?) {}
+
+                override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                    println("getVersion success")
+                }
+
+                override fun onWriteFailure(exception: BleException?) {
+                    println("getVersion fail")
+                }
+            })
+        }
+        mBinding?.power?.setOnClickListener {
+            BleCmdManager.getPower(bleDevice?.mac, object : CustomBleWriteCallback() {
+                override fun onPrompt(promptStr: String?) {}
+
+                override fun onConnectPrompt(promptStr: String?) {}
+
+                override fun onDisConnectPrompt(promptStr: String?) {}
+
+                override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                    println("getPower success")
+                }
+
+                override fun onWriteFailure(exception: BleException?) {
+                    println("getPower fail")
+                }
+            })
+        }
+//        mBinding?.status?.setOnClickListener {
+//            BleCmdManager.getCurrentStatus(bleDevice, object : CustomBleWriteCallback() {
+//                override fun onPrompt(promptStr: String?) {}
+//
+//                override fun onConnectPrompt(promptStr: String?) {}
+//
+//                override fun onDisConnectPrompt(promptStr: String?) {}
+//
+//                override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+//                    println("getCurrentStatus success")
+//                }
+//
+//                override fun onWriteFailure(exception: BleException?) {
+//                    println("getCurrentStatus fail")
+//                }
+//            })
+//        }
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+
+//        bleDevice?.let { BleManager.getInstance().stopIndicate(it, SERVICE_UUID, INDICATE_UUID) }
+//        BleManager.getInstance().disconnectAllDevice()
+//        BlueToothUtil.instance?.deviceList?.remove(bleDevice)
+    }
+}

+ 44 - 0
app/src/main/java/com/example/whatever/activity/MainActivity.kt

@@ -0,0 +1,44 @@
+package com.example.whatever.activity
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import com.example.whatever.R
+import com.example.whatever.databinding.ActivityMainBinding
+
+class MainActivity : BaseBindingActivity<ActivityMainBinding>() {
+
+    override val viewBinding: ActivityMainBinding
+        get() = ActivityMainBinding.inflate(layoutInflater)
+
+    override fun initView() {
+
+//        NetHttpManager.getInstance().initCtx(this)
+//
+//        NetHttpManager.getInstance()
+//            .doRequestNet("/app-api/system/area/tree", false, emptyMap<String, Objects>(),
+//                { res, errMsg, codeInt ->
+//                    res?.let {
+//                        val rsp = res.toBean(RealResponse::class.java)
+//                        Log.i("haha1", "${rsp.code}")
+//                        Log.i("haha2", rsp.msg)
+//                        Log.i("haha3", "${rsp.data}")
+//                    }
+//                    Log.i("xixi", "$errMsg : $codeInt")
+//                }, isGet = true, isAuth = false
+//            )
+
+//        startActivity(Intent(this, ScreenAdaptActivity::class.java))
+//        startActivity(Intent(this, WebActivity::class.java))
+
+
+        mBinding?.ble?.setOnClickListener {
+            startActivity(Intent(this, BleActivity::class.java))
+        }
+
+        mBinding?.modbus?.setOnClickListener {
+            startActivity(Intent(this, ModbusActivity::class.java))
+
+        }
+    }
+}

+ 22 - 0
app/src/main/java/com/example/whatever/activity/ModbusActivity.kt

@@ -0,0 +1,22 @@
+package com.example.whatever.activity
+
+import com.example.whatever.databinding.ActivityModbusBinding
+import com.example.whatever.modbus.PortManager
+
+class ModbusActivity : BaseBindingActivity<ActivityModbusBinding>() {
+    override val viewBinding: ActivityModbusBinding
+        get() = ActivityModbusBinding.inflate(layoutInflater)
+
+    override fun initView() {
+        mBinding!!.textView.text = "Modbus"
+
+        PortManager.openPlc {
+
+        }
+
+        mBinding?.send?.setOnClickListener {
+            PortManager.plcPort?.send(byteArrayOf(0x03, 0x00, 0x6B, 0x00, 0x03))
+        }
+    }
+
+}

+ 20 - 0
app/src/main/java/com/example/whatever/activity/ScreenAdaptActivity.kt

@@ -0,0 +1,20 @@
+package com.example.whatever.activity
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import com.example.whatever.databinding.ActivityScreenAdaptBinding
+
+class ScreenAdaptActivity : AppCompatActivity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        val binding = ActivityScreenAdaptBinding.inflate(layoutInflater)
+
+        setContentView(binding.root)
+        var i = 0
+
+        binding.tvTest.setOnClickListener {
+            binding.tvTest.text = "点击了$i"
+            i++
+        }
+    }
+}

+ 57 - 0
app/src/main/java/com/example/whatever/activity/WebActivity.kt

@@ -0,0 +1,57 @@
+package com.example.whatever.activity
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.view.KeyEvent
+import android.view.View
+import android.webkit.WebViewClient
+import androidx.appcompat.app.AppCompatActivity
+import com.example.whatever.databinding.ActivityWebBinding
+
+
+class WebActivity : AppCompatActivity() {
+
+    private var mBinding: ActivityWebBinding? = null
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        mBinding = ActivityWebBinding.inflate(layoutInflater)
+        setContentView(mBinding!!.root)
+
+        createWebView()
+    }
+
+    @SuppressLint("SetJavaScriptEnabled")
+    private fun createWebView() {
+        // 创建 WebView 实例并通过 id 绑定我们刚在布局中创建的 WebView 标签
+        // 这里的 R.id.webview 就是 activity_main.xml 中的 WebView 标签的 id
+
+
+        // 设置 WebView 允许执行 JavaScript 脚本
+        mBinding!!.webView.settings.javaScriptEnabled = true
+
+        // 确保跳转到另一个网页时仍然在当前 WebView 中显示
+        // 而不是调用浏览器打开
+        mBinding!!.webView.webViewClient = WebViewClient()
+
+        // 加载指定网页
+//        val url = "https://yaie.hihakid.com/homePage"
+        val url = "www.ithome.com"
+        mBinding!!.webView.loadUrl(url)
+
+        mBinding!!.webView.setOnKeyListener(View.OnKeyListener { v, keyCode, event ->
+            if (keyCode == KeyEvent.KEYCODE_BACK && mBinding!!.webView.canGoBack()) {
+                mBinding!!.webView.goBack()
+                return@OnKeyListener true
+            }
+            false
+        })
+    }
+
+    override fun onBackPressed() {
+        if (mBinding!!.webView.canGoBack()) {
+            mBinding!!.webView.goBack()
+        } else {
+            super.onBackPressed()
+        }
+    }
+}

+ 419 - 0
app/src/main/java/com/example/whatever/ble/BleCmdManager.kt

@@ -0,0 +1,419 @@
+package com.example.whatever.ble
+
+import com.clj.fastble.data.BleDevice
+import com.clj.fastble.exception.BleException
+import com.example.whatever.CustomBleWriteCallback
+import com.example.whatever.extentions.crc16
+import com.example.whatever.extentions.startsWith
+import com.example.whatever.extentions.toByteArray
+import com.example.whatever.extentions.toHexString
+import com.example.whatever.model.BleBean
+import com.example.whatever.ble.BleConst.REQ_CURRENT_STATUS
+import com.example.whatever.ble.BleConst.REQ_GET_TOKEN
+import com.example.whatever.ble.BleConst.REQ_GET_VERSION
+import com.example.whatever.ble.BleConst.REQ_POWER_STATUS
+import com.example.whatever.ble.BleConst.REQ_SEND_WORK_TICKET
+import com.example.whatever.ble.BleConst.REQ_SWITCH_MODE
+import com.example.whatever.ble.BleConst.REQ_TRANSFER_FILE
+import com.example.whatever.ble.BleConst.REQ_WORK_TICKET_RESULT
+import com.example.whatever.ble.BleConst.REQ_WORK_TICKET_RESULT_PART
+import com.example.whatever.ble.BleConst.WRITE_UUID
+import com.example.whatever.util.Executor
+import com.example.whatever.util.log.LogUtil
+import java.io.File
+
+/**
+ * 指令操作类
+ */
+object BleCmdManager {
+
+    /**
+     * 拼接时间戳
+     */
+    private fun assembleTimeStamp(byteArray: ByteArray): ByteArray {
+        return byteArray + getTimeStamp()
+    }
+
+    /**
+     * 拼接时间戳 + token
+     */
+    private fun assembleData(bleBean: BleBean, byteArray: ByteArray): ByteArray? {
+        bleBean.token?.let {
+            // TODO 封装assembleTimeStamp
+            return assembleTimeStamp(byteArray) + it
+        } ?: run {
+            // TODO 有问题,一直循环
+//            getToken(bleBean.bleDevice.mac, object : CustomBleWriteCallback() {
+//                override fun onPrompt(promptStr: String?) {}
+//
+//                override fun onConnectPrompt(promptStr: String?) {}
+//
+//                override fun onDisConnectPrompt(promptStr: String?) {}
+//
+//                override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {}
+//
+//                override fun onWriteFailure(exception: BleException?) {}
+//
+//            })
+//            // TODO 临时方案
+//            Thread.sleep(100)
+//            return assembleData(bleBean, byteArray)
+
+            return null
+        }
+    }
+
+    private fun getTimeStamp(): ByteArray {
+        val tempArr = (System.currentTimeMillis() / 1000).toByteArray()
+        val timeStampArr = byteArrayOf(tempArr[0], tempArr[1], tempArr[2], tempArr[3])
+        return timeStampArr
+    }
+
+    fun handleRsp(bleBean: BleBean, byteArray: ByteArray) {
+        val len = byteArray[2].toInt()
+        val token = byteArray.copyOfRange(len + 7, len + 11)
+        if (token.contentEquals(bleBean.token)) {
+            LogUtil.i("Token is right")
+        } else {
+            LogUtil.e("Token is wrong")
+        }
+        when {
+            // 获取令牌
+            byteArray.startsWith(BleConst.RSP_GET_TOKEN) -> handleToken(bleBean.bleDevice, byteArray)
+            // 工作模式切换
+            byteArray.startsWith(BleConst.RSP_SWITCH_MODE) -> handleSwitchModeResult(byteArray)
+            // 工作票下发
+            byteArray.startsWith(BleConst.RSP_SEND_WORK_TICKET) -> handleWorkTicketResult(bleBean, byteArray)
+            // 获取设备当前状态
+            byteArray.startsWith(BleConst.RSP_CURRENT_STATUS) -> handleCurrentStatus(byteArray)
+            // 获取钥匙电量
+            byteArray.startsWith(BleConst.RSP_POWER_STATUS) -> handlePowerStatus(byteArray)
+            // 传输文件
+            byteArray.startsWith(BleConst.RSP_TRANSFER_FILE) && byteArray[3] == 0x01.toByte() -> handleFileRsp(bleBean, byteArray)
+            // 获取固件版本号
+            byteArray.startsWith(BleConst.RSP_GET_VERSION) -> handleVersion(byteArray)
+            // 获取设备工作票完成情况
+            byteArray.startsWith(BleConst.RSP_WORK_TICKET_RESULT) && byteArray[3] == 0x02.toByte() ->
+                handleTicketStatus(bleBean.bleDevice, byteArray)
+        }
+    }
+
+    /**
+     * 获取令牌
+     */
+    fun getToken(mac: String?, callback: CustomBleWriteCallback?) {
+        LogUtil.i("$mac")
+        BleUtil.instance?.getBleDeviceByMac(mac)?.bleDevice?.let {
+            LogUtil.i("Get token : $mac")
+            BleUtil.instance?.write(it, cmd = assembleTimeStamp(REQ_GET_TOKEN), writeCallback = callback)
+        }
+    }
+
+    /**
+     * 令牌处理
+     */
+    private fun handleToken(bleDevice: BleDevice, byteArray: ByteArray) {
+        LogUtil.i("handleToken : ${byteArray.toHexString()}")
+        BleUtil.instance?.getBleDeviceByMac(bleDevice.mac)?.let {
+            it.token = byteArrayOf(byteArray[11], byteArray[12], byteArray[13], byteArray[14])
+
+            println("Token 赋值 ${it.token?.toHexString()} : ${bleDevice.mac}")
+        }
+    }
+
+    /**
+     * 工作模式切换
+     *
+     * @param mode 0x01:工作模式 0x02:待机模式
+     */
+    fun switchMode(mode: ByteArray, bleDevice: BleDevice, callback: CustomBleWriteCallback?) {
+        BleUtil.instance?.getBleDeviceByMac(bleDevice.mac)?.let {
+            BleUtil.instance?.write(it.bleDevice, cmd = assembleData(it, REQ_SWITCH_MODE + mode), writeCallback = callback)
+        }
+    }
+
+    /**
+     * 工作模式切换结果
+     * job : 0x01:工作模式 0x02:待机模式
+     * res : 0x01:成功 0x02:失败
+     */
+    private fun handleSwitchModeResult(byteArray: ByteArray) {
+        LogUtil.i("handleSwitchModeResult : ${byteArray.toHexString()}")
+        val job = byteArray[4]
+        val res = byteArray[5]
+    }
+
+    /**
+     * 工作票下发
+     */
+    fun sendWorkTicket(json: String, idx: Int = 0, bleDevice: BleDevice, callback: CustomBleWriteCallback?) {
+        LogUtil.i("sendWorkTicket : $idx")
+        BleUtil.instance?.getBleDeviceByMac(bleDevice.mac)?.let {
+            it.ticketSend = json
+        }
+        val totalLength = json.toByteArray().size
+        val totalPackets = (totalLength + 128 - 1) / 128
+        val total = totalPackets.toByteArray()
+
+        val data = if (idx == totalPackets - 1) {
+            json.toByteArray().copyOfRange(idx * 128, json.toByteArray().size - 1)
+        } else {
+            json.toByteArray().copyOfRange(idx * 128, (idx + 1) * 128)
+        }
+//        val jsonInfo = total + idx.toByteArray() + CRC16.crc16(data, 0, data.size - 1).toByteArray() + data.size.toByteArray() + data
+        val jsonInfo = total + idx.toByteArray() + data.crc16(0, data.size) + data.size.toByteArray() + data
+        println("xixi1 : ${total.size} : ${idx.toByteArray().size} : ${data.crc16(0, data.size).size} : ${data.size.toByteArray().size} : ${data.size}")
+        println("xixi2 : ${(jsonInfo.size + 1).toByteArray(1).size} : ${0x02.toByteArray(1).size} : ${jsonInfo.size}")
+
+        val cmd = REQ_SEND_WORK_TICKET + (jsonInfo.size + 1).toByteArray(1) + 0x02.toByteArray(1) + jsonInfo
+
+        BleUtil.instance?.getBleDeviceByMac(bleDevice.mac)?.let {
+            BleUtil.instance?.write(it.bleDevice, cmd = assembleData(it, cmd), writeCallback = callback)
+        }
+    }
+
+    /**
+     * 工作票下发结果
+     * res:0x00:成功 0x01:失败 0x02:传输超时 0x0D:当前IDX超出范围 0x0E:当前数据CRC校验失败 0x14:JSON结构错误 0x63:未知错误
+     */
+    private fun handleWorkTicketResult(bleBean: BleBean, byteArray: ByteArray) {
+        LogUtil.i("handleWorkTicketResult : ${byteArray.toHexString()}")
+        val idx = byteArray[4] + byteArray[5]
+        val total = byteArray[6] + byteArray[7]
+        val res = byteArray[8]
+        if (idx != total - 1 && (res == 0x00.toByte() || res == 0x02.toByte())) {
+            // TODO 要判断res
+            sendWorkTicket(
+                BleUtil.instance?.getBleDeviceByMac(bleBean.bleDevice.mac)?.ticketSend!!,
+                if (res == 0x00.toByte()) idx + 1 else idx,
+                bleBean.bleDevice,
+                object : CustomBleWriteCallback() {
+                    override fun onPrompt(promptStr: String?) {}
+
+                    override fun onConnectPrompt(promptStr: String?) {}
+
+                    override fun onDisConnectPrompt(promptStr: String?) {}
+
+                    override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {}
+
+                    override fun onWriteFailure(exception: BleException?) {}
+
+                })
+        }
+    }
+
+    /**
+     * 获取设备当前状态
+     */
+    fun getCurrentStatus(bleDevice: BleDevice, callback: CustomBleWriteCallback?) {
+        BleUtil.instance?.getBleDeviceByMac(bleDevice.mac)?.let {
+            BleUtil.instance?.write(it.bleDevice, cmd = assembleData(it, REQ_CURRENT_STATUS), writeCallback = callback)
+        }
+    }
+
+    /**
+     * 处理设备当前状态
+     * 0x01:工作模式 0x02:待机模式 0x03:故障状态
+     */
+    private fun handleCurrentStatus(byteArray: ByteArray) {
+        LogUtil.i("handleCurrentStatus : ${byteArray.toHexString()}")
+        val job = byteArray[4]
+    }
+
+    /**
+     * 获取工作票完成情况
+     */
+    fun getTicketStatus(bleDevice: BleDevice, callback: CustomBleWriteCallback?) {
+        BleUtil.instance?.getBleDeviceByMac(bleDevice.mac)?.let {
+            BleUtil.instance?.write(it.bleDevice, cmd = assembleData(it, REQ_WORK_TICKET_RESULT), writeCallback = callback)
+        }
+    }
+
+    /**
+     * 处理工作票完成情况
+     */
+    private fun handleTicketStatus(bleDevice: BleDevice, byteArray: ByteArray) {
+        // TODO 需要有超时重传机制
+        LogUtil.i("handleTicketStatus : ${byteArray.toHexString()}")
+
+        val total = byteArray[4] + byteArray[5]
+        val idx = byteArray[6] + byteArray[7]
+        val crc = byteArray[8] + byteArray[9]
+        val size = byteArray[10].toUByte() + byteArray[11].toUByte()
+        println("工作票数据 : $total : $idx : $size")
+        // 数据组装
+        BleUtil.instance?.getBleDeviceByMac(bleDevice.mac)?.let {
+            it.ticketStatus += byteArray.copyOfRange(12, 12 + size.toInt())
+        }
+        // TODO 缺少res处理
+        if (idx != total - 1) {
+            getTicketStatusPart((idx + 1).toByteArray(), total.toByteArray(), byteArrayOf(0x01.toByte()), bleDevice, object : CustomBleWriteCallback() {
+                override fun onPrompt(promptStr: String?) {}
+
+                override fun onConnectPrompt(promptStr: String?) {}
+
+                override fun onDisConnectPrompt(promptStr: String?) {}
+
+                override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                    println("getTicketStatusPart success")
+                }
+
+                override fun onWriteFailure(exception: BleException?) {
+                    println("getTicketStatusPart fail")
+                }
+            })
+        } else {
+            BleUtil.instance?.getBleDeviceByMac(bleDevice.mac)?.let {
+                println("工作票完成接收 : ${String(it.ticketStatus)}")
+                // TODO 清空ticket
+//                it.ticket = byteArrayOf()
+            }
+        }
+    }
+
+    /**
+     * 获取工作票完成情况分包
+     */
+    private fun getTicketStatusPart(idx: ByteArray, total: ByteArray, res: ByteArray, bleDevice: BleDevice, callback: CustomBleWriteCallback?) {
+        BleUtil.instance?.getBleDeviceByMac(bleDevice.mac)?.let {
+            BleUtil.instance?.write(it.bleDevice, cmd = assembleData(it, REQ_WORK_TICKET_RESULT_PART + idx + total + res), writeCallback = callback)
+        }
+    }
+
+    /**
+     * 获取钥匙电量
+     */
+    fun getPower(mac: String?, callback: CustomBleWriteCallback?) {
+        BleUtil.instance?.getBleDeviceByMac(mac)?.let {
+            BleUtil.instance?.write(it.bleDevice, cmd = assembleData(it, REQ_POWER_STATUS), writeCallback = callback)
+        }
+    }
+
+    /**
+     * 处理钥匙电量
+     * bat:电量百分比,范围 0-100,单位:%
+     * chg:0x01:未充电 0x02:充电中 0x03:充满
+     */
+    private fun handlePowerStatus(byteArray: ByteArray) {
+        LogUtil.i("handlePowerStatus : ${byteArray.toHexString()}")
+        val bat = byteArray[4].toInt()
+        val chg = byteArray[5]
+    }
+
+    /**
+     * 发送文件
+     * type: 1:固件文件 2:点位PNG文件
+     * FLNM:文件名(不含扩展名)
+     * FLSZ:文件大小(字节)
+     * FLCRC:文件的CRC-16
+     * PGTOTAL:文件总包数
+     * PGIDX:当前包idx
+     * PGCRC:当前包CRC-16
+     * PGSZ:当前包长度(字节)
+     * PGDATA:当前包数据
+     */
+    fun sendFile(type: Int, file: File, idx: Int = 0, mac: String?, callback: CustomBleWriteCallback?) {
+        Executor.runOnIO {
+            LogUtil.i("sendFile : $idx")
+
+            BleUtil.instance?.getBleDeviceByMac(mac)?.let {
+                it.fileSend = file
+            }
+
+            val pgtotal = (file.readBytes().size + 128 - 1) / 128
+            if (idx == pgtotal) {
+                LogUtil.i("Send finish")
+                return@runOnIO
+            }
+
+            val flnm = file.name.substringBeforeLast(".").toByteArray(8)
+            val flsz = file.readBytes().size.toByteArray(4)
+            val flcrc = file.readBytes().crc16()
+            val pgdata = if (idx == pgtotal - 1) {
+                file.readBytes().copyOfRange(idx * 128, file.readBytes().size - 1)
+            } else {
+                file.readBytes().copyOfRange(idx * 128, (idx + 1) * 128)
+            }
+            val pgsz = pgdata.size.toByteArray()
+            val pgcrc = pgdata.crc16()
+
+            val fileInfo = byteArrayOf(type.toByte()) + flnm + flsz + flcrc + pgtotal.toByteArray() + idx.toByteArray() + pgcrc + pgsz + pgdata
+//        println("______________________________________________________________________________")
+//        println("${file.readBytes().size}")
+//        println("${byteArrayOf(type.toByte()).size} : ${flnm.size} : ${flsz.size} : ${flcrc.size} : ${pgtotal.toByteArray().size} : ${idx.toByteArray().size} : ${pgcrc.size} : ${pgsz.size} : ${pgdata.size}")
+//        println("______________________________________________________________________________")
+//        println("file info size : ${fileInfo.size}")
+            val cmd = REQ_TRANSFER_FILE + (fileInfo.size + 1).toByteArray(1) + 0x01.toByteArray(1) + fileInfo
+//        println("cmd size : ${REQ_TRANSFER_FILE.size} : ${(fileInfo.size + 1).toByteArray(1).size} : ${0x01.toByteArray(1).size} : ${fileInfo.size}")
+
+            BleUtil.instance?.getBleDeviceByMac(mac)?.let {
+                BleUtil.instance?.write(it.bleDevice, writeUUID = WRITE_UUID, cmd = assembleData(it, cmd), writeCallback = callback)
+            }
+            Thread.sleep(50)
+            sendFile(type, file, idx + 1, mac, callback)
+        }
+    }
+
+    /**
+     * 处理发送分包文件响应
+     * type: 0x01:固件文件 0x02:点位PNG文件
+     */
+    private fun handleFileRsp(bleBean: BleBean, byteArray: ByteArray) {
+        LogUtil.i("handleFileRsp : ${byteArray.toHexString()}")
+        val type = byteArray[4]
+        val res = byteArray[17]
+        val total = byteArray[15] + byteArray[16]
+        val idx = byteArray[13] + byteArray[14]
+        if (idx != total - 1 && (res == 0x00.toByte() || res == 0x02.toByte())) {
+            // TODO 不用等回复再发
+//            sendFile(type.toInt(),
+//                BleUtil.instance?.getBleDeviceByMac(bleBean.bleDevice.mac)?.fileSend!!,
+//                if (res == 0x00.toByte()) idx + 1 else idx,
+//                bleBean.bleDevice.mac,
+//                object : CustomBleWriteCallback() {
+//                    override fun onPrompt(promptStr: String?) {}
+//
+//                    override fun onConnectPrompt(promptStr: String?) {}
+//
+//                    override fun onDisConnectPrompt(promptStr: String?) {}
+//
+//                    override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {}
+//
+//                    override fun onWriteFailure(exception: BleException?) {}
+//
+//                })
+        }
+    }
+
+    /**
+     * 获取版本
+     */
+    fun getVersion(mac: String?, callback: CustomBleWriteCallback?) {
+        BleUtil.instance?.getBleDeviceByMac(mac)?.let {
+            BleUtil.instance?.write(it.bleDevice, cmd = assembleData(it, REQ_GET_VERSION), writeCallback = callback)
+        }
+    }
+
+    /**
+     * 处理软件/硬件版本
+     */
+    private fun handleVersion(byteArray: ByteArray) {
+        val sofVersion = parseVersion(byteArray[4])
+        val hardVersion = parseVersion(byteArray[5])
+        LogUtil.i("$sofVersion : $hardVersion")
+    }
+
+    /**
+     * 版本解析
+     */
+    private fun parseVersion(byte: Byte): String {
+        // 将 Byte 转换为 Int 以便更容易进行位操作
+        val intValue = byte.toInt()
+        // 提取高 4 位作为主版本号
+        val majorVersion = (intValue and 0xF0) ushr 4
+        // 提取低 4 位作为次版本号
+        val minorVersion = intValue and 0x0F
+
+        return "V$majorVersion.$minorVersion"
+    }
+}

+ 59 - 0
app/src/main/java/com/example/whatever/ble/BleConst.kt

@@ -0,0 +1,59 @@
+package com.example.whatever.ble
+
+/**
+ * 指令,均为未加密或解密后的
+ */
+object BleConst {
+
+    const val MTU = 500
+
+    const val SERVICE_UUID = "0000FEE7-0000-1000-8000-00805F9B34FB"
+    const val INDICATE_UUID = "0000FED1-0000-1000-8000-00805F9B34FB"
+    const val WRITE_UUID = "0000FED2-0000-1000-8000-00805F9B34FB"
+
+    /**
+     * byteArrayOf不可变,可以使用 mutableListOf 来创建一个可变的列表,然后使用 toByteArray 方法
+     * byteArray也有toList()方法
+     */
+    // 获取令牌,需增加4字节的时间戳,总长8个字节长度
+    val REQ_GET_TOKEN = byteArrayOf(0x01, 0x01, 0x05, 0x00)
+    // 获取令牌响应,最后4个是token,总长15个字节长度
+    val RSP_GET_TOKEN = byteArrayOf(0x01, 0x02, 0x04)
+
+    // 设备工作模式切换
+    val REQ_SWITCH_MODE = byteArrayOf(0x02, 0x01, 0x02, 0x01)
+    // 工作模式切换响应
+    val RSP_SWITCH_MODE = byteArrayOf(0x02, 0x02, 0x03, 0x01)
+
+    // 工作票下发
+    val REQ_SEND_WORK_TICKET = byteArrayOf(0x02, 0x01)
+    // 工作票下发响应
+    val RSP_SEND_WORK_TICKET = byteArrayOf(0x02, 0x02, 0x06, 0x02)
+
+    // 获取设备当前状态
+    val REQ_CURRENT_STATUS = byteArrayOf(0x03, 0x01, 0x01, 0x01)
+    // 获取当前设备响应
+    val RSP_CURRENT_STATUS = byteArrayOf(0x03, 0x02, 0x02, 0x01)
+
+    // 获取设备工作票完成情况
+    val REQ_WORK_TICKET_RESULT = byteArrayOf(0x03, 0x01, 0x01, 0x02)
+    // 获取设备工作票完成情况响应
+    val RSP_WORK_TICKET_RESULT = byteArrayOf(0x03, 0x02)
+    // 获取设备工作票完成情况分包
+    val REQ_WORK_TICKET_RESULT_PART = byteArrayOf(0x03, 0x01, 0x06, 0x02)
+
+    // 获取钥匙电量
+    val REQ_POWER_STATUS = byteArrayOf(0x03, 0x01, 0x01, 0x03)
+    // 获取钥匙电量响应
+    val RSP_POWER_STATUS = byteArrayOf(0x03, 0x02, 0x03, 0x03)
+
+    // 传输文件
+    val REQ_TRANSFER_FILE = byteArrayOf(0x06, 0x01)
+    // 传输文件完成响应
+    val RSP_TRANSFER_FILE = byteArrayOf(0x06, 0x02)
+
+    // 获取固件版本号
+    val REQ_GET_VERSION = byteArrayOf(0xEE.toByte(), 0x01, 0x02, 0x01, 0x01)
+    // 获取固件版本号响应
+    val RSP_GET_VERSION = byteArrayOf(0xEE.toByte(), 0x02, 0x03, 0x01)
+}

+ 194 - 0
app/src/main/java/com/example/whatever/ble/BleUtil.kt

@@ -0,0 +1,194 @@
+package com.example.whatever.ble
+
+import android.app.Application
+import android.bluetooth.BluetoothGatt
+import android.os.Build
+import android.util.Log
+import com.clj.fastble.BleManager
+import com.clj.fastble.callback.BleGattCallback
+import com.clj.fastble.callback.BleMtuChangedCallback
+import com.clj.fastble.data.BleDevice
+import com.clj.fastble.exception.BleException
+import com.clj.fastble.scan.BleScanRuleConfig
+import com.example.whatever.CustomBleGattCallback
+import com.example.whatever.CustomBleIndicateCallback
+import com.example.whatever.CustomBleScanCallback
+import com.example.whatever.CustomBleWriteCallback
+import com.example.whatever.model.BleBean
+import com.example.whatever.model.Constants.BLE_LOCAL_NAME
+import com.example.whatever.ble.BleConst.INDICATE_UUID
+import com.example.whatever.ble.BleConst.MTU
+import com.example.whatever.ble.BleConst.SERVICE_UUID
+import com.example.whatever.ble.BleConst.WRITE_UUID
+
+/**
+ * 蓝牙工具类
+ */
+class BleUtil private constructor() {
+
+    // 已连接的设备集合(TODO 现在的和界面共用,是所有的,要改掉)
+    var deviceList: MutableList<BleBean> = mutableListOf()
+
+    companion object {
+        var instance: BleUtil? = null
+            get() {
+                if (field == null) field = BleUtil()
+                return field
+            }
+            private set
+    }
+
+    fun initBle(application: Application?) {
+        try {
+            BleManager.getInstance().init(application)
+            BleManager.getInstance().enableLog(false)
+                .setConnectOverTime(10 * 1000L)
+                .setReConnectCount(3, 300) // 设置重新连接次数和间隔时间,默认为0次,不重连
+                .setSplitWriteNum(500)
+                .operateTimeout = 60 * 60 * 1000 // 设置操作readRssi、setMtu、write、read、notify、indicate的超时时间(毫秒)
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+                //Android 12及以上不允许添加过滤器
+                val bleScanRuleConfig = BleScanRuleConfig.Builder()
+//                .setServiceUuids(arrayOf(UUID.fromString(BLUETOOTH_SERVICEUUID)))
+                    .setDeviceName(true, BLE_LOCAL_NAME)
+                    .build()
+                BleManager.getInstance().initScanRule(bleScanRuleConfig)
+            }
+        } catch (e: Exception) {
+            Log.d("initBlueTooth", "蓝牙初始化:${e.message}")
+        }
+    }
+
+    fun getBleDeviceByMac(mac: String?): BleBean? {
+        return deviceList.stream().filter { it.bleDevice.mac == mac }?.findFirst()?.get()
+    }
+
+    fun scan(bleScanCallback: CustomBleScanCallback) {
+        if (BleManager.getInstance().isSupportBle) {
+            if (BleManager.getInstance().isBlueEnable) {
+                BleManager.getInstance().scan(bleScanCallback)
+            } else {
+                bleScanCallback.onPrompt("请打开您的蓝牙后重试")
+            }
+        } else {
+            bleScanCallback.onPrompt("您的设备不支持蓝牙设备")
+        }
+    }
+
+    fun connectBySelect(bleDevice: BleDevice?, bleGattCallback: CustomBleGattCallback) {
+        if (BleManager.getInstance().isSupportBle) {
+            if (BleManager.getInstance().isBlueEnable) {
+                BleManager.getInstance().connect(bleDevice, bleGattCallback)
+            } else {
+                bleGattCallback.onPrompt("请打开您的蓝牙后重试")
+            }
+        } else {
+            bleGattCallback.onPrompt("您的设备不支持蓝牙设备")
+        }
+    }
+
+    fun connectByMac(mac: String?, bleGattCallback: CustomBleGattCallback) {
+        if (BleManager.getInstance().isSupportBle) {
+            if (BleManager.getInstance().isBlueEnable) {
+                BleManager.getInstance().connect(mac, bleGattCallback)
+            } else {
+                bleGattCallback.onPrompt("请打开您的蓝牙后重试")
+            }
+        } else {
+            bleGattCallback.onPrompt("您的设备不支持蓝牙设备")
+        }
+    }
+
+    fun setMtu(bleDevice: BleDevice) {
+        BleManager.getInstance().setMtu(bleDevice, MTU, object : BleMtuChangedCallback() {
+            override fun onSetMTUFailure(exception: BleException?) {
+//                indicate()
+            }
+
+            override fun onMtuChanged(mtu: Int) {
+//                indicate()
+            }
+        })
+    }
+
+    fun indicate(bleDevice: BleDevice,
+                 serviceUUID: String = SERVICE_UUID,
+                 indicateUUID: String = INDICATE_UUID,
+                 isStart: Boolean = true,
+                 indicateCallback: CustomBleIndicateCallback?) {
+        BleManager.getInstance().removeIndicateCallback(bleDevice, indicateUUID)
+        BleManager.getInstance().stopIndicate(bleDevice, serviceUUID, indicateUUID)
+        if (!isStart) {
+            return
+        }
+        if (!BleManager.getInstance().isSupportBle) {
+            indicateCallback?.onPrompt("该设备不支持蓝牙BLE")
+            return
+        }
+
+        if (!BleManager.getInstance().isBlueEnable) {
+            indicateCallback?.onPrompt("蓝牙已关闭,请打开蓝牙后重试")
+            return
+        }
+        if (BleManager.getInstance().isConnected(bleDevice.mac)) {
+            BleManager.getInstance().indicate(bleDevice, serviceUUID, indicateUUID, indicateCallback)
+        } else {
+            BleManager.getInstance().connect(bleDevice.mac, object : BleGattCallback() {
+                override fun onStartConnect() {}
+                override fun onConnectFail(bleDevice: BleDevice, exception: BleException) {
+                    BleManager.getInstance().removeConnectGattCallback(bleDevice)
+                    indicateCallback?.onConnectPrompt("连接失败!请检查设备是否打开,并尝试重新连接 : $exception")
+                }
+
+                override fun onConnectSuccess(bleDevice: BleDevice, gatt: BluetoothGatt, status: Int) {
+                    BleManager.getInstance().removeConnectGattCallback(bleDevice)
+                    BleManager.getInstance().indicate(bleDevice, serviceUUID, indicateUUID, indicateCallback)
+                }
+
+                override fun onDisConnected(isActiveDisConnected: Boolean, device: BleDevice, gatt: BluetoothGatt, status: Int) {
+                    BleManager.getInstance().removeConnectGattCallback(device)
+                    indicateCallback?.onDisConnectPrompt("连接断开!请检查设备状态,并尝试重新连接!")
+                }
+            })
+        }
+    }
+
+
+    fun write(bleDevice: BleDevice,
+              serviceUUID: String = SERVICE_UUID,
+              writeUUID: String = INDICATE_UUID,
+              cmd: ByteArray?,
+              writeCallback: CustomBleWriteCallback?
+    ) {
+        if (!BleManager.getInstance().isSupportBle) {
+            writeCallback?.onPrompt("该设备不支持蓝牙BLE")
+            return
+        }
+
+        if (!BleManager.getInstance().isBlueEnable) {
+            writeCallback?.onPrompt("蓝牙已关闭,请打开蓝牙后重试")
+            return
+        }
+        if (BleManager.getInstance().isConnected(bleDevice.mac)) {
+            BleManager.getInstance().write(bleDevice, serviceUUID, writeUUID, cmd, writeCallback)
+        } else {
+            BleManager.getInstance().connect(bleDevice.mac, object : BleGattCallback() {
+                override fun onStartConnect() {}
+                override fun onConnectFail(bleDevice: BleDevice, exception: BleException) {
+                    BleManager.getInstance().removeConnectGattCallback(bleDevice)
+                    writeCallback?.onConnectPrompt("连接失败!请检查设备是否打开,并尝试重新连接 : $exception")
+                }
+
+                override fun onConnectSuccess(bleDevice: BleDevice, gatt: BluetoothGatt, status: Int) {
+                    BleManager.getInstance().removeConnectGattCallback(bleDevice)
+                    BleManager.getInstance().write(bleDevice, serviceUUID, writeUUID, cmd, writeCallback)
+                }
+
+                override fun onDisConnected(isActiveDisConnected: Boolean, device: BleDevice, gatt: BluetoothGatt, status: Int) {
+                    BleManager.getInstance().removeConnectGattCallback(device)
+                    writeCallback?.onDisConnectPrompt("连接断开!请检查设备状态,并尝试重新连接!")
+                }
+            })
+        }
+    }
+}

+ 50 - 0
app/src/main/java/com/example/whatever/ble/KeyGenerator.java

@@ -0,0 +1,50 @@
+package com.example.whatever.ble;
+
+/**
+ * 蓝牙 章节 4.1
+ */
+public class KeyGenerator {
+    private static final int[] PASSWORD_ARRAY = {
+            0x1D83, 0x0E52, 0xAF42, 0x3072, 0x7D8C, 0xC0EC, 0xBD93, 0x3258,
+            0x95DD, 0x5D28, 0xCE35, 0x59AA, 0xDEC0, 0xCFD1, 0x2F36, 0x8DAA,
+            0xC910, 0x5DE8, 0x0AC6, 0x8C46, 0xD98B, 0x33D2, 0x3C43, 0x12E0,
+            0x25FC, 0xCC82, 0x1158, 0xB585, 0x6616, 0x621D, 0xF02F, 0xA136,
+            0x3614, 0x3C5C, 0x68DF, 0x5C26, 0x9ACC, 0x5D80, 0xEB45, 0x2AC8,
+            0xD1FC, 0x992D, 0x8DC5, 0x91F0, 0x70F5, 0x5C32, 0xC8BD, 0xE750,
+            0x69FC, 0x67BD, 0x89FB, 0xD298, 0x5CC2, 0xBE27, 0x5C5C, 0xAE6B,
+            0x03F5, 0x7C23, 0xB6AA, 0x0999, 0x6D5E, 0x5F89, 0x2A1C, 0xD3F7
+    };
+
+    public static byte[] generateKey(byte[] macAddress, long token) {
+        byte[] key = new byte[16];
+
+        // Step 1: Calculate remainder of each byte of the MAC address with 64.
+        int[] remainders = new int[6];
+        for (int i = 0; i < 6; i++) {
+            remainders[i] = macAddress[i] & 0xFF;
+            remainders[i] %= 64;
+        }
+
+        // Step 2: Use each result as index to fetch 6 values from PASSWORD_ARRAY.
+        for (int i = 0; i < 6; i++) {
+            System.arraycopy(intToBytes(PASSWORD_ARRAY[remainders[i]]), 2, key, i * 2, 2);
+        }
+
+        // Steps 3-6: Extract bytes from token and place them in the key array.
+        key[12] = (byte)((token >> 24) & 0xFF);
+        key[13] = (byte)((token >> 16) & 0xFF);
+        key[14] = (byte)((token >> 8) & 0xFF);
+        key[15] = (byte)(token & 0xFF);
+
+        return key;
+    }
+
+    private static byte[] intToBytes(int value) {
+        return new byte[]{
+                (byte) (value >>> 24),
+                (byte) (value >>> 16),
+                (byte) (value >>> 8),
+                (byte) value
+        };
+    }
+}

+ 97 - 0
app/src/main/java/com/example/whatever/extentions/ByteArray.kt

@@ -0,0 +1,97 @@
+package com.example.whatever.extentions
+
+import android.util.Base64
+import com.example.whatever.util.CRC16
+import java.io.ByteArrayOutputStream
+import kotlin.experimental.xor
+
+fun ByteArray.startsWith(prefix: ByteArray): Boolean {
+    require(this.size >= prefix.size) { "ByteArray is smaller than the prefix." }
+
+    for (i in prefix.indices) {
+        if (this[i] != prefix[i]) {
+            return false
+        }
+    }
+    return true
+}
+
+fun ByteArray.base64() : String {
+    return Base64.encodeToString(this, Base64.NO_WRAP)
+}
+
+private val hexEncodingTable = byteArrayOf(
+    '0'.toByte(), '1'.toByte(), '2'.toByte(), '3'.toByte(), '4'.toByte(), '5'.toByte(), '6'.toByte(), '7'.toByte(),
+    '8'.toByte(), '9'.toByte(), 'A'.toByte(), 'B'.toByte(), 'C'.toByte(), 'D'.toByte(), 'E'.toByte(), 'F'.toByte()
+)
+
+fun ByteArray.toHexString(space: Boolean = true) : String {
+    val out = ByteArrayOutputStream()
+    try {
+        for (i in 0 until size) {
+            val v: Int = this[i].toInt() and 0xff
+            out.write(hexEncodingTable[v ushr 4].toInt())
+            out.write(hexEncodingTable[v and 0xf].toInt())
+            if (space) {
+                out.write(' '.toInt())
+            }
+        }
+    } catch (e: java.lang.Exception) {
+        throw IllegalStateException("exception encoding Hex string: " + e.message, e)
+    }
+    val bytes = out.toByteArray()
+    return String(bytes)
+}
+
+const val RADIX_62 = 62
+
+/**
+ * 把[from, to) 部分的子数组,转换为以 0-9 a-z A-Z 共 62个字符组成的数字
+ *
+ * @param from 起始
+ * @param to 结束(不包含)
+ */
+fun ByteArray.to62Num(from: Int, to: Int) : Int {
+    var sum = 0
+    var scale = 1
+    for (i in to - 1 downTo from) {
+        sum += from62(this[i]) * scale
+        scale *= RADIX_62
+    }
+    return sum
+}
+
+/**
+ * 把[from, to) 部分的字节做异或
+ */
+fun ByteArray.xor(from: Int, to: Int) : Byte {
+    var res = this[from]
+    for (i in (from + 1) until to) {
+        res = res xor this[i]
+    }
+    return res
+}
+
+private fun from62(b: Byte): Int {
+    if (b in 0x30..0x39) {
+        return b - 0x30
+    }
+    if (b in 0x61..0x7a) {
+        return b - 0x57
+    }
+    if (b in 0x41..0x5a) {
+        return b - 0x1d
+    }
+    throw IllegalArgumentException("$b is not a num char")
+}
+
+/**
+ * 计算 [from, to) 部分的字节做 的 CRC16 校验值
+ * @return 两字节的校验值
+ */
+fun ByteArray.crc16(from: Int = 0, to: Int = size) : ByteArray {
+    val value = CRC16.crc16(this, from.coerceAtLeast(0), to.coerceAtMost(size))
+    val c1 = (0xff00 and value shr 8).toByte()
+    val c2 = (0xff and value).toByte()
+    return byteArrayOf(c1, c2)
+}

+ 18 - 0
app/src/main/java/com/example/whatever/extentions/Int.kt

@@ -0,0 +1,18 @@
+package com.example.whatever.extentions
+
+//fun Int.toByteArray(capability: Int = 2): ByteArray {
+//    return ByteBuffer.allocate(capability)
+//        .order(ByteOrder.BIG_ENDIAN) // 可以根据需要改为 ByteOrder.LITTLE_ENDIAN
+//        .putShort(this.toShort()) // 只取低16位
+//        .array()
+//}
+
+fun Int.toByteArray(capability: Int = 2): ByteArray {
+//    require(capability in 1..4) { "Length must be between 1 and 4" }
+    val bytes = ByteArray(capability)
+    for (i in 0 until capability) {
+//        bytes[capability - i - 1] = ((this ushr (i * 8)) and 0xFF).toByte() // 大端模式
+        bytes[i] = ((this ushr (i * 8)) and 0xFF).toByte()  // 小端模式
+    }
+    return bytes
+}

+ 10 - 0
app/src/main/java/com/example/whatever/extentions/Long.kt

@@ -0,0 +1,10 @@
+package com.example.whatever.extentions
+
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+
+fun Long.toByteArray(): ByteArray {
+    return ByteBuffer.allocate(java.lang.Long.BYTES)
+        .order(ByteOrder.LITTLE_ENDIAN)
+        .putLong(this).array()
+}

+ 19 - 0
app/src/main/java/com/example/whatever/extentions/String.kt

@@ -0,0 +1,19 @@
+package com.example.whatever.extentions
+
+/**
+ * 长度不足默认补0
+ */
+fun String.toByteArray(length: Int, paddingByte: Byte = 0): ByteArray {
+    val bytes = this.toByteArray(Charsets.UTF_8)
+    return if (bytes.size >= length) {
+        bytes.copyOf(length)
+    } else {
+        ByteArray(length) { index ->
+            if (index < bytes.size) {
+                bytes[index]
+            } else {
+                paddingByte
+            }
+        }
+    }
+}

+ 493 - 0
app/src/main/java/com/example/whatever/modbus/ModBusController.kt

@@ -0,0 +1,493 @@
+package com.fenbide.utilities.peripheral
+
+import android.content.Context
+import com.example.whatever.modbus.FRAME_TYPE_WRITE_FILE
+import com.example.whatever.modbus.FRAME_TYPE_WRITE_MULTI
+import com.example.whatever.modbus.MBFrame
+import com.example.whatever.modbus.MODBUS_SLAVE_STATUS__FULL
+import com.example.whatever.modbus.MODBUS_SLAVE_STATUS__NO_CHANGE
+import com.example.whatever.modbus.ModBusManager
+import com.example.whatever.modbus.PortManager
+import com.example.whatever.util.Executor
+import com.fenbide.utilities.*
+import java.io.File
+import java.math.BigInteger
+import java.util.*
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.math.ceil
+
+
+/**
+ * ModBus 主控板控制器
+ */
+object ModBusController {
+
+    private const val LISTENER_TYPE_BOTTLE = 1
+    private const val LISTENER_TYPE_WEIGHT = 2
+    private const val LISTENER_TYPE_STATUS = 3
+    private const val LISTENER_TYPE_TEMPERA = 4
+
+    // 主控板管理器
+    private var modBusManager: ModBusManager? = null
+    private var slaveCount: Int = 0
+
+    private val threadPool = Executors.newScheduledThreadPool(4)
+
+    private val listeners = ArrayList<StatusListener>()
+
+    private val fullPercentListeners = ArrayList<FullPercentListener>()
+
+    // 是否中断读取桶状态
+    private var interruptReadTrashBinStatus: ArrayList<Boolean> = ArrayList()
+
+    var shouldStopUpgrade = false
+
+    fun setSlaveCount(count: Int) {
+        modBusManager?.slaveCount = count
+        slaveCount = count
+    }
+
+    data class FullPercent(val percent: Int, val checkByLaser: Boolean)
+
+    class FullPercentListener(
+        val key: Any,
+        val listener: (List<FullPercent>) -> Unit
+    )
+
+    class StatusListener(
+        val key: Any,
+        val listener: (List<Int>) -> Unit,
+        val type: Int
+    )
+
+    fun interruptReadTrashBinStatus(interrupt: Boolean) {
+        interruptReadTrashBinStatus.clear()
+        interruptReadTrashBinStatus.add(interrupt)
+    }
+
+    @ExperimentalUnsignedTypes
+    fun start(ctx: Context) {
+        modBusManager?.stop()
+        PortManager.openCtrlBord(ctx)
+            ?.let { pm ->
+                return@let ModBusManager(slaveCount, pm, true)
+            }
+            // 间隔 1 秒读一遍桶的状态
+            ?.repeatSendToAll(MBFrame.READ_STATUS, {
+                interruptReadTrashBinStatus
+            }, { res ->
+//                // Logger.d("ModbusController", "res: ${res.map { it.toHexString() }}")
+                // 第 0 个字节是 从机地址,第 1 个字节是 功能码,第 2 个字节是 字节数
+                // 瓶类计数
+                val bottles = res.map { bytes ->
+                    if (bytes.size > 4) bytes[4].toUByte()
+                        .toInt() else MODBUS_SLAVE_STATUS__NO_CHANGE
+                }
+                // 桶的重量
+                val weights = res.map { bytes ->
+                    if (bytes.size > 6) (bytes[5].toUByte() * 256u + bytes[6].toUByte()).toInt() else MODBUS_SLAVE_STATUS__NO_CHANGE
+                }.map {
+                    if (it > 32767) 0 else it     // 大于 32767 的表示负数,作 0 处理
+                }
+                // 其它状态
+                val status = res.map { bytes ->
+                    if (bytes.size > 8) (bytes[7].toUByte() * 256u + bytes[8].toUByte()).toInt() else MODBUS_SLAVE_STATUS__NO_CHANGE
+                }
+                // 箱体温度
+                val temperatures = res.map { bytes ->
+                    if (bytes.size > 10) {
+//                            // Logger.d("ModBus", "桶状态: ${bytes.toHexString()}")
+                        (bytes[9].toUByte() * 256u + bytes[10].toUByte()).toInt()
+                    } else MODBUS_SLAVE_STATUS__NO_CHANGE
+                }
+                for (l in listeners) {
+                    if (l.type == LISTENER_TYPE_BOTTLE) {
+                        l.listener(bottles)
+                    }
+                    if (l.type == LISTENER_TYPE_WEIGHT) {
+                        l.listener(weights)
+                    }
+                    if (l.type == LISTENER_TYPE_STATUS) {
+                        l.listener(status)
+                    }
+                    if (l.type == LISTENER_TYPE_TEMPERA) {
+                        l.listener(temperatures)
+                    }
+                }
+                if (fullPercentListeners.isNotEmpty()) {
+                    // 满溢程度
+                    val fullPercents = res.mapIndexed { index, bytes ->
+                        var percent = MODBUS_SLAVE_STATUS__NO_CHANGE
+                        var checkByLaser = false
+                        if (bytes.size > 3) {
+                            if (percent < 0 || percent >= 255) {
+                                checkByLaser = false
+                                percent = if (status[index] and MODBUS_SLAVE_STATUS__FULL > 0) {
+                                    100
+                                } else {
+                                    0
+                                }
+                            } else {
+                                checkByLaser = true
+                                percent = (bytes[3].toUByte().toInt())
+                            }
+                        }
+                        FullPercent(percent, checkByLaser)
+                    }
+                    for (l in fullPercentListeners) {
+                        l.listener(fullPercents)
+                    }
+                }
+            }, 1000)
+            ?.also {
+                modBusManager = it
+                Executor.runOnIO {
+//                    refreshAllowOpenDoorUnidentified(ctx, it)
+                }
+            }
+            ?.start()
+    }
+
+
+
+    /**
+     * 满溢百分比(0-100, -1表示无效值)
+     */
+    fun registerFullPercentListener(key: Any, listener: (List<FullPercent>) -> Unit) {
+        fullPercentListeners.add(FullPercentListener(key, listener))
+    }
+
+    fun registerBottleListener(key: Any, listener: (List<Int>) -> Unit) {
+        listeners.add(StatusListener(key, listener, LISTENER_TYPE_BOTTLE))
+    }
+
+    /**
+     * 回调的重量单位:0.01kg、10g
+     */
+    fun registerWeightListener(key: Any, listener: (List<Int>) -> Unit) {
+        listeners.add(StatusListener(key, listener, LISTENER_TYPE_WEIGHT))
+    }
+
+    fun registerStatusListener(key: Any, listener: (List<Int>) -> Unit) {
+        listeners.add(StatusListener(key, listener, LISTENER_TYPE_STATUS))
+    }
+
+    fun registerTemperaListener(key: Any, listener: (List<Int>) -> Unit) {
+        listeners.add(StatusListener(key, listener, LISTENER_TYPE_TEMPERA))
+    }
+
+    fun unregisterListener(key: Any) {
+        val it = listeners.iterator()
+        while (it.hasNext()) {
+            if (it.next().key == key) {
+                it.remove()
+            }
+        }
+    }
+
+    /**
+     * 设置垃圾桶类别--智能厨余
+     */
+    fun setTrashBinTypeIntelliKitchen(index: Int) {
+        modBusManager?.sendTo(index, MBFrame.WRITE_TRASH_BIN_TYPE_INTELLI_KITCHEN) {
+            if (it.isNotEmpty()) {
+//                // Logger.d("AI_SETTINGS", "设置垃圾桶[${index}]类别--智能厨余--成功")
+            } else {
+                // // Logger.d("AI_SETTINGS", "设置垃圾桶[${index}]类别--智能厨余--失败")
+            }
+        }
+    }
+
+    /**
+     * 设置氨气浓度报警值
+     */
+    fun writeNh3AlarmValue(index: Int, value: Int) {
+        modBusManager?.sendTo(index, MBFrame.writeNh3AlarmValue(value)) {
+            if (it.isNotEmpty()) {
+                // // Logger.d("AI_SETTINGS", "设置氨气浓度报警值: ${value}--成功")
+            } else {
+                // // Logger.d("AI_SETTINGS", "设置氨气浓度报警值: ${value}--失败")
+            }
+        }
+    }
+
+    /**
+     * 设置硫化氢浓度报警值
+     */
+    fun writeH2sAlarmValue(index: Int, value: Int) {
+        modBusManager?.sendTo(index, MBFrame.writeH2sAlarmValue(value)) {
+            if (it.isNotEmpty()) {
+                // // Logger.d("AI_SETTINGS", "设置硫化氢浓度报警值: ${value}--成功")
+            } else {
+                // // Logger.d("AI_SETTINGS", "设置硫化氢浓度报警值: ${value}--失败")
+            }
+        }
+    }
+
+    /**
+     * 设置新风系统强风时长
+     */
+    fun writeFierceWindSeconds(index: Int, value: Int) {
+        modBusManager?.sendTo(index, MBFrame.writeFierceWindSeconds(value)) {
+            if (it.isNotEmpty()) {
+                // // Logger.d("AI_SETTINGS", "设置新风系统强风时长: ${value}--成功")
+            } else {
+                // // Logger.d("AI_SETTINGS", "设置新风系统强风时长: ${value}--失败")
+            }
+        }
+    }
+
+    /**
+     * 设置新风系统弱风时长
+     */
+    fun writeWeakWindSeconds(index: Int, value: Int) {
+        modBusManager?.sendTo(index, MBFrame.writeWeakWindSeconds(value)) {
+            if (it.isNotEmpty()) {
+                // // Logger.d("AI_SETTINGS", "设置新风系统弱风时长: ${value}--成功")
+            } else {
+                // // Logger.d("AI_SETTINGS", "设置新风系统弱风时长: ${value}--失败")
+            }
+        }
+    }
+
+    /**
+     * 设置消毒喷雾间隔时长
+     */
+    fun writeSprayIntervalSeconds(index: Int, value: Int) {
+        modBusManager?.sendTo(index, MBFrame.writeSprayIntervalSeconds(value)) {
+            if (it.isNotEmpty()) {
+                // // Logger.d("AI_SETTINGS", "设置消毒喷雾间隔时长: ${value}--成功")
+            } else {
+                // // Logger.d("AI_SETTINGS", "设置消毒喷雾间隔时长: ${value}--失败")
+            }
+        }
+    }
+
+    /**
+     * 设置消毒喷雾单次时长
+     */
+    fun writeSprayRunningSeconds(index: Int, value: Int) {
+        modBusManager?.sendTo(index, MBFrame.writeSprayRunningSeconds(value)) {
+            if (it.isNotEmpty()) {
+                // // Logger.d("AI_SETTINGS", "设置消毒喷雾单次时长: ${value}--成功")
+            } else {
+                // // Logger.d("AI_SETTINGS", "设置消毒喷雾单次时长: ${value}--失败")
+            }
+        }
+    }
+
+    /**
+     * 校准称重传感器
+     */
+    fun calibrateWeigher(index: Int, v0: Int, v10: Int, callback: (success: Boolean) -> Unit) {
+        val v0HH = (v0 shr 24).toByte()
+        val v0HL = (v0 shr 16).toByte()
+        val v0LH = (v0 shr 8).toByte()
+        val v0LL = (v0 % 256).toByte()
+        val v10HH = (v10 shr 24).toByte()
+        val v10HL = (v10 shr 16).toByte()
+        val v10LH = (v10 shr 8).toByte()
+        val v10LL = (v10 % 256).toByte()
+        val frame = MBFrame(
+            FRAME_TYPE_WRITE_MULTI,
+            byteArrayOf(
+                0x01, 0x02, 0x00, 0x04, 0x08,
+                v0HH, v0HL, v0LH, v0LL,
+                v10HH, v10HL, v10LH, v10LL
+            )
+        )
+        modBusManager?.sendTo(index, frame) { bytes ->
+            callback(bytes.isNotEmpty())
+        }
+    }
+
+    /**
+     * 打开清运门
+     */
+    fun openCleaningDoor(index: Int, onComplete: ((success: Boolean) -> Unit)? = null) {
+        // // Logger.d("CLEANING_DOOR", "下发清运门开启指令:${index}")
+        modBusManager?.sendTo(index, MBFrame.WRITE_CLEANING_DOOR_NON_OPENABLE) {
+            if (it.isEmpty()) {
+                // Tip.toast("电子锁[${index}]标记位重置失败!")
+                onComplete?.invoke(false)
+                return@sendTo
+            }
+            Executor.delayOnIO({
+                modBusManager?.sendTo(index, MBFrame.WRITE_CLEANING_DOOR_OPENABLE) { it2 ->
+                    if (it2.isEmpty()) {
+                        // Tip.toast("清运门[${index}]开启指令发送失败!")
+                    }
+                    onComplete?.invoke(it2.isNotEmpty())
+                }
+            }, 1000)
+        }
+    }
+
+
+    /**
+     * 启用瓶类计数
+     */
+    fun enableBottleCounter(
+        index: Int,
+        enable: Boolean = true,
+        callback: ((success: Boolean) -> Unit)? = null
+    ) {
+        val frame = if (enable) MBFrame.ENABLE_BOTTLE_COUNTER else MBFrame.DISABLE_BOTTLE_COUNTER
+        modBusManager?.sendTo(index, frame) {
+            if (it.isEmpty()) {
+                // Tip.toast("舱门[${index}]设置瓶类计数指令失败!")
+            }
+            callback?.invoke(it.isNotEmpty())
+        }
+    }
+
+    /**
+     * 开门
+     */
+    fun openDoor(index: Int) {
+        // Logger.d("TRASH_BIN", "下发开门指令:${index}")
+        modBusManager?.sendTo(index, MBFrame.WRITE_OPEN) {
+            if (it.isEmpty()) {
+                // Tip.toast("舱门[${index}]开启失败!")
+            }
+        }
+    }
+
+    /**
+     * 关门
+     */
+    fun closeDoor(index: Int) {
+        // Logger.d("TRASH_BIN", "下发关门指令:${index}")
+        modBusManager?.sendTo(index, MBFrame.WRITE_CLOSE) {
+            if (it.isEmpty()) {
+                // Tip.toast("舱门[${index}]关闭失败!")
+            }
+        }
+    }
+
+    /**
+     * 关托盘
+     */
+    fun closePallet(index: Int) {
+        // Logger.d("TRASH_BIN", "下发关托盘指令:${index}")
+        modBusManager?.sendTo(index, MBFrame.WRITE_PALLET_OFF) {
+            if (it.isEmpty()) {
+                // Tip.toast("托盘[${index}]关闭失败!")
+            }
+        }
+    }
+
+    /**
+     * 开托盘
+     */
+    fun openPallet(index: Int) {
+        // Logger.d("TRASH_BIN", "下发打开托盘指令:${index}")
+        modBusManager?.sendTo(index, MBFrame.WRITE_PALLET_ON) {
+            if (it.isEmpty()) {
+//                // Tip.toast("托盘[${index}]打开失败!")
+            }
+        }
+    }
+
+    /**
+     * 打开排风扇
+     * 目前用户拍照照明灯
+     */
+    fun openFun(index: Int) {
+        // Logger.d("TRASH_BIN", "下发打开排风扇指令:${index}")
+        modBusManager?.sendTo(index, MBFrame.WRITE_FAN_ON) {
+            if (it.isEmpty()) {
+            }
+        }
+    }
+
+    /**
+     * 打开排风扇
+     * 目前用户拍照照明灯
+     */
+    fun closeFun(index: Int) {
+        // Logger.d("TRASH_BIN", "下发关闭排风扇指令:${index}")
+        modBusManager?.sendTo(index, MBFrame.WRITE_FAN_OFF) {
+            if (it.isEmpty()) {
+            }
+        }
+    }
+
+//    /**
+//     * 关门
+//     */
+//    fun closeDoor(index: Int) {
+//        // Logger.d("TRASH_BIN", "下发关门指令:${index}")
+//        modBusManager?.sendTo(index, MBFrame.WRITE_CLOSE) {
+//            if (it.isEmpty()) {
+//                // Tip.toast("舱门[${index}]关闭失败!")
+//            }
+//        }
+//    }
+
+    /**
+     * 读取扫码头数据
+     * @param index 从机编号
+     */
+    fun readScannerData(index: Int, success: (ByteArray) -> Unit) {
+        modBusManager?.sendTo(index, MBFrame.READ_CANNER) {
+            if (it.isEmpty()) {
+                // Tip.toast("扫码数据读取失败!")
+                return@sendTo
+            }
+            val length = it[2].toInt()  // 扫码数据长度
+            if (length == 0) {
+                // Tip.toast("扫码数据长度为 0 !: " + it.toHexString())
+                return@sendTo
+            }
+            if (it.size != length + 5) {
+                // Tip.toast("扫码数据长度不匹配: " + it.toHexString())
+                return@sendTo
+            }
+            success(it.copyOfRange(3, 3 + length))
+        }
+    }
+
+    /**
+     * 读刷卡板
+     * @param done 完成回调
+     */
+    fun readCardBoardData(index: Int, success: (res: ByteArray) -> Unit) {
+        modBusManager?.sendTo(index, MBFrame.READ_CARD_BOARD) {
+            // Logger.d("CARD_BOARD", "刷卡板数据(原始):${it.toHexString()}")
+            if (it.isEmpty()) {
+                // Tip.toast("刷卡板数据读取失败!")
+                return@sendTo
+            }
+            val length = it[2].toInt()  // 扫码数据长度
+            if (length == 0) {
+//                // Tip.toast("刷卡板数据长度为 0 !: " + it.toHexString())
+                return@sendTo
+            }
+            if (it.size != length + 5) {
+                // Tip.toast("刷卡板数据长度不匹配: " + it.toHexString())
+                return@sendTo
+            }
+            success(it.copyOfRange(3, 3 + length))
+        }
+    }
+
+    /**
+     * 开关灯
+     */
+    fun turnLight(on: Boolean) {
+        if (on) {
+            modBusManager?.sendToAll(MBFrame.WRITE_LIGHT_ON)
+        } else {
+            modBusManager?.sendToAll(MBFrame.WRITE_LIGHT_OFF)
+        }
+    }
+
+    fun stop() {
+        modBusManager?.stop()
+    }
+
+}

+ 730 - 0
app/src/main/java/com/example/whatever/modbus/ModBusManager.kt

@@ -0,0 +1,730 @@
+package com.example.whatever.modbus
+
+import com.example.whatever.extentions.crc16
+import com.example.whatever.util.Executor
+import com.example.whatever.util.jvmSeconds
+import java.util.concurrent.LinkedBlockingQueue
+
+/**
+ * modbus 最小发送间隔(80豪秒)
+ */
+const val MODBUS_MIN_SEND_INTERVAL = 80_000_000
+
+/**
+ * 最大从机数量
+ */
+const val MODBUS_MAX_SLAVE_COUNT = 16
+
+/**
+ * 从机状态:未变化
+ */
+const val MODBUS_SLAVE_STATUS__NO_CHANGE = -1
+
+/**
+ * 从机状态:已经满溢
+ */
+const val MODBUS_SLAVE_STATUS__FULL = 0b00100000
+
+/**
+ * 从机状态:满溢百分比
+ */
+const val MODBUS_SLAVE_STATUS__FULL_PERCENT = 0b00100000
+
+/**
+ * 从机状态:烟雾报警
+ * */
+const val MODBUS_SLAVE_STATUS__SMOKE = 0b01000000
+
+/**
+ * 从机状态:是否已完全关闭
+ */
+const val MODBUS_SLAVE_STATUS__DOOR_CLOSED = 0b00000001
+
+/**
+ * 从机状态:是否已完全开启
+ */
+const val MODBUS_SLAVE_STATUS__DOOR_OPENED = 0b00000010
+
+/**
+ * 从机状态:开门按钮是否被按下
+ */
+const val MODBUS_SLAVE_STATUS__OPEN_BUTTON_PRESSED = 0b00010000
+
+/**
+ * 从机状态:是否有扫码头事件(刷卡|扫码)
+ */
+const val MODBUS_SLAVE_STATUS__SCANNER = 0b00001000
+
+/**
+ * 从机状态:托盘关到位
+ */
+const val MODBUS_SLAVE_STATUS__PALLET_CLOSED = 0b00001000
+
+/**
+ * 从机状态:托盘开到位
+ */
+const val MODBUS_SLAVE_STATUS__PALLET_OPENED = 0b00010000
+
+/**
+ * 从机状态:清运门状态
+ */
+const val  MODBUS_SLAVE_STATUS__CLEANING_DOOR_STATUS = 0b00100000_00000000
+
+/**
+ * 从机状态:是否有开门按钮事件
+ */
+const val MODBUS_SLAVE_STATUS__OPEN_DOOR_BY_PHYSICAL_BUTTON: Byte = 0b00010000
+
+/**
+ * 从机状态:是否允许无授权开门
+ */
+const val MODBUS_SLAVE_CTRL__ALLOW_OPEN_DOOR_UNIDENTIFIED: Byte = 0b01000000
+
+/**
+ * 从机状态:是否感应到人体
+ */
+const val MODBUS_SLAVE_STATUS__BODY_NEAR = 0b00000100
+
+
+/**
+ * ModBus 协议管理器
+ */
+class ModBusManager(
+    // 从机数量
+    var slaveCount: Int,
+    // 串口管理器
+    val portManager: PortManager,
+    // 是否输出详细信息
+    val verbose: Boolean = false
+) {
+
+    @Volatile
+    private var running = true
+
+    /**
+     * 正在发送的任务
+     */
+    @Volatile
+    private var sending: FrameTask? = null
+
+    /**
+     * 等待发送队列
+     */
+    private val pendings = LinkedBlockingQueue<FrameTask>()
+
+    /**
+     * 线程锁
+     */
+    private val lock = Any()
+
+
+    private var thread: Thread? = null
+
+
+    init {
+        portManager.listen { res ->
+            if (verbose) {
+//                Logger.i("ModBus", "接收:${res.toHexString()}")
+            }
+            synchronized(lock) {
+                sending?.run {
+                    if (match(res) && running) {
+                        done?.let { it(res) }
+                        sending = null
+                    } else {
+//                        Logger.i("ModBus", "响应: ${res.toHexString()}未匹配, running:${running}")
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 发送队列的消息
+     */
+    private fun takePendingToSend() {
+        if (sending == null) {
+            sending = pendings.take()
+        }
+        if (!running) {
+            return
+        }
+        sending?.run {
+            waitIfNecessary()
+        }
+        synchronized(lock) {
+            sending?.run {
+                if (shouldSend()) {
+                    if (portManager.send(req)) {
+                        afterSent()
+                        if (verbose) {
+//                            Logger.i("ModBus", "发送:${req.toHexString()}")
+                        }
+                    } else {
+//                        Tip.toast("无法与主控板通讯")
+                    }
+                } else {
+//                    Logger.i("ModBus", "未响应: ${req.toHexString()}")
+                    // 放弃处理,回调空数据
+                    done?.let { it(byteArrayOf()) }
+                    sending = null
+//                    onFrameTimeout()
+                }
+            }
+        }
+    }
+
+    private var timeouts = 0
+    private var lastTimeout = 0L
+
+    private fun onFrameTimeout() {
+        val now = jvmSeconds()
+        if (now - lastTimeout > 10) {
+            timeouts = 0
+        }
+        // 如果连续超时达到 15 次,则重建 Modbus 连接
+        if (++timeouts > 15) {
+//            EventBus.getDefault().post(ConfigEvent())
+        }
+        lastTimeout = now
+    }
+
+    /**
+     * 循环发送给所有从机
+     * @param frame 发送报文
+     * @param listener 每轮发送完后的数据监听
+     * @param delayMills 每轮发送的间隔
+     */
+    fun repeatSendToAll(
+        frame: MBFrame,
+        interrupt: (() -> List<Boolean>)? = null,
+        listener: (res: List<ByteArray>) -> Unit,
+        delayMills: Long
+    ): ModBusManager {
+        val keep = interrupt?.invoke()?.run { !this[0] } ?: false
+        if (keep) {
+            sendToAll(frame) {
+                if (running) {
+                    listener(it)
+                    Executor.delayOnIO({
+                        if (running) {
+                            repeatSendToAll(frame, interrupt, listener, delayMills)
+                        }
+                    }, delayMills)
+                }
+            }
+        } else {
+            Executor.delayOnIO({
+                if (running) {
+                    repeatSendToAll(frame, interrupt, listener, delayMills)
+                }
+            }, delayMills)
+        }
+        return this
+    }
+
+
+    /**
+     * 发送给所有从机
+     * @param frame 发送报文
+     * @param done 所有从机都发送完成后的回调
+     */
+    fun sendToAll(frame: MBFrame, done: ((res: List<ByteArray>) -> Unit)? = null) {
+        if (slaveCount == 0) {
+            done?.let { it(listOf()) }
+            return
+        }
+        sendUp(0, frame, done, ArrayList())
+    }
+
+    private fun sendUp(
+        index: Int,
+        frame: MBFrame,
+        done: ((res: List<ByteArray>) -> Unit)?,
+        resList: ArrayList<ByteArray>
+    ) {
+        sendTo(index, frame) { res ->
+            resList.add(res)
+            if (index >= slaveCount - 1) {
+                // 已经发送完
+                if (running) {
+                    done?.let { it(resList) }
+                }
+            } else {
+                // 发送给下一个从机
+                sendUp(index + 1, frame, done, resList)
+            }
+        }
+    }
+
+    var machineType: String? = null
+
+    /**
+     * 发送给序号为 index 的从机
+     * @param index 从机序号
+     * @param frame 发送报文
+     * @param done 完成回调
+     */
+    fun sendTo(
+        index: Int,
+        frame: MBFrame,
+        allowRetransmission: Boolean = true,
+        minSendIntervalNanoSeconds: Int = MODBUS_MIN_SEND_INTERVAL,
+        done: ((res: ByteArray) -> Unit)? = null
+    ) {
+        if (slaveCount <= 0) {
+//            Logger.i("ModBus", "sendTo($index), slaveCount为0, 返回空数据")
+            done?.invoke(byteArrayOf())
+            return
+        }
+        if (index < 0 || index >= slaveCount) {
+            throw IllegalArgumentException("index [${index}] out of bound [${slaveCount}]")
+        }
+//        //  The following code is just for test
+//        if (machineType == null) {
+//            machineType = AppContext.getInstance().getConfigStr(Config.ID_GC_MACHINE_TYPE)
+//        }
+//        if (machineType == Config.MACHINE_TYPE__BOTTLE.toString() && index == 0) {
+//            val task = FrameTask(frame.compile(1), done)
+//            task.allowRetransmission = allowRetransmission
+//            task.minSendInterval = minSendIntervalNanoSeconds
+//            pendings.add(task)
+//            return
+//        }
+//        if (machineType == Config.MACHINE_TYPE__MIXED_RECYCLABLE_AND_BOTTLE.toString() && index == 1) {
+//            val task = FrameTask(frame.compile(1), done)
+//            task.allowRetransmission = allowRetransmission
+//            task.minSendInterval = minSendIntervalNanoSeconds
+//            pendings.add(task)
+//            return
+//        }
+//        //  The above code is just for test
+
+        val task = FrameTask(frame.compile(index), done)
+        task.allowRetransmission = allowRetransmission
+        task.minSendInterval = minSendIntervalNanoSeconds
+        pendings.add(task)
+    }
+
+    fun start() {
+        thread = Thread {
+            while (running) {
+                try {
+                    takePendingToSend()
+                } catch (e: InterruptedException) {
+                }
+            }
+        }
+        thread?.isDaemon = true
+        thread?.start()
+    }
+
+    fun isRunning(): Boolean {
+        return running
+    }
+
+    fun stop() {
+        running = false
+        thread?.interrupt()
+        portManager.close()
+    }
+
+}
+
+
+class FrameTask(
+    val req: ByteArray,
+    val done: ((res: ByteArray) -> Unit)?    // 响应回调
+) {
+
+    /**
+     * 是否允许重发
+     */
+    var allowRetransmission: Boolean = true
+
+    /**
+     * 上次发送时间
+     */
+    var lastSent: Long = 0
+
+    /**
+     * 已发送次数
+     */
+    var sentCount = 0
+
+    /**
+     * 最小发送间隔
+     */
+    var minSendInterval: Int = MODBUS_MIN_SEND_INTERVAL
+
+
+    fun waitIfNecessary() {
+        val interval = System.nanoTime() - lastSent
+        if (interval < minSendInterval) {
+            Thread.sleep((minSendInterval - interval) / 1000_000)
+        }
+    }
+
+    fun shouldSend(): Boolean {
+        return if (allowRetransmission) {
+            sentCount < 3
+        } else {
+            sentCount < 1
+        }
+    }
+
+    fun afterSent() {
+        sentCount++
+        lastSent = System.nanoTime()
+    }
+
+    /**
+     * 判断 res 是否是 frame 的响应
+     */
+    fun match(res: ByteArray): Boolean {
+        // 从机地址 和 功能码 必须相同
+        if (res.size < 5 || req[0] != res[0] || req[1] != res[1]) {
+            return false
+        }
+        // 报文2 的 CRC校验得正确
+        val crc16 = res.crc16(0, res.size - 2)
+        return crc16[0] == res[res.size - 2] && crc16[1] == res[res.size - 1]
+    }
+
+}
+
+private const val FRAME_TYPE_READ: Byte = 0x03
+private const val FRAME_TYPE_WRITE: Byte = 0x06
+private const val FRAME_TYPE_SCANNER: Byte = 0x43
+const val FRAME_TYPE_WRITE_MULTI: Byte = 0x10
+const val FRAME_TYPE_WRITE_FILE: Byte = 0x15
+
+/**
+ * ModBus 数据帧
+ */
+class MBFrame(
+
+    // 类型
+    val type: Byte,
+    // 数据域:D1 和 D2
+    val data: ByteArray
+
+) {
+
+    /**
+     * @param index 从机序号
+     */
+    fun compile(index: Int): ByteArray {
+        val bytes = ByteArray(4 + data.size)
+        bytes[0] = (0x80 + index).toByte()
+        bytes[1] = type
+        for (i in data.indices) {
+            bytes[2 + i] = data[i]
+        }
+        val crc16 = bytes.crc16(0, bytes.size - 2)
+        bytes[bytes.size - 2] = crc16[0]
+        bytes[bytes.size - 1] = crc16[1]
+        return bytes
+    }
+
+    companion object {
+
+        /**
+         * 读桶的状态(从桶的重量 0x000 开始读,读 4 个寄存器 即 8 个字节)
+         */
+        val READ_STATUS = MBFrame(
+            FRAME_TYPE_READ,
+            byteArrayOf(0x00, 0x00, 0x00, 0x04)
+        )
+
+        /**
+         * 读扫码头/刷卡板数据
+         */
+        val READ_CANNER = MBFrame(
+            FRAME_TYPE_SCANNER,
+            byteArrayOf(0x00, 0x00, 0x00, 0x00)
+        )
+
+        /**
+         * 读控制板软硬件版本
+         */
+        val READ_CTRL_BOARD_VERSION = MBFrame(
+            FRAME_TYPE_READ,
+            byteArrayOf(0xF0.toByte(), 0x00, 0x00, 0x01)
+        )
+
+        /**
+         * 读刷卡板
+         */
+        val READ_CARD_BOARD = MBFrame(FRAME_TYPE_SCANNER, byteArrayOf(0x00, 0x00, 0x00, 0x06))
+
+        /**
+         * 清运门开门授权,同时点亮 按钮指示灯
+         */
+        val WRITE_AUTH_GRANT_CLEANING_DOOR = MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(0x00, 0x05, 0b01111011.toByte(), 0b10000100.toByte())
+        )
+
+        /**
+         * 收回清运门开门授权,同时吹灭 按钮指示灯
+         */
+        val WRITE_AUTH_REVOKE_CLEANING_DOOR = MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(0x00, 0x05, 0b01111011.toByte(), 0b00000000)
+        )
+
+        /**
+         * 设置清运门可开启
+         */
+        val WRITE_CLEANING_DOOR_OPENABLE = MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(0x00, 0x05, 0b01110111.toByte(), 0b10001000.toByte())
+        )
+
+        /**
+         * 设置清运门不可开启
+         */
+        val WRITE_CLEANING_DOOR_NON_OPENABLE = MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(0x00, 0x05, 0b01110111.toByte(), 0b00000000)
+        )
+
+        /**
+         * 开门授权,同时点亮 按钮指示灯
+         */
+        val WRITE_AUTH_GRANT = MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(0x00, 0x05, 0b11111010.toByte(), 0b00000101)
+        )
+
+        /**
+         * 收回开门授权,同时吹灭 按钮指示灯
+         */
+        val WRITE_AUTH_REVOKE = MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(0x00, 0x05, 0b11111010.toByte(), 0b00000000)
+        )
+
+        /**
+         * 启用瓶类计数
+         */
+        val ENABLE_BOTTLE_COUNTER = MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(0x00, 0x05, 0b11101111.toByte(), 0b00010000)
+        )
+
+        /**
+         * 禁用瓶类计数
+         */
+        val DISABLE_BOTTLE_COUNTER = MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(0x00, 0x05, 0b11101111.toByte(), 0b00000000)
+        )
+
+        /**
+         * 开门命令
+         */
+        val WRITE_OPEN = MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(0x00, 0x05, 0b11111101.toByte(), 0b00000010)
+        )
+
+        /**
+         * 关门命令
+         */
+        val WRITE_CLOSE = MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(0x00, 0x05, 0b01111111.toByte(), 0b10000000.toByte())
+        )
+
+        /**
+         * 开照明灯
+         */
+        val WRITE_LIGHT_ON = MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(0x00, 0x05, 0b11110111.toByte(), 0b00001000)
+        )
+
+        /**
+         * 关照明灯
+         */
+        val WRITE_LIGHT_OFF = MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(0x00, 0x05, 0b11110111.toByte(), 0b00000000)
+        )
+
+        /**
+         * 开换气扇
+         */
+        val WRITE_FAN_ON = MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(0x00, 0x05, 0b11011111.toByte(), 0b00100000)
+        )
+
+        /**
+         * 关换气扇
+         */
+        val WRITE_FAN_OFF = MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(0x00, 0x05, 0b11011111.toByte(), 0b00000000)
+        )
+
+        /**
+         * 关托盘
+         */
+        val WRITE_PALLET_OFF = MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(0x00, 0x10, 0b01111111.toByte(), 0b10000000.toByte())
+        )
+
+        /**
+         * 开托盘
+         */
+        val WRITE_PALLET_ON = MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(0x00, 0x10, 0b10111111.toByte(), 0b01000000.toByte())
+        )
+
+        /**
+         * 读取称重传感器原始读取值
+         */
+        val READ_WEIGHER_SENSOR_RAW_VALUE = MBFrame(
+            FRAME_TYPE_READ,
+            byteArrayOf(0x01, 0x00, 0x00, 0x02)
+        )
+
+        /**
+         * 允许无授权开门
+         */
+        val WRITE_ALLOW_OPEN_DOOR_UNIDENTIFIED =
+            MBFrame(FRAME_TYPE_WRITE, byteArrayOf(0x00, 0x05, 0b10111111.toByte(), 0b01000000))
+
+        /**
+         * 禁止无授权开门
+         */
+        val WRITE_DISALLOW_OPEN_DOOR_UNIDENTIFIED =
+            MBFrame(FRAME_TYPE_WRITE, byteArrayOf(0x00, 0x05, 0b10111111.toByte(), 0b00000000))
+
+        /**
+         * 修改 授权时间 和 自动关门的时间
+         * @param authSeconds 在这个时间内可以开门
+         * @param openSeconds 门开起来后,维持多久时间才关
+         */
+        fun writeAuthAndCloseTime(
+            authSeconds: Int,
+            openSeconds: Int,
+            putterPowerOnAlways: Boolean
+        ): MBFrame {
+            if (putterPowerOnAlways) {
+                val s1 = ((openSeconds and 0xFF) shl 8).toShort()
+                val s2 = (authSeconds and 0xFF).toShort()
+                val value = -(s1 + s2)
+                val b1 = (value shr 8).toByte()
+                val b2 = (value and 0xFF).toByte()
+                return MBFrame(FRAME_TYPE_WRITE, byteArrayOf(0x00, 0x04, b1, b2))
+            } else {
+                return MBFrame(
+                    FRAME_TYPE_WRITE,
+                    byteArrayOf(0x00, 0x04, openSeconds.toByte(), authSeconds.toByte())
+                )
+            }
+        }
+
+        /**
+         * 修改 授权时间 和 自动关门的时间
+         * @param travelMuills 行程时间(取值范围:0 ~ 40000)
+         * @param putDelaySeconds 投放成功后延时关门时间(取值范围:0 ~ 7)
+         * @param doorPushOpenable 门是否可推开
+         */
+        fun writeTravelTime(
+            travelMuills: Int,
+            putDelaySeconds: Int,
+            doorPushOpenable: Boolean
+        ): MBFrame {
+            var value = travelMuills / 10 + (putDelaySeconds shl 12)
+            if (doorPushOpenable) {
+                value = -value
+            }
+            val b1 = (value shr 8).toByte()
+            val b2 = (value % 256).toByte()
+            return MBFrame(
+                FRAME_TYPE_WRITE,
+                byteArrayOf(0x00, 0x06, b1, b2)
+            )
+        }
+
+        /**
+         * 读控制板接收升级文件数据包标志位
+         */
+        fun readCtrlBoardUpgradeFlag(count: Int): MBFrame {
+            val b1 = (count shr 8).toByte()
+            val b2 = (count % 256).toByte()
+            return MBFrame(FRAME_TYPE_READ, byteArrayOf(0xF0.toByte(), 0x04, b1, b2))
+        }
+
+        /**
+         * 设置垃圾桶类别--智能厨余
+         */
+        val WRITE_TRASH_BIN_TYPE_INTELLI_KITCHEN = MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(0x00, 0x1A, 0x01, 0x01)
+        )
+
+        /**
+         * 设置氨气浓度报警值
+         */
+        fun writeNh3AlarmValue(value: Int): MBFrame {
+            val b1 = (value shr 8).toByte()
+            val b2 = (value % 256).toByte()
+            return MBFrame(FRAME_TYPE_WRITE, byteArrayOf(0x00, 0x13, b1, b2))
+        }
+
+        /**
+         * 设置硫化氢浓度报警值
+         */
+        fun writeH2sAlarmValue(value: Int): MBFrame {
+            val b1 = (value shr 8).toByte()
+            val b2 = (value % 256).toByte()
+            return MBFrame(FRAME_TYPE_WRITE, byteArrayOf(0x00, 0x14, b1, b2))
+        }
+
+        /**
+         * 设置新风系统强风时长
+         */
+        fun writeFierceWindSeconds(value: Int): MBFrame {
+            val b1 = (value shr 8).toByte()
+            val b2 = (value % 256).toByte()
+            return MBFrame(FRAME_TYPE_WRITE, byteArrayOf(0x00, 0x15, b1, b2))
+        }
+
+        /**
+         * 设置新风系统弱风时长
+         */
+        fun writeWeakWindSeconds(value: Int): MBFrame {
+            val b1 = (value shr 8).toByte()
+            val b2 = (value % 256).toByte()
+            return MBFrame(FRAME_TYPE_WRITE, byteArrayOf(0x00, 0x16, b1, b2))
+        }
+
+        /**
+         * 设置消毒喷雾间隔时长
+         */
+        fun writeSprayIntervalSeconds(value: Int): MBFrame {
+            val b1 = (value shr 8).toByte()
+            val b2 = (value % 256).toByte()
+            return MBFrame(FRAME_TYPE_WRITE, byteArrayOf(0x00, 0x17, b1, b2))
+        }
+
+        /**
+         * 设置消毒喷雾单次时长
+         */
+        fun writeSprayRunningSeconds(value: Int): MBFrame {
+            val b1 = (value shr 8).toByte()
+            val b2 = (value % 256).toByte()
+            return MBFrame(FRAME_TYPE_WRITE, byteArrayOf(0x00, 0x18, b1, b2))
+        }
+
+    }
+
+}

+ 155 - 0
app/src/main/java/com/example/whatever/modbus/PortManager.kt

@@ -0,0 +1,155 @@
+package com.example.whatever.modbus
+
+import android.content.Context
+import androidx.annotation.WorkerThread
+import com.epton.sdk.SerialPort
+import com.example.whatever.util.Executor
+import com.example.whatever.util.log.LogUtil
+import java.io.File
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+import java.util.concurrent.Executors
+import java.util.concurrent.ScheduledExecutorService
+import java.util.concurrent.TimeUnit
+
+/**
+ * 串口通信管理器
+ */
+class PortManager private constructor(
+    private val input: InputStream,
+    private val output: OutputStream
+) {
+
+    // 是否正在监听
+    @Volatile
+    private var listening = false
+
+    /**
+     * 向串口发送数据
+     */
+    fun send(data: ByteArray) : Boolean {
+        try {
+            output.write(data)
+            output.flush()
+            return true
+        } catch (e: IOException) {
+//            Logger.e("PortManager", "串口写数据失败", e)
+            return false
+        }
+    }
+
+    var t : Thread ? = null
+
+    /**
+     * 监听串口数据
+     */
+    fun listen(listener: (ByteArray) -> Unit) {
+        if (listening) {
+            throw IllegalStateException("已经被监听")
+        }
+        listening = true
+        t = Thread {
+            val buffer = ByteArray(BUFFER_SIZE)
+            while (listening) {
+                try {
+                    val data = ByteArray(input.read(buffer))
+                    System.arraycopy(buffer, 0, data, 0, data.size)
+                    if (data.isNotEmpty()) {
+                        listener(data)
+                    }
+                } catch (e: Exception) {
+//                    Logger.e("PortManager", "串口读取或处理异常", e)
+                }
+            }
+        }
+        t?.isDaemon = true
+        t?.start()
+    }
+
+    /**
+     * 关闭
+     */
+    fun close() {
+        t = null
+        listening = false
+        try {
+            input.close()
+            output.close()
+        } catch (e: Exception) { }
+    }
+
+    companion object {
+
+        private const val BUFFER_SIZE = 256
+        private var plcThread: ScheduledExecutorService? = null
+
+        var plcPort: PortManager? = null
+
+        /**
+         * 打开串口
+         * @param port 串口号,从 0 开始
+         * @param bps 波特率,正整数
+         */
+        @WorkerThread
+        fun open(port: Int, bps: Int, usb: Boolean) : PortManager? {
+            var blocked = true
+            val thread = Thread.currentThread()
+            Executor.delayOnMain({
+                // 如果线程被阻塞达到 1 秒,则打断唤醒
+                if (blocked) {
+//                    Logger.w("PortManager", "超 0.5 秒未打开串口,打断唤醒")
+                    thread.interrupt()
+                }
+            }, 500)
+            try {
+                val file = File(if(usb) "/dev/ttyUSB${port}" else "/dev/ttyS${port}")
+//                Logger.i("PortManager", "连接 port file")
+                SerialPort(file, bps, 0).run {
+                    blocked = false
+//                    Logger.i("PortManager", "建立 SerialPort")
+                    return PortManager(inputStream, outputStream)
+                }
+            } catch (e: Exception) {
+                blocked = false
+//                Logger.e("PortManager", "异常", e)
+//                Tip.toast("串口[${port}, ${bps}, ${usb}]打开失败[${e.javaClass.simpleName}]${e.message}")
+//                synchronized(Tip) {
+//                    Tip.audioNum(port)
+//                    Tip.audio(R.raw.unconnect_port)
+//                }
+                return null
+            }
+        }
+
+
+        /**
+         * 打开主控板
+         */
+        @WorkerThread
+        fun openCtrlBord(ctx: Context) : PortManager? {
+            val port = 1
+            val bps = 9600
+            val usb = false
+//            Logger.i("PortManager", "主控板 port = ${port}, bps = ${bps}, usb = ${usb}")
+            return open(port, bps, usb)
+        }
+
+        fun openPlc(onRst: ((ByteArray) -> Unit)? = null) {
+            plcThread?.shutdownNow()?.clear()
+            plcThread = Executors.newScheduledThreadPool(1)
+            plcThread?.scheduleWithFixedDelay({
+                open(1, 9600, false)?.let { itSerialPortUtil ->
+                    LogUtil.d("openPlc : 连接成功")
+                    plcPort = itSerialPortUtil
+                    itSerialPortUtil.listen {
+                        LogUtil.d("openPlc : $it")
+                        onRst?.invoke(it)
+                    }
+                    plcThread?.shutdownNow()?.clear()
+                }
+            }, 0, 10, TimeUnit.SECONDS)
+        }
+    }
+
+}

+ 12 - 0
app/src/main/java/com/example/whatever/model/BleBean.kt

@@ -0,0 +1,12 @@
+package com.example.whatever.model
+
+import com.clj.fastble.data.BleDevice
+import java.io.File
+
+data class BleBean(
+    var bleDevice: BleDevice,
+    var token: ByteArray? = null,
+    var fileSend: File? = null,
+    var ticketSend: String? = null,
+    var ticketStatus: ByteArray = byteArrayOf()
+)

+ 6 - 0
app/src/main/java/com/example/whatever/model/Constants.kt

@@ -0,0 +1,6 @@
+package com.example.whatever.model
+
+object Constants {
+    const val PERMISSION_REQUEST_CODE = 1
+    const val BLE_LOCAL_NAME = "keyLock"
+}

+ 3 - 0
app/src/main/java/com/example/whatever/model/FileBean.kt

@@ -0,0 +1,3 @@
+package com.example.whatever.model
+
+data class FileBean(var fileName:String?, var pathStr:String, var isAssets:Boolean)

+ 10 - 0
app/src/main/java/com/example/whatever/research/UnixTime.java

@@ -0,0 +1,10 @@
+package com.example.whatever.research;
+
+import java.time.Instant;
+
+public class UnixTime {
+
+    public String getUnixTimeHexStr() {
+        return String.format("%02x", System.currentTimeMillis() / 1000);
+    }
+}

+ 77 - 0
app/src/main/java/com/example/whatever/research/WorkTicketBean.kt

@@ -0,0 +1,77 @@
+package com.example.whatever.research
+
+class WorkTicketBean {
+    /**
+     * 权限卡号
+     */
+    private val cardNo: String? = null
+    /**
+     * 用户密码
+     */
+    private val password: String? = null
+    /**
+     * 工作有效期(小时)
+     */
+    private val effectiveTime: Int? = null
+    /**
+     * 工作票数组
+     */
+    private val data: MutableList<DataDTO>? = null
+    /**
+     * 挂锁数组
+     */
+    private val lockList: MutableList<LockListDTO>? = null
+
+    class DataDTO {
+        /**
+         * 工作票号
+         */
+        private val taskCode: String? = null
+        /**
+         * 工作票ID
+         */
+        private val taskId: String? = null
+        /**
+         * 工作票序号
+         */
+        private val codeId: Int? = null
+        /**
+         * 工作票下挂任务列表
+         */
+        private val dataList: MutableList<DataListDTO>? = null
+
+        class DataListDTO {
+            /**
+             * 任务ID
+             */
+            private val dataId: Int? = null
+            /**
+             * 工作点位RFID号
+             */
+            private val equipRfidNo: String? = null
+            /**
+             * 锁RFID号
+             */
+            private val infoRfidNo: String? = null
+            /**
+             * 任务目标 0:挂锁 1:解锁
+             */
+            private val target: Int? = null
+            /**
+             * 前序任务ID
+             */
+            private val prevId: Int? = null
+        }
+    }
+
+    class LockListDTO {
+        /**
+         * 挂锁ID
+         */
+        private val lockId: String? = null
+        /**
+         * 挂锁RFID
+         */
+        private val rfid: String? = null
+    }
+}

+ 122 - 0
app/src/main/java/com/example/whatever/util/CRC16.java

@@ -0,0 +1,122 @@
+package com.example.whatever.util;
+
+public class CRC16 {
+
+    private static final byte[] auchCRCHi = { 0x00, (byte) 0xC1, (byte) 0x81,
+            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
+            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
+            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0,
+            (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
+            (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40,
+            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x01,
+            (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1,
+            (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
+            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
+            (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01,
+            (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x01, (byte) 0xC0,
+            (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
+            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
+            (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00,
+            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0,
+            (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
+            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
+            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
+            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1,
+            (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80,
+            (byte) 0x41, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
+            (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01,
+            (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1,
+            (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
+            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
+            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
+            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1,
+            (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80,
+            (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40,
+            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x01,
+            (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1,
+            (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
+            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
+            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
+            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0,
+            (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
+            (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40,
+            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
+            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0,
+            (byte) 0x80, (byte) 0x41, (byte) 0x01, (byte) 0xC0, (byte) 0x80,
+            (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40,
+            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
+            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1,
+            (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80,
+            (byte) 0x41, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
+            (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00,
+            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0,
+            (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
+            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
+            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
+            (byte) 0xC1, (byte) 0x81, (byte) 0x40 };
+
+    private static final byte[] auchCRCLo = { (byte) 0x00, (byte) 0xC0, (byte) 0xC1,
+            (byte) 0x01, (byte) 0xC3, (byte) 0x03, (byte) 0x02, (byte) 0xC2,
+            (byte) 0xC6, (byte) 0x06, (byte) 0x07, (byte) 0xC7, (byte) 0x05,
+            (byte) 0xC5, (byte) 0xC4, (byte) 0x04, (byte) 0xCC, (byte) 0x0C,
+            (byte) 0x0D, (byte) 0xCD, (byte) 0x0F, (byte) 0xCF, (byte) 0xCE,
+            (byte) 0x0E, (byte) 0x0A, (byte) 0xCA, (byte) 0xCB, (byte) 0x0B,
+            (byte) 0xC9, (byte) 0x09, (byte) 0x08, (byte) 0xC8, (byte) 0xD8,
+            (byte) 0x18, (byte) 0x19, (byte) 0xD9, (byte) 0x1B, (byte) 0xDB,
+            (byte) 0xDA, (byte) 0x1A, (byte) 0x1E, (byte) 0xDE, (byte) 0xDF,
+            (byte) 0x1F, (byte) 0xDD, (byte) 0x1D, (byte) 0x1C, (byte) 0xDC,
+            (byte) 0x14, (byte) 0xD4, (byte) 0xD5, (byte) 0x15, (byte) 0xD7,
+            (byte) 0x17, (byte) 0x16, (byte) 0xD6, (byte) 0xD2, (byte) 0x12,
+            (byte) 0x13, (byte) 0xD3, (byte) 0x11, (byte) 0xD1, (byte) 0xD0,
+            (byte) 0x10, (byte) 0xF0, (byte) 0x30, (byte) 0x31, (byte) 0xF1,
+            (byte) 0x33, (byte) 0xF3, (byte) 0xF2, (byte) 0x32, (byte) 0x36,
+            (byte) 0xF6, (byte) 0xF7, (byte) 0x37, (byte) 0xF5, (byte) 0x35,
+            (byte) 0x34, (byte) 0xF4, (byte) 0x3C, (byte) 0xFC, (byte) 0xFD,
+            (byte) 0x3D, (byte) 0xFF, (byte) 0x3F, (byte) 0x3E, (byte) 0xFE,
+            (byte) 0xFA, (byte) 0x3A, (byte) 0x3B, (byte) 0xFB, (byte) 0x39,
+            (byte) 0xF9, (byte) 0xF8, (byte) 0x38, (byte) 0x28, (byte) 0xE8,
+            (byte) 0xE9, (byte) 0x29, (byte) 0xEB, (byte) 0x2B, (byte) 0x2A,
+            (byte) 0xEA, (byte) 0xEE, (byte) 0x2E, (byte) 0x2F, (byte) 0xEF,
+            (byte) 0x2D, (byte) 0xED, (byte) 0xEC, (byte) 0x2C, (byte) 0xE4,
+            (byte) 0x24, (byte) 0x25, (byte) 0xE5, (byte) 0x27, (byte) 0xE7,
+            (byte) 0xE6, (byte) 0x26, (byte) 0x22, (byte) 0xE2, (byte) 0xE3,
+            (byte) 0x23, (byte) 0xE1, (byte) 0x21, (byte) 0x20, (byte) 0xE0,
+            (byte) 0xA0, (byte) 0x60, (byte) 0x61, (byte) 0xA1, (byte) 0x63,
+            (byte) 0xA3, (byte) 0xA2, (byte) 0x62, (byte) 0x66, (byte) 0xA6,
+            (byte) 0xA7, (byte) 0x67, (byte) 0xA5, (byte) 0x65, (byte) 0x64,
+            (byte) 0xA4, (byte) 0x6C, (byte) 0xAC, (byte) 0xAD, (byte) 0x6D,
+            (byte) 0xAF, (byte) 0x6F, (byte) 0x6E, (byte) 0xAE, (byte) 0xAA,
+            (byte) 0x6A, (byte) 0x6B, (byte) 0xAB, (byte) 0x69, (byte) 0xA9,
+            (byte) 0xA8, (byte) 0x68, (byte) 0x78, (byte) 0xB8, (byte) 0xB9,
+            (byte) 0x79, (byte) 0xBB, (byte) 0x7B, (byte) 0x7A, (byte) 0xBA,
+            (byte) 0xBE, (byte) 0x7E, (byte) 0x7F, (byte) 0xBF, (byte) 0x7D,
+            (byte) 0xBD, (byte) 0xBC, (byte) 0x7C, (byte) 0xB4, (byte) 0x74,
+            (byte) 0x75, (byte) 0xB5, (byte) 0x77, (byte) 0xB7, (byte) 0xB6,
+            (byte) 0x76, (byte) 0x72, (byte) 0xB2, (byte) 0xB3, (byte) 0x73,
+            (byte) 0xB1, (byte) 0x71, (byte) 0x70, (byte) 0xB0, (byte) 0x50,
+            (byte) 0x90, (byte) 0x91, (byte) 0x51, (byte) 0x93, (byte) 0x53,
+            (byte) 0x52, (byte) 0x92, (byte) 0x96, (byte) 0x56, (byte) 0x57,
+            (byte) 0x97, (byte) 0x55, (byte) 0x95, (byte) 0x94, (byte) 0x54,
+            (byte) 0x9C, (byte) 0x5C, (byte) 0x5D, (byte) 0x9D, (byte) 0x5F,
+            (byte) 0x9F, (byte) 0x9E, (byte) 0x5E, (byte) 0x5A, (byte) 0x9A,
+            (byte) 0x9B, (byte) 0x5B, (byte) 0x99, (byte) 0x59, (byte) 0x58,
+            (byte) 0x98, (byte) 0x88, (byte) 0x48, (byte) 0x49, (byte) 0x89,
+            (byte) 0x4B, (byte) 0x8B, (byte) 0x8A, (byte) 0x4A, (byte) 0x4E,
+            (byte) 0x8E, (byte) 0x8F, (byte) 0x4F, (byte) 0x8D, (byte) 0x4D,
+            (byte) 0x4C, (byte) 0x8C, (byte) 0x44, (byte) 0x84, (byte) 0x85,
+            (byte) 0x45, (byte) 0x87, (byte) 0x47, (byte) 0x46, (byte) 0x86,
+            (byte) 0x82, (byte) 0x42, (byte) 0x43, (byte) 0x83, (byte) 0x41,
+            (byte) 0x81, (byte) 0x80, (byte) 0x40 };
+
+    public static int crc16(byte[] puchMsg, int from, int to) {
+        byte uchCRCHi = (byte) 0xFF;
+        byte uchCRCLo = (byte) 0xFF;
+        for (int i = from; i < to; i++) {
+            int uIndex = (uchCRCHi ^ puchMsg[i]) & 0xff;
+            uchCRCHi = (byte) (uchCRCLo ^ auchCRCHi[uIndex]);
+            uchCRCLo = auchCRCLo[uIndex];
+        }
+        return ((((int) uchCRCHi) << 8 | (((int) uchCRCLo) & 0xff))) & 0xffff;
+    }
+
+}

+ 16 - 0
app/src/main/java/com/example/whatever/util/DownloadCallBack.kt

@@ -0,0 +1,16 @@
+package com.example.whatever.util
+
+import cn.zhxu.okhttps.Process
+import java.io.File
+
+interface DownloadCallBack {
+
+    fun onProcess(process: Process)
+
+    fun onResult(
+        isSuccess: Boolean,
+        code: Int? = null,
+        file: File? = null,
+        errorMsg: String? = null
+    )
+}

+ 133 - 0
app/src/main/java/com/example/whatever/util/Executor.kt

@@ -0,0 +1,133 @@
+package com.example.whatever.util
+
+import android.os.Handler
+import android.os.Looper
+import java.io.PrintWriter
+import java.io.StringWriter
+import java.util.concurrent.CountDownLatch
+
+object Executor {
+
+    private var io: Handler? = null
+    private val main: Handler = Handler(Looper.getMainLooper())
+
+    private val latch = CountDownLatch(1)
+
+    init {
+        val thread = Thread {
+            Looper.prepare()
+            io = Handler()
+            latch.countDown()
+            Looper.loop()
+        }
+        thread.isDaemon = true
+        thread.start()
+    }
+
+    val mainHandler: Handler
+        get() = main
+
+    val ioHandler: Handler
+        get() {
+            if (io == null) {
+                latch.await()
+            }
+            return io!!
+        }
+
+    fun runOnMain(run: Runnable) {
+        main.post(run)
+    }
+
+    fun delayOnMain(run: Runnable, delayMills: Long) {
+        main.postDelayed(run, delayMills)
+    }
+
+    fun repeatOnMain(run: () -> Boolean, intervalMills: Long, immediately: Boolean = true) {
+        repeat(main, run, intervalMills, immediately)
+    }
+
+    fun runOnIO(run: Runnable) {
+        val traces = Thread.currentThread().stackTrace
+        ioHandler.post {
+            runWithTraces(traces, run)
+        }
+    }
+
+    fun runOnIO(run: Runnable, runnableTimeoutMillis: Long = 5000) {
+        val traces = Thread.currentThread().stackTrace
+        ioHandler.post {
+            runWithTraces(traces, run, runnableTimeoutMillis)
+        }
+    }
+
+    fun delayOnIO(run: Runnable, delayMills: Long, runnableTimeoutMillis: Long = 10_000) {
+        val traces = Thread.currentThread().stackTrace
+        ioHandler.postDelayed({
+            runWithTraces(traces, run, runnableTimeoutMillis)
+        }, delayMills)
+    }
+
+    fun repeatOnIO(run: () -> Boolean, intervalMills: Long, immediately: Boolean = true, runnableTimeoutMillis: Long = 5_000) {
+        val traces = Thread.currentThread().stackTrace
+        repeat(ioHandler, {
+            var blocked = true
+            val thread = Thread.currentThread()
+            delayOnMain({
+                // 如果线程被阻塞达到 1 秒,则打断唤醒
+                if (blocked) {
+                    printTimeoutTraces(traces)
+                    thread.interrupt()
+                }
+            }, runnableTimeoutMillis)
+            val res = run()
+            blocked = false
+            return@repeat res
+        }, intervalMills, immediately)
+    }
+
+    private fun runWithTraces(traces: Array<StackTraceElement>, run: Runnable, runnableTimeoutMillis: Long = 10_000) {
+        var blocked = true
+        val thread = Thread.currentThread()
+        delayOnMain({
+            // 如果线程被阻塞达到 1 秒,则打断唤醒
+            if (blocked) {
+                printTimeoutTraces(traces)
+                thread.interrupt()
+            }
+        }, runnableTimeoutMillis)
+        run.run()
+        blocked = false
+    }
+
+    private fun printTimeoutTraces(traces: Array<StackTraceElement>) {
+        val writer = StringWriter()
+        val print = PrintWriter(writer)
+        for (traceElement in traces) {
+            print.println("\tat $traceElement")
+        }
+//        Logger.w("Executor", "执行器超时(10秒):\n${writer}")
+    }
+
+    private fun repeat(
+        handler: Handler,
+        run: () -> Boolean,
+        intervalMills: Long,
+        immediately: Boolean = true
+    ) {
+        if (immediately) {
+            if (run()) {
+                handler.postDelayed({
+                    repeat(handler, run, intervalMills, immediately)
+                }, intervalMills)
+            }
+        } else {
+            handler.postDelayed({
+                if (run()) {
+                    repeat(handler, run, intervalMills, immediately)
+                }
+            }, intervalMills)
+        }
+    }
+
+}

+ 118 - 0
app/src/main/java/com/example/whatever/util/FileUtil.kt

@@ -0,0 +1,118 @@
+package com.example.whatever.util
+
+import android.content.Context
+import android.os.Environment
+import java.io.*
+import java.io.File.separator
+import java.text.SimpleDateFormat
+import java.util.*
+
+class FileUtil {
+    companion object{
+        val ROOT_APP="ble-manager"
+        val DOWNLOAD_DIR="${separator}download"
+        val LOG_DIR="${separator}log"
+
+
+        /**
+         * @param permissionType:0:默认(根目录>私有可见文件缓存);1:私有可见缓存;2:私有可见文件;3:私有缓存;4:私有文件
+         */
+        fun getRootFolder(mContext: Context,permissionType:Int=0):File?{
+            var rootFile:File?=null
+            when(permissionType){
+                1->{
+                    rootFile=File(mContext.externalCacheDir,"$separator$ROOT_APP")
+                }
+                2->{
+                    rootFile=File(mContext.getExternalFilesDir(null),"$separator$ROOT_APP")
+                }
+                3->{
+                    rootFile=File(mContext.cacheDir,"$separator$ROOT_APP")
+                }
+                4->{
+                    rootFile=File(mContext.filesDir,"$separator$ROOT_APP")
+                }
+                else ->{
+                    try {
+                        rootFile=File(Environment.getExternalStorageDirectory(),"$separator$ROOT_APP")
+                        if (rootFile.exists()){
+                            return rootFile
+                        }else{
+                            val isSucc = rootFile.mkdirs()
+                            if (isSucc)return rootFile
+                        }
+                    }catch (e:Exception){
+//                        d("创建文件夹异常",e)
+                    }
+                    rootFile=File(mContext.getExternalFilesDir(null),"$separator$ROOT_APP")
+                    if (rootFile.exists()){
+                        return rootFile
+                    }else{
+                        val isSucc = rootFile.mkdirs()
+                        if (isSucc)return rootFile
+                    }
+//                    d("创建文件夹失败2:${rootFile?.absolutePath}")
+                    return null
+                }
+            }
+            rootFile?.let {
+                if (it.exists()){
+                    return it
+                }else{
+                    val isSucc = it.mkdirs()
+                    if (isSucc)return it
+                }
+            }
+//            d("创建文件夹失败:${rootFile?.absolutePath}")
+            return null
+        }
+
+
+
+        /**
+         * 单个文件复制
+         */
+        fun copyfile(srcFile: File, destFile: File) {
+
+            var fis = FileInputStream(srcFile);
+            var fos = FileOutputStream(destFile)
+
+            var bis = BufferedInputStream(fis)
+            var bos = BufferedOutputStream(fos)
+
+
+            var buf = ByteArray(1024)
+
+            var len = 0;
+            while (true) {
+                len = bis.read(buf)
+                if (len == -1) break;
+                bos.write(buf, 0, len)
+            }
+            fis.close()
+            fos.close()
+
+        }
+
+        /**
+         * 带文件夹复制
+         */
+        fun copyDirToDir(srcFile: File, destFile: File) {
+            for (f in srcFile.listFiles()) {
+                //是文件就拷贝
+                var newfile = File(destFile.absolutePath, f.name)
+                if (f.isFile) {
+                    println("${f.absolutePath}-->${newfile.absolutePath}")
+                    copyfile(f, newfile)
+                } else { //如果是目录就递归复制
+                    //如果目标文件不存在,则创建
+                    if (!newfile.exists()) {
+                        if (!newfile.mkdir()) return
+                    }
+                    copyDirToDir(f, newfile)
+                }
+            }
+            return
+        }
+    }
+}

+ 55 - 0
app/src/main/java/com/example/whatever/util/Funcs.kt

@@ -0,0 +1,55 @@
+package com.example.whatever.util
+
+import android.content.Context
+import android.os.Environment
+import com.google.gson.Gson
+import java.io.Closeable
+import java.io.File
+
+
+val gson = Gson()
+
+/**
+ * 虚拟机相对时间(秒)
+ */
+fun jvmSeconds() : Long {
+    return System.nanoTime() / 1000_000_000
+}
+
+/**
+ * 当前时间(秒)
+ */
+fun nowSeconds() : Long {
+    return System.currentTimeMillis() / 1000
+}
+
+
+val EXT_ROOT_PATH : String = Environment.getExternalStorageDirectory().absolutePath
+
+fun extFile(path: String) : File {
+    return File(EXT_ROOT_PATH + File.separator + "refuse-class" + File.separator + path)
+}
+
+fun fileRootPath() : String {
+    return EXT_ROOT_PATH + File.separator + "refuse-class"
+}
+
+
+fun Closeable.closeQuietly() {
+    try {
+        close()
+    } catch (rethrown: RuntimeException) {
+        throw rethrown
+    } catch (_: Exception) {
+    }
+}
+
+fun dp2px(context: Context, dpValue: Float): Int {
+    val scale: Float = context.resources.displayMetrics.density
+    return (dpValue * scale + 0.5f).toInt()
+}
+
+fun px2dp(context: Context, pxValue: Float): Int {
+    val scale: Float = context.resources.displayMetrics.density
+    return (pxValue / scale + 0.5f).toInt()
+}

+ 271 - 0
app/src/main/java/com/example/whatever/util/NetHttpManager.kt

@@ -0,0 +1,271 @@
+package com.example.whatever.util
+
+import android.content.Context
+import android.util.Log
+import cn.zhxu.okhttps.HTTP
+import cn.zhxu.data.Mapper
+import cn.zhxu.okhttps.HttpResult
+import cn.zhxu.okhttps.HttpTask
+import cn.zhxu.okhttps.OkHttps
+import cn.zhxu.okhttps.gson.GsonMsgConvertor
+import cn.zhxu.okhttps.okhttp.OkHttpClientWrapper
+import com.example.whatever.Token
+import okhttp3.logging.HttpLoggingInterceptor
+import java.io.IOException
+import java.net.SocketTimeoutException
+
+class NetHttpManager {
+    lateinit var myHttp: HTTP
+    var context: Context? = null
+    var exceptionCount: Int = 0
+
+    companion object {
+        fun getInstance() = InstanceHelper.sSingle
+        val tagAuth = "Auth"
+    }
+
+    object InstanceHelper {
+        val sSingle = NetHttpManager()
+    }
+
+    fun initCtx(ctx: Context) {
+        context = ctx
+        myHttp = HTTP.builder()
+            .addMsgConvertor(GsonMsgConvertor())
+//            .baseUrl("${UrlConsts.BASE_URL_PROD}$DEFAULT_DOMAIN")
+//            .baseUrl(Environment.baseUrl(ctx))
+            .baseUrl("http://192.168.1.2:48080")
+            .responseListener { task: HttpTask<*>?, result: HttpResult? ->
+                if (result?.status != 200) {
+//                    LogUtil.d(
+//                        "Api fail : Url : ${task?.url}, " +
+//                                "Status : ${result?.status}, " +
+//                                "Params : ${task?.urlParas ?: task?.bodyParas}"
+//                    )
+                }
+                true
+            }
+            .config {
+                it.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
+            }
+            .addSerialPreprocessor { itPreChain ->
+                if (!itPreChain.task.isTagged(tagAuth)) {
+                    itPreChain.proceed()
+                    return@addSerialPreprocessor
+                }
+                requestTokenAndRefreshIfExpired {
+                    itPreChain.task.addHeader("Access-Token", it)
+                    itPreChain.proceed()
+                }
+            }
+            .build()
+    }
+
+    /**
+     * 获取TOKEN,若过期则刷新(代码中的字符串可以替换为常量)
+     */
+    fun requestTokenAndRefreshIfExpired(callback: (String?) -> Unit) {
+        val token = Token.fromSp(context!!)
+        if (token == null) {
+//            doLogin(callback)
+            return
+        }
+        if (token.isValid()) {
+            callback(token.accessToken)
+            return
+        }
+        // 访问令牌已过期,刷新令牌未过期,则调接口刷新当前令牌
+//        myHttp.async(TOKEN_REFRESH)
+        myHttp.async("/app-api/member/auth/refresh-token")
+            .skipPreproc() // 跳过所有预处理器
+            .addBodyPara("refreshToken", token.refreshToken)
+            .nextOnIO()
+            .setOnResponse { res: HttpResult ->
+                exceptionCount = 0
+                if (!res.isSuccessful) {
+//                    doLogin(callback)
+                    return@setOnResponse
+                }
+                try {
+                    val newToken = res.body.toBean(Token::class.java)
+                    newToken.saveToSp(context!!)
+                    callback(newToken.accessToken)
+                } catch (e: Exception) {
+//                    doLogin(callback)
+                    return@setOnResponse
+                }
+            }
+            .setOnException { e: IOException ->
+                evictHttpConnectPool(e)
+//                doLogin(callback)
+            }
+            .post()
+    }
+
+//    private fun doLogin(callback: (String?) -> Unit) {
+//        myHttp.async(SIGN_IN)
+//            .skipPreproc()
+//            .addBodyPara("username", "IPC")
+//            .addBodyPara("password", "123456")
+//            .nextOnIO()
+//            .setOnResponse {
+//                exceptionCount = 0
+//                if (!it.isSuccessful) {
+//                    callback(null)
+//                    return@setOnResponse
+//                }
+//                try {
+//                    val newToken = it.body.toBean(Token::class.java)
+//                    newToken.saveToSp(context!!)
+//                    callback(newToken.accessToken)
+//                } catch (e: Exception) {
+//                    callback(null)
+//                    return@setOnResponse
+//                }
+//            }
+//            .setOnException {
+//                evictHttpConnectPool(it)
+//                callback(null)
+//            }
+//            .post()
+//    }
+
+    fun doRequestNet(
+        urlStr: String, isSkipPreproc: Boolean,
+        bodyParas: Map<String, *>,
+        callback: (HttpResult.Body?, String?, Int) -> Unit,
+        isGet: Boolean, isAuth: Boolean,
+        mapperCallBack: ((Mapper) -> Unit)? = null
+    ) {
+        var httpTask = myHttp.async(urlStr).setOnException {
+            evictHttpConnectPool(it)
+//            callback(null, context?.getString(R.string.common_net_dis), 0)
+            callback(null, "haha", 0)
+        }
+        if (isAuth) httpTask.tag(tagAuth)
+        if (isSkipPreproc) {
+            httpTask.skipPreproc()
+        }
+        httpTask.nextOnIO()
+            .setOnResponse {
+                exceptionCount = 0
+                if (it.isSuccessful) {
+//                    Log.i("wtf1", it.body.toString())
+                    callback(it.body, null, it.status)
+                } else {
+                    var bobyStr = it.body.toString()
+                    callback(
+                        null, if (bobyStr.isNullOrEmpty()) {
+                            it.toString()
+                        } else {
+                            bobyStr
+                        }, it.status
+                    )
+                }
+            }
+            .setOnException {
+                evictHttpConnectPool(it)
+//                callback(null, context?.getString(R.string.common_net_dis), 0)
+                callback(null, "xixi", 0)
+                Log.i("wtf2", it.toString())
+            }
+        mapperCallBack?.let {
+            httpTask.setOnResMapper {
+                mapperCallBack.invoke(it)
+            }
+        }
+//            .setOnResMapper {
+//                mapperCallBack?.invoke(it)
+//            }
+        if (isGet) {
+            httpTask.addUrlPara(bodyParas)
+            httpTask.get()
+        } else {
+            httpTask.addBodyPara(bodyParas)
+            httpTask.post()
+        }
+    }
+
+    fun downloadFileWithProcess(
+        url: String,
+        downloadCallBack: DownloadCallBack,
+        filePath: String? = null,
+        folderPath: String? = null
+    ) {
+        try {
+            if (filePath.isNullOrBlank() && folderPath.isNullOrBlank()) {
+                downloadCallBack.onResult(
+                    false,
+//                    errorMsg = context?.getString(R.string.common_download_erro_notag)
+                    errorMsg = "lala"
+                )
+                return
+            }
+            OkHttps.async(url).setOnResponse {
+                exceptionCount = 0
+                if (it.isSuccessful) {
+                    if (filePath != null && filePath.isNotEmpty()) {
+                        it.body
+                            .stepRate(0.01)
+                            .setOnProcess {
+                                downloadCallBack.onProcess(it)
+                            }
+                            .toFile(filePath)
+                            .setOnSuccess {
+                                downloadCallBack.onResult(true, file = it)
+                            }
+                            .setOnFailure {
+                                downloadCallBack.onResult(false, errorMsg = it.exception.toString())
+                            }.start()
+                    } else if (folderPath != null && folderPath.isNotEmpty()) {
+                        it.body
+                            .stepRate(0.01)
+                            .setOnProcess {
+                                downloadCallBack.onProcess(it)
+                            }
+                            .toFolder(folderPath)
+                            .setOnSuccess {
+                                downloadCallBack.onResult(true, file = it)
+                            }
+                            .setOnFailure {
+                                downloadCallBack.onResult(false, errorMsg = it.exception.toString())
+                            }.start()
+                    }
+                } else {
+                    downloadCallBack.onResult(
+                        false,
+                        it.status,
+//                        errorMsg = context?.getString(R.string.common_net_download)
+                        errorMsg = "dd"
+                    )
+                }
+            }.setOnException {
+                evictHttpConnectPool(it)
+                downloadCallBack.onResult(
+                    false,
+//                    errorMsg = context?.getString(R.string.common_net_download)
+                    errorMsg = "qq"
+                )
+            }.get()
+        } catch (e: Exception) {
+            downloadCallBack.onResult(false, errorMsg = e.message)
+        }
+    }
+
+    private fun evictHttpConnectPool(e: Exception) {
+        try {
+            if (e is SocketTimeoutException) {
+                exceptionCount += 1
+                if (exceptionCount >= 5) {
+                    exceptionCount = 0
+                    (myHttp as OkHttpClientWrapper).okClient().connectionPool().evictAll()
+                }
+            } else {
+                exceptionCount = 0
+            }
+        } catch (e: Exception) {
+        }
+    }
+
+
+}

+ 22 - 0
app/src/main/java/com/example/whatever/util/ToastUtils.kt

@@ -0,0 +1,22 @@
+package com.example.whatever.util
+
+import android.content.Context
+import android.os.Looper
+import android.widget.Toast
+import com.example.whatever.MyApplication
+
+class ToastUtils private constructor(context: Context) : Toast(context) {
+
+    companion object {
+
+        fun tip(text: CharSequence?, duration: Int = LENGTH_SHORT) {
+            if (Looper.myLooper() != Looper.getMainLooper()) {
+                Executor.runOnMain {
+                    tip(text)
+                }
+                return
+            }
+            makeText(MyApplication.instance!!, text, duration).show()
+        }
+    }
+}

+ 41 - 0
app/src/main/java/com/example/whatever/util/log/ILog.kt

@@ -0,0 +1,41 @@
+package com.example.whatever.util.log
+
+import android.content.Context
+
+interface ILog {
+    /**
+     * 日志工具初始化
+     * @param logPath 日志输出路径
+     */
+    fun init(mContext: Context, logPath: String)
+
+    /**
+     * debug日志
+     * @param msg
+     */
+    fun d(tag: String?, msg: String?)
+
+    /**
+     * info日志
+     * @param msg
+     */
+    fun i(tag: String?, msg: String?)
+
+    /**
+     * warn日志
+     * @param msg
+     */
+    fun w(tag: String?, msg: String?)
+
+    /**
+     * error日志
+     * @param msg
+     */
+    fun e(tag: String?, msg: String?)
+
+    /**
+     * exception日志
+     * @param throwable
+     */
+    fun e(tag: String?, throwable: Throwable)
+}

+ 16 - 0
app/src/main/java/com/example/whatever/util/log/LogDiskStrategy.kt

@@ -0,0 +1,16 @@
+package com.example.whatever.util.log
+
+import android.os.Handler
+import com.orhanobut.logger.DiskLogStrategy
+
+/**
+ *
+ * 功能描述:日志打印策略
+ * 使用场景:需要打印日志并输出到本地文件夹中
+ */
+class LogDiskStrategy(private val handler: Handler) : DiskLogStrategy(handler) {
+    override fun log(level: Int, tag: String?, message: String) {
+        // do nothing on the calling thread, simply pass the tag/msg to the background thread
+        handler.sendMessage(handler.obtainMessage(level, message))
+    }
+}

+ 64 - 0
app/src/main/java/com/example/whatever/util/log/LogHandle.kt

@@ -0,0 +1,64 @@
+package com.example.whatever.util.log
+
+import android.content.Context
+import android.os.Handler
+import android.os.HandlerThread
+import com.orhanobut.logger.*
+
+/**
+ * 功能描述:打印日志到本地
+ */
+class LogHandle : ILog {
+    override fun init(mContext: Context,logPath: String) {
+        val ht = HandlerThread("AndroidFileLogger.$logPath")
+        ht.start()
+        try {
+            //单个文件最大限制5M 超过则创建新文件记录日志
+            val maxFileSize = 5 * 1024 * 1024
+            //日志打印线程
+            val cxHandle: Handler = LogWriteHandler(ht.looper, logPath, maxFileSize)
+            //创建缓存策略
+            val diskLogStrategy: LogStrategy = LogDiskStrategy(cxHandle)
+            //构建格式策略
+//            val strategy: FormatStrategy = CsvFormatStrategy.newBuilder().logStrategy(diskLogStrategy).build()
+            val strategy: FormatStrategy =
+                MyCsvFormatStrategy.newBuilder().logStrategy(diskLogStrategy).build(mContext)
+
+//            val formatStrategy = PrettyFormatStrategy.newBuilder()
+//                    .showThreadInfo(true)     // (可选)是否显示线程信息。 默认值为true
+//                    .methodCount(5)            //(可选)要显示的方法行数。 默认2
+//                    .methodOffset(0)           //(可选)设置调用堆栈的函数偏移值,0的话则从打印该Log的函数开始输出堆栈信息,默认是0
+//                    .logStrategy(diskLogStrategy)  //(可选)更改要打印的日志策略。 默认LogCat
+//                    .tag("Logger") //(可选)TAG内容. 默认是 PRETTY_LOGGER
+//                    .build();
+
+            //创建适配器
+            val adapter = DiskLogAdapter(strategy)
+            //设置日志适配器
+            Logger.addLogAdapter(adapter)
+        } catch (e: Exception) {
+            e.printStackTrace()
+            ht.quit() //退出
+        }
+    }
+
+    override fun d(tag: String?, log: String?) {
+        Logger.t(tag).d(log)
+    }
+
+    override fun i(tag: String?, log: String?) {
+        Logger.t(tag).d(log)
+    }
+
+    override fun w(tag: String?, log: String?) {
+        Logger.t(tag).d(log)
+    }
+
+    override fun e(tag: String?, log: String?) {
+        Logger.t(tag).e(log!!)
+    }
+
+    override fun e(tag: String?, throwable: Throwable) {
+        Logger.t(tag).e(throwable, "App crash")
+    }
+}

+ 178 - 0
app/src/main/java/com/example/whatever/util/log/LogUtil.kt

@@ -0,0 +1,178 @@
+package com.example.whatever.util.log
+
+import android.content.Context
+import android.util.Log
+import java.io.File
+import java.text.DateFormat
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.Collections
+
+/**
+ * Log工具类
+ */
+object LogUtil {
+    //使用接口 避免耦合 后续若要更换日志打印框架 直接new 其它实现类即可
+    private val log: ILog = LogHandle()
+
+    // 是否打印代码行数
+    private const val isPrintLine = false
+
+    /**
+     * 日志初始化
+     *
+     * @param path 日志文件路径
+     */
+    fun init(mContext: Context,path: String) {
+        log.init(mContext,path)
+        d("日志初始化成功!")
+        clearOldFolder(path)
+    }
+
+    /**
+     * debug日志
+     *
+     * @param str
+     */
+    fun d(str: String?) {
+        log.d(Thread.currentThread().id.toString() + "  " + generateTag(), str)
+        Log.d(generateTag(), str!!)
+    }
+
+    /**
+     * info日志
+     *
+     * @param str
+     */
+    fun i(str: String?) {
+        log.i(Thread.currentThread().id.toString() + "  " + generateTag(), str)
+        Log.i(generateTag(), str!!)
+    }
+
+    /**
+     * warn日志
+     *
+     * @param str
+     */
+    fun w(str: String?) {
+        log.w(Thread.currentThread().id.toString() + "  " + generateTag(), str)
+        Log.w(generateTag(), str!!)
+    }
+
+    /**
+     * error日志
+     *
+     * @param str
+     */
+    @JvmStatic
+    fun e(str: String?) {
+        log.e(Thread.currentThread().id.toString() + "  " + generateTag(), str)
+        Log.e(generateTag(), str!!)
+    }
+
+    /**
+     * exception日志
+     *
+     * @param throwable
+     */
+    fun e(throwable: Throwable) {
+        log.e(Thread.currentThread().id.toString() + "  " + generateTag(), throwable)
+        Log.e(generateTag(), throwable.message.toString())
+    }
+
+    fun generateTag(): String {
+        val caller = Thread.currentThread().stackTrace[4]
+        var callerClazzName = caller.className
+        callerClazzName = callerClazzName.substring(callerClazzName.lastIndexOf(".") + 1)
+        var tag = "%s.%s"
+        if (isPrintLine) {
+            tag += "(Line:%d)"
+            tag = String.format(tag, callerClazzName, caller.methodName, caller.lineNumber)
+        } else {
+            // 打印方法名
+            tag = String.format(tag, callerClazzName, caller.methodName)
+
+            // 不打印方法名
+            // tag = "%s";
+            // tag = String.format(tag, callerClazzName);
+        }
+        return tag
+    }
+
+    /**
+     * 清理超过15天的日志
+     *
+     * @param folderPath 文件夹路径
+     */
+    private fun clearOldFolder(folderPath: String) {
+        val list: List<String> = getAllFolderName(folderPath)
+        // 日志文件夹只保留15天的
+        if (list.size > 15) {
+            deleteFirstFolder(list, folderPath)
+        }
+    }
+
+    /**
+     * 获取所有文件夹名称
+     *
+     * @param folderPath 文件夹路径
+     * @return 文件夹名称集合
+     */
+    private fun getAllFolderName(folderPath: String): ArrayList<String> {
+        val fileList = ArrayList<String>()
+        val file = File(folderPath)
+        val tempList = file.listFiles()
+        tempList?.let {
+            for (i in it.indices) {
+                if (it[i].isDirectory) {
+                    d("文件夹:" + it[i].name)
+                    fileList.add(it[i].name)
+                }
+            }
+        }
+        return fileList
+    }
+
+    /**
+     * 按日期排序删除最早的一个文件夹
+     */
+    private fun deleteFirstFolder(list: List<String>?, path: String) {
+        if (list.isNullOrEmpty()) {
+            e("deleteFirstFolder fail")
+            return
+        }
+        // 先排序
+        Collections.sort(list) { o1: String, o2: String ->
+            // 按照时间进行降序排列
+            val timeFormat: DateFormat = SimpleDateFormat("yyyy-MM-dd") //日期格式
+            try {
+                val date0 = timeFormat.parse(o1)
+                val date1 = timeFormat.parse(o2)
+                if (date0.time > date1.time) {
+                    return@sort 1
+                }
+                if (o1 === o2) {
+                    return@sort 0
+                }
+            } catch (e: ParseException) {
+                e("日期转换异常")
+                e.printStackTrace()
+            }
+            -1
+        }
+        val dirPath = path + File.separator + list[0]
+        e("dir path : $dirPath")
+        val file = File(dirPath)
+        if (file.exists() && file.isDirectory) {
+            val childFiles = file.listFiles()
+            if (childFiles == null || childFiles.size == 0) {
+                file.delete()
+            } else {
+                for (i in childFiles.indices) {
+                    childFiles[i].delete()
+                }
+                file.delete()
+            }
+        }
+    }
+}

+ 81 - 0
app/src/main/java/com/example/whatever/util/log/LogWriteHandler.kt

@@ -0,0 +1,81 @@
+package com.example.whatever.util.log
+
+import android.os.Handler
+import android.os.Looper
+import android.os.Message
+
+import java.io.File
+import java.io.FileWriter
+import java.io.IOException
+import java.text.SimpleDateFormat
+import java.util.*
+
+import kotlin.Throws
+
+/**
+ * 功能描述:日志写入类
+ */
+class LogWriteHandler(
+    looper: Looper?, //日志存储路径
+    private val folder: String, //单个日志最大占用内存
+    private val maxFileSize: Int
+) : Handler(looper!!) {
+    private val sdfTime = SimpleDateFormat("yyyy-MM-dd")
+    override fun handleMessage(msg: Message) {
+        val content = msg.obj as String
+        var fileWriter: FileWriter? = null
+        val logFile = getLogFile(folder, "logs")
+        try {
+            fileWriter = FileWriter(logFile, true)
+            writeLog(fileWriter, content)
+            fileWriter.flush()
+            fileWriter.close()
+        } catch (e: IOException) {
+            handleException(fileWriter)
+        }
+    }
+
+    private fun handleException(fileWriter: FileWriter?) {
+        if (fileWriter == null) return
+        try {
+            fileWriter.flush()
+            fileWriter.close()
+        } catch (e1: IOException) {
+            e1.printStackTrace()
+        }
+    }
+
+    /**
+     * This is always called on a single background thread.
+     * Implementing classes must ONLY write to the fileWriter and nothing more.
+     * The abstract class takes care of everything else including close the stream and catching IOException
+     *
+     * @param fileWriter an instance of FileWriter already initialised to the correct file
+     */
+    @Throws(IOException::class)
+    private fun writeLog(fileWriter: FileWriter, content: String) {
+        fileWriter.append(content)
+    }
+
+    private fun getLogFile(folderName: String, fileName: String): File {
+        var folderName = folderName
+        var fileName = fileName
+        folderName = "$folderName/$time"
+        fileName = "$fileName-$time"
+        val folder = File(folderName)
+        if (!folder.exists()) folder.mkdirs()
+        var newFileCount = 0
+        var existingFile: File? = null
+        var newFile = File(folder, String.format("%s-%s.txt", fileName, newFileCount))
+        while (newFile.exists()) {
+            existingFile = newFile
+            newFileCount++
+            newFile = File(folder, String.format("%s-%s.txt", fileName, newFileCount))
+        }
+        if (existingFile == null) return newFile
+        return if (existingFile.length() >= maxFileSize) newFile else existingFile
+    }
+
+    private val time: String
+        private get() = sdfTime.format(Date())
+}

+ 147 - 0
app/src/main/java/com/example/whatever/util/log/MyCsvFormatStrategy.kt

@@ -0,0 +1,147 @@
+package com.example.whatever.util.log
+
+import android.content.Context
+import android.os.Handler
+import android.os.HandlerThread
+import android.text.TextUtils
+import com.example.whatever.util.FileUtil
+import com.orhanobut.logger.FormatStrategy
+import com.orhanobut.logger.LogStrategy
+import com.orhanobut.logger.Logger
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+class MyCsvFormatStrategy private constructor(builder: Builder) : FormatStrategy {
+    private val date: Date
+    private val dateFormat: SimpleDateFormat
+    private val logStrategy: LogStrategy
+    private val tag: String?
+
+    init {
+        checkNotNull(builder)
+        date = builder.date!!
+        dateFormat = builder.dateFormat!!
+        logStrategy = builder.logStrategy!!
+        tag = builder.tag
+    }
+
+    override fun log(priority: Int, onceOnlyTag: String?, message: String) {
+        var message = message
+        checkNotNull(message)
+        val tag = formatTag(onceOnlyTag)
+        date.time = System.currentTimeMillis()
+        val builder = StringBuilder()
+
+        // machine-readable date/time
+//        builder.append(Long.toString(date.getTime()));
+
+        // human-readable date/time
+//        builder.append(SEPARATOR);
+        builder.append(dateFormat.format(date))
+
+        // level
+        builder.append(SEPARATOR + SEPARATOR)
+        builder.append(logLevel(priority))
+
+        // tag
+        builder.append(SEPARATOR + SEPARATOR)
+        //        builder.append(tag);
+        builder.append(onceOnlyTag)
+
+        // message
+        if (message.contains(NEW_LINE)) {
+            // a new line would break the CSV format, so we replace it here
+            message = message.replace(NEW_LINE.toRegex(), NEW_LINE_REPLACEMENT)
+        }
+        builder.append(" : ")
+        builder.append(message)
+
+        // new line
+        builder.append(NEW_LINE)
+        logStrategy.log(priority, tag, builder.toString())
+    }
+
+    private fun formatTag(tag: String?): String? {
+        return if (!TextUtils.isEmpty(tag) && !TextUtils.equals(this.tag, tag)) {
+            this.tag + "-" + tag
+        } else this.tag
+    }
+
+    class Builder {
+        var date: Date? = null
+        var dateFormat: SimpleDateFormat? = null
+        var logStrategy: LogStrategy? = null
+        var tag: String? = "PRETTY_LOGGER"
+        fun date(`val`: Date?): Builder {
+            date = `val`
+            return this
+        }
+
+        fun dateFormat(`val`: SimpleDateFormat?): Builder {
+            dateFormat = `val`
+            return this
+        }
+
+        fun logStrategy(`val`: LogStrategy?): Builder {
+            logStrategy = `val`
+            return this
+        }
+
+        fun tag(tag: String?): Builder {
+            this.tag = tag
+            return this
+        }
+
+        fun build(mContext: Context): MyCsvFormatStrategy {
+            if (date == null) {
+                date = Date()
+            }
+            if (dateFormat == null) {
+                dateFormat = SimpleDateFormat("yyyy.MM.dd HH:mm:ss.SSS", Locale.UK)
+            }
+            if (logStrategy == null) {
+                val diskPath = FileUtil.getRootFolder(mContext)?.absolutePath
+                val folder = diskPath + File.separatorChar + "logger"
+                val ht = HandlerThread("AndroidFileLogger.$folder")
+                ht.start()
+                val handler: Handler = MyDiskLogStrategy.WriteHandler(ht.looper, folder, MAX_BYTES)
+                logStrategy = MyDiskLogStrategy(handler)
+            }
+            return MyCsvFormatStrategy(this)
+        }
+
+        companion object {
+            private const val MAX_BYTES = 500 * 1024 // 500K averages to a 4000 lines per file
+        }
+    }
+
+    companion object {
+        private val NEW_LINE = System.getProperty("line.separator")
+        private const val NEW_LINE_REPLACEMENT = " <br> "
+        private const val SEPARATOR = " "
+        fun newBuilder(): Builder {
+            return Builder()
+        }
+
+        fun logLevel(value: Int): String {
+            return when (value) {
+                Logger.VERBOSE -> "V"
+                Logger.DEBUG -> "D"
+                Logger.INFO -> "I"
+                Logger.WARN -> "W"
+                Logger.ERROR -> "E"
+                Logger.ASSERT -> "A"
+                else -> "UNKNOWN"
+            }
+        }
+
+        fun <T> checkNotNull(obj: T?): T {
+            if (obj == null) {
+                throw NullPointerException()
+            }
+            return obj
+        }
+    }
+}

+ 105 - 0
app/src/main/java/com/example/whatever/util/log/MyDiskLogStrategy.kt

@@ -0,0 +1,105 @@
+package com.example.whatever.util.log
+
+import android.os.Handler
+import android.os.Looper
+import android.os.Message
+
+import com.orhanobut.logger.LogStrategy
+
+import java.io.File
+import java.io.FileWriter
+import java.io.IOException
+
+class MyDiskLogStrategy(handler: Handler) : LogStrategy {
+    private val handler: Handler
+
+    init {
+        this.handler = checkNotNull(handler)
+    }
+
+    override fun log(level: Int, tag: String?, message: String) {
+        checkNotNull(message)
+
+        // do nothing on the calling thread, simply pass the tag/msg to the background thread
+        handler.sendMessage(handler.obtainMessage(level, message))
+    }
+
+    internal class WriteHandler(looper: Looper, folder: String, maxFileSize: Int) : Handler(
+        checkNotNull(looper)
+    ) {
+        private val folder: String
+        private val maxFileSize: Int
+
+        init {
+            this.folder = checkNotNull(folder)
+            this.maxFileSize = maxFileSize
+        }
+
+        override fun handleMessage(msg: Message) {
+            val content = msg.obj as String
+            var fileWriter: FileWriter? = null
+            val logFile = getLogFile(folder, "logs")
+            try {
+                fileWriter = FileWriter(logFile, true)
+                writeLog(fileWriter, content)
+                fileWriter.flush()
+                fileWriter.close()
+            } catch (e: IOException) {
+                if (fileWriter != null) {
+                    try {
+                        fileWriter.flush()
+                        fileWriter.close()
+                    } catch (e1: IOException) { /* fail silently */
+                    }
+                }
+            }
+        }
+
+        /**
+         * This is always called on a single background thread.
+         * Implementing classes must ONLY write to the fileWriter and nothing more.
+         * The abstract class takes care of everything else including close the stream and catching IOException
+         *
+         * @param fileWriter an instance of FileWriter already initialised to the correct file
+         */
+        @Throws(IOException::class)
+        private fun writeLog(fileWriter: FileWriter, content: String) {
+            checkNotNull(fileWriter)
+            checkNotNull(content)
+            fileWriter.append(content)
+        }
+
+        private fun getLogFile(folderName: String, fileName: String): File {
+            checkNotNull(folderName)
+            checkNotNull(fileName)
+            val folder = File(folderName)
+            if (!folder.exists()) {
+                //TODO: What if folder is not created, what happens then?
+                folder.mkdirs()
+            }
+            var newFileCount = 0
+            var newFile: File
+            var existingFile: File? = null
+            newFile = File(folder, String.format("%s_%s.txt", fileName, newFileCount))
+            while (newFile.exists()) {
+                existingFile = newFile
+                newFileCount++
+                newFile = File(folder, String.format("%s_%s.txt", fileName, newFileCount))
+            }
+            return if (existingFile != null) {
+                if (existingFile.length() >= maxFileSize) {
+                    newFile
+                } else existingFile
+            } else newFile
+        }
+    }
+
+    companion object {
+        fun <T> checkNotNull(obj: T?): T {
+            if (obj == null) {
+                throw NullPointerException()
+            }
+            return obj
+        }
+    }
+}

BIN
app/src/main/jniLibs/armeabi-v7a/libserial_port.so


+ 170 - 0
app/src/main/res/drawable/ic_launcher_background.xml

@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path
+        android:fillColor="#3DDC84"
+        android:pathData="M0,0h108v108h-108z" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M9,0L9,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,0L19,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,0L29,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,0L39,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,0L49,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,0L59,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,0L69,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,0L79,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M89,0L89,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M99,0L99,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,9L108,9"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,19L108,19"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,29L108,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,39L108,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,49L108,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,59L108,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,69L108,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,79L108,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,89L108,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,99L108,99"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,29L89,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,39L89,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,49L89,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,59L89,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,69L89,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,79L89,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,19L29,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,19L39,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,19L49,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,19L59,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,19L69,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,19L79,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+</vector>

+ 30 - 0
app/src/main/res/drawable/ic_launcher_foreground.xml

@@ -0,0 +1,30 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+        <aapt:attr name="android:fillColor">
+            <gradient
+                android:endX="85.84757"
+                android:endY="92.4963"
+                android:startX="42.9492"
+                android:startY="49.59793"
+                android:type="linear">
+                <item
+                    android:color="#44000000"
+                    android:offset="0.0" />
+                <item
+                    android:color="#00000000"
+                    android:offset="1.0" />
+            </gradient>
+        </aapt:attr>
+    </path>
+    <path
+        android:fillColor="#FFFFFF"
+        android:fillType="nonZero"
+        android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+        android:strokeWidth="1"
+        android:strokeColor="#00000000" />
+</vector>

+ 36 - 0
app/src/main/res/layout/activity_ble.xml

@@ -0,0 +1,36 @@
+<?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"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/main"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@color/white"
+    android:orientation="vertical"
+    tools:context=".activity.BleActivity">
+
+    <com.scwang.smart.refresh.layout.SmartRefreshLayout
+        android:id="@+id/srl"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <com.scwang.smart.refresh.header.ClassicsHeader
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:srlTextRefreshing="正在扫描..." />
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/rv_ble"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:overScrollMode="never"
+            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+            tools:itemCount="100"
+            tools:listitem="@layout/item_ble" />
+
+        <com.scwang.smart.refresh.footer.ClassicsFooter
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+    </com.scwang.smart.refresh.layout.SmartRefreshLayout>
+
+</LinearLayout>

+ 79 - 0
app/src/main/res/layout/activity_cmd.xml

@@ -0,0 +1,79 @@
+<?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"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/main"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    tools:context=".activity.CmdActivity">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+        <TextView
+            android:id="@+id/indicate"
+            android:layout_width="120dp"
+            android:layout_height="50dp"
+            android:text="Indicate"
+            android:textSize="20sp"
+            android:background="#456789"
+            android:gravity="center"/>
+
+        <TextView
+            android:id="@+id/token"
+            android:layout_width="120dp"
+            android:layout_height="50dp"
+            android:text="Get Token"
+            android:textSize="20sp"
+            android:background="#456789"
+            android:gravity="center"
+            android:layout_marginLeft="20dp"/>
+
+        <TextView
+            android:id="@+id/version"
+            android:layout_width="120dp"
+            android:layout_height="50dp"
+            android:text="Get Version"
+            android:textSize="20sp"
+            android:background="#456789"
+            android:gravity="center"
+            android:layout_marginLeft="20dp"/>
+
+        <TextView
+            android:id="@+id/power"
+            android:layout_width="120dp"
+            android:layout_height="50dp"
+            android:text="Get Power"
+            android:textSize="20sp"
+            android:background="#456789"
+            android:gravity="center"
+            android:layout_marginLeft="20dp"/>
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:layout_marginTop="20dp">
+        <TextView
+            android:id="@+id/status"
+            android:layout_width="120dp"
+            android:layout_height="50dp"
+            android:text="Status"
+            android:textSize="20sp"
+            android:background="#456789"
+            android:gravity="center"/>
+    </LinearLayout>
+
+    <TextView
+        android:id="@+id/exit"
+        android:layout_width="120dp"
+        android:layout_height="50dp"
+        android:text="Exit"
+        android:textSize="20sp"
+        android:background="#456789"
+        android:gravity="center"
+        android:layout_marginTop="20dp"/>
+</LinearLayout>

+ 30 - 0
app/src/main/res/layout/activity_main.xml

@@ -0,0 +1,30 @@
+<?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"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/main"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="horizontal"
+    tools:context=".activity.MainActivity">
+
+    <Button
+        android:id="@+id/ble"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:minWidth="0dp"
+        android:minHeight="0dp"
+        android:text="BLE"
+        android:textSize="10sp"
+        android:layout_margin="5dp"/>
+
+    <Button
+        android:id="@+id/modbus"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:minWidth="0dp"
+        android:minHeight="0dp"
+        android:text="ModBus"
+        android:textSize="10sp"
+        android:layout_margin="5dp"/>
+</LinearLayout>

+ 25 - 0
app/src/main/res/layout/activity_modbus.xml

@@ -0,0 +1,25 @@
+<?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"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/main"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    tools:context=".activity.ModbusActivity">
+
+    <TextView
+        android:id="@+id/textView"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+
+    <Button
+        android:id="@+id/send"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:minWidth="0dp"
+        android:minHeight="0dp"
+        android:text="Send"
+        android:textSize="10sp"
+        android:layout_margin="5dp"/>
+</LinearLayout>

+ 23 - 0
app/src/main/res/layout/activity_screen_adapt.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout 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:id="@+id/main"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".activity.ScreenAdaptActivity">
+
+    <ImageView
+        android:layout_width="180dp"
+        android:layout_height="640dp"
+        android:background="#123456"/>
+
+    <TextView
+        android:id="@+id/tv_test"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerInParent="true"
+        android:text="Hello World!"
+        android:textSize="50sp"/>
+
+</RelativeLayout>

+ 15 - 0
app/src/main/res/layout/activity_web.xml

@@ -0,0 +1,15 @@
+<?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:id="@+id/main"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".activity.WebActivity">
+
+    <WebView
+        android:id="@+id/webView"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 214 - 0
app/src/main/res/layout/item_ble.xml

@@ -0,0 +1,214 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    xmlns:tools="http://schemas.android.com/tools"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:orientation="vertical"
+    android:id="@+id/main"
+    android:layout_marginHorizontal="16dp"
+    android:layout_marginVertical="10dp">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <TextView
+            android:id="@+id/ble_name"
+            android:layout_width="200dp"
+            android:layout_height="wrap_content"
+            tools:text="名称:蓝牙名称"
+            android:textSize="10sp"/>
+
+        <TextView
+            android:id="@+id/ble_mac"
+            android:layout_width="200dp"
+            android:layout_height="wrap_content"
+            tools:text="mac:蓝牙名称"
+            android:textSize="10sp"/>
+
+        <TextView
+            android:id="@+id/blc_connection"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            tools:text="mac:蓝牙名称"
+            android:textSize="10sp"/>
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+        <Button
+            android:id="@+id/connect"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minWidth="0dp"
+            android:minHeight="0dp"
+            android:text="Connect"
+            android:textSize="10sp"
+            android:layout_margin="5dp"/>
+
+        <Button
+            android:id="@+id/disconnect"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minWidth="0dp"
+            android:minHeight="0dp"
+            android:text="Disconnect"
+            android:textSize="10sp"
+            android:layout_margin="5dp"/>
+
+        <Button
+            android:id="@+id/mtu"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minWidth="0dp"
+            android:minHeight="0dp"
+            android:text="Mtu"
+            android:textSize="10sp"
+            android:layout_margin="5dp"/>
+
+        <Button
+            android:id="@+id/indicate"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minWidth="0dp"
+            android:minHeight="0dp"
+            android:text="Indicate"
+            android:textSize="10sp"
+            android:layout_margin="5dp"/>
+
+        <Button
+            android:id="@+id/stopIndicate"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minWidth="0dp"
+            android:minHeight="0dp"
+            android:text="Stop Indicate"
+            android:textSize="10sp"
+            android:layout_margin="5dp"/>
+
+        <Button
+            android:id="@+id/exit"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minWidth="0dp"
+            android:minHeight="0dp"
+            android:text="Exit"
+            android:textSize="10sp"
+            android:layout_margin="5dp"/>
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <Button
+            android:id="@+id/token"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minWidth="0dp"
+            android:minHeight="0dp"
+            android:text="Token"
+            android:textSize="10sp"
+            android:layout_margin="5dp"/>
+
+        <Button
+            android:id="@+id/version"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minWidth="0dp"
+            android:minHeight="0dp"
+            android:text="Version"
+            android:textSize="10sp"
+            android:layout_margin="5dp"/>
+
+        <Button
+            android:id="@+id/power"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minWidth="0dp"
+            android:minHeight="0dp"
+            android:text="Power"
+            android:textSize="10sp"
+            android:layout_margin="5dp"/>
+
+        <Button
+            android:id="@+id/device"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minWidth="0dp"
+            android:minHeight="0dp"
+            android:text="Device Status"
+            android:textSize="10sp"
+            android:layout_margin="5dp"/>
+
+        <Button
+            android:id="@+id/mode1"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minWidth="0dp"
+            android:minHeight="0dp"
+            android:text="Mode1"
+            android:textSize="10sp"
+            android:layout_margin="5dp"/>
+
+        <Button
+            android:id="@+id/mode2"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minWidth="0dp"
+            android:minHeight="0dp"
+            android:text="Mode2"
+            android:textSize="10sp"
+            android:layout_margin="5dp"/>
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <Button
+            android:id="@+id/ticketStatus"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minWidth="0dp"
+            android:minHeight="0dp"
+            android:text="Ticket Status"
+            android:textSize="10sp"
+            android:layout_margin="5dp"/>
+
+        <Button
+            android:id="@+id/sendTicket"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minWidth="0dp"
+            android:minHeight="0dp"
+            android:text="Send Ticket"
+            android:textSize="10sp"
+            android:layout_margin="5dp"/>
+
+        <Button
+            android:id="@+id/sendFile"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:minWidth="0dp"
+            android:minHeight="0dp"
+            android:text="Send File"
+            android:textSize="10sp"
+            android:layout_margin="5dp"/>
+    </LinearLayout>
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:background="@color/black"
+        app:layout_constraintBottom_toBottomOf="parent"/>
+
+
+</LinearLayout>

+ 6 - 0
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

+ 6 - 0
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp


BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp


BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp


BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp


BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp


BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp


BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp


BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp


BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp


BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp


+ 7 - 0
app/src/main/res/values-night/themes.xml

@@ -0,0 +1,7 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+    <!-- Base application theme. -->
+    <style name="Base.Theme.Whatever" parent="Theme.Material3.DayNight.NoActionBar">
+        <!-- Customize your dark theme here. -->
+        <!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
+    </style>
+</resources>

+ 6 - 0
app/src/main/res/values/colors.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="black">#FF000000</color>
+    <color name="white">#FFFFFFFF</color>
+    <color name="aquamarine">#7FFFD4</color>
+</resources>

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

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

+ 9 - 0
app/src/main/res/values/themes.xml

@@ -0,0 +1,9 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+    <!-- Base application theme. -->
+    <style name="Base.Theme.Whatever" parent="Theme.Material3.DayNight.NoActionBar">
+        <!-- Customize your light theme here. -->
+        <!-- <item name="colorPrimary">@color/my_light_primary</item> -->
+    </style>
+
+    <style name="Theme.Whatever" parent="Base.Theme.Whatever" />
+</resources>

+ 13 - 0
app/src/main/res/xml/backup_rules.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+   Sample backup rules file; uncomment and customize as necessary.
+   See https://developer.android.com/guide/topics/data/autobackup
+   for details.
+   Note: This file is ignored for devices older that API 31
+   See https://developer.android.com/about/versions/12/backup-restore
+-->
+<full-backup-content>
+    <!--
+   <include domain="sharedpref" path="."/>
+   <exclude domain="sharedpref" path="device.xml"/>
+-->
+</full-backup-content>

+ 4 - 0
app/src/main/res/xml/common_network_config.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+    <base-config cleartextTrafficPermitted="true" />
+</network-security-config>

+ 19 - 0
app/src/main/res/xml/data_extraction_rules.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+   Sample data extraction rules file; uncomment and customize as necessary.
+   See https://developer.android.com/about/versions/12/backup-restore#xml-changes
+   for details.
+-->
+<data-extraction-rules>
+    <cloud-backup>
+        <!-- TODO: Use <include> and <exclude> to control what is backed up.
+        <include .../>
+        <exclude .../>
+        -->
+    </cloud-backup>
+    <!--
+    <device-transfer>
+        <include .../>
+        <exclude .../>
+    </device-transfer>
+    -->
+</data-extraction-rules>

+ 17 - 0
app/src/test/java/com/example/whatever/ExampleUnitTest.kt

@@ -0,0 +1,17 @@
+package com.example.whatever
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+    @Test
+    fun addition_isCorrect() {
+        assertEquals(4, 2 + 2)
+    }
+}

+ 5 - 0
build.gradle

@@ -0,0 +1,5 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+alias(libs.plugins.android.application) apply false
+    alias(libs.plugins.jetbrains.kotlin.android) apply false
+}

+ 24 - 0
gradle.properties

@@ -0,0 +1,24 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
+android.enableJetifier=true

+ 36 - 0
gradle/libs.versions.toml

@@ -0,0 +1,36 @@
+[versions]
+agp = "8.5.1"
+kotlin = "1.9.0"
+coreKtx = "1.13.0"
+junit = "4.13.2"
+junitVersion = "1.1.5"
+espressoCore = "3.5.1"
+appcompat = "1.7.0"
+material = "1.10.0"
+activity = "1.8.0"
+constraintlayout = "2.1.4"
+mvvmhabit = "4.0.0"
+okhttps = "4.0.2"
+log-interceptor = "3.14.9"
+autosize = "v1.2.1"
+fastble = "2.4.0"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
+androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+mvvmhabit = { group = "com.github.goldze", name = "MVVMHabit", version.ref = "mvvmhabit" }
+okhttps = { group = "cn.zhxu", name = "okhttps-gson", version.ref = "okhttps" }
+log-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "log-interceptor" }
+autosize = { group = "com.github.JessYanCoding", name = "AndroidAutoSize", version.ref = "autosize" }
+fastble = { group = "com.github.Jasonchenlijian", name = "FastBle", version.ref = "fastble" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+

BIN
gradle/wrapper/gradle-wrapper.jar


+ 6 - 0
gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Thu Aug 29 16:56:56 GMT+08:00 2024
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https://mirrors.cloud.tencent.com/gradle/gradle-8.7-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 185 - 0
gradlew

@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# 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
+#
+#      https://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.
+#
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=`expr $i + 1`
+    done
+    case $i in
+        0) set -- ;;
+        1) set -- "$args0" ;;
+        2) set -- "$args0" "$args1" ;;
+        3) set -- "$args0" "$args1" "$args2" ;;
+        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"

+ 89 - 0
gradlew.bat

@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 30 - 0
settings.gradle

@@ -0,0 +1,30 @@
+pluginManagement {
+    repositories {
+        maven { url 'https://maven.aliyun.com/repository/google' }
+//        maven { url 'https://maven.aliyun.com/repository/jcenter' }
+        maven { url 'https://maven.aliyun.com/repository/public' }
+        google {
+            content {
+                includeGroupByRegex("com\\.android.*")
+                includeGroupByRegex("com\\.google.*")
+                includeGroupByRegex("androidx.*")
+            }
+        }
+        mavenCentral()
+        gradlePluginPortal()
+    }
+}
+dependencyResolutionManagement {
+    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+    repositories {
+        maven { url 'https://maven.aliyun.com/repository/google' }
+//        maven { url 'https://maven.aliyun.com/repository/jcenter' }
+        maven { url 'https://maven.aliyun.com/repository/public' }
+        google()
+        mavenCentral()
+        maven { url 'https://jitpack.io' }
+    }
+}
+
+rootProject.name = "Whatever"
+include ':app'