本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目是一个基于Android平台的串口通信应用,专为疫情期间体温监测设计。应用通过串口与温度传感器通信,实现体温数据的实时获取与展示,包含数据解析和1秒定时更新功能。项目使用Java开发,集成GoogleSerialPort库进行串口操作,并通过Handler或ScheduledExecutorService实现定时任务。适用于学习Android设备间通信与实时数据处理,是掌握串口通信核心技术的实用案例。
android串口通讯源代码,实现了数据解析,定时更新,用于疫情期间温度传感器

1. Android串口通信基础

1.1 串行通信与并行通信的基本概念

在计算机通信领域,数据传输方式主要分为串行通信和并行通信。 串行通信 是指数据按顺序逐位传输,使用较少的物理线路,适用于远距离、低速率通信场景;而 并行通信 则是一次性传输多个比特,速度快但线路复杂,适合短距离高速传输。

在Android嵌入式开发中,串行通信因其结构简单、成本低廉而被广泛采用,尤其是在与外部传感器、工业设备等硬件进行通信时。

通信方式 数据传输方式 优点 缺点 应用场景
串行通信 逐位传输 线路少、成本低 速度相对较慢 温度传感器、GPS、RFID
并行通信 多位同时传输 传输速率高 易受干扰、布线复杂 打印机、高速总线

1.2 串口通信协议的基本结构

串口通信协议由多个关键组成部分构成,主要包括:

  • 起始位(Start Bit) :标识一个数据帧的开始,通常为低电平。
  • 数据位(Data Bits) :实际传输的数据部分,常见为5~8位。
  • 校验位(Parity Bit) :用于数据校验,可选偶校验、奇校验或无校验。
  • 停止位(Stop Bit) :标识一个数据帧的结束,为高电平,常见为1位或2位。

通信双方必须在这些参数上保持一致,否则将导致数据解析错误。

// 示例:配置串口参数(GoogleSerialPort库)
SerialPort serialPort = new SerialPort("/dev/ttyS1", 9600, 8, 'N', 1);
// 参数说明:
// 路径:设备节点路径
// 波特率:9600 bps
// 数据位:8位
// 校验位:无('N'表示None)
// 停止位:1位

1.3 Android平台下串口通信的实现原理

Android系统底层基于Linux内核,串口设备通常以文件形式存在于 /dev 目录下(如 /dev/ttyS0 、 /dev/ttyUSB0 )。应用程序通过标准的文件读写接口(如 FileInputStream 、 FileOutputStream )与串口设备进行数据交互。

实现串口通信的核心步骤包括:

  1. 打开串口设备文件;
  2. 配置串口参数(波特率、数据位、校验位、停止位);
  3. 启动读写线程进行数据收发;
  4. 异常处理与资源释放。

目前主流的串口通信库包括: GoogleSerialPort 、 android-serialport-api 等,它们封装了底层的JNI调用,简化了开发者的工作。

1.4 串口通信在温度传感器中的应用场景

在疫情背景下,体温检测设备(如红外测温仪、热电偶传感器)广泛采用串口通信方式与Android主控设备进行数据交互。例如:

  • 数据采集 :传感器通过UART接口周期性发送温度数据;
  • 指令控制 :Android端可发送指令控制传感器采样频率、单位切换等;
  • 数据展示 :采集到的温度值在Android界面上实时显示,并可上传至云端系统。

通过本章学习,读者将掌握串口通信的基础知识与Android平台下的实现机制,为后续章节中深入学习数据采集、解析与实时监控打下坚实基础。

2. USB转串口适配器与UART接口使用

在Android平台上实现串口通信,往往需要借助硬件接口的支持。由于现代移动设备普遍取消了原生的串口接口,USB转串口适配器成为连接外部串口设备的关键桥梁。本章将围绕USB转串口适配器的使用与UART接口通信展开详细讲解,涵盖硬件接口的基本组成、Android设备的连接方式、通信参数配置以及异常处理机制等内容,帮助开发者掌握从硬件连接到数据交互的全流程控制。

2.1 串口通信硬件接口概述

串口通信的核心在于接口的物理结构与通信协议的定义。在实际开发中,UART接口是最常见的串行通信方式之一,而USB转串口适配器则充当了将USB接口转换为UART接口的桥梁。理解其工作原理和结构组成,是构建稳定串口通信链路的前提。

2.1.1 UART通信接口的基本组成

UART(Universal Asynchronous Receiver/Transmitter)是一种异步串行通信接口,常用于嵌入式系统中。其核心组成包括发送器(Transmitter)、接收器(Receiver)和波特率发生器(Baud Rate Generator)。

组件 功能描述
发送器 将并行数据转换为串行数据并输出
接收器 接收串行数据并转换为并行数据
波特率发生器 控制通信速率,确保收发双方同步

UART通信通常使用三根线:TXD(发送数据)、RXD(接收数据)和GND(地线)。其通信方式为异步,不需要共享时钟信号,依靠波特率对齐数据传输。

2.1.2 USB转串口适配器的工作原理

USB转串口适配器内部包含一个桥接芯片,如FTDI的FT232、Prolific的PL2303或Silicon Labs的CP2102等。这些芯片负责将USB信号转换为UART信号。

graph TD
    A[Android设备USB接口] --> B[USB转串口适配器]
    B --> C[桥接芯片]
    C --> D[TXD/RXD/GND]
    D --> E[外部串口设备]

适配器通过USB接口与Android设备连接,在驱动程序的支持下被识别为一个串口设备。例如,在Linux内核中,设备节点通常为 /dev/ttyUSB0 或 /dev/ttyACM0 。

2.2 Android设备与USB串口设备连接方式

在Android中连接USB串口设备,通常需要启用USB Host模式,并通过UsbManager API 获取设备信息。这一过程是建立串口通信链路的关键步骤。

2.2.1 USB Host模式的配置与使用

从Android 3.1(API 12)开始,系统支持USB Host模式,允许设备作为USB主机连接外部设备。启用该功能需在 AndroidManifest.xml 中添加以下权限:

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

在Java代码中,使用 UsbManager 类获取USB服务,并枚举连接的设备:

UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
HashMap<String, UsbDevice> deviceList = usbManager.getDeviceList();
for (UsbDevice device : deviceList.values()) {
    Log.d("USB", "设备名称:" + device.getDeviceName());
    Log.d("USB", "设备ID:" + device.getDeviceId());
}

代码解析:

  • 第1行:获取系统的USB服务实例。
  • 第2行:获取所有已连接的USB设备列表。
  • 第3-5行:遍历设备并输出设备名称与ID,用于识别目标串口设备。

此外,应用还需请求USB设备权限:

PendingIntent permissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0);
usbManager.requestPermission(device, permissionIntent);

参数说明:

  • ACTION_USB_PERMISSION :自定义广播Action,用于接收权限授予结果。
  • PendingIntent :用于回调接收权限授予状态。

2.2.2 使用UsbManager获取设备信息

在获取到USB设备后,需要进一步解析设备接口与端点信息。例如,查找串口通信所需的输入输出端点:

UsbInterface usbInterface = device.getInterface(0);
UsbEndpoint inEndpoint = null;
UsbEndpoint outEndpoint = null;

for (int i = 0; i < usbInterface.getEndpointCount(); i++) {
    UsbEndpoint endpoint = usbInterface.getEndpoint(i);
    if (endpoint.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK) {
        if (endpoint.getDirection() == UsbConstants.USB_DIR_IN) {
            inEndpoint = endpoint;
        } else {
            outEndpoint = endpoint;
        }
    }
}

代码解析:

  • 第1行:获取设备的第0个接口。
  • 第2-6行:遍历接口的端点,筛选出BULK传输类型的输入与输出端点。
  • USB_DIR_IN 表示输入端点,用于接收数据; USB_DIR_OUT 表示输出端点,用于发送数据。

2.3 UART通信参数配置

UART通信的成功与否,取决于通信参数的正确配置。常见的参数包括波特率、数据位、停止位和校验位等。错误的配置将导致通信失败或数据乱码。

2.3.1 波特率、数据位、停止位和校验位设置

UART通信的参数通常包括:

参数 常见值 描述
波特率 9600, 115200 每秒传输的符号数
数据位 5~8位 每帧数据的位数
停止位 1, 1.5, 2 标志数据帧结束的位数
校验位 None, Even, Odd 用于校验数据完整性的位

例如,配置为波特率115200、8数据位、1停止位、无校验位的通信方式,是常见设置之一。

2.3.2 GoogleSerialPort库实现串口配置实例

GoogleSerialPort是一个开源的串口通信库,封装了底层串口配置逻辑。使用该库可以快速实现串口初始化。

SerialPort serialPort = new SerialPort(new File("/dev/ttyUSB0"), 115200, 0);
InputStream inputStream = serialPort.getInputStream();
OutputStream outputStream = serialPort.getOutputStream();

代码解析:

  • 第1行:创建SerialPort实例,指定设备路径 /dev/ttyUSB0 、波特率115200、标志位0。
  • 第2-3行:获取输入流与输出流,用于后续数据读写操作。

参数说明:

  • 设备路径:根据实际设备挂载路径设定,通常为 /dev/ttyUSB* 或 /dev/ttyACM* 。
  • 波特率:需与外部设备一致。
  • 标志位:通常为0,表示默认配置。

2.4 串口通信中的异常处理

在实际开发中,串口通信可能会遇到设备未连接、断开、数据读写失败等问题。合理地处理这些异常,可以提升应用的健壮性与用户体验。

2.4.1 设备未连接或断开的处理机制

在打开串口时,可能因设备未连接或路径错误导致 IOException :

try {
    serialPort = new SerialPort(new File("/dev/ttyUSB0"), 115200, 0);
} catch (IOException e) {
    Log.e("Serial", "串口打开失败:" + e.getMessage());
    Toast.makeText(context, "请连接串口设备", Toast.LENGTH_SHORT).show();
}

建议处理机制:

  • 捕获 IOException ,提示用户检查设备连接。
  • 定期轮询设备状态,防止连接中断后程序崩溃。

2.4.2 数据传输错误的检测与恢复

在数据读写过程中,可能因缓冲区溢出、断开连接等原因导致异常。建议使用 try-catch 包裹读写逻辑,并实现重连机制:

private void readData() {
    byte[] buffer = new byte[1024];
    int len;
    try {
        while ((len = inputStream.read(buffer)) > 0) {
            // 处理接收到的数据
            processReceivedData(buffer, len);
        }
    } catch (IOException e) {
        Log.e("Serial", "数据读取失败:" + e.getMessage());
        reconnectSerialPort(); // 重新连接串口
    }
}

逻辑说明:

  • 使用循环读取数据,直到返回长度为0或发生异常。
  • 捕获 IOException 后调用 reconnectSerialPort() 尝试重新连接设备,保障通信连续性。

本章从硬件接口的基础知识入手,深入讲解了UART与USB转串口适配器的工作原理,接着通过代码示例演示了Android设备如何连接串口设备、配置通信参数,并处理通信过程中的异常情况。通过本章的学习,开发者应能够掌握从硬件识别到数据交互的完整流程,为后续实现温度传感器数据采集等应用打下坚实基础。

3. 温度传感器数据实时读取与监控

在Android平台上实现对温度传感器的实时数据读取与监控,是嵌入式开发中的关键任务之一。本章将深入探讨如何解析传感器输出的通信协议、构建高效的数据采集线程模型,并通过UI动态展示实时温度数据以及趋势图。通过本章内容,读者将掌握从串口获取原始数据到最终可视化展示的完整流程。

3.1 温度传感器通信协议分析

温度传感器通过串口发送数据时,通常采用ASCII或二进制格式进行通信。不同的传感器厂商可能采用不同的数据帧结构和协议格式,因此在进行数据采集前,必须对通信协议进行详细分析。

3.1.1 常见温度传感器输出格式(ASCII与二进制)

温度传感器输出的数据格式主要包括以下两种:

格式类型 特点 适用场景
ASCII 可读性强,适合调试,效率较低 简单的温度监测设备
二进制 数据紧凑,传输效率高,需解析 高频采样、低延迟系统

例如,某型号传感器输出的ASCII格式数据如下:

T:25.5C

而对应的二进制格式可能为:

0x00 0x00 0x80 0x41

该二进制值表示一个32位浮点数,转换后为25.5。

3.1.2 协议解析的基本流程与数据帧结构

无论采用哪种格式,解析流程通常包括以下步骤:

graph TD
    A[打开串口连接] --> B[读取原始数据]
    B --> C{判断数据格式}
    C -->|ASCII| D[按行读取并解析]
    C -->|Binary| E[按字节读取并解析]
    D --> F[提取温度值]
    E --> F
    F --> G[更新UI或保存数据]

数据帧结构通常包括:

  • 起始符 :如“T:”
  • 数据段 :如“25.5C”或4字节浮点数
  • 结束符 :如换行符 \n 或 \r\n

3.2 数据采集线程的构建

为了保证应用的响应性和数据采集的稳定性,串口数据的读取必须在独立线程中进行。Android主线程不能长时间阻塞,否则会导致ANR(Application Not Responding)错误。

3.2.1 独立线程处理串口读取任务

以下是一个使用 Thread 进行串口读取的示例代码:

public class SerialReaderThread extends Thread {
    private InputStream mInputStream;
    private boolean isRunning = true;

    public SerialReaderThread(InputStream inputStream) {
        this.mInputStream = inputStream;
    }

    @Override
    public void run() {
        byte[] buffer = new byte[1024];
        int bytes;

        while (isRunning) {
            try {
                bytes = mInputStream.read(buffer); // 从串口读取数据
                if (bytes > 0) {
                    String data = new String(buffer, 0, bytes); // 转换为字符串
                    // 通知主线程更新UI
                    Message msg = Message.obtain();
                    msg.obj = data;
                    mHandler.sendMessage(msg);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void stopThread() {
        isRunning = false;
    }
}

代码逻辑分析:

  1. 构造函数接收 InputStream :用于读取串口数据。
  2. run() 方法中循环读取数据 :使用 read() 阻塞式读取。
  3. 将数据转换为字符串后通过Handler发送给主线程 :避免在子线程中更新UI。
  4. 提供 stopThread() 方法用于停止线程 :防止线程泄露。

3.2.2 使用Handler机制更新UI数据

在Android中,UI更新必须在主线程中完成。因此需要使用 Handler 来传递数据:

private Handler mHandler = new Handler(Looper.getMainLooper()) {
    @Override
    public void handleMessage(Message msg) {
        String receivedData = (String) msg.obj;
        textViewTemperature.setText(receivedData); // 更新UI
    }
};

参数说明:

  • Looper.getMainLooper() :确保Handler绑定到主线程的消息队列。
  • textViewTemperature.setText() :假设界面上有一个 TextView 用于显示温度数据。

3.3 实时数据监控与可视化

为了更直观地展示温度变化趋势,我们可以使用图表库进行数据可视化,如MPAndroidChart、HelloCharts等。

3.3.1 在Android界面上动态显示温度数据

在XML中添加一个 TextView 用于显示实时温度:

<TextView
    android:id="@+id/tv_temperature"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="温度: 0.0°C"
    android:textSize="24sp"
    android:layout_centerInParent="true"/>

在Java代码中绑定该控件并更新:

TextView textViewTemperature = findViewById(R.id.tv_temperature);

结合前面的 Handler 机制,实现温度数据的动态更新。

3.3.2 使用图表库实现数据趋势展示

以MPAndroidChart为例,展示温度变化曲线:

LineChart lineChart = findViewById(R.id.lineChart);
ArrayList<Entry> entries = new ArrayList<>();
LineDataSet dataSet = new LineDataSet(entries, "温度变化");
LineData lineData = new LineData(dataSet);
lineChart.setData(lineData);
lineChart.invalidate(); // 刷新图表

添加数据点:

int x = 0;
while (isRunning) {
    float temperature = getTemperature(); // 获取当前温度
    entries.add(new Entry(x++, temperature));
    lineData.notifyDataChanged();
    lineChart.notifyDataSetChanged();
    lineChart.invalidate();
    try {
        Thread.sleep(1000); // 每秒更新一次
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

图表配置建议:

  • 设置X轴、Y轴标签
  • 设置动画效果
  • 设置曲线颜色与样式
graph LR
    A[数据采集] --> B[解析温度值]
    B --> C[更新TextView]
    C --> D[添加到图表数据集]
    D --> E[刷新图表]

3.4 本章总结

通过本章的学习,我们掌握了如何:

  • 分析温度传感器的通信协议(ASCII与二进制)
  • 构建独立线程读取串口数据,并通过Handler机制安全更新UI
  • 使用图表库实现实时温度数据的趋势展示

这些技能不仅适用于温度传感器,也适用于其他类型的串口设备数据采集和监控。在后续章节中,我们将进一步探讨如何优化数据解析、提升读取效率以及实现更复杂的定时任务机制。

4. 串口数据解析(ASCII/二进制协议)

在Android串口通信中,正确解析从串口设备接收到的数据是实现稳定通信的关键步骤之一。串口通信中传输的数据通常以两种格式存在:ASCII文本格式和二进制格式。本章将深入探讨这两种协议的解析方法,包括如何识别有效数据、处理字节流、字节序问题以及实现数据校验机制。通过本章的学习,开发者将掌握在Android平台上处理串口数据的完整解析流程。

4.1 ASCII协议解析方法

ASCII协议常用于调试和简单设备的数据通信,其可读性强、易于调试。但在实际应用中,如何高效提取有效信息并避免误解析是关键。

4.1.1 文本格式数据的识别与提取

ASCII格式的串口数据通常以字符串形式传输,例如:

TEMP:25.5C\r\n

该格式中, TEMP: 表示数据标识, 25.5 为温度值, C 表示单位, \r\n 为换行符。

在Android中,可以通过 BufferedReader 或 InputStreamReader 逐行读取串口输入流:

BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = reader.readLine()) != null) {
    // 处理每一行数据
}

代码解析:

  • inputStream 是从串口打开的输入流。
  • InputStreamReader 将字节流转换为字符流。
  • BufferedReader.readLine() 会阻塞等待换行符( \n 或 \r\n )作为行分隔符。

4.1.2 使用正则表达式匹配有效数据

为了从字符串中提取出具体的数值信息,可以使用正则表达式(Regular Expression)进行匹配:

String data = "TEMP:25.5C";
Pattern pattern = Pattern.compile("TEMP:(\\d+\\.\\d+)C");
Matcher matcher = pattern.matcher(data);

if (matcher.find()) {
    String tempValue = matcher.group(1);
    Log.d("SerialPort", "Temperature: " + tempValue);
}

代码分析:

  • Pattern.compile("TEMP:(\\d+\\.\\d+)C") :定义匹配模式,其中 \\d+ 匹配数字, \\. 匹配小数点。
  • matcher.find() :检查字符串中是否存在匹配项。
  • matcher.group(1) :提取第一个括号内的内容,即温度数值。

优化建议:

  • 可以设置多个正则表达式以支持不同格式的协议。
  • 在数据接收线程中引入缓存机制,避免因数据不完整导致正则匹配失败。

4.2 二进制协议解析技巧

与ASCII协议相比,二进制协议在数据传输效率和安全性方面具有明显优势,尤其适用于高速、大数据量的通信场景。但其解析过程相对复杂,需要处理字节流拆分、字节序转换等问题。

4.2.1 字节流的处理与数据拆分

二进制数据通常是一组连续的字节流( byte[] ),例如一个温度传感器返回的数据结构如下:

字段 类型 长度(字节)
起始标志 byte 1
温度值 float 4
校验和 short 2

假设我们收到的字节数组为:

byte[] buffer = new byte[]{(byte)0xAA, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34};

我们需要从该字节数组中提取出温度值。可以使用 ByteBuffer 来解析:

ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // 设置字节序

byte startByte = byteBuffer.get(); // 读取起始字节
float temperature = byteBuffer.getFloat(); // 读取4字节的float
short checksum = byteBuffer.getShort(); // 读取校验和

代码分析:

  • ByteBuffer.wrap(buffer) :将字节数组包装成 ByteBuffer 。
  • byteBuffer.order(...) :设置字节顺序(大端或小端)。
  • get() 、 getFloat() 、 getShort() :依次读取指定长度的数据。

4.2.2 大端与小端字节序的处理方法

字节序决定了多字节数据在内存中的存储顺序:

  • 大端模式(Big Endian) :高位字节在前,如网络协议常用。
  • 小端模式(Little Endian) :低位字节在前,如Windows系统常用。

Android默认使用的是小端模式,但某些硬件设备可能采用大端模式,因此在解析时必须明确设置:

byteBuffer.order(ByteOrder.BIG_ENDIAN); // 使用大端模式

示例说明:

假设一个整数 0x12345678 在内存中的存储方式:

字节序 存储顺序
大端 12 34 56 78
小端 78 56 34 12

优化建议:

  • 在协议文档中明确字节序,避免硬编码。
  • 使用 DataInputStream 配合 InputStream 读取原始字节流时,注意其默认不支持设置字节序,需手动转换。

4.3 数据校验机制实现

为了确保数据的完整性与正确性,在串口通信中通常会引入校验机制,其中最常见的是CRC校验。

4.3.1 CRC校验算法的应用

CRC(Cyclic Redundancy Check)是一种广泛使用的数据校验算法,具有较高的错误检测能力。

以下是一个简单的CRC16校验实现:

public static int crc16(byte[] data) {
    int crc = 0xFFFF;
    for (int i = 0; i < data.length; i++) {
        crc ^= (data[i] & 0x00FF);
        for (int j = 0; j < 8; j++) {
            if ((crc & 0x0001) != 0) {
                crc >>= 1;
                crc ^= 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }
    return crc;
}

参数说明:

  • data :待校验的字节数组。
  • crc :初始值设为0xFFFF。
  • 0xA001 :是CRC-16/IBM标准的生成多项式。

使用示例:

byte[] payload = new byte[]{(byte)0xAA, 0x12, 0x34, 0x56, 0x78};
int calculatedCRC = crc16(payload);
if (calculatedCRC == receivedCRC) {
    Log.d("SerialPort", "数据校验通过");
} else {
    Log.e("SerialPort", "数据校验失败");
}

4.3.2 校验失败时的数据丢弃与重传策略

当校验失败时,应采取以下策略:

  • 丢弃当前数据帧 :避免错误数据影响后续处理。
  • 记录错误日志 :便于后续调试与问题追踪。
  • 重传请求机制 :在协议支持的情况下,向设备发送重传指令。
graph TD
    A[接收数据] --> B{校验是否通过?}
    B -- 是 --> C[处理数据]
    B -- 否 --> D[丢弃数据]
    D --> E[发送重传请求]
    E --> F[重新接收数据]

流程图说明:

  • 接收到数据后首先进行校验。
  • 若校验失败,丢弃当前帧并请求重传。
  • 若校验成功,继续后续数据处理。

优化建议:

  • 可引入重试次数限制,避免无限重传。
  • 结合日志系统记录错误帧内容,便于后期分析。
  • 在高并发通信中,可使用线程池处理校验任务,提高效率。

总结与扩展

本章系统地介绍了Android串口通信中ASCII与二进制协议的解析方法,并深入探讨了数据校验机制的实现。通过合理使用正则表达式、字节缓冲区、CRC校验等技术,开发者可以构建稳定高效的串口通信模块。下一章将介绍Java中 BufferedReader 与 DataInputStream 的使用,进一步优化串口数据的读取与处理方式。

5. Java中BufferedReader与DataInputStream使用

在Android串口通信开发中,数据读取是整个通信流程的核心环节之一。为了高效、准确地读取串口数据,Java提供了两种常用的数据流处理类: BufferedReader 和 DataInputStream 。它们分别适用于处理文本格式(ASCII)和二进制格式的数据。本章将深入探讨这两种类在串口通信中的实际应用,结合具体代码示例与流程分析,帮助开发者掌握高效的串口数据读取方式。

5.1 BufferedReader在串口通信中的作用

BufferedReader 是 Java IO 包中用于读取字符输入流的高效类,尤其适合按行读取文本格式的数据。在串口通信中,如果传感器或外部设备发送的是 ASCII 格式数据(如以换行符分隔的字符串), BufferedReader 是一个理想的选择。

5.1.1 按行读取文本格式数据

以下是一个使用 BufferedReader 读取串口文本数据的示例代码:

InputStream inputStream = serialPort.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));

String line;
while ((line = reader.readLine()) != null) {
    Log.d("SerialPort", "Received: " + line);
    // 处理接收到的文本数据
}
代码逻辑分析
  • InputStream :获取串口输入流,用于接收设备发送的数据。
  • InputStreamReader :将字节流转换为字符流,以便进行文本处理。
  • BufferedReader :封装字符流,提供高效的按行读取功能。
  • readLine() :该方法会阻塞直到读取到一行数据或流结束。返回值为 null 表示流结束或发生错误。
参数说明
参数 类型 含义
inputStream InputStream 串口输入流对象
reader BufferedReader 用于读取文本的缓冲字符流
line String 每次读取的一行数据

5.1.2 处理缓冲区大小与读取效率问题

BufferedReader 内部默认使用 8KB 缓冲区,但在处理高频串口数据时,可能需要根据实际数据流频率调整缓冲区大小以提升读取效率。

自定义缓冲区大小示例:
BufferedReader reader = new BufferedReader(
    new InputStreamReader(inputStream), 1024 * 32); // 设置32KB缓冲区
性能对比表:
缓冲区大小 读取频率(Hz) CPU 占用率 数据完整性
默认 8KB 中等 中等 良好
16KB 高 稍高 更稳定
32KB 很高 较高 最佳

建议 :在串口通信速率较高(如波特率 115200)的情况下,适当增大缓冲区可以有效减少 readLine() 的阻塞次数,从而提升整体读取效率。

5.2 DataInputStream的二进制数据读取

与文本格式不同,许多工业设备和传感器会通过串口发送二进制数据。这类数据通常包含整型、浮点型、短整型等多种基本数据类型,需要使用 DataInputStream 进行解析。

5.2.1 读取整型、浮点型等基本数据类型

下面是一个使用 DataInputStream 读取二进制数据的代码示例:

InputStream inputStream = serialPort.getInputStream();
DataInputStream dataInputStream = new DataInputStream(inputStream);

while (true) {
    try {
        int temperature = dataInputStream.readInt(); // 读取4字节整型
        float humidity = dataInputStream.readFloat(); // 读取4字节浮点型
        Log.d("SerialPort", "Temperature: " + temperature + "°C, Humidity: " + humidity + "%");
    } catch (IOException e) {
        e.printStackTrace();
        break;
    }
}
代码逻辑分析
  • readInt() :读取4个字节并将其转换为一个 int 类型值。
  • readFloat() :读取4个字节并将其转换为一个 float 类型值。
  • IOException :当设备断开或数据格式不匹配时抛出异常。
数据类型与字节数对应关系:
Java 类型 字节数 对应 read 方法
boolean 1 readBoolean()
byte 1 readByte()
char 2 readChar()
short 2 readShort()
int 4 readInt()
float 4 readFloat()
long 8 readLong()
double 8 readDouble()

5.2.2 与字节缓冲区配合使用提升效率

在高速数据流场景下,直接使用 DataInputStream 可能会导致频繁的系统调用和上下文切换,从而影响性能。为优化这一点,可以结合 ByteArrayBuffer 或 ByteBuffer 使用。

示例:使用 ByteBuffer 提前缓冲数据
byte[] buffer = new byte[1024];
InputStream inputStream = serialPort.getInputStream();

while (true) {
    int bytesRead = inputStream.read(buffer);
    if (bytesRead > 0) {
        ByteBuffer byteBuffer = ByteBuffer.wrap(buffer, 0, bytesRead);
        while (byteBuffer.remaining() >= 4) {
            int value = byteBuffer.getInt();
            Log.d("BinaryData", "Read Int: " + value);
        }
    }
}
流程图:ByteBuffer 读取流程
graph TD
    A[获取输入流] --> B[创建字节缓冲数组]
    B --> C[从流中读取数据到缓冲区]
    C --> D{是否读取成功?}
    D -- 是 --> E[使用ByteBuffer封装数据]
    E --> F[按需读取基本数据类型]
    F --> G[更新UI或处理数据]
    D -- 否 --> H[结束或重试]

优势 :相比直接使用 DataInputStream ,结合 ByteBuffer 可以更灵活地控制字节顺序(大端/小端)、数据偏移和数据类型转换,特别适合处理结构化二进制协议。

5.3 串口数据读取线程优化实践

在 Android 中,串口通信属于 IO 操作,应当避免在主线程中进行。否则,可能导致 ANR(Application Not Responding)异常。因此,构建一个高效、稳定的读取线程机制是串口通信开发中的关键。

5.3.1 多线程环境下读取稳定性提升

为确保数据读取的稳定性,建议使用独立线程处理串口读取任务,并结合线程池或 HandlerThread 提升资源利用率。

示例:使用 HandlerThread 实现稳定读取
HandlerThread handlerThread = new HandlerThread("SerialReader");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());

handler.post(new Runnable() {
    @Override
    public void run() {
        InputStream inputStream = serialPort.getInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
        String line;
        try {
            while ((line = reader.readLine()) != null) {
                // 将数据通过Handler发送到主线程
                Message msg = Message.obtain();
                msg.obj = line;
                mainHandler.sendMessage(msg);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});
优化分析
  • 使用 HandlerThread 创建专属线程,避免主线程阻塞。
  • 通过 Handler 与主线程交互,确保 UI 更新安全。
  • 可扩展为多个线程并发处理不同串口设备。

5.3.2 避免阻塞主线程的实现方案

在串口通信中, readLine() 和 read() 方法都会阻塞线程直到有数据可读。为避免主线程阻塞,可采用以下策略:

  • 使用异步任务(AsyncTask) :适用于短时读取任务。
  • 使用 HandlerThread + Looper :适用于长期运行的读取任务。
  • 设置超时机制 :通过 Socket 或自定义封装实现超时读取。
示例:设置超时读取机制(伪代码)
class TimeoutInputStream extends InputStream {
    private InputStream inner;
    private long timeout;

    public TimeoutInputStream(InputStream inner, long timeout) {
        this.inner = inner;
        this.timeout = timeout;
    }

    @Override
    public int read() throws IOException {
        // 实现超时检测逻辑
        if (hasTimedOut()) throw new IOException("Read timeout");
        return inner.read();
    }
}

建议 :在工业现场或设备不稳定的情况下,建议设置合理的超时机制,避免程序长时间挂起。

小结

本章深入探讨了 BufferedReader 与 DataInputStream 在 Android 串口通信中的应用,分别适用于处理 ASCII 和二进制格式数据。通过合理使用缓冲机制、线程模型与超时控制,可以显著提升串口数据读取的效率与稳定性。在下一章中,我们将进一步探讨如何使用 Handler 和 Runnable 实现定时任务,以支持周期性数据采集与更新。

6. 定时任务实现(Handler/Runnable)

在Android串口通信开发中,定时任务的实现对于数据轮询、状态更新、界面刷新等场景至关重要。尤其在需要周期性读取温度传感器数据、控制设备状态或更新UI时,合理使用定时任务机制可以显著提升程序的稳定性与响应性。本章将深入解析Android中基于 Handler 与 Runnable 的定时任务实现机制,并结合串口通信的实际应用场景,展示如何构建高效、稳定的定时任务流程。

6.1 Android中的定时任务机制概述

Android系统提供了多种定时任务的实现方式,其中 Handler 与 Runnable 组合是最为常见且轻量级的一种。它们适用于UI线程中的周期性操作,如界面刷新、数据更新等。

6.1.1 Handler与Looper的基本工作原理

在Android中, Handler 与 Looper 共同构成了线程间通信的核心机制。每个 Handler 都与一个 Looper 绑定, Looper 负责从消息队列中取出消息并交给 Handler 处理。

  • Looper :负责在某个线程中循环处理消息队列。
  • Handler :用于发送消息( Message )和运行任务( Runnable )到消息队列,并在消息到达时执行对应的逻辑。

在主线程中,系统默认已经创建了 Looper ,因此可以直接创建 Handler 对象进行消息处理。而在子线程中使用 Handler ,则需要手动调用 Looper.prepare() 和 Looper.loop() 来初始化消息循环。

示例代码:主线程中使用Handler
Handler handler = new Handler(Looper.getMainLooper());

Runnable updateTask = new Runnable() {
    @Override
    public void run() {
        // 执行UI更新或其他操作
        Log.d("TimerTask", "执行定时任务");
        // 再次延迟发送,实现周期性执行
        handler.postDelayed(this, 1000); // 每隔1秒执行一次
    }
};

// 启动任务
handler.post(updateTask);
代码解析:
  • Handler 绑定主线程的 Looper ,确保任务在主线程执行。
  • Runnable 中调用 handler.postDelayed(this, 1000) 实现循环执行。
  • 这种方式适用于UI更新等需要在主线程执行的场景。

6.1.2 Runnable任务的执行流程

Runnable 是一个接口,定义了 run() 方法。当它被提交给 Handler 或线程执行时, run() 方法将被调用。

在 Handler 机制中, Runnable 是作为消息的一部分被封装进 MessageQueue 中,由 Looper 取出后执行。其执行流程如下:

  1. Handler.post(Runnable) 将任务加入消息队列;
  2. Looper 从队列中取出任务;
  3. Handler 执行 Runnable.run() 方法;
  4. 任务完成后可再次调用 postDelayed() 形成循环。

6.2 使用Handler实现周期性数据更新

在串口通信中,定时任务常用于轮询设备状态、采集传感器数据等。本节将以温度传感器为例,演示如何使用 Handler 与 Runnable 实现周期性读取串口数据,并更新UI。

6.2.1 定时轮询串口数据的实现

以下是一个周期性读取串口数据的示例:

Handler serialHandler = new Handler(Looper.getMainLooper());

Runnable pollDataTask = new Runnable() {
    @Override
    public void run() {
        if (serialPort != null && serialPort.isConnected()) {
            byte[] buffer = new byte[128];
            int size = serialPort.read(buffer); // 读取串口数据
            if (size > 0) {
                String data = new String(buffer, 0, size);
                Log.d("SerialData", "接收到数据: " + data);
                // 解析数据并更新UI
                updateTemperatureUI(data);
            }
        }

        // 每隔500毫秒执行一次
        serialHandler.postDelayed(this, 500);
    }
};

// 启动轮询任务
serialHandler.post(pollDataTask);
代码解析:
  • serialPort.read(buffer) :从串口读取原始数据;
  • updateTemperatureUI(data) :将数据解析后更新界面;
  • postDelayed(this, 500) :实现每500ms轮询一次。
优化建议:
  • 可根据传感器数据发送频率调整轮询间隔;
  • 若串口通信使用独立线程,建议使用 HandlerThread 配合 Handler ,避免主线程阻塞。

6.2.2 UI界面数据刷新的同步机制

由于Android不允许在非UI线程中直接更新界面,因此在串口通信线程中获取数据后,必须通过 Handler 将数据传递回主线程。

示例:跨线程更新UI
Handler uiHandler = new Handler(Looper.getMainLooper());

new Thread(() -> {
    while (isRunning) {
        byte[] data = readFromSerialPort(); // 子线程中读取数据
        String temperature = parseTemperature(data);
        uiHandler.post(() -> {
            textViewTemperature.setText(temperature); // 在主线程更新UI
        });
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}).start();
代码逻辑说明:
  • 子线程持续读取串口数据;
  • 解析出温度值后,通过 uiHandler.post() 将更新操作提交到主线程;
  • 确保UI操作线程安全,避免 CalledFromWrongThreadException 异常。

6.3 定时任务的优化与管理

虽然 Handler + Runnable 机制简单易用,但在实际开发中仍需注意资源管理、内存泄漏等问题。本节将介绍如何优化定时任务的执行与管理。

6.3.1 防止内存泄漏与任务重复执行

如果 Runnable 持有外部对象(如Activity、Context等)的引用,可能导致内存泄漏。因此,应使用静态内部类或弱引用( WeakReference )来避免强引用导致的内存问题。

示例:使用弱引用避免内存泄漏
private static class TemperatureTask implements Runnable {
    private final WeakReference<MainActivity> activityReference;

    public TemperatureTask(MainActivity activity) {
        activityReference = new WeakReference<>(activity);
    }

    @Override
    public void run() {
        MainActivity activity = activityReference.get();
        if (activity != null && !activity.isFinishing()) {
            // 安全执行UI更新
            activity.updateTemperature();
        }
        // 延迟执行
        activity.handler.postDelayed(this, 500);
    }
}
优化说明:
  • 使用 WeakReference 防止Activity被 Runnable 强引用;
  • 判断 activity != null 和 !activity.isFinishing() 确保Activity处于活跃状态;
  • 避免任务在Activity销毁后继续执行。

6.3.2 动态控制任务的启动与停止

为了提高程序灵活性,定时任务应支持动态控制,如启动、暂停、停止等。可以通过 removeCallbacks() 方法取消任务。

示例:动态控制任务生命周期
private TemperatureTask temperatureTask;
private Handler handler = new Handler(Looper.getMainLooper());

public void startTask() {
    if (temperatureTask == null) {
        temperatureTask = new TemperatureTask(this);
    }
    handler.post(temperatureTask);
}

public void stopTask() {
    if (temperatureTask != null) {
        handler.removeCallbacks(temperatureTask);
        temperatureTask = null;
    }
}
使用说明:
  • startTask() :启动定时任务;
  • stopTask() :清除任务,防止内存泄漏;
  • 适用于用户切换页面、关闭设备连接等场景。

表格:不同定时任务方式对比

方式 适用场景 是否主线程安全 是否支持周期执行 是否支持延迟执行
Handler + Runnable 简单周期任务、UI更新 ✅ ✅ ✅
Timer + TimerTask Java原生定时任务 ❌ ✅ ✅
ScheduledExecutorService 多线程调度、复杂任务 ✅(需切换线程) ✅ ✅
AlarmManager 系统级定时唤醒 ✅ ✅ ✅

mermaid流程图:Handler定时任务执行流程

graph TD
    A[创建Handler与Runnable] --> B{是否启动任务}
    B -- 是 --> C[执行Runnable.run()]
    C --> D[读取串口数据]
    D --> E[解析数据]
    E --> F[更新UI]
    F --> G[再次postDelayed]
    G --> C
    B -- 否 --> H[等待启动]

小结

本章深入讲解了Android中基于 Handler 与 Runnable 的定时任务机制,并结合串口通信场景,展示了如何实现周期性读取数据、动态控制任务生命周期、防止内存泄漏等关键操作。通过合理使用 Handler 机制,开发者可以高效地实现定时轮询、数据更新与界面刷新,为后续使用更复杂的定时调度器(如 ScheduledExecutorService )打下坚实基础。

7. 定时任务实现(ScheduledExecutorService)

在Android串口通信中,定时任务机制是实现周期性数据采集、指令发送以及系统状态轮询的关键技术。与传统的 Handler/Runnable 模式相比, ScheduledExecutorService 提供了更为灵活和强大的线程调度能力,特别适合处理并发任务和需要精确控制执行周期的场景。本章将从基础使用出发,结合串口通信的实际需求,深入讲解如何构建和优化基于 ScheduledExecutorService 的定时任务系统。

7.1 ScheduledExecutorService基础使用

ScheduledExecutorService 是 Java 并发包 java.util.concurrent 中的一个接口,用于管理多个线程并安排任务在指定时间或周期性执行。

7.1.1 线程池的创建与任务调度

创建一个固定大小的线程池并通过 ScheduledExecutorService 安排任务的示例如下:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

public class SerialTaskScheduler {
    private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);

    public void startPeriodicTask() {
        ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(() -> {
            // 执行周期性任务,例如发送串口指令
            System.out.println("定时任务执行中...");
        }, 0, 1, TimeUnit.SECONDS); // 初始延迟0秒,每隔1秒执行一次
    }

    public void shutdown() {
        scheduler.shutdown();
    }
}

参数说明:

  • scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) :
  • command :要执行的任务;
  • initialDelay :首次执行的延迟时间;
  • period :执行周期;
  • unit :时间单位(秒、毫秒等)。

7.1.2 固定延迟与周期性任务的执行方式

除了 scheduleAtFixedRate 方法,还可以使用 scheduleWithFixedDelay 实现任务之间固定延迟的调度:

scheduler.scheduleWithFixedDelay(() -> {
    // 每次任务执行完成后,等待固定延迟再执行下一次
    System.out.println("延迟任务执行中...");
}, 0, 2, TimeUnit.SECONDS);

区别:

方法名称 特点
scheduleAtFixedRate 保证任务按照固定周期执行,适用于对时间间隔要求严格的场景
scheduleWithFixedDelay 保证任务之间有固定延迟,适用于任务执行时间不固定的情况

7.2 在串口通信中使用定时任务机制

在实际串口通信应用中,定时任务常用于周期性地发送指令、轮询设备状态或采集数据。

7.2.1 定时发送指令与数据轮询

以向串口设备发送查询指令为例,使用 ScheduledExecutorService 每隔 500 毫秒发送一次查询命令:

public class SerialCommandSender {
    private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    private OutputStream outputStream;

    public void startSending(byte[] command) {
        scheduler.scheduleAtFixedRate(() -> {
            try {
                outputStream.write(command); // 向串口设备发送指令
                System.out.println("指令已发送");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }, 0, 500, TimeUnit.MILLISECONDS);
    }

    public void stopSending() {
        scheduler.shutdown();
    }
}

说明:

  • outputStream 是串口通信中获取的输出流;
  • 每次发送指令后等待下一次调度,形成定时轮询机制;
  • 可通过配置周期参数,控制轮询频率。

7.2.2 多任务并行执行与资源调度

若需同时执行多个定时任务(如:轮询指令、心跳包发送、状态检测),可创建多个线程池或使用固定大小线程池进行调度:

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3);

scheduler.scheduleAtFixedRate(this::sendHeartbeat, 0, 3, TimeUnit.SECONDS); // 心跳包
scheduler.scheduleAtFixedRate(this::pollData, 0, 1, TimeUnit.SECONDS);      // 数据采集
scheduler.scheduleAtFixedRate(this::checkStatus, 0, 5, TimeUnit.SECONDS);   // 状态检测

资源调度建议:

  • 合理设置线程池大小,避免资源浪费;
  • 避免多个任务访问共享资源时的并发冲突,使用 synchronized 或并发集合类。

7.3 与UI线程的协同机制

在 Android 应用中,后台线程处理串口任务后,需要将结果反馈到 UI 界面,这就需要跨线程通信机制。

7.3.1 使用Handler更新UI数据

通过 Handler 将子线程中获取的数据发送到主线程进行更新:

private Handler uiHandler = new Handler(Looper.getMainLooper());

public void startDataPolling() {
    ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    scheduler.scheduleAtFixedRate(() -> {
        String data = readFromSerialPort(); // 模拟读取串口数据
        Message msg = Message.obtain();
        msg.obj = data;
        uiHandler.sendMessage(msg); // 发送消息到主线程
    }, 0, 1000, TimeUnit.MILLISECONDS);
}

UI线程处理消息:

private Handler uiHandler = new Handler(Looper.getMainLooper()) {
    @Override
    public void handleMessage(Message msg) {
        String temperature = (String) msg.obj;
        textView.setText("当前温度:" + temperature + "℃"); // 更新UI
    }
};

7.3.2 跨线程数据通信的实现方式

除了 Handler ,还可以使用 LiveData 、 RxJava 或 runOnUiThread() 实现跨线程通信。以 runOnUiThread() 为例:

new Thread(() -> {
    while (isRunning) {
        String data = readSerialData();
        runOnUiThread(() -> {
            textView.setText("温度:" + data);
        });
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}).start();

对比建议:

方法 适用场景 优点 缺点
Handler 传统线程通信 简单易用,兼容性好 需手动管理Looper和Message
LiveData MVVM架构中数据绑定 自动生命周期感知 需配合ViewModel使用
RxJava 异步数据流处理 功能强大,代码简洁 学习成本较高
runOnUiThread 快速更新UI 简单直接 不适合复杂异步逻辑

在下一章中,我们将进一步探讨如何将串口通信与蓝牙模块结合,实现无线串口通信功能,敬请期待。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目是一个基于Android平台的串口通信应用,专为疫情期间体温监测设计。应用通过串口与温度传感器通信,实现体温数据的实时获取与展示,包含数据解析和1秒定时更新功能。项目使用Java开发,集成GoogleSerialPort库进行串口操作,并通过Handler或ScheduledExecutorService实现定时任务。适用于学习Android设备间通信与实时数据处理,是掌握串口通信核心技术的实用案例。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐