GrayCarbon 1 місяць тому
коміт
137fb2c92d
82 змінених файлів з 3181 додано та 0 видалено
  1. 15 0
      .gitignore
  2. 3 0
      .idea/.gitignore
  3. 6 0
      .idea/AndroidProjectSystem.xml
  4. 6 0
      .idea/compiler.xml
  5. 443 0
      .idea/dbnavigator.xml
  6. 10 0
      .idea/deploymentTargetSelector.xml
  7. 13 0
      .idea/deviceManager.xml
  8. 20 0
      .idea/gradle.xml
  9. 61 0
      .idea/inspectionProfiles/Project_Default.xml
  10. 10 0
      .idea/migrations.xml
  11. 9 0
      .idea/misc.xml
  12. 17 0
      .idea/runConfigurations.xml
  13. 1 0
      app/.gitignore
  14. 58 0
      app/build.gradle.kts
  15. 21 0
      app/proguard-rules.pro
  16. 24 0
      app/src/androidTest/java/com/iscs/comm/ExampleInstrumentedTest.kt
  17. 27 0
      app/src/main/AndroidManifest.xml
  18. 72 0
      app/src/main/java/com/iscs/comm/MainActivity.kt
  19. 11 0
      app/src/main/java/com/iscs/comm/ui/theme/Color.kt
  20. 57 0
      app/src/main/java/com/iscs/comm/ui/theme/Theme.kt
  21. 34 0
      app/src/main/java/com/iscs/comm/ui/theme/Type.kt
  22. 170 0
      app/src/main/res/drawable/ic_launcher_background.xml
  23. 30 0
      app/src/main/res/drawable/ic_launcher_foreground.xml
  24. 6 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  25. 6 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  26. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher.webp
  27. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
  28. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher.webp
  29. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
  30. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher.webp
  31. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
  32. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
  33. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
  34. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
  35. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
  36. 10 0
      app/src/main/res/values/colors.xml
  37. 3 0
      app/src/main/res/values/strings.xml
  38. 5 0
      app/src/main/res/values/themes.xml
  39. 13 0
      app/src/main/res/xml/backup_rules.xml
  40. 19 0
      app/src/main/res/xml/data_extraction_rules.xml
  41. 17 0
      app/src/test/java/com/iscs/comm/ExampleUnitTest.kt
  42. 7 0
      build.gradle.kts
  43. 23 0
      gradle.properties
  44. 37 0
      gradle/libs.versions.toml
  45. BIN
      gradle/wrapper/gradle-wrapper.jar
  46. 8 0
      gradle/wrapper/gradle-wrapper.properties
  47. 251 0
      gradlew
  48. 94 0
      gradlew.bat
  49. 24 0
      settings.gradle.kts
  50. 1 0
      transport/.gitignore
  51. 52 0
      transport/build.gradle.kts
  52. 0 0
      transport/consumer-rules.pro
  53. 21 0
      transport/proguard-rules.pro
  54. 24 0
      transport/src/androidTest/java/com/iscs/comm/ExampleInstrumentedTest.kt
  55. 4 0
      transport/src/main/AndroidManifest.xml
  56. 37 0
      transport/src/main/cpp/CMakeLists.txt
  57. 215 0
      transport/src/main/cpp/comm_can.cpp
  58. 1 0
      transport/src/main/cpp/comm_serial.cpp
  59. 116 0
      transport/src/main/java/com/iscs/comm/CommManager.kt
  60. 41 0
      transport/src/main/java/com/iscs/comm/entity/Frame.kt
  61. 63 0
      transport/src/main/java/com/iscs/comm/entity/device/Device.kt
  62. 82 0
      transport/src/main/java/com/iscs/comm/entity/device/DeviceKeySlot.kt
  63. 51 0
      transport/src/main/java/com/iscs/comm/entity/device/DeviceLockSlot.kt
  64. 23 0
      transport/src/main/java/com/iscs/comm/entity/device/DeviceNone.kt
  65. 16 0
      transport/src/main/java/com/iscs/comm/entity/device/status/DeviceStatus.kt
  66. 76 0
      transport/src/main/java/com/iscs/comm/entity/device/status/DeviceStatusKeySlot.kt
  67. 60 0
      transport/src/main/java/com/iscs/comm/entity/device/status/DeviceStatusLockSlot.kt
  68. 15 0
      transport/src/main/java/com/iscs/comm/entity/device/status/DeviceStatusNone.kt
  69. 22 0
      transport/src/main/java/com/iscs/comm/entity/device/status/bean/SlotBean.kt
  70. 12 0
      transport/src/main/java/com/iscs/comm/entity/device/status/bean/SlotKeyBean.kt
  71. 10 0
      transport/src/main/java/com/iscs/comm/enums/CommType.kt
  72. 7 0
      transport/src/main/java/com/iscs/comm/enums/DeviceType.kt
  73. 81 0
      transport/src/main/java/com/iscs/comm/extension/CommFrameExt.kt
  74. 42 0
      transport/src/main/java/com/iscs/comm/extension/DataConvertExt.kt
  75. 25 0
      transport/src/main/java/com/iscs/comm/extension/DeviceFactoryExt.kt
  76. 100 0
      transport/src/main/java/com/iscs/comm/intf/AbsCommBase.kt
  77. 21 0
      transport/src/main/java/com/iscs/comm/intf/IDeviceListener.kt
  78. 52 0
      transport/src/main/java/com/iscs/comm/jni/NativeCan.kt
  79. 150 0
      transport/src/main/java/com/iscs/comm/manager/CanManager.kt
  80. 34 0
      transport/src/main/java/com/iscs/comm/protocol/CanProtocol.kt
  81. 69 0
      transport/src/main/java/com/iscs/comm/utils/ShellTools.kt
  82. 17 0
      transport/src/test/java/com/iscs/comm/ExampleUnitTest.kt

+ 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

+ 6 - 0
.idea/AndroidProjectSystem.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="AndroidProjectSystem">
+    <option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
+  </component>
+</project>

+ 6 - 0
.idea/compiler.xml

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

+ 443 - 0
.idea/dbnavigator.xml

@@ -0,0 +1,443 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="DBNavigator.Project.DDLFileAttachmentManager">
+    <mappings />
+    <preferences />
+  </component>
+  <component name="DBNavigator.Project.DatabaseAssistantManager">
+    <assistants />
+  </component>
+  <component name="DBNavigator.Project.DatabaseFileManager">
+    <open-files />
+  </component>
+  <component name="DBNavigator.Project.ExecutionManager">
+    <retain-sticky-names value="false" />
+  </component>
+  <component name="DBNavigator.Project.Settings">
+    <connections />
+    <browser-settings>
+      <general>
+        <display-mode value="TABBED" />
+        <navigation-history-size value="100" />
+        <show-object-details value="false" />
+        <enable-sticky-paths value="true" />
+        <enable-quick-filters value="false" />
+      </general>
+      <filters>
+        <object-type-filter>
+          <object-type name="SCHEMA" enabled="true" />
+          <object-type name="USER" enabled="true" />
+          <object-type name="ROLE" enabled="true" />
+          <object-type name="PRIVILEGE" enabled="true" />
+          <object-type name="CHARSET" enabled="true" />
+          <object-type name="TABLE" enabled="true" />
+          <object-type name="VIEW" enabled="true" />
+          <object-type name="JSON_VIEW" enabled="true" />
+          <object-type name="MATERIALIZED_VIEW" enabled="true" />
+          <object-type name="NESTED_TABLE" enabled="true" />
+          <object-type name="COLUMN" enabled="true" />
+          <object-type name="INDEX" enabled="true" />
+          <object-type name="CONSTRAINT" enabled="true" />
+          <object-type name="DATASET_TRIGGER" enabled="true" />
+          <object-type name="DATABASE_TRIGGER" enabled="true" />
+          <object-type name="SYNONYM" enabled="true" />
+          <object-type name="SEQUENCE" enabled="true" />
+          <object-type name="PROCEDURE" enabled="true" />
+          <object-type name="FUNCTION" enabled="true" />
+          <object-type name="PACKAGE" enabled="true" />
+          <object-type name="TYPE" enabled="true" />
+          <object-type name="TYPE_ATTRIBUTE" enabled="true" />
+          <object-type name="ARGUMENT" enabled="true" />
+          <object-type name="JAVA_CLASS" enabled="true" />
+          <object-type name="JAVA_FIELD" enabled="true" />
+          <object-type name="JAVA_METHOD" enabled="true" />
+          <object-type name="JAVA_RESOURCE" enabled="true" />
+          <object-type name="DIMENSION" enabled="true" />
+          <object-type name="CLUSTER" enabled="true" />
+          <object-type name="DBLINK" enabled="true" />
+          <object-type name="CREDENTIAL" enabled="true" />
+          <object-type name="AI_PROFILE" enabled="true" />
+        </object-type-filter>
+      </filters>
+      <sorting>
+        <object-type name="COLUMN" sorting-type="NAME" />
+        <object-type name="FUNCTION" sorting-type="NAME" />
+        <object-type name="PROCEDURE" sorting-type="NAME" />
+        <object-type name="ARGUMENT" sorting-type="POSITION" />
+        <object-type name="TYPE ATTRIBUTE" sorting-type="POSITION" />
+      </sorting>
+      <default-editors>
+        <object-type name="VIEW" editor-type="SELECTION" />
+        <object-type name="PACKAGE" editor-type="SELECTION" />
+        <object-type name="TYPE" editor-type="SELECTION" />
+      </default-editors>
+    </browser-settings>
+    <navigation-settings>
+      <lookup-filters>
+        <lookup-objects>
+          <object-type name="SCHEMA" enabled="true" />
+          <object-type name="USER" enabled="false" />
+          <object-type name="ROLE" enabled="false" />
+          <object-type name="PRIVILEGE" enabled="false" />
+          <object-type name="CHARSET" enabled="false" />
+          <object-type name="TABLE" enabled="true" />
+          <object-type name="VIEW" enabled="true" />
+          <object-type name="JSON VIEW" enabled="true" />
+          <object-type name="MATERIALIZED VIEW" enabled="true" />
+          <object-type name="INDEX" enabled="true" />
+          <object-type name="CONSTRAINT" enabled="true" />
+          <object-type name="DATASET TRIGGER" enabled="true" />
+          <object-type name="DATABASE TRIGGER" enabled="true" />
+          <object-type name="SYNONYM" enabled="false" />
+          <object-type name="SEQUENCE" enabled="true" />
+          <object-type name="PROCEDURE" enabled="true" />
+          <object-type name="FUNCTION" enabled="true" />
+          <object-type name="PACKAGE" enabled="true" />
+          <object-type name="TYPE" enabled="true" />
+          <object-type name="JAVA CLASS" enabled="true" />
+          <object-type name="INNER CLASS" enabled="true" />
+          <object-type name="JAVA FIELD" enabled="true" />
+          <object-type name="JAVA METHOD" enabled="true" />
+          <object-type name="JAVA PARAMETER" enabled="true" />
+          <object-type name="JAVA RESOURCE" enabled="true" />
+          <object-type name="DIMENSION" enabled="false" />
+          <object-type name="CLUSTER" enabled="false" />
+          <object-type name="DBLINK" enabled="false" />
+          <object-type name="CREDENTIAL" enabled="false" />
+        </lookup-objects>
+        <force-database-load value="false" />
+        <prompt-connection-selection value="true" />
+        <prompt-schema-selection value="true" />
+      </lookup-filters>
+    </navigation-settings>
+    <dataset-grid-settings>
+      <general>
+        <enable-zooming value="true" />
+        <enable-column-tooltip value="true" />
+      </general>
+      <sorting>
+        <nulls-first value="true" />
+        <max-sorting-columns value="4" />
+      </sorting>
+      <audit-columns>
+        <column-names value="" />
+        <visible value="true" />
+        <editable value="false" />
+      </audit-columns>
+    </dataset-grid-settings>
+    <dataset-editor-settings>
+      <text-editor-popup>
+        <active value="false" />
+        <active-if-empty value="false" />
+        <data-length-threshold value="100" />
+        <popup-delay value="1000" />
+      </text-editor-popup>
+      <values-actions-popup>
+        <show-popup-button value="true" />
+        <element-count-threshold value="1000" />
+        <data-length-threshold value="250" />
+      </values-actions-popup>
+      <general>
+        <fetch-block-size value="100" />
+        <fetch-timeout value="30" />
+        <trim-whitespaces value="true" />
+        <convert-empty-strings-to-null value="true" />
+        <select-content-on-cell-edit value="true" />
+        <large-value-preview-active value="true" />
+      </general>
+      <filters>
+        <prompt-filter-dialog value="true" />
+        <default-filter-type value="BASIC" />
+      </filters>
+      <qualified-text-editor text-length-threshold="300">
+        <content-types>
+          <content-type name="Text" enabled="true" />
+          <content-type name="Properties" enabled="true" />
+          <content-type name="XML" enabled="true" />
+          <content-type name="DTD" enabled="true" />
+          <content-type name="HTML" enabled="true" />
+          <content-type name="XHTML" enabled="true" />
+          <content-type name="Java" enabled="true" />
+          <content-type name="SQL" enabled="true" />
+          <content-type name="PL/SQL" enabled="true" />
+          <content-type name="JSON" enabled="true" />
+          <content-type name="JSON5" enabled="true" />
+          <content-type name="Groovy" enabled="true" />
+          <content-type name="AIDL" enabled="true" />
+          <content-type name="YAML" enabled="true" />
+          <content-type name="Manifest" enabled="true" />
+        </content-types>
+      </qualified-text-editor>
+      <record-navigation>
+        <navigation-target value="VIEWER" />
+      </record-navigation>
+    </dataset-editor-settings>
+    <code-editor-settings>
+      <general>
+        <show-object-navigation-gutter value="false" />
+        <show-spec-declaration-navigation-gutter value="true" />
+        <enable-spellchecking value="true" />
+        <enable-reference-spellchecking value="false" />
+      </general>
+      <confirmations>
+        <save-changes value="false" />
+        <revert-changes value="true" />
+        <exit-on-changes value="ASK" />
+      </confirmations>
+    </code-editor-settings>
+    <code-completion-settings>
+      <filters>
+        <basic-filter>
+          <filter-element type="RESERVED_WORD" id="keyword" selected="true" />
+          <filter-element type="RESERVED_WORD" id="function" selected="true" />
+          <filter-element type="RESERVED_WORD" id="parameter" selected="true" />
+          <filter-element type="RESERVED_WORD" id="datatype" selected="true" />
+          <filter-element type="RESERVED_WORD" id="exception" selected="true" />
+          <filter-element type="OBJECT" id="schema" selected="true" />
+          <filter-element type="OBJECT" id="role" selected="true" />
+          <filter-element type="OBJECT" id="user" selected="true" />
+          <filter-element type="OBJECT" id="privilege" selected="true" />
+          <user-schema>
+            <filter-element type="OBJECT" id="table" selected="true" />
+            <filter-element type="OBJECT" id="view" selected="true" />
+            <filter-element type="OBJECT" id="json view" selected="true" />
+            <filter-element type="OBJECT" id="materialized view" selected="true" />
+            <filter-element type="OBJECT" id="index" selected="true" />
+            <filter-element type="OBJECT" id="constraint" selected="true" />
+            <filter-element type="OBJECT" id="trigger" selected="true" />
+            <filter-element type="OBJECT" id="synonym" selected="false" />
+            <filter-element type="OBJECT" id="sequence" selected="true" />
+            <filter-element type="OBJECT" id="procedure" selected="true" />
+            <filter-element type="OBJECT" id="function" selected="true" />
+            <filter-element type="OBJECT" id="package" selected="true" />
+            <filter-element type="OBJECT" id="type" selected="true" />
+            <filter-element type="OBJECT" id="dimension" selected="true" />
+            <filter-element type="OBJECT" id="cluster" selected="true" />
+            <filter-element type="OBJECT" id="dblink" selected="true" />
+          </user-schema>
+          <public-schema>
+            <filter-element type="OBJECT" id="table" selected="false" />
+            <filter-element type="OBJECT" id="view" selected="false" />
+            <filter-element type="OBJECT" id="json view" selected="false" />
+            <filter-element type="OBJECT" id="materialized view" selected="false" />
+            <filter-element type="OBJECT" id="index" selected="false" />
+            <filter-element type="OBJECT" id="constraint" selected="false" />
+            <filter-element type="OBJECT" id="trigger" selected="false" />
+            <filter-element type="OBJECT" id="synonym" selected="false" />
+            <filter-element type="OBJECT" id="sequence" selected="false" />
+            <filter-element type="OBJECT" id="procedure" selected="false" />
+            <filter-element type="OBJECT" id="function" selected="false" />
+            <filter-element type="OBJECT" id="package" selected="false" />
+            <filter-element type="OBJECT" id="type" selected="false" />
+            <filter-element type="OBJECT" id="dimension" selected="false" />
+            <filter-element type="OBJECT" id="cluster" selected="false" />
+            <filter-element type="OBJECT" id="dblink" selected="false" />
+          </public-schema>
+          <any-schema>
+            <filter-element type="OBJECT" id="table" selected="true" />
+            <filter-element type="OBJECT" id="view" selected="true" />
+            <filter-element type="OBJECT" id="json view" selected="true" />
+            <filter-element type="OBJECT" id="materialized view" selected="true" />
+            <filter-element type="OBJECT" id="index" selected="true" />
+            <filter-element type="OBJECT" id="constraint" selected="true" />
+            <filter-element type="OBJECT" id="trigger" selected="true" />
+            <filter-element type="OBJECT" id="synonym" selected="true" />
+            <filter-element type="OBJECT" id="sequence" selected="true" />
+            <filter-element type="OBJECT" id="procedure" selected="true" />
+            <filter-element type="OBJECT" id="function" selected="true" />
+            <filter-element type="OBJECT" id="package" selected="true" />
+            <filter-element type="OBJECT" id="type" selected="true" />
+            <filter-element type="OBJECT" id="dimension" selected="true" />
+            <filter-element type="OBJECT" id="cluster" selected="true" />
+            <filter-element type="OBJECT" id="dblink" selected="true" />
+          </any-schema>
+        </basic-filter>
+        <extended-filter>
+          <filter-element type="RESERVED_WORD" id="keyword" selected="true" />
+          <filter-element type="RESERVED_WORD" id="function" selected="true" />
+          <filter-element type="RESERVED_WORD" id="parameter" selected="true" />
+          <filter-element type="RESERVED_WORD" id="datatype" selected="true" />
+          <filter-element type="RESERVED_WORD" id="exception" selected="true" />
+          <filter-element type="OBJECT" id="schema" selected="true" />
+          <filter-element type="OBJECT" id="user" selected="true" />
+          <filter-element type="OBJECT" id="role" selected="true" />
+          <filter-element type="OBJECT" id="privilege" selected="true" />
+          <user-schema>
+            <filter-element type="OBJECT" id="table" selected="true" />
+            <filter-element type="OBJECT" id="view" selected="true" />
+            <filter-element type="OBJECT" id="json view" selected="true" />
+            <filter-element type="OBJECT" id="materialized view" selected="true" />
+            <filter-element type="OBJECT" id="index" selected="true" />
+            <filter-element type="OBJECT" id="constraint" selected="true" />
+            <filter-element type="OBJECT" id="trigger" selected="true" />
+            <filter-element type="OBJECT" id="synonym" selected="true" />
+            <filter-element type="OBJECT" id="sequence" selected="true" />
+            <filter-element type="OBJECT" id="procedure" selected="true" />
+            <filter-element type="OBJECT" id="function" selected="true" />
+            <filter-element type="OBJECT" id="package" selected="true" />
+            <filter-element type="OBJECT" id="type" selected="true" />
+            <filter-element type="OBJECT" id="dimension" selected="true" />
+            <filter-element type="OBJECT" id="cluster" selected="true" />
+            <filter-element type="OBJECT" id="dblink" selected="true" />
+          </user-schema>
+          <public-schema>
+            <filter-element type="OBJECT" id="table" selected="true" />
+            <filter-element type="OBJECT" id="view" selected="true" />
+            <filter-element type="OBJECT" id="json view" selected="true" />
+            <filter-element type="OBJECT" id="materialized view" selected="true" />
+            <filter-element type="OBJECT" id="index" selected="true" />
+            <filter-element type="OBJECT" id="constraint" selected="true" />
+            <filter-element type="OBJECT" id="trigger" selected="true" />
+            <filter-element type="OBJECT" id="synonym" selected="true" />
+            <filter-element type="OBJECT" id="sequence" selected="true" />
+            <filter-element type="OBJECT" id="procedure" selected="true" />
+            <filter-element type="OBJECT" id="function" selected="true" />
+            <filter-element type="OBJECT" id="package" selected="true" />
+            <filter-element type="OBJECT" id="type" selected="true" />
+            <filter-element type="OBJECT" id="dimension" selected="true" />
+            <filter-element type="OBJECT" id="cluster" selected="true" />
+            <filter-element type="OBJECT" id="dblink" selected="true" />
+          </public-schema>
+          <any-schema>
+            <filter-element type="OBJECT" id="table" selected="true" />
+            <filter-element type="OBJECT" id="view" selected="true" />
+            <filter-element type="OBJECT" id="json view" selected="true" />
+            <filter-element type="OBJECT" id="materialized view" selected="true" />
+            <filter-element type="OBJECT" id="index" selected="true" />
+            <filter-element type="OBJECT" id="constraint" selected="true" />
+            <filter-element type="OBJECT" id="trigger" selected="true" />
+            <filter-element type="OBJECT" id="synonym" selected="true" />
+            <filter-element type="OBJECT" id="sequence" selected="true" />
+            <filter-element type="OBJECT" id="procedure" selected="true" />
+            <filter-element type="OBJECT" id="function" selected="true" />
+            <filter-element type="OBJECT" id="package" selected="true" />
+            <filter-element type="OBJECT" id="type" selected="true" />
+            <filter-element type="OBJECT" id="dimension" selected="true" />
+            <filter-element type="OBJECT" id="cluster" selected="true" />
+            <filter-element type="OBJECT" id="dblink" selected="true" />
+          </any-schema>
+        </extended-filter>
+      </filters>
+      <sorting enabled="true">
+        <sorting-element type="RESERVED_WORD" id="keyword" />
+        <sorting-element type="RESERVED_WORD" id="datatype" />
+        <sorting-element type="OBJECT" id="column" />
+        <sorting-element type="OBJECT" id="table" />
+        <sorting-element type="OBJECT" id="view" />
+        <sorting-element type="OBJECT" id="json view" />
+        <sorting-element type="OBJECT" id="materialized view" />
+        <sorting-element type="OBJECT" id="index" />
+        <sorting-element type="OBJECT" id="constraint" />
+        <sorting-element type="OBJECT" id="trigger" />
+        <sorting-element type="OBJECT" id="synonym" />
+        <sorting-element type="OBJECT" id="sequence" />
+        <sorting-element type="OBJECT" id="procedure" />
+        <sorting-element type="OBJECT" id="function" />
+        <sorting-element type="OBJECT" id="package" />
+        <sorting-element type="OBJECT" id="type" />
+        <sorting-element type="OBJECT" id="dimension" />
+        <sorting-element type="OBJECT" id="cluster" />
+        <sorting-element type="OBJECT" id="dblink" />
+        <sorting-element type="OBJECT" id="schema" />
+        <sorting-element type="OBJECT" id="role" />
+        <sorting-element type="OBJECT" id="user" />
+        <sorting-element type="RESERVED_WORD" id="function" />
+        <sorting-element type="RESERVED_WORD" id="parameter" />
+      </sorting>
+      <format>
+        <enforce-code-style-case value="true" />
+      </format>
+    </code-completion-settings>
+    <execution-engine-settings>
+      <statement-execution>
+        <fetch-block-size value="100" />
+        <execution-timeout value="20" />
+        <debug-execution-timeout value="600" />
+        <focus-result value="false" />
+        <prompt-execution value="false" />
+      </statement-execution>
+      <script-execution>
+        <command-line-interfaces />
+        <execution-timeout value="300" />
+      </script-execution>
+      <method-execution>
+        <execution-timeout value="30" />
+        <debug-execution-timeout value="600" />
+        <parameter-history-size value="10" />
+      </method-execution>
+    </execution-engine-settings>
+    <operation-settings>
+      <transactions>
+        <uncommitted-changes>
+          <on-project-close value="ASK" />
+          <on-disconnect value="ASK" />
+          <on-autocommit-toggle value="ASK" />
+        </uncommitted-changes>
+        <multiple-uncommitted-changes>
+          <on-commit value="ASK" />
+          <on-rollback value="ASK" />
+        </multiple-uncommitted-changes>
+      </transactions>
+      <session-browser>
+        <disconnect-session value="ASK" />
+        <kill-session value="ASK" />
+        <reload-on-filter-change value="false" />
+      </session-browser>
+      <compiler>
+        <compile-type value="KEEP" />
+        <compile-dependencies value="ASK" />
+        <always-show-controls value="false" />
+      </compiler>
+    </operation-settings>
+    <ddl-file-settings>
+      <extensions>
+        <mapping file-type-id="VIEW" extensions="vw" />
+        <mapping file-type-id="TRIGGER" extensions="trg" />
+        <mapping file-type-id="PROCEDURE" extensions="prc" />
+        <mapping file-type-id="FUNCTION" extensions="fnc" />
+        <mapping file-type-id="PACKAGE" extensions="pkg" />
+        <mapping file-type-id="PACKAGE_SPEC" extensions="pks" />
+        <mapping file-type-id="PACKAGE_BODY" extensions="pkb" />
+        <mapping file-type-id="TYPE" extensions="tpe" />
+        <mapping file-type-id="TYPE_SPEC" extensions="tps" />
+        <mapping file-type-id="TYPE_BODY" extensions="tpb" />
+        <mapping file-type-id="JAVA_SOURCE" extensions="sql" />
+      </extensions>
+      <general>
+        <lookup-ddl-files value="true" />
+        <create-ddl-files value="false" />
+        <synchronize-ddl-files value="true" />
+        <use-qualified-names value="false" />
+        <make-scripts-rerunnable value="true" />
+      </general>
+    </ddl-file-settings>
+    <assistant-settings>
+      <credential-settings>
+        <credentials />
+      </credential-settings>
+    </assistant-settings>
+    <general-settings>
+      <regional-settings>
+        <date-format value="MEDIUM" />
+        <number-format value="UNGROUPED" />
+        <locale value="SYSTEM_DEFAULT" />
+        <use-custom-formats value="false" />
+      </regional-settings>
+      <environment>
+        <environment-types>
+          <environment-type id="development" name="Development" description="Development environment" color="-2430209/-12296320" readonly-code="false" readonly-data="false" />
+          <environment-type id="integration" name="Integration" description="Integration environment" color="-2621494/-12163514" readonly-code="true" readonly-data="false" />
+          <environment-type id="production" name="Production" description="Productive environment" color="-11574/-10271420" readonly-code="true" readonly-data="true" />
+          <environment-type id="other" name="Other" description="" color="-1576/-10724543" readonly-code="false" readonly-data="false" />
+        </environment-types>
+        <visibility-settings>
+          <connection-tabs value="true" />
+          <dialog-headers value="true" />
+          <object-editor-tabs value="true" />
+          <script-editor-tabs value="false" />
+          <execution-result-tabs value="true" />
+        </visibility-settings>
+      </environment>
+    </general-settings>
+  </component>
+</project>

+ 10 - 0
.idea/deploymentTargetSelector.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="deploymentTargetSelector">
+    <selectionStates>
+      <SelectionState runConfigName="app">
+        <option name="selectionMode" value="DROPDOWN" />
+      </SelectionState>
+    </selectionStates>
+  </component>
+</project>

+ 13 - 0
.idea/deviceManager.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="DeviceTable">
+    <option name="columnSorters">
+      <list>
+        <ColumnSorterState>
+          <option name="column" value="Name" />
+          <option name="order" value="ASCENDING" />
+        </ColumnSorterState>
+      </list>
+    </option>
+  </component>
+</project>

+ 20 - 0
.idea/gradle.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="GradleMigrationSettings" migrationVersion="1" />
+  <component name="GradleSettings">
+    <option name="linkedExternalProjectsSettings">
+      <GradleProjectSettings>
+        <option name="testRunner" value="CHOOSE_PER_TEST" />
+        <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" />
+            <option value="$PROJECT_DIR$/transport" />
+          </set>
+        </option>
+      </GradleProjectSettings>
+    </option>
+  </component>
+</project>

+ 61 - 0
.idea/inspectionProfiles/Project_Default.xml

@@ -0,0 +1,61 @@
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="Project Default" />
+    <inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+    <inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
+      <option name="composableFile" value="true" />
+      <option name="previewFile" value="true" />
+    </inspection_tool>
+  </profile>
+</component>

+ 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_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
+    <output url="file://$PROJECT_DIR$/build/classes" />
+  </component>
+  <component name="ProjectType">
+    <option name="id" value="Android" />
+  </component>
+</project>

+ 17 - 0
.idea/runConfigurations.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="RunConfigurationProducerService">
+    <option name="ignoredProducers">
+      <set>
+        <option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
+        <option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
+        <option value="com.intellij.execution.junit.PatternConfigurationProducer" />
+        <option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
+        <option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
+        <option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
+        <option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
+        <option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
+      </set>
+    </option>
+  </component>
+</project>

+ 1 - 0
app/.gitignore

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

+ 58 - 0
app/build.gradle.kts

@@ -0,0 +1,58 @@
+plugins {
+    alias(libs.plugins.android.application)
+    alias(libs.plugins.kotlin.android)
+    alias(libs.plugins.kotlin.compose)
+}
+
+android {
+    namespace = "com.iscs.comm"
+    compileSdk {
+        version = release(36)
+    }
+
+    defaultConfig {
+        applicationId = "com.iscs.comm"
+        minSdk = 24
+        targetSdk = 36
+        versionCode = 1
+        versionName = "1.0"
+
+        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+    }
+
+    buildTypes {
+        release {
+            isMinifyEnabled = false
+            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+        }
+    }
+    compileOptions {
+        sourceCompatibility = JavaVersion.VERSION_11
+        targetCompatibility = JavaVersion.VERSION_11
+    }
+    kotlinOptions {
+        jvmTarget = "11"
+    }
+    buildFeatures {
+        compose = true
+    }
+}
+
+dependencies {
+    implementation(libs.androidx.core.ktx)
+    implementation(libs.androidx.lifecycle.runtime.ktx)
+    implementation(libs.androidx.activity.compose)
+    implementation(platform(libs.androidx.compose.bom))
+    implementation(libs.androidx.compose.ui)
+    implementation(libs.androidx.compose.ui.graphics)
+    implementation(libs.androidx.compose.ui.tooling.preview)
+    implementation(libs.androidx.compose.material3)
+    implementation(project(":transport"))
+    testImplementation(libs.junit)
+    androidTestImplementation(libs.androidx.junit)
+    androidTestImplementation(libs.androidx.espresso.core)
+    androidTestImplementation(platform(libs.androidx.compose.bom))
+    androidTestImplementation(libs.androidx.compose.ui.test.junit4)
+    debugImplementation(libs.androidx.compose.ui.tooling)
+    debugImplementation(libs.androidx.compose.ui.test.manifest)
+}

+ 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/iscs/comm/ExampleInstrumentedTest.kt

@@ -0,0 +1,24 @@
+package com.iscs.comm
+
+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.iscs.communication", appContext.packageName)
+    }
+}

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

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <application
+        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:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/Theme.CommunicationDemo">
+        <activity
+            android:name=".MainActivity"
+            android:exported="true"
+            android:label="@string/app_name"
+            android:theme="@style/Theme.CommunicationDemo">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>

+ 72 - 0
app/src/main/java/com/iscs/comm/MainActivity.kt

@@ -0,0 +1,72 @@
+package com.iscs.comm
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Button
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.lifecycleScope
+import com.iscs.comm.entity.device.Device
+import com.iscs.comm.entity.device.DeviceLockSlot
+import com.iscs.comm.intf.IDeviceListener
+import com.iscs.comm.ui.theme.CommunicationDemoTheme
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class MainActivity : ComponentActivity() {
+
+    private val list = ArrayList<Device>()
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        enableEdgeToEdge()
+        setContent {
+            CommunicationDemoTheme {
+                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
+                    Column {
+                        Text("测试", modifier = Modifier.padding(innerPadding))
+                        Row {
+                            Button({ ctrlLockSlotLock(0, false) }) { Text("开锁0") }
+                            Spacer(modifier = Modifier.width(10.dp))
+                            Button({ ctrlLockSlotLock(0, true) }) { Text("关锁0") }
+                            Spacer(modifier = Modifier.width(10.dp))
+                            Button({ ctrlLockSlotLock(1, false) }) { Text("开锁1") }
+                            Spacer(modifier = Modifier.width(10.dp))
+                            Button({ ctrlLockSlotLock(1, true) }) { Text("关锁1") }
+                        }
+                    }
+                }
+            }
+        }
+        lifecycleScope.launch(Dispatchers.IO) {
+            CommManager.init()
+            CommManager.setOnDeviceListener(object : IDeviceListener {
+                override fun onDeviceList(devices: List<Device>) {
+                    list.addAll(devices)
+                }
+
+                override fun onDeviceChanged(device: Device) {
+
+                }
+            })
+        }
+
+    }
+
+    fun ctrlLockSlotLock(ch: Int, isLock: Boolean) {
+        val device = list.find { it.frame.cmd == 0x0602 }
+        if (device is DeviceLockSlot) {
+            CommManager.write(device.ctrlSlotLock(ch, isLock))
+        }
+    }
+}

+ 11 - 0
app/src/main/java/com/iscs/comm/ui/theme/Color.kt

@@ -0,0 +1,11 @@
+package com.iscs.comm.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Purple80 = Color(0xFFD0BCFF)
+val PurpleGrey80 = Color(0xFFCCC2DC)
+val Pink80 = Color(0xFFEFB8C8)
+
+val Purple40 = Color(0xFF6650a4)
+val PurpleGrey40 = Color(0xFF625b71)
+val Pink40 = Color(0xFF7D5260)

+ 57 - 0
app/src/main/java/com/iscs/comm/ui/theme/Theme.kt

@@ -0,0 +1,57 @@
+package com.iscs.comm.ui.theme
+
+import android.os.Build
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+private val DarkColorScheme = darkColorScheme(
+    primary = Purple80,
+    secondary = PurpleGrey80,
+    tertiary = Pink80
+)
+
+private val LightColorScheme = lightColorScheme(
+    primary = Purple40,
+    secondary = PurpleGrey40,
+    tertiary = Pink40
+
+    /* Other default colors to override
+    background = Color(0xFFFFFBFE),
+    surface = Color(0xFFFFFBFE),
+    onPrimary = Color.White,
+    onSecondary = Color.White,
+    onTertiary = Color.White,
+    onBackground = Color(0xFF1C1B1F),
+    onSurface = Color(0xFF1C1B1F),
+    */
+)
+
+@Composable
+fun CommunicationDemoTheme(
+    darkTheme: Boolean = isSystemInDarkTheme(),
+    // Dynamic color is available on Android 12+
+    dynamicColor: Boolean = true,
+    content: @Composable () -> Unit
+) {
+    val colorScheme = when {
+        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+            val context = LocalContext.current
+            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
+        }
+
+        darkTheme -> DarkColorScheme
+        else -> LightColorScheme
+    }
+
+    MaterialTheme(
+        colorScheme = colorScheme,
+        typography = Typography,
+        content = content
+    )
+}

+ 34 - 0
app/src/main/java/com/iscs/comm/ui/theme/Type.kt

@@ -0,0 +1,34 @@
+package com.iscs.comm.ui.theme
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+    bodyLarge = TextStyle(
+        fontFamily = FontFamily.Default,
+        fontWeight = FontWeight.Normal,
+        fontSize = 16.sp,
+        lineHeight = 24.sp,
+        letterSpacing = 0.5.sp
+    )
+    /* Other default text styles to override
+    titleLarge = TextStyle(
+        fontFamily = FontFamily.Default,
+        fontWeight = FontWeight.Normal,
+        fontSize = 22.sp,
+        lineHeight = 28.sp,
+        letterSpacing = 0.sp
+    ),
+    labelSmall = TextStyle(
+        fontFamily = FontFamily.Default,
+        fontWeight = FontWeight.Medium,
+        fontSize = 11.sp,
+        lineHeight = 16.sp,
+        letterSpacing = 0.5.sp
+    )
+    */
+)

+ 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>

+ 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


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

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="purple_200">#FFBB86FC</color>
+    <color name="purple_500">#FF6200EE</color>
+    <color name="purple_700">#FF3700B3</color>
+    <color name="teal_200">#FF03DAC5</color>
+    <color name="teal_700">#FF018786</color>
+    <color name="black">#FF000000</color>
+    <color name="white">#FFFFFFFF</color>
+</resources>

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

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

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

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <style name="Theme.CommunicationDemo" parent="android:Theme.Material.Light.NoActionBar" />
+</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 than 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>

+ 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/iscs/comm/ExampleUnitTest.kt

@@ -0,0 +1,17 @@
+package com.iscs.comm
+
+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)
+    }
+}

+ 7 - 0
build.gradle.kts

@@ -0,0 +1,7 @@
+// 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.kotlin.android) apply false
+    alias(libs.plugins.kotlin.compose) apply false
+    alias(libs.plugins.android.library) apply false
+}

+ 23 - 0
gradle.properties

@@ -0,0 +1,23 @@
+# 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

+ 37 - 0
gradle/libs.versions.toml

@@ -0,0 +1,37 @@
+[versions]
+agp = "8.13.1"
+kotlin = "2.0.21"
+coreKtx = "1.17.0"
+junit = "4.13.2"
+junitVersion = "1.3.0"
+espressoCore = "3.7.0"
+lifecycleRuntimeKtx = "2.9.4"
+activityCompose = "1.11.0"
+composeBom = "2024.09.00"
+appcompat = "1.7.1"
+material = "1.13.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-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
+androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+android-library = { id = "com.android.library", version.ref = "agp" }
+

BIN
gradle/wrapper/gradle-wrapper.jar


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

@@ -0,0 +1,8 @@
+#Thu Nov 13 13:20:33 CST 2025
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 251 - 0
gradlew

@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original 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.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+    echo "$*"
+} >&2
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+} >&2
+
+# 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  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
+esac
+
+CLASSPATH="\\\"\\\""
+
+
+# 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
+    if ! command -v java >/dev/null 2>&1
+    then
+        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
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
+        fi
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
+    done
+fi
+
+
+# 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"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+        "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
+
+exec "$JAVACMD" "$@"

+ 94 - 0
gradlew.bat

@@ -0,0 +1,94 @@
+@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
+@rem SPDX-License-Identifier: Apache-2.0
+@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=.
+@rem This is normally unused
+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% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 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!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 24 - 0
settings.gradle.kts

@@ -0,0 +1,24 @@
+pluginManagement {
+    repositories {
+        google {
+            content {
+                includeGroupByRegex("com\\.android.*")
+                includeGroupByRegex("com\\.google.*")
+                includeGroupByRegex("androidx.*")
+            }
+        }
+        mavenCentral()
+        gradlePluginPortal()
+    }
+}
+dependencyResolutionManagement {
+    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+
+rootProject.name = "CommDemo"
+include(":app")
+include(":transport")

+ 1 - 0
transport/.gitignore

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

+ 52 - 0
transport/build.gradle.kts

@@ -0,0 +1,52 @@
+plugins {
+    alias(libs.plugins.android.library)
+    alias(libs.plugins.kotlin.android)
+}
+
+android {
+    namespace = "com.iscs.comm"
+    compileSdk {
+        version = release(36)
+    }
+
+    defaultConfig {
+        minSdk = 24
+
+        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles("consumer-rules.pro")
+        externalNativeBuild {
+            cmake {
+                cppFlags("")
+            }
+        }
+    }
+
+    buildTypes {
+        release {
+            isMinifyEnabled = false
+            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+        }
+    }
+    externalNativeBuild {
+        cmake {
+            path("src/main/cpp/CMakeLists.txt")
+            version = "3.22.1"
+        }
+    }
+    compileOptions {
+        sourceCompatibility = JavaVersion.VERSION_11
+        targetCompatibility = JavaVersion.VERSION_11
+    }
+    kotlinOptions {
+        jvmTarget = "11"
+    }
+}
+
+dependencies {
+    implementation(libs.androidx.core.ktx)
+    implementation(libs.androidx.appcompat)
+    implementation(libs.material)
+    testImplementation(libs.junit)
+    androidTestImplementation(libs.androidx.junit)
+    androidTestImplementation(libs.androidx.espresso.core)
+}

+ 0 - 0
transport/consumer-rules.pro


+ 21 - 0
transport/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
transport/src/androidTest/java/com/iscs/comm/ExampleInstrumentedTest.kt

@@ -0,0 +1,24 @@
+package com.iscs.comm
+
+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.iscs.transport.test", appContext.packageName)
+    }
+}

+ 4 - 0
transport/src/main/AndroidManifest.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+</manifest>

+ 37 - 0
transport/src/main/cpp/CMakeLists.txt

@@ -0,0 +1,37 @@
+# For more information about using CMake with Android Studio, read the
+# documentation: https://d.android.com/studio/projects/add-native-code.html.
+# For more examples on how to use CMake, see https://github.com/android/ndk-samples.
+
+# Sets the minimum CMake version required for this project.
+cmake_minimum_required(VERSION 3.22.1)
+
+# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
+# Since this is the top level CMakeLists.txt, the project name is also accessible
+# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
+# build script scope).
+project("iscs_comm")
+
+# Creates and names a library, sets it as either STATIC
+# or SHARED, and provides the relative paths to its source code.
+# You can define multiple libraries, and CMake builds them for you.
+# Gradle automatically packages shared libraries with your APK.
+#
+# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
+# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
+# is preferred for the same purpose.
+#
+# In order to load a library into your app from Java/Kotlin, you must call
+# System.loadLibrary() and pass the name of the library defined here;
+# for GameActivity/NativeActivity derived applications, the same library name must be
+# used in the AndroidManifest.xml file.
+add_library(${CMAKE_PROJECT_NAME} SHARED
+        # List C/C++ source files with relative paths to this CMakeLists.txt.
+        comm_can.cpp comm_serial.cpp)
+
+# Specifies libraries CMake should link to your target library. You
+# can link libraries from various origins, such as libraries defined in this
+# build script, prebuilt third-party libraries, or Android system libraries.
+target_link_libraries(${CMAKE_PROJECT_NAME}
+        # List libraries link to the target library
+        android
+        log)

+ 215 - 0
transport/src/main/cpp/comm_can.cpp

@@ -0,0 +1,215 @@
+#include <jni.h>
+#include <cstring>
+#include <unistd.h>
+#include <fcntl.h>
+#include <cerrno>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <linux/can.h>
+#include <linux/can/raw.h>
+#include <linux/if.h>
+#include <poll.h>
+#include <android/log.h>
+#include <sstream>
+#include <iomanip>
+
+// 定义JNI层通信TAG
+#define TAG "native_iscs_can"
+
+#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,  TAG, __VA_ARGS__)
+#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
+
+static void throwIOException(JNIEnv *env, const char *msg) {
+    jclass ex = env->FindClass("java/io/IOException");
+    if (ex) env->ThrowNew(ex, msg);
+}
+
+/**
+ * 将字节数组转化为16进制字符串
+ *
+ * @param env
+ * @param array
+ * @param uppercase
+ * @param separator
+ * @return
+ */
+std::string bytesToHexString(JNIEnv *env, jbyteArray array, bool uppercase = true, const std::string &separator = " ") {
+    if (array == nullptr) return "";
+
+    jsize len = env->GetArrayLength(array);
+    if (len <= 0) return "";
+
+    jbyte *bytes = env->GetByteArrayElements(array, nullptr);
+    if (!bytes) return "";
+
+    std::ostringstream oss;
+    oss << std::hex << std::setfill('0');
+    if (uppercase) oss << std::uppercase;
+
+    for (jsize i = 0; i < len; ++i) {
+        oss << std::setw(2) << (static_cast<unsigned int>(bytes[i]) & 0xFF);
+        if (i != len - 1) oss << separator;
+    }
+
+    env->ReleaseByteArrayElements(array, bytes, JNI_ABORT); // 不修改原数据,使用 JNI_ABORT
+    return oss.str();
+}
+
+jbyteArray append_can_id_to_data(JNIEnv *env, jint can_id, const void *jdata, jint len) {
+    if (env == nullptr || jdata == nullptr || len <= 0) {
+        return nullptr;
+    }
+
+    const int id_len = 5;                // can_id 占 4 字节
+    int total_len = id_len + len;        // 总长度
+
+    jbyteArray result = env->NewByteArray(total_len);
+    if (result == nullptr) return nullptr;
+
+    // 分配本地缓冲区
+    auto *buffer = new jbyte[total_len];
+
+    buffer[0] = static_cast<jbyte>((can_id >> 24) & 0xFF);
+    buffer[1] = static_cast<jbyte>((can_id >> 16) & 0xFF);
+    buffer[2] = static_cast<jbyte>((can_id >> 8) & 0xFF);
+    buffer[3] = static_cast<jbyte>(can_id & 0xFF);
+    buffer[4] = static_cast<jbyte>(len);
+
+    memcpy(buffer + id_len, jdata, len);
+    env->SetByteArrayRegion(result, 0, total_len, buffer);
+    delete[] buffer;
+
+    return result;
+}
+
+extern "C" JNIEXPORT jint JNICALL
+Java_com_iscs_comm_jni_NativeCan_canOpen(JNIEnv *env, jobject thiz, jstring can_port) {
+    const char *can_name = env->GetStringUTFChars(can_port, nullptr);
+    LOGI("CAN ---> opening %s", can_name);
+
+    int s = socket(PF_CAN, SOCK_RAW, CAN_RAW);
+    if (s < 0) {
+        LOGE("CAN ---> socket failed: %s", strerror(errno));
+        env->ReleaseStringUTFChars(can_port, can_name);
+        return -1;
+    }
+
+    struct ifreq ifr{};
+    strncpy(ifr.ifr_name, can_name, IFNAMSIZ - 1);
+    if (ioctl(s, SIOCGIFINDEX, &ifr) < 0) {
+        LOGE("CAN ---> ioctl(SIOCGIFINDEX) failed: %s", strerror(errno));
+        close(s);
+        env->ReleaseStringUTFChars(can_port, can_name);
+        return -2;
+    }
+
+    struct sockaddr_can addr{};
+    addr.can_family = AF_CAN;
+    addr.can_ifindex = ifr.ifr_ifindex;
+
+    if (bind(s, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
+        LOGE("CAN ---> bind %s failed: %s", can_name, strerror(errno));
+        close(s);
+        env->ReleaseStringUTFChars(can_port, can_name);
+        return -3;
+    }
+
+    // 可选: 非阻塞模式
+    // fcntl(s, F_SETFL, O_NONBLOCK);
+
+    LOGI("CAN ---> opened  %s port_id %d", can_name, s);
+    env->ReleaseStringUTFChars(can_port, can_name);
+    return s;
+}
+
+/**
+ * 关闭指定CAN总线的连接状态
+ *
+ * @param env
+ * @param thiz
+ * @param fd    需要关闭的CAN口id
+ */
+extern "C" JNIEXPORT jint JNICALL
+Java_com_iscs_comm_jni_NativeCan_canClose(JNIEnv *env, jobject thiz, jint fd) {
+    LOGI("CAN ---> close %d", fd);
+    if (fd >= 0) close(fd);
+    return 0;
+}
+
+/**
+ * 写数据帧到CAN总线
+ *
+ * @param env
+ * @param thiz
+ * @param fd        打开的CAN口id
+ * @param can_id    写数据到CAN总线的具体地址
+ * @param jdata     数据帧
+ * @param len       数据帧长度
+ * @param is_ext    是否扩展帧
+ * @param is_rtr    是否远程帧
+ * @param is_fd     是否大包数据
+ */
+extern "C" JNIEXPORT jint JNICALL
+Java_com_iscs_comm_jni_NativeCan_canWrite(
+        JNIEnv *env, jobject thiz,
+        jint fd, jint can_id,
+        jbyteArray jdata, jint len,
+        jboolean is_ext, jboolean is_rtr, jboolean is_fd) {
+
+    jbyte *data = env->GetByteArrayElements(jdata, nullptr);
+    int ret = -1;
+    // 是否大包数据isFd = true 大包数据
+    if (!is_fd) {
+        struct can_frame f{};
+        f.can_id = (uint32_t) can_id;
+        if (is_ext) f.can_id |= CAN_EFF_FLAG;
+        if (is_rtr) f.can_id |= CAN_RTR_FLAG;
+        f.can_dlc = (uint8_t) ((len > 8) ? 8 : len);
+        memcpy(f.data, data, f.can_dlc);
+        LOGI("CAN ---> %s", bytesToHexString(env, append_can_id_to_data(env, f.can_id, f.data, f.can_dlc)).c_str());
+        ret = write(fd, &f, sizeof(f));
+    } else {
+        struct canfd_frame f{};
+        f.can_id = (uint32_t) can_id;
+        if (is_ext) f.can_id |= CAN_EFF_FLAG;
+        if (is_rtr) f.can_id |= CAN_RTR_FLAG;
+        f.len = (uint8_t) ((len > 64) ? 64 : len);
+        memcpy(f.data, data, f.len);
+        LOGI("CAN ---> %s", bytesToHexString(env, append_can_id_to_data(env, f.can_id, f.data, f.len)).c_str());
+        ret = write(fd, &f, sizeof(f));
+    }
+
+    if (ret < 0) LOGE("CAN ---> write failed: %s", strerror(errno));
+
+    env->ReleaseByteArrayElements(jdata, data, JNI_ABORT);
+    return ret;
+}
+
+/**
+ * 从CAN总线中读数据帧
+ *
+ * @param env
+ * @param thiz
+ * @param fd                打开的CAN口id
+ * @param timeout_millis    超时时长
+ */
+extern "C" JNIEXPORT jbyteArray JNICALL
+Java_com_iscs_comm_jni_NativeCan_canRead(JNIEnv *env, jobject thiz, jint fd, jint timeout_millis) {
+    struct pollfd pfd{.fd = fd, .events = POLLIN, .revents = 0};
+    int pr = poll(&pfd, 1, timeout_millis);  // 确保 timeoutMs > 0
+    if (pr <= 0) {
+        if (pr < 0) LOGE("CAN ---> poll error: %s", strerror(errno));  // 打印 poll 错误
+        return env->NewByteArray(0);  // 没有数据或超时
+    }
+
+    struct can_frame f{};
+    int r = read(fd, &f, sizeof(f));  // 读取 CAN 帧
+    if (r < 0) {
+        LOGE("CAN ---> read failed: %s", strerror(errno));  // 读取失败的错误日志
+        return env->NewByteArray(0);  // 返回空数组表示没有数据
+    }
+
+    jbyteArray out = append_can_id_to_data(env, f.can_id, f.data, f.can_dlc);
+    LOGI("CAN <--- %s", bytesToHexString(env, out).c_str());
+    return out;
+}

+ 1 - 0
transport/src/main/cpp/comm_serial.cpp

@@ -0,0 +1 @@
+

+ 116 - 0
transport/src/main/java/com/iscs/comm/CommManager.kt

@@ -0,0 +1,116 @@
+package com.iscs.comm
+
+import com.iscs.comm.entity.Frame
+import com.iscs.comm.entity.device.Device
+import com.iscs.comm.extension.buildReadDeviceInfo
+import com.iscs.comm.extension.factoryDevice
+import com.iscs.comm.intf.AbsCommBase
+import com.iscs.comm.intf.IDeviceListener
+import com.iscs.comm.manager.CanManager
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.util.concurrent.ConcurrentLinkedQueue
+
+/**
+ * @author GrayCarbon
+ *
+ * 构建通信层通用管理器,用于管理底层连接方式:485、CAN等
+ */
+object CommManager {
+
+    // 用于管理通信连接
+    private var manager: AbsCommBase? = null
+
+    // 用于存储通信后获取到的设备列表,支持高并发
+    private val deviceList = ConcurrentLinkedQueue<Device>()
+
+    // 设备变化监听
+    private var deviceListener: IDeviceListener? = null
+
+    fun init() {
+        // 连接之前先对已连接进行断开连接处理
+        manager?.close()
+        manager = CanManager("can0")
+        manager?.open()
+        initDeviceList()
+    }
+
+    /**
+     * 设置设备变化监听
+     */
+    fun setOnDeviceListener(listener: IDeviceListener) {
+        this.deviceListener = listener
+    }
+
+    /**
+     * 发送数据帧,不带响应
+     *
+     * @param frame 要发送的数据帧
+     */
+    fun write(frame: Frame) = manager?.scope?.launch {
+        manager?.write(frame)
+    }
+
+    /**
+     * 发送数据帧,带响应
+     *
+     * @param frame 要发送的数据帧
+     */
+    suspend fun writeWithResponse(frame: Frame): Frame = withContext(Dispatchers.Main) {
+        manager?.writeWithResponse(frame) ?: Frame()
+    }
+
+    /**
+     * 初始化设备列表
+     */
+    private fun initDeviceList() = manager?.scope?.launch {
+        val frames = Frame().buildReadDeviceInfo()
+        deviceList.clear()
+        frames.forEach {
+            // 遍历CAN口,看有哪些设备存在
+            val frame = writeWithResponse(it)
+            // 通过数据帧转换设备类型
+            if (frame.cmd > 0) deviceList.add(frame.factoryDevice())
+            delay(20)
+        }
+        // 遍历一遍设备状态
+        checkDeviceStatus()?.join()
+        // 将设备列表回调给外部
+        deviceListener?.onDeviceList(ArrayList(deviceList))
+        delay(20)
+        // 开启轮询设备状态
+        startDeviceStatusLoop()
+    }
+
+    /**
+     * 开启设备状态检测
+     */
+    private fun startDeviceStatusLoop() = manager?.scope?.launch(Dispatchers.IO) {
+        // 开启轮询检查
+        while (isActive) {
+            checkDeviceStatus()?.join()
+        }
+    }
+
+    /**
+     * 检查设备状态任务
+     */
+    private fun checkDeviceStatus() = manager?.scope?.launch(Dispatchers.IO) {
+        deviceList.forEach {
+            // 根据已有的设备,遍历设备状态
+            // 读取设备信息
+            val iFrame = writeWithResponse(it.ctrlCheckDeviceInfo())
+            // 通过响应的数据帧更新设备模型的状态
+            it.updateDeviceInfo(iFrame)
+            // 读取设备属性状态
+            val sFrame = writeWithResponse(it.ctrlCheckDeviceStatus())
+            it.updateDeviceStatus(sFrame)
+            // 限制轮询的速度
+            delay(50)
+        }
+    }
+
+}

+ 41 - 0
transport/src/main/java/com/iscs/comm/entity/Frame.kt

@@ -0,0 +1,41 @@
+package com.iscs.comm.entity
+
+import com.iscs.comm.enums.CommType
+
+// 该文件扩展用于对数据帧进行操作
+
+/**
+ * 封装用于通信的数据帧
+ *
+ * @param cmd       命令
+ * @param data      数据
+ * @param frameFrom 数据帧来源,用于处理不同通信数据差异部分
+ */
+data class Frame(var cmd: Int = -1, var data: ByteArray = byteArrayOf(), var frameFrom: CommType = CommType.NONE) {
+
+    /**
+     * 创建新的数据帧
+     */
+    fun newFrame() = Frame().apply {
+        frameFrom = this@Frame.frameFrom
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as Frame
+
+        if (cmd != other.cmd) return false
+        if (!data.contentEquals(other.data)) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = cmd
+        result = 31 * result + data.contentHashCode()
+        return result
+    }
+
+}

+ 63 - 0
transport/src/main/java/com/iscs/comm/entity/device/Device.kt

@@ -0,0 +1,63 @@
+package com.iscs.comm.entity.device
+
+import com.iscs.comm.entity.Frame
+import com.iscs.comm.entity.device.status.DeviceStatus
+import com.iscs.comm.entity.device.status.DeviceStatusNone
+import com.iscs.comm.enums.DeviceType
+import com.iscs.comm.extension.buildCtrlKeySlotStatus
+
+/**
+ * 用于封装设备类型对外输出
+ */
+abstract class Device(val frame: Frame) {
+
+    /**
+     * 定义设备类型
+     */
+    open val deviceType: DeviceType = DeviceType.NONE
+
+    /**
+     * 设备状态
+     */
+    open var deviceStatus: DeviceStatus = DeviceStatusNone()
+
+    /**
+     * 设备ID
+     *      CAN总线中是设备CAN_ID
+     */
+    protected var id: Int = -1
+
+    // 数据初始化操作
+    init {
+        id = frame.cmd
+    }
+
+    /**
+     * 构建获取当前底座状态的数据帧
+     *
+     * 问题:如果是钥匙仓位的话,开启充电时仓位是否有设备就不上报了
+     */
+    open fun ctrlCheckDeviceInfo(): Frame {
+        return frame.newFrame().apply { cmd = frame.cmd }.buildCtrlKeySlotStatus()
+    }
+
+    /**
+     * 构建获取当前底座卡扣状态的数据帧
+     */
+    abstract fun ctrlCheckDeviceStatus(): Frame
+
+    /**
+     * 更新设备信息
+     *
+     * @param frame 查询设备信息返回的数据帧,由设备模型自己处理
+     */
+    abstract fun updateDeviceInfo(frame: Frame)
+
+    /**
+     * 更新设备状态
+     *
+     * @param frame 设备属性状态
+     */
+    abstract fun updateDeviceStatus(frame: Frame)
+
+}

+ 82 - 0
transport/src/main/java/com/iscs/comm/entity/device/DeviceKeySlot.kt

@@ -0,0 +1,82 @@
+package com.iscs.comm.entity.device
+
+import com.iscs.comm.entity.Frame
+import com.iscs.comm.entity.device.status.DeviceStatus
+import com.iscs.comm.entity.device.status.DeviceStatusKeySlot
+import com.iscs.comm.enums.DeviceType
+import com.iscs.comm.extension.buildCheckDeviceStatus
+import com.iscs.comm.extension.buildCtrlKeySlotLockAndCharge
+
+/**
+ * 钥匙底座类型的设备
+ */
+class DeviceKeySlot(frame: Frame) : Device(frame) {
+
+    /**
+     * 配置当前设备类型
+     */
+    override val deviceType: DeviceType get() = DeviceType.SLOT_KEY
+
+    /**
+     * 构建钥匙仓位的状态
+     */
+    override var deviceStatus: DeviceStatus = DeviceStatusKeySlot()
+
+    /**
+     * 构建获取钥匙锁定状态的数据帧
+     */
+    override fun ctrlCheckDeviceStatus(): Frame {
+        return frame.newFrame().apply { cmd = frame.cmd }.buildCheckDeviceStatus()
+    }
+
+    /**
+     * 更新设备模型操作
+     *
+     * @param frame 查询到的设备信息
+     */
+    override fun updateDeviceInfo(frame: Frame) {
+        deviceStatus.updateInfo(frame)
+    }
+
+    /**
+     * 更新设备状态操作
+     */
+    override fun updateDeviceStatus(frame: Frame) {
+        deviceStatus.updateStatus(frame)
+    }
+
+    /**
+     * 控制锁仓是否锁定
+     *
+     * @param ch        控制槽位 默认-1控制所有
+     * @param isLock    是否锁定
+     */
+    fun ctrlSlotLock(ch: Int, isLock: Boolean): Frame {
+        if (ch !in 0..1) return Frame(-1, byteArrayOf())
+        return frame.newFrame().apply { cmd = frame.cmd }.buildCtrlKeySlotLockAndCharge(ch, isLock, false)
+    }
+
+    /**
+     * 控制槽位是否充电
+     *
+     * @param ch        控制槽位
+     * @param isCharge  是否充电
+     */
+    fun ctrlSlotCharge(ch: Int, isCharge: Boolean): Frame {
+        if (ch !in 0..1) return Frame(-1, byteArrayOf())
+        // 不控制的一项从本地缓存状态中获取
+        return frame.newFrame().apply { cmd = frame.cmd }.buildCtrlKeySlotLockAndCharge(ch, false, isCharge)
+    }
+
+    /**
+     * 控制锁住和充电状态
+     *
+     * @param ch        控制通道 支持全部控制:-1
+     * @param isLock    是否锁住
+     * @param isCharge  是否充电
+     */
+    fun ctrlSlotLockAndCharge(ch: Int, isLock: Boolean, isCharge: Boolean): Frame {
+        return frame.newFrame().apply { cmd = frame.cmd }.buildCtrlKeySlotLockAndCharge(ch, isLock, isCharge)
+    }
+
+}

+ 51 - 0
transport/src/main/java/com/iscs/comm/entity/device/DeviceLockSlot.kt

@@ -0,0 +1,51 @@
+package com.iscs.comm.entity.device
+
+import com.iscs.comm.entity.Frame
+import com.iscs.comm.entity.device.status.DeviceStatus
+import com.iscs.comm.entity.device.status.DeviceStatusLockSlot
+import com.iscs.comm.enums.DeviceType
+import com.iscs.comm.extension.buildCheckDeviceStatus
+import com.iscs.comm.extension.buildCtrlLockSlotLock
+
+/**
+ * 锁底座设备类型
+ */
+class DeviceLockSlot(frame: Frame) : Device(frame) {
+
+    /**
+     * 配置当前设备类型
+     */
+    override val deviceType: DeviceType
+        get() = DeviceType.SLOT_LOCK
+
+    /**
+     * 构建默认设备状态
+     */
+    override var deviceStatus: DeviceStatus = DeviceStatusLockSlot()
+
+    override fun ctrlCheckDeviceStatus(): Frame {
+        return frame.newFrame().apply { cmd = frame.cmd }.buildCheckDeviceStatus()
+    }
+
+    /**
+     * 更新锁底座设备状态
+     */
+    override fun updateDeviceInfo(frame: Frame) {
+        deviceStatus.updateInfo(frame)
+    }
+
+    override fun updateDeviceStatus(frame: Frame) {
+        deviceStatus.updateStatus(frame)
+    }
+
+    /**
+     * 控制锁仓是否锁定
+     *
+     * @param ch        控制哪个槽位 默认-1控制所有
+     * @param isLock    是否锁定
+     */
+    fun ctrlSlotLock(ch: Int = -1, isLock: Boolean): Frame {
+        return frame.newFrame().apply { cmd = frame.cmd }.buildCtrlLockSlotLock(ch, isLock)
+    }
+
+}

+ 23 - 0
transport/src/main/java/com/iscs/comm/entity/device/DeviceNone.kt

@@ -0,0 +1,23 @@
+package com.iscs.comm.entity.device
+
+import com.iscs.comm.entity.Frame
+import com.iscs.comm.extension.buildCheckDeviceStatus
+
+/**
+ * 未知设备类型
+ */
+class DeviceNone(frame: Frame) : Device(frame) {
+
+    override fun updateDeviceInfo(frame: Frame) {
+
+    }
+
+    override fun updateDeviceStatus(frame: Frame) {
+
+    }
+
+    override fun ctrlCheckDeviceStatus(): Frame {
+        return frame.newFrame().apply { cmd = frame.cmd }.buildCheckDeviceStatus()
+    }
+
+}

+ 16 - 0
transport/src/main/java/com/iscs/comm/entity/device/status/DeviceStatus.kt

@@ -0,0 +1,16 @@
+package com.iscs.comm.entity.device.status
+
+import com.iscs.comm.entity.Frame
+
+abstract class DeviceStatus() {
+
+    /**
+     * 更新设备信息
+     */
+    abstract fun updateInfo(frame: Frame)
+
+    /**
+     * 更新设备状态
+     */
+    abstract fun updateStatus(frame: Frame)
+}

+ 76 - 0
transport/src/main/java/com/iscs/comm/entity/device/status/DeviceStatusKeySlot.kt

@@ -0,0 +1,76 @@
+package com.iscs.comm.entity.device.status
+
+import com.iscs.comm.entity.Frame
+import com.iscs.comm.entity.device.status.bean.SlotKeyBean
+import com.iscs.comm.extension.toBinaryString
+import java.util.concurrent.ConcurrentLinkedQueue
+
+/**
+ * 钥匙仓位状态模型
+ */
+class DeviceStatusKeySlot : DeviceStatus() {
+
+    // 仓位状态列表
+    val slotList = ConcurrentLinkedQueue<SlotKeyBean>()
+
+    init {
+        // 初始化构建锁底座
+        for (i in 0..1) slotList.add(SlotKeyBean().apply {
+            ch = i
+        })
+    }
+
+    /**
+     * 更新设备状态
+     *
+     * @param frame 设备状态帧
+     */
+    override fun updateInfo(frame: Frame) {
+        if (frame.data.size >= 10) {
+            // 获取指定位置数据
+            val dataL = frame.data[9].toBinaryString()
+            val dataH = frame.data[10].toBinaryString()
+            // Log.d("xiaoming","检查钥匙仓位是否有设备 L = $dataL H = $dataH")
+            for (ch in 0..1) {
+                val data = if (ch == 0) dataL else dataH
+                val isNotEmpty = data[7] == '1'
+                val find = slotList.find { it.ch == ch }
+                find?.isUsed = isNotEmpty
+            }
+        }
+    }
+
+    override fun updateStatus(frame: Frame) {
+        if (frame.data.size >= 10) {
+            // 获取指定位置数据
+            val dataL = frame.data[9].toBinaryString()
+            val dataH = frame.data[10].toBinaryString()
+            // Log.d("xiaoming","检查钥匙仓位工作状态 L = $dataL H = $dataH")
+            for (ch in 0..1) {
+                when (ch) {
+                    // 左边仓位
+                    0 -> {
+                        val isSlotLock = dataL[7] == '1'
+                        val isCharging = dataL[6] == '1'
+                        val isWorked = dataH[7] == '0'
+                        val find = slotList.find { it.ch == 0 }
+                        find?.isSlotLock = isSlotLock
+                        find?.isCharging = isCharging
+                        find?.isWorked = isWorked
+                    }
+                    // 右边仓位
+                    1 -> {
+                        val isSlotLock = dataL[3] == '1'
+                        val isCharging = dataL[2] == '1'
+                        val isWorked = dataH[3] == '0'
+                        val find = slotList.find { it.ch == 1 }
+                        find?.isSlotLock = isSlotLock
+                        find?.isCharging = isCharging
+                        find?.isWorked = isWorked
+                    }
+                }
+            }
+        }
+    }
+
+}

+ 60 - 0
transport/src/main/java/com/iscs/comm/entity/device/status/DeviceStatusLockSlot.kt

@@ -0,0 +1,60 @@
+package com.iscs.comm.entity.device.status
+
+import android.util.Log
+import com.iscs.comm.entity.Frame
+import com.iscs.comm.entity.device.status.bean.SlotBean
+import com.iscs.comm.extension.toBinaryString
+import java.util.concurrent.ConcurrentLinkedQueue
+
+/**
+ * 锁底座状态
+ *
+ * 锁默认是5路的锁,默认构建五路
+ */
+class DeviceStatusLockSlot() : DeviceStatus() {
+
+    // 仓位状态列表
+    val slotList = ConcurrentLinkedQueue<SlotBean>()
+
+    init {
+        // 初始化构建锁底座
+        for (i in 0..4) slotList.add(SlotBean(i, true))
+    }
+
+    /**
+     * 更新设备状态
+     *
+     * @param frame 设备状态帧
+     */
+    override fun updateInfo(frame: Frame) {
+        if (frame.data.size >= 9) {
+            // 获取指定位置数据
+            val data = frame.data[9].toBinaryString()
+            // Log.d("xiaoming", "$data")
+            for (i in 7 downTo 3) {
+                val ch = 7 - i
+                val isNotEmpty = data[i] == '1'
+                val find = slotList.find { it.ch == ch }
+                find?.isUsed = isNotEmpty
+            }
+        }
+    }
+
+    override fun updateStatus(frame: Frame) {
+        if (frame.data.size >= 10) {
+            // 获取指定位置数据
+            val dataL = frame.data[9].toBinaryString()
+            val dataH = frame.data[10].toBinaryString()
+            // Log.d("xiaoming", "$dataL $dataH")
+            for (i in 7 downTo 3) {
+                val ch = 7 - i
+                val isSlotLock = dataL[i] == '1'
+                val isWorked = dataH[i] == '0'
+                val find = slotList.find { it.ch == ch }
+                find?.isSlotLock = isSlotLock
+                find?.isWorked = isWorked
+            }
+        }
+    }
+
+}

+ 15 - 0
transport/src/main/java/com/iscs/comm/entity/device/status/DeviceStatusNone.kt

@@ -0,0 +1,15 @@
+package com.iscs.comm.entity.device.status
+
+import com.iscs.comm.entity.Frame
+
+class DeviceStatusNone() : DeviceStatus() {
+
+    override fun updateInfo(frame: Frame) {
+
+    }
+
+    override fun updateStatus(frame: Frame) {
+
+    }
+
+}

+ 22 - 0
transport/src/main/java/com/iscs/comm/entity/device/status/bean/SlotBean.kt

@@ -0,0 +1,22 @@
+package com.iscs.comm.entity.device.status.bean
+
+/**
+ * 锁仓基础字段构造
+ *
+ * @param ch            当前仓位ID
+ * @param isUsed        仓位是否使用的 true 被用 false 未使用
+ * @param isSlotLock    仓位是否锁定
+ * @param isWorked      是否工作的
+ */
+open class SlotBean(
+    open var ch: Int = -1,
+    open var isUsed: Boolean = false,
+    open var isSlotLock: Boolean = false,
+    open var isWorked: Boolean = false
+) {
+
+    override fun toString(): String {
+        return "SlotBean(ch: $ch, isUsed: $isUsed, isSlotLock: $isSlotLock, isWorked: $isWorked)"
+    }
+
+}

+ 12 - 0
transport/src/main/java/com/iscs/comm/entity/device/status/bean/SlotKeyBean.kt

@@ -0,0 +1,12 @@
+package com.iscs.comm.entity.device.status.bean
+
+/**
+ * 钥匙底座状态基础类
+ */
+class SlotKeyBean(var isCharging: Boolean = false) : SlotBean() {
+
+    override fun toString(): String {
+        return "SlotKeyBean(ch: $ch, isUsed: $isUsed, isSlotLock: $isSlotLock, isWorked: $isWorked, isCharging: $isCharging)"
+    }
+
+}

+ 10 - 0
transport/src/main/java/com/iscs/comm/enums/CommType.kt

@@ -0,0 +1,10 @@
+package com.iscs.comm.enums
+
+/**
+ * 协议类型定义
+ */
+enum class CommType {
+    NONE,   // 未配置
+    RS485,  // 485通信
+    CAN,    // CAN总线通信
+}

+ 7 - 0
transport/src/main/java/com/iscs/comm/enums/DeviceType.kt

@@ -0,0 +1,7 @@
+package com.iscs.comm.enums
+
+enum class DeviceType {
+    NONE,
+    SLOT_KEY,
+    SLOT_LOCK,
+}

+ 81 - 0
transport/src/main/java/com/iscs/comm/extension/CommFrameExt.kt

@@ -0,0 +1,81 @@
+package com.iscs.comm.extension
+
+import com.iscs.comm.entity.Frame
+import com.iscs.comm.protocol.CanProtocol.CAN_ID_BASE
+import com.iscs.comm.protocol.CanProtocol.CAN_KEY_RW
+import com.iscs.comm.protocol.CanProtocol.CAN_KEY_STATUS
+import com.iscs.comm.protocol.CanProtocol.CAN_READ_CMD
+import com.iscs.comm.protocol.CanProtocol.CAN_WRITE_CMD_2BYTE
+
+
+/**
+ * 构建读CAN总线下设备信息命令集合
+ */
+fun Frame.buildReadDeviceInfo(): List<Frame> {
+    val list = arrayListOf<Frame>()
+    for (i in 1..8) {
+        list += this.newFrame().apply {
+            cmd = CAN_ID_BASE + i
+            data = byteArrayOf(CAN_READ_CMD.toByte(), 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00)
+        }
+    }
+    return list
+}
+
+/**
+ * 构建读设备信息命令
+ */
+fun Frame.buildCheckDeviceStatus(): Frame {
+    data = byteArrayOf(
+        CAN_READ_CMD.toByte(), (CAN_KEY_RW and 0xFF).toByte(), ((CAN_KEY_RW shr 8) and 0xFF).toByte(), 0x00,
+        0x00, 0x00, 0x00, 0x00
+    )
+    return this
+}
+
+/**
+ * 构建用于控制锁仓位的开关是否锁定
+ *
+ * @param
+ */
+fun Frame.buildCtrlLockSlotLock(ch: Int, isLock: Boolean): Frame {
+    val value = if (ch == -1) 0xFF.toByte() else (1 shl ch).toByte()
+    data = byteArrayOf(
+        CAN_WRITE_CMD_2BYTE.toByte(), (CAN_KEY_RW and 0xFF).toByte(), ((CAN_KEY_RW shr 8) and 0xFF).toByte(), 0x00,
+        if (isLock) 0xFF.toByte() else 0x00, value, 0x00, 0x00
+    )
+    return this
+}
+
+/**
+ * 控制钥匙底座锁定和充电
+ *
+ * @param ch        通道:物理设备从左到右
+ * @param isLock    是否锁定
+ * @param isCharge  是否充电
+ */
+fun Frame.buildCtrlKeySlotLockAndCharge(ch: Int, isLock: Boolean, isCharge: Boolean): Frame {
+    val value = ((if (isCharge) 1 else 0) shl 1) + (if (isLock) 1 else 0)
+    val ctrl = if (ch == 0) value else if (ch == 1) value shl 4 else ((value shl 4) or value)
+    val work = if (ch == 0) 0B00000011 else if (ch == 1) 0B00110000 else 0B00110011
+    data = byteArrayOf(
+        CAN_WRITE_CMD_2BYTE.toByte(), (CAN_KEY_RW and 0xFF).toByte(), ((CAN_KEY_RW shr 8) and 0xFF).toByte(), 0x00,
+        ctrl.toByte(), work.toByte(), 0x00, 0x00
+    )
+    return this
+}
+
+/**
+ * 查询钥匙底座状态
+ */
+fun Frame.buildCtrlKeySlotStatus(): Frame {
+    val id = this.cmd
+    return this.newFrame().apply {
+        cmd = id
+        data = byteArrayOf(
+            CAN_READ_CMD.toByte(),
+            (CAN_KEY_STATUS and 0xFF).toByte(), ((CAN_KEY_STATUS shr 8) and 0xFF).toByte(),
+            0x00, 0x00, 0x00, 0x00, 0x00
+        )
+    }
+}

+ 42 - 0
transport/src/main/java/com/iscs/comm/extension/DataConvertExt.kt

@@ -0,0 +1,42 @@
+package com.iscs.comm.extension
+
+/**
+ * 将4位字节转为Int类型
+ *
+ * @param isBE 是否大端序
+ *             注释:CAN/MCU一般小端序,网络/协议一般大端序
+ */
+fun ByteArray.byte4ToInt(isBE: Boolean = false): Int {
+    if (this.size != 4) return -1
+    return if (!isBE) {
+        (this[0].toInt() and 0xFF shl 24) or (this[1].toInt() and 0xFF shl 16) or (this[2].toInt() and 0xFF shl 8) or (this[3].toInt() and 0xFF)
+    } else {
+        (this[3].toInt() and 0xFF shl 24) or (this[2].toInt() and 0xFF shl 16) or (this[1].toInt() and 0xFF shl 8) or (this[0].toInt() and 0xFF)
+    }
+}
+
+/**
+ * 将2位字节数组按照大小端的方式转换为Int类型
+ */
+fun ByteArray.byte2ToInt(isBE: Boolean = true): Int {
+    if (this.size != 2) return -1
+    return if (isBE) {
+        (this[0].toInt() and 0xFF) or (this[1].toInt() and 0xFF shl 8)
+    } else {
+        (this[1].toInt() and 0xFF) or (this[0].toInt() and 0xFF shl 8)
+    }
+}
+
+/**
+ * 将字节数组转换为16进制字符串
+ */
+fun ByteArray.toHexString(): String {
+    return joinToString("") { "%02X".format(it) }
+}
+
+/**
+ * 将字节转换为2进制
+ */
+fun Byte.toBinaryString(): String {
+    return this.toInt().and(0xFF).toString(2).padStart(8, '0')
+}

+ 25 - 0
transport/src/main/java/com/iscs/comm/extension/DeviceFactoryExt.kt

@@ -0,0 +1,25 @@
+package com.iscs.comm.extension
+
+import com.iscs.comm.entity.Frame
+import com.iscs.comm.entity.device.Device
+import com.iscs.comm.entity.device.DeviceNone
+import com.iscs.comm.entity.device.DeviceKeySlot
+import com.iscs.comm.entity.device.DeviceLockSlot
+
+/**
+ * 通过数据帧构建设备类型
+ */
+fun Frame.factoryDevice(): Device {
+    // Log.d("xiaoming", "构建设备 CAN_ID = ${this.cmd.toString(16)} DATA = ${this.data.toHexString()}")
+    val nodeId = this.data.copyOfRange(6, 8).byte2ToInt()
+    val subNodeId = this.data[8].toInt()
+    val type = this.data.copyOfRange(9, 11).byte2ToInt()
+    return when (type) {
+        // 钥匙底座
+        0 -> DeviceKeySlot(this)
+        // 5路挂锁底座
+        1 -> DeviceLockSlot(this)
+        // 其余未知设备
+        else -> DeviceNone(this)
+    }
+}

+ 100 - 0
transport/src/main/java/com/iscs/comm/intf/AbsCommBase.kt

@@ -0,0 +1,100 @@
+package com.iscs.comm.intf
+
+import com.iscs.comm.entity.Frame
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import java.io.ByteArrayOutputStream
+
+/**
+ * 抽象通信层,封装通用通信相关的操作
+ */
+abstract class AbsCommBase {
+
+    companion object {
+        // 读数据缓存容量
+        private const val READ_BUFF = 2048
+    }
+
+    // 定义携程,loop操作交由携程处理
+    val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
+
+    // 端口是否已连接
+    protected var isOpened = false
+
+    // 配置数据接收回调
+    private var listener: (ByteArray) -> Unit = {}
+
+
+    /**
+     * 打开端口
+     */
+    open fun open() {
+        startLoopReceive()
+    }
+
+    /**
+     * 关闭端口
+     */
+    open fun close() {
+
+    }
+
+    /**
+     * 写数据帧
+     */
+    abstract suspend fun write(frame: Frame)
+
+    /**
+     * 带响应式写数据
+     *
+     * @param frame     请求的数据帧
+     * @param timeout   配置响应式超时
+     */
+    abstract suspend fun writeWithResponse(frame: Frame, timeout: Long = 50): Frame
+
+    /**
+     * 读数据帧
+     */
+    abstract suspend fun read(): ByteArray
+
+    /**
+     * 校验包的完整性
+     *
+     * @param bytes 原始数据
+     */
+    abstract fun checkPackageComplete(bytes: ByteArray): Boolean
+
+    /**
+     * 设置数据回调
+     *
+     * @param listener 接收到的数据
+     */
+    fun setOnReceiverListener(listener: (ByteArray) -> Unit) {
+        this.listener = listener
+    }
+
+    /**
+     * 开启轮播去读取数据,连接成功后可通信
+     */
+    private fun startLoopReceive() = scope.launch(Dispatchers.IO) {
+        val buffer = ByteArrayOutputStream(READ_BUFF)
+        while (isActive && isOpened) {
+            val bytes = read()
+            if (bytes.isNotEmpty()) {
+                buffer.write(bytes)
+                // 校验是否完整包,不完整就继续获取数据
+                if (!checkPackageComplete(buffer.toByteArray())) continue
+            }
+            if (buffer.size() > 0) {
+                // 处理数据
+                listener(buffer.toByteArray())
+                // 数据获取完成,重置
+                buffer.reset()
+            }
+        }
+    }
+
+}

+ 21 - 0
transport/src/main/java/com/iscs/comm/intf/IDeviceListener.kt

@@ -0,0 +1,21 @@
+package com.iscs.comm.intf
+
+import com.iscs.comm.entity.device.Device
+
+interface IDeviceListener {
+
+    /**
+     * 回调所有设备列表
+     *
+     * @param devices 回调的设备列表
+     */
+    fun onDeviceList(devices: List<Device>)
+
+    /**
+     * 回调设备变化了监听
+     *
+     * @param device
+     */
+    fun onDeviceChanged(device: Device)
+
+}

+ 52 - 0
transport/src/main/java/com/iscs/comm/jni/NativeCan.kt

@@ -0,0 +1,52 @@
+package com.iscs.comm.jni
+
+/**
+ * JNI层通信
+ */
+object NativeCan {
+
+    init {
+        System.loadLibrary("iscs_comm")
+    }
+
+    /**
+     * CAN总线连接
+     *
+     *
+     * @param port 端口名 can0 can1 ...
+     *
+     * @return CAN连接成功的portId端口id
+     */
+    external fun canOpen(port: String): Int
+
+    /**
+     * CAN总线关闭
+     *
+     * @param portId CAN总线连接成功的id 通过 canOpen() return
+     */
+    external fun canClose(portId: Int): Int
+
+    /**
+     * CAN总线写数据操作
+     *
+     * @param portId    连接的端口id
+     * @param canId     CAN总线上需要查询的总线地址
+     * @param data      传输的数据
+     * @param len       数据长度
+     * @param isExt
+     * @param isRtr
+     * @param isFd
+     *
+     * @return 数据发送结果
+     */
+    external fun canWrite(portId: Int, canId: Int, data: ByteArray, len: Int, isExt: Boolean = false, isRtr: Boolean = false, isFd: Boolean = false): Int
+
+    /**
+     * 从CAN总线读取数据
+     *
+     * @param portId        CAN口端口id
+     * @param timeoutMillis 超时时间 单位:ms
+     */
+    external fun canRead(portId: Int, timeoutMillis: Int): ByteArray
+
+}

+ 150 - 0
transport/src/main/java/com/iscs/comm/manager/CanManager.kt

@@ -0,0 +1,150 @@
+package com.iscs.comm.manager
+
+import android.util.Log
+import com.iscs.comm.entity.Frame
+import com.iscs.comm.enums.CommType
+import com.iscs.comm.extension.byte2ToInt
+import com.iscs.comm.extension.byte4ToInt
+import com.iscs.comm.intf.AbsCommBase
+import com.iscs.comm.jni.NativeCan
+import com.iscs.comm.utils.ShellTools
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeout
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * Can总线操作帮助类
+ *
+ * @author GrayCarbon
+ */
+class CanManager(private val port: String) : AbsCommBase() {
+
+    companion object {
+        // CAN总线响应码的起始
+        private const val CAN_RESPONSE_INDEX = 1408
+    }
+
+    // JNI层CanSocket的id
+    private var portId = -1
+
+    // 待响应式的请求数据接收池
+    private val receiverPool = ConcurrentHashMap<String, CompletableDeferred<Frame>>()
+
+    /**
+     * 打开CAN口操作
+     *
+     *  1. 首先要对CAN口进行设置,需要通过shell方式且su操作 先关闭CAN口再打开CAN口及设置传输波特率
+     *  2. 成功后连接CAN口,设置数据读监听
+     */
+    override fun open() {
+        // 打开CAN口之前需要先对CAN口进行操作
+        ShellTools.execAsync(listOf("ip link set $port down", "ip link set $port up type can bitrate 1000000"), true) {
+            openListener()
+            portId = NativeCan.canOpen(port)
+            isOpened = portId > 0
+            super.open()
+        }
+    }
+
+    override fun close() {
+        NativeCan.canClose(portId)
+        portId = -1
+    }
+
+    /**
+     * 写数据帧到CAN总线
+     *
+     * @param frame 数据帧
+     */
+    override suspend fun write(frame: Frame) {
+        if (portId <= 0) return
+        frame.frameFrom = CommType.CAN
+        val ret = NativeCan.canWrite(portId, frame.cmd, frame.data, frame.data.size)
+        if (ret < 0 && isOpened) {
+            isOpened = false
+            close()
+            // 这里通知外部CAN连接中断了,重连操作交给外部使用
+            Log.d("xiaoming", "CAN disconnected")
+        }
+    }
+
+    /**
+     * 写数据到CAN总线中,带响应回调
+     *
+     * @param frame     写的数据帧
+     * @param timeout   超时时间 ms
+     *
+     * @param Frame 从CAN中响应的数据帧
+     */
+    override suspend fun writeWithResponse(frame: Frame, timeout: Long): Frame = withContext(Dispatchers.IO) {
+        frame.frameFrom = CommType.CAN
+        val deferred = CompletableDeferred<Frame>()
+        receiverPool["${frame.cmd}_${frame.data.copyOfRange(1, 3).byte2ToInt()}"] = deferred
+        write(frame)
+        try {
+            withTimeout(timeout) { deferred.await() }
+        } catch (_: TimeoutCancellationException) {
+            Frame(-408)
+        }
+    }
+
+    /**
+     * 从CAN中读取数据
+     *
+     * @return ByteArray 从CAN中读取到的字节数据
+     */
+    override suspend fun read(): ByteArray {
+        return NativeCan.canRead(portId, 10)
+    }
+
+    /**
+     * 校验CAN数据包的完整性
+     *
+     * @param bytes 从CAN中读取到的数据
+     *
+     * @return Boolean true or false 表示:数据包是否完整
+     */
+    override fun checkPackageComplete(bytes: ByteArray): Boolean {
+        if (bytes.size < 5) return false
+        var isComplete = false
+        // 校验包的长度是否完整
+        // 校验规则是查找数据长度字段匹配后面的长度,
+        // 接续查找,直至查找到尾部数据的长度和所校验的长度匹配
+        val pkgSize = bytes.size
+        var index = 0
+        while (index < (pkgSize - 1)) {
+            // 帧数据长度所在位置
+            val lenIndex = index + 4
+            // 帧的数据长度
+            val len = bytes[lenIndex].toInt()
+            // CAN总线响应码SDO
+            val canResponseIndex = bytes.copyOfRange(index, index + 4).byte4ToInt()
+            // 响应码必须是起始响应码以后的响应数字
+            if (canResponseIndex < CAN_RESPONSE_INDEX) return false
+            // 如果当前包校验完成,没有发现多余数据则认为是完整数据包
+            if (lenIndex + len + 1 == pkgSize) isComplete = true
+            index = lenIndex + len
+        }
+        return isComplete
+    }
+
+    /**
+     * 打开CAN读取的数据监听
+     */
+    private fun openListener() {
+        setOnReceiverListener {
+            // CAN_ID响应码比请求码小128,一一对应关系
+            val canId = it.copyOfRange(0, 4).byte4ToInt() + 128
+            // 这个是操作具体内容
+            val nodeId = it.copyOfRange(6, 8).byte2ToInt()
+            val deferred = receiverPool.remove("${canId}_${nodeId}")
+            if (deferred != null && !deferred.isCompleted) {
+                deferred.complete(Frame(canId, it, CommType.CAN))
+            }
+        }
+    }
+
+}

+ 34 - 0
transport/src/main/java/com/iscs/comm/protocol/CanProtocol.kt

@@ -0,0 +1,34 @@
+package com.iscs.comm.protocol
+
+/**
+ * 用于配置CAN的传输协议等信息
+ */
+object CanProtocol {
+
+    // CAN总线写数据 1字节的数据命令
+    const val CAN_WRITE_CMD_1BYTE = 0x2F
+
+    // CAN总线写数据 2字节的数据命令
+    const val CAN_WRITE_CMD_2BYTE = 0x2B
+
+    // CAN总线写数据 4字节的数据命令
+    const val CAN_WRITE_CMD_4BYTE = 0x23
+
+    // CAN总线读数据
+    const val CAN_READ_CMD = 0x40
+
+    // CAN下配置的设备地址,默认从CAN_ID 6000开始
+    const val CAN_ID_BASE = 0x0600
+
+    /** -----------------------------  操作类型  -------------------------------- **/
+
+    // 设备类型
+    const val CAN_DEVICE_TYPE = 0x6000
+
+    // 钥匙底座状态获取
+    const val CAN_KEY_STATUS = 0x6010
+
+    // 钥匙底座读写标记
+    const val CAN_KEY_RW = 0x6011
+
+}

+ 69 - 0
transport/src/main/java/com/iscs/comm/utils/ShellTools.kt

@@ -0,0 +1,69 @@
+package com.iscs.comm.utils
+
+import java.io.BufferedReader
+import java.io.DataOutputStream
+import java.io.InputStreamReader
+
+/**
+ * Shell工具类:支持同步、异步执行命令,可选 root。
+ */
+object ShellTools {
+
+    /**
+     * 同步执行命令
+     * @param commands 命令列表
+     * @param isRoot 是否使用su执行
+     */
+    fun exec(commands: List<String>, isRoot: Boolean = false): ShellResult {
+        val process: Process
+        val output = StringBuilder()
+        val error = StringBuilder()
+
+        try {
+            process = Runtime.getRuntime().exec(if (isRoot) "su" else "sh")
+            val os = DataOutputStream(process.outputStream)
+
+            // 写入命令
+            commands.forEach { cmd ->
+                os.writeBytes("$cmd\n")
+            }
+            os.writeBytes("exit\n")
+            os.flush()
+
+            // 读取输出
+            val reader = BufferedReader(InputStreamReader(process.inputStream))
+            val errorReader = BufferedReader(InputStreamReader(process.errorStream))
+
+            var line: String?
+            while (reader.readLine().also { line = it } != null) {
+                output.append(line).append("\n")
+            }
+            while (errorReader.readLine().also { line = it } != null) {
+                error.append(line).append("\n")
+            }
+
+            process.waitFor()
+            return ShellResult(process.exitValue(), output.toString(), error.toString())
+
+        } catch (e: Exception) {
+            return ShellResult(-1, "", e.message ?: "Unknown error")
+        }
+    }
+
+    /**
+     * 异步执行
+     */
+    fun execAsync(commands: List<String>, isRoot: Boolean = false, callback: (ShellResult) -> Unit) {
+        Thread {
+            val result = exec(commands, isRoot)
+            callback(result)
+        }.start()
+    }
+
+    /** 返回结果数据类 */
+    data class ShellResult(
+        val code: Int,        // 0 表示成功
+        val output: String,   // 标准输出
+        val error: String     // 错误输出
+    )
+}

+ 17 - 0
transport/src/test/java/com/iscs/comm/ExampleUnitTest.kt

@@ -0,0 +1,17 @@
+package com.iscs.comm
+
+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)
+    }
+}