安卓与串口通信-实践篇

news2024/11/28 20:37:02

前言

在上一篇文章中我们讲解了关于串口的基础知识,没有看过的同学推荐先看一下,否则你可能会不太理解这篇文章所述的某些内容。

这篇文章我们将讲解安卓端的串口通信实践,即如何使用串口通信实现安卓设备与其他设备例如PLC主板之间数据交互。

需要注意的是正如上一篇文章所说的,我目前的条件只允许我使用 ESP32 开发版烧录 Arduino 程序与安卓真机(小米10U)进行串口通信演示。

准备工作

由于我们需要使用 ESP32 烧录 Arduino 程序演示安卓端的串口通信,所以在开始之前我们应该先把程序烧录好。

那么烧录一个怎样的程序呢?

很简单,我这里直接烧了一个 ESP32 使用 9600 的波特率进行串口通信,程序内容就是 ESP32 不断的向串口发送数据 “e” ,并且监听串口数据,如果接收到数据 “o” 则打开开发版上自带的 LED 灯,如果接收到数据 “c” 则关闭这个 LED 灯。

代码如下:

#define LED 12

void setup() {
  Serial.begin(9600);
  pinMode(LED, OUTPUT);
}

void loop() {
  if (Serial.available()) {
    char c = Serial.read();
    if (c == 'o') {
      digitalWrite(LED, HIGH);
    }
    if (c == 'c') {
      digitalWrite(LED, LOW);
    }
  }

  Serial.write('e');

  delay(100);
}

上面的 12 号 Pin 是这块开发版的 LED。

使用 Arduino自带串口监视器测试结果:

1.gif

可以看到,确实如我们设想的通过串口不断的发送字符 “e”,并且在接收到字符 “o” 后点亮了 LED。

安卓实现串口通信

原理概述

众所周知,安卓其实是基于 Linux 的操作系统,所以在安卓中对于串口的处理与 Linux 一致。

在 Linux 中串口会被视为一个“设备”,并体现为 /dev/ttys 文件。

/dev/ttys 又被称为字符终端,例如 ttys0 对应的是 DOS/Windows 系统中的 COM1 串口文件。

通常,我们可以简单理解,如果我们插入了某个串口设备,则这个设备与 Linux 的通信会由 /dev/ttys 文件进行 “中转”。

即,如果 Linux 想要发送数据给串口设备,则可以通过往 /dev/ttys 文件中直接写入要发送的数据来实现,如:

echo test > /dev/ttyS1 这个命令会将 “test” 这串字符发送给串口设备。

如果想读取串口发送的数据也是一样的,可以通过读取 /dev/ttys 文件内容实现。

所以,如果我们在安卓中想要实现串口通信,大概率也会想到直接读取/写入这个特殊文件。

android-serialport-api

在上文中我们说到,在安卓中也可以通过与 Linux 一样的方式–直接读写 /dev/ttys 实现串口通信。

但是其实并不需要我们自己去处理读写和数据的解析,因为谷歌官方给出了一个解决方案:android-serialport-api

为了便于理解,我们会大致说一下这个解决方案的源码,但是就不上示例了,至于为什么,同学们往下看就知道了。另外,虽然这个方案历史比较悠久,也很长时间没有人维护了,但是并不意味着不能使用了,只是使用条件比较苛刻,当然,我司目前使用的还是这套方案(哈哈哈哈)。

不过这里我们不直接看 android-serialport-api 的源码,而是通过其他大佬二次封装的库来看: Android-SerialPort-API

在这个库中,通过

// 默认直接初始化,使用8N1(8数据位、无校验位、1停止位),path为串口路径(如 /dev/ttys1),baudrate 为波特率
SerialPort serialPort = new SerialPort(path, baudrate);

// 使用可选参数配置初始化,可配置数据位、校验位、停止位 - 7E2(7数据位、偶校验、2停止位)
SerialPort serialPort = SerialPort 
    .newBuilder(path, baudrate)
// 校验位;0:无校验位(NONE,默认);1:奇校验位(ODD);2:偶校验位(EVEN)
//    .parity(2) 
// 数据位,默认8;可选值为5~8
//    .dataBits(7) 
// 停止位,默认1;1:1位停止位;2:2位停止位
//    .stopBits(2) 
    .build();

初始化串口,然后通过:

InputStream in = serialPort.getInputStream();
OutputStream out = serialPort.getOutputStream();

获取到输入/输出流,通过读取/写入这两个流来实现与串口设备的数据通信。

我们首先来看看初始化串口是怎么做的。

2.png

首先检查了当前是否具有串口文件的读写权限,如果没有则通过 shell 命令更改权限为 666 ,更改后再次检查是否有权限,如果还是没有就抛出异常。

注意这里的执行 shell 时使用的 runtime 是 Runtime.getRuntime().exec(sSuPath); 也就是说,它是通过 root 权限来执行这段命令的!

换句话说,如果想要通过这种方式实现串口通信,必须要有 ROOT 权限!这就是我说我不会给出示例的原因,因为我手头的设备无法 ROOT 啊。至于为啥我司还能继续使用这种方案的原因也很简单,因为我们工控机的安卓设备都是定制版的啊,拥有 ROOT 权限不是基本操作?

确定权限可用后通过 open 方法拿到一个类型为 FileDescriptor 的变量 mFd ,最后通过这个 mFd 拿到输入输出流。

所以核心在于 open 方法,而 open 方法是一个 native 方法,即 C 代码:

private native FileDescriptor open(String absolutePath, int baudrate, int dataBits, int parity,
    int stopBits, int flags);

C 的源码这里就不放了,只需要知道它做的工作就是打开了 /dev/ttys 文件(准确的说是“终端”),然后通过传递进去的这些参数去按串口规则解析数据,最后返回一个 java 的 FileDescriptor 对象。

在 java 中我们再通过这个 FileDescriptor 对象可以拿到输入/输出流。

原理说起来是十分的简单。

看完通信部分的原理后,我们再来看看我们如何查找可用的串口呢?

其实和 Linux 上也一样:

public Vector<File> getDevices() {
    if (mDevices == null) {
        mDevices = new Vector<File>();
        File dev = new File("/dev");
        
        File[] files = dev.listFiles();

        if (files != null) {
            int i;
            for (i = 0; i < files.length; i++) {
                if (files[i].getAbsolutePath().startsWith(mDeviceRoot)) {
                    Log.d(TAG, "Found new device: " + files[i]);
                    mDevices.add(files[i]);
                }
            }
        }
    }
    return mDevices;
}

也是通过直接遍历 /dev 下的文件,只不过这里做了一些额外的过滤。

或者也可以通过读取 /proc/tty/drivers 配置文件后过滤:

Vector<Driver> getDrivers() throws IOException {
    if (mDrivers == null) {
        mDrivers = new Vector<Driver>();
        LineNumberReader r = new LineNumberReader(new FileReader("/proc/tty/drivers"));
        String l;
        while ((l = r.readLine()) != null) {
            // Issue 3:
            // Since driver name may contain spaces, we do not extract driver name with split()
            String drivername = l.substring(0, 0x15).trim();
            String[] w = l.split(" +");
            if ((w.length >= 5) && (w[w.length - 1].equals("serial"))) {
                Log.d(TAG, "Found new driver " + drivername + " on " + w[w.length - 4]);
                mDrivers.add(new Driver(drivername, w[w.length - 4]));
            }
        }
        r.close();
    }
    return mDrivers;
}

关于读取可用串口设备,其实从这里的路径也可以看出,都是系统路径,也就是说,如果没有权限,大概率也是读取不到东西的。

这就是使用与 Linux 一样的方式去读取串口数据的基本原理,那么问题来了,既然我说这个方法使用条件比较苛刻,那么更易用的替代方案是什么呢?

我们下面就会介绍,那就是使用安卓的 USB host (USB主机)的功能。

USB host

Android 3.1(API 级别 12)或更高版本的平台直接支持 USB 配件和主机模式。USB 配件模式还作为插件库向后移植到 Android 2.3.4(API 级别 10)中,以支持更广泛的设备。设备制造商可以选择是否在设备的系统映像中添加该插件库。

在安卓 3.1 版本开始,支持将USB作为主机模式(USB host)使用,而我们如果想要通过 USB 读取串口数据则需要依赖于这个主机模式。

在正式开始介绍USB主机模式前,我们先简要介绍一下安卓上支持的USB模式。

安卓上的USB支持三种模式:设备模式、主机模式、配件模式。

设备模式即我们常用的直接将安卓设备连接至电脑上,此时电脑上显示为 USB 外设,即可以当成 “U盘” 使用拷贝数据,不过现在安卓普遍还支持 MTP模式(作为摄像头)、文件传输模式(即当U盘用)、网卡模式等。

主机模式即将我们的安卓设备作为主机,连接其他外设,此时安卓设备就相当于上面设备模式中的电脑。此时安卓设备可以连接键盘、鼠标、U盘以及嵌入式应用USB转串口、转I2C等设备。但是如果想要将安卓设备作为主机模式可能需要一条支持 OTG 的数据线或转接头。(Micro-USB 或 USB type-c 转 USB-A 口)

而在 USB 配件模式下,外部 USB 硬件充当 USB 主机。配件示例可能包括机器人控制器、扩展坞、诊断和音乐设备、自助服务终端、读卡器等等。这样,不具备主机功能的 Android 设备就能够与 USB 硬件互动。Android USB 配件必须设计为与 Android 设备兼容,并且必须遵守 Android 配件通信协议。

设备模式与配件模式的区别在于在配件模式下,除了 adb 之外,主机还可以看到其他 USB 功能。

usb-host-accessory.png

使用USB主机模式与外设交互数据

在介绍完安卓中的三种USB模式后,下面我们开始介绍如何使用USB主机模式。当然,这里只是大概介绍原生APi的使用方法,我们在实际使用中一般都都是直接使用大佬编写的第三方库。

准备工作

在开始正式使用USB主机模式时我们需要先做一些准备工作。

首先我们需要在清单文件(AndroidManifest.xml)中添加:

<!-- 声明需要USB主机模式支持,避免不支持的设备安装了该应用 -->
<uses-feature android:name="android.hardware.usb.host" />

<!-- …… -->

<!-- 声明需要接收USB连接事件 -->
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />

一个完整的清单文件示例如下:

<manifest ...>
    <uses-feature android:name="android.hardware.usb.host" />
    <uses-sdk android:minSdkVersion="12" />
    ...
    <application>
        <activity ...>
            ...
            <intent-filter>
                <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
            </intent-filter>
        </activity>
    </application>
</manifest>

声明好清单文件后,我们就可以查找当前可用的设备信息了:

private fun scanDevice(context: Context) {
    val manager = context.getSystemService(Context.USB_SERVICE) as UsbManager
    val deviceList: HashMap<String, UsbDevice> = manager.deviceList
    Log.i(TAG, "scanDevice: $deviceList")
}

将 ESP32 开发版插上手机,运行程序,输出如下:

3.png

可以看到,正确的查找到了我们的 ESP32 开发版。

这里提一下,因为我们的手机只有一个 USB 口,此时已经插上了 ESP32 开发版,所以无法再通过数据线直接连接电脑的 ADB 了,此时我们需要使用无线 ADB,具体怎么使用无线 ADB,请自行搜索。

另外,如果我们想要通过查找到设备后请求连接的方式连接到串口设备的话,还需要额外申请权限。(同理,如果我们直接在清单文件中提前声明需要连接的设备则不需要额外申请权限,具体可以看看参考资料5,这里不再赘述)

首先声明一个广播接收器,用于接收授权结果:

private lateinit var permissionIntent: PendingIntent

private const val ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"

private val usbReceiver = object : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        if (ACTION_USB_PERMISSION == intent.action) {
            synchronized(this) {
                val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)

                if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                    device?.apply {
                    	// 已授权,可以在这里开始请求连接
                        connectDevice(context, device)
                    }
                } else {
                    Log.d(TAG, "permission denied for device $device")
                }
            }
        }
    }
}

声明好之后在 Acticity 的 OnCreate 中注册这个广播接收器:

permissionIntent = PendingIntent.getBroadcast(this, 0, Intent(ACTION_USB_PERMISSION), FLAG_MUTABLE)
val filter = IntentFilter(ACTION_USB_PERMISSION)
registerReceiver(usbReceiver, filter)

最后,在查找到设备后,调用 manager.requestPermission(deviceList.values.first(), permissionIntent) 弹出对话框申请权限。

连接到设备并收发数据

完成上述的准备工作后,我们终于可以连接搜索到的设备并进行数据交互了:

private fun connectDevice(context: Context, device: UsbDevice) {
    val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager

    CoroutineScope(Dispatchers.IO).launch {
        device.getInterface(0).also { intf ->
            intf.getEndpoint(0).also { endpoint ->
                usbManager.openDevice(device)?.apply {
                    claimInterface(intf, forceClaim)
                    while (true) {
                        val validLength = bulkTransfer(endpoint, bytes, bytes.size, TIMEOUT)
                        if (validLength > 0) {
                            val result = bytes.copyOfRange(0, validLength)
                            Log.i(TAG, "connectDevice: length = $validLength")
                            Log.i(TAG, "connectDevice: byte = ${result.contentToString()}")
                        }
                        else {
                            Log.i(TAG, "connectDevice: Not recv data!")
                        }
                    }
                }
            }
        }
    }
}

在上面的代码中,我们使用 usbManager.openDevice 打开了指定的设备,即连接到设备。

然后通过 bulkTransfer 接收数据,它会将接收到的数据写入缓冲数组 bytes 中,并返回成功接收到的数据长度。

运行程序,连接设备,日志打印如下:

4.png

可以看到,输出的数据并不是我们预料中的数据。

这是因为这是非常原始的数据,如果我们想要读取数据,还需要针对不同的串口转USB芯片或协议编写驱动程序才能获取到正确的数据。

顺道一提,如果想要将数据写入串口数据的话可以使用 controlTransfer()

所以,我们在实际生产环境中使用的都是基于此封装好的第三方库。

这里推荐使用 usb-serial-for-android

usb-serial-for-android

使用这个库的第一步当然是导入依赖:

// 添加仓库
allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}
// 添加依赖
dependencies {
    implementation 'com.github.mik3y:usb-serial-for-android:3.4.6'
}

添加完依赖同样需要在清单文件中添加相应字段以及处理权限,因为和上述使用原生API一致,所以这里不再赘述。

和原生 API 不同的是,因为我们此时已经知道了我们的 ESP32 主板的设备信息,以及使用的驱动(CDC),所以我们就不使用原生的查找可用设备的方法了,我们这里直接指定我们已知的这个设备(当然,你也可以继续使用原生API的查找和连接方法):

private fun scanDevice(context: Context) {
    val manager = context.getSystemService(Context.USB_SERVICE) as UsbManager

    val customTable = ProbeTable()
    // 添加我们的设备信息,三个参数分别为 vendroId、productId、驱动程序
    customTable.addProduct(0x1a86, 0x55d3, CdcAcmSerialDriver::class.java)

    val prober = UsbSerialProber(customTable)
    // 查找指定的设备是否存在
    val drivers: List<UsbSerialDriver> = prober.findAllDrivers(manager)

    if (drivers.isNotEmpty()) {
        val driver = drivers[0]
        // 这个设备存在,连接到这个设备
        val connection = manager.openDevice(driver.device)
    }
    else {
        Log.i(TAG, "scanDevice: 无设备!")
    }
}

连接到设备后,下一步就是和数据交互,这里封装的十分方便,只需要获取到 UsbSerialPort 后,直接调用它的 read()write() 即可读写数据:

port = driver.ports[0] // 大多数设备都只有一个 port,所以大多数情况下直接取第一个就行

port.open(connection)
// 设置连接参数,波特率9600,以及 “8N1”
port.setParameters(9600, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)

// 读取数据
val responseBuffer = ByteArray(1024)
port.read(responseBuffer, 0)

// 写入数据
val sendData = byteArrayOf(0x6F)
port.write(sendData, 0)

此时,一个完整的,用于测试我们上述 ESP32 程序的代码如下:

@Composable
fun SerialScreen() {
    val context = LocalContext.current


    Column(
        Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Button(onClick = { scanDevice(context) }) {
            Text(text = "查找并连接设备")
        }

        Button(onClick = { switchLight(true) }) {
            Text(text = "开灯")
        }
        Button(onClick = { switchLight(false) }) {
            Text(text = "关灯")
        }

    }
}

private fun scanDevice(context: Context) {
    val manager = context.getSystemService(Context.USB_SERVICE) as UsbManager

    val customTable = ProbeTable()
    customTable.addProduct(0x1a86, 0x55d3, CdcAcmSerialDriver::class.java)

    val prober = UsbSerialProber(customTable)
    val drivers: List<UsbSerialDriver> = prober.findAllDrivers(manager)

    if (drivers.isNotEmpty()) {
        val driver = drivers[0]

        val connection = manager.openDevice(driver.device)
        if (connection == null) {
            Log.i(TAG, "scanDevice: 连接失败")
            return
        }

        port = driver.ports[0]

        port.open(connection)
        port.setParameters(9600, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE)

        Log.i(TAG, "scanDevice: Connect success!")

        CoroutineScope(Dispatchers.IO).launch {
            while (true) {
                val responseBuffer = ByteArray(1024)

                val len = port.read(responseBuffer, 0)

                Log.i(TAG, "scanDevice: recv data = ${responseBuffer.copyOfRange(0, len).contentToString()}")
            }
        }
    }
    else {
        Log.i(TAG, "scanDevice: 无设备!")
    }
}

private fun switchLight(isON: Boolean) {
    val sendData = if (isON) byteArrayOf(0x6F) else byteArrayOf(0x63)

    port.write(sendData, 0)
}

运行这个程序,并且连接设备,输出如下:

5.png

可以看到输出的是 byte 的 101,转换为 ASCII 即为 “e”。

然后我们点击 “开灯”、“关灯” 效果如下:

6.gif

对了,这里发送的数据 “0x6F” 即 ASCII “o” 的十六进制,同理,“0x63” 即 “c”。

可以看到,可以完美的和我们的 ESP32 开发版进行通信。

实例

无论使用什么方式与串口通信,我们在安卓APP的代码层面能够拿到的数据已经是处理好了的数据。

即,在上一篇文章中我们说过串口通信的一帧数据包括起始位、数据位、校验位、停止位。但是我们在安卓中使用时一般拿到的都只有 数据位 的数据,其他数据已经在底层被解析好了,无需我们去关心怎么解析,或者使用。

我们可以直接拿到的就是可用数据。

这里举一个我之前用过的某型号驱动版的例子。

这块驱动版关于通信的信息如图:

7.png

可以看到,它采用了 RS485 的通信方式,波特率支持 9600 或 38400,8位数据位,无校验,1位停止位。

并且,它还规定了一个数据协议。

在它定义的协议中,第一位为地址;第二位为指令;第三位到第N位为数据内容;最后两位为CRC校验。

需要注意的是,这里定义的协议是基于串口通信的,不要把这个协议和串口通信搞混了,简单来说就是在串口通信协议的数据位中又定义了一个自己的协议。

而且可以看到,虽然定义串口参数时没有指定校验,但是在它自己的协议中指定了使用 CRC 校验。

另外,弱弱的吐槽一句,这个驱动版的协议真的不好使。

在实际使用过程中,主机与驱动版的通信数据无法保证一定会在同一个数据帧中发送完成,所以可能会造成“粘包”、“分包”现象,也就是说,数据可能会分几次发过来,而且你不好判断这数据是上次没发送完的数据还是新的数据。

我使用过的另外一款驱动版就方便的多,因为它会在帧头加上开始符号和数据长度,帧尾加上结束符号。

这样一来,即使出现“粘包”、“分包”我们也能很好的给它解析出来。

当然,它这样设计协议肯定是有它的道理的,无非就是减少通信代价之类的。

我还遇到过一款十分简洁的驱动版,直接发送一个整数过去表示执行对应的指令。

驱动版回传的数据同样非常简单,就是一个数字,然后事先约定各个数字表示什么意思……

说归说,我们还是继续来看这款驱动版的通信协议:

8.png

这是它的其中一个指令内容,我们发送指令 “1” 过去后,它会返回当前驱动版的型号和版本信息给我们。

因为我们的主板是定制工控主板,所以使用的通信方式是直接用 android-serialport-api。

最终发送与接收回复也很简单:

/**
 * 将十六进制字符串转成 ByteArray
 * */
private fun hexStrToBytes(hexString: String): ByteArray {
    check(hexString.length % 2 == 0) { return ByteArray(0) }

    return hexString.chunked(2)
        .map { it.toInt(16).toByte() }
        .toByteArray()
}

private fun isReceivedLegalData(receiveBuffer: ByteArray): Boolean {

    val rcvData = receiveBuffer.copyOf()  //重新拷贝一个使用,避免原数据被清零

    if (cmd.cmdId.checkDataFormat(rcvData)) {  //检查回复数据格式
        isPkgLost = false
        if (cmd.cmdId.isResponseBelong(rcvData)) {  //检查回复命令来源
            if (!AdhShareData.instance.getIsUsingCrc()) {  //如果不开启CRC检验则直接返回 true
                resolveRcvData(cmdRcvDataCallback, rcvData, cmd.cmdId)
                coroutineScope.launch(Dispatchers.Main) {
                    cmdResponseCallback?.onCmdResponse(ResponseStatus.Success, rcvData, 0, rcvData.size, cmd.cmdId)
                }
                return true
            }

            if (cmd.cmdId.checkCrc(rcvData)) {  //检验CRC
                 resolveRcvData(cmdRcvDataCallback, rcvData, cmd.cmdId)
                coroutineScope.launch(Dispatchers.Main) {
                    cmdResponseCallback?.onCmdResponse(ResponseStatus.Success, rcvData, 0, rcvData.size, cmd.cmdId)
                }

                return true
            }
            else {
                coroutineScope.launch(Dispatchers.Main) {
                    cmdResponseCallback?.onCmdResponse(ResponseStatus.FailCauseCrcError, ByteArray(0), -1, -1, cmd.cmdId)
                }

                return false
            }
        }
        else {
            coroutineScope.launch(Dispatchers.Main) {
                cmdResponseCallback?.onCmdResponse(ResponseStatus.FailCauseNotFromThisCmd, ByteArray(0), -1, -1, cmd.cmdId)
            }

            return false
        }
    }
    else {  //数据不符合,可能是遇到了分包,继续等待下一个数据,然后合并
        isPkgLost = true
        return isReceivedLegalData(cmd)
        /*coroutineScope.launch(Dispatchers.Main) {
            cmdResponseCallback?.onCmdResponse(ResponseStatus.FailCauseWrongFormat, ByteArray(0), -1, -1, cmd.cmdId)
        }

        return false  */
    }
}

// ……省略初始化和连接代码

// 发送数据
val bytes = hexStrToBytes("0201C110")
outputStream.write(bytes, 0, bytes.size)

// 解析数据
val recvBuffer = ByteArray(0)
inputStream.read(recvBuffer)

while (receiveBuffer.isEmpty()) {
   delay(10)
}

isReceivedLegalData()

本来打算直接发我封装好的这个驱动版的协议库的,想了想,好像不太合适,所以就大概抽出了这些不完整的代码,懂这个意思就行了,哈哈。

总结

从上面介绍的两种方式可以看出,两种方式使用各有优缺点。

使用 android-serialport-api 可以直接读取串口数据内容,不需要转USB接口,不需要驱动支持,但是需要 ROOT,适合于定制安卓主板上已经预留了 RS232 或 RS485 接口且设备已 ROOT 的情况下使用。

而使用 USB host ,可以直接读取USB接口转接的串口数据,不需要ROOT,但是只支持有驱动的串口转USB芯片,且只支持使用USB接口,不支持直接连接串口设备。

各位可以根据自己的实际情况灵活选择使用什么方式来实现串口通信。

当然,除了现在介绍的这些串口通信,其实还有一个通信协议在实际使用中用的非常多,那就是 MODBUS 协议。

下一篇文章,我们将介绍 MODBUS。

参考资料

  1. android-serialport-api
  2. What is tty?
  3. Text-Terminal-HOWTO
  4. Terminal Special Files
  5. USB host
  6. Android开启OTG功能/USB Host API功能

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/550658.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【计算机网络复习】第五章 数据链路层 1

数据链路层的作用 两台主机&#xff08;端到端&#xff09;之间的通信是依靠相邻的主机/路由器之间的逐步数据传送来实现 数据链路层实现相邻主机/路由器间的可靠的数据传输 网络层&#xff1a;主机-主机通信 数据链路层&#xff1a;点到点通信 数据链路层的主要功能 u 链路…

3R幸福法

3R幸福法 3R幸福法可以帮助我们《建立和谐亲密关系》 模型介绍 3R幸福法体现的是一个人被关注的完整逻辑&#xff1a; Reson-理由&#xff0c;关注缘起。不会让你想表达的感情看起来显得唐突&#xff0c;不会让对方和自己不好意思。Respond-反应&#xff0c;是关注影响。更多…

ROS 工作空间与功能包的创建(三)

执行命令&#xff1a; mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src/catkin_init_workspace echo "source ~/catkin_ws/devel/setup.bash" >> ~/.bashrc 效果&#xff1a; 输入命令查看添加成功了没有 tail ~/.bashrc 编译工程&#xff1a;执行命令 cd ~…

csdn文章markdown格式下载

前言 网上下载CSDN文章的方式多种多样&#xff0c;有的直接下载为pdf格式&#xff0c;有的利用python/java代码将文章下载为md格式&#xff0c;也有的用JavaScript下载文章&#xff0c;但下载的文章仅限于自己写的&#xff08;会介绍&#xff09;&#xff0c;这里也提供一种简单…

关于接口中的一些常用实例以及比较方法的区别

文章目录 &#x1f490;文章导读&#x1f334;Object 类toString() 获取对象信息equals 对象比较方法hashCode 获得对象的具体位置 &#x1f334;接口使用实例Comparable 接口Comparator 接口 &#x1f490;文章导读 在本篇文章中&#xff0c;详解了什么是Object类&#xff0c;…

MyBatis中三级缓存的理解

文章目录 前言1. 环境搭建1.1 依赖引入1.2 mybatis-config.xml配置配置db.properties在mybatis-config.xml引入db.properties 1.3 实体类1.4 mappermapper接口mapper映射文件 1.5 测试 2.缓存2.1 一级缓存mybatis-config.xml 配置日志开启日志配置日志文件logback.xml测试相同m…

深度学习之搭建LSTM模型预测股价

大家好&#xff0c;我是带我去滑雪&#xff01; 本期利用Google股价数据集&#xff0c;该数据集中GOOG_Stock_Price_Train.csv为训练集&#xff0c;GOOG_Stock_Price_Test.csv为测试集&#xff0c;里面有开盘价、最高股价、最低股价、收盘价、调整后的收盘价、成交量&#xff0…

Flutter项目webview加载没有HTTPS证书的网页在Android和iOS设备上无法显示的解决方案

一、问题描述 Flutter项目使用谷歌官方webview库 webview_flutter&#xff0c;加载自签名证书、证书失效、无证书等HTTPS网页地址时&#xff0c;在Android或pc浏览器中提示证书失效&#xff0c;在iOS设备上为空白页&#xff0c;为了加载自签名证书的网页&#xff0c;需要饶过i…

AVR单片机ATemga328P中断原理的介绍

1、一AVR单片机中断原理的介绍 ATmega328P微控制器具有两个外部中断引脚&#xff0c;分别是INT0和INT1。 外部中断0&#xff08;INT0&#xff09;&#xff1a;它对应的引脚是PD2&#xff08;数字引脚2&#xff09;。INT0可以用于响应外部信号的边沿触发&#xff08;上升沿、下…

【服务器】使用Nodejs搭建HTTP web服务器

Yan-英杰的主页 悟已往之不谏 知来者之可追 C程序员&#xff0c;2024届电子信息研究生 目录 前言 1.安装Node.js环境 2.创建node.js服务 3. 访问node.js 服务 4.内网穿透 4.1 安装配置cpolar内网穿透 4.2 创建隧道映射本地端口 5.固定公网地址 [TOC] 转载自内网穿透…

Unity Addressables学习笔记(1)---创建远程服务器加载资源

例子1&#xff1a;加载一个图片 1.首先创建一个UI Image&#xff0c;空白图片,资源打包方式选择真是部署的 2.修改远程发布和加载配置 Bulid Path选择RemoteBuildPath Load Path我选择了custom,地址是http://localhost:8080/WebGL/ 遇坑1 :最开始我选择的Build Path 是 Loca…

windows安装mysql 5.7.41

前言 要学mysql&#xff0c;肯定得本地装上一个玩一玩啦&#xff0c;下面一起来安装mysql吧 一、下载 https://downloads.mysql.com/archives/community/ 顺便说一下&#xff0c;下载按钮下方有个md5&#xff0c;可以验证下文件是否被篡改&#xff0c;理论上官网下载的应该问…

初识结构体

目录 结构体的声明 结构体的基础知识 结构体的声明 结构体成员的类型 结构体变量的定义和初始化 定义 初始化 结构体成员的访问 结构体变量访问成员 结构体指针访问指向变量的成员 结构体传参 传地址 传结构体 结论 结构体的声明 结构体的基础知识 数组&#xff…

【ChatGPT】IOS如何下载注册使用ChatGPT的APP(教学)

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化 &#x1f449;专__注&#x1f448;&#xff1a;专注主流机器人、人工智能等相关领域的开发、…

iptables 防火墙

iptables概述 Linux系统的防火墙&#xff1a;ip信息过滤系统&#xff0c;它实际上由两个组件netfilter和iptables组成。 主要工作在网络层&#xff0c;针对IP数据包。体现在对包内的IP地址、端口、协议等信息的处理上 netfilter / iptables关系&#xff1a; netfilter:属于…

Electron中如何创建模态窗口?

目录 前言一、模态窗口1.Web页面模态框2.Electron中的模态窗口3.区分父子窗口与模态窗口 二、实际案例使用总结 前言 模态框是一种常用的交互元素&#xff0c;无论是在 Web 网站、桌面应用还是移动 APP 中&#xff0c;都有其应用场景。模态框指的是一种弹出窗口&#xff0c;它…

【TES714】JFM7K325T(复旦微FPGA)+HI3531DV200(华为海思)的综合视频处理平台设计原理图及调试经验

板卡概述 TES714是自主研制的一款5路HD-SDI视频采集图像处理平台&#xff0c;该平台采用上海复旦微的高性能Kintex系列FPGA加上华为海思的高性能视频处理器HI3531DV200来实现。 华为海思的HI3531DV200是一款集成了ARM A53四核处理器性能强大的神经网络引擎&#xff0c;支持多种…

【运维知识进阶篇】集群架构-Nginx动静分离详解

我们先前将静态资源放到NFS&#xff0c;动态资源放到MySQL&#xff0c;一是为了提高我们Web服务器性能&#xff0c;减轻它的压力&#xff0c;另一面如果Web宕机了&#xff0c;我们的静态和动态资源还可以访问到。但是之前方式不管是静态还是动态文件&#xff0c;都是走的代码文…

ssl vpn 与 ipsec vpn 区别

VPN 安全协议有两种主要类型&#xff0c;IPsec 和 SSL&#xff0c;了解它们之间的区别对于确保客户的安全至关重要。在本文中&#xff0c;我们将解释IPsec 和 SSL VPN 协议之间的区别&#xff0c;以及如何选择合适的协议来满足客户的需求。了解更多SSL技术最新信息&#xff0c;…

Linux_证书_Openssl实现对称加密、非对称加密、CA颁布证书

文章目录 OpenSSLopenssl实现对称加密openssl实现非对称加密生成密钥对非对称加密数字签名小结 根据CA颁布证书生成ca私钥和ca证书根据ca生成证书 尾声 OpenSSL 常用证书生成工具包括三个&#xff1a;ssh-keygen、cfssl、openssl。这里介绍 OpenSSL , OpenSSL 是一个开源项目&…