Android外接扫码器

最近做了一个关于Android设备Usb外接扫码器的项目,在此记录下。
言归正传,首先扫码器有这两种模式:USB HID-KBW,USB 虚拟串口。

  • USB HID-KBW:扫码器会将扫描出来的内容模拟成键盘事件,就是Android中就是KeyEvent里面对应的常量(比如:KeyEvent.KEYCODE_A 表示小写字母a,如果要表示大写字母A,就会组合KeyEvent.KEYCODE_SHIFT_LEFT或者KeyEvent.KEYCODE_SHIFT_RIGHT + KeyEvent.KEYCODE_A)。
    这种模式优点是使用简单,只要有焦点的文本输入框,系统会自动将扫码出的内容填充到文本框内,缺点是不支持中文!

  • USB 虚拟串口:这种模式相对来说复杂点,但也比较简单,Android系统屏蔽了底层串口通信(需要了解Android串口通信的可以看下android-serialport-api ),直接使用UsbDevice进行通信,连接到UsbDevice,读取数据,好处是支持中文扫码。

上代码:
USB HID-KBW,这种模式我们可以自定义EditText对内容进行拦截,过滤出我们想要的内容或者触发我们的业务逻辑。自定义EditText很简单,初始化的时候设置调用setOnKeyListener方法,设置OnKeyListener,通过onKey方法返回true来拦截键盘事件,至于拦截到的键盘事件通过KeyEvent的对应常量转换成相应的字符(注意这里需要让EditText获取到焦点,不然KeyEvent不会触发)。

public class CustomEditText extends android.support.v7.widget.AppCompatEditText {

    private ScanCodeListener codeListener;
    private StringBuffer buffer = new StringBuffer();

    public CustomEditText(Context context) {
        super(context);
        init();
    }

    public CustomEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CustomEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }


    private int preKeyCode = -1;

    private void init() {
        setOnKeyListener(new OnKeyListener() {
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                if (event.getAction() == KeyEvent.ACTION_UP) {
                    Log.d("ooo", "Code:" + keyCode + ", display int:" + (int) event.getDisplayLabel() + ", char:" + event.getDisplayLabel());

                    if (keyCode == KeyEvent.KEYCODE_ENTER) {
                        String str = buffer.toString();
                        buffer.setLength(0);
                        preKeyCode = -1;
                        if (codeListener != null) {
                            codeListener.handleCode(str);
                        }
                    } else if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
                        preKeyCode = KeyEvent.KEYCODE_SHIFT_RIGHT;
                    } else {
                        if (preKeyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
                            String shiftChar = SHIFT_KEY_MAP.get(keyCode);
                            if (shiftChar != null) {
                                buffer.append(shiftChar);
                            }
                        } else {
                            char label = event.getDisplayLabel();
                            if (label != 0) {
                                buffer.append(toLowCase(label));
                            }
                        }
                        preKeyCode = -1;
                    }
                }
                return true;
            }
        });
    }

    private char toLowCase(char c) {
        if (c >= 'A' && c <= 'Z') {
            c += 32;
            return c;
        }
        return c;
    }

    public void setScanCodeListener(ScanCodeListener codeListener) {
        this.codeListener = codeListener;
    }

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        return super.dispatchKeyEvent(event);
    }

    @Override
    public boolean dispatchKeyEventPreIme(KeyEvent event) {
        boolean value = super.dispatchKeyEventPreIme(event);
        return value;
    }

    @Override
    public void setText(CharSequence text, BufferType type) {
        super.setText(text, type);
    }

    @Override
    public void setOnKeyListener(OnKeyListener l) {
        super.setOnKeyListener(l);
    }

    public interface ScanCodeListener {
        void handleCode(String code);
    }

    static SparseArray<String> SHIFT_KEY_MAP = new SparseArray<>();

    static {

        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_GRAVE, "~");

        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_0, ")");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_1, "!");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_2, "@");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_3, "#");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_4, "$");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_5, "%");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_6, "^");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_7, "&");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_8, "*");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_9, ")");

        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_MINUS, "_");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_EQUALS, "+");

        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_LEFT_BRACKET, "{");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_RIGHT_BRACKET, "}");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_BACKSLASH, "|");

        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_SEMICOLON, ":");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_APOSTROPHE, "\"");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_COMMA, "<");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_PERIOD, ">");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_SLASH, "?");

        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_A, "A");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_B, "B");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_C, "C");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_D, "D");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_E, "E");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_F, "F");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_G, "G");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_H, "H");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_I, "I");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_J, "J");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_K, "K");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_L, "L");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_M, "M");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_N, "N");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_O, "O");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_P, "P");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_Q, "Q");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_R, "R");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_S, "S");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_T, "T");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_U, "U");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_V, "V");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_W, "W");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_X, "X");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_Y, "Y");
        SHIFT_KEY_MAP.put(KeyEvent.KEYCODE_Z, "Z");
    }
}

USB 虚拟串口,这种模式稍稍复杂点,要自己去连接USB设备,读取数据,其实也很简单

  1. 找到设备
  2. 设备授权
  3. 连接设备
  4. 读取数据
public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    public static final String ACTION_DEVICE_PERMISSION = "com.demo.USB_PERMISSION";

    private final int MY_PRODUCT_ID = 33286;//产品ID,根据你的设备修改
    private final int MY_VENDOR_ID = 7851;  //厂商ID,根据你的设备修改
    
    private UsbManager usbManager;
    private TextView textView;
    
    private UsbEndpoint inUsbEndpoint;
    private UsbEndpoint outUsbEndpoint;
    private UsbInterface mUsbInterface;
    private UsbDeviceConnection mUsbDeviceConnection;

    private Thread thread;
    private Buff buff = new Buff(); //负责分割扫码数据

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
        findViewById(R.id.list_usb_device).setOnClickListener(this);
        findViewById(R.id.connect).setOnClickListener(this);
        findViewById(R.id.read).setOnClickListener(this);
        findViewById(R.id.send).setOnClickListener(this);
        findViewById(R.id.clear).setOnClickListener(this);
        textView = (TextView) findViewById(R.id.device_info);

    }

    protected void onResume() {
        super.onResume();
        IntentFilter usbFilter = new IntentFilter();
        usbFilter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
        usbFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
        usbFilter.addAction(ACTION_DEVICE_PERMISSION);
        registerReceiver(mUsbReceiver, usbFilter);
    }
    
    @Override
    protected void onPause() {
        super.onPause();
        unregisterReceiver(mUsbReceiver);
    }

    private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();

            if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
                Log.d("xxx", "ACTION_USB_DEVICE_ATTACHED");
            } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
                Log.d("xxx", "ACTION_USB_DEVICE_DETACHED");
            } else if (ACTION_DEVICE_PERMISSION.equals(action)) {
                UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
                if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                    Toast.makeText(MainActivity.this, "已授权回调", Toast.LENGTH_SHORT).show();
                    if (device != null) {
                        initCommunication(device);
                    }
                } else {
                    Toast.makeText(MainActivity.this, "未授权回调", Toast.LENGTH_SHORT).show();
                }
            }
        }
    };

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.list_usb_device) {
            HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList();
            if (deviceList != null) {
                Set<String> keySet = deviceList.keySet();
                StringBuffer stringBuffer = new StringBuffer();
                for (String key : keySet) {
                    stringBuffer.append(deviceList.get(key).toString() + "\n");
                    stringBuffer.append("\n\n");
                }
                textView.setText(stringBuffer.toString());
            } else {
                textView.setText("没有设备");
            }
        } else if (v.getId() == R.id.connect) {
            HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList();
            if (deviceList.size() > 0) {
                for (UsbDevice device : deviceList.values()) {
                    if (MY_VENDOR_ID == device.getVendorId() && MY_PRODUCT_ID == device.getProductId()) {
                        if (usbManager.hasPermission(device)) {
                            initCommunication(device);
                            continue;
                        } else {
                            requestUsbPermission(device);
                        }
                        break;
                    }
                }
            }
        } else if (v.getId() == R.id.read) {
            startRead();
        } else if (v.getId() == R.id.send) {
            sendData();
        } else if (v.getId() == R.id.clear) {
            textView.setText("");
        }
    }

    //申请授权
    private void requestUsbPermission(UsbDevice device) {
        PendingIntent mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_DEVICE_PERMISSION), 0);
        usbManager.requestPermission(device, mPermissionIntent);
    }
    
    //开启线程读取数据
    private void startRead() {
        if (mUsbDeviceConnection == null && !mUsbDeviceConnection.claimInterface(mUsbInterface, true))
            return;
        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                
                while (true) {
                    byte[] bytes = new byte[inUsbEndpoint.getMaxPacketSize()];
                    //读取数据, 这里注意最后一个参数为0时 为阻塞读取,知道读到数据为止
                    int ret = mUsbDeviceConnection.bulkTransfer(inUsbEndpoint, bytes, bytes.length, 0);
                    if (ret > 0) {
                        //这里因为扫码器的maxPacketSize很小(我的设备只有64byte),所以扫码到的结果可能分多次读取,
                        //所以这里将读取到的数据丢到Buff里面,Buff负责分割成单条数据
                        buff.append(Arrays.copyOf(bytes, ret));
                        //分割获取单条扫码数据
                        byte[] code = buff.getCode();
                        if (code != null) {
                            final String str = new String(code);
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    textView.setText(str);
                                }
                            });
                        }
                    } else {
                        Log.d("xxx", "Read empty data");
                    }
                }
            }
        });
        thread.start();
    }

    //初始化设备,找到对应的通信接口
    private void initCommunication(UsbDevice device) {
        int interfaceCount = device.getInterfaceCount();
        for (int interfaceIndex = 0; interfaceIndex < interfaceCount; interfaceIndex++) {
            UsbInterface usbInterface = device.getInterface(interfaceIndex);
            if ((UsbConstants.USB_CLASS_CDC_DATA != usbInterface.getInterfaceClass())
                    && (UsbConstants.USB_CLASS_COMM != usbInterface.getInterfaceClass())
                    && UsbConstants.USB_CLASS_HID != usbInterface.getInterfaceClass()) {
                continue;
            }
            for (int i = 0; i < usbInterface.getEndpointCount(); i++) {

                UsbEndpoint ep = usbInterface.getEndpoint(i);
                if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) {
                    if (ep.getDirection() == UsbConstants.USB_DIR_IN) {
                        inUsbEndpoint = ep;
                    } else {
                        outUsbEndpoint = ep;
                    }
                }

            }

            if (null == inUsbEndpoint) {
                inUsbEndpoint = null;
                outUsbEndpoint = null;
                mUsbInterface = null;
            } else {
                //连接成功
                mUsbInterface = usbInterface;
                mUsbDeviceConnection = usbManager.openDevice(device);
                initConnection();
                break;
            }
        }
        if (mUsbDeviceConnection == null) {
            Toast.makeText(this, "连接失败", Toast.LENGTH_SHORT).show();
        }
    }

    //初始化,设置比特率
    private void initConnection() {
        if (!mUsbDeviceConnection.claimInterface(mUsbInterface, true)) {
            return;
        }
        
        int ret = mUsbDeviceConnection.controlTransfer(0x21, 0x22, 0x00, 0, null, 0, 0);
        setCdcBaudrate(9600);
    }

    private void setCdcBaudrate(int baudrate) {
        byte[] baudByte = new byte[4];

        baudByte[0] = (byte) (baudrate & 0x000000FF);
        baudByte[1] = (byte) ((baudrate & 0x0000FF00) >> 8);
        baudByte[2] = (byte) ((baudrate & 0x00FF0000) >> 16);
        baudByte[3] = (byte) ((baudrate & 0xFF000000) >> 24);
        int ret = mUsbDeviceConnection.controlTransfer(0x21, 0x20, 0, 0, new byte[]{
                baudByte[0], baudByte[1], baudByte[2], baudByte[3], 0x00, 0x00,
                0x08
        }, 7, 0);
    }

    //发送数据
    private void sendData() {
        byte[] buffer = "test data".getBytes();
        int ret = mUsbDeviceConnection.controlTransfer(UsbConstants.USB_DIR_OUT, 1, 1, 1, buffer, buffer.length, 1000);
        Toast.makeText(this, "send " + ret + " bytes data", Toast.LENGTH_SHORT).show();
    }
}

Buff 用于 分割扫码数据,我用的设备是以回车符(0X )+换行符(0X0A)为扫码结束分隔符

public class Buff {
    private final int DELIMITER_1 = 0xd; //回车符
    private final int DELIMITER_2 = 0xa; //换行符
    private byte[] buffer;

    public void append(byte[] data) {
        if (buffer == null) {
            buffer = new byte[data.length];
            System.arraycopy(data, 0, buffer, 0, data.length);
        } else {
            byte[] temp = new byte[buffer.length + data.length];
            System.arraycopy(buffer, 0, temp, 0, buffer.length);
            System.arraycopy(data, 0, temp, buffer.length, data.length);
            buffer = temp;
        }
    }

    public byte[] getCode() {
        if (buffer == null)
            return null;
        if (buffer.length > 2) {
            boolean found = false;
            int i = 0;
            for (; i < buffer.length - 1; i++) {
                if (buffer[i] == DELIMITER_1 && buffer[i + 1] == DELIMITER_2) {
                    found = true;
                    break;
                }
            }
            if (found) {
                byte[] result = Arrays.copyOf(buffer, i);
                if (i < buffer.length - 2) {
                    buffer = Arrays.copyOfRange(buffer, i + 2, buffer.length);
                } else {
                    buffer = null;
                }
                return result;
            }
        }
        return null;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,566评论 25 707
  • afinalAfinal是一个android的ioc,orm框架 https://github.com/yangf...
    passiontim阅读 15,085评论 2 44
  • 先说一下这篇文章里面的内容:TCP 客户端, 自定义对话框, 自定义按钮, ProgressBar竖直显示, 重力...
    杨奉武阅读 3,158评论 0 3
  • 这是一篇很现实的小说,女人和兄弟,我相信有很多人会选择兄弟,但是最后的做法却让人担心。 让我一起看一看凌晨的故事,...
    小破丐阅读 565评论 0 1
  • 2017年1月7日,雨一直下,气氛还算融洽。 穿花蝴蝶般绕过迷宫似的、比世博入口还曲折的博览中心入口,进入了N2展...
    昊昊爸阅读 348评论 0 1