Home
avatar

叁石

Uniapp实现串口调试(安卓原生插件)

安卓原生开发uniapp插件,实现安卓设备的串口调试

基于插件源码开发

建议后期再独立创建,主要是几处配置需要修改,如果博主后续玩了再写文章给大家看~

此部分官方教程

不看就继续看下去

下载安卓插件开发包
官方最新源码

解压获取 UniPlugin-Hello-AS 目录使用 Android studio 打开

示例中本身集成了 uniplugin_module ,直接使用(先偷懒啦~)

引入串口操作相关包

module下build.gradle文件增加第三方串口包引入

感谢开源源码
dependencies {
    compileOnly 'com.gitee.sscl:SerialPortLibrary:1.0.7',
    //......
}

切记修改后,gradle同步一下

开发串口相关函数

module下TestModule类修改增加串口相关封装函数

package io.dcloud.uniplugin;

import android.util.Log;
import com.alibaba.fastjson.JSONObject;
import io.dcloud.feature.uniapp.annotation.UniJSMethod;
import io.dcloud.feature.uniapp.bridge.UniJSCallback;
import io.dcloud.feature.uniapp.common.UniModule;
import com.jackiepenghe.serialportlibrary.OnSerialPortDataChangedListener;
import com.jackiepenghe.serialportlibrary.SerialPortManager;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;


public class TestModule extends UniModule {

    String TAG = "TestModule";
    public static int REQUEST_CODE = 1000;

    public static byte[] hexStrToBinaryStr(String hexString) {
        if (hexString == null|| hexString.isEmpty()) {
            return null;
        }
        hexString = hexString.replaceAll(" ", "");
        int len = hexString.length();
        int index = 0;
        byte[] bytes = new byte[len / 2];
        while (index < len) {
            String sub = hexString.substring(index, index + 2);
            bytes[index/2] = (byte)Integer.parseInt(sub,16);
            index += 2;
        }
        return bytes;
    }
    /**
     * 获取设备信息
     *
     * @param callback
     */
    @UniJSMethod(uiThread = true)
    public void getDevices(UniJSCallback callback) {
        Log.i(TAG, "getDevices--");
        String[] allDevices = SerialPortManager.getAllDevices();
        String[] allDevicesPath = SerialPortManager.getAllDevicesPath();
        Log.i(TAG, "getDevices--" + Arrays.toString(allDevices));
        Log.i(TAG, "getDevices--" + Arrays.toString(allDevicesPath));
        if (callback != null) {
            JSONObject data = new JSONObject();
            data.put("allDevices", allDevices);
            data.put("allDevicesPath", allDevicesPath);
            callback.invoke(data);
            // callback.invokeAndKeepAlive(data);
        }
    }

    /**
     * 打开串口
     *
     * @param callback
     */
    @UniJSMethod(uiThread = true)
    public void openDevice(JSONObject options, UniJSCallback callback) {
        Log.i(TAG, "openDevice = " + options);
        if (SerialPortManager.isOpened()) {
            JSONObject data = new JSONObject();
            data.put("error", "串口已打开");
            callback.invoke(data);
        }
        String serialPortPath = options.getString("serialPortPath");
        int baudRate = options.getInteger("baudRate");
        Log.i(TAG, "serialPortPath = " + serialPortPath);
        Log.i(TAG, "baudRate = " + baudRate);
        boolean open = SerialPortManager.openSerialPort(serialPortPath, baudRate);
        if (open) {
            JSONObject data = new JSONObject();
            data.put("msg", "开启成功");
            //监听 获取串口消息
            SerialPortManager.setOnSerialPortDataChangedListener(new OnSerialPortDataChangedListener() {
                @Override
                public void serialPortDataReceived(byte[] data, int size) {
                    Log.i(TAG, "事件监听 = " + Arrays.toString(data));
                    StringBuilder sb = new StringBuilder();
                    for (byte b : data) {
                        sb.append(String.format("%02X", b));
                    }
                    Map<String, Object> params = new HashMap<>();
                    params.put("value", sb.toString());
                    mWXSDKInstance.fireGlobalEventCallback("serialPortData", params);
                }
            });
            callback.invoke(data);
        } else {
            JSONObject data = new JSONObject();
            data.put("error", "开启失败");
            callback.invoke(data);
        }
    }

    /**
     * 关闭串口
     *
     * @param callback
     */
    @UniJSMethod(uiThread = true)
    public void closeDevice(UniJSCallback callback) {
        Log.i(TAG, "closeDevice = ");
        SerialPortManager.closeSerialPort();
        JSONObject data = new JSONObject();
        data.put("msg", "串口关闭成功");
        callback.invoke(data);
    }

    /**
     * 发送消息
     *
     * @param callback
     */
    @UniJSMethod(uiThread = true)
    public void sendMsg(JSONObject options, UniJSCallback callback) {
        int type = options.getInteger("type");
        Log.i(TAG, "type = " + type);
        boolean succeed = false;
        if (type == 0) {
            SerialPortManager.writeData(hexStrToBinaryStr("AE0000B8"));
        } else if (type == 1) {
            SerialPortManager.writeData(hexStrToBinaryStr("AE0101B8"));
        } else if (type == 2) {
            SerialPortManager.writeData(hexStrToBinaryStr("AE0102B8"));
        }
        if (succeed) {
            JSONObject data = new JSONObject();
            data.put("msg", "发送成功");
            callback.invoke(data);
        } else {
            JSONObject data = new JSONObject();
            data.put("error", "发送失败");
            callback.invoke(data);
        }
    }
    
}

配置cloud相关参数(Hbuilder离线打包必要)

此部分官方教程

不看就继续看下去

cmd 生成 keystore 文件(Java环境自带的 keytool

keytool -genkey -alias 证书名称 -keyalg RSA -keysize 2048 -validity 36500 -keystore 证书文件名.keystore
#后续会让输入信息可以直接回车默认空,密码需要设置

在项目里 appbuild.gradle 文件中配置(证书文件名.keystore 放在文件可以读取的路径即可)

image-20250613133726425

查看生成信息(证书文件名.keystore 路径下 cmd 执行)

keytool -list -v -keystore 证书文件名.keystore  

image-20250613134838285

拷贝必要信息 SHA1SHA256Dcloud开发者中心 上进行配置

Dcloud开发者中心

创建应用 > 各平台信息 > 配置 包名、SHA1SHA256

image-20250613140001441

注意包名和本地的配置包名一致, appbuild.gradle 文件中 applicationId

image-20250613140600562

配置cloud相关参数(Hbuilder离线打包必要)

Uniapp页面编写

为了展示数据去安装了图表插件,看了 30秒的广告 ,害!

图表插件地址

不需要展示就去掉我代码里的一部分(问就是我懒得删掉)

下拉框选择器插件地址

新建项目 index.vue 页面(丑?因为博主只关心技术难点)

<template>
	<uni-data-select v-model="selDevice" :localdata="devices"></uni-data-select>
	<uni-data-select v-model="selDevicePath" :localdata="devicesPath"></uni-data-select>
	<button class="btn" type="button" @click="getDevices()">点击获取串口信息</button>
	<button class="btn" type="button" @click="openDevice()">打开串口</button>
	<button class="btn" type="button" @click="closeDevice()">关闭串口</button>
	<input type="text" v-model="sendMessage" />
	<button class="btn" type="button" @click="sendMsg(0)">停止采集</button>
	<button class="btn" type="button" @click="sendMsg(1)">点动采集</button>
	<button class="btn" type="button" @click="sendMsg(2)">持续采集</button>
	<button class="btn" type="button" @click="showECharts()">显示图表(以当前数据)</button>
	<button class="btn" type="button" @click="clearData()">清除历史数据</button>
	获取消息:
	<p>
		{{resultMsg}}
	</p>
	<view class="charts-box">
		<qiun-data-charts type="line" :chartData="chartData" />
	</view>

</template>

<script>
	var serialPortManagerModule = uni.requireNativePlugin("SerialPortManagerModule")
	export default {
		data() {
			return {
				selDevice: "",
				selDevicePath: "",
				devices: [],
				devicesPath: [],
				resultMsg: "",
				sendMessage: "",
				log: "",
				datas: [1, 3, 6, 5, 5],
				categoriesIndex:0,
				categories: [1, 2, 3, 4, 5],
				chartData: {},
			}
		},
		onLoad() {

		},
		methods: {
			getDevices() {
				var _this = this
				uni.showToast({
					title: "开始调用" + this.devices.length
				})
				_this.devices = []
				// 注意调用的方法名和原生中定义的方法名一致,
				//参数中获的为num1和num2,所以这里也传入这两个值
				serialPortManagerModule.getDevices((res) => {
					let allDevices = res["allDevices"]
					for (var i = 0; i < allDevices.length; i++) {
						let device = {
							text: allDevices[i],
							value: allDevices[i],
						}
						_this.devices.push(device)
					}
					let allDevicesPath = res["allDevicesPath"]
					for (var i = 0; i < allDevicesPath.length; i++) {
						let devicePath = {
							text: allDevicesPath[i],
							value: allDevicesPath[i],
						}
						_this.devicesPath.push(devicePath)
					}
				})
				uni.showToast({
					title: "调用结束"
				})
			},
			openDevice() {
				var _this = this
				uni.showToast({
					title: this.selDevicePath + "开启"
				})
				serialPortManagerModule.openDevice({
					serialPortPath: _this.selDevicePath,
					baudRate: 115200
				}, (res) => {
					if (!res["error"]) {
						plus.globalEvent.addEventListener('serialPortData', function(e) {
							_this.resultMsg += e['value'] + " "
							let hexString = e['value']
							let highHex = hexString.substring(6, 8);
							let lowHex = hexString.substring(8, 10);
							let data = (parseInt(highHex, 16) << 4) | parseInt(lowHex, 16);
							_this.datas.push(data)
							const now = new Date();
							const minutes = now.getMinutes();
							const seconds = now.getSeconds();
							_this.categoriesIndex += 1;
							_this.categories.push(_this.categoriesIndex)
						});
						uni.showToast({
							title: res["msg"]
						})
					} else {
						uni.showToast({
							title: res["error"]
						})
					}
				})

			},
			closeDevice() {
				var _this = this
				uni.showToast({
					title: this.selDevice + "关闭串口"
				})
				serialPortManagerModule.closeDevice((res) => {
					if (!res["error"]) {
						uni.showToast({
							title: res["msg"]
						})
					} else {
						uni.showToast({
							title: res["error"]
						})
					}
				})
			},
			sendMsg(type) {
				var _this = this
				uni.showToast({
					title: "发送消息"
				})
				serialPortManagerModule.sendMsg({
					type: type
				}, (res) => {
					if (!res["error"]) {
						uni.showToast({
							title: res["msg"]
						})
					} else {
						uni.showToast({
							title: res["error"]
						})
					}
				})
			},
			clearData() {
				var _this = this
				uni.showToast({
					title: "清除历史数据"
				})
				_this.resultMsg = ""
				_this.datas = []
				_this.categories = []
				_this.categoriesIndex = 0
			},
			showECharts() {
				var _this = this
				let res = {
					categories: _this.categories,
					series: [{
						name: "采集数据",
						lineType: "dash",
						data: _this.datas
					}]
				};
				this.chartData = JSON.parse(JSON.stringify(res));
			}
		}
	}
</script>

<style scoped>
	.charts-box {
		width: 100%;
		height: 300px;
	}
</style>

伪打包(自定义基座运行,模拟器上导出的APK即可到处安装)

此处只考虑到处 apk 文件在平板上运行,所以非完整上架打包

自定义基座离线打包官方文档
uniapp项目 本地打包

① 本地打包

② 结束后去 resources 下复制打包目录

image-20250613144210861

Android Studio 修改配置,粘贴打包目录

① 如图所示,增加 app 标签配置上打包目录名称(见 uniapp 项目中的 manifest.json 文件的 AppId

② 粘贴打包目录到 apps 目录下

image-20250613145128696

Android打包

① 打开侧边 Gradle

② 在 module 下进行 assembleRelease 打包(如果没有 assembleRelease 选项,请看下图设置里勾上它)

image-20250613162654176

③ 打开 build 下进行 Generate APKs

image-20250613155424773

Uniapp 运行自定义基座

① 复制 APKuniapp 项目 unpackage/debug 目录下

② 打开运行 > 运行到手机或模拟器 > 运行到 Android App 基座

③ 选择自定义基座,然后开始运行即可

image-20250613160033722

模拟器测试和导出

模拟器里打开 apk 简单测试下,再导出就好

image-20250613161003964image-20250613160944921

到此,你拥有了一款可以调试串口的uniapp开发的工具软件,可以使用前端开发了,单纯想调试安卓设备的串口,百度搜索 安卓串口调试助手 就好了

Uniapp Android 插件 串口