专栏名称: 郭霖
Android技术分享平台,每天都有优质技术文章推送。你还可以向公众号投稿,将自己总结的技术心得分享给大家。
目录
相关文章推荐
郭霖  ·  iPhone 到 Android ... ·  2 天前  
郭霖  ·  Android外接设备开发使用一网打尽 ·  昨天  
郭霖  ·  Android Surface截图方法总结 ·  4 天前  
鸿洋  ·  Android系统native进程之我是in ... ·  3 天前  
郭霖  ·  Android ... ·  1 周前  
51好读  ›  专栏  ›  郭霖

Android外接设备开发使用一网打尽

郭霖  · 公众号  · android  · 2024-11-22 08:00

正文



/   今日科技快讯   /


近日,美国苹果公司正对印尼政府大力公关,以期取消印尼对苹果iPhone 16手机的销售禁令。知情人士称,苹果已向印尼方面提出,将在今后两年内向印尼投资1亿美元用于提升本地制造。此前苹果曾计划向印尼万隆一座工厂投资1000万美元,最新计划投向印尼制造领域的资金相当于原计划的10倍之多。苹果提交上述投资计划后,印尼工业部并未作出最终决定,不过提出苹果在印尼的投资计划应聚焦智能手机研发。


/   作者简介   /


本篇文章转自Wgllss的博客,文章主要分享了如何对Android外接设备进行开发,相信会对大家有所帮助!


原文地址:

https://juejin.cn/post/7439231301869305910


/   前言   /


在Android智能设备开发过程中,难免会遇到串口,USB,扫码枪,支付盒子,打印机,键盘,鼠标等接入场景,其实这些很简单,只是大多数情况下,大家都在做手机端的App开发,接触这方面的很少。本文重点介绍下这些在Android系统下是怎么接入使用的。


/   正文   /


串口接入使用


1. 可以到官网下载串口包 里面含有 libprt_serial_port.so 这个库,下载下来按照so使用方式接入就行了,还有 SerialPort 类:如下:


public class SerialPort {

   private static final String TAG = "SerialPort";

   /*
    * Do not remove or rename the field mFd: it is used by native method close();
    */

   private FileDescriptor mFd;
   private FileInputStream mFileInputStream;
   private FileOutputStream mFileOutputStream;

   public SerialPort(File device, int baudrate, int flags) throws SecurityException, IOException {

      mFd = open(device.getAbsolutePath(), baudrate, flags);
      if (mFd == null) {
         Log.e(TAG, "native open returns null");
         throw new IOException();
      }
      mFileInputStream = new FileInputStream(mFd);
      mFileOutputStream = new FileOutputStream(mFd);
   }

   // Getters and setters
   public InputStream getInputStream() {
      return mFileInputStream;
   }

   public OutputStream getOutputStream() {
      return mFileOutputStream;
   }

   // JNI
   private native static FileDescriptor open(String path, int baudrate, int flags);
   public native void close();
   static {
      System.loadLibrary("serial_port");
   }
}

2. 使用串口读取或者写入数据


需要配置串口路径和波特率,如下:路径为:/dev/ttyS4, 波特率为9600


val serialPort = SerialPort(File("/dev/ttyS4"), 96000)

读写数据需要从串口里面拿到 输入输出流:


inputStream = serialPort.inputStream
outputStream = serialPort.outputStream

比如读取数据:


val length = inputStream!!.available()
val bytes = new byte[length];
inputStream.read(bytes);

到此,串口的使用基本就完成了。


至于串口读取后的数据怎么解析?


需要看串口数据的文档,不同硬件设备读取的不同内容出来格式不一样,按照厂商给的格式文档解析就完了,比如,串口连接的是秤,秤厂商硬件那边约定好的数据格式是怎样的,数据第1位什么意思,第2到第X位什么意思,xxx位什么意思,这不同的厂商不同的,如果串口连接的不是秤,是其他硬件,约定的格式可能又不一样。


同理:串口写数据,使用 outputStream流写入就行了, 写的具体内容,具体硬件厂商会有写入的文档,写入哪个数据是干什么用的,都在文档里面有。不同的写入功能,对应不同的写入内容命令。


USB接入使用


1、在AndroidManifest中添加USB使用配置


<uses-permission android:name="android.permission.USB_PERMISSION" />
<uses-feature android:name="android.hardware.usb.host" />

<meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
<meta-data android:name="android.hardware.usb.action.USB_DEVICE_DETACHED" />

2、防止USB插入拔出导致Activity生命周期发生变化需要在Activity 下添加配置


android:configChanges="orientation|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"

3、代码中具体使用:


比如接入USB打印机:


//拿到USB管理器
mUsbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
mPermissionIntent = PendingIntent.getBroadcast(context, 0, Intent(ACTION_USB_PERMISSION), 0)
val filter = IntentFilter(USBPrinter.ACTION_USB_PERMISSION)
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
//注册监听USB插入拔出监听广播
context.registerReceiver(mUsbDeviceReceiver, filter)
//开始检索出已经连接的USB设备
setUsbDevices()

找到打印机设备,打印机设备的接口类型值固定式为7(usbInterface.interfaceClass)


/**
 * 检索usb打印设备
 */

private fun setUsbDevices() {
    // 列出所有的USB设备,并且都请求获取USB权限
    mUsbManager?.deviceList?.let {
        for (device in it.values) {
            val usbInterface = device.getInterface(0)
            if (usbInterface.interfaceClass == 7) {
                //检查该USB设备是否有权限
                if (!mUsbManager!!.hasPermission(device)) {
                    //申请该打印机USB权限
                    mUsbManager!!.requestPermission(device, mPermissionIntent)
                } else {
                    connectUsbPrinter(device)
                }
                break
            }
        }
    }
}

USB权限广播action收到后,就可以连接打印了


private val mUsbDeviceReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val action = intent.action
        if (ACTION_USB_PERMISSION == action) {
            synchronized(this) {
                val usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
                if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                    //UsbDevice:在Android开发中用于表示连接到Android设备的USB设备  
                    mUsbDevice = usbDevice
                    if (mUsbDevice != null) {
                        connectUsbPrinter(mUsbDevice)
                    }
                } else {
                    WLog.e(this"Permission denied for device $usbDevice")
                }
            }
        } else if (UsbManager.ACTION_USB_DEVICE_ATTACHED == action) {
           //USB插入了       
        } else if (UsbManager.ACTION_USB_DEVICE_DETACHED == action) {
           //USB拔出了
            if (mUsbDevice != null) {
                WLog.e(this"Device closed")
                if (mUsbDeviceConnection != null) {
                    mUsbDeviceConnection!!.close()
                }
            }
        }
    }
}

打印机的使用


Android上面的打印机大多数是USB连接的打印机,还有蓝牙打印机。下面重点介绍USB打印机的使用:在前面代码里找到USB打印设备后,我们需要拿到打印机的 UsbEndpoint,如下:


//UsbEndpoint:表示USB设备的单个端点。USB协议中,端点是用于发送和接收数据的逻辑
private var printerEp: UsbEndpoint? = null
private var usbInterface: UsbInterface? = null

fun connectUsbPrinter(mUsbDevice: UsbDevice?) {
    if (mUsbDevice != null) {
        usbInterface = mUsbDevice.getInterface(0)
        for (i in 0 until usbInterface!!.endpointCount) {
            val ep = usbInterface!!.getEndpoint(i)
            if (ep.type == UsbConstants.USB_ENDPOINT_XFER_BULK) {
                if (ep.direction == UsbConstants.USB_DIR_OUT) {
                    mUsbManager?.let {
                        //与USB设备建立连接
                        mUsbDeviceConnection = mUsbManager!!.openDevice(mUsbDevice)
                        //拿到USB设备的端点
                        printerEp = ep //拿到UsbEndpoint
                    }
                }
            }
        }
    }
}

开始打印:写入打印数据:


/**
 * usb写入
 *
 * @param bytes
 */

fun write(bytes: ByteArray) {
    if (mUsbDeviceConnection != null) {
        try {
            mUsbDeviceConnection!!.claimInterface(usbInterface, true)
            //注意设定合理的超时值,以避免长时间阻塞
            val b = mUsbDeviceConnection!!.bulkTransfer(printerEp, bytes, bytes.size, USBPrinter.TIME_OUT)
            mUsbDeviceConnection!!.releaseInterface(usbInterface)
        } catch (e: Exception) {
            e.printStackTrace()
           
        }
    }
}

一般通用USB打印命令都是ESC打印命令如下:


初始化打印机指令


//初始化打印机
public static byte[] init_printer() {
    byte[] result = new byte[2];
    result[0] = ESC;
    result[1] = 0x40;
    return result;
}

打印位置设置为居左对齐指令


 /**
     * 居左
     */

    public static byte[] alignLeft() {
        byte[] result = new byte[3];
        result[0] = ESC;
        result[1] = 97;
        result[2] = 0;
        return result;
    }

打印位置设置为居中对齐指令


    /**
     * 居中对齐
     */

    public static byte[] alignCenter() {
        byte[] result = new byte[3];
        result[0] = ESC;
        result[1] = 97;
        result[2] = 1;
        return result;
    }

打印位置设置居右对齐指令


    /**
     * 居右
     */

    public static byte[] alignRight() {
        byte[] result = new byte[3];
        result[0] = ESC;
        result[1] = 97;
        result[2] = 2;
        return result;
    }

打印结束切刀指令


    //切刀
    public static byte[] cutter() {
        byte[] box = new byte[6];
        box[0] = 0x1B;
        box[1] = 0x64;
        box[2] = 0x01;
        box[3] = 0x1d;
        box[4] = 0x56;
        box[5] = 0x31;
//        byte[] data = new byte[]{0x1d, 0x56, 0x01};
        return box;
    }

打印文字


/**
 * 打印文字
 *
 * @param msg
 */

///**
//     * 安卓9.0之前
//     * 只要你传送的数据不大于16384 bytes,传送不会出问题,一旦数据大于16384 bytes,也可以传送,
//     * 只是大于16384后面的数据就会丢失,获取到的数据永远都是前面的16384 bytes,
//     * 所以,android USB Host 模式与HID使用bulkTransfer(endpoint,buffer,length,timeout)通讯时
//     * buffer的长度不能超过16384。
//     * 


//     * controlTransfer( int requestType, int request , int value , int index , byte[] buffer , int length , int timeout)
//     * 该方法通过0节点向此设备传输数据,传输的方向取决于请求的类别,如果requestType 为 USB_DIR_OUT 则为写数据 , USB _DIR_IN ,则为读数据
//     */
fun printText(msg: String) {
    try {
        write(msg.toByteArray(charset("gbk")))
    } catch (e: UnsupportedEncodingException) {
        e.printStackTrace()
    }
}

打印图片,条码,二维码,可以将图片条码二维码转化为bitmap,然后再打印


//光栅位图打印
public static byte[] printBitmap(Bitmap bitmap) {
    byte[] bytes1 = new byte[4];
    bytes1[0] = GS;
    bytes1[1] = 0x76;
    bytes1[2] = 0x30;
    bytes1[3] = 0x00;

    byte[] bytes2 = getBytesFromBitMap(bitmap);
    return byteMerger(bytes1, bytes2);
}

蓝牙打印机,放在下一篇文章介绍吧,一起介绍蓝牙,及蓝牙打印


扫码枪、支付盒子、键盘、鼠标使用


扫码枪,支付盒子,键盘,鼠标都是USB连接设备,只需要插入Android 设备即可,前提是Android 设备硬件含有USB 接口,比如智能硬件 收银机,收银秤,车载插入U盘等


收银机 扫码枪、支付盒子 怎么扫码的?大家知道,我们的支付码,条码,其实是一串数字内容的,扫到后是怎么解析的?有两种方式的


  • 方式1:广播接收如下:

1. 先注册扫码广播

<receiver android:name=".ScanGunReceiver">
    <intent-filter>
        
        <action android:name="SCAN_ACTION" />
    intent-filter>
receiver>

2. 在广播接收器里面拿到扫码内容

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
 
public class ScanGunReceiver extends BroadcastReceiver {
 
    @Override
    public void onReceive(Context context, Intent intent) {
        // 获取扫码内容,这里的 "SCAN_RESULT" 是扫码枪提供的action,具体可能不同
        String scanContent = intent.getStringExtra("SCAN_RESULT");
 
        // 处理扫码内容
        if (scanContent != null) {
            // 扫码内容非空,执行相关逻辑
        }
    }
}

  • 方式2:在Activity的onKeyDown方法中监听,或者在Dialog.setOnKeyListener里面onKey中接收

1. Activity中onKeyDown:中解析每一个keyCode对应的数字值

override fun onKeyDown(keyCode: Int, event: KeyEvent?)Boolean {
    if (KeyUtils.doNotSwitchViewPagerByKey(keyCode)) {
        //按了键盘上 左右键 tab 键
        return true
    }
    scanHelpL.get().acceptKey(this, keyCode) {
        viewModel.scanByBarcode(it)
    }
    return super.onKeyDown(keyCode, event)
}

2. keyCode值与具体对照值如下:

object KeyUtils {

    //控制按键 左右 tab 键 不切换 viewpage
    fun doNotSwitchViewPagerByKey(keyCode: Int) = keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_DOWN || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || keyCode == KeyEvent.KEYCODE_TAB

    /**
     * keyCode转换为字符
     */

    fun keyCodeToChar(code: Int, isShift: Boolean): String{
        return when (code) {
            KeyEvent.KEYCODE_SHIFT_LEFT -> ""
            KeyEvent.KEYCODE_0 -> if (isShift) ")" else "0"
            KeyEvent.KEYCODE_1 -> if (isShift) "!" else "1"
            KeyEvent.KEYCODE_2 -> if (isShift) "@" else "2"
            KeyEvent.KEYCODE_3 -> if (isShift) "#" else "3"
            KeyEvent.KEYCODE_4 -> if (isShift) "$" else "4"
            KeyEvent.KEYCODE_5 -> if (isShift) "%" else "5"
            KeyEvent.KEYCODE_6 -> if (isShift) "^" else "6"
            KeyEvent.KEYCODE_7 -> if (isShift) "&" else "7"
            KeyEvent.KEYCODE_8 -> if (isShift) "*" else "8"
            KeyEvent.KEYCODE_9 -> if (isShift) "(" else "9"
            KeyEvent.KEYCODE_A -> if (isShift) "A" else "a"
            KeyEvent.KEYCODE_B -> if (isShift) "B" else "b"
            KeyEvent.KEYCODE_C -> if (isShift) "C" else "c"
            KeyEvent.KEYCODE_D -> if (isShift) "D" else "d"
            KeyEvent.KEYCODE_E -> if (isShift) "E" else "e"
            KeyEvent.KEYCODE_F -> if (isShift) "F" else "f"
            KeyEvent.KEYCODE_G -> if (isShift) "G" else "g"
            KeyEvent.KEYCODE_H -> if (isShift) "H" else "h"
            KeyEvent.KEYCODE_I -> if (isShift) "I" else "i"
            KeyEvent.KEYCODE_J -> if (isShift) "J" else "j"
            KeyEvent.KEYCODE_K -> if (isShift) "K" else "k"
            KeyEvent.KEYCODE_L -> if (isShift) "L" else "l"
            KeyEvent.KEYCODE_M -> if (isShift) "M" else "m"
            KeyEvent.KEYCODE_N -> if (isShift) "N" else "n"
            KeyEvent.KEYCODE_O -> if (isShift) "O" else "o"
            KeyEvent.KEYCODE_P -> if (isShift) "P" else "p"
            KeyEvent.KEYCODE_Q -> if (isShift) "Q" else "q"
            KeyEvent.KEYCODE_R -> if (isShift) "R" else "r"
            KeyEvent.KEYCODE_S -> if (isShift) "S" else "s"
            KeyEvent.KEYCODE_T -> if (isShift) "T" else "t"
            KeyEvent.KEYCODE_U -> if (isShift) "U" else "u"
            KeyEvent.KEYCODE_V -> if (isShift) "V" else "v"
            KeyEvent.KEYCODE_W -> if (isShift) "W" else "w"
            KeyEvent.KEYCODE_X -> if (isShift) "X" else "x"
            KeyEvent.KEYCODE_Y -> if (isShift) "Y" else "y"
            KeyEvent.KEYCODE_Z -> if (isShift) "Z" else "z"
            else -> ""
        }
    }
}

3. 扫码枪和支付盒子扫完,最后一位是回车键:检测到回车键值时候,就可以将扫到的码的内容 提交出去处理支付等操作。如下:

private fun acceptKey(keyCode: Int, block: (resultString) -> Unit) {
    //监听扫码广播
    if (keyCode != KeyEvent.KEYCODE_ENTER) {
        if (isDeleteStringBuilder) {
            val tmp: String = KeyUtils.keyCodeToChar(keyCode, hasShift)
            stringBuilder.append(tmp)
            hasShift = keyCode == KeyEvent.KEYCODE_SHIFT_LEFT
        }
    } else if (keyCode == KeyEvent.KEYCODE_ENTER) {
        if (isDeleteStringBuilder) {
            isDeleteStringBuilder = false
            if (!TextUtils.isEmpty(stringBuilder.toString())) {
                block?.invoke(stringBuilder.toString())
            }
            stringBuilder.delete(0, stringBuilder.length)
            isDeleteStringBuilder = true
        }
    }
}

需要注意的是,扫码枪,支付盒子,键盘都是输入设备,要避免UI视图上面 控件焦点设置为 false,同时界面不能有 EditText控件,否则会将扫到的内容自动填入EditText控件里面去。

/   总结   /

本文重点介绍了Android 智能嵌入式设备,接入串口,USB,打印机,扫码枪支付盒子,键盘鼠标等,接入的简单开发。当然涉及到的蓝牙,蓝牙打印机,分屏这些会在后面的文章中进行介绍。

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
原创:写给初学者的Jetpack Compose教程,edge-to-edge全面屏体验
Android 15新特性,强制edge-to-edge全面屏体验

欢迎关注我的公众号
学习技术或投稿

长按上图,识别图中二维码即可关注