Browse Source

项目初始化

王育民 5 years ago
commit
5534524170
37 changed files with 1414 additions and 0 deletions
  1. 1 0
      app/.gitignore
  2. 45 0
      app/build.gradle
  3. BIN
      app/libs/SenseID_SDK_Release_v1.1.2.jar
  4. 21 0
      app/proguard-rules.pro
  5. 26 0
      app/src/androidTest/java/cn/minbb/hid/ExampleInstrumentedTest.java
  6. 44 0
      app/src/main/AndroidManifest.xml
  7. 12 0
      app/src/main/java/cn/minbb/hid/App.java
  8. 6 0
      app/src/main/java/cn/minbb/hid/Commands.java
  9. 170 0
      app/src/main/java/cn/minbb/hid/CrashHandler.java
  10. 280 0
      app/src/main/java/cn/minbb/hid/MainActivity.java
  11. 251 0
      app/src/main/java/cn/minbb/hid/services/DataService.java
  12. 12 0
      app/src/main/java/cn/minbb/hid/utils/ByteUtil.java
  13. 75 0
      app/src/main/java/cn/minbb/hid/utils/CRC16Util.java
  14. 142 0
      app/src/main/java/cn/minbb/hid/utils/USBHIDUtil.java
  15. 34 0
      app/src/main/res/drawable-v24/ic_launcher_foreground.xml
  16. 170 0
      app/src/main/res/drawable/ic_launcher_background.xml
  17. 34 0
      app/src/main/res/layout/activity_main.xml
  18. 5 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  19. 5 0
      app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  20. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher.png
  21. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  22. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher.png
  23. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  24. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher.png
  25. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  26. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  27. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  28. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  29. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  30. 6 0
      app/src/main/res/values/colors.xml
  31. 3 0
      app/src/main/res/values/strings.xml
  32. 11 0
      app/src/main/res/values/styles.xml
  33. 6 0
      app/src/main/res/xml/device_filter.xml
  34. 17 0
      app/src/test/java/cn/minbb/hid/ExampleUnitTest.java
  35. 31 0
      build.gradle
  36. 6 0
      gradle/wrapper/gradle-wrapper.properties
  37. 1 0
      settings.gradle

+ 1 - 0
app/.gitignore

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

+ 45 - 0
app/build.gradle

@@ -0,0 +1,45 @@
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 28
+    defaultConfig {
+        applicationId "cn.minbb.hid"
+        minSdkVersion 16
+        targetSdkVersion 28
+        versionCode 1
+        versionName "1.0"
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+
+    sourceSets {
+        main {
+            assets.srcDirs = ['assets']
+            jniLibs.srcDirs = ['libs']
+        }
+    }
+
+    // 指定项目编码方式
+    compileOptions.encoding = "UTF-8"
+
+    // 指定编译版本
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+    implementation fileTree(include: ['*.jar'], dir: 'libs')
+    implementation 'com.android.support:appcompat-v7:28.0.0'
+    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
+    testImplementation 'junit:junit:4.12'
+    androidTestImplementation 'com.android.support.test:runner:1.0.2'
+    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+    implementation 'com.github.felHR85:UsbSerial:6.0.6'
+}

BIN
app/libs/SenseID_SDK_Release_v1.1.2.jar


+ 21 - 0
app/proguard-rules.pro

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

+ 26 - 0
app/src/androidTest/java/cn/minbb/hid/ExampleInstrumentedTest.java

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

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

@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="cn.minbb.hid">
+
+    <!-- 添加 USB HOST 权限 -->
+    <uses-feature
+        android:name="android.hardware.usb.host"
+        android:required="true" />
+
+    <uses-permission
+        android:name="android.hardware.usb.host"
+        android:required="false" />
+
+    <application
+        android:name=".App"
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme">
+
+        <activity android:name=".MainActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
+            </intent-filter>
+
+            <meta-data
+                android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
+                android:resource="@xml/device_filter" />
+        </activity>
+
+        <service
+            android:name=".services.DataService"
+            android:enabled="true"
+            android:exported="true" />
+    </application>
+
+</manifest>

+ 12 - 0
app/src/main/java/cn/minbb/hid/App.java

@@ -0,0 +1,12 @@
+package cn.minbb.hid;
+
+import android.app.Application;
+
+public class App extends Application {
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        CrashHandler.getInstance().init(this);
+    }
+}

+ 6 - 0
app/src/main/java/cn/minbb/hid/Commands.java

@@ -0,0 +1,6 @@
+package cn.minbb.hid;
+
+public class Commands {
+    public static final byte[] IN = new byte[4];
+    public static final byte[] OUT_S = new byte[4];
+}

+ 170 - 0
app/src/main/java/cn/minbb/hid/CrashHandler.java

@@ -0,0 +1,170 @@
+package cn.minbb.hid;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Environment;
+import android.os.Looper;
+import android.util.Log;
+import android.widget.Toast;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TimeZone;
+
+public class CrashHandler implements Thread.UncaughtExceptionHandler {
+
+    private static final String TAG = "CrashHandler";
+    private Context mContext;
+    private static final String SDCARD_ROOT = Environment.getExternalStorageDirectory().toString();
+    private static CrashHandler mInstance = new CrashHandler();
+
+    private CrashHandler() {
+    }
+
+    /**
+     * 单例模式,保证只有一个CrashHandler实例存在
+     *
+     * @return
+     */
+    public static CrashHandler getInstance() {
+        return mInstance;
+    }
+
+    /**
+     * 异常发生时,系统回调的函数,我们在这里处理一些操作
+     */
+    @Override
+    public void uncaughtException(Thread thread, Throwable ex) {
+        // 将一些信息保存到 SDCard 中
+        savaInfoToSD(mContext, ex);
+        // 提示用户程序即将退出
+        showToast(mContext, "很抱歉,程序遭遇异常,即将退出!");
+        try {
+            thread.sleep(2000);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+        // 完美退出程序方法
+        android.os.Process.killProcess(android.os.Process.myPid());
+        System.exit(1);
+    }
+
+    /**
+     * 为我们的应用程序设置自定义Crash处理
+     */
+    public void init(Context context) {
+        mContext = context;
+        Thread.setDefaultUncaughtExceptionHandler(this);
+    }
+
+    /**
+     * 显示提示信息,需要在线程中显示Toast
+     *
+     * @param context
+     * @param msg
+     */
+    private void showToast(Context context, String msg) {
+        new Thread(() -> {
+            Looper.prepare();
+            Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
+            Looper.loop();
+        }).start();
+    }
+
+    /**
+     * 获取一些简单的信息,软件版本,手机版本,型号等信息存放在HashMap中
+     *
+     * @param context
+     * @return
+     */
+    private HashMap<String, String> obtainSimpleInfo(Context context) {
+        HashMap<String, String> map = new HashMap<>();
+        PackageManager mPackageManager = context.getPackageManager();
+        PackageInfo mPackageInfo = null;
+        try {
+            mPackageInfo = mPackageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);
+        } catch (PackageManager.NameNotFoundException e) {
+            e.printStackTrace();
+        }
+        map.put("versionName", mPackageInfo.versionName);
+        map.put("versionCode", "" + mPackageInfo.versionCode);
+        map.put("MODEL", "" + Build.MODEL);
+        map.put("SDK_INT", "" + Build.VERSION.SDK_INT);
+        map.put("PRODUCT", "" + Build.PRODUCT);
+        return map;
+    }
+
+    /**
+     * 获取系统未捕捉的错误信息
+     *
+     * @param throwable
+     * @return
+     */
+    private String obtainExceptionInfo(Throwable throwable) {
+        StringWriter mStringWriter = new StringWriter();
+        PrintWriter mPrintWriter = new PrintWriter(mStringWriter);
+        throwable.printStackTrace(mPrintWriter);
+        mPrintWriter.close();
+        Log.e(TAG, mStringWriter.toString());
+        return mStringWriter.toString();
+    }
+
+    /**
+     * 保存获取的软件信息,设备信息和出错信息保存在 SDCard 中
+     *
+     * @param context
+     * @param ex
+     * @return
+     */
+    private String savaInfoToSD(Context context, Throwable ex) {
+        String fileName = null;
+        StringBuilder sb = new StringBuilder();
+        for (Map.Entry<String, String> entry : obtainSimpleInfo(context).entrySet()) {
+            String key = entry.getKey();
+            String value = entry.getValue();
+            sb.append(key).append(" = ").append(value).append("\n");
+        }
+        sb.append(obtainExceptionInfo(ex));
+
+        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+            // File dir = new File(SDCARD_ROOT + File.separator + "crash" + File.separator);
+            File dir = new File("/sdcard/crash/");
+            if (!dir.exists()) {
+                dir.mkdir();
+            }
+
+            try {
+                fileName = dir.toString() + File.separator + parseTime(System.currentTimeMillis()) + ".log";
+                FileOutputStream fos = new FileOutputStream(fileName);
+                fos.write(sb.toString().getBytes());
+                fos.flush();
+                fos.close();
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+        return fileName;
+    }
+
+    /**
+     * 将毫秒数转换成yyyy-MM-dd-HH-mm-ss的格式
+     *
+     * @param milliseconds
+     * @return
+     */
+    private String parseTime(long milliseconds) {
+        System.setProperty("memberInfo.timezone", "Asia/Shanghai");
+        TimeZone tz = TimeZone.getTimeZone("Asia/Shanghai");
+        TimeZone.setDefault(tz);
+        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        return format.format(new Date(milliseconds));
+    }
+}

+ 280 - 0
app/src/main/java/cn/minbb/hid/MainActivity.java

@@ -0,0 +1,280 @@
+package cn.minbb.hid;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbDeviceConnection;
+import android.hardware.usb.UsbEndpoint;
+import android.hardware.usb.UsbInterface;
+import android.hardware.usb.UsbManager;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.felhr.usbserial.UsbSerialDevice;
+import com.felhr.usbserial.UsbSerialInterface;
+import com.sensetime.serialport.SerialPortHelper;
+
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+
+import cn.minbb.hid.services.DataService;
+
+public class MainActivity extends AppCompatActivity {
+
+    private static final String TAG = "MainActivity";
+
+    private UsbManager usbManager;
+    private UsbDevice usbDevice;
+    private UsbInterface usbInterface;
+    private UsbDeviceConnection usbDeviceConnection;
+    private UsbEndpoint epIn, epOut;
+    private final int VendorID = 1061, ProductID = 33113;
+
+    private HIDDataReceiver receiver;
+    private SerialPortHelper mSerialPortHelper;
+    private TextView text;
+    private Thread threadDataIn;
+    private boolean isInterrupted = false;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        text = findViewById(R.id.text);
+
+        // 获取UsbManager
+        usbManager = (UsbManager) getSystemService(USB_SERVICE);
+        enumerateDevice();
+        findInterface();
+        openDevice();
+        assignEndpoint();
+
+        /**
+         * 如果你的程序经过上面的四步运行得到下面的打印信息,说明你可以进行通讯处理了
+         * 05-27 14:54:24.140: D/USB_HOST(10870): DeviceInfo: 8457 , 30264
+         * 05-27 14:54:24.140: D/USB_HOST(10870): 枚举设备成功
+         * 05-27 14:54:24.140: D/USB_HOST(10870): interfaceCounts : 1
+         * 05-27 14:54:24.140: D/USB_HOST(10870): 找到我的设备接口
+         * 05-27 14:54:24.160: D/USB_HOST(10870): 打开设备成功
+         * 05-27 14:54:24.170: D/USB_HOST(10870): 到此为止:
+         * 发现设备 -> 枚举设备 -> 找到设备的接口 -> 连接设备 -> 分配相应的端点,都已完成,下一步可以进行通讯处理。
+         */
+
+//        mSerialPortHelper = SerialPortHelper.newInstance(this);
+//        mSerialPortHelper.init();
+//        mSerialPortHelper.setCallBack(new SerialPortHelper.CallBack() {
+//            @Override
+//            public void receivedStartMessage() {
+//                Log.d(TAG, "启动人脸比对Activity");
+//                // startSenseTimeActivityFromLib();
+//            }
+//
+//            @Override
+//            public void onError(int code, String msg) {
+//                Log.e("zzf", msg);
+//            }
+//        });
+//
+//        mSerialPortHelper.start();
+
+        receiver = new HIDDataReceiver();
+        IntentFilter filter = new IntentFilter();
+        filter.addAction("cn.minbb.hid.services.DataService");
+        MainActivity.this.registerReceiver(receiver, filter);
+
+        UsbSerialDevice serial = UsbSerialDevice.createUsbSerialDevice(usbDevice, usbDeviceConnection);
+        if (null != serial) {
+            serial.open();
+            serial.setBaudRate(115200);
+            serial.setDataBits(UsbSerialInterface.DATA_BITS_8);
+            serial.setParity(UsbSerialInterface.PARITY_ODD);
+            serial.setFlowControl(UsbSerialInterface.FLOW_CONTROL_OFF);
+            serial.read(data -> {
+                Log.d(TAG, "************收到数据************");
+                Log.d(TAG, new String(data));
+                text.setText(new String(data));
+                Log.d(TAG, "**********************************");
+            });
+            serial.getCTS(new UsbSerialInterface.UsbCTSCallback() {
+                @Override
+                public void onCTSChanged(boolean state) {
+                    Log.d(TAG, "***************************** " + state);
+                    text.setText("*************" + state);
+                }
+            });
+        } else {
+            Log.e(TAG, "串口为空");
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        if (null != receiver) unregisterReceiver(receiver);
+//        mSerialPortHelper.release(); //释放串口
+    }
+
+    public class HIDDataReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String time = SimpleDateFormat.getTimeInstance().format(new Date());
+            Bundle bundle = intent.getExtras();
+            if (null != bundle) {
+                boolean success = bundle.getBoolean("success");
+                if (success) {
+                    text.setText(time + ": " + new String(bundle.getByteArray("data")));
+                } else {
+                    text.setText(time + ": " + bundle.getInt("data"));
+                }
+            } else {
+                Toast.makeText(getApplicationContext(), "数据包为空", Toast.LENGTH_SHORT).show();
+            }
+        }
+    }
+
+    /**
+     * 枚举设备
+     */
+    private void enumerateDevice() {
+        if (usbManager == null) return;
+        HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList();
+        if (!deviceList.isEmpty()) {
+            // deviceList不为空
+            StringBuffer sb = new StringBuffer();
+            for (UsbDevice device : deviceList.values()) {
+                sb.append(device.toString());
+                sb.append("\n");
+                // 输出设备信息
+                Log.d(TAG, "DeviceInfo: " + device.getVendorId() + ", " + device.getProductId());
+                // 枚举到设备
+                if (device.getVendorId() == VendorID && device.getProductId() == ProductID) {
+                    usbDevice = device;
+                    Log.d(TAG, "枚举设备成功");
+                } else {
+                    Log.d(TAG, "枚举设备失败");
+                }
+            }
+        }
+    }
+
+    /**
+     * 找设备接口
+     */
+    private void findInterface() {
+        if (usbDevice != null) {
+            Log.d(TAG, "接口数量 : " + usbDevice.getInterfaceCount());
+            for (int i = 0; i < usbDevice.getInterfaceCount(); i++) {
+                UsbInterface intf = usbDevice.getInterface(i);
+                // 根据手上的设备做一些判断,可以在枚举到设备时打印出来
+                System.out.println("*************************");
+                System.out.println("getInterfaceClass = " + intf.getInterfaceClass());
+                System.out.println("getInterfaceSubclass = " + intf.getInterfaceSubclass());
+                System.out.println("getInterfaceProtocol = " + intf.getInterfaceProtocol());
+                if (intf.getInterfaceClass() == 3 && intf.getInterfaceSubclass() == 0 && intf.getInterfaceProtocol() == 0) {
+                    usbInterface = intf;
+                    Log.d(TAG, "找到我的设备接口");
+                    break;
+                } else {
+                    Log.d(TAG, "没有符合的设备接口");
+                }
+            }
+        }
+    }
+
+    /**
+     * 打开设备
+     */
+    private boolean openDevice() {
+        if (usbInterface != null) {
+            UsbDeviceConnection conn = null;
+            // 在open前判断是否有连接权限;对于连接权限可以静态分配,也可以动态分配权限,可以查阅相关资料
+            if (usbManager.hasPermission(usbDevice)) {
+                conn = usbManager.openDevice(usbDevice);
+            } else {
+                Log.e(TAG, "权限不足");
+            }
+
+            if (conn == null) {
+                Log.e(TAG, "打开设备失败");
+                return false;
+            }
+
+            if (conn.claimInterface(usbInterface, true)) {
+                // Android 设备已经连上 HID 设备
+                usbDeviceConnection = conn;
+                Log.d(TAG, "打开设备成功");
+                return true;
+            } else {
+                Log.e(TAG, "无法打开连接通道");
+                conn.close();
+            }
+        } else {
+            Log.e(TAG, "USB 接口为空");
+        }
+        return false;
+    }
+
+    /**
+     * 分配端点,IN | OUT,即输入输出;此处我直接用1为OUT端点,0为IN,当然你也可以通过判断
+     */
+    private void assignEndpoint() {
+        Log.i(TAG, "USB接口端点数量 = " + usbInterface.getEndpointCount());
+        if (usbInterface == null) {
+            Log.e(TAG, "USB 端点为空");
+            return;
+        }
+        UsbEndpoint ep = usbInterface.getEndpoint(0);
+        // 开启线程接收数据
+        threadDataIn = new Thread(() -> {
+            while (!isInterrupted) {
+                try {
+                    byte[] buffer = new byte[255];
+                    int transfer = usbDeviceConnection.bulkTransfer(ep, buffer, buffer.length, 1000);
+                    // 发送广播
+                    Intent intent = new Intent();
+                    if (transfer < 0) {
+                        Log.e(TAG, "bulkIn返回输入为负数 = " + transfer);
+                        intent.putExtra("success", false);
+                        intent.putExtra("data", transfer);
+                    } else {
+                        Log.i(TAG, "数据接收成功 = " + transfer + " / " + Arrays.toString(buffer));
+                        intent.putExtra("success", true);
+                        intent.putExtra("data", buffer);
+                    }
+                    intent.setAction("cn.minbb.hid.services.DataService");
+                    sendBroadcast(intent);
+                } catch (Exception e) {
+                    e.printStackTrace();
+                    break;
+                }
+            }
+        });
+        threadDataIn.start();
+//        if (usbInterface.getEndpoint(1) != null) {
+//            epOut = usbInterface.getEndpoint(1);
+//        }
+//        if (usbInterface.getEndpoint(0) != null) {
+//            epIn = usbInterface.getEndpoint(0);
+//        }
+    }
+
+    public void onClick(View view) {
+        switch (view.getId()) {
+            case R.id.startService:
+                DataService.start(this);
+                break;
+            case R.id.stopService:
+                stopService(new Intent(MainActivity.this, DataService.class));
+                break;
+        }
+    }
+}

+ 251 - 0
app/src/main/java/cn/minbb/hid/services/DataService.java

@@ -0,0 +1,251 @@
+package cn.minbb.hid.services;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.usb.UsbConstants;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbDeviceConnection;
+import android.hardware.usb.UsbEndpoint;
+import android.hardware.usb.UsbInterface;
+import android.hardware.usb.UsbManager;
+import android.os.IBinder;
+import android.util.Log;
+import android.widget.Toast;
+
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+import cn.minbb.hid.utils.ByteUtil;
+
+public class DataService extends Service {
+
+    private static final String TAG = "DataService";
+    private UsbManager usbManager;
+    private UsbDevice usbDevice;
+    private UsbDeviceConnection usbDeviceConnection;
+    private List<UsbInterface> usbInterfaceList = new ArrayList<>(8);
+    private UsbEndpoint epBulkIn, epBulkOut, epControl, epIntEndpointIn, epIntEndpointOut;
+    private int VendorID, ProductID;
+    private Thread threadDataIn;
+    private boolean isInterrupted = false;
+
+    public DataService() {
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        throw new UnsupportedOperationException("Not yet implemented");
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        super.onStartCommand(intent, flags, startId);
+        Log.i(TAG, "数据接收服务启动");
+        // 获取 USB 管理器
+        usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
+
+        // 枚举设备
+        enumerateDevice(usbManager);
+        // 查找设备接口
+        getDeviceInterface();
+        // 获取设备端点
+        assignEndpoint(usbInterfaceList.get(0));
+        // 打开连接通道
+        openDevice(usbInterfaceList.get(0));
+        return super.onStartCommand(intent, flags, startId);
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (threadDataIn != null && !threadDataIn.isInterrupted()) {
+            isInterrupted = true;
+        }
+        Log.i(TAG, "数据接收服务停止");
+    }
+
+    public static void start(Context context) {
+        Intent starter = new Intent(context, DataService.class);
+        context.startService(starter);
+    }
+
+    // 枚举设备
+    private void enumerateDevice(UsbManager usbManager) {
+        Log.i(TAG, "开始进行枚举设备");
+        if (usbManager == null) {
+            Log.i(TAG, "创建 UsbManager 失败,请重新启动应用");
+        } else {
+            HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList();
+            if (!(deviceList.isEmpty())) {
+                // USB 设备列表不为空
+                Log.i(TAG, "USB 设备列表不为空");
+                for (UsbDevice device : deviceList.values()) {
+                    // 输出设备信息
+                    Log.i(TAG, "DeviceInfo: " + device.getVendorId() + " , " + device.getProductId());
+                    // 保存设备 VID 和 PID
+                    VendorID = device.getVendorId();
+                    ProductID = device.getProductId();
+                    // 保存匹配到的设备
+                    if (VendorID == 1061 && ProductID == 33113) {
+                        // 获取 USBDevice
+                        this.usbDevice = device;
+                        Log.i(TAG, "发现待匹配设备:" + device.getVendorId() + "," + device.getProductId());
+                        Toast.makeText(getApplicationContext(), "发现待匹配设备", Toast.LENGTH_SHORT).show();
+                    } else {
+                        Log.i(TAG, "未发现指定设备");
+                    }
+                }
+            } else {
+                Log.e(TAG, "请连接USB设备至PAD!");
+                Toast.makeText(getApplicationContext(), "请连接USB设备至PAD!", Toast.LENGTH_SHORT).show();
+            }
+        }
+    }
+
+    // 寻找设备接口
+    private void getDeviceInterface() {
+        if (usbDevice != null) {
+            Log.d(TAG, "接口数量 = " + usbDevice.getInterfaceCount());
+            for (int i = 0; i < usbDevice.getInterfaceCount(); i++) {
+                UsbInterface inter = usbDevice.getInterface(i);
+                // 保存设备接口
+                usbInterfaceList.add(inter);
+                Log.i(TAG, "成功获得设备接口 " + i + ": " + inter.getId());
+            }
+        } else {
+            Log.e(TAG, "设备为空!");
+        }
+    }
+
+    // 分配端点,IN | OUT,即输入输出;可以通过判断
+    private UsbEndpoint assignEndpoint(UsbInterface usbInterface) {
+        if (usbInterface == null) {
+            Log.e(TAG, "USB 端点为空");
+            return null;
+        }
+        int endpointCount = usbInterface.getEndpointCount();
+        Log.i(TAG, "端点数量 = " + endpointCount);
+        for (int i = 0; i < endpointCount; i++) {
+            UsbEndpoint ep = usbInterface.getEndpoint(i);
+            // look for bulk endpoint
+            if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) {
+                if (ep.getDirection() == UsbConstants.USB_DIR_OUT) {
+                    epBulkOut = ep;
+                    Log.i(TAG, "Find the BulkEndpointOut," + "index:" + i + ", 使用端点号:" + epBulkOut.getEndpointNumber());
+                } else {
+                    epBulkIn = ep;
+                    Log.i(TAG, "Find the BulkEndpointIn:" + "index:" + i + ", 使用端点号:" + epBulkIn.getEndpointNumber());
+                }
+            }
+            // look for contorl endpoint
+            if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_CONTROL) {
+                epControl = ep;
+                Log.i(TAG, "Find the ControlEndPoint:" + "index:" + i + ", 使用端点号:" + epControl.getEndpointNumber());
+            }
+            // look for interrupte endpoint
+            if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_INT) {
+                if (ep.getDirection() == UsbConstants.USB_DIR_OUT) {
+                    epIntEndpointOut = ep;
+                    Log.i(TAG, "Find the InterruptEndpointOut:" + "index:" + i + ", 使用端点号:" + epIntEndpointOut.getEndpointNumber());
+                }
+                if (ep.getDirection() == UsbConstants.USB_DIR_IN) {
+                    epIntEndpointIn = ep;
+                    Log.i(TAG, "Find the InterruptEndpointIn:" + "index:" + i + ", 使用端点号:" + epIntEndpointIn.getEndpointNumber());
+                }
+            }
+        }
+        if (epBulkOut == null && epBulkIn == null && epControl == null && epIntEndpointOut == null && epIntEndpointIn == null) {
+            throw new IllegalArgumentException("没有找到任何端点");
+        }
+        return epIntEndpointIn;
+    }
+
+    // 打开设备
+    public void openDevice(UsbInterface usbInterface) {
+        if (usbInterface != null) {
+            UsbDeviceConnection conn = null;
+            // 在 open 前判断是否有连接权限;对于连接权限可以静态分配,也可以动态分配权限
+            if (usbManager.hasPermission(usbDevice)) {
+                conn = usbManager.openDevice(usbDevice);
+            } else {
+                Log.e(TAG, "权限不足");
+            }
+
+            if (conn == null) {
+                Log.e(TAG, "打开设备失败");
+                return;
+            }
+
+            if (conn.claimInterface(usbInterface, true)) {
+                usbDeviceConnection = conn;
+                Log.i(TAG, "打开设备成功");
+                Log.i(TAG, "Serial number = " + usbDeviceConnection.getSerial());
+                Log.i(TAG, "FileDescriptor = " + usbDeviceConnection.getFileDescriptor());
+                Log.i(TAG, "RawDescriptors = " + Arrays.toString(usbDeviceConnection.getRawDescriptors()));
+                try {
+                    Log.i(TAG, "RawDescriptors = " + new String(usbDeviceConnection.getRawDescriptors(), 0, 120, "utf-16le"));
+                } catch (UnsupportedEncodingException e) {
+                    e.printStackTrace();
+                }
+
+                // 开启线程接收数据
+                threadDataIn = new Thread(() -> {
+                    while (!isInterrupted) {
+                        try {
+                            byte[] buffer = new byte[255];
+                            int transfer = usbDeviceConnection.bulkTransfer(epIntEndpointIn, buffer, buffer.length, 1000);
+                            // 发送广播
+                            Intent intent = new Intent();
+                            if (transfer < 0) {
+                                Log.e(TAG, "bulkIn返回输入为负数 = " + transfer);
+                                intent.putExtra("success", false);
+                                intent.putExtra("data", transfer);
+                            } else {
+                                Log.i(TAG, "数据接收成功 = " + transfer + " / " + Arrays.toString(buffer));
+                                intent.putExtra("success", true);
+                                intent.putExtra("data", buffer);
+                            }
+                            intent.setAction("cn.minbb.hid.services.DataService");
+                            sendBroadcast(intent);
+                        } catch (Exception e) {
+                            e.printStackTrace();
+                            break;
+                        }
+                    }
+                });
+                threadDataIn.start();
+            } else {
+                Log.e(TAG, "无法打开连接通道");
+                conn.close();
+            }
+        }
+    }
+
+    // 从设备接收数据 bulkIn
+    private byte[] receiveMessageFromPoint() {
+        byte[] buffer = new byte[15];
+        int transfer = usbDeviceConnection.bulkTransfer(epBulkIn, buffer, buffer.length, 2000);
+        if (transfer < 0)
+            Log.e(TAG, "bulkIn返回输入为负数 = " + transfer);
+        else {
+            Log.i(TAG, "数据接收成功 = " + transfer + " / " + Arrays.toString(buffer));
+        }
+        return buffer;
+    }
+
+    // 发送数据
+    private void sendMessageToPoint(byte[] buffer) {
+        // bulkOut传输
+        int transfer = usbDeviceConnection.bulkTransfer(epBulkOut, buffer, buffer.length, 0);
+        if (transfer < 0)
+            Log.e(TAG, "bulkOut返回输出为负数 = " + transfer);
+        else {
+            Log.i(TAG, "数据发送成功 = " + transfer);
+        }
+    }
+}

+ 12 - 0
app/src/main/java/cn/minbb/hid/utils/ByteUtil.java

@@ -0,0 +1,12 @@
+package cn.minbb.hid.utils;
+
+public class ByteUtil {
+
+    public static byte[] byte10ToByte16(byte[] byte10) {
+        byte[] byte16 = new byte[byte10.length];
+        for (int i = 0; i < byte10.length; i++) {
+            byte16[i] = Byte.parseByte(Integer.toHexString(byte10[i]));
+        }
+        return byte16;
+    }
+}

+ 75 - 0
app/src/main/java/cn/minbb/hid/utils/CRC16Util.java

@@ -0,0 +1,75 @@
+package cn.minbb.hid.utils;
+
+import android.util.Log;
+
+public class CRC16Util {
+
+    private static byte[] crc16_tab_h = {(byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x01, (byte) 0xC0,
+            (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1,
+            (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0,
+            (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0,
+            (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40};
+
+    private static byte[] crc16_tab_l = {(byte) 0x00, (byte) 0xC0, (byte) 0xC1, (byte) 0x01, (byte) 0xC3, (byte) 0x03, (byte) 0x02, (byte) 0xC2, (byte) 0xC6, (byte) 0x06, (byte) 0x07, (byte) 0xC7, (byte) 0x05, (byte) 0xC5, (byte) 0xC4, (byte) 0x04, (byte) 0xCC, (byte) 0x0C, (byte) 0x0D, (byte) 0xCD, (byte) 0x0F, (byte) 0xCF, (byte) 0xCE, (byte) 0x0E, (byte) 0x0A, (byte) 0xCA, (byte) 0xCB, (byte) 0x0B, (byte) 0xC9, (byte) 0x09, (byte) 0x08, (byte) 0xC8, (byte) 0xD8, (byte) 0x18, (byte) 0x19, (byte) 0xD9, (byte) 0x1B, (byte) 0xDB, (byte) 0xDA, (byte) 0x1A, (byte) 0x1E, (byte) 0xDE, (byte) 0xDF, (byte) 0x1F, (byte) 0xDD, (byte) 0x1D, (byte) 0x1C, (byte) 0xDC, (byte) 0x14, (byte) 0xD4, (byte) 0xD5, (byte) 0x15, (byte) 0xD7, (byte) 0x17, (byte) 0x16, (byte) 0xD6, (byte) 0xD2, (byte) 0x12,
+            (byte) 0x13, (byte) 0xD3, (byte) 0x11, (byte) 0xD1, (byte) 0xD0, (byte) 0x10, (byte) 0xF0, (byte) 0x30, (byte) 0x31, (byte) 0xF1, (byte) 0x33, (byte) 0xF3, (byte) 0xF2, (byte) 0x32, (byte) 0x36, (byte) 0xF6, (byte) 0xF7, (byte) 0x37, (byte) 0xF5, (byte) 0x35, (byte) 0x34, (byte) 0xF4, (byte) 0x3C, (byte) 0xFC, (byte) 0xFD, (byte) 0x3D, (byte) 0xFF, (byte) 0x3F, (byte) 0x3E, (byte) 0xFE, (byte) 0xFA, (byte) 0x3A, (byte) 0x3B, (byte) 0xFB, (byte) 0x39, (byte) 0xF9, (byte) 0xF8, (byte) 0x38, (byte) 0x28, (byte) 0xE8, (byte) 0xE9, (byte) 0x29, (byte) 0xEB, (byte) 0x2B, (byte) 0x2A, (byte) 0xEA, (byte) 0xEE, (byte) 0x2E, (byte) 0x2F, (byte) 0xEF, (byte) 0x2D, (byte) 0xED, (byte) 0xEC, (byte) 0x2C, (byte) 0xE4, (byte) 0x24, (byte) 0x25, (byte) 0xE5, (byte) 0x27, (byte) 0xE7,
+            (byte) 0xE6, (byte) 0x26, (byte) 0x22, (byte) 0xE2, (byte) 0xE3, (byte) 0x23, (byte) 0xE1, (byte) 0x21, (byte) 0x20, (byte) 0xE0, (byte) 0xA0, (byte) 0x60, (byte) 0x61, (byte) 0xA1, (byte) 0x63, (byte) 0xA3, (byte) 0xA2, (byte) 0x62, (byte) 0x66, (byte) 0xA6, (byte) 0xA7, (byte) 0x67, (byte) 0xA5, (byte) 0x65, (byte) 0x64, (byte) 0xA4, (byte) 0x6C, (byte) 0xAC, (byte) 0xAD, (byte) 0x6D, (byte) 0xAF, (byte) 0x6F, (byte) 0x6E, (byte) 0xAE, (byte) 0xAA, (byte) 0x6A, (byte) 0x6B, (byte) 0xAB, (byte) 0x69, (byte) 0xA9, (byte) 0xA8, (byte) 0x68, (byte) 0x78, (byte) 0xB8, (byte) 0xB9, (byte) 0x79, (byte) 0xBB, (byte) 0x7B, (byte) 0x7A, (byte) 0xBA, (byte) 0xBE, (byte) 0x7E, (byte) 0x7F, (byte) 0xBF, (byte) 0x7D, (byte) 0xBD, (byte) 0xBC, (byte) 0x7C, (byte) 0xB4, (byte) 0x74,
+            (byte) 0x75, (byte) 0xB5, (byte) 0x77, (byte) 0xB7, (byte) 0xB6, (byte) 0x76, (byte) 0x72, (byte) 0xB2, (byte) 0xB3, (byte) 0x73, (byte) 0xB1, (byte) 0x71, (byte) 0x70, (byte) 0xB0, (byte) 0x50, (byte) 0x90, (byte) 0x91, (byte) 0x51, (byte) 0x93, (byte) 0x53, (byte) 0x52, (byte) 0x92, (byte) 0x96, (byte) 0x56, (byte) 0x57, (byte) 0x97, (byte) 0x55, (byte) 0x95, (byte) 0x94, (byte) 0x54, (byte) 0x9C, (byte) 0x5C, (byte) 0x5D, (byte) 0x9D, (byte) 0x5F, (byte) 0x9F, (byte) 0x9E, (byte) 0x5E, (byte) 0x5A, (byte) 0x9A, (byte) 0x9B, (byte) 0x5B, (byte) 0x99, (byte) 0x59, (byte) 0x58, (byte) 0x98, (byte) 0x88, (byte) 0x48, (byte) 0x49, (byte) 0x89, (byte) 0x4B, (byte) 0x8B, (byte) 0x8A, (byte) 0x4A, (byte) 0x4E, (byte) 0x8E, (byte) 0x8F, (byte) 0x4F, (byte) 0x8D, (byte) 0x4D,
+            (byte) 0x4C, (byte) 0x8C, (byte) 0x44, (byte) 0x84, (byte) 0x85, (byte) 0x45, (byte) 0x87, (byte) 0x47, (byte) 0x46, (byte) 0x86, (byte) 0x82, (byte) 0x42, (byte) 0x43, (byte) 0x83, (byte) 0x41, (byte) 0x81, (byte) 0x80, (byte) 0x40};
+
+    /**
+     * 计算CRC16校验  对外的接口
+     *
+     * @param data 需要计算的数组
+     * @return CRC16校验值
+     */
+    public static int calcCrc16(byte[] data) {
+        return calcCrc16(data, 0, data.length);
+    }
+
+    /**
+     * 计算CRC16校验
+     *
+     * @param data   需要计算的数组
+     * @param offset 起始位置
+     * @param len    长度
+     * @return CRC16校验值
+     */
+    public static int calcCrc16(byte[] data, int offset, int len) {
+        return calcCrc16(data, offset, len, 0xffff);
+    }
+
+    /**
+     * 计算CRC16校验
+     *
+     * @param data   需要计算的数组
+     * @param offset 起始位置
+     * @param len    长度
+     * @param preval 之前的校验值
+     * @return CRC16校验值
+     */
+    public static int calcCrc16(byte[] data, int offset, int len, int preval) {
+        int ucCRCHi = (preval & 0xff00) >> 8;
+        int ucCRCLo = preval & 0x00ff;
+        int iIndex;
+        for (int i = 0; i < len; ++i) {
+            iIndex = (ucCRCLo ^ data[offset + i]) & 0x00ff;
+            ucCRCLo = ucCRCHi ^ crc16_tab_h[iIndex];
+            ucCRCHi = crc16_tab_l[iIndex];
+        }
+        return ((ucCRCHi & 0x00ff) << 8) | (ucCRCLo & 0x00ff) & 0xffff;
+    }
+
+    /**
+     * 将计算的CRC值转换为加空格的  比如:crc值为 A30A -> A3 0A
+     *
+     * @param res
+     * @return
+     */
+    public static String getCrc(int res) {
+        String format = String.format("%04x", res);
+        String substring = format.substring(0, 2);
+        String substring1 = format.substring(2, 4);
+        Log.i("BLUE DATA = ", "crc ---- : " + substring + "  " + substring1);
+        return substring.concat(" ").concat(substring1).concat(" ");
+    }
+}

+ 142 - 0
app/src/main/java/cn/minbb/hid/utils/USBHIDUtil.java

@@ -0,0 +1,142 @@
+package cn.minbb.hid.utils;
+
+import android.content.Context;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbDeviceConnection;
+import android.hardware.usb.UsbEndpoint;
+import android.hardware.usb.UsbInterface;
+import android.hardware.usb.UsbManager;
+import android.util.Log;
+import android.widget.Toast;
+
+import java.util.HashMap;
+
+import cn.minbb.hid.Commands;
+
+public class USBHIDUtil {
+
+    private Context context;
+
+    private static final String TAG = "USBHIDUtil";
+    private UsbManager usbManager;
+    private UsbDevice usbDevice;
+    private UsbInterface usbInterface;
+    private UsbDeviceConnection usbDeviceConnection;
+    private UsbEndpoint epIn, epOut;
+
+    public USBHIDUtil(Context context) {
+        this.context = context;
+        usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
+    }
+
+    public void getAllHIDDevice() {
+        if (usbManager == null) {
+            Toast.makeText(context, "USB 管理器为空", Toast.LENGTH_SHORT).show();
+            return;
+        }
+        HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList();
+        if (!deviceList.isEmpty()) {
+            for (UsbDevice device : deviceList.values()) {
+                int VendorID = 0x5302, ProductID = 0x636D;
+                if (device.getVendorId() == VendorID && device.getProductId() == ProductID) {
+                    usbDevice = device;
+                    // 找到 Device 接口并分配相应端点
+                    findInterfaceAndEndpoint();
+                } else {
+                    showToastAndLog("Not Find VID and PID");
+                }
+            }
+        } else {
+            showToastAndLog("未枚举到设备!请先连接设备,再重启程序。");
+        }
+    }
+
+    // 查找接口和指定分配端点
+    private void findInterfaceAndEndpoint() {
+        if (usbDevice == null) {
+            showToastAndLog("USB 设备不存在");
+            return;
+        }
+
+        // 查找设备接口
+        for (int i = 0; i < usbDevice.getInterfaceCount(); ) {
+            // 获取设备接口,一般都是一个接口,可以打印getInterfaceCount()方法查看接口的个数,在这个接口上有两个端点,OUT 和 IN
+            UsbInterface inter = usbDevice.getInterface(i);
+            Log.d(TAG, i + " " + inter);
+            if (inter.getInterfaceClass() == 8 && inter.getInterfaceSubclass() == 6 && inter.getInterfaceProtocol() == 80) {
+                // HID设备的相关信息
+                usbInterface = inter;
+            }
+            break;
+        }
+
+        if (usbInterface != null) {
+            UsbDeviceConnection connection;
+            // 判断是否有权限
+            if (usbManager.hasPermission(usbDevice)) {
+                // 打开设备,获取 UsbDeviceConnection 对象,连接设备,用于后面的通讯
+                connection = usbManager.openDevice(usbDevice);
+                if (connection == null) {
+                    return;
+                }
+                if (connection.claimInterface(usbInterface, true)) {
+                    usbDeviceConnection = connection;
+                    getEndpoint(connection, usbInterface);
+                } else {
+                    connection.close();
+                }
+            } else {
+                showToastAndLog("没有权限");
+            }
+        } else {
+            showToastAndLog("USB 接口不存在");
+        }
+    }
+
+    private void getEndpoint(UsbDeviceConnection connection, UsbInterface usbInterface) {
+        if (usbInterface.getEndpoint(1) != null) {
+            epOut = usbInterface.getEndpoint(1);
+        }
+        if (usbInterface.getEndpoint(0) != null) {
+            epIn = usbInterface.getEndpoint(0);
+        }
+    }
+
+    // 发送包操作,发送OUT + 发送COM + 接收IN
+    private void sendPackage(byte[] command) {
+        int ret = -100;
+        int length = command.length;
+        // 组织准备命令
+        byte[] sendOut = Commands.OUT_S;
+        sendOut[8] = (byte) (length & 0xff);
+        sendOut[9] = (byte) ((length >> 8) & 0xff);
+        sendOut[10] = (byte) ((length >> 16) & 0xff);
+        sendOut[11] = (byte) ((length >> 24) & 0xff);
+        // 1, 发送准备命令
+        ret = usbDeviceConnection.bulkTransfer(epOut, sendOut, 31, 10000);
+        if (ret != 31) {
+            return;
+        }
+        // 2, 发送 COM
+        ret = usbDeviceConnection.bulkTransfer(epOut, command, length, 10000);
+        if (ret != length) {
+            return;
+        }
+        // 3, 接收发送成功信息
+        ret = usbDeviceConnection.bulkTransfer(epIn, Commands.IN, 13, 10000);
+        if (ret != 13) {
+            return;
+        }
+    }
+
+    public void sendMessage() {
+        byte[] sendOut = new byte[200];
+        // sendOut[0] = 0x7349AB01;
+        int transfer = usbDeviceConnection.bulkTransfer(epOut, sendOut, sendOut.length, 10000);
+    }
+
+    private void showToastAndLog(String message) {
+        Log.d(TAG, message);
+        Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
+    }
+}

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

@@ -0,0 +1,34 @@
+<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:fillType="evenOdd"
+        android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
+        android:strokeWidth="1"
+        android:strokeColor="#00000000">
+        <aapt:attr name="android:fillColor">
+            <gradient
+                android:endX="78.5885"
+                android:endY="90.9159"
+                android:startX="48.7653"
+                android:startY="61.0927"
+                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="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
+        android:strokeWidth="1"
+        android:strokeColor="#00000000" />
+</vector>

+ 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="#008577"
+        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>

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

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="16dp"
+    tools:context=".MainActivity">
+
+    <Button
+        android:id="@+id/startService"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:onClick="onClick"
+        android:text="启动服务"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <Button
+        android:id="@+id/stopService"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:onClick="onClick"
+        android:text="停止服务"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/startService" />
+
+    <TextView
+        android:id="@+id/text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent" />
+</android.support.constraint.ConstraintLayout>

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

@@ -0,0 +1,5 @@
+<?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" />
+</adaptive-icon>

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

@@ -0,0 +1,5 @@
+<?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" />
+</adaptive-icon>

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


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


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


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


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


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


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


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


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


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


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

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#008577</color>
+    <color name="colorPrimaryDark">#00574B</color>
+    <color name="colorAccent">#D81B60</color>
+</resources>

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

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

+ 11 - 0
app/src/main/res/values/styles.xml

@@ -0,0 +1,11 @@
+<resources>
+
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="colorAccent">@color/colorAccent</item>
+    </style>
+
+</resources>

+ 6 - 0
app/src/main/res/xml/device_filter.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resource>
+    <usb-device
+        product-id="33113"
+        vendor-id="1061" />
+</resource>

+ 17 - 0
app/src/test/java/cn/minbb/hid/ExampleUnitTest.java

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

+ 31 - 0
build.gradle

@@ -0,0 +1,31 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+    repositories {
+        maven { url 'https://maven.aliyun.com/repository/jcenter' }
+        maven { url 'https://maven.aliyun.com/repository/google' }
+        google()
+        jcenter()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.6.2'
+
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+allprojects {
+    repositories {
+        maven { url 'https://maven.aliyun.com/repository/jcenter' }
+        maven { url 'https://maven.aliyun.com/repository/central' }
+        maven { url 'https://maven.aliyun.com/repository/google' }
+        maven { url "https://jitpack.io" }
+        google()
+        jcenter()
+    }
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}

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

@@ -0,0 +1,6 @@
+#Thu Apr 09 16:31:06 CST 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

+ 1 - 0
settings.gradle

@@ -0,0 +1 @@
+include ':app'