Bump to 1.0.4.

1. App : Add HelpView.
2. SerialPortAssistant: Add input validation.
This commit is contained in:
Mentalflow 2024-03-16 01:14:22 +08:00
parent dde53267ea
commit 0a340aff4b
Signed by: Mentalflow
GPG Key ID: 5AE68D4401A2EE71
41 changed files with 12499 additions and 284 deletions

View File

@ -35,7 +35,7 @@ jobs:
with: with:
version: ${{ matrix.qt_ver }} version: ${{ matrix.qt_ver }}
arch: ${{ matrix.qt_arch }} arch: ${{ matrix.qt_arch }}
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport' modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport qtwebview'
- name: Set up Ninja - name: Set up Ninja
uses: seanmiddleditch/gha-setup-ninja@v3 uses: seanmiddleditch/gha-setup-ninja@v3

View File

@ -35,7 +35,7 @@ jobs:
with: with:
version: ${{ matrix.qt_ver }} version: ${{ matrix.qt_ver }}
arch: ${{ matrix.qt_arch }} arch: ${{ matrix.qt_arch }}
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport' modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport qtwebview'
- name: Set up Ninja - name: Set up Ninja
uses: seanmiddleditch/gha-setup-ninja@v3 uses: seanmiddleditch/gha-setup-ninja@v3

View File

@ -36,7 +36,7 @@ jobs:
with: with:
version: ${{ matrix.qt_ver }} version: ${{ matrix.qt_ver }}
arch: ${{ matrix.qt_arch }} arch: ${{ matrix.qt_arch }}
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport' modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport qtwebview'
- name: Set up Ninja - name: Set up Ninja
uses: seanmiddleditch/gha-setup-ninja@v3 uses: seanmiddleditch/gha-setup-ninja@v3

View File

@ -36,7 +36,7 @@ jobs:
with: with:
version: ${{ matrix.qt_ver }} version: ${{ matrix.qt_ver }}
arch: ${{ matrix.qt_arch }} arch: ${{ matrix.qt_arch }}
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport' modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport qtwebview'
- name: Set up Ninja - name: Set up Ninja
uses: seanmiddleditch/gha-setup-ninja@v3 uses: seanmiddleditch/gha-setup-ninja@v3

View File

@ -43,7 +43,7 @@ jobs:
with: with:
version: ${{ matrix.qt_ver }} version: ${{ matrix.qt_ver }}
arch: ${{ matrix.qt_arch }} arch: ${{ matrix.qt_arch }}
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport' modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport qtwebview'
- name: Qt6 environment configuration - name: Qt6 environment configuration
if: ${{ startsWith( matrix.qt_ver, 6 ) }} if: ${{ startsWith( matrix.qt_ver, 6 ) }}

View File

@ -43,7 +43,7 @@ jobs:
with: with:
version: ${{ matrix.qt_ver }} version: ${{ matrix.qt_ver }}
arch: ${{ matrix.qt_arch }} arch: ${{ matrix.qt_arch }}
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport' modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport qtwebview'
- name: Qt6 environment configuration - name: Qt6 environment configuration
if: ${{ startsWith( matrix.qt_ver, 6 ) }} if: ${{ startsWith( matrix.qt_ver, 6 ) }}

View File

@ -46,7 +46,7 @@ jobs:
with: with:
version: ${{ matrix.qt_ver }} version: ${{ matrix.qt_ver }}
arch: ${{ matrix.qt_arch }} arch: ${{ matrix.qt_arch }}
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport' modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport qtwebview'
- name: msvc-build - name: msvc-build
id: build id: build

View File

@ -46,7 +46,7 @@ jobs:
with: with:
version: ${{ matrix.qt_ver }} version: ${{ matrix.qt_ver }}
arch: ${{ matrix.qt_arch }} arch: ${{ matrix.qt_arch }}
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport' modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport qtwebview'
- name: msvc-build - name: msvc-build
id: build id: build

2
3rdparty/RibbonUI vendored

@ -1 +1 @@
Subproject commit 18edf9f0fdf0f11663d328a8166486226246e656 Subproject commit c8d7d6d4689b422bdbe7cf1ef0cdcaf2948aeee6

View File

@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.20) cmake_minimum_required(VERSION 3.20)
project(ProtocolParser_APP VERSION 1.0.3 LANGUAGES CXX) project(ProtocolParser_APP VERSION 1.0.4 LANGUAGES CXX)
set(RIBBONUI_BUILD_FRAMELESSHEPLER ON) set(RIBBONUI_BUILD_FRAMELESSHEPLER ON)
set(RIBBONUI_BUILD_EXAMPLES OFF) set(RIBBONUI_BUILD_EXAMPLES OFF)

291
README.md
View File

@ -2,10 +2,10 @@
<div align="center"> <div align="center">
<img src="app_source/resources/imgs/icon.png" alt="Logo" style="width:40%; height:auto;"> <img src="app_source/resources/imgs/icon.png" alt="Logo" style="width:40%; height:auto;">
</div> </div>
<h1 align="center"> ProtocolParser 协议解析器 V1.0.3</h1> <h1 align="center"> ProtocolParser 协议解析器 V1.0.4</h1>
## 一、介绍 ## 一、介绍
这是一个用于与DKY的THM3682实验箱搭配使用的上位机协议解析器具体协议如「通讯协议解析」所示,后续功能使用说明待补充 这是一个用于与DKY的THM3682实验箱搭配使用的上位机协议解析器具体如何使用请看[使用说明](#二使用说明)
<div align="center"> <div align="center">
<div align="center"> <div align="center">
<img src="documents/pictures/home-light.png" alt="主界面(浅色)" style="width:45%; height:auto;"> <img src="documents/pictures/home-light.png" alt="主界面(浅色)" style="width:45%; height:auto;">
@ -14,271 +14,22 @@
<p align="center"> 主界面(浅色/深色模式) </p> <p align="center"> 主界面(浅色/深色模式) </p>
</div> </div>
## 二、通讯协议解析 ## 二、使用说明
## 1. [协议解析](documents/protocol.md)
- [通讯协议解析](#通讯协议解析) + 概述
- [1. 概述](#1-概述) + 密钥
- [2. 密钥](#2-密钥) + 认证流程
- [3. 认证流程](#3-认证流程) + 数据结构
- [3.1 非对称双向认证](#31-非对称双向认证) + 接口解析
- [3.2 对称双向认证](#32-对称双向认证) ## 2. [串口使用](documents/serialport.md)
- [4. 数据结构](#4-数据结构) + 参数配置
- [4.1 基础帧base\_frame](#41-基础帧base_frame) + 串口消息设置
- [4.2 数据帧data\_frame](#42-数据帧data_frame) + 串口助手设置
- [4.3 数字信封digi\_env](#43-数字信封digi_env) + 串口数据流向
- [4.4 非对称双向认证包ssl\_frame](#44-非对称双向认证包ssl_frame) ## 3. [紫蜂协议](documents/zigbee.md)
- [4.5 对称双向认证包hmac\_frame](#45-对称双向认证包hmac_frame) + 设备列表
- [4.6 对称加密数据帧crypto\_zdata\_frame](#46-对称加密数据帧crypto_zdata_frame) + 密钥管理
- [4.7 设备信息存储device](#47-设备信息存储device) + 调试选项
- [5. 接口解析](#5-接口解析) ## 4. [其他](documents/others.md)
- [5.1 协议封装/解析](#51-协议封装解析) + 主题
- [5.1.1 void protocal\_wrapper(data\_frame \*frame, u8 type, u16 length, u8 \*data, bool use\_crc)](#511-void-protocal_wrapperdata_frame-frame-u8-type-u16-length-u8-data-bool-use_crc) + 数据结构自定义
- [5.1.2 void base\_frame\_maker(void \*in\_frame, base\_frame \*out\_frame, u16 dest\_addr,device \*dev,u16 node\_addr=0)](#512-void-base_frame_makervoid-in_frame-base_frame-out_frame-u16-dest_addrdevice-devu16-node_addr0)
- [5.1.3 bool base\_frame\_parser(base\_frame \*in\_frame, void \*\*out\_frame, device \*dev)](#513-bool-base_frame_parserbase_frame-in_frame-void-out_frame-device-dev)
- [5.1.4 void ssl\_frame\_maker(ssl\_frame \*frame, u8 \*data, int data\_len)](#514-void-ssl_frame_makerssl_frame-frame-u8-data-int-data_len)
- [5.1.5 void zigbee\_data\_encrypt(data\_frame \*data, crypto\_zdata\_frame *zdata, bool (* SM4\_encrypt)(u8 \*key\_origin, u32 key\_len, u8 \*in\_origin, u32 in\_len, u8 \*out, u32 \*out\_len, bool use\_real\_cbc),QString en\_key = "")](#515-void-zigbee_data_encryptdata_frame-data-crypto_zdata_frame-zdata-bool--sm4_encryptu8-key_origin-u32-key_len-u8-in_origin-u32-in_len-u8-out-u32-out_len-bool-use_real_cbcqstring-en_key--)
- [5.1.6 bool zigbee\_data\_dectypt(data\_frame \*data, crypto\_zdata\_frame *zdata,bool (* SM4\_decrypt)(u8 \*key\_origin, u32 key\_len, u8 \*in, u32 in\_len, u8 \*out, u32 \*out\_len, bool use\_real\_cbc),QString en\_key = "")](#516-bool-zigbee_data_dectyptdata_frame-data-crypto_zdata_frame-zdatabool--sm4_decryptu8-key_origin-u32-key_len-u8-in-u32-in_len-u8-out-u32-out_len-bool-use_real_cbcqstring-en_key--)
- [5.2 协议认证/验证](#52-协议认证验证)
- [5.2.1 void HMAC\_identify(device \*self, device \*node, hmac\_frame \*hframe, void (\*sendTonode)(ZigbeeFrame \&data), void (\*SM3\_HMAC)(u8 \*key, int keylen,u8 \*input, int ilen,u8 output\[32\]))](#521-void-hmac_identifydevice-self-device-node-hmac_frame-hframe-void-sendtonodezigbeeframe-data-void-sm3_hmacu8-key-int-keylenu8-input-int-ilenu8-output32)
- [5.2.2 bool data\_frame\_verify(data\_frame \*frame)](#522-bool-data_frame_verifydata_frame-frame)
- [5.2.3 void HMAC\_changeVerifykey(u8 key\[16\], device\* self, device \*node, void (*sendTonode)(ZigbeeFrame \&data),bool (* SM4\_encrypt)(u8 \*key\_origin, u32 key\_len, u8 \*in\_origin, u32 in\_len, u8 \*out, u32 \*out\_len, bool use\_real\_cbc))](#523-void-hmac_changeverifykeyu8-key16-device-self-device-node-void-sendtonodezigbeeframe-databool--sm4_encryptu8-key_origin-u32-key_len-u8-in_origin-u32-in_len-u8-out-u32-out_len-bool-use_real_cbc)
- [5.3 工具](#53-工具)
- [5.3.1 uint16\_t crc16\_xmodem(const uint8\_t \*buffer, uint32\_t buffer\_length)](#531-uint16_t-crc16_xmodemconst-uint8_t-buffer-uint32_t-buffer_length)
- [5.3.2 bool bytecmp(u8 \*a, u8 \*b, u16 length)](#532-bool-bytecmpu8-a-u8-b-u16-length)
## 1. 概述
本协议是一套运行在应用层的,用于端到端通讯的轻量级安全协议,支持非对称/对称的双向、单向认证及加密通讯。该协议的认证和加密通讯由SM系列SM2、SM3、SM4算法保障安全性较强可抵御重放攻击。协议易拓展数据包采用二进制传输适合在多种窄带宽场景下使用目前已在嵌入式、ARM、X86/64平台上测试通过
## 2. 密钥
本协议采用两类密钥一是用于双向认证的SM2公私钥对二是用于对称双、单向认证和加密传输的SM4密钥。
+ SM2密钥长度为公钥64Bytes512Bits私钥32 Bytes256Bits。密文长度为明文长度再加上96Bytes。签名长度为64Bytes被签名数据内包含随机数盐值因此对同一数据签名的结果不相同
+ SM4密钥长度为16Bytes128Bits
## 3. 认证流程
### 3.1 非对称双向认证
由于SM2算法会占用一部分资源非对称双向认证一般用于性能较好的终端如性能较强的单片机、树莓派或者PC等设备之间的认证性能较差的单片机将会出现死机等预料外情况板载硬件密码算法模块除外
```mermaid
sequenceDiagram
participant 服务器
participant 客户端
客户端-->服务器: 客户端验证服务器
客户端->>服务器: 包含Hello信息的ssl_frame请求
服务器->>客户端: 包含Hello和64Bytes服务器公钥信息的ssl_frame响应
客户端-->>客户端: 用服务器公钥加密自己的公钥
客户端->>服务器: 包含服务器公钥加密的客户端公钥信息的ssl_frame数据包
服务器-->>服务器: 用自己的私钥解密出客户端公钥
服务器->>客户端: 包含用客户端公钥加密的Verified信息的ssl_frame数据包
客户端-->>客户端: 用自己的私钥解密出Verified信息
客户端->>服务器: 包含服务器公钥加密的OK信息的ssl_frame数据包
服务器-->客户端: 服务器验证客户端
服务器-->>服务器: 生成8Bytes随机数挑战值
服务器->>客户端: 包含客户端公钥加密的8Bytes挑战值信息的ssl_frame数据包
客户端-->>客户端: 用自己的私钥解密出挑战值,<br>并用自己的私钥对它签名
客户端->>服务器: 包含客户端公钥64Bytes、签名64Bytes<br>挑战值8Bytes共136Bytes数据的ssl_frame数据包
服务器-->>服务器: 进行三轮比对:<br>1.将获得的客户端公钥与<br>第一轮验证中获取的客户端公钥进行比对<br>2.将获得的挑战值与发送的进行比对<br>3.用客户端公钥对挑战值签名数据进行验签
服务器->>客户端: 包含客户端公钥加密的OK信息的ssl_frame数据包
服务器-->客户端: 双向认证结束
```
### 3.2 对称双向认证
对称双向认证使用基于SM3的HMAC算法实现。通过验证双方是否共同持有同一对预设定密钥来实现认证在实际运用过程中为节省资源服务器发送给客户端的Identified信息可不加密采用明文传输因为后续传输的数据包均为加密数据包
```mermaid
sequenceDiagram
participant 服务器
participant 客户端
客户端-->>客户端: 生成1Byte随机数并对其使用基于SM3的HMAC算法和<br>预设定16Bytes密钥生成值
客户端->>服务器: 包含随机数和使用预设定16Bytes密钥生成的<br>HMAC值信息的hmac_frame请求
服务器-->>服务器: 使用自己拥有的预设定16Bytes密钥对收到的<br>1Byte随机数进行HMAC运算并将值与收到值进行比对
服务器->>客户端: 包含使用预设定16Bytes密钥加密的<br>Identified信息的crypto_zdata_frame数据包
客户端-->>客户端: 用预设定16Bytes密钥解密出Identified信息
客户端->>服务器: 包含正常数据的用预设定密钥加密的<br>crypto_zdata_frame数据包
```
## 4. 数据结构
### 4.1 基础帧base_frame
基础帧是本协议传输最底层的帧,用于承载其他帧。
| 帧结构 | 长度Bytes | 说明 |
|:----:|:----:|:----:|
| head | 2 | 包头,数值固定为`0xAAAD` |
| ori_addr | 2 | 源地址,即该帧发送方的地址 |
| des_addr | 2 | 目的地址,即该帧接收方的地址 |
| node_addr | 2 | 节点地址,仅在此帧被服务器进行路由时使用,<br>用以标记该帧由哪个节点最先产生 |
| id | 2 | 帧标号,用以与`rand_num`共同唯一标记一个帧,防止重放攻击 |
| length | 2 | 帧长度包括帧头最大为65535个Bits |
| reset_num | 1 | 重置标记位,用以告知对方在接收此帧后,<br>下一帧的帧标号将归零,`rand_num`将被重新生成 |
| rand_num | 1 | 随机数 |
| data | 可变长度 | 数据最大不超过8191 - `BASE_FRAME_PREFIX_LEN`= 8177 |
另有其他说明如下:
+ 宏定义`BASE_FRAME_PREFIX_LEN`基础帧帧头长度固定为14Bytes
+ 宏定义`BASE_FRAME_HEAD`:基础帧帧头,固定为`0xAAAD`
+ 宏定义`BASE_FRAME_RESET_NUM`:基础帧重置阈值:当发送帧数达到该阈值时,下一帧将被重置帧标号和随机数,该阈值一般为`10000`,最大不超过`65535`
+ 宏定义`new_base_frame(num)`:用以生成基础帧数据结构,`num`为`data`部分长度,使用时请注意强制类型转换
### 4.2 数据帧data_frame
数据帧是本协议传输中最顶层的帧,用于承载各类数据。
| 帧结构 | 长度Bytes | 说明 |
|:----:|:----:|:----:|
| head | 2 | 包头,数值固定为`0xAAAA` |
| type | 1 | 数据类型,由用户根据业务不同自定义 |
| use_crc | 1 | CRC标记位当其为`0xFF`时将对数据进行CRC校验 |
| data_length | 2 | 数据长度,不包括包头 |
| crc | 2 | CRC16校验码当CRC标记位使能且对数据进行CRC校验后数值与该码不同包将被判定为损坏 |
| data | 可变长度 | 根据底层帧的不同数据最大不超过8191 - `BASE_FRAME_PREFIX_LEN` - `DATA_FRAME_PREFIX_LEN`= 8169若叠加多重帧需要减去对应帧的帧头 |
另有其他说明如下:
+ 宏定义`DATA_FRAME_PREFIX_LEN`数据帧帧头长度固定为8Bytes
+ 宏定义`DATA_FRAME_HEAD`:数据帧帧头,固定为`0xAAAA`
+ 宏定义`new_data_frame(num)`:用以生成数据帧数据结构,`num`为`data`部分长度,使用时请注意强制类型转换
### 4.3 数字信封digi_env
数字信封是通信双方进行非对称双向认证后,用于加密传输数据的数据结构。
| 帧结构 | 长度Bytes | 说明 |
|:----:|:----:|:----:|
| head | 2 | 包头,数值固定为`0xAAAB` |
| length | 2 | 长度,包括包头 |
| crypted_session_key | 112 | 由接收方公钥加密的16Bytes会话密钥再加上96Bytes的额外数据 |
| data | 可变长度 | 由会话密钥进行SM4加密的数据长度数据最大不超过8191 - `BASE_FRAME_PREFIX_LEN` - `DIGI_ENV_PREFIX_LEN` - `DIGI_ENV_SESSION_KEY_LEN`= 8061又因SM4的密文为16的倍数因此最大不能超过8048 |
另有其他说明如下:
+ 宏定义`SM4_PADDING_LEN`SM4默认填充长度固定为16Bytes
+ 宏定义`DIGI_ENV_PREFIX_LEN`数字信封包头长度固定为4Bytes
+ 宏定义`DIGI_ENV_SESSION_KEY_LEN`数字信封密态会话密钥长度固定为112Bytes
+ 宏定义`DIGI_ENV_HEAD`:数字信封包头,固定为`0xAAAB`
+ 宏定义`new_digi_env(num)`:用以生成数字信封数据结构,`num`为`data`部分长度,使用时请注意强制类型转换
### 4.4 非对称双向认证包ssl_frame
非对称双向认证包负责承载非对称双向认证相关数据。
| 帧结构 | 长度Bytes | 说明 |
|:----:|:----:|:----:|
| head | 2 | 包头,数值固定为`0xAAAC` |
| length | 2 | 长度,包括包头 |
| data | 可变长度 | 数据最大不超过8191 - `BASE_FRAME_PREFIX_LEN` - `SSL_FRAME_PREFIX_LEN`= 8173 |
另有其他说明如下:
+ 宏定义`SSL_FRAME_PREFIX_LEN`非对称双向认证包包头长度固定为4Bytes
+ 宏定义`SSL_FRAME_HEAD`:非对称双向认证包包头,固定为`0xAAAC`
+ 宏定义`new_ssl_frame(num)`:用以生成非对称双向认证包数据结构,`num`为`data`部分长度,使用时请注意强制类型转换
### 4.5 对称双向认证包hmac_frame
对称双向认证包负责承载对称双向认证相关数据。
| 帧结构 | 长度Bytes | 说明 |
|:----:|:----:|:----:|
| head | 2 | 包头,数值固定为`0xAAAE` |
| length | 2 | 长度,包括包头 |
| value | 1 | 随机数 |
| hmac | 32 | 随机数`value`对应的HMAC数值 |
另有其他说明如下:
+ 宏定义`HMAC_FRAME_PREFIX_LEN`对称双向认证包包头长度固定为4Bytes
+ 宏定义`HMAC_FRAME_HEAD`:非对称双向认证包包头,固定为`0xAAAE`
### 4.6 对称加密数据帧crypto_zdata_frame
对称加密数据帧用于通信双方完成对称双向认证后数据加密传输。
| 帧结构 | 长度Bytes | 说明 |
|:----:|:----:|:----:|
| head | 2 | 包头,数值固定为`0xAAAF` |
| length | 2 | 长度,包括包头 |
| crc | 2 | CRC16校验码,对解密数据进行CRC校验后数值与该码不同将被判定为解密失败 |
| data | 可变长度 | 数据最大不超过8191 - `BASE_FRAME_PREFIX_LEN` - `CRYPTO_ZDATA_FRAME_PREFIX_LEN`= 8171 |
另有其他说明如下:
+ 宏定义`CRYPTO_ZDATA_FRAME_PREFIX_LEN`对称加密数据帧帧头长度固定为6Bytes
+ 宏定义`CRYPTO_ZDATA_FRAME_HEAD`:对称加密数据帧帧头,固定为`0xAAAF`
### 4.7 设备信息存储device
协议采用`device`结构体用作存储相关通讯数据。
| 结构 | 说明 |
|:----:|:----:|
| addr | 用以保存通讯对象的地址(可以是发送方,也可以是接收方) |
| id | 缓存发送/接收到的基础帧帧标号 |
| rand_num | 缓存发送/接收到的基础帧随机数 |
| verified | 标记该通讯对象是否通过认证 |
| online | 标记该通讯对象是否在线 |
| logined | 仅作为服务器时使用,记录该通讯对象是否已经登录 |
| stage | 仅在非对称双向认证时使用,记录该通讯对象正处于认证的第几阶段 |
| chlg_buf | 仅在非对称双向认证时使用,记录该通讯对象的挑战值 |
| ip | 仅在TCP链路中使用记录该通讯对象的IP地址 |
| port | 仅在TCP链路中使用记录该通讯对象的端口号 |
| key_pair | 仅在非对称双向认证时使用,记录该通讯对象的公钥和会话密钥 |
## 5. 接口解析
### 5.1 协议封装/解析
#### 5.1.1 void protocal_wrapper(data_frame *frame, u8 type, u16 length, u8 *data, bool use_crc)
将对应数据打包进数据帧中。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| frame | 输出 | 数据帧指针 |
| type | 输入 | 数据类型 |
| length | 输入 | 数据长度 |
| use_crc | 输入 | 是否启用CRC验证 |
#### 5.1.2 void base_frame_maker(void *in_frame, base_frame *out_frame, u16 dest_addr,device *dev,u16 node_addr=0)
将对应数据打包进基础帧中。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| in_frame | 输入 | 数据包指针,指向需要被打包进基础帧的数据地址 |
| out_frame | 输出 | 基础帧指针 |
| dest_addr | 输入 | 目的地址 |
| dev | 输出 | 发送方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
| node_addr | 输入 | 仅在服务器端路由时使用,用以保存该基础帧的最初创建者地址 |
#### 5.1.3 bool base_frame_parser(base_frame *in_frame, void **out_frame, device *dev)
从基础帧中解析出数据,返回`true`为解析成功,`false`为解析失败,需要检查包基础帧数据内容是否错误。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| in_frame | 输入 | 基础帧指针 |
| out_frame | 输出 | 数据包指针,指向需要从基础帧中解析出的数据包地址 |
| dev | 输出 | 接收方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
#### 5.1.4 void ssl_frame_maker(ssl_frame *frame, u8 *data, int data_len)
将相关数据打包进非对称双向认证包中。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| in_frame | 输出 | 非对称双向认证包指针,指向需要被打包进非对称双向认证包的数据地址 |
| data | 输入 | 数据指针 |
| data_len | 输入 | 数据长度 |
#### 5.1.5 void zigbee_data_encrypt(data_frame *data, crypto_zdata_frame *zdata, bool (* SM4_encrypt)(u8 *key_origin, u32 key_len, u8 *in_origin, u32 in_len, u8 *out, u32 *out_len, bool use_real_cbc),QString en_key = "")
对数据进行对称加密。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| data | 输入 | 待加密的数据指针 |
| zdata | 输出 | 待填充的对称加密数据包指针 |
| SM4_encrypt | 输入 | SM4加密算法函数指针 |
| en_key | 输入 | 若不提供密钥,则将使用默认密钥进行加密 |
#### 5.1.6 bool zigbee_data_dectypt(data_frame *data, crypto_zdata_frame *zdata,bool (* SM4_decrypt)(u8 *key_origin, u32 key_len, u8 *in, u32 in_len, u8 *out, u32 *out_len, bool use_real_cbc),QString en_key = "")
对数据进行对称解密,返回`true`代表解密成功,`false`代表解密失败,需要检查输入是否错误。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| data | 输出 | 待解密的数据地址指针 |
| zdata | 输入 | 收到的对称加密数据包指针 |
| SM4_decrypt | 输入 | SM4解密算法函数指针 |
| en_key | 输入 | 若不提供密钥,则将使用默认密钥进行解密 |
### 5.2 协议认证/验证
#### 5.2.1 void HMAC_identify(device *self, device *node, hmac_frame *hframe, void (*sendTonode)(ZigbeeFrame &data), void (*SM3_HMAC)(u8 *key, int keylen,u8 *input, int ilen,u8 output[32]))
对接收到的HMAC包进行数据认证。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| self | 输出 | 接收方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
| node | 输出 | 发送方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
| hframe | 输入 | 接收到的对称双向认证包指针 |
| sendTonode | 输入 | 发送数据函数的指针 |
#### 5.2.2 bool data_frame_verify(data_frame *frame)
对数据帧进行验证,返回`true`代表包有效,`false`代表包损坏。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| frame | 输入 | 待检验的数据帧指针 |
#### 5.2.3 void HMAC_changeVerifykey(u8 key[16], device* self, device *node, void (*sendTonode)(ZigbeeFrame &data),bool (* SM4_encrypt)(u8 *key_origin, u32 key_len, u8 *in_origin, u32 in_len, u8 *out, u32 *out_len, bool use_real_cbc))
发送对称双向认证密钥更换指令包。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| key | 输入 | 新密钥 |
| self | 输出 | 接收方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
| node | 输出 | 发送方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
| sendTonode | 输入 | 发送数据函数的指针 |
| SM4_encrypt | 输入 | SM4加密算法函数指针 |
### 5.3 工具
#### 5.3.1 uint16_t crc16_xmodem(const uint8_t *buffer, uint32_t buffer_length)
生成CRC16校验码。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| buffer | 输入 | 需要生成校验码的数据指针 |
| buffer_length | 输入 | 需要生成校验码的数据长度 |
#### 5.3.2 bool bytecmp(u8 *a, u8 *b, u16 length)
将两个输入进行逐字比较,即`memcmp`,返回`true`代表完全一致,`false`代表存在差异。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| a | 输入 | 需对比数据A |
| b | 输入 | 需对比数据B |
| length | 输入 | 需对比数据长度 |

View File

@ -26,7 +26,7 @@ set(PROJECT_BUNDLE_NAME ${PROJECT_NAME})
set(version_str "${CMAKE_PROJECT_VERSION_MAJOR},${CMAKE_PROJECT_VERSION_MINOR},${CMAKE_PROJECT_VERSION_PATCH}") set(version_str "${CMAKE_PROJECT_VERSION_MAJOR},${CMAKE_PROJECT_VERSION_MINOR},${CMAKE_PROJECT_VERSION_PATCH}")
add_definitions(-DPROTOCOLPARSER_VERSION=${version_str}) add_definitions(-DPROTOCOLPARSER_VERSION=${version_str})
find_package(Qt6 COMPONENTS Quick SerialPort LinguistTools REQUIRED) find_package(Qt6 COMPONENTS Quick SerialPort LinguistTools WebView REQUIRED)
set( set(
sources_files source/main.cpp include/serialportmanager.h source/serialportmanager.cpp include/eventsbus.h source/eventsbus.cpp sources_files source/main.cpp include/serialportmanager.h source/serialportmanager.cpp include/eventsbus.h source/eventsbus.cpp
@ -37,6 +37,7 @@ set(
include/serialdataresolver.h source/serialdataresolver.cpp include/serialdataresolver.h source/serialdataresolver.cpp
include/zigbeedataresolver.h source/zigbeedataresolver.cpp include/zigbeedataresolver.h source/zigbeedataresolver.cpp
include/eventbusresolver.h source/eventbusresolver.cpp include/eventbusresolver.h source/eventbusresolver.cpp
include/tools.h source/tools.cpp
languages/zh_CN.ts languages/en_US.ts languages/zh_CN.ts languages/en_US.ts
) )
@ -46,7 +47,18 @@ set(
qml/components/ZigBeeMessage.qml qml/components/SerialPortAssistant.qml qml/components/ZigBeeMessage.qml qml/components/SerialPortAssistant.qml
qml/components/ZigBeeDataView.qml qml/components/TabBar.qml qml/components/CenterView.qml qml/components/ZigBeeDataView.qml qml/components/TabBar.qml qml/components/CenterView.qml
qml/components/ListTable.qml qml/components/DeviceList.qml qml/components/KeysList.qml qml/components/ListTable.qml qml/components/DeviceList.qml qml/components/KeysList.qml
qml/components/EventsHistoryList.qml qml/components/FrameChooser.qml qml/components/EventsHistoryList.qml qml/components/FrameChooser.qml qml/components/RibbonMarkDownViewer.qml
qml/components/HelpView.qml
)
set(js_files js/markdown-it.js js/markdown-it-deflist.js js/markdown-it-emoji.js
js/markdown-it-sub.js js/markdown-it-abbr.js js/markdown-it-container.js js/markdown-it-footnote.js
js/markdown-it-ins.js js/markdown-it-mark.js js/markdown-it-sup.js js/prism.js
)
set(doc_files ${CMAKE_SOURCE_DIR}/documents/menu.md ${CMAKE_SOURCE_DIR}/documents/protocol.md
${CMAKE_SOURCE_DIR}/documents/serialport.md ${CMAKE_SOURCE_DIR}/documents/zigbee.md
${CMAKE_SOURCE_DIR}/documents/others.md
) )
INCLUDE_DIRECTORIES(dlln3x include) INCLUDE_DIRECTORIES(dlln3x include)
@ -97,10 +109,21 @@ foreach(qmlfile ${qml_files})
set_source_files_properties(${qmlfile} PROPERTIES QT_RESOURCE_ALIAS ${fixedfile}) set_source_files_properties(${qmlfile} PROPERTIES QT_RESOURCE_ALIAS ${fixedfile})
endforeach(qmlfile) endforeach(qmlfile)
foreach(jsfile ${js_files})
string(REPLACE "js/" "" fixedfile ${jsfile})
set_source_files_properties(${jsfile} PROPERTIES QT_RESOURCE_ALIAS ${fixedfile})
endforeach(jsfile)
foreach(docfile ${doc_files})
string(REPLACE "${CMAKE_SOURCE_DIR}/" "" fixedfile ${docfile})
set_source_files_properties(${docfile} PROPERTIES QT_RESOURCE_ALIAS ${fixedfile})
endforeach(docfile)
qt_add_qml_module(${PROJECT_NAME} qt_add_qml_module(${PROJECT_NAME}
URI ${PROJECT_NAME} URI ${PROJECT_NAME}
QML_FILES ${qml_files} QML_FILES ${qml_files}
RESOURCE_PREFIX "/qt/qml/" RESOURCE_PREFIX "/qt/qml/"
RESOURCES ${js_files} ${doc_files} resources/theme.css resources/default.css resources/prism-light.css resources/prism-dark.css
VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR} VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}
) )
@ -125,6 +148,7 @@ if(RIBBONUI_BUILD_STATIC_LIB)
target_link_libraries(${PROJECT_NAME} PRIVATE target_link_libraries(${PROJECT_NAME} PRIVATE
Qt::Quick Qt::Quick
Qt::SerialPort Qt::SerialPort
Qt::WebView
RibbonUIplugin RibbonUIplugin
sm_crypto sm_crypto
FramelessHelper::Core FramelessHelper::Core
@ -135,6 +159,7 @@ else()
target_link_libraries(${PROJECT_NAME} PRIVATE target_link_libraries(${PROJECT_NAME} PRIVATE
Qt::Quick Qt::Quick
Qt::SerialPort Qt::SerialPort
Qt::WebView
RibbonUI RibbonUI
sm_crypto sm_crypto
FramelessHelper::Core FramelessHelper::Core

View File

@ -0,0 +1,30 @@
#ifndef TOOLS_H
#define TOOLS_H
#include <QObject>
#include <QQmlEngine>
#include <QQuickTextDocument>
class Tools : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
QML_NAMED_ELEMENT(Tools)
public:
static Tools* instance();
static Tools* create(QQmlEngine *qmlEngine, QJSEngine *jsEngine){return instance();}
Q_INVOKABLE void setDefaultStyleSheet(QQuickTextDocument *qd, QString path);
Q_INVOKABLE QString readAll(QString path);
Q_INVOKABLE void setBaseUrl(QQuickTextDocument *qd, QUrl url);
Q_INVOKABLE QString fileSuffix(const QString &filePath);
Q_INVOKABLE QString fileDir(const QString &filePath);
private:
explicit Tools(QObject *parent = nullptr);
Q_DISABLE_COPY_MOVE(Tools)
signals:
};
#endif // TOOLS_H

View File

@ -0,0 +1,140 @@
/*! markdown-it-abbr 2.0.0 https://github.com/markdown-it/markdown-it-abbr @license MIT */
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
global.markdownitAbbr = factory());
})(this, (function() {
"use strict";
// Enclose abbreviations in <abbr> tags
function abbr_plugin(md) {
const escapeRE = md.utils.escapeRE;
const arrayReplaceAt = md.utils.arrayReplaceAt;
// ASCII characters in Cc, Sc, Sm, Sk categories we should terminate on;
// you can check character classes here:
// http://www.unicode.org/Public/UNIDATA/UnicodeData.txt
const OTHER_CHARS = " \r\n$+<=>^`|~";
const UNICODE_PUNCT_RE = md.utils.lib.ucmicro.P.source;
const UNICODE_SPACE_RE = md.utils.lib.ucmicro.Z.source;
function abbr_def(state, startLine, endLine, silent) {
let labelEnd;
let pos = state.bMarks[startLine] + state.tShift[startLine];
const max = state.eMarks[startLine];
if (pos + 2 >= max) {
return false;
}
if (state.src.charCodeAt(pos++) !== 42 /* * */) {
return false;
}
if (state.src.charCodeAt(pos++) !== 91 /* [ */) {
return false;
}
const labelStart = pos;
for (;pos < max; pos++) {
const ch = state.src.charCodeAt(pos);
if (ch === 91 /* [ */) {
return false;
} else if (ch === 93 /* ] */) {
labelEnd = pos;
break;
} else if (ch === 92 /* \ */) {
pos++;
}
}
if (labelEnd < 0 || state.src.charCodeAt(labelEnd + 1) !== 58 /* : */) {
return false;
}
if (silent) {
return true;
}
const label = state.src.slice(labelStart, labelEnd).replace(/\\(.)/g, "$1");
const title = state.src.slice(labelEnd + 2, max).trim();
if (label.length === 0) {
return false;
}
if (title.length === 0) {
return false;
}
if (!state.env.abbreviations) {
state.env.abbreviations = {};
}
// prepend ':' to avoid conflict with Object.prototype members
if (typeof state.env.abbreviations[":" + label] === "undefined") {
state.env.abbreviations[":" + label] = title;
}
state.line = startLine + 1;
return true;
}
function abbr_replace(state) {
const blockTokens = state.tokens;
if (!state.env.abbreviations) {
return;
}
const regSimple = new RegExp("(?:" + Object.keys(state.env.abbreviations).map((function(x) {
return x.substr(1);
})).sort((function(a, b) {
return b.length - a.length;
})).map(escapeRE).join("|") + ")");
const regText = "(^|" + UNICODE_PUNCT_RE + "|" + UNICODE_SPACE_RE + "|[" + OTHER_CHARS.split("").map(escapeRE).join("") + "])" + "(" + Object.keys(state.env.abbreviations).map((function(x) {
return x.substr(1);
})).sort((function(a, b) {
return b.length - a.length;
})).map(escapeRE).join("|") + ")" + "($|" + UNICODE_PUNCT_RE + "|" + UNICODE_SPACE_RE + "|[" + OTHER_CHARS.split("").map(escapeRE).join("") + "])";
const reg = new RegExp(regText, "g");
for (let j = 0, l = blockTokens.length; j < l; j++) {
if (blockTokens[j].type !== "inline") {
continue;
}
let tokens = blockTokens[j].children;
// We scan from the end, to keep position when new tags added.
for (let i = tokens.length - 1; i >= 0; i--) {
const currentToken = tokens[i];
if (currentToken.type !== "text") {
continue;
}
let pos = 0;
const text = currentToken.content;
reg.lastIndex = 0;
const nodes = [];
// fast regexp run to determine whether there are any abbreviated words
// in the current token
if (!regSimple.test(text)) {
continue;
}
let m;
while (m = reg.exec(text)) {
if (m.index > 0 || m[1].length > 0) {
const token = new state.Token("text", "", 0);
token.content = text.slice(pos, m.index + m[1].length);
nodes.push(token);
}
const token_o = new state.Token("abbr_open", "abbr", 1);
token_o.attrs = [ [ "title", state.env.abbreviations[":" + m[2]] ] ];
nodes.push(token_o);
const token_t = new state.Token("text", "", 0);
token_t.content = m[2];
nodes.push(token_t);
const token_c = new state.Token("abbr_close", "abbr", -1);
nodes.push(token_c);
reg.lastIndex -= m[3].length;
pos = reg.lastIndex;
}
if (!nodes.length) {
continue;
}
if (pos < text.length) {
const token = new state.Token("text", "", 0);
token.content = text.slice(pos);
nodes.push(token);
}
// replace current node
blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, nodes);
}
}
}
md.block.ruler.before("reference", "abbr_def", abbr_def, {
alt: [ "paragraph", "reference" ]
});
md.core.ruler.after("linkify", "abbr_replace", abbr_replace);
}
return abbr_plugin;
}));

View File

@ -0,0 +1,132 @@
/*! markdown-it-container 4.0.0 https://github.com/markdown-it/markdown-it-container @license MIT */
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
global.markdownitContainer = factory());
})(this, (function() {
"use strict";
// Process block-level custom containers
function container_plugin(md, name, options) {
// Second param may be useful if you decide
// to increase minimal allowed marker length
function validateDefault(params /*, markup */) {
return params.trim().split(" ", 2)[0] === name;
}
function renderDefault(tokens, idx, _options, env, slf) {
// add a class to the opening tag
if (tokens[idx].nesting === 1) {
tokens[idx].attrJoin("class", name);
}
return slf.renderToken(tokens, idx, _options, env, slf);
}
options = options || {};
const min_markers = 3;
const marker_str = options.marker || ":";
const marker_char = marker_str.charCodeAt(0);
const marker_len = marker_str.length;
const validate = options.validate || validateDefault;
const render = options.render || renderDefault;
function container(state, startLine, endLine, silent) {
let pos;
let auto_closed = false;
let start = state.bMarks[startLine] + state.tShift[startLine];
let max = state.eMarks[startLine];
// Check out the first character quickly,
// this should filter out most of non-containers
if (marker_char !== state.src.charCodeAt(start)) {
return false;
}
// Check out the rest of the marker string
for (pos = start + 1; pos <= max; pos++) {
if (marker_str[(pos - start) % marker_len] !== state.src[pos]) {
break;
}
}
const marker_count = Math.floor((pos - start) / marker_len);
if (marker_count < min_markers) {
return false;
}
pos -= (pos - start) % marker_len;
const markup = state.src.slice(start, pos);
const params = state.src.slice(pos, max);
if (!validate(params, markup)) {
return false;
}
// Since start is found, we can report success here in validation mode
if (silent) {
return true;
}
// Search for the end of the block
let nextLine = startLine;
for (;;) {
nextLine++;
if (nextLine >= endLine) {
// unclosed block should be autoclosed by end of document.
// also block seems to be autoclosed by end of parent
break;
}
start = state.bMarks[nextLine] + state.tShift[nextLine];
max = state.eMarks[nextLine];
if (start < max && state.sCount[nextLine] < state.blkIndent) {
// non-empty line with negative indent should stop the list:
// - ```
// test
break;
}
if (marker_char !== state.src.charCodeAt(start)) {
continue;
}
if (state.sCount[nextLine] - state.blkIndent >= 4) {
// closing fence should be indented less than 4 spaces
continue;
}
for (pos = start + 1; pos <= max; pos++) {
if (marker_str[(pos - start) % marker_len] !== state.src[pos]) {
break;
}
}
// closing code fence must be at least as long as the opening one
if (Math.floor((pos - start) / marker_len) < marker_count) {
continue;
}
// make sure tail has spaces only
pos -= (pos - start) % marker_len;
pos = state.skipSpaces(pos);
if (pos < max) {
continue;
}
// found!
auto_closed = true;
break;
}
const old_parent = state.parentType;
const old_line_max = state.lineMax;
state.parentType = "container";
// this will prevent lazy continuations from ever going past our end marker
state.lineMax = nextLine;
const token_o = state.push("container_" + name + "_open", "div", 1);
token_o.markup = markup;
token_o.block = true;
token_o.info = params;
token_o.map = [ startLine, nextLine ];
state.md.block.tokenize(state, startLine + 1, nextLine);
const token_c = state.push("container_" + name + "_close", "div", -1);
token_c.markup = state.src.slice(start, pos);
token_c.block = true;
state.parentType = old_parent;
state.lineMax = old_line_max;
state.line = nextLine + (auto_closed ? 1 : 0);
return true;
}
md.block.ruler.before("fence", "container_" + name, container, {
alt: [ "paragraph", "reference", "blockquote", "list" ]
});
md.renderer.rules["container_" + name + "_open"] = render;
md.renderer.rules["container_" + name + "_close"] = render;
}
return container_plugin;
}));

View File

@ -0,0 +1,203 @@
/*! markdown-it-deflist 3.0.0 https://github.com/markdown-it/markdown-it-deflist.git @license MIT */
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
global.markdownitDeflist = factory());
})(this, (function() {
"use strict";
// Process definition lists
function deflist_plugin(md) {
const isSpace = md.utils.isSpace;
// Search `[:~][\n ]`, returns next pos after marker on success
// or -1 on fail.
function skipMarker(state, line) {
let start = state.bMarks[line] + state.tShift[line];
const max = state.eMarks[line];
if (start >= max) {
return -1;
}
// Check bullet
const marker = state.src.charCodeAt(start++);
if (marker !== 126 /* ~ */ && marker !== 58 /* : */) {
return -1;
}
const pos = state.skipSpaces(start);
// require space after ":"
if (start === pos) {
return -1;
}
// no empty definitions, e.g. " : "
if (pos >= max) {
return -1;
}
return start;
}
function markTightParagraphs(state, idx) {
const level = state.level + 2;
for (let i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
if (state.tokens[i].level === level && state.tokens[i].type === "paragraph_open") {
state.tokens[i + 2].hidden = true;
state.tokens[i].hidden = true;
i += 2;
}
}
}
function deflist(state, startLine, endLine, silent) {
if (silent) {
// quirk: validation mode validates a dd block only, not a whole deflist
if (state.ddIndent < 0) {
return false;
}
return skipMarker(state, startLine) >= 0;
}
let nextLine = startLine + 1;
if (nextLine >= endLine) {
return false;
}
if (state.isEmpty(nextLine)) {
nextLine++;
if (nextLine >= endLine) {
return false;
}
}
if (state.sCount[nextLine] < state.blkIndent) {
return false;
}
let contentStart = skipMarker(state, nextLine);
if (contentStart < 0) {
return false;
}
// Start list
const listTokIdx = state.tokens.length;
let tight = true;
const token_dl_o = state.push("dl_open", "dl", 1);
const listLines = [ startLine, 0 ];
token_dl_o.map = listLines;
// Iterate list items
let dtLine = startLine;
let ddLine = nextLine;
// One definition list can contain multiple DTs,
// and one DT can be followed by multiple DDs.
// Thus, there is two loops here, and label is
// needed to break out of the second one
/* eslint no-labels:0,block-scoped-var:0 */ OUTER: for (;;) {
let prevEmptyEnd = false;
const token_dt_o = state.push("dt_open", "dt", 1);
token_dt_o.map = [ dtLine, dtLine ];
const token_i = state.push("inline", "", 0);
token_i.map = [ dtLine, dtLine ];
token_i.content = state.getLines(dtLine, dtLine + 1, state.blkIndent, false).trim();
token_i.children = [];
state.push("dt_close", "dt", -1);
for (;;) {
const token_dd_o = state.push("dd_open", "dd", 1);
const itemLines = [ nextLine, 0 ];
token_dd_o.map = itemLines;
let pos = contentStart;
const max = state.eMarks[ddLine];
let offset = state.sCount[ddLine] + contentStart - (state.bMarks[ddLine] + state.tShift[ddLine]);
while (pos < max) {
const ch = state.src.charCodeAt(pos);
if (isSpace(ch)) {
if (ch === 9) {
offset += 4 - offset % 4;
} else {
offset++;
}
} else {
break;
}
pos++;
}
contentStart = pos;
const oldTight = state.tight;
const oldDDIndent = state.ddIndent;
const oldIndent = state.blkIndent;
const oldTShift = state.tShift[ddLine];
const oldSCount = state.sCount[ddLine];
const oldParentType = state.parentType;
state.blkIndent = state.ddIndent = state.sCount[ddLine] + 2;
state.tShift[ddLine] = contentStart - state.bMarks[ddLine];
state.sCount[ddLine] = offset;
state.tight = true;
state.parentType = "deflist";
state.md.block.tokenize(state, ddLine, endLine, true);
// If any of list item is tight, mark list as tight
if (!state.tight || prevEmptyEnd) {
tight = false;
}
// Item become loose if finish with empty line,
// but we should filter last element, because it means list finish
prevEmptyEnd = state.line - ddLine > 1 && state.isEmpty(state.line - 1);
state.tShift[ddLine] = oldTShift;
state.sCount[ddLine] = oldSCount;
state.tight = oldTight;
state.parentType = oldParentType;
state.blkIndent = oldIndent;
state.ddIndent = oldDDIndent;
state.push("dd_close", "dd", -1);
itemLines[1] = nextLine = state.line;
if (nextLine >= endLine) {
break OUTER;
}
if (state.sCount[nextLine] < state.blkIndent) {
break OUTER;
}
contentStart = skipMarker(state, nextLine);
if (contentStart < 0) {
break;
}
ddLine = nextLine;
// go to the next loop iteration:
// insert DD tag and repeat checking
}
if (nextLine >= endLine) {
break;
}
dtLine = nextLine;
if (state.isEmpty(dtLine)) {
break;
}
if (state.sCount[dtLine] < state.blkIndent) {
break;
}
ddLine = dtLine + 1;
if (ddLine >= endLine) {
break;
}
if (state.isEmpty(ddLine)) {
ddLine++;
}
if (ddLine >= endLine) {
break;
}
if (state.sCount[ddLine] < state.blkIndent) {
break;
}
contentStart = skipMarker(state, ddLine);
if (contentStart < 0) {
break;
}
// go to the next loop iteration:
// insert DT and DD tags and repeat checking
}
// Finilize list
state.push("dl_close", "dl", -1);
listLines[1] = nextLine;
state.line = nextLine;
// mark paragraphs tight if needed
if (tight) {
markTightParagraphs(state, listTokIdx);
}
return true;
}
md.block.ruler.before("paragraph", "deflist", deflist, {
alt: [ "paragraph", "reference", "blockquote" ]
});
}
return deflist_plugin;
}));

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,299 @@
/*! markdown-it-footnote 4.0.0 https://github.com/markdown-it/markdown-it-footnote @license MIT */
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
global.markdownitFootnote = factory());
})(this, (function() {
"use strict";
// Process footnotes
/// /////////////////////////////////////////////////////////////////////////////
// Renderer partials
function render_footnote_anchor_name(tokens, idx, options, env /*, slf */) {
const n = Number(tokens[idx].meta.id + 1).toString();
let prefix = "";
if (typeof env.docId === "string") prefix = `-${env.docId}-`;
return prefix + n;
}
function render_footnote_caption(tokens, idx /*, options, env, slf */) {
let n = Number(tokens[idx].meta.id + 1).toString();
if (tokens[idx].meta.subId > 0) n += `:${tokens[idx].meta.subId}`;
return `[${n}]`;
}
function render_footnote_ref(tokens, idx, options, env, slf) {
const id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
const caption = slf.rules.footnote_caption(tokens, idx, options, env, slf);
let refid = id;
if (tokens[idx].meta.subId > 0) refid += `:${tokens[idx].meta.subId}`;
return `<sup class="footnote-ref"><a href="#fn${id}" id="fnref${refid}">${caption}</a></sup>`;
}
function render_footnote_block_open(tokens, idx, options) {
return (options.xhtmlOut ? '<hr class="footnotes-sep" />\n' : '<hr class="footnotes-sep">\n') + '<section class="footnotes">\n' + '<ol class="footnotes-list">\n';
}
function render_footnote_block_close() {
return "</ol>\n</section>\n";
}
function render_footnote_open(tokens, idx, options, env, slf) {
let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;
return `<li id="fn${id}" class="footnote-item">`;
}
function render_footnote_close() {
return "</li>\n";
}
function render_footnote_anchor(tokens, idx, options, env, slf) {
let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`
/* ↩ with escape code to prevent display as Apple Emoji on iOS */;
return ` <a href="#fnref${id}" class="footnote-backref">\u21a9\ufe0e</a>`;
}
function footnote_plugin(md) {
const parseLinkLabel = md.helpers.parseLinkLabel;
const isSpace = md.utils.isSpace;
md.renderer.rules.footnote_ref = render_footnote_ref;
md.renderer.rules.footnote_block_open = render_footnote_block_open;
md.renderer.rules.footnote_block_close = render_footnote_block_close;
md.renderer.rules.footnote_open = render_footnote_open;
md.renderer.rules.footnote_close = render_footnote_close;
md.renderer.rules.footnote_anchor = render_footnote_anchor;
// helpers (only used in other rules, no tokens are attached to those)
md.renderer.rules.footnote_caption = render_footnote_caption;
md.renderer.rules.footnote_anchor_name = render_footnote_anchor_name;
// Process footnote block definition
function footnote_def(state, startLine, endLine, silent) {
const start = state.bMarks[startLine] + state.tShift[startLine];
const max = state.eMarks[startLine];
// line should be at least 5 chars - "[^x]:"
if (start + 4 > max) return false;
if (state.src.charCodeAt(start) !== 91 /* [ */) return false;
if (state.src.charCodeAt(start + 1) !== 94 /* ^ */) return false;
let pos;
for (pos = start + 2; pos < max; pos++) {
if (state.src.charCodeAt(pos) === 32) return false;
if (state.src.charCodeAt(pos) === 93 /* ] */) {
break;
}
}
if (pos === start + 2) return false;
// no empty footnote labels
if (pos + 1 >= max || state.src.charCodeAt(++pos) !== 58 /* : */) return false;
if (silent) return true;
pos++;
if (!state.env.footnotes) state.env.footnotes = {};
if (!state.env.footnotes.refs) state.env.footnotes.refs = {};
const label = state.src.slice(start + 2, pos - 2);
state.env.footnotes.refs[`:${label}`] = -1;
const token_fref_o = new state.Token("footnote_reference_open", "", 1);
token_fref_o.meta = {
label: label
};
token_fref_o.level = state.level++;
state.tokens.push(token_fref_o);
const oldBMark = state.bMarks[startLine];
const oldTShift = state.tShift[startLine];
const oldSCount = state.sCount[startLine];
const oldParentType = state.parentType;
const posAfterColon = pos;
const initial = state.sCount[startLine] + pos - (state.bMarks[startLine] + state.tShift[startLine]);
let offset = initial;
while (pos < max) {
const ch = state.src.charCodeAt(pos);
if (isSpace(ch)) {
if (ch === 9) {
offset += 4 - offset % 4;
} else {
offset++;
}
} else {
break;
}
pos++;
}
state.tShift[startLine] = pos - posAfterColon;
state.sCount[startLine] = offset - initial;
state.bMarks[startLine] = posAfterColon;
state.blkIndent += 4;
state.parentType = "footnote";
if (state.sCount[startLine] < state.blkIndent) {
state.sCount[startLine] += state.blkIndent;
}
state.md.block.tokenize(state, startLine, endLine, true);
state.parentType = oldParentType;
state.blkIndent -= 4;
state.tShift[startLine] = oldTShift;
state.sCount[startLine] = oldSCount;
state.bMarks[startLine] = oldBMark;
const token_fref_c = new state.Token("footnote_reference_close", "", -1);
token_fref_c.level = --state.level;
state.tokens.push(token_fref_c);
return true;
}
// Process inline footnotes (^[...])
function footnote_inline(state, silent) {
const max = state.posMax;
const start = state.pos;
if (start + 2 >= max) return false;
if (state.src.charCodeAt(start) !== 94 /* ^ */) return false;
if (state.src.charCodeAt(start + 1) !== 91 /* [ */) return false;
const labelStart = start + 2;
const labelEnd = parseLinkLabel(state, start + 1);
// parser failed to find ']', so it's not a valid note
if (labelEnd < 0) return false;
// We found the end of the link, and know for a fact it's a valid link;
// so all that's left to do is to call tokenizer.
if (!silent) {
if (!state.env.footnotes) state.env.footnotes = {};
if (!state.env.footnotes.list) state.env.footnotes.list = [];
const footnoteId = state.env.footnotes.list.length;
const tokens = [];
state.md.inline.parse(state.src.slice(labelStart, labelEnd), state.md, state.env, tokens);
const token = state.push("footnote_ref", "", 0);
token.meta = {
id: footnoteId
};
state.env.footnotes.list[footnoteId] = {
content: state.src.slice(labelStart, labelEnd),
tokens: tokens
};
}
state.pos = labelEnd + 1;
state.posMax = max;
return true;
}
// Process footnote references ([^...])
function footnote_ref(state, silent) {
const max = state.posMax;
const start = state.pos;
// should be at least 4 chars - "[^x]"
if (start + 3 > max) return false;
if (!state.env.footnotes || !state.env.footnotes.refs) return false;
if (state.src.charCodeAt(start) !== 91 /* [ */) return false;
if (state.src.charCodeAt(start + 1) !== 94 /* ^ */) return false;
let pos;
for (pos = start + 2; pos < max; pos++) {
if (state.src.charCodeAt(pos) === 32) return false;
if (state.src.charCodeAt(pos) === 10) return false;
if (state.src.charCodeAt(pos) === 93 /* ] */) {
break;
}
}
if (pos === start + 2) return false;
// no empty footnote labels
if (pos >= max) return false;
pos++;
const label = state.src.slice(start + 2, pos - 1);
if (typeof state.env.footnotes.refs[`:${label}`] === "undefined") return false;
if (!silent) {
if (!state.env.footnotes.list) state.env.footnotes.list = [];
let footnoteId;
if (state.env.footnotes.refs[`:${label}`] < 0) {
footnoteId = state.env.footnotes.list.length;
state.env.footnotes.list[footnoteId] = {
label: label,
count: 0
};
state.env.footnotes.refs[`:${label}`] = footnoteId;
} else {
footnoteId = state.env.footnotes.refs[`:${label}`];
}
const footnoteSubId = state.env.footnotes.list[footnoteId].count;
state.env.footnotes.list[footnoteId].count++;
const token = state.push("footnote_ref", "", 0);
token.meta = {
id: footnoteId,
subId: footnoteSubId,
label: label
};
}
state.pos = pos;
state.posMax = max;
return true;
}
// Glue footnote tokens to end of token stream
function footnote_tail(state) {
let tokens;
let current;
let currentLabel;
let insideRef = false;
const refTokens = {};
if (!state.env.footnotes) {
return;
}
state.tokens = state.tokens.filter((function(tok) {
if (tok.type === "footnote_reference_open") {
insideRef = true;
current = [];
currentLabel = tok.meta.label;
return false;
}
if (tok.type === "footnote_reference_close") {
insideRef = false;
// prepend ':' to avoid conflict with Object.prototype members
refTokens[":" + currentLabel] = current;
return false;
}
if (insideRef) {
current.push(tok);
}
return !insideRef;
}));
if (!state.env.footnotes.list) {
return;
}
const list = state.env.footnotes.list;
state.tokens.push(new state.Token("footnote_block_open", "", 1));
for (let i = 0, l = list.length; i < l; i++) {
const token_fo = new state.Token("footnote_open", "", 1);
token_fo.meta = {
id: i,
label: list[i].label
};
state.tokens.push(token_fo);
if (list[i].tokens) {
tokens = [];
const token_po = new state.Token("paragraph_open", "p", 1);
token_po.block = true;
tokens.push(token_po);
const token_i = new state.Token("inline", "", 0);
token_i.children = list[i].tokens;
token_i.content = list[i].content;
tokens.push(token_i);
const token_pc = new state.Token("paragraph_close", "p", -1);
token_pc.block = true;
tokens.push(token_pc);
} else if (list[i].label) {
tokens = refTokens[`:${list[i].label}`];
}
if (tokens) state.tokens = state.tokens.concat(tokens);
let lastParagraph;
if (state.tokens[state.tokens.length - 1].type === "paragraph_close") {
lastParagraph = state.tokens.pop();
} else {
lastParagraph = null;
}
const t = list[i].count > 0 ? list[i].count : 1;
for (let j = 0; j < t; j++) {
const token_a = new state.Token("footnote_anchor", "", 0);
token_a.meta = {
id: i,
subId: j,
label: list[i].label
};
state.tokens.push(token_a);
}
if (lastParagraph) {
state.tokens.push(lastParagraph);
}
state.tokens.push(new state.Token("footnote_close", "", -1));
}
state.tokens.push(new state.Token("footnote_block_close", "", -1));
}
md.block.ruler.before("reference", "footnote_def", footnote_def, {
alt: [ "paragraph", "reference" ]
});
md.inline.ruler.after("image", "footnote_inline", footnote_inline);
md.inline.ruler.after("footnote_inline", "footnote_ref", footnote_ref);
md.core.ruler.after("inline", "footnote_tail", footnote_tail);
}
return footnote_plugin;
}));

View File

@ -0,0 +1,114 @@
/*! markdown-it-ins 4.0.0 https://github.com/markdown-it/markdown-it-ins @license MIT */
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
global.markdownitIns = factory());
})(this, (function() {
"use strict";
function ins_plugin(md) {
// Insert each marker as a separate text token, and add it to delimiter list
function tokenize(state, silent) {
const start = state.pos;
const marker = state.src.charCodeAt(start);
if (silent) {
return false;
}
if (marker !== 43 /* + */) {
return false;
}
const scanned = state.scanDelims(state.pos, true);
let len = scanned.length;
const ch = String.fromCharCode(marker);
if (len < 2) {
return false;
}
if (len % 2) {
const token = state.push("text", "", 0);
token.content = ch;
len--;
}
for (let i = 0; i < len; i += 2) {
const token = state.push("text", "", 0);
token.content = ch + ch;
if (!scanned.can_open && !scanned.can_close) {
continue;
}
state.delimiters.push({
marker: marker,
length: 0,
// disable "rule of 3" length checks meant for emphasis
jump: i / 2,
// 1 delimiter = 2 characters
token: state.tokens.length - 1,
end: -1,
open: scanned.can_open,
close: scanned.can_close
});
}
state.pos += scanned.length;
return true;
}
// Walk through delimiter list and replace text tokens with tags
function postProcess(state, delimiters) {
let token;
const loneMarkers = [];
const max = delimiters.length;
for (let i = 0; i < max; i++) {
const startDelim = delimiters[i];
if (startDelim.marker !== 43 /* + */) {
continue;
}
if (startDelim.end === -1) {
continue;
}
const endDelim = delimiters[startDelim.end];
token = state.tokens[startDelim.token];
token.type = "ins_open";
token.tag = "ins";
token.nesting = 1;
token.markup = "++";
token.content = "";
token = state.tokens[endDelim.token];
token.type = "ins_close";
token.tag = "ins";
token.nesting = -1;
token.markup = "++";
token.content = "";
if (state.tokens[endDelim.token - 1].type === "text" && state.tokens[endDelim.token - 1].content === "+") {
loneMarkers.push(endDelim.token - 1);
}
}
// If a marker sequence has an odd number of characters, it's splitted
// like this: `~~~~~` -> `~` + `~~` + `~~`, leaving one marker at the
// start of the sequence.
// So, we have to move all those markers after subsequent s_close tags.
while (loneMarkers.length) {
const i = loneMarkers.pop();
let j = i + 1;
while (j < state.tokens.length && state.tokens[j].type === "ins_close") {
j++;
}
j--;
if (i !== j) {
token = state.tokens[j];
state.tokens[j] = state.tokens[i];
state.tokens[i] = token;
}
}
}
md.inline.ruler.before("emphasis", "ins", tokenize);
md.inline.ruler2.before("emphasis", "ins", (function(state) {
const tokens_meta = state.tokens_meta;
const max = (state.tokens_meta || []).length;
postProcess(state, state.delimiters);
for (let curr = 0; curr < max; curr++) {
if (tokens_meta[curr] && tokens_meta[curr].delimiters) {
postProcess(state, tokens_meta[curr].delimiters);
}
}
}));
}
return ins_plugin;
}));

View File

@ -0,0 +1,114 @@
/*! markdown-it-mark 4.0.0 https://github.com/markdown-it/markdown-it-mark @license MIT */
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
global.markdownitMark = factory());
})(this, (function() {
"use strict";
function ins_plugin(md) {
// Insert each marker as a separate text token, and add it to delimiter list
function tokenize(state, silent) {
const start = state.pos;
const marker = state.src.charCodeAt(start);
if (silent) {
return false;
}
if (marker !== 61 /* = */) {
return false;
}
const scanned = state.scanDelims(state.pos, true);
let len = scanned.length;
const ch = String.fromCharCode(marker);
if (len < 2) {
return false;
}
if (len % 2) {
const token = state.push("text", "", 0);
token.content = ch;
len--;
}
for (let i = 0; i < len; i += 2) {
const token = state.push("text", "", 0);
token.content = ch + ch;
if (!scanned.can_open && !scanned.can_close) {
continue;
}
state.delimiters.push({
marker: marker,
length: 0,
// disable "rule of 3" length checks meant for emphasis
jump: i / 2,
// 1 delimiter = 2 characters
token: state.tokens.length - 1,
end: -1,
open: scanned.can_open,
close: scanned.can_close
});
}
state.pos += scanned.length;
return true;
}
// Walk through delimiter list and replace text tokens with tags
function postProcess(state, delimiters) {
const loneMarkers = [];
const max = delimiters.length;
for (let i = 0; i < max; i++) {
const startDelim = delimiters[i];
if (startDelim.marker !== 61 /* = */) {
continue;
}
if (startDelim.end === -1) {
continue;
}
const endDelim = delimiters[startDelim.end];
const token_o = state.tokens[startDelim.token];
token_o.type = "mark_open";
token_o.tag = "mark";
token_o.nesting = 1;
token_o.markup = "==";
token_o.content = "";
const token_c = state.tokens[endDelim.token];
token_c.type = "mark_close";
token_c.tag = "mark";
token_c.nesting = -1;
token_c.markup = "==";
token_c.content = "";
if (state.tokens[endDelim.token - 1].type === "text" && state.tokens[endDelim.token - 1].content === "=") {
loneMarkers.push(endDelim.token - 1);
}
}
// If a marker sequence has an odd number of characters, it's splitted
// like this: `~~~~~` -> `~` + `~~` + `~~`, leaving one marker at the
// start of the sequence.
// So, we have to move all those markers after subsequent s_close tags.
while (loneMarkers.length) {
const i = loneMarkers.pop();
let j = i + 1;
while (j < state.tokens.length && state.tokens[j].type === "mark_close") {
j++;
}
j--;
if (i !== j) {
const token = state.tokens[j];
state.tokens[j] = state.tokens[i];
state.tokens[i] = token;
}
}
}
md.inline.ruler.before("emphasis", "mark", tokenize);
md.inline.ruler2.before("emphasis", "mark", (function(state) {
let curr;
const tokens_meta = state.tokens_meta;
const max = (state.tokens_meta || []).length;
postProcess(state, state.delimiters);
for (curr = 0; curr < max; curr++) {
if (tokens_meta[curr] && tokens_meta[curr].delimiters) {
postProcess(state, tokens_meta[curr].delimiters);
}
}
}));
}
return ins_plugin;
}));

View File

@ -0,0 +1,60 @@
/*! markdown-it-sub 2.0.0 https://github.com/markdown-it/markdown-it-sub @license MIT */
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
global.markdownitSub = factory());
})(this, (function() {
"use strict";
// Process ~subscript~
// same as UNESCAPE_MD_RE plus a space
const UNESCAPE_RE = /\\([ \\!"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])/g;
function subscript(state, silent) {
const max = state.posMax;
const start = state.pos;
if (state.src.charCodeAt(start) !== 126 /* ~ */) {
return false;
}
if (silent) {
return false;
}
// don't run any pairs in validation mode
if (start + 2 >= max) {
return false;
}
state.pos = start + 1;
let found = false;
while (state.pos < max) {
if (state.src.charCodeAt(state.pos) === 126 /* ~ */) {
found = true;
break;
}
state.md.inline.skipToken(state);
}
if (!found || start + 1 === state.pos) {
state.pos = start;
return false;
}
const content = state.src.slice(start + 1, state.pos);
// don't allow unescaped spaces/newlines inside
if (content.match(/(^|[^\\])(\\\\)*\s/)) {
state.pos = start;
return false;
}
// found!
state.posMax = state.pos;
state.pos = start + 1;
// Earlier we checked !silent, but this implementation does not need it
const token_so = state.push("sub_open", "sub", 1);
token_so.markup = "~";
const token_t = state.push("text", "", 0);
token_t.content = content.replace(UNESCAPE_RE, "$1");
const token_sc = state.push("sub_close", "sub", -1);
token_sc.markup = "~";
state.pos = state.posMax + 1;
state.posMax = max;
return true;
}
function sub_plugin(md) {
md.inline.ruler.after("emphasis", "sub", subscript);
}
return sub_plugin;
}));

View File

@ -0,0 +1,60 @@
/*! markdown-it-sup 2.0.0 https://github.com/markdown-it/markdown-it-sup @license MIT */
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
global.markdownitSup = factory());
})(this, (function() {
"use strict";
// Process ^superscript^
// same as UNESCAPE_MD_RE plus a space
const UNESCAPE_RE = /\\([ \\!"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])/g;
function superscript(state, silent) {
const max = state.posMax;
const start = state.pos;
if (state.src.charCodeAt(start) !== 94 /* ^ */) {
return false;
}
if (silent) {
return false;
}
// don't run any pairs in validation mode
if (start + 2 >= max) {
return false;
}
state.pos = start + 1;
let found = false;
while (state.pos < max) {
if (state.src.charCodeAt(state.pos) === 94 /* ^ */) {
found = true;
break;
}
state.md.inline.skipToken(state);
}
if (!found || start + 1 === state.pos) {
state.pos = start;
return false;
}
const content = state.src.slice(start + 1, state.pos);
// don't allow unescaped spaces/newlines inside
if (content.match(/(^|[^\\])(\\\\)*\s/)) {
state.pos = start;
return false;
}
// found!
state.posMax = state.pos;
state.pos = start + 1;
// Earlier we checked !silent, but this implementation does not need it
const token_so = state.push("sup_open", "sup", 1);
token_so.markup = "^";
const token_t = state.push("text", "", 0);
token_t.content = content.replace(UNESCAPE_RE, "$1");
const token_sc = state.push("sup_close", "sup", -1);
token_sc.markup = "^";
state.pos = state.posMax + 1;
state.posMax = max;
return true;
}
function sup_plugin(md) {
md.inline.ruler.after("emphasis", "sup", superscript);
}
return sup_plugin;
}));

6930
app_source/js/markdown-it.js Normal file

File diff suppressed because one or more lines are too long

54
app_source/js/prism.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -15,6 +15,25 @@ RibbonWindow {
minimumHeight: 800 minimumHeight: 800
title: qsTr("Protocol Parser") + ` V${PPAPP_Version}` title: qsTr("Protocol Parser") + ` V${PPAPP_Version}`
title_bar.right_content:RowLayout{
spacing: 1
layoutDirection: Qt.RightToLeft
RibbonButton{
show_bg:false
icon_source: RibbonIcons.QuestionCircle
icon_source_filled: RibbonIcons_Filled.QuestionCircle
tip_text: qsTr("帮助")
hover_color: Qt.rgba(0,0,0, 0.3)
pressed_color: Qt.rgba(0,0,0, 0.4)
text_color: title_bar.title_text_color
text_color_reverse: false
onClicked: {
show_popup("components/HelpView.qml")
}
}
}
TabBar{ TabBar{
id: tab_bar id: tab_bar
center_view: center_view center_view: center_view

View File

@ -0,0 +1,73 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import RibbonUI
import ProtocolParser
import "."
Item {
id:root
implicitHeight: 500
implicitWidth: 500
property string title: qsTr("帮助")
RibbonButton{
anchors{
top:parent.top
margins: 4
right:parent.right
}
show_bg: false
show_hovered_bg: false
icon_source: RibbonIcons.Dismiss
onClicked: window_popup.close()
}
RibbonText{
anchors{
top:parent.top
topMargin: 10
horizontalCenter: parent.horizontalCenter
}
text: title
}
RibbonMarkDownViewer{
id: viewer
anchors.fill: parent
text: Tools.readAll(':/qt/qml/ProtocolParser/documents/menu.md')
base_url: 'qrc:/qt/qml/ProtocolParser/documents/'
}
RibbonButton{
anchors{
bottom: root.bottom
bottomMargin: 10
left: root.left
leftMargin: 30
}
show_bg: false
show_hovered_bg: false
icon_source: RibbonIcons.ArrowReply
text: qsTr('返回目录')
onClicked: viewer.go_back()
visible: viewer.can_goback
show_tooltip: false
}
RibbonButton{
anchors{
bottom: root.bottom
bottomMargin: 10
right: root.right
rightMargin: 30
}
show_bg: false
show_hovered_bg: false
icon_source: RibbonIcons.ArrowForward
text: qsTr('返回帮助')
onClicked: viewer.go_forward()
visible: viewer.can_goforward
show_tooltip: false
}
}

View File

@ -0,0 +1,154 @@
import QtQuick
import RibbonUI
import QtWebView
import ProtocolParser
import "qrc:/qt/qml/ProtocolParser/markdown-it.js" as MarkdownIt
import "qrc:/qt/qml/ProtocolParser/markdown-it-deflist.js" as MarkdownItDeflist
import "qrc:/qt/qml/ProtocolParser/markdown-it-emoji.js" as MarkdownItEmoji
import "qrc:/qt/qml/ProtocolParser/markdown-it-sub.js" as MarkdownItSub
import "qrc:/qt/qml/ProtocolParser/markdown-it-sup.js" as MarkdownItSup
import "qrc:/qt/qml/ProtocolParser/markdown-it-abbr.js" as MarkdownItAbbr
import "qrc:/qt/qml/ProtocolParser/markdown-it-container.js" as MarkdownItContainer
import "qrc:/qt/qml/ProtocolParser/markdown-it-footnote.js" as MarkdownItFootnote
import "qrc:/qt/qml/ProtocolParser/markdown-it-ins.js" as MarkdownItIns
import "qrc:/qt/qml/ProtocolParser/markdown-it-mark.js" as MarkdownItMark
Item {
id:root
property string text: ""
property string base_url: ""
property int page_height: 0
property alias can_goback: viewer.canGoBack
property alias can_goforward: viewer.canGoForward
onTextChanged: reload()
WebView{
id: viewer
anchors{
top: parent.top
left: parent.left
margins: 30
}
width: parent.width
height: parent.height
property int pre_height: 0
onLoadingChanged: function(request){
if (request.status === WebView.LoadStartedStatus)
{
if(request.url.toString().match(/^http(s)?:\/\/.+/))
{
viewer.stop()
Qt.openUrlExternally(request.url)
}
else if (request.url.toString().match(/^qrc?:\/.+/))
{
console.log(request.url)
text = Tools.readAll(request.url.toString().replace("qrc:/",":/"))
pre_height = 0
load()
}
}
else if (request.status === WebView.LoadSucceededStatus)
{
viewer.width = parent.width - (anchors.margins * 2)
viewer.height = parent.height - (anchors.margins * 2)
viewer.runJavaScript(`document.body.scrollTop = ${viewer.pre_height};`)
get_height()
}
else if (request.status === WebView.LoadFailedStatus)
console.error(request.errorString)
}
}
Component.onCompleted: {
load()
}
Connections{
target: RibbonTheme
function onDark_modeChanged(){
reload()
}
}
function get_height()
{
viewer.runJavaScript("document.body.scrollHeight", function(height) {
page_height = height
});
}
function set_current_height(height)
{
viewer.runJavaScript(`document.body.scrollTop = ${height};`)
viewer.pre_height = height
}
function reload()
{
viewer.runJavaScript("document.body.scrollTop", function(height) {
viewer.pre_height = height
load()
});
}
function load()
{
var style = Tools.readAll(":/qt/qml/ProtocolParser/resources/theme.css")
var prism = Tools.readAll(`:/qt/qml/ProtocolParser/resources/prism-${RibbonTheme.dark_mode ? 'dark' : 'light'}.css`)
var prismjs = Tools.readAll(":/qt/qml/ProtocolParser/prism.js")
var ex = `
html {
height:100%;
overflow:hidden;
position:relative;
}
.markdown-body {
box-sizing: border-box;
min-width: 200px;
max-width: 980px;
margin: 0 auto;
padding: 45px;
height:100%;
overflow:auto;
position:relative;
}
@media (max-width: 767px) {
.markdown-body {
padding: 15px;
}
}`
var md = markdownit({
html: true,
linkify: true,
typographer: true
});
md.use(markdownitDeflist)
md.use(markdownitEmoji)
md.use(markdownitSub)
md.use(markdownitSup)
md.use(markdownitAbbr)
md.use(markdownitContainer)
md.use(markdownitFootnote)
md.use(markdownitIns)
md.use(markdownitMark)
var result = md.render(text);
result = result.replace(new RegExp("href=\"(./)?", "gi"), 'href="'+base_url);
viewer.loadHtml(`<html data-theme=${RibbonTheme.dark_mode ? 'dark' : 'light'}>` + '<head><meta charset="UTF-8", name="viewport" content="width=device-width, initial-scale=1">' +
"<style>" + ex + prism + style + "</style></head>" + "<body>" + '<script>' + prismjs + '</script>' +
'<article class="markdown-body">' + result + '</article>' + "</body></html>")
}
function go_back()
{
viewer.goBack()
}
function go_forward()
{
viewer.goForward()
}
}

View File

@ -188,7 +188,28 @@ Item{
: serial_send_type_combo.currentText === "回车" ? SerialPortManager.WithCarriageEnter : serial_send_type_combo.currentText === "回车" ? SerialPortManager.WithCarriageEnter
: serial_send_type_combo.currentText === "换行" ? SerialPortManager.WithLineFeed : serial_send_type_combo.currentText === "换行" ? SerialPortManager.WithLineFeed
: SerialPortManager.WithCarriageEnterAndLineFeed : SerialPortManager.WithCarriageEnterAndLineFeed
SerialPortManager.write(message_sender_textbox.text) let data = input_validate(message_sender_textbox.text)
switch(data)
{
case 'empty':
message_sender_textbox.text = "待发送内容为空!"
return
case 'hex':
message_sender_textbox.text = "请严格按照16进制输入"
return
case 'case':
message_sender_textbox.text = "请使用全大写或全小写16进制输入"
return
case 'blank':
message_sender_textbox.text = "首尾部不可出现空格!"
return
case 'separate':
message_sender_textbox.text = "字节间需用空格分隔!"
return
default:
break
}
SerialPortManager.write(data)
serial_view.message_model.append({ serial_view.message_model.append({
time: Qt.formatDateTime(new Date(), "yyyy-MM-dd hh:mm:ss.zzz"), time: Qt.formatDateTime(new Date(), "yyyy-MM-dd hh:mm:ss.zzz"),
note_text: message_sender_textbox.text, note_text: message_sender_textbox.text,
@ -196,8 +217,27 @@ Item{
}) })
message_sender_textbox.textedit.clear() message_sender_textbox.textedit.clear()
} }
enabled: SerialPortManager.opened enabled: SerialPortManager.opened
function input_validate(str)
{
if (str.length === 0 && serial_send_type_combo.currentText === "无")
return 'empty';
if (!/^[0-9a-z ]+$/i.test(str) && SerialPortManager.send_hex) {
return 'hex';
}
if ((!(str === str.toUpperCase() || str === str.toLowerCase())) && SerialPortManager.send_hex) {
return 'case';
}
if ((str.startsWith(' ') || str.endsWith(' ')) && SerialPortManager.send_hex) {
return 'blank';
}
for (let i = 0; (i < str.length - 1) && SerialPortManager.send_hex; i += 3) {
if (str[i + 2] !== ' ' && i !== str.length - 2) {
return 'separate';
}
}
return str;
}
} }
} }
} }

View File

@ -0,0 +1,296 @@
html {
font-size: 100%;
overflow-y: scroll;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
body {
color: #444;
font-family: Georgia, Palatino, 'Palatino Linotype', Times, 'Times New Roman', serif;
font-size: 12px;
line-height: 1.5em;
padding: 1em;
margin: auto;
max-width: 42em;
background: #fefefe;
}
a {
color: #8aaaeb;
text-decoration: none;
}
a:visited {
color: #0645ad;
}
a:hover {
color: #06e;
}
a:active {
color: #faa700;
}
a:focus {
outline: thin dotted;
}
a:hover,
a:active {
outline: 0;
}
::-moz-selection {
background: rgba(255, 255, 0, 0.3);
color: #000;
}
::selection {
background: rgba(255, 255, 0, 0.3);
color: #000;
}
a::-moz-selection {
background: rgba(255, 255, 0, 0.3);
color: #0645ad;
}
a::selection {
background: rgba(255, 255, 0, 0.3);
color: #0645ad;
}
p {
margin: 1em 0;
}
img {
max-width: 100%;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: normal;
line-height: 1em;
}
h4,
h5,
h6 {
font-weight: bold;
}
h1 {
font-size: 2.5em;
}
h2 {
font-size: 2em;
}
h3 {
font-size: 1.5em;
}
h4 {
font-size: 1.2em;
}
h5 {
font-size: 1em;
}
h6 {
font-size: 0.9em;
}
blockquote {
color: #666666;
margin: 0;
padding-left: 3em;
border-left: 0.5em #eee solid;
}
hr {
display: block;
border: 0;
border-top: 1px solid #aaa;
border-bottom: 1px solid #eee;
margin: 1em 0;
padding: 0;
}
pre,
code,
kbd,
samp {
font-family: monospace, monospace;
_font-family: 'courier new', monospace;
font-size: 0.98em;
}
pre {
white-space: pre;
white-space: pre-wrap;
word-wrap: break-word;
}
b,
strong {
font-weight: bold;
}
dfn {
font-style: italic;
}
ins {
background: #ff9;
color: #000;
text-decoration: none;
}
mark {
background: #ff0;
color: #000;
font-style: italic;
font-weight: bold;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
ul,
ol {
margin: 1em 0;
padding: 0 0 0 2em;
}
li p:last-child {
margin: 0;
}
dd {
margin: 0 0 0 2em;
}
img {
border: 0;
-ms-interpolation-mode: bicubic;
vertical-align: middle;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
td {
vertical-align: top;
}
@media only screen and (min-width: 480px) {
body {
font-size: 14px;
}
}
@media only screen and (min-width: 768px) {
body {
font-size: 16px;
}
}
@media print {
* {
background: transparent !important;
color: black !important;
filter: none !important;
-ms-filter: none !important;
}
body {
font-size: 12pt;
max-width: 100%;
}
a,
a:visited {
text-decoration: underline;
}
hr {
height: 1px;
border: 0;
border-bottom: 1px solid black;
}
a[href]:after {
content: " (" attr(href) ")";
}
abbr[title]:after {
content: " (" attr(title) ")";
}
.ir a:after,
a[href^="javascript:"]:after,
a[href^="#"]:after {
content: "";
}
pre,
blockquote {
border: 1px solid #999;
padding-right: 1em;
page-break-inside: avoid;
}
tr,
img {
page-break-inside: avoid;
}
img {
max-width: 100% !important;
}
@page :left {
margin: 15mm 20mm 15mm 10mm;
}
@page :right {
margin: 15mm 10mm 15mm 20mm;
}
p,
h2,
h3 {
orphans: 3;
widows: 3;
}
h2,
h3 {
page-break-after: avoid;
}
}

View File

@ -0,0 +1,5 @@
/* PrismJS 1.29.0
https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+arduino+armasm+bash+c+cpp+docker+git+go+go-module+gradle+http+hpkp+hsts+java+javadoc+javadoclike+jsdoc+js-extras+json+json5+jsonp+jsstacktrace+js-templates+kotlin+latex+log+markup-templating+mermaid+objectivec+perl+php+phpdoc+php-extras+plsql+powerquery+powershell+qml+ruby+rust+shell-session+sql+typescript+wasm+yaml&plugins=line-highlight+line-numbers+highlight-keywords */
code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#161b22}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}
pre[data-line]{position:relative;padding:1em 0 1em 3em}.line-highlight{position:absolute;left:0;right:0;padding:inherit 0;margin-top:1em;background:hsla(24,20%,50%,.08);background:linear-gradient(to right,hsla(24,20%,50%,.1) 70%,hsla(24,20%,50%,0));pointer-events:none;line-height:inherit;white-space:pre}@media print{.line-highlight{-webkit-print-color-adjust:exact;color-adjust:exact}}.line-highlight:before,.line-highlight[data-end]:after{content:attr(data-start);position:absolute;top:.4em;left:.6em;min-width:1em;padding:0 .5em;background-color:hsla(24,20%,50%,.4);color:#f4f1ef;font:bold 65%/1.5 sans-serif;text-align:center;vertical-align:.3em;border-radius:999px;text-shadow:none;box-shadow:0 1px #fff}.line-highlight[data-end]:after{content:attr(data-end);top:auto;bottom:.4em}.line-numbers .line-highlight:after,.line-numbers .line-highlight:before{content:none}pre[id].linkable-line-numbers span.line-numbers-rows{pointer-events:all}pre[id].linkable-line-numbers span.line-numbers-rows>span:before{cursor:pointer}pre[id].linkable-line-numbers span.line-numbers-rows>span:hover:before{background-color:rgba(128,128,128,.2)}
pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}

View File

@ -0,0 +1,5 @@
/* PrismJS 1.29.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+arduino+armasm+bash+c+cpp+docker+git+go+go-module+gradle+http+hpkp+hsts+java+javadoc+javadoclike+jsdoc+js-extras+json+json5+jsonp+jsstacktrace+js-templates+kotlin+latex+log+markup-templating+mermaid+objectivec+perl+php+phpdoc+php-extras+plsql+powerquery+powershell+qml+ruby+rust+shell-session+sql+typescript+wasm+yaml&plugins=line-highlight+line-numbers+highlight-keywords */
code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f6f8fa}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
pre[data-line]{position:relative;padding:1em 0 1em 3em}.line-highlight{position:absolute;left:0;right:0;padding:inherit 0;margin-top:1em;background:hsla(24,20%,50%,.08);background:linear-gradient(to right,hsla(24,20%,50%,.1) 70%,hsla(24,20%,50%,0));pointer-events:none;line-height:inherit;white-space:pre}@media print{.line-highlight{-webkit-print-color-adjust:exact;color-adjust:exact}}.line-highlight:before,.line-highlight[data-end]:after{content:attr(data-start);position:absolute;top:.4em;left:.6em;min-width:1em;padding:0 .5em;background-color:hsla(24,20%,50%,.4);color:#f4f1ef;font:bold 65%/1.5 sans-serif;text-align:center;vertical-align:.3em;border-radius:999px;text-shadow:none;box-shadow:0 1px #fff}.line-highlight[data-end]:after{content:attr(data-end);top:auto;bottom:.4em}.line-numbers .line-highlight:after,.line-numbers .line-highlight:before{content:none}pre[id].linkable-line-numbers span.line-numbers-rows{pointer-events:all}pre[id].linkable-line-numbers span.line-numbers-rows>span:before{cursor:pointer}pre[id].linkable-line-numbers span.line-numbers-rows>span:hover:before{background-color:rgba(128,128,128,.2)}
pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@
#include <FramelessHelper/Core/private/framelessconfig_p.h> #include <FramelessHelper/Core/private/framelessconfig_p.h>
#include <QTranslator> #include <QTranslator>
#include <QQmlContext> #include <QQmlContext>
#include <QtWebView>
FRAMELESSHELPER_USE_NAMESPACE FRAMELESSHELPER_USE_NAMESPACE
#ifdef RIBBONUI_BUILD_STATIC_LIB #ifdef RIBBONUI_BUILD_STATIC_LIB
@ -13,6 +14,7 @@ Q_IMPORT_QML_PLUGIN(RibbonUIPlugin)
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
qputenv("QT_QUICK_CONTROLS_STYLE","Basic"); qputenv("QT_QUICK_CONTROLS_STYLE","Basic");
QtWebView::initialize();
FramelessHelper::Quick::initialize(); FramelessHelper::Quick::initialize();
QGuiApplication app(argc, argv); QGuiApplication app(argc, argv);

View File

@ -0,0 +1,57 @@
#include "tools.h"
#include <QMutex>
#include <QFile>
#include <QFileInfo>
Tools::Tools(QObject *parent)
: QObject{parent}
{}
Tools* Tools::instance(){
static QMutex mutex;
QMutexLocker locker(&mutex);
static Tools *singleton = nullptr;
if (!singleton) {
singleton = new Tools();
}
return singleton;
}
void Tools::setDefaultStyleSheet(QQuickTextDocument *qd, QString path)
{
auto td = qd->textDocument();
td->setDefaultStyleSheet(readAll(path));
}
QString Tools::readAll(QString path)
{
QFile file(path);
if(!file.exists()){
qWarning()<<"File not exist!";
return "";
}
if(!file.open(QIODevice::ReadOnly|QIODevice::Text)){
qWarning()<<"Cannot open the file!";
return "";
}
auto content = file.readAll();
return QString(content);
}
void Tools::setBaseUrl(QQuickTextDocument *qd, QUrl url)
{
qd->textDocument()->setBaseUrl(url);
}
QString Tools::fileSuffix(const QString &filePath)
{
QFileInfo info(filePath);
return info.suffix();
}
QString Tools::fileDir(const QString &filePath)
{
QFileInfo info(filePath);
return info.absolutePath();
}

19
documents/menu.md Normal file
View File

@ -0,0 +1,19 @@
## 目录
## 1. [协议解析](protocol.md)
+ 概述
+ 密钥
+ 认证流程
+ 数据结构
+ 接口解析
## 2. [串口使用](serialport.md)
+ 参数配置
+ 串口消息设置
+ 串口助手设置
+ 串口数据流向
## 3. [紫蜂协议](zigbee.md)
+ 设备列表
+ 密钥管理
+ 调试选项
## 4. [其他](others.md)
+ 主题
+ 数据结构自定义

16
documents/others.md Normal file
View File

@ -0,0 +1,16 @@
## 1. 主题
+ 选择应用程序主题(浅色/深色/跟随系统)。注意,在`Windows`系统下,手动设置日间/夜间主题是无效的,仅建议使用**跟随系统**。
## 2. 数据结构自定义
+ 此选项可以自定义下位机节点发送的**数据帧**中携带的数据结构组成。
+ 受限于`DL-LN3X`模块最大传输长度,仅建议携带最大不超过`16`字节的数据。
+ 如需自定义数据结构,需从默认数据结构中删除一项或多项,再根据实际情况输入数据名称和选择数据类型。
+ 请注意,数据按照**小端序**排列。
+ 下位机如使用结构体组织传感器数据,请务必注意结构体**内存对齐**的问题。
+ 默认数据结构如下:
| 数据 | 数据类型(字节数) | 说明 |
|:----:|:----:|:----:|
| 温度 | `float, 4 Bytes` | 位于第`0`~`3`字节 |
| 湿度 | `float, 4 Bytes` | 位于第`4`~`7`字节 |
| 气体浓度 | `float, 4 Bytes` | 位于第`8`~`11`字节 |
| 火焰指数 | `float, 4 Bytes` | 位于第`12`~`15`字节 |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 962 KiB

After

Width:  |  Height:  |  Size: 975 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 835 KiB

After

Width:  |  Height:  |  Size: 841 KiB

236
documents/protocol.md Normal file
View File

@ -0,0 +1,236 @@
## 1. 概述
本协议是一套运行在应用层的,用于端到端通讯的轻量级安全协议,支持非对称/对称的双向、单向认证及加密通讯。该协议的认证和加密通讯由SM系列SM2、SM3、SM4算法保障安全性较强可抵御重放攻击。协议易拓展数据包采用二进制传输适合在多种窄带宽场景下使用目前已在嵌入式、ARM、X86/64平台上测试通过
## 2. 密钥
本协议采用两类密钥一是用于双向认证的SM2公私钥对二是用于对称双、单向认证和加密传输的SM4密钥。
+ SM2密钥长度为公钥64Bytes512Bits私钥32 Bytes256Bits。密文长度为明文长度再加上96Bytes。签名长度为64Bytes被签名数据内包含随机数盐值因此对同一数据签名的结果不相同
+ SM4密钥长度为16Bytes128Bits
## 3. 认证流程
### 3.1 非对称双向认证
由于SM2算法会占用一部分资源非对称双向认证一般用于性能较好的终端如性能较强的单片机、树莓派或者PC等设备之间的认证性能较差的单片机将会出现死机等预料外情况板载硬件密码算法模块除外
```mermaid
sequenceDiagram
participant 服务器
participant 客户端
客户端-->服务器: 客户端验证服务器
客户端->>服务器: 包含Hello信息的ssl_frame请求
服务器->>客户端: 包含Hello和64Bytes服务器公钥信息的ssl_frame响应
客户端-->>客户端: 用服务器公钥加密自己的公钥
客户端->>服务器: 包含服务器公钥加密的客户端公钥信息的ssl_frame数据包
服务器-->>服务器: 用自己的私钥解密出客户端公钥
服务器->>客户端: 包含用客户端公钥加密的Verified信息的ssl_frame数据包
客户端-->>客户端: 用自己的私钥解密出Verified信息
客户端->>服务器: 包含服务器公钥加密的OK信息的ssl_frame数据包
服务器-->客户端: 服务器验证客户端
服务器-->>服务器: 生成8Bytes随机数挑战值
服务器->>客户端: 包含客户端公钥加密的8Bytes挑战值信息的ssl_frame数据包
客户端-->>客户端: 用自己的私钥解密出挑战值,<br>并用自己的私钥对它签名
客户端->>服务器: 包含客户端公钥64Bytes、签名64Bytes<br>挑战值8Bytes共136Bytes数据的ssl_frame数据包
服务器-->>服务器: 进行三轮比对:<br>1.将获得的客户端公钥与<br>第一轮验证中获取的客户端公钥进行比对<br>2.将获得的挑战值与发送的进行比对<br>3.用客户端公钥对挑战值签名数据进行验签
服务器->>客户端: 包含客户端公钥加密的OK信息的ssl_frame数据包
服务器-->客户端: 双向认证结束
```
### 3.2 对称双向认证
对称双向认证使用基于SM3的HMAC算法实现。通过验证双方是否共同持有同一对预设定密钥来实现认证在实际运用过程中为节省资源服务器发送给客户端的Identified信息可不加密采用明文传输因为后续传输的数据包均为加密数据包
```mermaid
sequenceDiagram
participant 服务器
participant 客户端
客户端-->>客户端: 生成1Byte随机数并对其使用基于SM3的HMAC算法和<br>预设定16Bytes密钥生成值
客户端->>服务器: 包含随机数和使用预设定16Bytes密钥生成的<br>HMAC值信息的hmac_frame请求
服务器-->>服务器: 使用自己拥有的预设定16Bytes密钥对收到的<br>1Byte随机数进行HMAC运算并将值与收到值进行比对
服务器->>客户端: 包含使用预设定16Bytes密钥加密的<br>Identified信息的crypto_zdata_frame数据包
客户端-->>客户端: 用预设定16Bytes密钥解密出Identified信息
客户端->>服务器: 包含正常数据的用预设定密钥加密的<br>crypto_zdata_frame数据包
```
## 4. 数据结构
### 4.1 基础帧base_frame
基础帧是本协议传输最底层的帧,用于承载其他帧。
| 帧结构 | 长度Bytes | 说明 |
|:----:|:----:|:----:|
| head | 2 | 包头,数值固定为`0xAAAD` |
| ori_addr | 2 | 源地址,即该帧发送方的地址 |
| des_addr | 2 | 目的地址,即该帧接收方的地址 |
| node_addr | 2 | 节点地址,仅在此帧被服务器进行路由时使用,<br>用以标记该帧由哪个节点最先产生 |
| id | 2 | 帧标号,用以与`rand_num`共同唯一标记一个帧,防止重放攻击 |
| length | 2 | 帧长度包括帧头最大为65535个Bits |
| reset_num | 1 | 重置标记位,用以告知对方在接收此帧后,<br>下一帧的帧标号将归零,`rand_num`将被重新生成 |
| rand_num | 1 | 随机数 |
| data | 可变长度 | 数据最大不超过8191 - `BASE_FRAME_PREFIX_LEN`= 8177 |
另有其他说明如下:
+ 宏定义`BASE_FRAME_PREFIX_LEN`基础帧帧头长度固定为14Bytes
+ 宏定义`BASE_FRAME_HEAD`:基础帧帧头,固定为`0xAAAD`
+ 宏定义`BASE_FRAME_RESET_NUM`:基础帧重置阈值:当发送帧数达到该阈值时,下一帧将被重置帧标号和随机数,该阈值一般为`10000`,最大不超过`65535`
+ 宏定义`new_base_frame(num)`:用以生成基础帧数据结构,`num`为`data`部分长度,使用时请注意强制类型转换
### 4.2 数据帧data_frame
数据帧是本协议传输中最顶层的帧,用于承载各类数据。
| 帧结构 | 长度Bytes | 说明 |
|:----:|:----:|:----:|
| head | 2 | 包头,数值固定为`0xAAAA` |
| type | 1 | 数据类型,由用户根据业务不同自定义 |
| use_crc | 1 | CRC标记位当其为`0xFF`时将对数据进行CRC校验 |
| data_length | 2 | 数据长度,不包括包头 |
| crc | 2 | CRC16校验码当CRC标记位使能且对数据进行CRC校验后数值与该码不同包将被判定为损坏 |
| data | 可变长度 | 根据底层帧的不同数据最大不超过8191 - `BASE_FRAME_PREFIX_LEN` - `DATA_FRAME_PREFIX_LEN`= 8169若叠加多重帧需要减去对应帧的帧头 |
另有其他说明如下:
+ 宏定义`DATA_FRAME_PREFIX_LEN`数据帧帧头长度固定为8Bytes
+ 宏定义`DATA_FRAME_HEAD`:数据帧帧头,固定为`0xAAAA`
+ 宏定义`new_data_frame(num)`:用以生成数据帧数据结构,`num`为`data`部分长度,使用时请注意强制类型转换
### 4.3 数字信封digi_env
数字信封是通信双方进行非对称双向认证后,用于加密传输数据的数据结构。
| 帧结构 | 长度Bytes | 说明 |
|:----:|:----:|:----:|
| head | 2 | 包头,数值固定为`0xAAAB` |
| length | 2 | 长度,包括包头 |
| crypted_session_key | 112 | 由接收方公钥加密的16Bytes会话密钥再加上96Bytes的额外数据 |
| data | 可变长度 | 由会话密钥进行SM4加密的数据长度数据最大不超过8191 - `BASE_FRAME_PREFIX_LEN` - `DIGI_ENV_PREFIX_LEN` - `DIGI_ENV_SESSION_KEY_LEN`= 8061又因SM4的密文为16的倍数因此最大不能超过8048 |
另有其他说明如下:
+ 宏定义`SM4_PADDING_LEN`SM4默认填充长度固定为16Bytes
+ 宏定义`DIGI_ENV_PREFIX_LEN`数字信封包头长度固定为4Bytes
+ 宏定义`DIGI_ENV_SESSION_KEY_LEN`数字信封密态会话密钥长度固定为112Bytes
+ 宏定义`DIGI_ENV_HEAD`:数字信封包头,固定为`0xAAAB`
+ 宏定义`new_digi_env(num)`:用以生成数字信封数据结构,`num`为`data`部分长度,使用时请注意强制类型转换
### 4.4 非对称双向认证包ssl_frame
非对称双向认证包负责承载非对称双向认证相关数据。
| 帧结构 | 长度Bytes | 说明 |
|:----:|:----:|:----:|
| head | 2 | 包头,数值固定为`0xAAAC` |
| length | 2 | 长度,包括包头 |
| data | 可变长度 | 数据最大不超过8191 - `BASE_FRAME_PREFIX_LEN` - `SSL_FRAME_PREFIX_LEN`= 8173 |
另有其他说明如下:
+ 宏定义`SSL_FRAME_PREFIX_LEN`非对称双向认证包包头长度固定为4Bytes
+ 宏定义`SSL_FRAME_HEAD`:非对称双向认证包包头,固定为`0xAAAC`
+ 宏定义`new_ssl_frame(num)`:用以生成非对称双向认证包数据结构,`num`为`data`部分长度,使用时请注意强制类型转换
### 4.5 对称双向认证包hmac_frame
对称双向认证包负责承载对称双向认证相关数据。
| 帧结构 | 长度Bytes | 说明 |
|:----:|:----:|:----:|
| head | 2 | 包头,数值固定为`0xAAAE` |
| length | 2 | 长度,包括包头 |
| value | 1 | 随机数 |
| hmac | 32 | 随机数`value`对应的HMAC数值 |
另有其他说明如下:
+ 宏定义`HMAC_FRAME_PREFIX_LEN`对称双向认证包包头长度固定为4Bytes
+ 宏定义`HMAC_FRAME_HEAD`:非对称双向认证包包头,固定为`0xAAAE`
### 4.6 对称加密数据帧crypto_zdata_frame
对称加密数据帧用于通信双方完成对称双向认证后数据加密传输。
| 帧结构 | 长度Bytes | 说明 |
|:----:|:----:|:----:|
| head | 2 | 包头,数值固定为`0xAAAF` |
| length | 2 | 长度,包括包头 |
| crc | 2 | CRC16校验码,对解密数据进行CRC校验后数值与该码不同将被判定为解密失败 |
| data | 可变长度 | 数据最大不超过8191 - `BASE_FRAME_PREFIX_LEN` - `CRYPTO_ZDATA_FRAME_PREFIX_LEN`= 8171 |
另有其他说明如下:
+ 宏定义`CRYPTO_ZDATA_FRAME_PREFIX_LEN`对称加密数据帧帧头长度固定为6Bytes
+ 宏定义`CRYPTO_ZDATA_FRAME_HEAD`:对称加密数据帧帧头,固定为`0xAAAF`
### 4.7 设备信息存储device
协议采用`device`结构体用作存储相关通讯数据。
| 结构 | 说明 |
|:----:|:----:|
| addr | 用以保存通讯对象的地址(可以是发送方,也可以是接收方) |
| id | 缓存发送/接收到的基础帧帧标号 |
| rand_num | 缓存发送/接收到的基础帧随机数 |
| verified | 标记该通讯对象是否通过认证 |
| online | 标记该通讯对象是否在线 |
| logined | 仅作为服务器时使用,记录该通讯对象是否已经登录 |
| stage | 仅在非对称双向认证时使用,记录该通讯对象正处于认证的第几阶段 |
| chlg_buf | 仅在非对称双向认证时使用,记录该通讯对象的挑战值 |
| ip | 仅在TCP链路中使用记录该通讯对象的IP地址 |
| port | 仅在TCP链路中使用记录该通讯对象的端口号 |
| key_pair | 仅在非对称双向认证时使用,记录该通讯对象的公钥和会话密钥 |
## 5. 接口解析
### 5.1 协议封装/解析
#### 5.1.1 void protocal_wrapper(data_frame *frame, u8 type, u16 length, u8 *data, bool use_crc)
将对应数据打包进数据帧中。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| frame | 输出 | 数据帧指针 |
| type | 输入 | 数据类型 |
| length | 输入 | 数据长度 |
| use_crc | 输入 | 是否启用CRC验证 |
#### 5.1.2 void base_frame_maker(void *in_frame, base_frame *out_frame, u16 dest_addr,device *dev,u16 node_addr=0)
将对应数据打包进基础帧中。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| in_frame | 输入 | 数据包指针,指向需要被打包进基础帧的数据地址 |
| out_frame | 输出 | 基础帧指针 |
| dest_addr | 输入 | 目的地址 |
| dev | 输出 | 发送方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
| node_addr | 输入 | 仅在服务器端路由时使用,用以保存该基础帧的最初创建者地址 |
#### 5.1.3 bool base_frame_parser(base_frame *in_frame, void **out_frame, device *dev)
从基础帧中解析出数据,返回`true`为解析成功,`false`为解析失败,需要检查包基础帧数据内容是否错误。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| in_frame | 输入 | 基础帧指针 |
| out_frame | 输出 | 数据包指针,指向需要从基础帧中解析出的数据包地址 |
| dev | 输出 | 接收方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
#### 5.1.4 void ssl_frame_maker(ssl_frame *frame, u8 *data, int data_len)
将相关数据打包进非对称双向认证包中。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| in_frame | 输出 | 非对称双向认证包指针,指向需要被打包进非对称双向认证包的数据地址 |
| data | 输入 | 数据指针 |
| data_len | 输入 | 数据长度 |
#### 5.1.5 void zigbee_data_encrypt(data_frame *data, crypto_zdata_frame *zdata, bool (* SM4_encrypt)(u8 *key_origin, u32 key_len, u8 *in_origin, u32 in_len, u8 *out, u32 *out_len, bool use_real_cbc),QString en_key = "")
对数据进行对称加密。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| data | 输入 | 待加密的数据指针 |
| zdata | 输出 | 待填充的对称加密数据包指针 |
| SM4_encrypt | 输入 | SM4加密算法函数指针 |
| en_key | 输入 | 若不提供密钥,则将使用默认密钥进行加密 |
#### 5.1.6 bool zigbee_data_dectypt(data_frame *data, crypto_zdata_frame *zdata,bool (* SM4_decrypt)(u8 *key_origin, u32 key_len, u8 *in, u32 in_len, u8 *out, u32 *out_len, bool use_real_cbc),QString en_key = "")
对数据进行对称解密,返回`true`代表解密成功,`false`代表解密失败,需要检查输入是否错误。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| data | 输出 | 待解密的数据地址指针 |
| zdata | 输入 | 收到的对称加密数据包指针 |
| SM4_decrypt | 输入 | SM4解密算法函数指针 |
| en_key | 输入 | 若不提供密钥,则将使用默认密钥进行解密 |
### 5.2 协议认证/验证
#### 5.2.1 void HMAC_identify(device *self, device *node, hmac_frame *hframe, void (*sendTonode)(ZigbeeFrame &data), void (*SM3_HMAC)(u8 *key, int keylen,u8 *input, int ilen,u8 output[32]))
对接收到的HMAC包进行数据认证。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| self | 输出 | 接收方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
| node | 输出 | 发送方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
| hframe | 输入 | 接收到的对称双向认证包指针 |
| sendTonode | 输入 | 发送数据函数的指针 |
#### 5.2.2 bool data_frame_verify(data_frame *frame)
对数据帧进行验证,返回`true`代表包有效,`false`代表包损坏。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| frame | 输入 | 待检验的数据帧指针 |
#### 5.2.3 void HMAC_changeVerifykey(u8 key[16], device* self, device *node, void (*sendTonode)(ZigbeeFrame &data),bool (* SM4_encrypt)(u8 *key_origin, u32 key_len, u8 *in_origin, u32 in_len, u8 *out, u32 *out_len, bool use_real_cbc))
发送对称双向认证密钥更换指令包。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| key | 输入 | 新密钥 |
| self | 输出 | 接收方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
| node | 输出 | 发送方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
| sendTonode | 输入 | 发送数据函数的指针 |
| SM4_encrypt | 输入 | SM4加密算法函数指针 |
### 5.3 工具
#### 5.3.1 uint16_t crc16_xmodem(const uint8_t *buffer, uint32_t buffer_length)
生成CRC16校验码。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| buffer | 输入 | 需要生成校验码的数据指针 |
| buffer_length | 输入 | 需要生成校验码的数据长度 |
#### 5.3.2 bool bytecmp(u8 *a, u8 *b, u16 length)
将两个输入进行逐字比较,即`memcmp`,返回`true`代表完全一致,`false`代表存在差异。
| 变量名 | 输入/输出 | 说明 |
|:----:|:----:|:----:|
| a | 输入 | 需对比数据A |
| b | 输入 | 需对比数据B |
| length | 输入 | 需对比数据长度 |

25
documents/serialport.md Normal file
View File

@ -0,0 +1,25 @@
## 1. 参数配置
| 参数 | 说明 |
|:----:|:----:|
| 端口号 | 选择`DL-LN3X`模块对应的端口号 |
| 校验码 | 选择串口校验码(如奇校验/偶校验),一般为无 |
| 波特率 | 选择串口波特率,`DL-LN3X`模块默认为`115200` |
| 数据位 | 选择一个数据帧内包含多少位数据,一般默认为`8`位,即一个字节 |
| 停止位 | 选择停止位长度,一般默认为`1`位 |
## 2. 串口消息设置
| 选项 | 说明 |
|:----:|:----:|
| 显示消息解析 | 为`ZigBee`信息流气泡中的消息内容显示解析 |
| 自动定位最新消息 | `ZigBee`信息流自动跟踪最新收到的消息 |
| 清除旧消息 | 清空`ZigBee`信息流 |
## 3. 串口助手设置
| 选项 | 说明 |
|:----:|:----:|
| 十六进制接收 | 让串口助手以`HEX`的形式显示收到的数据 |
| 十六进制发送 | 让串口助手以`HEX`的形式发送用户输入的数据 |
## 4. 串口数据流向
标记当前串口数据显示在哪个界面,如紫蜂/串口助手。
将鼠标放在窗口右侧边缘,点击出现的按钮即可进入串口助手界面。

26
documents/zigbee.md Normal file
View File

@ -0,0 +1,26 @@
## 1. 设备列表
+ 在此选择需要管理的设备列表。
+ 仅当节点被从**等待队列**中加入**认证列表**后,上位机才会下发**验证通过**指令。
+ 被加入黑名单的设备,上位机将**不会**响应其任何请求,直到被移除出黑名单。
| 选项 | 说明 |
|:----:|:----:|
| 等待队列 | 管理正在等待验证的节点列表 |
| 认证列表 | 管理已经通过认证的节点列表 |
| 黑名单列表 | 管理上位机忽略请求的设备列表 |
## 2. 密钥管理
+ 在此处可以更改当前使用的`SM4`加解密和`HMAC`所使用的`16`字节密钥。
+ 请务必输入:`16`字节`HEX`形式密钥,即至少为`32`个字符。
+ 密钥每个字节间可以用空格分隔。
+ 密钥可以全大写,也可以全小写,但不可以混用。
## 3. 调试选项
+ 此处的设置为开发者调试软件所用,建议普通用户不要使用。
| 选项 | 说明 |
|:----:|:----:|
| 生成接收数据 | 在**接收数据流**中模拟生成一条接收数据 |
| 生成验证数据 | 在**认证信息流**中模拟生成一条验证数据 |
| 生成更换密钥数据 | 在**认证信息流**中模拟生成一条更换密钥数据,仅在存在历史密钥的情况下可用 |
| 事件历史信息 | 查看**事件总线**的调试数据 |