一、 问题解构与方案推演
针对在 Android Studio 中使用 Java 实现 LoRa 串口通信、数据解析及地图标定的需求,首先对系统架构进行分层解构。该系统本质上是一个典型的物联网网关应用,涉及底层硬件交互、中间层数据处理以及上层可视化展示。
| 核心模块 | 关键技术点 | 实现难点与解决方案 |
|---|---|---|
| 串口通信层 | Android SerialPort API、JNI 调用、读写线程管理 | Android 默认不支持直接串口操作,需通过 JNI 调用 C/C++ 库或使用开源库(如android-serialport-api)。需开启独立线程持续读取数据以防阻塞 UI 。 |
| 协议解析层 | 字节流处理、大小端转换、CRC 校验、帧头帧尾定位 | LoRa 传输的是二进制流,需根据自定义协议(如帧头 ` |
| 0xAA 0x55`)提取有效载荷,并将字节数组转换为经纬度浮点数 。 | ||
| 地图展示层 | 地图 SDK (高德/百度/Google)、Marker 管理、坐标转换 | 将解析出的经纬度转换为 SDK 的LatLng对象,并在地图上动态添加或更新覆盖物 。 |
二、 串口通信实现
在 Android 中实现串口通信,通常需要加载libserial_port.so库并通过 Java 类进行封装。以下是基于android-serialport-api的核心实现逻辑。
1. 配置串口参数
首先需要配置串口路径(如/dev/ttyS1或/dev/ttyUSB0)和波特率(如 9600)。这通常涉及到 Linux 底层驱动的调用 。
import android.util.Log; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; // 假设使用了 android-serialport-api 库 import android_serialport_api.SerialPort; public class LoraSerialManager { private static final String TAG = "LoraSerialManager"; private SerialPort mSerialPort; private OutputStream mOutputStream; private InputStream mInputStream; private ReadThread mReadThread; private boolean isRunning = false; // 初始化串口 public void initSerialPort(String path, int baudrate) { try { // 打开串口设备,获取输入输出流 mSerialPort = new SerialPort(new File(path), baudrate, 0); mOutputStream = mSerialPort.getOutputStream(); mInputStream = mSerialPort.getInputStream(); isRunning = true; // 开启读取线程 mReadThread = new ReadThread(); mReadThread.start(); Log.d(TAG, "串口初始化成功: " + path + " @ " + baudrate); } catch (IOException e) { e.printStackTrace(); Log.e(TAG, "串口初始化失败"); } } // 发送数据(可选,用于发送指令给LoRa模块) public void sendData(byte[] data) { try { if (mOutputStream != null) { mOutputStream.write(data); } } catch (IOException e) { e.printStackTrace(); } } // 关闭串口 public void closeSerialPort() { isRunning = false; if (mReadThread != null) { mReadThread.interrupt(); } if (mSerialPort != null) { mSerialPort.close(); } } }2. 多线程读取数据
串口读取是阻塞操作,必须在独立线程中进行。读取到的原始字节数据需要传递给解析模块 。
// 内部读取线程类 private class ReadThread extends Thread { @Override public void run() { super.run(); while (isRunning && !isInterrupted()) { try { if (mInputStream == null) return; // 简单的读取逻辑:每次读取一个字节或缓冲区 // 实际项目中建议使用缓冲区减少系统调用 byte[] buffer = new byte[1024]; int size = mInputStream.read(buffer); if (size > 0) { byte[] receivedData = new byte[size]; System.arraycopy(buffer, 0, receivedData, 0, size); // 将数据传递给解析器 parseData(receivedData); } } catch (IOException e) { e.printStackTrace(); } } } }三、 数据报文解析
LoRa 模块回传的数据通常是二进制流。假设协议格式为:帧头(2字节) + 经度(4字节 float) + 纬度(4字节 float) + 校验(1字节)。解析过程需要处理字节序(大端或小端)并验证数据完整性 。
import java.nio.ByteBuffer; import java.nio.ByteOrder; public void parseData(byte[] buffer) { // 定义协议常量 final byte FRAME_HEADER_1 = (byte) 0xAA; final byte FRAME_HEADER_2 = (byte) 0x55; final int FRAME_LENGTH = 11; // 2 + 4 + 4 + 1 // 简单的滑动窗口解析逻辑(实际生产环境需处理粘包/半包) for (int i = 0; i < buffer.length - FRAME_LENGTH + 1; i++) { // 1. 校验帧头 if (buffer[i] == FRAME_HEADER_1 && buffer[i + 1] == FRAME_HEADER_2) { // 2. 提取经纬度数据 (假设为小端模式 Little Endian) // 使用 ByteBuffer 方便转换 byte[] lonBytes = new byte[4]; byte[] latBytes = new byte[4]; System.arraycopy(buffer, i + 2, lonBytes, 0, 4); System.arraycopy(buffer, i + 6, latBytes, 0, 4); float longitude = ByteBuffer.wrap(lonBytes).order(ByteOrder.LITTLE_ENDIAN).getFloat(); float latitude = ByteBuffer.wrap(latBytes).order(ByteOrder.LITTLE_ENDIAN).getFloat(); // 3. 校验位验证 (此处省略具体 CRC 算法,仅作占位) // byte checksum = buffer[i + 10]; // if (verifyChecksum(...)) { ... } // 4. 有效数据回调 onLocationReceived(latitude, longitude); // 跳过已处理的数据 i += FRAME_LENGTH - 1; } } } // 数据解析成功后的回调接口 private void onLocationReceived(float lat, float lon) { Log.d("LoraParser", "收到坐标 -> Lat: " + lat + ", Lon: " + lon); // 切换到主线程更新 UI if (mContext instanceof MainActivity) { ((MainActivity) mContext).updateMapMarker(lat, lon); } }四、 地图标定实现
获取到经纬度后,使用地图 SDK(此处以高德地图为例)进行坐标标定。需要将解析出的坐标转换为地图 SDK 的LatLng对象,并添加Marker。
import com.amap.api.maps2d.AMap; import com.amap.api.maps2d.model.LatLng; import com.amap.api.maps2d.model.Marker; import com.amap.api.maps2d.model.MarkerOptions; public class MainActivity extends AppCompatActivity { private AMap aMap; private Marker currentMarker; // 在地图初始化后调用 private void initMap() { if (aMap == null) { aMap = mapView.getMap(); } } // 更新地图标记的方法(由 LoraSerialManager 回调调用) public void updateMapMarker(double lat, double lon) { runOnUiThread(new Runnable() { @Override public void run() { // 1. 构建坐标点 LatLng point = new LatLng(lat, lon); // 2. 如果已有标记,先移除(或移动位置) if (currentMarker != null) { currentMarker.setPosition(point); } else { // 3. 创建新标记选项 MarkerOptions markerOptions = new MarkerOptions() .position(point) .title("LoRa 目标位置") .draggable(true); // 4. 添加到地图 currentMarker = aMap.addMarker(markerOptions); } // 5. 移动地图视角到目标点 aMap.animateCamera(CameraUpdateFactory.newLatLngZoom(point, 15)); } }); } }五、 完整流程整合与注意事项
将上述模块整合,完整的业务流程如下:
权限申请:在
AndroidManifest.xml中添加串口访问权限。<!-- 针对较新的 Android 版本,可能需要处理运行时权限 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />初始化流程:
- App 启动,初始化地图 SDK。
- 打开串口(
/dev/ttyS1),启动读取线程。
数据流转:
- LoRa 模块接收到 GPS 数据 -> 通过 UART 发送给 Android。
- Android
ReadThread读取到字节流 ->parseData识别帧头0xAA 0x55。 - 提取中间 4 字节经度和 4 字节纬度 -> 转换为
float类型。 - 调用
MainActivity.updateMapMarker-> 在主线程更新Marker位置。
异常处理:
- 粘包处理:如果数据发送过快,串口可能一次读取多条报文。解析逻辑应具备缓存机制,将未处理完的字节存入缓冲区,与下一次读取的数据拼接 。
- 坐标系转换:LoRa 模块通常输出 WGS-84 坐标系,而国内地图(高德/百度)使用 GCJ-02 或 BD-09。直接标定会有偏移,需在解析后进行坐标系转换算法处理 。
通过以上步骤,即构建了一个完整的从底层硬件通信到上层可视化展示的 Android 物联网网关应用。
参考来源
- 新大陆物联网-Android实现网关功能-连接云平台并上传传感器数据-获取执行器指令并执行-Android网关开发-通信-数据上传云平台-JAVA原理讲解-免费云平台使用-竞赛2022国赛真题
- android串口通信实例
- 开源免费的手机版 LoRa App,演示和调试 LoRaWAN 数据的神器
- 从论文到代码:EasyControl核心组件Condition Injection LoRA原理解析
- LoRa 应用数据快速演示神器
- 安卓手机变身AI推理终端,Open-AutoGLM本地部署全解析