美文网首页安卓学习安卓开发博客
Android串口盒子+扫码枪开发

Android串口盒子+扫码枪开发

作者: Gorilla_L | 来源:发表于2018-11-26 11:50 被阅读213次

PS:本文为本人学习的一个过程,大神直接绕道,若发现有错误之处,请评论留言,小编会及时更正,不喜勿喷,谢谢。

今年年初接到一个项目,是通过串口来和计算机通信,然后实现相关业务功能,所以现在整理一下相关知识点,当作笔记,方便日后回顾。

1. 简介

因为这个项目,有幸接触到一个Android设备,我管它叫Android盒子吧,该项目需求是这样的:通过该Android盒子连接计算机和usb扫码枪,当Android盒子收到对应的指令的时候,做对应的事情,例如收到计算机发过来的扣款指令时就调起扫码枪扫描付款码,然后请求支付网关进行扣款。

说明:
  1. Android盒子是通过usb转串口线和计算机相连的,通信也是通过串口来进行通信;
  2. Android盒子通过usb接口外接了一个usb扫码枪,用于扫码。

1.1 设备介绍

1.1.1 Android盒子介绍

1. 硬件介绍:
该设备自带有3个USB接口,1个Micro USB接口(Android手机充电、数据接口,非TypeC接口),1个预留按钮,1个电源键,3个指示灯,1个电源接口,1个RJ45网口,1个HDMI接口,1个串口公头接口。

视图1:

Android盒子接口1.jpg
接口说明:
  1. 串口:Android盒子和计算机的通讯接口;
  2. HDMI接口:连接显示器的通讯接口;
  3. RJ45网线口:连接互联网的通讯接口(也可通过该接口与计算机通信);
  4. 电源接口:Android盒子的供电接口。

视图2:

Android盒子接口2.jpg
说明:
  1. 电源指示灯:显示Android盒子供电情况,亮红灯为正常情况;
  2. 预留指示灯:预留指示灯,尚未使用;
  3. 状态灯:Android盒子状态灯,每分钟亮一次,若Android盒子异常(网络异常),则Android盒子会发出“滴滴滴”响声;
  4. 电源键:Android盒子开机键,设备每次加电时需长按按钮2-3秒,直到电源指示灯亮起;
  5. 预留按钮:预留按钮,尚未使用;
  6. 数据接口:Android盒子与外界设备交互接口;
  7. USB接口:连接扫码枪等设备的接口。

2. 软件介绍
Android盒子是里面装了Android系统的一个终端设备,不过该系统具有root权限。

1.1.2 扫码枪介绍

扫码枪:

USB接口扫码枪.jpg
说明:
usb扫码枪无特殊要求或配置,作为一个外设设备,只要通过usb接口连接上Android盒子就可以工作。

1.2 项目介绍

该项目要求做一个App,用于计算机程序和支付网关间的桥梁,通过该App实现获取付款码并向支付网关发起扣费请求,以及根据计算机程序的不同指令处理不同的业务逻辑。
该篇文章主要整理该项目用到以下几个知识点:

扫码程序开机启动

  1. 因为这个Android盒子正常使用的时候是不接屏幕的,所以扫码程序也没有界面,操作人员也不能点击扫码程序图标让扫码程序启动,所以扫码程序就要设置成开机自动启动。

日志上传程序

  1. 考虑到如果这个设备如果要用到一些其他地方(例如其他城市的超时等,反正不在身边)的时候,程序出现bug,需要获取那个盒子的日志来分析问题,所以就开发了一个功能比较简单的日志上传程序,日志上传程序的主功能是把扫码程序的日志上传到服务器,还有另一个功能是监听扫码程序,当扫码程序因内存回收被杀了或者因为有bug而崩溃等等情况下重启启动扫码程序,又或者当扫码程序不在前台的时候把扫码程序调回前台运行。

扫码枪开发:

  1. 获取扫码枪扫到的内容并解析出来。

Android串口编程:

  1. Android串口编程介绍。
  2. 开源项目android-serialport-api的原理和实现。

2. 扫码程序开机启动

2.1 原理:

在Android系统启动时,会发出一个系统广播 ACTION_BOOT_COMPLETED,它的字符串常量表示为 “android.intent.action.BOOT_COMPLETED”。我们要做的就是写一个广播接收器BroadcastReceiver,广播接收器中写你启动的Activity或者Service等的代码,然后静态注册这个广播接收器,添加BOOT_COMPLETED这个action,当然,别忘记添加相应的权限。完成上述步骤之后,当系统开机发出系统广播 ACTION_BOOT_COMPLETED的时候,我们的程序就会“捕捉”到这个广播,然后执行我们广播中的代码,启动我们的Activity或者service等。

2.2 实现:

  1. 添加权限

     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    
  2. StartUpBroadcast

     public class StartUpBroadcast extends BroadcastReceiver {
         @Override
         public void onReceive(Context context, Intent intent) {
             Intent intent1 = new Intent(context, MainActivity.class);
             intent1.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
             context.startActivity(intent1);
           }
     }
    
  3. 静态注册广播接收器

     <receiver
         android:name=".StartUpBroadcast"
         android:enabled="true"
         android:exported="true">
         <intent-filter>
             <action android:name="android.intent.action.BOOT_COMPLETED" />
    
             <category android:name="android.intent.category.DEFAULT" />
         </intent-filter>
     </receiver>
    

3. 日志上传程序

3.1 功能

  1. 这个程序的主要业务逻辑都在一个服务(service)中,要做到时刻监听扫码程序,需要做到自己不被杀掉,所以这个服务是做了“杀不死”的处理。
  2. 上传扫码程序的本地日志到服务器。
  3. 监听扫码程序,当扫码程序不在前台运行/扫码程序被杀掉的时候把扫码程序调回前台运行/重新启动扫码程序。至于为什么扫码程序一定要在前台运行,后面的扫码枪开发章节再解释。

3.2 原理

3.2.1 保证service不被杀掉

在onStartCommand方法,返回START_STICKY。手动返回START_STICKY,当service因内存不足被杀掉,当内存又有的时候,service又会被重新创建,但是不能保证任何情况下都被重建,比如进程被干掉了的情况,具体原理请看下面的介绍。

StartCommond几个常量参数简介:

  1. START_STICKY
    在运行onStartCommand后service进程被kill后,那将保留在开始状态,但是不保留那些传入的intent。不久后service就会再次尝试重新创建,因为保留在开始状态,在创建 service后将保证调用onstartCommand。如果没有传递任何开始命令给service,那将获取到null的intent。
  2. START_NOT_STICKY
    在运行onStartCommand后service进程被kill后,并且没有新的intent传递给它。Service将移出开始状态,并且直到新的明显的方法(startService)调用才重新创建。因为如果没有传递任何未决定的intent那么service是不会启动,也就是期间onstartCommand不会接收到任何null的intent。
  3. START_REDELIVER_INTENT
    在运行onStartCommand后service进程被kill后,系统将会再次启动service,并传入最后一个intent给onstartCommand。直到调用stopSelf(int)才停止传递intent。如果在被kill后还有未处理好的intent,那被kill后服务还是会自动启动。因此onstartCommand不会接收到任何null的intent。

ps: 保证service不被杀掉的方法有很多,但是在现在各种的定制系统以及杀毒软件手机管家中,不可能保证service不被杀死。因为我的这个项目比较特殊,没有界面,用户操作不了,所以不存在用户主动杀死进程或服务的情况,也不会有系统被定制或安装杀毒软件的情况,Android盒子系统里面也不会安装其他太多的程序,所以也很少会出现内存不足的情况,所以我这里只用到这一个方法就够用了,就是在onStartCommand方法,返回START_STICKY。如果读者想要service不被杀死的更好的方案,可以自行百度,这里也推荐一篇文章:http://blog.csdn.net/mad1989/article/details/22492519

3.2.2 上传扫码程序的本地日志到服务器的功能原理

其实这个也没什么好说的,就是后台提供一个上传文件的接口,日志上传程序这边去拿特定文件夹下的日志文件,然后通过后台接口上传到服务器,我这里用的网络框架是OkHttp。

3.2.3 监听扫码程序原理

在日志上传程序中的onStartCommand方法中开一个定时任务,每隔60秒则检测一下扫码程序是否在栈顶,如果不在栈顶,则发送一个广播,在扫码程序中,当接收到这个广播之后,获得当前运行的task,找到当前应用的task,并启动task的栈顶activity,达到程序切换到前台,若没有找到运行的task,用户结束了task或被系统释放,则重新启动mainactivity

3.3 实现

3.3.1 保证service不被杀掉

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    Log.i("onStartCommand","......");

    PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
    Notification noti = new Notification.Builder(this)
            .setContentTitle("Title")
            .setContentText("Message")
            .setContentIntent(pendingIntent)
            .build();
    startForeground(12346, noti);

    // 这里执行自己的业务逻辑
    if (intent != null) {
        final String action = intent.getAction();
        if (ACTION_BAZ.equals(action)) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    handleActionBaz();
                }
            });
            thread.start();
        }
    }

    //延迟0秒后,每隔60秒执行一次该任务
    singleThreadScheduledPool.scheduleWithFixedDelay(new Runnable() {
        @Override
        public void run() {
            String topPackageName = getForegroundApp(getBaseContext());
            Log.d("packageName", "topPackageName:"+topPackageName);
            // com.gorilla.scanningcodedemo为扫码程序包名
            if (!"com.gorilla.scanningcodedemo".equals(topPackageName)) {
                Intent intent = new Intent("com.gorilla.scanningcodedemo.RestartAppBroadcast");
                sendBroadcast(intent);
            }
        }
    }, 60, 60, TimeUnit.SECONDS);

    return START_STICKY;// 重点在这
}

3.3.2 上传扫码程序的本地日志到服务器

略。可自行百度OkHttp文件上传功能。

3.3.3 监听扫码程序

日志上传程序中的代码

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    Log.i("onStartCommand","......");

    PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
    Notification noti = new Notification.Builder(this)
            .setContentTitle("Title")
            .setContentText("Message")
            .setContentIntent(pendingIntent)
            .build();
    startForeground(12346, noti);

    // 这里执行自己的业务逻辑
    if (intent != null) {
        final String action = intent.getAction();
        if (ACTION_BAZ.equals(action)) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    handleActionBaz();
                }
            });
            thread.start();
        }
    }

    //延迟0秒后,每隔60秒执行一次该任务
    singleThreadScheduledPool.scheduleWithFixedDelay(new Runnable() {
        @Override
        public void run() {
            String topPackageName = getForegroundApp(getBaseContext());
            Log.d("packageName", "topPackageName:"+topPackageName);
             // com.gorilla.scanningcodedemo为扫码程序包名
            if (!"com.gorilla.scanningcodedemo".equals(topPackageName)) {
                Intent intent = new Intent("com.gorilla.scanningcodedemo.RestartAppBroadcast");
                sendBroadcast(intent);
            }
        }
    }, 60, 60, TimeUnit.SECONDS);

    return START_STICKY;// 重点在这
}

public String getForegroundApp(Context context) {
    ActivityManager am =
            (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    List<ActivityManager.RunningAppProcessInfo> lr = am.getRunningAppProcesses();
    if (lr == null) {
        return null;
    }

    for (ActivityManager.RunningAppProcessInfo ra : lr) {
        if (ra.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE
                || ra.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
            return ra.processName;
        }
    }

    return null;
}

扫码程序的代码

  1. 添加权限

     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    
  2. RestartAppBroadcast

     public class RestartAppBroadcast extends BroadcastReceiver {
       @Override
       public void onReceive(Context context, Intent intent) {
           if ("com.gorilla.scanningcodedemo.RestartAppBroadcast".equals(intent.getAction())) {
               //获取ActivityManager
               ActivityManager mAm = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
               //获得当前运行的task
               List<ActivityManager.RunningTaskInfo> taskList = mAm.getRunningTasks(100);
               for (ActivityManager.RunningTaskInfo rti : taskList) {
                   //找到当前应用的task,并启动task的栈顶activity,达到程序切换到前台
                   if(rti.topActivity.getPackageName().equals(context.getPackageName())) {
                       try {
                           Intent  resultIntent = new Intent(context, Class.forName(rti.topActivity.getClassName()));
                           resultIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
                           context.startActivity(resultIntent);
                           Logger.i("后台->前台");
                           LogUtil.startActionFoo("后台->前台");
                           return;
                       }catch (ClassNotFoundException e) {
                           e.printStackTrace();
                       }
    
                   }
               }
               //若没有找到运行的task,用户结束了task或被系统释放,则重新启动mainactivity
               Intent resultIntent = new Intent(context, MainActivity.class);
               resultIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
               context.startActivity(resultIntent);
               Logger.i("重新启动");
               getConfig();
               LogUtil.startActionFoo("终端app重新启动!");
           }
        }
     }
    
  3. 静态注册广播接收器

     <receiver
         android:name=".RestartAppBroadcast"
         android:enabled="true"
         android:exported="true">
         <intent-filter>
             <action android:name="com.gorilla.scanningcodedemo.RestartAppBroadcast" />
         </intent-filter>
     </receiver>
    

4. 扫码枪开发

一般的扫码枪设备都相当于一个普通的外接输入设备,像外接键盘一样。扫码枪扫到的内容就相当于用户在键盘输入内容一样,所以要获取扫码枪扫到的内容,就要对键盘事件做出解析。要解析键盘输入的内容,那就要重写Activity的dispatchKeyEvent(KeyEvent event),所以前面说的Activity必须要在前台运行就是这个原因,因为如果Activity不在前台运行,那么dispatchKeyEvent(KeyEvent event)方法就执行不了,就获取不了扫码内容
扫码枪开发中的一些常见问题

  1. 怎么判断扫码枪是否已经连接?
    每个品牌型号的扫码枪都有一个名字,你要判断扫码枪是否已经连接,就获取到Android盒子连接的所有外接设备,遍历,通过名字来匹配,如果匹配到这个扫码枪的名字,则认为是连接上了。
  2. 怎么区分扫描到的结果是一次扫描到的还是多次扫描的结果?
    一般扫码枪扫描一次的内容都会以回车键作为结束符,所以可以判断是否有回车键来区分是否是一次扫描的结果。(可能不同扫码枪有不同的结束标识,我在其他博文中看到有些串口扫码枪,它们有特定的数据结束标识,但是我用过三个品牌的usb扫码枪,都是以回车键作为结束标识)
  3. 当应用到各个城市的Android盒子的扫码枪坏了,要更换扫码枪,又找不到相同牌子的扫码枪怎么办?
    前面说了,我们是通过扫码枪的名字来判断扫码枪是否已经连接的,所以我们可以将这个扫码枪通过配置文件的方式来写到配置文件中,当要更换不同品牌的扫码枪的时候,我们就可以在配置文件中把扫码枪名字配置上去,然后在扫码程序中读取配置文件来获取扫码枪名字,然后和Android盒子连接上的外接设备名字来对比。

4.1 原理

4.1.1 重写Activity的dispatchKeyEvent(KeyEvent event)方法

首先我们来了解一下当按键按下和弹起,activity中回调的一系列方法:
当键盘按下时

  1. 首先触发dispatchKeyEvent
  2. 然后触发onUserInteraction
  3. 再次onKeyDown

如果按下紧接着松开,则是俩步

  1. 紧跟着触发dispatchKeyEvent
  2. 然后触发onUserInteraction
  3. 再次onKeyUp

总结:如果按下接着松开的方法调用顺序是:dispatchKeyEvent-->onUserInteraction-->onKeyDown-->dispatchKeyEvent-->onUserInteraction-->onKeyUp

问题1:为什么不重写onKeyDown或onKeyUp来实现呢?
答:因为onKeyDown和onKeyUp貌似只有activity中可以触发,activityGroup,listActivity,tabActivity好像不好用,所以在dispatchKeyEvent监控按键不管是activity还是activitygroup都会触发

问题2:要在dispatchKeyEvent方法里做什么事?
答:首先,为了避免Android盒子同时接上了usb扫码枪和键盘,导致出现扫码错误的情况,我们要判断一下监听到的事件是否是扫码枪事件,如果不是扫码枪事件的话,就交给super.dispatchKeyEvent(event)来处理,如果是扫码枪事件就解析出扫到的内容

4.1.2 判断是否是扫码枪事件

判断方法:通过dispatchKeyEvent(KeyEvent event)的KeyEvent对象获取到设备的名字,然后和自己的扫码枪设备的名字做比较,是扫码枪事件,则解析扫码内容。

4.1.3 解析扫码枪扫码内容。

解析原理:通过KeyEvent的KeyCode来解析出来,然后通过一个StringBuffer来转换成一串字符串,然后通过判断KeyCode是否为回车键来判断是否扫码结束,扫码结束则把扫到的字符串保存起来使用。

4.2 实现

4.2.1 监听Activity的dispatchKeyEvent(KeyEvent event)方法代码实现

@Override
public boolean dispatchKeyEvent(KeyEvent event) {
    // mScanGunKeyEventHelper是用于对按键事件做处理的对象,isScanGunEvent(event)是判断是否是扫码枪事件
    if (mScanGunKeyEventHelper.isScanGunEvent(event)) {
        // analysisKeyEvent(event)解析扫码枪扫到的内容
        mScanGunKeyEventHelper.analysisKeyEvent(event);
        return true;// 最后消费掉,不要继续传递事件下去
    }
    return super.dispatchKeyEvent(event);
}

4.2.2 判断是否是扫码枪事件代码实现

/**
 * 是否为扫码枪事件
 *
 * @param event KeyEvent
 * @return
 */
public boolean isScanGunEvent(KeyEvent event) {
    boolean isDeviceExist = false; // 是否是扫码枪设备
    for (String deviceName : mDeviceName) {// mDeviceName是一个字符串数组,存的自己的扫码枪设备名字
        // event.getDevice().getName()获取当前输入设备的设备名字
        if (event.getDevice().getName().equals(deviceName)) {
            isDeviceExist = true;
        }
    }
    if (isDeviceExist) {
        return true;
    }
    return false;
}

4.1.3 解析扫码枪扫码内容代码实现

/**
 * 扫码枪事件解析
 *
 * @param event KeyEvent
 */
public void analysisKeyEvent(KeyEvent event) {

    int keyCode = event.getKeyCode();

    if (event.getAction() == KeyEvent.ACTION_DOWN) {

        char aChar = getInputCode(event);

        if (aChar != 0) {
            mStringBufferResult.append(aChar);
        }

        if (keyCode == KeyEvent.KEYCODE_ENTER) {
            //若为回车键,直接返回
            mHandler.removeCallbacks(mScanningFishedRunnable);
            mHandler.post(mScanningFishedRunnable);
        } else {
            // 延迟post,若500ms内,有其他事件
            mHandler.removeCallbacks(mScanningFishedRunnable);
            mHandler.postDelayed(mScanningFishedRunnable, MESSAGE_DELAY);
        }

    }
}
//获取扫描内容
private char getInputCode(KeyEvent event) {
    int keyCode = event.getKeyCode();
    char aChar = 0;
    // 这里根据自己业务需求决定是否需要英文字母什么的,我这里只需要数字
    if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
        //数字
        aChar = (char) ('0' + keyCode - KeyEvent.KEYCODE_0);
    }
    return aChar;
}

4.1.4 最后附上扫码枪帮助类代码实现

public class ScanGunKeyEventHelper {
private final static long MESSAGE_DELAY = 500;             //延迟500ms,判断扫码是否完成。
private final Handler mHandler;
private final Runnable mScanningFishedRunnable;
private StringBuffer mStringBufferResult;                  //扫码内容
private OnScanSuccessListener mOnScanSuccessListener;
private String[] mDeviceName = null;

public ScanGunKeyEventHelper(OnScanSuccessListener onScanSuccessListener) {
    mOnScanSuccessListener = onScanSuccessListener;
    mStringBufferResult = new StringBuffer();
    mHandler = new Handler();
    // 通过配置文件获取扫码枪名字
    String deviceNames = GetConfig.getScanDeviceName();
    // 若没配置扫码枪名字,则使用默认的
    if (deviceNames == null || deviceNames.length() <= 0) {
        mDeviceName = new String[]{"deviceName1", "deviceName2"};
    } else {
        mDeviceName = deviceNames.split(",");
    }
    mScanningFishedRunnable = new Runnable() {
        @Override
        public void run() {
            performScanSuccess();
        }
    };
}

/**
 * 返回扫码成功后的结果
 */
private void performScanSuccess() {
    String barcode = mStringBufferResult.toString();
    if (mOnScanSuccessListener != null)
        mOnScanSuccessListener.onScanSuccess(barcode);
    mStringBufferResult.setLength(0);
}

/**
 * 扫码枪事件解析
 *
 * @param event KeyEvent
 */
public void analysisKeyEvent(KeyEvent event) {

    int keyCode = event.getKeyCode();

    if (event.getAction() == KeyEvent.ACTION_DOWN) {

        char aChar = getInputCode(event);

        if (aChar != 0) {
            mStringBufferResult.append(aChar);
        }

        if (keyCode == KeyEvent.KEYCODE_ENTER) {
            //若为回车键,直接返回
            mHandler.removeCallbacks(mScanningFishedRunnable);
            mHandler.post(mScanningFishedRunnable);
        } else {
            //延迟post,若500ms内,有其他事件
            mHandler.removeCallbacks(mScanningFishedRunnable);
            mHandler.postDelayed(mScanningFishedRunnable, MESSAGE_DELAY);
        }

    }
}

//获取扫描内容
private char getInputCode(KeyEvent event) {

    int keyCode = event.getKeyCode();

    char aChar = 0;
    if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
        //数字
        aChar = (char) ('0' + keyCode - KeyEvent.KEYCODE_0);
    }
    return aChar;

}

public void onDestroy() {
    mHandler.removeCallbacks(mScanningFishedRunnable);
    mOnScanSuccessListener = null;
}

/**
 * 输入设备是否存在
 *
 * @return 设备是否存在
 */
public boolean isInputDeviceExist() {
    int[] deviceIds = InputDevice.getDeviceIds();

    for (int id : deviceIds) {
        boolean isDeviceExist = false;
        for (String deviceName : mDeviceName) {
            if (InputDevice.getDevice(id).getName().equals(deviceName)) {
                isDeviceExist = true;
            }
        }
        if (isDeviceExist) {
            return true;
        }
    }
    return false;
}

/**
 * 是否为扫码枪事件
 *
 * @param event KeyEvent
 * @return
 */
public boolean isScanGunEvent(KeyEvent event) {
    boolean isDeviceExist = false;
    for (String deviceName : mDeviceName) {
        if (event.getDevice().getName().equals(deviceName)) {
            isDeviceExist = true;
        }
    }
    if (isDeviceExist) {
        return true;
    }
    return false;
}

// 扫码成功后回调
public interface OnScanSuccessListener {
    void onScanSuccess(String barcode);
}
}


未完待续...



相关文章

网友评论

    本文标题:Android串口盒子+扫码枪开发

    本文链接:https://www.haomeiwen.com/subject/hstovxtx.html