Android串口通信实战项目:温度传感器数据解析与定时更新
简介:本项目是一个基于Android平台的串口通信应用,专为疫情期间体温监测设计。应用通过串口与温度传感器通信,实现体温数据的实时获取与展示,包含数据解析和1秒定时更新功能。项目使用Java开发,集成GoogleSerialPort库进行串口操作,并通过Handler或ScheduledExecutorService实现定时任务。适用于学习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 )与串口设备进行数据交互。
实现串口通信的核心步骤包括:
- 打开串口设备文件;
- 配置串口参数(波特率、数据位、校验位、停止位);
- 启动读写线程进行数据收发;
- 异常处理与资源释放。
目前主流的串口通信库包括: 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;
}
}
代码逻辑分析:
- 构造函数接收
InputStream:用于读取串口数据。 -
run()方法中循环读取数据 :使用read()阻塞式读取。 - 将数据转换为字符串后通过Handler发送给主线程 :避免在子线程中更新UI。
- 提供
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 取出后执行。其执行流程如下:
-
Handler.post(Runnable)将任务加入消息队列; -
Looper从队列中取出任务; -
Handler执行Runnable.run()方法; - 任务完成后可再次调用
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 | 简单直接 | 不适合复杂异步逻辑 |
在下一章中,我们将进一步探讨如何将串口通信与蓝牙模块结合,实现无线串口通信功能,敬请期待。
简介:本项目是一个基于Android平台的串口通信应用,专为疫情期间体温监测设计。应用通过串口与温度传感器通信,实现体温数据的实时获取与展示,包含数据解析和1秒定时更新功能。项目使用Java开发,集成GoogleSerialPort库进行串口操作,并通过Handler或ScheduledExecutorService实现定时任务。适用于学习Android设备间通信与实时数据处理,是掌握串口通信核心技术的实用案例。
更多推荐
所有评论(0)