王旭东
充电桩压测方案
海镕多联机空调485控制器方案说明
电能平台压测程序使用说明
中台软网关解析规则整理
星纵物联LoraWAN网关方案整理
国网376.1协议整理
云快充协议对接方案
科德4G水表离线问题排查
海镕3种空调平台与中台对接参数文档
电信AEP平台NB设备接入教程
牧原项目ARCM500蓝牙调试程序对接文档
中台-Expasion架构设计调整
中台-蓝牙调试小程序对接说明
ADW300-IOT报警新版参数设置(增加DO1和DO2联动)
迈格瑞能MPS微电网混合逆变器整理
微电网混合逆变器参数下发整理
云南交投充电桩协议对接方案
AAC系列空调控制器整理
云快充2.1协议对接方案(新增V2G协议)
本文档使用 MrDoc 发布
-
+
首页
中台-蓝牙调试小程序对接说明
## 1. 协议设计 ### 1.1 蓝牙广播包 >蓝牙广播包`0x09`名称以`Acr`为前缀,用来区分是我司设备 ### 1.2 OTA升级 收发命令使用同一功能码,通过数据长度不同来表示读取或写入操作,具体根据不同功能码自行定义,收发格式如下。 | 描述 | 地址 | 功能码 | 子功能码 | 长度 | 操作码 | 数据 | crc | | ------ | ---- | ------ | -------- | ---- | ------ | ---- | ---- | | 字节数 | 1 | 1 | 1 | 2 | 1 | n | 2 | 协议说明: >- 字节组合按照小端模式,低位在前,高位在后(芯片基本都为小端模式); >- 长度使用两个字节,数据域的长度(操作码+数据); >- 操作码为1个字节(用于显示应答情况标识,返回错误或故障使用0xEx或0xFx); >- 地址0是广播地址,0xff是万能地址,广播地址不回复; >- crc校验从地址开始计算,到前一个字节结束 功能码列表: <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>表格</title> <style> table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid black; padding: 8px; text-align: center; } </style> </head> <body> <table> <tr> <th>功能码</th> <th>说明</th> <th>子功能码</th> <th>说明</th> <th>操作码</th> </tr> <tr> <td rowspan="7">0x55</td> <td rowspan="7">设备基本信息 固件升级</td> <td>0x01</td> <td>读取设备基本信息&出厂配置信息</td> <td>0x03</td> </tr> <tr> <td>0x02</td> <td>向设备发送固件升级请求</td> <td>0x10</td> </tr> <tr> <td>0xAA</td> <td>写入升级固件数据</td> <td>0x10</td> </tr> <tr> <td>0x03</td> <td>向设备发送模块升级请求</td> <td>0x10</td> </tr> <tr> <td>0xAB</td> <td>写入模块升级包数据</td> <td>0x10</td> </tr> <tr> <td>0xFF</td> <td>查看升级是否成功</td> <td>0x01</td> </tr> <tr> <td>0xF0</td> <td>重启设备</td> <td>0x01</td> </tr> </table> </body> </html> #### 1.2.1 读取设备基本信息(0x03) <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>表格</title> <style> table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid black; padding: 8px; text-align: center; } </style> </head> <body> <table> <tr> <th colspan="3">请求</th> <th colspan="3">回复</th> </tr> <tr> <th>类型</th> <th>报文</th> <th>说明</th> <th>类型</th> <th>报文</th> <th>说明</th> </tr> <tr> <td>表地址</td> <td>0xff</td> <td></td> <td>表地址</td> <td>0xff</td> <td></td> </tr> <tr> <td>功能码</td> <td>0x55</td> <td></td> <td>功能码</td> <td>0x55</td> <td></td> </tr> <tr> <td>子功能码</td> <td>0x01</td> <td></td> <td>子功能码</td> <td>0x01</td> <td></td> </tr> <tr> <td>长度</td> <td>0x0100</td> <td>低位在前</td> <td>长度</td> <td>0xXXXX</td> <td>可变,低位在前</td> </tr> <tr> <td>操作码</td> <td>0x03</td> <td>读取信息</td> <td>操作码</td> <td>0x01/0xEE</td> <td>正常/失败</td> </tr> <tr> <td>数据</td> <td>---</td> <td></td> <td>数据</td> <td>N</td> <td>见下表</td> </tr> <tr> <td>校验</td> <td>Crc16</td> <td></td> <td>校验</td> <td>Crc16</td> <td></td> </tr> </table> </body> </html> 数据详情: | 类型 | 长度 | 参数名 | 说明 | | ------------ | ---- | -------------------------------------------------- | ------------------------------------------------------------ | | 硬件系列编号 | 2 | seriesCode | 公司统⼀管控,低位在前 | | 硬件产品编号 | 2 | productCode | 产品编号,低位在前 | | 软件编号 | 2 | softcode | 例如 `3306` 33 和 06 各用一个字节表示,低位在前,即 0x06 0x21 | | 软件版本 | 2 | softversion | 例如 `1107` 11 和 07 各用一个字节表示,低位在前,即 0x07 0x0B | | 设备标识 | 1 | deviceType | 0x55 子设备,0xAA 采集设备(网关) | | 断点续传 | 1 | 暂无 | 0x00不支持,0xFF支持 | | 差分升级 | 1 | 暂无 | 0x00不支持,0xFF支持 | | 数据报文长度 | 2 | MTU | 单次传输最大字节数,128,256,512,1024等,低位在前 | | 固件信息地址 | 4 | 暂无 | Bin文件的绝对地址,文件起始为0,低位在前,用于读取bin文件自动获取以上信息 | | 序列号 | 20 | 设备SN,字符串,默认一般按公司14位数字编号,/0结束 | | | 预留 | 8 | | | >报文示例: >发送: >FF 55 01 01 00 03 08 25 >成功回复: >FF 55 01 2D 00 01 00 00 01 00 50 0B 6B 00 55 00 00 80 00 00 10 32 34 30 38 32 38 30 30 32 38 30 30 34 38 00 00 00 00 00 00 00 00 00 00 00 00 00 00 27 0C >失败回复: >FF 55 01 01 00 EE C8 68 #### 1.2.2 向设备发送固件升级请求(0x02) 请求报文包含以下内容: 硬件系列标识、硬件产品标识、软件编号、软件版本、设备标识、数据报文长度、升级方式、完整文件校验码 返回允许升级后,设备进入升级模式,允许0xAA、0xFF子功能码生效,1分钟内无子功能码0xAA写入升级软件数据报文,退出升级模式。 - 表1 | 请求: | | | 回复: | | | | -------- | ------ | ------------- | -------- | ------ | -------- | | 类型 | 报文 | 说明 | 类型 | 报文 | 说明 | | 表地址 | 0xff | | 表地址 | 0xff | | | 功能码 | 0x55 | | 功能码 | 0x55 | | | 子功能码 | 0x02 | | 子功能码 | 0x02 | | | 长度 | 0xXXXX | 可变,低位在前 | 长度 | 0x0500 | 低位在前 | | 操作码 | 0x10 | 写入升级信息 | 操作码 | 0x01 | 见表3 | | 数据 | N*byte | 见表2 | 数据 | 4*byte | | | 校验 | Crc16 | | 校验 | Crc16 | | - 表2 | 类型 | 长度 | 参数名 | 说明 | | ------------- | ---- | ----------- | ----------------------------------------------------- | | 硬件系列编号 | 2 | seriesCode | 公司统⼀管控,低位在前 | | 硬件产品编号 | 2 | productCode | 产品编号,低位在前 | | 软件编号 | 2 | softcode | softcode 例如 `3306`, 33 和 06 各用一个字节表示,低位在前,即 0x06 0x21 | | 软件版本 | 2 | softversion | softversion 例如 `1107`, 11 和 07 各用一个字节表示,低位在前,即 0x07 0x0B | | 设备标识 | 1 | deviceType | 0x55 `SUB_DEVICE`子设备, 0xAA `GATEWAY`采集设备(网关) | | 数据报文长度 | 2 | MTU | 单次最大传输字节数,128, 256, 512, 1024等,低位在前 | | 升级方式 | 1 | upgradeMode | 0x00完全升级, 0xAA差分升级 | | 升级包长度 | 4 | fileSize | Bin文件大小,低位在前 | | 16位CRC校验码 | 2 | crc16 | Bin文件的校验码 | | 32位CRC校验码 | 4 | crc32 | Bin文件的校验码 | | MD5校验码 | 16 | md5 | Bin文件的校验码 | - 表3 返回数据 | 操作码 | 其他数据 | 说明 | | ------ | -------- | ------------------------------------------------------------ | | 0x01 | 4个字节 | 允许升级,返回对应升级文件地址,0x00表示从头开始,非0x00可能是断点续传 | | 0xEE | 4个字节 | 不允许升级,错误信息 | #### 1.2.3 写入固件数据(0xAA) 根据上一条报文,发送对应文件地址的数据。 - 表1 | 请求: | | | 回复: | | | | -------- | ------------- | ---------------- | -------- | ------------- | -------- | | 类型 | 报文 | 说明 | 类型 | 报文 | 说明 | | 表地址 | 0xff | | 表地址 | 0xff | | | 功能码 | 0x55 | | 功能码 | 0x55 | | | 子功能码 | 0xAA | | 子功能码 | 0xAA | | | 长度 | 0xXXXX | 可变,低位在前 | 长度 | 0x0500/0x0100 | 低位在前 | | 操作码 | 0x10 | 写入升级软件 | 操作码 | 0x01 | 见表2 | | 数据 | 4*byte+N*byte | 4地址; N升级数据 | 数据 | 4*byte | 后续地址 | | 校验 | Crc16 | | 校验 | Crc16 | | - 表2 返回数据 | 操作码 | 其他数据 | 说明 | | ------ | ------------------ | ----------------------------- | | 0x01 | 4个字节,0x00000080 | 继续升级,返回后续升级文件地址 | | 0xAA | 4个字节,0XFFFFFFFF | 完成所有数据传输 | | 0xEE | 0字节 | 其他未知错误 | #### 1.2.4 向设备发送模块升级请求(0x03) 请求报文包含以下内容: 硬件系列标识、硬件产品标识、软件编号、软件版本、设备标识、数据报文长度、升级方式、完整文件校验码 返回允许升级后,设备进入升级模式,允许0xAB、0xFF子功能码生效,1分钟内无子功能码0xAB写入模块升级数据报文,退出升级模式。 - 表1 | 请求: | | | 回复: | | | | -------- | ------ | ------------- | -------- | ------ | -------- | | 类型 | 报文 | 说明 | 类型 | 报文 | 说明 | | 表地址 | 0xff | | 表地址 | 0xff | | | 功能码 | 0x55 | | 功能码 | 0x55 | | | 子功能码 | 0x03 | | 子功能码 | 0x03 | | | 长度 | 0xXXXX | 可变,低位在前 | 长度 | 0x0500 | 低位在前 | | 操作码 | 0x10 | 写入升级信息 | 操作码 | 0x01 | 见表3 | | 数据 | N*byte | 见表2 | 数据 | 4*byte | | | 校验 | Crc16 | | 校验 | Crc16 | | - 表2 | 类型 | 长度 | 参数名 | 说明 | | ------------- | ---- | ----------- | ----------------------------------------------------- | | 硬件系列编号 | 2 | seriesCode | 公司统⼀管控,低位在前 | | 硬件产品编号 | 2 | productCode | 产品编号,低位在前 | | 软件编号 | 2 | softcode | softcode 例如 `3306`, 33 和 06 各用一个字节表示,低位在前,即 0x06 0x21 | | 软件版本 | 2 | softversion | softversion 例如 `1107`, 11 和 07 各用一个字节表示,低位在前,即 0x07 0x0B | | 设备标识 | 1 | deviceType | 0x55 `SUB_DEVICE`子设备, 0xAA `GATEWAY`采集设备(网关) | | 数据报文长度 | 2 | MTU | 单次最大传输字节数,128, 256, 512, 1024等,低位在前 | | 升级方式 | 1 | upgradeMode | 0x00完全升级, 0xAA差分升级 | | 升级包长度 | 4 | fileSize | Bin文件大小,低位在前 | | 16位CRC校验码 | 2 | crc16 | Bin文件的校验码 | | 32位CRC校验码 | 4 | crc32 | Bin文件的校验码 | | MD5校验码 | 16 | md5 | Bin文件的校验码 | - 表3 返回数据 | 操作码 | 其他数据 | 说明 | | ------ | -------- | ------------------------------------------------------------ | | 0x01 | 4个字节 | 允许升级,返回对应升级文件地址,0x00表示从头开始,非0x00可能是断点续传 | | 0xEE | 4个字节 | 不允许升级,错误信息 | #### 1.2.5 写入固件数据(0xAB) 根据上一条报文,发送对应文件地址的数据。 - 表1 | 请求: | | | 回复: | | | | -------- | ------------- | ---------------- | -------- | ------------- | -------- | | 类型 | 报文 | 说明 | 类型 | 报文 | 说明 | | 表地址 | 0xff | | 表地址 | 0xff | | | 功能码 | 0x55 | | 功能码 | 0x55 | | | 子功能码 | 0xAB | | 子功能码 | 0xAB | | | 长度 | 0xXXXX | 可变,低位在前 | 长度 | 0x0500/0x0100 | 低位在前 | | 操作码 | 0x10 | 写入升级软件 | 操作码 | 0x01 | 见表2 | | 数据 | 4*byte+N*byte | 4地址; N升级数据 | 数据 | 4*byte | 后续地址 | | 校验 | Crc16 | | 校验 | Crc16 | | - 表2 返回数据 | 操作码 | 其他数据 | 说明 | | ------ | ------------------ | ----------------------------- | | 0x01 | 4个字节,0x00000080 | 继续升级,返回后续升级文件地址 | | 0xAA | 4个字节,0XFFFFFFFF | 完成所有数据传输 | | 0xEE | 0字节 | 其他未知错误 | #### 1.2.6 查看升级是否成功(0xFF) 接收到数据传输完成报文后,发送此报文,用于查询升级是否成功,被升级设备检查文件校验信息,若成功,返回成功报文并在3秒后重启,将升级后的固件替换原有固件 | 请求: | | | 回复: | | | | -------- | ------ | -------- | -------- | --------- | ------------------------- | | 类型 | 报文 | 说明 | 类型 | 报文 | 说明 | | 表地址 | 0xff | | 表地址 | 0xff | | | 功能码 | 0x55 | | 功能码 | 0x55 | | | 子功能码 | 0XFF | | 子功能码 | 0XFF | | | 长度 | 0x0100 | 低位在前 | 长度 | 0x0100 | 低位在前 | | 操作码 | 0x01 | | 操作码 | 0x01/0xEE | 0x01表示成功 0xEE表示失败 | | 校验 | Crc16 | | 校验 | Crc16 | | >报文示例: >发送: >FF 55 FF 01 00 01 B8 0C >成功回复: >FF 55 FF 01 00 01 B8 0C >失败回复: >FF 55 FF 01 00 EE F9 80 #### 1.2.7 重启设备 小程序发送重启命令到设备,设备响应后执行重启操作 | 请求: | | | 回复: | | | | -------- | ------ | -------- | -------- | --------- | ------------------------- | | 类型 | 报文 | 说明 | 类型 | 报文 | 说明 | | 表地址 | 0xff | | 表地址 | 0xff | | | 功能码 | 0x55 | | 功能码 | 0x55 | | | 子功能码 | 0XF0 | | 子功能码 | 0XF0 | | | 长度 | 0x0100 | 低位在前 | 长度 | 0x0100 | 低位在前 | | 操作码 | 0x01 | | 操作码 | 0x01/0xEE | 0x01表示成功 0xEE表示失败 | | 校验 | Crc16 | | 校验 | Crc16 | | >报文示例: >发送: >FF 55 F0 01 00 01 BB 18 >成功回复: >FF 55 F0 01 00 01 BB 18 >失败回复: >FF 55 F0 01 00 EE FA 94 ### 1.3 抄表模板 ```js { "id": 91, "templateName": "模板名称", "btProductId": 99, "createTime": "2024-04-07 13:23:07", "params": [{ "serialNum": 1, "group": 1, "paramCode": "Temp1", "paramName": "温度1", "unit": "°C", "registerAddr": 100, "dataType": "UINT_16", "endian": "UINT_16_H2H1", "readFuncCode": 3, "collectionScale": 1, "dispatchScale": 1, "calcExpression": "", "paramType": "READ", "paramGroup": "温度", "paramGroupSort": 1, "visible": "TRUE", "i18nName": { "en": "Temperature 1", "zh": "温度1" } }, { "serialNum": 2, "group": 1, "paramCode": "T1HighVal02", "paramName": "温度1告警值", "unit": "°C", "registerAddr": 101, "dataType": "UINT_16", "endian": "UINT_16_H2H1", "readFuncCode": 3, "writeFuncCode": 6, "collectionScale": 1, "dispatchScale": 1, "calcExpression": "", "paramType": "WRITE", "paramGroup": "温度1", "paramGroupSort": 2, "componentType": "TEXT_BOX", "constraint": "{\"max\":140,\"min\":0}", "visible": "TRUE", "i18nName": { "en": "Temperature 1 alarm value", "zh": "温度1报警值" }, ... }] } ``` 参数说明: | 字段名 | 类型 | 是否必填 | 描述 | 示例值 | | ------------ | -------- | -------- | ------------------------ | ------------------- | | id | 整数 | 否 | 模板唯一标识(系统生成) | 91 | | templateName | 字符串 | 否 | 模板名称 | ARCM300-Z-4G | | btProductId | 整数 | **是** | 中台蓝牙产品Id | 99 | | createTime | 字符串 | 否 | 模板创建时间 | 2024-04-07 13:23:07 | | params | 对象数组 | **是** | 参数配置列表 | 见下方子表 | Params 参数字段表格 | 字段名 | 类型 | 是否必填 | 描述 | 示例值 | | --------------- | ------ | -------- | ------------------------------------------------------------ | ---------------------------------------- | | serialNum | 整数 | **是** | 参数序号(全局唯一) | 1 | | i18nName | 对象 | **是** | 多语言名称 | { "en": "Temperature 1", "zh": "温度1" } | | group | 整数 | **是** | 参数所属分组编号 | 1 | | paramCode | 字符串 | **是** | 参数编码 | Temp1 | | paramName | 字符串 | 否 | 参数名称 | 温度1 | | unit | 字符串 | 否 | 参数单位 | °C | | registerAddr | 整数 | **是** | 寄存器地址 | 100 | | dataType | 字符串 | **是** | 寄存器数据类型,枚举类型,详见下表1 | UINT_16 | | endian | 字符串 | **是** | 字节序,枚举类型,和寄存器数据类型关联,详见下表2 | UINT_16_H2H1 | | readFuncCode | 整数 | **是** | 读功能码(Modbus协议,默认 3) | 3 | | writeFuncCode | 整数 | 条件必填 | 写功能码(可写参数必填,默认 6) | 6 | | collectionScale | 整数 | 否 | 采集精度 (小数位),不填默认取整 | 1 | | dispatchScale | 整数 | 否 | 转发精度 (小数位),不填默认和采集精度一致 | 1 | | calcExpression | 字符串 | 否 | 二次计算公式 (仅限只读参数) | 示例:"[#1*#2]" #后面跟参数序号 | | paramType | 字符串 | **是** | 参数类型(`READ`/`READ_WRITE`/`WRITE`) | READ | | paramGroup | 字符串 | **是** | 页面分组(如温度、电压) | 温度 | | paramGroupSort | 整数 | **是** | 页面分组顺序 | 1 | | componentType | 字符串 | 否 | 页面组件类型(输入框、单选按钮、下拉框) 仅限可写参数 详见下表3 | TEXT_BOX | | constraint | 字符串 | 否 | 约束条件(JSON格式)和页面组件类型关联 | {\"max\":140,\"min\":0} | | visible | 字符串 | 是 | 是否显示(TRUE/FALSE) | TRUE | 表1 寄存器数据类型 | 枚举常量 | 类型编号 | 描述 | | -------- | -------- | ---------------- | | BIT_16_1 | 1 | 单比特位 | | BIT_16_2 | 2 | 双比特位 | | BIT_16_4 | 3 | 四比特位 | | UINT_8 | 4 | 8位无符号整型 | | INT_8 | 5 | 8位有符号整型 | | UINT_16 | 6 | 16位无符号整型 | | INT_16 | 7 | 16位有符号整型 | | UINT_32 | 8 | 32位无符号整型 | | INT_32 | 9 | 32位有符号整型 | | UINT_64 | 10 | 64位无符号整型 | | INT_64 | 11 | 64位有符号整型 | | FLOAT | 12 | 32位单精度浮点型 | | ASCII | 13 | ASCII码 | | BCD_8 | 14 | 8位BCD码 | | BCD_16 | 15 | 16位BCD码 | | BCD_32 | 16 | 32位BCD码 | 表2 字节序 | 枚举常量 | 描述 | 寄存器数据类型 (表1类型编号) | | ---------------- | ------------------------- | ---------------------------- | | BIT_16_1_B0 | 单比特位B0 | 1 | | BIT_16_1_B1 | 单比特位B1 | 1 | | BIT_16_1_B2 | 单比特位B2 | 1 | | BIT_16_1_B3 | 单比特位B3 | 1 | | BIT_16_1_B4 | 单比特位B4 | 1 | | BIT_16_1_B5 | 单比特位B5 | 1 | | BIT_16_1_B6 | 单比特位B6 | 1 | | BIT_16_1_B7 | 单比特位B7 | 1 | | BIT_16_1_B8 | 单比特位B8 | 1 | | BIT_16_1_B9 | 单比特位B9 | 1 | | BIT_16_1_B10 | 单比特位B10 | 1 | | BIT_16_1_B11 | 单比特位B11 | 1 | | BIT_16_1_B12 | 单比特位B12 | 1 | | BIT_16_1_B13 | 单比特位B13 | 1 | | BIT_16_1_B14 | 单比特位B14 | 1 | | BIT_16_1_B15 | 单比特位B15 | 1 | | BIT_16_2_B0B1 | 双比特位B0B1 | 2 | | BIT_16_2_B2B3 | 双比特位B2B3 | 2 | | BIT_16_2_B4B5 | 双比特位B4B5 | 2 | | BIT_16_2_B6B7 | 双比特位B6B7 | 2 | | BIT_16_2_B8B9 | 双比特位B8B9 | 2 | | BIT_16_2_B10B11 | 双比特位B10B11 | 2 | | BIT_16_2_B12B13 | 双比特位B12B13 | 2 | | BIT_16_2_B14B15 | 双比特位B14B15 | 2 | | BIT_16_4_B0B3 | 四比特位B0到B3 | 3 | | BIT_16_4_B4B7 | 四比特位B4到B7 | 3 | | BIT_16_4_B8B11 | 四比特位B8到B11 | 3 | | BIT_16_4_B12B15 | 四比特位B12到B15 | 3 | | UINT_8_HIGH | 8位无符号整型-高字节 | 4 | | UINT_8_LOW | 8位无符号整型-低字节 | 4 | | INT_8_HIGH | 8位有符号整型-高字节 | 5 | | INT_8_LOW | 8位有符号整型-低字节 | 5 | | UINT_16_H2H1 | 16位无符号整型-高位在前 | 6 | | UINT_16_H1H2 | 16位无符号整型-低位在前 | 6 | | INT_16_H2H1 | 16位有符号整型-高位在前 | 7 | | INT_16_H1H2 | 16位有符号整型-低位在前 | 7 | | UINT_32_H1H2H3H4 | 32位无符号整型-H1H2H3H4 | 8 | | UINT_32_H4H3H2H1 | 32位无符号整型-H4H3H2H1 | 8 | | UINT_32_H3H4H1H2 | 32位无符号整型-H3H4H1H2 | 8 | | UINT_32_H2H1H4H3 | 32位无符号整型-H2H1H4H3 | 8 | | INT_32_H1H2H3H4 | 32位有符号整型-H1H2H3H4 | 9 | | INT_32_H4H3H2H1 | 32位有符号整型-H4H3H2H1 | 9 | | INT_32_H3H4H1H2 | 32位有符号整型-H3H4H1H2 | 9 | | INT_32_H2H1H4H3 | 32位有符号整型-H2H1H4H3 | 9 | | UINT_64_H1H8 | 64位无符号整型-低位在前 | 10 | | UINT_64_H8H1 | 64位无符号整型-高位在前 | 10 | | INT_64_H1H8 | 64位有符号整型-低位在前 | 11 | | INT_64_H8H1 | 64位有符号整型-高位在前 | 11 | | FLOAT_H1H2H3H4 | 32位单精度浮点型-H1H2H3H4 | 12 | | FLOAT_H4H3H2H1 | 32位单精度浮点型-H4H3H2H1 | 12 | | FLOAT_H3H4H1H2 | 32位单精度浮点型-H3H4H1H2 | 12 | | FLOAT_H2H1H4H3 | 32位单精度浮点型-H2H1H4H3 | 12 | | ASCII | ASCII码 | 13 | | BCD_8 | 8位BCD码 | 14 | | BCD_16 | 16位BCD码 | 15 | | BCD_32 | 32位BCD码 | 16 | 表3 页面组件类型 | 枚举常量 | 类型编号 | 描述 | `constraint`值约束条件 | 说明 | | ------------ | -------- | ---------- | ------------------------------------------------------------ | -------------------------- | | TEXT_BOX | 0 | 文本输入框 | "{\"max\":140,\"min\":0}" | `max`-最大值, `min`-最小值 | | RADIO_BUTTON | 1 | 单选按钮 | "{\"list\":[{\"key\":\"开\",\"value\":0},{\"key\":\"关\",\"value\":1}]}" | | | SELECT | 2 | 下拉框 | "{\"list\":[4800,9600,19200]}" | | ### 1.4 蓝牙指令 功能码列表: <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>表格</title> <style> table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid black; padding: 8px; text-align: center; } </style> </head> <body> <table> <tr> <th>功能码</th> <th>说明</th> <th>子功能码</th> <th>说明</th> <th>操作码</th> </tr> <tr> <td rowspan="4">0x60</td> <td rowspan="4">4g模块基础信息</td> <td>0x01</td> <td>读取联网状态信息</td> <td>0x03</td> </tr> <tr> <td>0x02</td> <td>读取APN信息</td> <td>0x03</td> </tr> <tr> <td>0x03</td> <td>写入APN信息</td> <td>0x10</td> </tr> <tr> <td>0x04</td> <td>读取sim卡信息</td> <td>0x03</td> </tr> <tr> <td rowspan="2">0x70</td> <td rowspan="2">EIOT中台协议配置信息</td> <td>0x01</td> <td>读取协议配置信息</td> <td>0x03</td> </tr> <tr> <td>0x02</td> <td>写入协议配置信息</td> <td>0x10</td> </tr> </table> </body> </html> #### 1.4.1 读取联网状态信息(0x03) <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>表格</title> <style> table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid black; padding: 8px; text-align: center; } </style> </head> <body> <table> <tr> <th colspan="3">请求</th> <th colspan="3">回复</th> </tr> <tr> <th>类型</th> <th>报文</th> <th>说明</th> <th>类型</th> <th>报文</th> <th>说明</th> </tr> <tr> <td>表地址</td> <td>0xff</td> <td></td> <td>表地址</td> <td>0xff</td> <td></td> </tr> <tr> <td>功能码</td> <td>0x60</td> <td></td> <td>功能码</td> <td>0x60</td> <td></td> </tr> <tr> <td>子功能码</td> <td>0x01</td> <td></td> <td>子功能码</td> <td>0x01</td> <td></td> </tr> <tr> <td>长度</td> <td>0x0100</td> <td>低位在前</td> <td>长度</td> <td>0x0300</td> <td>低位在前</td> </tr> <tr> <td>操作码</td> <td>0x03</td> <td>读取信息</td> <td>操作码</td> <td>0x01/0xEE</td> <td>正常/失败</td> </tr> <tr> <td rowspan="2">数据</td> <td rowspan="2">---</td> <td rowspan="2"></td> <td rowspan="2">数据</td> <td>0x01</td> <td>1字节的网络状态</td> </tr> <tr> <td>0x81</td> <td>Int8类型的信号强度</td> </tr> <tr> <td>校验</td> <td>Crc16</td> <td></td> <td>校验</td> <td>Crc16</td> <td></td> </tr> </table> </body> </html> >报文示例: >发送: >FF 60 01 01 00 03 84 21 >成功回复: >FF 60 01 03 00 01 01 81 BA E8 >失败回复: >FF 60 01 01 00 EE 44 6C #### 1.4.2 读取APN信息(0x03) <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>表格</title> <style> table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid black; padding: 8px; text-align: center; } </style> </head> <body> <table> <tr> <th colspan="3">请求</th> <th colspan="3">回复</th> </tr> <tr> <th>类型</th> <th>报文</th> <th>说明</th> <th>类型</th> <th>报文</th> <th>说明</th> </tr> <tr> <td>表地址</td> <td>0xff</td> <td></td> <td>表地址</td> <td>0xff</td> <td></td> </tr> <tr> <td>功能码</td> <td>0x60</td> <td></td> <td>功能码</td> <td>0x60</td> <td></td> </tr> <tr> <td>子功能码</td> <td>0x02</td> <td></td> <td>子功能码</td> <td>0x02</td> <td></td> </tr> <tr> <td>长度</td> <td>0x0100</td> <td>低位在前</td> <td>长度</td> <td>0x6100</td> <td>低位在前</td> </tr> <tr> <td>操作码</td> <td>0x03</td> <td>读取信息</td> <td>操作码</td> <td>0x01/0xEE</td> <td>正常/失败</td> </tr> <tr> <td rowspan="4">数据</td> <td rowspan="4">---</td> <td rowspan="4"></td> <td rowspan="4">数据</td> <td>0x00/0x01</td> <td>1字节,APN是否启用</td> </tr> <tr> <td>Apn</td> <td>ASCII码32字节、不足的补0</td> </tr> <tr> <td>ApnUser</td> <td>ASCII码32字节、不足的补0</td> </tr> <tr> <td>ApnPass</td> <td>ASCII码32字节、不足的补0</td> </tr> <tr> <td>校验</td> <td>Crc16</td> <td></td> <td>校验</td> <td>Crc16</td> <td></td> </tr> </table> </body> </html> >报文示例: >发送: >FF 60 02 01 00 03 84 65 >成功回复: >FF 60 02 61 00 01 (97个字节APN信息)(2个字节CRC) >失败回复: >FF 60 02 01 00 EE 44 28 #### 1.4.3 写入APN信息(0x10) <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>表格</title> <style> table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid black; padding: 8px; text-align: center; } </style> </head> <body> <table> <tr> <th colspan="3">请求</th> <th colspan="3">回复</th> </tr> <tr> <th>类型</th> <th>报文</th> <th>说明</th> <th>类型</th> <th>报文</th> <th>说明</th> </tr> <tr> <td>表地址</td> <td>0xff</td> <td></td> <td>表地址</td> <td>0xff</td> <td></td> </tr> <tr> <td>功能码</td> <td>0x60</td> <td></td> <td>功能码</td> <td>0x60</td> <td></td> </tr> <tr> <td>子功能码</td> <td>0x03</td> <td></td> <td>子功能码</td> <td>0x03</td> <td></td> </tr> <tr> <td>长度</td> <td>0x6100</td> <td>低位在前</td> <td>长度</td> <td>0x0100</td> <td>低位在前</td> </tr> <tr> <td>操作码</td> <td>0x10</td> <td>写入信息</td> <td>操作码</td> <td>0x01/0xEE</td> <td>正常/失败</td> </tr> <tr> <td rowspan="4">数据</td> <td>0x00/0x01</td> <td>1字节,APN是否启用</td> <td rowspan="4">数据</td> <td rowspan="4">---</td> <td rowspan="4"></td> </tr> <tr> <td>Apn</td> <td>ASCII码32字节、不足的补0</td> </tr> <tr> <td>ApnUser</td> <td>ASCII码32字节、不足的补0</td> </tr> <tr> <td>ApnPass</td> <td>ASCII码32字节、不足的补0</td> </tr> <tr> <td>校验</td> <td>Crc16</td> <td></td> <td>校验</td> <td>Crc16</td> <td></td> </tr> </table> </body> </html> >报文示例: >发送: >FF 60 03 61 00 10 (97个字节APN信息)(2个字节CRC) >成功回复: >FF 60 03 01 00 01 04 58 >失败回复: >FF 60 03 01 00 EE 45 D4 #### 1.4.4 读取SIM卡信息(0x03) <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>表格</title> <style> table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid black; padding: 8px; text-align: center; } </style> </head> <body> <table> <tr> <th colspan="3">请求</th> <th colspan="3">回复</th> </tr> <tr> <th>类型</th> <th>报文</th> <th>说明</th> <th>类型</th> <th>报文</th> <th>说明</th> </tr> <tr> <td>表地址</td> <td>0xff</td> <td></td> <td>表地址</td> <td>0xff</td> <td></td> </tr> <tr> <td>功能码</td> <td>0x60</td> <td></td> <td>功能码</td> <td>0x60</td> <td></td> </tr> <tr> <td>子功能码</td> <td>0x04</td> <td></td> <td>子功能码</td> <td>0x04</td> <td></td> </tr> <tr> <td>长度</td> <td>0x0100</td> <td>低位在前</td> <td>长度</td> <td>0x4100</td> <td>低位在前</td> </tr> <tr> <td>操作码</td> <td>0x03</td> <td>读取信息</td> <td>操作码</td> <td>0x01/0xEE</td> <td>正常/失败</td> </tr> <tr> <td rowspan="3">数据</td> <td rowspan="3">---</td> <td rowspan="3"></td> <td rowspan="3">数据</td> <td>0x00/0x01</td> <td>1字节,APN是否启用</td> </tr> <tr> <td>imei</td> <td>ASCII码32字节、不足的补0</td> </tr> <tr> <td>ccid</td> <td>ASCII码32字节、不足的补0</td> </tr> <tr> <td>校验</td> <td>Crc16</td> <td></td> <td>校验</td> <td>Crc16</td> <td></td> </tr> </table> </body> </html> >报文示例: >发送: >FF 60 04 01 00 03 84 ED >成功回复: >FF 60 04 41 00 01 (65个字节imei/ccid信息)(2个字节CRC) >失败回复: >FF 60 04 01 00 EE 44 A0 #### 1.4.5 读取EIOT中台协议参数(0x03) <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>表格</title> <style> table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid black; padding: 8px; text-align: center; } </style> </head> <body> <table> <tr> <th colspan="3">请求</th> <th colspan="3">回复</th> </tr> <tr> <th>类型</th> <th>报文</th> <th>说明</th> <th>类型</th> <th>报文</th> <th>说明</th> </tr> <tr> <td>表地址</td> <td>0xff</td> <td></td> <td>表地址</td> <td>0xff</td> <td></td> </tr> <tr> <td>功能码</td> <td>0x70</td> <td></td> <td>功能码</td> <td>0x70</td> <td></td> </tr> <tr> <td>子功能码</td> <td>0x01</td> <td></td> <td>子功能码</td> <td>0x01</td> <td></td> </tr> <tr> <td>长度</td> <td>0x0100</td> <td>低位在前</td> <td>长度</td> <td>0x2C00</td> <td>低位在前</td> </tr> <tr> <td>操作码</td> <td>0x03</td> <td>读取信息</td> <td>操作码</td> <td>0x01/0xEE</td> <td>正常/失败</td> </tr> <tr> <td rowspan="3">数据</td> <td rowspan="3">---</td> <td rowspan="3"></td> <td rowspan="3">数据</td> <td>0x05</td> <td>1字节,上报周期、单位分钟</td> </tr> <tr> <td>IP</td> <td>ASCII码40字节、不足的补0</td> </tr> <tr> <td>0x674e</td> <td>端口号、低字节在前</td> </tr> <tr> <td>校验</td> <td>Crc16</td> <td></td> <td>校验</td> <td>Crc16</td> <td></td> </tr> </table> </body> </html> >报文示例: >发送: >FF 70 01 01 00 03 45 E2 >成功回复: >FF 70 01 2C 00 01 05 (40个字节IP信息) 0x674e (2个字节CRC) >失败回复: >FF 70 01 01 00 EE 85 AF #### 1.4.6 写入EIOT中台协议参数(0x10) <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>表格</title> <style> table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid black; padding: 8px; text-align: center; } </style> </head> <body> <table> <tr> <th colspan="3">请求</th> <th colspan="3">回复</th> </tr> <tr> <th>类型</th> <th>报文</th> <th>说明</th> <th>类型</th> <th>报文</th> <th>说明</th> </tr> <tr> <td>表地址</td> <td>0xff</td> <td></td> <td>表地址</td> <td>0xff</td> <td></td> </tr> <tr> <td>功能码</td> <td>0x70</td> <td></td> <td>功能码</td> <td>0x70</td> <td></td> </tr> <tr> <td>子功能码</td> <td>0x02</td> <td></td> <td>子功能码</td> <td>0x02</td> <td></td> </tr> <tr> <td>长度</td> <td>0x2C00</td> <td>低位在前</td> <td>长度</td> <td>0x0100</td> <td>低位在前</td> </tr> <tr> <td>操作码</td> <td>0x10</td> <td>读取信息</td> <td>操作码</td> <td>0x01/0xEE</td> <td>正常/失败</td> </tr> <tr> <td rowspan="3">数据</td> <td>0x05</td> <td>1字节,上报周期、单位分钟</td> <td rowspan="3">数据</td> <td rowspan="3">---</td> <td rowspan="3"></td> </tr> <tr> <td>IP</td> <td>ASCII码40字节、不足的补0</td> </tr> <tr> <td>0x674e</td> <td>端口号、低字节在前</td> </tr> <tr> <td>校验</td> <td>Crc16</td> <td></td> <td>校验</td> <td>Crc16</td> <td></td> </tr> </table> </body> </html> >报文示例: >发送: >FF 70 02 2C 00 10 05 (40个字节IP信息) 0x674e (2个字节CRC) >成功回复: >FF 70 02 01 00 01 C4 67 >失败回复: >FF 70 02 01 00 EE 85 AF ## 2. 功能实现 ### 2.1 实时状态 > 实时状态页面由小程序根据抄表模板`READ`类型的可见参数动态生成,所有分组按照`paramGroupSort`字段排序,分组内的参数按照`serialNum`字段排序 #### 2.1.1 实时状态全部读取 - 小程序进入实时状态页面时读取一次全部`READ`类型参数,耗时可能较长,建议增加进度条,过程如下: ```js 1. 遍历抄表模板中每个`paramType`为`READ`的参数,从第一个参数开始,先调用函数库的`parseReadDownAdapter`方法组抄表的透传报文,返回结果示例: `01031400000181FA` 2. 下发透传报文到设备,接收设备回复的报文,示例:`010302092FFFC8`, 若超时5秒未收到设备回复,则重发透传报文,最多重复两次,依然收不到设备回复则提示读取失败 3. 调用函数库`parseUpAdapter`方法解析报文得到参数值(需要处理小数位和判断是否需要二次计算),将结果存入`dataSet`结果集中,再读取下一个参数,二次计算需要用到结果集中的参数 方法入参: `params` 抄表模板中所有参量的配置集合 `hexData` 读取命令下发后收到的响应报文 `paramCode` 当前报文对应的参数编码 `dataSet` 结果集,初始为空对象,每个解析出的参数放入结果集中 示例: { "paramCode": "Ua", "hexData": "010302092FFFC8", "dataSet": { "PT": 1, "CT": 30, ... }, "params": [ { serialNum: 1, paramCode: "PT", paramName: "电压变比", registerAddr: 4143, dataType: "UINT_8", endian: "UINT_8_HIGH", readFuncCode: 3, writeFuncCode: 6, collectionScale: 0, dispatchScale: 0, calcExpression: "", paramType: "READ_WRITE", visible: "TRUE", }, { serialNum: 2, paramCode: "CT", paramName: "电流变比", registerAddr: 4143, dataType: "UINT_8", endian: "UINT_8_LOW", readFuncCode: 3, writeFuncCode: 6, collectionScale: 0, dispatchScale: 0, calcExpression: "", paramType: "READ_WRITE", visible: "TRUE", }, { serialNum: 3, paramCode: "T1HighSw", paramName: "温度1保护开关", registerAddr: 4145, dataType: "BIT_16_1", endian: "BIT_16_1_B1", readFuncCode: 3, writeFuncCode: 16, collectionScale: 0, dispatchScale: 0, calcExpression: "", paramType: "READ_WRITE", visible: "TRUE", }, { serialNum: 4, paramCode: "T2HighSw", paramName: "温度2保护开关", registerAddr: 4145, dataType: "BIT_16_1", endian: "BIT_16_1_B2", readFuncCode: 3, writeFuncCode: 16, collectionScale: 0, dispatchScale: 0, calcExpression: "", paramType: "READ_WRITE", visible: "TRUE", }, { serialNum: 5, paramCode: "T3HighSw", paramName: "温度3保护开关", registerAddr: 4145, dataType: "BIT_16_1", endian: "BIT_16_2_B6B7", readFuncCode: 3, writeFuncCode: 16, collectionScale: 0, dispatchScale: 0, calcExpression: "", paramType: "READ_WRITE", visible: "TRUE", }, { serialNum: 6, paramCode: "TempHighVal", paramName: "温度保护阈值", registerAddr: 4145, dataType: "UINT_8", endian: "UINT_8_HIGH", readFuncCode: 3, writeFuncCode: 16, collectionScale: 0, dispatchScale: 0, calcExpression: "", paramType: "READ_WRITE", visible: "TRUE", }, ... ]; } 4. 直到参数全部抄读完毕,汇总结果集给前端载入表单,小程序将结果集缓存 { "data": { "Ua": 235.1, "Ia": 1.6, ... } } ``` #### 2.1.2 实时状态分组数据读取 - 用户可以点击每个分组数据旁边的刷新按钮读取这一组内的全部参数,过程如下: ```js 1. 遍历本组的每个参数编码,按抄表模板中的序号排序将对应行的配置筛选出来 2. 从筛选出的第一个参数开始,先调用函数库的`parseReadDownAdapter`方法组抄表的透传报文,返回结果示例: `01031400000181FA` 3. 下发透传报文到设备,接收设备回复的报文,示例:`010302092FFFC8`, 若超时5秒未收到设备回复,则重发透传报文,最多重复两次,依然收不到设备回复则提示读取失败 4. 调用函数库`parseUpAdapter`方法解析报文得到参数值(需要处理小数位和判断是否需要二次计算),将结果更新到结果集中,再读取下一个参数,二次计算需要用到结果集中的参数 方法入参同 2.1.1 5. 直到参数全部读取完毕,汇总结果集给前端载入表单,小程序将结果集缓存 { "data": { "Ua": 235.1, "Ia": 1.6, ... } } ``` #### 2.1.3 创建快照 - 点击创建快照按钮,将结果集中的数据保存到本地,数据时间为当前时间 ### 2.2 参数设置 > 参数设置页面由小程序根据抄表模板`WRITE`类型的参数动态生成,所有分组按照`paramGroupSort`字段排序,分组内的参数按照`serialNum`字段排序,参数组件类型和值范围根据`componentType`和`constraint`字段确定 #### 2.2.1 参数设置全部读取 - 小程序进入参数设置页面时读取一次全部`READ_WRITE`类型参数,建议增加进度条,过程如下: ```js 1. 遍历抄表模板中每个`paramType`为`READ_WRITE`的参数,从第一个参数开始,先调用函数库的`parseReadDownAdapter`方法组抄表的透传报文,返回结果示例: `01031400000181FA` 2. 下发透传报文到设备,接收设备回复的报文,示例:`010302092FFFC8`, 若超时5秒未收到设备回复,则重发透传报文,最多重复两次,依然收不到设备回复则提示读取失败 3. 调用函数库`parseUpAdapter`方法解析报文得到参数值(需要处理小数位),将结果存入结果集中,再读取下一个参数 方法入参同2.1.1 4. 直到参数全部抄读完毕,汇总结果集给前端载入表单,小程序将结果集缓存 { "data": { "UHighVal02": 110, "HighVal02": 80, ... } } ``` #### 2.2.2 单组参数读取 - 用户可以点击每个分组数据旁边的刷新按钮读取这一组内的全部参数,过程如下: ```js 1. 遍历本组的每个参数编码,按抄表模板中的序号排序将对应行的配置筛选出来 2. 从筛选出的第一个参数开始,先调用函数库的`parseReadDownAdapter`方法组抄表的透传报文,返回结果示例: `01031400000181FA` 3. 下发透传报文到设备,接收设备回复的报文,示例:`010302092FFFC8`, 若超时5秒未收到设备回复,则重发透传报文,最多重复两次,依然收不到设备回复则提示读取失败 4. 调用函数库`parseUpAdapter`方法解析报文得到参数值(需要处理小数位),将结果更新到结果集中,再读取下一个参数 方法入参同 2.1.1 5. 直到参数全部读取完毕,汇总结果集给前端载入表单,小程序将结果集缓存 { "data": { "UHighVal02": 110, "UHighSw": 1, ... } } ``` #### 2.2.3 单组参数设置 - 用户点击单组参数的下发按钮,可以设置这一整组参数的值,过程如下: ```js 1. 遍历本组的每个参数编码,按抄表模板中的序号排序将对应行的配置筛选出来 2. 从筛选出的第一个参数开始,先调用函数库的`parseWriteDownAdapter`方法组设置的透传报文,入参如下: `addr` 设备modbus协议地址 `paramCode` 参数编码 `params` 抄表模板参数配置集合 `dataSet` 克隆的参数设置读取结果集,要设置的参数值覆盖原来的参数值 注:由于某些参数可能在同一个寄存器中,比如ARCM300漏电和温度1-4的保护开关都对应同一个寄存器里的比特位,所以需要小程序在下发前先克隆dataSet结果集,在克隆的dataSet里面要将下发的参数值覆盖原来的参数值,作为入参,防止函数最终生成的报文设置了不应修改的参数 前端可以预先做一个筛选处理,若同组参数中有多个以上参数在同一个寄存器,可以只下发其中一个,避免重复发送 { "addr": 1, "paramCode": "UHighVal02", "dataSet": { "UHighVal02": 110, "UHighSw": 1, ... } "params": [ { "serialNum": 9, "group": 1, "paramCode": "UHighVal02", "paramName": "过压报警阈值", "unit": "%", "registerAddr": 100, "dataType": "UINT_16", "endian": "UINT_16_H2H1", "readFuncCode": 3, "writeFuncCode": 6, "collectionScale": 1, "dispatchScale": 1, "calcExpression": "", "paramType": "WRITE", "paramGroup": "过压", "paramGroupSort": 5, "componentType": "TEXT_BOX", "constraint": "{\"max\":140,\"min\":100}", "visible": "TRUE", "i18nName": { "en": "Overvoltage alarm threshold", "zh": "过压报警阈值" } }, ... ] } 返回的透传报文示例:`01061400044C8F0F` 3. 下发透传报文到设备,接收设备回复的报文,示例:`01061400044C8F0F`, 若超时5秒未收到设备回复,则重发透传报文,最多重复两次,依然收不到设备回复则提示下发失败 回复报文的第二个字节为功能码,与下发的报文功能码比较,不一致则说明命令下发失败 4. 直到设置报文全部下发完毕,执行一次本组参数读取操作,流程同 2.2.2 ``` ### 2.3 操作指令 > 操作指令页面默认有固件升级和重启两个按钮,针对仪表的一些复杂的个性化的命令下发,考虑通过配置和js脚本的方式实现 ### 2.4 日志 > 某些型号的设备有日志功能,例如ARCM300系列有报警日志、故障日志、开关日志,由于这部分内容不是标准化的,由小程序对这些型号定制开发日志查询功能,查询命令下行报文组包和设备上行报文的解析参照中台软网关解析规则的方式处理 ### 2.5 OTA升级 统一调用函数库`parseOtaDownAdapter`方法获取下发的报文,设备回复的报文通过函数库`checkOtaResult`方法解析 #### 2.5.1 读取设备基本信息 ```js 调用`parseOtaDownAdapter`方法获取下发的报文 参数说明: { "method": "OTA_READ_INFO", "addr": 255 } 返回Hex字符串 响应报文调用函数库`checkOtaResult`方法解析 参数说明: { "method": "OTA_READ_INFO", "hexData": "FF55012D000100000100500B6B005500008000001032343038323830303238303034380000000000000000000000000000270C" } 返回结果: 成功: { "res": 1, "data": { "seriesCode": 1, "productCode": 1, "softcode": "1703", "softversion": "1153", "deviceType": "SUB_DEVICE", "MTU": 128 } } 失败: { "res": 0, "data": {} } ``` #### 2.5.2 向设备发送固件升级请求 ```js 调用`parseOtaDownAdapter`方法获取下发的报文 参数说明: seriesCode 硬件系列标识 productCode 硬件产品标识 softcode 软件编号 softversion 软件版本 deviceType 设备类型 SUB_DEVICE 子设备 GATEWAY 网关 MTU 数据报文长度 128, 256, 512, 1024等 upgradeMode 升级方式 0-完全升级 1-差分升级 fileSize 升级包大小 crc16 升级包16位CRC校验码 crc32 升级包32位CRC校验码 md5 升级包MD5校验码 { "method": "OTA_UPGRADE", "addr": 1, "seriesCode": 1, "productCode": 1, "softcode": 1, "softversion": 1, "deviceType": "SUB_DEVICE", "MTU": 128, "upgradeMode": 0, "fileSize": 335571, "crc16": "F95A", "crc32": "3E57A5E9", "md5": "35149b871cf1db1b0fc8efbfdc99c37f" } 返回Hex字符串 响应报文调用函数库`checkOtaResult`方法解析,res为1表示允许升级,从data中获取到`startIndex`,即固件的起始地址 参数说明: { "method": "OTA_UPGRADE", "hexData": "FF55012D000100000100500B6B005500008000001032343038323830303238303034380000000000000000000000000000270C" } 返回结果: 成功: { "res": 1, "data": { "startIndex": 1001 } } 失败: { "res": 0, "data": { "errorCode": 0 } } ``` #### 2.5.3 写入固件数据 ```js 根据获取到的`MTU`,例如128字节,即单次发送给设备的报文不能超过128字节 固件bin文件需要根据MTU按顺序拆分成多个组块,组块的大小为MTU减去12字节,若MTU为128,则组块大小为128-12=116字节 每个组块根据其首个字节在bin文件中的位置作为索引标识,调用函数库`parseOtaDownAdapter`方法获取报文 参数说明: chunkIndex 组块起始地址,即首个字节在bin文件中的位置,从0开始 chunk 组块字节数组,UInt8Array { "method": "OTA_TRANSMISSION", "addr": 255 "chunkIndex": 1000, "chunk": [...] } 返回Hex字符串 响应报文调用函数库`checkOtaResult`方法解析,res为1表示发送成功,data里的startIndex表示下一包组块的起始地址,若startIndex值为0xffffffff,则表示全部传输完成 参数说明: { "method": "OTA_TRANSMISSION", "hexData": "FF55012D000100000100500B6B005500008000001032343038323830303238303034380000000000000000000000000000270C" } 返回结果: 成功: { "res": 1, "data": { "startIndex": 116 } } 失败: { "res": 0, "data": {} } ``` #### 2.5.4 向设备发送模块升级请求 ```js 调用`parseOtaDownAdapter`方法获取下发的报文 参数说明: seriesCode 硬件系列标识 productCode 硬件产品标识 softcode 软件编号 softversion 软件版本 deviceType 设备类型 SUB_DEVICE 子设备 GATEWAY 网关 MTU 数据报文长度 128, 256, 512, 1024等 upgradeMode 升级方式 0-完全升级 1-差分升级 fileSize 升级包大小 crc16 升级包16位CRC校验码 crc32 升级包32位CRC校验码 md5 升级包MD5校验码 { "method": "OTA_MODULE_UPGRADE", "addr": 1, "seriesCode": 1, "productCode": 1, "softcode": 1, "softversion": 1, "deviceType": "SUB_DEVICE", "MTU": 128, "upgradeMode": 0, "fileSize": 335571, "crc16": "F95A", "crc32": "3E57A5E9", "md5": "35149b871cf1db1b0fc8efbfdc99c37f" } 返回Hex字符串 响应报文调用函数库`checkOtaResult`方法解析,res为1表示允许升级,从data中获取到`startIndex`,即固件的起始地址 参数说明: { "method": "OTA_MODULE_UPGRADE", "hexData": "FF55012D000100000100500B6B005500008000001032343038323830303238303034380000000000000000000000000000270C" } 返回结果: 成功: { "res": 1, "data": { "startIndex": 1001 } } 失败: { "res": 0, "data": { "errorCode": 0 } } ``` #### 2.5.5 写入模块升级包数据 ```js 根据获取到的`MTU`,例如128字节,即单次发送给设备的报文不能超过128字节 固件bin文件需要根据MTU按顺序拆分成多个组块,组块的大小为MTU减去12字节,若MTU为128,则组块大小为128-12=116字节 每个组块根据其首个字节在bin文件中的位置作为索引标识,调用函数库`parseOtaDownAdapter`方法获取报文 参数说明: chunkIndex 组块起始地址,即首个字节在bin文件中的位置,从0开始 chunk 组块字节数组,UInt8Array { "method": "OTA_MODULE_TRANSMISSION", "addr": 255 "chunkIndex": 1000, "chunk": [...] } 返回Hex字符串 响应报文调用函数库`checkOtaResult`方法解析,res为1表示发送成功,data里的startIndex表示下一包组块的起始地址,若startIndex值为0xffffffff,则表示全部传输完成 参数说明: { "method": "OTA_MODULE_TRANSMISSION", "hexData": "FF55012D000100000100500B6B005500008000001032343038323830303238303034380000000000000000000000000000270C" } 返回结果: 成功: { "res": 1, "data": { "startIndex": 116 } } 失败: { "res": 0, "data": {} } ``` #### 2.5.6 查看升级是否成功 ```js 全部传输完成后,确认升级是否成功,调用函数库`parseOtaDownAdapter`获取下发的报文 参数说明: { "method": "OTA_CONFIRM", "addr": 255 } 返回Hex字符串 响应报文调用函数库`checkOtaResult`方法解析 参数说明: { "method": "OTA_CONFIRM", "hexData": "FF55FF010001B80C" } 返回结果: 成功: { "res": 1, "data": {} } 失败: { "res": 0, "data": {} } ``` #### 2.5.7 重启 ```js 调用函数库`parseOtaDownAdapter`获取下发的报文 参数说明: { "method": "RESTART", "addr": 255 } 返回Hex字符串 响应报文调用函数库`checkOtaResult`方法解析 参数说明: { "method": "RESTART", "hexData": "FF55F0010001BB18" } 返回结果: 成功: { "res": 1, "data": {} } 失败: { "res": 0, "data": {} } ``` ### 2.6 蓝牙指令 统一调用函数库`getBtDownData`方法获取下发的报文,设备回复的报文通过函数库`parseBtUpData`方法解析 #### 2.6.1 读取设备联网状态信息 1. 调用`getBtDownData`方法获取下发的报文 参数示例: ```js { "method": "4G_READ_CONN_STATUS" } ``` 返回示例: ``` FF60010100038421 ``` 2. 下发报文,获得响应,然后调用`parseBtUpData`方法解析响应报文 参数示例: ```js { "method": "4G_READ_CONN_STATUS", "resHexData": "FF60010300010181BAE8" } ``` 返回示例: 成功: ```js { "res": 1, "data": { "connectState": 1, // 联网状态,1在线 0离线 "rssi": 129 // 信号强度 } } ``` 失败: ```js { "res": 0, "data": {} } ``` #### 2.6.2 读取APN信息 1. 调用`getBtDownData`方法获取下发的报文 参数示例: ```js { "method": "4G_READ_APN_INFO" } ``` 返回示例: ``` FF60020100038465 ``` 2. 下发报文,获得响应,然后调用`parseBtUpData`方法解析响应报文 参数示例: ```js { "method": "4G_READ_APN_INFO", "resHexData": "FF6002610001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000025E2" } ``` 返回示例: 成功: ```js { "res": 1, "data": { "isApnOn": 1, // APN是否启用,1启用 0未启用 "Apn": "xxx", // 接入点名称 "ApnUser": "dev1", // 接入用户名 "ApnPass": "123456" // 接入密码 } } ``` 失败: ```js { "res": 0, "data": {} } ``` #### 2.6.3 写入APN信息 1. 调用`getBtDownData`方法获取下发的报文 参数示例: ```js { "method": "4G_WRITE_APN_INFO", "isApnOn": 1, // APN是否启用,1启用 0未启用 "Apn": "xxx", // 接入点名称 "ApnUser": "dev1", // 接入用户名 "ApnPass": "123456" // 接入密码 } ``` 返回示例: ``` FF600361001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004122 ``` 2. 下发报文,获得响应,然后调用`parseBtUpData`方法解析响应报文 参数示例: ```js { "method": "4G_WRITE_APN_INFO", "resHexData": "FF60030100010458" } ``` 返回示例: 成功: ```js { "res": 1, "data": {} } ``` 失败: ```js { "res": 0, "data": {} } ``` #### 2.6.4 读取SIM卡信息 1. 调用`getBtDownData`方法获取下发的报文 参数示例: ```js { "method": "4G_READ_SIM_INFO" } ``` 返回示例: ``` FF600401000384ED ``` 2. 下发报文,获得响应,然后调用`parseBtUpData`方法解析响应报文 参数示例: ```js { "method": "4G_READ_SIM_INFO", "resHexData": "FF60044100010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FC82" } ``` 返回示例: 成功: ```js { "res": 1, "data": { "isApnOn": 1, // APN是否启用,1启用 0未启用 "imei": "xxx", "ccid": "xxx" } } ``` 失败: ```js { "res": 0, "data": {} } ``` #### 2.6.5 读取EIOT中台协议配置参数 1. 调用`getBtDownData`方法获取下发的报文 参数示例: ```js { "method": "PROTOCOL_READ_EIOT_CONFIG" } ``` 返回示例: ``` FF700101000345E2 ``` 2. 下发报文,获得响应,然后调用`parseBtUpData`方法解析响应报文 参数示例: ```js { "method": "PROTOCOL_READ_EIOT_CONFIG", "resHexData": "FF70012C000105000000000000000000000000000000000000000000000000000000000000000000000000000000000000674EE22C" } ``` 返回示例: 成功: ```js { "res": 1, "data": { "upInterval": 5, // 上报周期、单位分钟 "ip": "0.0.0.0", // 连接IP "port": 20071 // 端口 } } ``` 失败: ```js { "res": 0, "data": {} } ``` #### 2.6.6 写入EIOT中台协议配置参数 1. 调用`getBtDownData`方法获取下发的报文 参数示例: ```js { "method": "PROTOCOL_WRITE_EIOT_CONFIG", "upInterval": 5, // 上报周期、单位分钟 "ip": "0.0.0.0", // 连接IP "port": 20071 // 端口 } ``` 返回示例: ``` FF70022C001005000000000000000000000000000000000000000000000000000000000000000000000000000000000000674E0C3E ``` 2. 下发报文,获得响应,然后调用`parseBtUpData`方法解析响应报文 参数示例: ```js { "method": "PROTOCOL_WRITE_EIOT_CONFIG", "resHexData": "FF7002010001C467" } ``` 返回示例: 成功: ```js { "res": 1, "data": {} } ``` 失败: ```js { "res": 0, "data": {} } ``` # 附录 ### 附录1:函数库代码 ```js function getBtDownData(params) { const { method } = params; if (!method) { throw new Error("method not found in params"); } let addressHex = "FF"; let functionCodeHex = "60"; let subFunCodeHex = ""; let dataLengthHex = "0100"; let modbusCodeHex = "03"; let dataBodyHex = ""; switch (method) { case "4G_READ_CONN_STATUS": subFunCodeHex = "01"; break; case "4G_READ_APN_INFO": subFunCodeHex = "02"; break; case "4G_WRITE_APN_INFO": subFunCodeHex = "03"; dataLengthHex = "6100"; modbusCodeHex = "10"; const { isApnOn, Apn, ApnUser, ApnPass } = params; if ( isApnOn == undefined || Apn == undefined || ApnUser == undefined || ApnPass == undefined ) { throw new Error("Missing parameter"); } let isApnOnHex = integerToHex(isApnOn, 1); let ApnHex = stringToAsciiHex(Apn); let ApnUserHex = stringToAsciiHex(ApnUser); let ApnPassHex = stringToAsciiHex(ApnPass); dataBodyHex = isApnOnHex + ApnHex + ApnUserHex + ApnPassHex break; case "4G_READ_SIM_INFO": subFunCodeHex = "04"; break; case "PROTOCOL_READ_EIOT_CONFIG": functionCodeHex = "70"; subFunCodeHex = "01"; break; case "PROTOCOL_WRITE_EIOT_CONFIG": functionCodeHex = "70"; subFunCodeHex = "02"; dataLengthHex = "2C00" modbusCodeHex = "10"; const { upInterval, ip, port } = params; if ( upInterval == undefined || ip == undefined || port == undefined ) { throw new Error("Missing parameter"); } let upIntervalHex = integerToHex(upInterval, 1) let portHex = integerToHex(port, 2, true); let ipHex = stringToAsciiHex(ip,40); dataBodyHex = upIntervalHex + portHex + ipHex; break; } let result = addressHex + functionCodeHex + subFunCodeHex + dataLengthHex + modbusCodeHex + dataBodyHex let sendData = hexStrTobyteArray(result); const crc = calculateCRC(sendData); const crcHex = toHex(crc[0], 2) + toHex(crc[1], 2); result = result + crcHex; return result; } function parseBtUpData(params) { const { method, hexData } = params; if (!method || !hexData) { throw new Error("Missing parameter"); } let result = {}; result.res = 0; let parseData = {}; let bytes = hexStrTobyteArray(hexData); let resultCode = bytes[5]; //去掉开头的字节 let dataArray = bytes.slice(6); if (resultCode !== 0x01) { result.data = {}; return result; } switch (method) { case "4G_READ_CONN_STATUS": //读取设备基本信息 if (resultCode === 0x01) { result.res = 1; parseData.connectState = dataArray[0]; parseData.rssi = dataArray[1]; } break; case "4G_READ_APN_INFO": if (resultCode === 0x01) { result.res = 1; parseData.isApnOn = dataArray[0]; parseData.Apn = byteArrayToAsciiString(dataArray, { startIndex: 1, length: 32 }); parseData.ApnUser = byteArrayToAsciiString(dataArray, { startIndex: 33, length: 32 }); parseData.ApnPass = byteArrayToAsciiString(dataArray, { startIndex: 65, length: 32 }); } break; case "4G_WRITE_APN_INFO": if (resultCode === 0x01) { result.res = 1; } break; case "4G_READ_SIM_INFO": if (resultCode === 0x01) { result.res = 1; parseData.isApnOn = dataArray[0]; parseData.imei = byteArrayToAsciiString(dataArray, { startIndex: 1, length: 32 }); parseData.ccid = byteArrayToAsciiString(dataArray, { startIndex: 33, length: 32 }); } break; case "PROTOCOL_READ_EIOT_CONFIG": if (resultCode === 0x01) { result.res = 1; parseData.upInterval = dataArray[0]; parseData.ip = byteArrayToAsciiString(dataArray, { startIndex: 1, length: 40 }); parseData.port = bytesToInteger(dataArray, 41, 2, true) } break; case "PROTOCOL_WRITE_EIOT_CONFIG": if (resultCode === 0x01) { result.res = 1; } break; } result.data = parseData; return result; } //解析0x55广播包(已弃用) function parseBroadcast(hexData) { let result = {}; //转换为字节数组 const bytes = hexStrTobyteArray(hexData); //校验蓝牙广播包长度,应不低于19字节 if (bytes.length < 19) { throw new Error("Message length error"); } //跳过前两个字节(长度和功能码) const dataBytes = bytes.slice(2); result.seriesCode = (dataBytes[0] << 8) | dataBytes[1]; result.productCode = (dataBytes[2] << 8) | dataBytes[3]; let softcode1 = dataBytes[5].toString(); let softcode2 = dataBytes[4] < 10 ? "0" + dataBytes[4].toString() : dataBytes[4].toString(); result.softcode = softcode1 + softcode2; let softversion1 = dataBytes[7].toString(); let softversion2 = dataBytes[6] < 10 ? "0" + dataBytes[6].toString() : dataBytes[6].toString(); result.softversion = softversion1 + softversion2; result.deviceType = dataBytes[8] === 0x55 ? "SUB_DEVICE" : "GATEWAY"; result.MTU = (dataBytes[12] << 8) | dataBytes[11]; return result; } //生成设备ota升级下发的报文,分为读取设备基本信息、写入升级固件相关信息、写入固件数据、查看升级是否成功、重启设备 function parseOtaDownAdapter(params) { const { method, addr } = params; if (!method) { throw new Error("method not found in params"); } let addressHex = toHex(addr, 2); //数据域 let dataDomin = ""; //子功能码 let functionCode = 0; //数据域长度 let dataLength = 1; switch (method) { case "OTA_READ_INFO": functionCode = 0x01; dataDomin = "03"; break; case "OTA_UPGRADE": functionCode = 0x02; { const { seriesCode, productCode, softcode, softversion, deviceType, MTU, upgradeMode, fileSize, crc16, crc32, md5, } = params; if ( seriesCode == undefined || productCode == undefined || softcode == undefined || softversion == undefined || MTU == undefined || fileSize == undefined || crc16 == undefined || crc32 == undefined || md5 == undefined ) { throw new Error("Missing parameter"); } let seriesCodeHex = toHex(seriesCode & 0xff, 2) + toHex((seriesCode >> 8) & 0xff, 2); let productCodeHex = toHex(productCode & 0xff, 2) + toHex((productCode >> 8) & 0xff, 2); //软件编号和软件版本号须保证4位数以内 if (parseInt(softcode) > 9999 || parseInt(softversion) > 9999) { throw new Error("softcode or softversion Length error"); } let softcodeHex = toHex(Math.floor(parseInt(softcode) % 100), 2) + toHex(Math.floor(parseInt(softcode) / 100), 2); let softversionHex = toHex(Math.floor(parseInt(softversion) % 100), 2) + toHex(Math.floor(parseInt(softversion) / 100), 2); let deviceTypeHex = "55"; if (deviceType !== undefined) { deviceTypeHex = toHex(deviceType === "SUB_DEVICE" ? 0x55 : 0xaa, 2); } let MTUHex = toHex(MTU & 0xff, 2) + toHex((MTU >> 8) & 0xff, 2); let upgradeModeHex = "00"; if (upgradeMode !== undefined) { upgradeModeHex = toHex(upgradeMode === 0 ? 0x00 : 0xaa, 2); } let fileSizeHex = toHex(fileSize & 0xff, 2) + toHex((fileSize >> 8) & 0xff, 2) + toHex((fileSize >> 16) & 0xff, 2) + toHex((fileSize >> 24) & 0xff, 2); //数据域 = 操作码+数据 dataDomin = "10" + seriesCodeHex + productCodeHex + softcodeHex + softversionHex + deviceTypeHex + MTUHex + upgradeModeHex + fileSizeHex + crc16 + crc32 + md5; dataLength = 39; } break; case "OTA_TRANSMISSION": functionCode = 0xaa; { const { chunkIndex, chunk } = params; if (chunkIndex == undefined || chunk == undefined) { throw new Error("Missing parameter"); } let chunkIndexHex = toHex(chunkIndex & 0xff, 2) + toHex((chunkIndex >> 8) & 0xff, 2) + toHex((chunkIndex >> 16) & 0xff, 2) + toHex((chunkIndex >> 24) & 0xff, 2); dataDomin = "10" + chunkIndexHex + byteArrayToHexString(chunk); dataLength = 5 + chunk.length; } break; case "OTA_MODULE_UPGRADE": functionCode = 0x02; { const { seriesCode, productCode, softcode, softversion, deviceType, MTU, upgradeMode, fileSize, crc16, crc32, md5, } = params; if ( seriesCode == undefined || productCode == undefined || softcode == undefined || softversion == undefined || MTU == undefined || fileSize == undefined || crc16 == undefined || crc32 == undefined || md5 == undefined ) { throw new Error("Missing parameter"); } let seriesCodeHex = toHex(seriesCode & 0xff, 2) + toHex((seriesCode >> 8) & 0xff, 2); let productCodeHex = toHex(productCode & 0xff, 2) + toHex((productCode >> 8) & 0xff, 2); //软件编号和软件版本号须保证4位数以内 if (parseInt(softcode) > 9999 || parseInt(softversion) > 9999) { throw new Error("softcode or softversion Length error"); } let softcodeHex = toHex(Math.floor(parseInt(softcode) % 100), 2) + toHex(Math.floor(parseInt(softcode) / 100), 2); let softversionHex = toHex(Math.floor(parseInt(softversion) % 100), 2) + toHex(Math.floor(parseInt(softversion) / 100), 2); let deviceTypeHex = "55"; if (deviceType !== undefined) { deviceTypeHex = toHex(deviceType === "SUB_DEVICE" ? 0x55 : 0xaa, 2); } let MTUHex = toHex(MTU & 0xff, 2) + toHex((MTU >> 8) & 0xff, 2); let upgradeModeHex = "00"; if (upgradeMode !== undefined) { upgradeModeHex = toHex(upgradeMode === 0 ? 0x00 : 0xaa, 2); } let fileSizeHex = toHex(fileSize & 0xff, 2) + toHex((fileSize >> 8) & 0xff, 2) + toHex((fileSize >> 16) & 0xff, 2) + toHex((fileSize >> 24) & 0xff, 2); //数据域 = 操作码+数据 dataDomin = "10" + seriesCodeHex + productCodeHex + softcodeHex + softversionHex + deviceTypeHex + MTUHex + upgradeModeHex + fileSizeHex + crc16 + crc32 + md5; dataLength = 39; } break; case "OTA_MODULE_TRANSMISSION": functionCode = 0xaa; { const { chunkIndex, chunk } = params; if (chunkIndex == undefined || chunk == undefined) { throw new Error("Missing parameter"); } let chunkIndexHex = toHex(chunkIndex & 0xff, 2) + toHex((chunkIndex >> 8) & 0xff, 2) + toHex((chunkIndex >> 16) & 0xff, 2) + toHex((chunkIndex >> 24) & 0xff, 2); dataDomin = "10" + chunkIndexHex + byteArrayToHexString(chunk); dataLength = 5 + chunk.length; } break; case "OTA_CONFIRM": functionCode = 0xff; dataDomin = "01"; break; case "RESTART": functionCode = 0xf0; dataDomin = "01"; break; } let result = addressHex + "55" + toHex(functionCode, 2) + toHex(dataLength & 0xff, 2) + toHex((dataLength >> 8) & 0xff, 2) + dataDomin; let sendData = hexStrTobyteArray(result); const crc = calculateCRC(sendData); const crcHex = toHex(crc[0], 2) + toHex(crc[1], 2); result = result + crcHex; return hexStrTobyteArray(result); } //处理设备ota升级响应的报文,确认处理结果 function checkOtaResult(params) { const { method, hexData } = params; if (!method || !hexData) { throw new Error("Missing parameter"); } let result = {}; result.res = 0; let parseData = {}; let bytes = hexStrTobyteArray(hexData); //去掉开头的字节 let dataArray = bytes.slice(5); let functionCode = dataArray[0]; switch (method) { case "OTA_READ_INFO": //读取设备基本信息 if (functionCode === 0x01) { parseData.seriesCode = (dataArray[2] << 8) | dataArray[1]; parseData.productCode = (dataArray[4] << 8) | dataArray[3]; parseData.softcode = dataArray[6].toString() + dataArray[5].toString().padStart(2, "0"); parseData.softversion = dataArray[8].toString() + dataArray[7].toString().padStart(2, "0"); parseData.deviceType = dataArray[9] === 0x55 ? "SUB_DEVICE" : "GATEWAY"; parseData.MTU = (dataArray[13] << 8) | dataArray[12]; result.res = 1; } break; case "OTA_UPGRADE": //写入升级固件相关信息 if (functionCode === 0x01) { //允许升级,返回对应升级文件地址 result.res = 1; parseData.startIndex = (dataArray[4] << 24) | (dataArray[3] << 16) | (dataArray[2] << 8) | dataArray[1]; } else { //不允许升级 parseData.errorCode = (dataArray[4] << 24) | (dataArray[3] << 16) | (dataArray[2] << 8) | dataArray[1]; } break; case "OTA_TRANSMISSION": //传输升级包组块 if (functionCode == 0x01) { //继续升级,返回后续升级文件地址 result.res = 1; parseData.startIndex = (dataArray[4] << 24) | (dataArray[3] << 16) | (dataArray[2] << 8) | dataArray[1]; } else if (functionCode == 0xaa) { //完成所有数据传输 result.res = 1; parseData.startIndex = 0xffffffff; } break; case "OTA_MODULE_UPGRADE": //写入升级固件相关信息 if (functionCode === 0x01) { //允许升级,返回对应升级文件地址 result.res = 1; parseData.startIndex = (dataArray[4] << 24) | (dataArray[3] << 16) | (dataArray[2] << 8) | dataArray[1]; } else { //不允许升级 parseData.errorCode = (dataArray[4] << 24) | (dataArray[3] << 16) | (dataArray[2] << 8) | dataArray[1]; } break; case "OTA_MODULE_TRANSMISSION": //传输升级包组块 if (functionCode == 0x01) { //继续升级,返回后续升级文件地址 result.res = 1; parseData.startIndex = (dataArray[4] << 24) | (dataArray[3] << 16) | (dataArray[2] << 8) | dataArray[1]; } else if (functionCode == 0xaa) { //完成所有数据传输 result.res = 1; parseData.startIndex = 0xffffffff; } break; case "OTA_CONFIRM": //确认升级结果 case "RESTART": //重启 if (functionCode == 0x01) { result.res = 1; } break; } result.data = parseData; return result; } //生成抄读命令的Modbus协议报文 function parseReadDownAdapter(addr, param) { //抄读命令(后续考虑增加0x01、0x02、0x04功能码,目前只考虑0x03功能码) const { registerAddr, dataType, readFuncCode } = param; if (!registerAddr || !dataType || !readFuncCode) { throw new Error("Missing parameter"); } // Modbus RTU报文格式:从站地址(1字节) + 功能码(1字节) + 起始地址(2字节) + 寄存器数量(2字节) + CRC(2字节) const startAddrHex = toHex(registerAddr, 4); // 抄读寄存器个数 const quantity = getQuantityByType(dataType); const quantityHex = toHex(quantity, 4); const funcCodeHex = toHex(readFuncCode, 2); const addressHex = toHex(addr, 2); let hexData = addressHex + funcCodeHex + startAddrHex + quantityHex; let sendData = hexStrTobyteArray(hexData); // 计算CRC校验 const crc = calculateCRC(sendData); const crcHex = toHex(crc[0], 2) + toHex(crc[1], 2); // 拼接完整的Modbus报文 const requestHex = hexData + crcHex; return hexStrTobyteArray(requestHex); } //生成写入命令的Modbus协议报文, 需要区分功能码,目前只支持0x06和0x10功能码 function parseWriteDownAdapter(addr, paramCode, params, dataSet) { const targetParam = params.find((p) => p.paramCode === paramCode); if (!targetParam) { throw new Error(`paramCode ${paramCode} not found in params`); } const { registerAddr, dataType, writeFuncCode, collectionScale, endian } = targetParam; //检查是否有相同寄存器地址的参数 const conflictParams = params.filter( (p) => p.registerAddr === registerAddr && p.writeFuncCode === writeFuncCode && p.paramCode !== paramCode ); let value = dataSet[paramCode]; if (!value) throw new Error("Param value not found"); //处理精度问题 if (collectionScale && collectionScale > 0) { if (value.constructor === BigInt) { value *= BigInt(Math.pow(10, collectionScale)); } else { value *= Math.pow(10, collectionScale); } } let buffer = modbusDataConverter(dataType, endian, value); if (conflictParams.length > 0) { //有相同地址表的参数,例如比特位和单个字节类型的参数可能需要合并,将buffer转为16位无符号整型,然后再合并 let finalValue = (buffer[0] << 8) | buffer[1]; buffer = []; for (let conflictParam of conflictParams) { let conflictParamValue = dataSet[conflictParam.paramCode]; if (!conflictParamValue) continue; let newBuffer = conflictParamConverter(conflictParam, conflictParamValue); finalValue |= (newBuffer[0] << 8) | newBuffer[1]; } buffer.push(...[finalValue >>> 8, finalValue & 0xff]); } const addressHex = toHex(addr, 2); let registerDataStr = byteArrayToHexString(buffer); const startAddrHex = toHex(registerAddr, 4); //写入寄存器个数 let quantity = getQuantityByType(dataType); if (dataType === "ASCII") { let length = value.toString().length; quantity = length % 2 === 0 ? length / 2 : length / 2 + 1; } const quantityHex = toHex(quantity, 4); let byteQuantity = quantity * 2; const byteQuantityHex = toHex(byteQuantity, 2); const funcCodeHex = toHex(writeFuncCode, 2); let requestHex = ""; if (writeFuncCode === 6) { //写单个寄存器, Modbus RTU报文格式:从站地址(1字节) + 功能码(1字节) + 起始地址(2字节) + 写入数据(2字节) let hexData = addressHex + funcCodeHex + startAddrHex + registerDataStr; let sendData = hexStrTobyteArray(hexData); // 计算CRC校验 const crc = calculateCRC(sendData); const crcHex = toHex(crc[0], 2) + toHex(crc[1], 2); // 拼接完整的Modbus报文 requestHex = hexData + crcHex; } else if (writeFuncCode === 16) { //连续写多个寄存器, Modbus RTU报文格式:从站地址(1字节) + 功能码(1字节) + 起始地址(2字节) + 寄存器数量(2字节) + 写入字节总数n(1字节) + 写入数据(n字节) + CRC(2字节) let hexData = addressHex + funcCodeHex + startAddrHex + quantityHex + byteQuantityHex + registerDataStr; let sendData = hexStrTobyteArray(hexData); // 计算CRC校验 const crc = calculateCRC(sendData); const crcHex = toHex(crc[0], 2) + toHex(crc[1], 2); // 拼接完整的Modbus报文 requestHex = hexData + crcHex; } return hexStrTobyteArray(requestHex); } // 转换寄存器地址冲突的参数值 function conflictParamConverter(param, value) { const { dataType, collectionScale, endian } = param; //处理精度问题 if (collectionScale != undefined && collectionScale > 0) { if (value.constructor === BigInt) { value *= BigInt(Math.pow(10, collectionScale)); } else { value *= Math.pow(10, collectionScale); } } return modbusDataConverter(dataType, endian, value); } // 辅助方法:处理字节序交换 function swapBytes(arr, wordSize) { const swapped = []; for (let i = 0; i < arr.length; i += wordSize) { swapped.push(...arr.slice(i, i + wordSize).reverse()); } return swapped; } // 根据数据类型、字节序、参数值得到写进寄存器的字节数组 function modbusDataConverter(dataType, endian, value) { let buffer = []; // 根据数据类型和字节序处理数据 switch (dataType) { // 位处理(BIT类型) case "BIT_16_1": const [bitIndex] = endian .match(/B(\d+)/) .slice(1) .filter(Boolean); value = (value & 1) << bitIndex; let bb = hexStrTobyteArray(toHex(value, 4)); buffer.push(...bb); break; case "BIT_16_2": case "BIT_16_4": const [startBit, endBit] = endian .match(/B(\d+)(?:B(\d+))?/) .slice(1) .filter(Boolean); // 合法性校验 if (endBit < startBit || startBit > 15 || endBit < 0) { throw new Error("Invalid bit range (0 ≤ endBit ≤ startBit ≤ 15)"); } let bitLength = endBit - startBit + 1; const bitMask = (1 << bitLength) - 1; value = (value & bitMask) << startBit; bb = hexStrTobyteArray(toHex(value, 4)); buffer.push(...bb); break; // 8位整型 case "UINT_8": case "INT_8": let view8 = new DataView(new ArrayBuffer(1)); dataType === "UINT_8" ? view8.setUint8(0, value) : view8.setInt8(0, value); let bytes8 = [...new Uint8Array(view8.buffer)]; // 默认当成低字节,高位补一个字节 bytes8.unshift(0); if (endian.includes("HIGH")) bytes8 = swapBytes(bytes8, 2); buffer.push(...bytes8); break; // 16位整型 case "UINT_16": case "INT_16": let view16 = new DataView(new ArrayBuffer(2)); dataType === "UINT_16" ? view16.setUint16(0, value) : view16.setInt16(0, value); let bytes16 = [...new Uint8Array(view16.buffer)]; if (endian.includes("H1H2")) bytes16 = swapBytes(bytes16, 2); buffer.push(...bytes16); break; // 32位整型/浮点型 case "UINT_32": case "INT_32": case "FLOAT": let view32 = new DataView(new ArrayBuffer(4)); switch (dataType) { case "UINT_32": view32.setUint32(0, value); break; case "INT_32": view32.setInt32(0, value); break; case "FLOAT": view32.setFloat32(0, value); break; } let bytes32 = [...new Uint8Array(view32.buffer)]; if (endian.match(/H\d+H\d+H\d+H\d+/)) { const pattern = endian.match(/H(\d)/g).map((h) => parseInt(h[1])); bytes32 = pattern.map((idx) => bytes32[idx - 1]); } buffer.push(...bytes32); break; // 64位整型 case "UINT_64": case "INT_64": let view64 = new DataView(new ArrayBuffer(8)); dataType === "UINT_64" ? view64.setBigUint64(0, BigInt(value)) : view64.setBigInt64(0, BigInt(value)); let bytes64 = [...new Uint8Array(view64.buffer)]; if (endian.includes("H1H8")) bytes64 = swapBytes(bytes64, 8); buffer.push(...bytes64); break; // ASCII和BCD类型 case "ASCII": buffer.push(...Array.from(value.toString()).map((c) => c.charCodeAt(0))); break; case "BCD_8": case "BCD_16": case "BCD_32": const digits = value.toString().split("").map(Number); const bcdBytes = []; for (let i = 0; i < digits.length; i += 2) { bcdBytes.push((digits[i] << 4) | (digits[i + 1] || 0)); } buffer.push(...bcdBytes); break; } return new Uint8Array(buffer); } // parseUpAdapter 方法:解析Modbus响应报文 function parseUpAdapter(params, hexData, paramCode, dataSet) { const param = params.find((p) => p.paramCode === paramCode); if (!param) throw new Error(`Parameter ${paramCode} not found in params`); const { serialNum, dataType, endian, readFuncCode, collectionScale, dispatchScale, calcExpression, } = param; // 解析Modbus响应报文 const registerValue = parseRegisterValue(hexData, dataType, endian); //转发精度 if (dispatchScale === undefined) { dispatchScale = 0; } // 应用数据精度 let value = registerValue; if (collectionScale !== undefined && collectionScale > 0) { let precision = 1.0 / Math.pow(10, collectionScale); if (value.constructor === BigInt) { let bingIntAsNumber = Number(value); value = Number(bingIntAsNumber * precision).toFixed(dispatchScale); } value = Number(value * precision).toFixed(dispatchScale); } // 放入结果集 dataSet[paramCode] = value; // 处理二次计算公式 if (calcExpression) { //预检所有引用参数的合法性 const referencedSerials = []; const matches = calcExpression.match(/#(\d+)/g); if (matches) { referencedSerials.push( ...matches.map((match) => parseInt(match.slice(1))) ); } //检查每个引用的参数是否存在且值有效 try { referencedSerials.forEach((serial) => { const refParam = params.find((p) => p.serialNum === serial); if (!refParam) { throw new Error(`Missing parameter reference: serialNum ${serial}`); } const refParamCode = refParam.paramCode; if (dataSet[refParamCode] === undefined) { throw new Error( `Parameter ${refParamCode} (serial ${serial}) has no value` ); } // 检查参数值类型是否为数值 const refValue = dataSet[refParamCode]; if (typeof refValue !== "number" && typeof refValue !== "bigint") { throw new Error( `Invalid type for parameter ${refParamCode}: ${typeof refValue}` ); } }); } catch (error) { throw new Error( `Pre-check failed for parameter ${paramCode}: ${error.message}` ); } let expression = calcExpression.replace(/#\d+/g, (match) => { const refSerialNum = parseInt(match.match(/\d+/)[0]); //如果序列号refSerialNum在参数集params里面有,并且在dataSet中已经收集了,那么接着计算 const refParam = params.find((p) => p.serialNum === refSerialNum); if (refParam && dataSet[refParam.paramCode] !== undefined) { return dataSet[refParam.paramCode].toString(); } // 如果找不到对应参数,抛出异常 throw new Error(`Parameter not found by serialNum ${refSerialNum}`); }); //去掉公式首尾的中括号 expression = expression.slice(1, -1); // 计算表达式结果(这里可以使用eval,但要注意安全性) try { const calculatedValue = eval(expression); dataSet[paramCode] = calculatedValue; } catch (e) { throw new Error( `Error evaluating expression for ${paramCode}, ${e.message}` ); } } return dataSet; } //解析Modbus协议报文里的数据 function parseRegisterValue(hexData, dataType, endian) { //转换为字节数组 const bytes = hexStrTobyteArray(hexData); //跳过地址域和功能码(前2字节)以及CRC校验,直接处理数据部分 const dataBytes = bytes.slice(3, bytes.length - 2); //根据数据类型和字节序解析 switch (dataType) { case "BIT_16_1": return parseBit(dataBytes[0], dataBytes[1], endian); case "BIT_16_2": return parseTwoBits(dataBytes[0], dataBytes[1], endian); case "BIT_16_4": return parseFourBits(dataBytes[0], dataBytes[1], endian); case "UINT_8": case "INT_8": return parseInt8(dataBytes[0], dataBytes[1], dataType, endian); case "UINT_16": case "INT_16": return parseInt16(dataBytes, dataType, endian); case "UINT_32": case "INT_32": return parseInt32(dataBytes, dataType, endian); case "UINT_64": case "INT_64": return parseInt64(dataBytes, dataType, endian); case "FLOAT": return parseFloat32(dataBytes, endian); case "ASCII": return String.fromCharCode(...dataBytes); case "BCD_8": case "BCD_16": case "BCD_32": return parseBCD(dataBytes, dataType); default: throw new Error("Unsupported data type"); } } //位解析辅助函数 function parseBit(highByte, lowByte, endian) { const [bitIndex] = endian .match(/B(\d+)/) .slice(1) .filter(Boolean); const value = (highByte << 8) | lowByte; return (value >> bitIndex) & 0x01; } function parseTwoBits(highByte, lowByte, endian) { const [start] = endian .match(/B(\d+)(?:B(\d+))?/) .slice(1) .filter(Boolean); const value = (highByte << 8) | lowByte; return (value >> start) & 0x03; } function parseFourBits(highByte, lowByte, endian) { const [start] = endian .match(/B(\d+)(?:B(\d+))?/) .slice(1) .filter(Boolean); const value = (highByte << 8) | lowByte; return (value >> start) & 0x0f; } //字节序处理函数 function parseInt8(highByte, lowByte, dataType, endian) { let view8 = new DataView(new ArrayBuffer(1)); view8.setUint8(0, endian.includes("HIGH") ? highByte : lowByte); return dataType === "INT_8" ? view8.getInt8(0) : view8.getUint8(0); } function parseInt16(bytes, dataType, endian) { let view16 = new DataView(new ArrayBuffer(2)); view16.setUint16( 0, endian.includes("H2H1") ? (bytes[0] << 8) | bytes[1] : (bytes[1] << 8) | bytes[0], false ); return dataType === "INT_16" ? view16.getInt16(0) : view16.getUint16(0); } function parseInt32(bytes, dataType, endian) { const ordered = reorderBytes(bytes, endian); let view32 = new DataView(new ArrayBuffer(4)); view32.setUint32( 0, (ordered[0] << 24) | (ordered[1] << 16) | (ordered[2] << 8) | ordered[3], false ); return dataType === "INT_32" ? view32.getInt32(0) : view32.getUint32(0); } function parseInt64(bytes, dataType, endian) { //处理字节顺序 if (endian.includes("H1H8")) bytes = swapBytes(bytes, 8); let view64 = new DataView(new ArrayBuffer(8)); //根据字节顺序设置字节 for (let i = 0; i < 8; i++) { const index = i; view64.setUint8(i, bytes[index]); } //根据dataType返回对应值 return dataType === "INT_64" ? view64.getBigInt64(0) : view64.getBigUint64(0); } function parseFloat32(bytes, endian) { const ordered = reorderBytes(bytes, endian); let view32 = new DataView(new ArrayBuffer(4)); view32.setUint32( 0, (ordered[0] << 24) | (ordered[1] << 16) | (ordered[2] << 8) | ordered[3], false ); return view32.getFloat32(0); } //BCD解析 function parseBCD(bytes, dataType) { //参数校验 const expectedLength = { BCD_8: 1, BCD_16: 2, BCD_32: 4, }[dataType]; if (!expectedLength) throw new Error("Invalid dataType"); if (bytes.length < expectedLength) throw new Error("Byte length mismatch"); let bcdStr = ""; //遍历每个字节(大端序处理) for (const byte of bytes) { //每个字节拆分为两个BCD数字 const high = (byte >> 4) & 0x0f; //高四位 const low = byte & 0x0f; //低四位 //有效性检查 if (high > 9 || low > 9) throw new Error("Invalid BCD value"); bcdStr += high.toString(); bcdStr += low.toString(); } //转换为数字(保留前导零需返回字符串时可修改) return parseInt(bcdStr, 10); } // 字节重排序(根据网页4的浮点解析规则) function reorderBytes(bytes, endian) { const patterns = { H1H2H3H4: [0, 1, 2, 3], H4H3H2H1: [3, 2, 1, 0], H3H4H1H2: [2, 3, 0, 1], H2H1H4H3: [1, 0, 3, 2], }; return patterns[endian.split("_").slice(-1)[0]].map((i) => bytes[i]); } //根据数据类型获取参数占用寄存器个数,ASCII类型除外,仅支持0x03功能码 function getQuantityByType(dataType) { var quantity = 1; switch (dataType) { case "UINT_32": case "INT_32": case "FLOAT": case "BCD_32": quantity = 2; break; case "UINT_64": case "INT_64": quantity = 4; default: break; } return quantity; } // 工具函数:将数字转换为16进制字符串,并补齐到指定长度 function toHex(num, length) { num = parseInt(num); let hex = num.toString(16).toUpperCase(); while (hex.length < length) hex = "0" + hex; return hex; } // 计算CRC16 function calculateCRC(sendData) { var CRCRegister = 0x0ffff; let bit; for (let i = 0; i < sendData.length; i++) { CRCRegister ^= sendData[i]; for (let j = 0; j < 8; j++) { bit = CRCRegister & 0x0001; CRCRegister = CRCRegister >> 1; if (bit === 1) { CRCRegister ^= 0xa001; } } } const CRC = [CRCRegister & 0x00ff, (CRCRegister >> 8) & 0x00ff]; return CRC; } // 计算CRC32 function calculateCRC32(data) { // 预生成CRC32表(多项式:0xEDB88320,反射算法) const table = new Uint32Array(256); for (let i = 0; i < 256; i++) { let c = i; for (let j = 0; j < 8; j++) { c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; } table[i] = c; } // 初始化CRC值为0xFFFFFFFF let crc = 0xffffffff; // 遍历Uint8Array中的每个字节 for (const byte of data) { crc = (crc >>> 8) ^ table[(crc ^ byte) & 0xff]; } // 最终异或操作并取反 crc = crc ^ 0xffffffff; // 将32位整数转换为字节数组(低位在前) const result = [ crc & 0xff, // 第1字节(最低位) (crc >> 8) & 0xff, // 第2字节 (crc >> 16) & 0xff, // 第3字节 (crc >> 24) & 0xff, // 第4字节(最高位) ]; return result; } // 校验CRC function checkCRC(getData) { const CRC = calculateCRC(getData); if ( CRC[0] === getData[getData.length - 2] && CRC[1] === getData[getData.length - 1] ) { return true; } else { return false; } } //转16进制字符串 function byteArrayToHexString(byteArray) { return Array.from(byteArray) .map((byte) => ("0" + (byte & 0xff).toString(16)).slice(-2)) .join(""); } //16进制字符串转byte数组 function hexStrTobyteArray(hexStr) { if (hexStr.length % 2 !== 0) { hexStr = "0" + hexStr; } const bytes = new Uint8Array(hexStr.length / 2); for (let i = 0; i < hexStr.length; i += 2) { bytes[i / 2] = parseInt(hexStr.substr(i, 2), 16); } return bytes; } function stringToAsciiHex(str, byteLength = 32, padChar = 0) { // 将字符串转换为ASCII码数组 let asciiArray = []; for (let i = 0; i < str.length; i++) { asciiArray.push(str.charCodeAt(i)); } // 不足指定字节数的补指定的字符 while (asciiArray.length < byteLength) { asciiArray.push(padChar); } // 限制为指定字节数 if (asciiArray.length > byteLength) { asciiArray = asciiArray.slice(0, byteLength); } // 转换为16进制字符串 let hexString = ''; for (let i = 0; i < asciiArray.length; i++) { let hex = asciiArray[i].toString(16); // 确保每个字节是两位16进制数 if (hex.length === 1) { hex = '0' + hex; } hexString += hex; } return hexString; } /** * 将整数转换为16进制字符串,支持字节序 * @param {number} num - 要转换的整数 * @param {number} byteLength - 字节长度 * @param {boolean} isLittleEndian - 是否为小端序,默认为false(大端序) * @returns {string} 16进制字符串 */ function integerToHex(num, byteLength = 4, isLittleEndian = false) { // 确保数字是整数 if (!Number.isInteger(num)) { throw new Error('输入必须是整数'); } // 检查数值范围是否适合指定的字节长度 const maxValue = Math.pow(2, byteLength * 8) - 1; if (num > maxValue) { throw new Error(`数值 ${num} 超过 ${byteLength} 字节能表示的最大值 ${maxValue}`); } // 创建字节数组 const bytes = new Array(byteLength); // 提取字节 for (let i = 0; i < byteLength; i++) { // 计算移位量 const shift = isLittleEndian ? i * 8 : (byteLength - 1 - i) * 8; // 提取字节并确保是无符号的 bytes[i] = (num >>> shift) & 0xFF; } // 转换为16进制字符串 let hexString = ''; for (let i = 0; i < byteLength; i++) { let hex = bytes[i].toString(16); if (hex.length === 1) { hex = '0' + hex; } hexString += hex; } return hexString; } /** * 将字节数组解析为ASCII字符串(增强版,支持指定下标范围和长度) * @param {number[]|Uint8Array} byteArray - 字节数组 * @param {Object} options - 配置选项 * @param {number} options.startIndex - 起始下标(默认: 0) * @param {number} options.length - 要解析的长度(默认: 到数组末尾) * @param {boolean} options.stopAtNull - 遇到0字节时是否停止(默认: true) * @param {boolean} options.trimTrailingNulls - 是否去除末尾的0字节(默认: true) * @returns {string} ASCII字符串 */ function byteArrayToAsciiString(byteArray, options = {}) { const { startIndex = 0, length = null, stopAtNull = true, trimTrailingNulls = true } = options; // 验证输入 if (!byteArray || !Array.isArray(byteArray) && !(byteArray instanceof Uint8Array)) { throw new Error('输入必须是字节数组或Uint8Array'); } if (startIndex < 0 || startIndex >= byteArray.length) { throw new Error(`起始下标 ${startIndex} 超出数组范围 [0, ${byteArray.length - 1}]`); } // 计算实际要解析的长度 const maxLength = length !== null ? Math.min(length, byteArray.length - startIndex) : byteArray.length - startIndex; if (maxLength <= 0) { return ''; } let result = ''; let actualLength = 0; for (let i = startIndex; i < startIndex + maxLength; i++) { const byte = byteArray[i]; // 检查是否为有效字节(0-255) if (byte < 0 || byte > 255) { throw new Error(`无效字节值: ${byte},必须在0-255范围内`); } // 如果遇到0字节且设置了stopAtNull,则停止解析 if (stopAtNull && byte === 0) { break; } // 将字节转换为ASCII字符 result += String.fromCharCode(byte); actualLength++; } // 如果设置了trimTrailingNulls,去除末尾的0字节 if (trimTrailingNulls) { let lastValidIndex = result.length - 1; while (lastValidIndex >= 0 && result.charCodeAt(lastValidIndex) === 0) { lastValidIndex--; } result = result.substring(0, lastValidIndex + 1); } return result; } /** * 从十六进制字符串创建字节数组并解析为ASCII字符串(增强版) * @param {string} hexString - 十六进制字符串 * @param {Object} options - 配置选项 * @returns {string} ASCII字符串 */ function hexToAsciiString(hexString, options = {}) { // 移除可能存在的空格和其他分隔符 hexString = hexString.replace(/[^0-9a-fA-F]/g, ''); // 验证十六进制字符串长度 if (hexString.length % 2 !== 0) { throw new Error('十六进制字符串长度必须是偶数'); } // 将十六进制字符串转换为字节数组 const byteArray = []; for (let i = 0; i < hexString.length; i += 2) { const hexByte = hexString.substr(i, 2); const byteValue = parseInt(hexByte, 16); byteArray.push(byteValue); } // 使用字节数组解析函数 return byteArrayToAsciiString(byteArray, options); } /** * 从指定位置开始截取指定数量的元素 * @param {Array} arr - 原数组 * @param {number} start - 起始索引 * @param {number} count - 要截取的元素数量 * @returns {Array} 截取后的新数组 */ function takeElements(arr, start, count) { if (!Array.isArray(arr)) { throw new Error('第一个参数必须是数组'); } const len = arr.length; // 处理边界情况 if (start >= len || count <= 0) { return []; } // 计算结束位置 const end = Math.min(start + count, len); return arr.slice(start, end); } /** * 将字节数组转换为整数(integerToHex的逆向操作) * @param {number[]|Uint8Array} byteArray - 字节数组 * @param {number} byteLength - 字节长度 * @param {boolean} isLittleEndian - 是否为小端序,默认为false(大端序) * @param {number} startIndex - 起始下标,默认为0 * @returns {number} 转换后的整数 */ function bytesToInteger(byteArray, startIndex = 0, byteLength = 4, isLittleEndian = false) { // 验证输入 if (!byteArray || !Array.isArray(byteArray) && !(byteArray instanceof Uint8Array)) { throw new Error('输入必须是字节数组或Uint8Array'); } if (startIndex < 0 || startIndex >= byteArray.length) { throw new Error(`起始下标 ${startIndex} 超出数组范围 [0, ${byteArray.length - 1}]`); } // 检查是否有足够的字节 if (startIndex + byteLength > byteArray.length) { throw new Error(`字节数组长度不足,需要 ${byteLength} 字节,但从下标 ${startIndex} 开始只有 ${byteArray.length - startIndex} 字节`); } // 根据字节序计算整数值 let result = 0; if (isLittleEndian) { // 小端序:低位字节在前 for (let i = 0; i < byteLength; i++) { const byte = byteArray[startIndex + i]; if (byte < 0 || byte > 255) { throw new Error(`无效字节值: ${byte},必须在0-255范围内`); } result += byte * Math.pow(256, i); } } else { // 大端序:高位字节在前 for (let i = 0; i < byteLength; i++) { const byte = byteArray[startIndex + i]; if (byte < 0 || byte > 255) { throw new Error(`无效字节值: ${byte},必须在0-255范围内`); } result += byte * Math.pow(256, byteLength - 1 - i); } } return result; } ``` ### 附录2:固件升级 以下内容为ChatGPT提供,仅供参考 以下是通过微信小程序实现蓝牙固件升级的具体流程: (1) 初始化蓝牙模块 ``` wx.openBluetoothAdapter({ success(res) { console.log('Bluetooth adapter initialized:', res) }, fail(err) { console.error('Failed to initialize Bluetooth adapter:', err) } }); (2) 扫描和连接设备 • 扫描设备: wx.startBluetoothDevicesDiscovery({ success(res) { console.log('Scanning for devices...', res); }, fail(err) { console.error('Failed to start scanning:', err); } }); ``` (2) 连接设备: ``` wx.createBLEConnection({ deviceId: targetDeviceId, // 从扫描结果中获取设备 ID success(res) { console.log('Connected to device:', res); }, fail(err) { console.error('Failed to connect to device:', err); } }); ``` (3) 发现服务和特性 获取设备的 GATT 服务: ``` wx.getBLEDeviceServices({ deviceId: targetDeviceId, success(res) { console.log('Services discovered:', res.services); } }); ``` 获取特定服务的特性(Characteristic): ``` wx.getBLEDeviceCharacteristics({ deviceId: targetDeviceId, serviceId: targetServiceId, // 固件传输相关服务的 UUID success(res) { console.log('Characteristics discovered:', res.characteristics); } }); ``` (4) 切换到升级模式 通过写入特定指令,将设备切换到升级模式: ``` wx.writeBLECharacteristicValue({ deviceId: targetDeviceId, serviceId: targetServiceId, characteristicId: controlCharacteristicId, // 控制特性的 UUID value: bufferForUpgradeCommand, // 升级指令的数据 success(res) { console.log('Entered upgrade mode:', res); } }); ``` (5) 分块传输固件 由于 BLE 每次写入的字节数有限,需要将固件文件切分成小块。 - 读取固件文件: 使用微信小程序的文件读取 API 加载固件文件(如通过下载或本地缓存)。 ``` wx.getFileSystemManager().readFile({ filePath: firmwarePath, success(res) { let firmwareBuffer = res.data; // 固件文件内容 sendFirmwareChunks(firmwareBuffer); } }); function sendFirmwareChunks(buffer) { const chunkSize = 20; // 每次写入20字节 let offset = 0; function sendNextChunk() { if (offset >= buffer.byteLength) { console.log('Firmware transfer complete'); return; } let chunk = buffer.slice(offset, offset + chunkSize); wx.writeBLECharacteristicValue({ deviceId: targetDeviceId, serviceId: targetServiceId, characteristicId: dataCharacteristicId, // 数据特性 UUID value: chunk, success(res) { console.log('Chunk sent:', chunk); offset += chunkSize; sendNextChunk(); // 发送下一个块 }, fail(err) { console.error('Failed to send chunk:', err); } }); } sendNextChunk(); } ``` (6) 校验和完成升级 传输完成后,通知设备校验接收到的固件完整性,并应用固件。 ``` wx.writeBLECharacteristicValue({ deviceId: targetDeviceId, serviceId: targetServiceId, characteristicId: controlCharacteristicId, value: bufferForCompleteCommand, // 通知完成的指令 success(res) { console.log('Upgrade process complete:', res); } }); ``` > 注意事项 > > 1. 分块传输效率: > - 根据设备的 MTU(最大传输单元),调整每次传输的字节数以提高效率。 > 2. 错误处理: > - 处理可能出现的连接丢失、写入失败等问题。 > - 添加断点续传功能。 > 3. 蓝牙权限: > - 确保用户在手机系统设置中授予微信蓝牙权限。 > 4. 用户交互: > - 在升级过程中显示进度条,提示用户不要断开设备或退出小程序。 (7) 示例完整流程代码 ``` wx.openBluetoothAdapter({ success(res) { wx.startBluetoothDevicesDiscovery({ success(res) { // 假设发现目标设备 let targetDeviceId = 'DEVICE_ID'; wx.createBLEConnection({ deviceId: targetDeviceId, success(res) { // 假设找到固件服务和特性 let firmwareBuffer = getFirmwareBuffer(); // 读取固件 sendFirmwareChunks(firmwareBuffer); } }); } }); } }); ```
王旭东
2025年10月21日 10:57
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码