Initial commit: CCU621_M firmware project with BLE debug link support.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-08 17:28:36 +08:00
commit 9ceb218f80
1597 changed files with 724159 additions and 0 deletions
+250
View File
@@ -0,0 +1,250 @@
# CCU–POS VTK 串口结算功能设计说明(草案)
版本:v0.1
协议参考:`vtk-protocol-en-1.23.pdf`VTK protocol 1.23
---
## 1. 应用场景概述
- **角色**
- **VMCVending Machine Controller**:本项目中的 **CCU 主控**,负责充电控制、计量和向 POS 发起扣款 / 结算请求。
- **POS 终端**:支持 VTK 协议的支付终端,完成银行卡 / 钱包扣款,并把结果回传给 CCU。
- **业务流程(高层逻辑)**
1. 车主在充电桩侧操作“启动充电”(扫码 / 刷卡 / 远程指令等),CCU 准备启动充电流程。
2. CCU 根据配置计算 **预扣款金额**(例如 50 元,可配置)。
3. CCU 通过 **串口上的 VTK 协议** 向 POS 发送“预扣款请求”消息,请求冻结该金额。
4. POS 完成预扣并返回“通过 / 拒绝 / 超时”等结果:
- 通过:CCU 进入正常充电流程。
- 拒绝:CCU 不启动充电,向用户提示相应原因。
5. 充电完成(正常结束 / 故障结束 / 用户中止)后,CCU 计算 **最终充电金额**(按电量 / 时间 / 固定服务费等计费规则)。
6. CCU 再次通过 VTK 协议向 POS 发送“结算请求”,携带最终金额以及会话标识。
7. POS 根据最终金额完成扣款 / 退款(与预扣差额),返回结算结果。
8. CCU 记录最终结算结果到本地订单 / 日志系统,并更新 UI / 平台状态。
- **通信方式**
- 物理层:**串口(UART)**,波特率等推荐按 VTK 文档默认值:`115200 8N1,无流控`
- 链路层:可选择直接使用原始串口帧结构(1F 起始 + 长度 + 协议号 + TLV + CRC16)。
- 应用层:严格遵循 VTK base protocol`0x96FB/0x97FB`)的 TLV 编码要求。
---
## 2. VTK 协议要点(与本项目相关部分)
### 2.1 基本帧格式(串口模式)
参考文档第 2 章,串口下消息帧结构:
- `Start`1 字节,固定 `0x1F`
- `Length`:2 字节,大端,表示“后续所有数据长度(不含 CRC16)”
- `Protocol discriminator`2 字节,大端
- `0x96FB`VMC → POS
- `0x97FB`POS → VMC
- `Application message`0..65533 字节,**BER TLV**ISO/IEC 8825-1
- `CRC16`2 字节,大端,CRC16-CCITT(初始值 0xFFFF),从 `Start` 到 TLV 最后一字节
### 2.2 TLV 参数
VTK base protocol 中应用消息是多个 TLV 参数的组合:
- `Tag`:1~3 字节(此协议中基本为单字节 Tag,例如 `0x01` Message name、`0x04` Amount 等)
- `Length`1~3 字节
- `Value`:具体值(ASCII / Binary / UTF-8
本项目中 **优先使用的 Tag**(参考文档 3.1):
- `0x01`Message nameASCII 3)—— 如 `"CDP"`, `"CRD"`, `"INF"`, `"PRS"` 等。
- `0x03`Operation numberASCII 数字,8 位)—— 可做一次充电会话的 ID。
- `0x04`Amount in minor currency unitASCII,金额的“最小货币单位”,例如分)。
- `0x11`:Local time(可选,用于时钟同步)。
- `0x12`System information(可选,用于 POS 侧展示桩信息)。
> 后续业务实现只需先支持必要的 Tag,留出扩展空间即可。
---
## 3. 业务消息与状态机设计(草案)
### 3.1 关键业务实体
- **ChargeSession(充电会话)**
- `sessionId`:对应 VTK 的 Operation numberTag 0x03)。
- `gunNo`:枪号(0/1/...)。
- `preAuthAmount`:预扣款金额(单位:分)。
- `finalAmount`:最终金额(单位:分)。
- `state``Idle / PreAuthPending / PreAuthOk / Charging / SettlePending / Settled / Failed`
- **VTKOperation**
- `messageName`Tag 0x01):例如 `"CDP"`(预扣款)、`"CRD"`(结算)等。
- `direction``VMC→POS``POS→VMC`
- `tlvList`:一个 TLV 参数列表。
### 3.2 建议消息映射(可根据实际对接调整)
- **预扣款请求**CCU→POS
- Message name (`0x01`)`"CDP"`(或自定义,如 `"CHP"`Charge PreAuth
- Operation number (`0x03`):本次充电会话 ID(ASCII 数字)
- Amount (`0x04`):预扣款金额,单位“分”,例如 5000 表示 50.00 元
- **预扣款响应**POS→CCU
- Message name (`0x01`)`"CDP"`(同请求)
- Operation number (`0x03`):与请求一致
- Amount (`0x04`):实际冻结金额(可与请求相同)
- 结果参数:可以使用 System information 或自定义扩展的 Simple data block`0x0D`)中定义结果码,例如:
- `0`Approved
- `1`Declined
- `2`Timeout
- **结算请求**CCU→POS
- Message name (`0x01`)`"CRD"`Charge Debit / Refund
- Operation number (`0x03`):与本会话相同
- Amount (`0x04`):实际消费总金额(分)
- **结算响应**POS→CCU
- Message name (`0x01`)`"CRD"`
- Operation number (`0x03`):同请求
- Amount (`0x04`):最终扣款金额
- 结果码(同上)
### 3.3 CCU 侧状态机(简化)
1. **Idle**
- 等待用户操作(插枪、刷卡、平台下发启动命令)。
2. **PreAuthPending**
- 组包 VTK `CDP` 预扣款请求,通过串口发给 POS。
- 等待响应(配置超时,例如 15s),期间可处理心跳 / 重发。
3. **PreAuthOk**
- 收到 POS 批准预扣款,更新内部状态,允许充电流程进入 **Charging**
4. **Charging**
- 现有充电流程不变(BMS/流控/计量)。
5. **SettlePending**
- 充电结束,计算 `finalAmount`,组包 VTK `CRD` 结算请求,发送给 POS。
- 等待响应;失败可重试若干次或降级处理(仅记录本地订单,提示用户到平台查询)。
6. **Settled / Failed**
- Settled:记录成功订单,通知平台/界面。
- Failed:记录失败原因(网络/卡拒绝/超时),可与平台对账或人工处理。
---
## 4. 模块划分与文件结构建议
`CCU601E_RUN/app/pos_vtk` 目录下建议创建如下模块(后续实现时使用):
1. **基础协议层**
- `vtk_frame.c / vtk_frame.h`
- 串口帧封装 / 解封装(1F 起始、长度、协议号、CRC16)。
- CRC16-CCITT 实现(参考文档第 5 章)。
- `vtk_tlv.c / vtk_tlv.h`
- TLV 编码 / 解码(Tag/Length/Value)。
- 提供便捷接口:`addAsciiParam(tag, QString)`, `getAsciiParam(tag, ...)` 等。
2. **业务协议层**
- `vtk_pos_protocol.c / vtk_pos_protocol.h`
- 构造/解析 **预扣款请求/响应**、**结算请求/响应**、**系统信息** 等应用消息:
- `buildPreAuthRequest(session, preAuthAmount, QByteArray &outFrame)`
- `parsePreAuthResponse(const QByteArray &frame, VtkResult &out)`
- `buildSettleRequest(...)` / `parseSettleResponse(...)`
3. **会话与状态机层**
- `pos_vtk_service.c / pos_vtk_service.h`
- 维护每把枪的 `ChargeSession` 状态:
- `posVtk_startPreAuth(gunNo, amount)`
- `posVtk_onFrameReceived(frame)` —— 调用基础解析,然后驱动状态机
- `posVtk_notifyChargeEnd(gunNo, finalAmount)` —— 进入结算流程
- 与现有 `flowctrl_task`/`collect_task`/`meter_calculate`/`plat_comm_task` 对接。
4. **串口接入层**
- 若已有串口任务(如 `uart_task`),只需在串口接收回调中把 VTK 帧交给 `vtk_frame` 层解析。
- 若没有,可在 `app_init``plat_comm_task` 下新增 POS 串口任务:
- 周期读取串口缓存、拼帧、调用 `posVtk_onFrameReceived`
---
## 5. 与现有 CCU 模块的集成点
- **启动充电**:在当前启动充电逻辑中(flowctrl / bms / collect),增加“调用 `posVtk_startPreAuth`”步骤,并根据其结果决定是否允许进入充电状态。
- **停止充电**:在充电任务或计量模块中,当确定实际消费金额后,调用 `posVtk_notifyChargeEnd` 进入结算流程。
- **告警与 UI 提示**
- 预扣款失败:在 UI 或平台上显示“支付预授权失败,请重试或更换支付方式”。
- 结算失败:记录订单异常状态,提示用户可联系运营方处理。
---
## 6. 后续实施步骤建议
1. **确认业务映射**:与 POS 提供方确认使用的 Message nameCDP/CRD/INF 等)、结果码格式以及参数集合。
2. **实现基础协议层**`vtk_frame` + `vtk_tlv`,并通过单元测试校验:
- CRC 计算正确。
- TLV 编解码对齐文档。
3. **实现预扣款 / 结算消息构造与解析**`vtk_pos_protocol`
4. **实现 POS 会话状态机**`pos_vtk_service`,并与现有充电流程打通(只改入口/出口逻辑,尽量不动核心 BMS/MDU 代码)。
5. **集成串口任务**:选定 UART 口,完成收发调试(先用日志工具验证 VTK 帧格式)。
6. **与实际 POS 联调**:根据联调结果调整错误码处理、超时重试策略等。
---
## 7. 示例报文
以下示例仅展示 **TLV 内容和串口帧结构**,CRC16 按实现自动计算,示例中不逐字列出。
### 7.1 预扣款请求(CDP
- 业务参数:
- Message name`"CDP"`Tag 0x01
- Operation number`"12345"`Tag 0x03,十进制 ASCII
- Amount`"5000"`Tag 0x04,表示 50.00 元)
- TLV 区段(十六进制):
- `01 03 43 44 50`
- Tag=0x01Len=0x03Value="CDP"
- `03 05 31 32 33 34 35`
- Tag=0x03Len=0x05Value="12345"
- `04 04 35 30 30 30`
- Tag=0x04Len=0x04Value="5000"
- 串口帧结构:
- `1F` 起始字节
- `00 LL` Length2 字节,大端,= ProtocolDisc + TLV 长度)
- `96 FB` Protocol discriminatorVMC→POS
- `01 03 43 44 50 03 05 31 32 33 34 35 04 04 35 30 30 30` TLV 数据
- `CC CC` CRC16-CCITT(由代码计算)
### 7.2 结算请求(CRD
- 参数示意:
- Message name`"CRD"`0x01
- Operation number:与预扣款相同 `"12345"`0x03
- Amount:最终金额 `"4200"`0x04,表示 42.00 元)
TLV 结构与上例相同,仅值不同。
### 7.3 响应报文(POS→CCU
- 假设 POS 对预扣款返回:
- Message name`"CDP"`
- Operation number`"12345"`
- Amount`"5000"`
- ResultTag 0x0DLength=1Value=0x00Approved
TLV 区段:
- `01 03 43 44 50` CDP
- `03 05 31 32 33 34 35` Operation number
- `04 04 35 30 30 30` Amount
- `0D 01 00` Simple data block,结果码=0
串口帧中的 Protocol discriminator 为 `0x97FB`POS→VMC)。
---
> 说明:本文件为第一版设计草案和实现说明,后续在实际联调和业务确认过程中,可根据需要继续细化:
> - 完善错误码与 POS 侧状态码的映射
> - 增加更多业务消息(如 INF、PRS 等)
> - 完善异常流程(网络断开、POS 重启、CRC 错误等)的处理策略。
+260
View File
@@ -0,0 +1,260 @@
#include "pos_vtk/pos_vtk_service.h"
#include "publicdata/public_define.h"
#include <string.h>
#if (POS_VTK_EN)
#ifndef GUN_MAX_CNT
#define GUN_MAX_CNT 2
#endif
/* 简单的会话管理:每把枪对应一个会话 */
static POS_VTK_SESSION_T s_sessions[GUN_MAX_CNT];
static VTK_FRAME_PARSER_T s_frameParser;
static U32_T s_nextOpNumber = 1U;
/* 初始化所有会话状态和 VTK 帧解析器。
* 建议在系统启动、相关任务初始化阶段调用一次。
*/
void pos_vtk_init(void)
{
(void)memset(s_sessions, 0, sizeof(s_sessions));
vtk_frame_parser_init(&s_frameParser);
s_nextOpNumber = 1U;
}
static POS_VTK_SESSION_T *pos_vtk_get_session_ptr(U8_T gunNo)
{
if (gunNo >= GUN_MAX_CNT)
return NULL;
return &s_sessions[gunNo];
}
U8_T pos_vtk_start_preauth(U8_T gunNo, U32_T amountMinor)
{
POS_VTK_SESSION_T *pSess;
U8_T frameBuf[VTK_MAX_FRAME_LEN];
U16_T frameLen;
pSess = pos_vtk_get_session_ptr(gunNo);
if (!pSess)
return 0;
/* 只有 IDLE/DONE/ERROR 状态允许重新发起预扣款 */
if (pSess->e_state != POS_VTK_STATE_IDLE &&
pSess->e_state != POS_VTK_STATE_DONE &&
pSess->e_state != POS_VTK_STATE_ERROR)
{
return 0;
}
if (s_nextOpNumber == 0U)
s_nextOpNumber = 1U;
pSess->opNumber = s_nextOpNumber++;
pSess->preAuthAmount = amountMinor;
pSess->finalAmount = 0U;
pSess->e_lastResult = VTK_POS_RESULT_UNKNOWN;
frameLen = vtk_build_preauth_request(pSess->opNumber, amountMinor,
frameBuf, sizeof(frameBuf));
if (frameLen == 0U)
{
pSess->e_state = POS_VTK_STATE_ERROR;
return 0;
}
POS_VTK_UART_SEND(frameBuf, frameLen);
pSess->e_state = POS_VTK_STATE_PREAUTH_PENDING;
pSess->lastUpdateTick = pos_vtk_get_tick_ms();
return 1;
}
U8_T pos_vtk_notify_charge_end(U8_T gunNo, U32_T amountMinor)
{
POS_VTK_SESSION_T *pSess;
U8_T frameBuf[VTK_MAX_FRAME_LEN];
U16_T frameLen;
pSess = pos_vtk_get_session_ptr(gunNo);
if (!pSess)
return 0;
/* 必须至少完成预扣款(无论是否 Approved),才发结算请求 */
if (pSess->e_state != POS_VTK_STATE_PREAUTH_OK &&
pSess->e_state != POS_VTK_STATE_CHARGING)
{
return 0;
}
pSess->finalAmount = amountMinor;
frameLen = vtk_build_settle_request(pSess->opNumber, amountMinor,
frameBuf, sizeof(frameBuf));
if (frameLen == 0U)
{
pSess->e_state = POS_VTK_STATE_ERROR;
return 0;
}
POS_VTK_UART_SEND(frameBuf, frameLen);
pSess->e_state = POS_VTK_STATE_SETTLE_PENDING;
pSess->lastUpdateTick = pos_vtk_get_tick_ms();
return 1;
}
/* 根据 Operation number 找到对应会话(可能来自任一枪) */
static POS_VTK_SESSION_T *pos_vtk_find_session_by_op(U32_T opNumber)
{
U8_T i;
for (i = 0U; i < GUN_MAX_CNT; i++)
{
if (s_sessions[i].opNumber == opNumber &&
s_sessions[i].opNumber != 0U)
{
return &s_sessions[i];
}
}
return NULL;
}
void pos_vtk_on_rx_byte(U8_T byte)
{
const U8_T *pFrame;
U16_T frameLen;
if (!vtk_frame_parser_input(&s_frameParser, byte, &pFrame, &frameLen))
return;
/* 收到完整 VTK 帧,解析为支付响应 */
{
VTK_POS_PAY_RESP_T resp;
POS_VTK_SESSION_T *pSess;
if (!vtk_parse_payment_response(pFrame, frameLen, &resp))
return;
pSess = pos_vtk_find_session_by_op(resp.operationNumber);
if (!pSess)
return;
pSess->e_lastResult = resp.eResult;
pSess->lastUpdateTick = pos_vtk_get_tick_ms();
/* 根据消息名更新状态机 */
if (strcmp(resp.messageName, "CDP") == 0)
{
if (resp.eResult == VTK_POS_RESULT_APPROVED)
{
pSess->e_state = POS_VTK_STATE_PREAUTH_OK;
}
else
{
pSess->e_state = POS_VTK_STATE_ERROR;
}
}
else if (strcmp(resp.messageName, "CRD") == 0)
{
if (resp.eResult == VTK_POS_RESULT_APPROVED)
{
pSess->e_state = POS_VTK_STATE_DONE;
}
else
{
pSess->e_state = POS_VTK_STATE_ERROR;
}
}
}
}
void pos_vtk_get_session(U8_T gunNo, POS_VTK_SESSION_T *pOut)
{
POS_VTK_SESSION_T *pSess;
if (!pOut)
return;
(void)memset(pOut, 0, sizeof(*pOut));
pSess = pos_vtk_get_session_ptr(gunNo);
if (!pSess)
return;
(void)memcpy(pOut, pSess, sizeof(*pSess));
}
/*
* 外部整体使用示例(伪代码):
*
* 1)初始化
* void System_Init(void)
* {
* ...
* pos_vtk_init();
* ...
* }
*
* 2)串口接收处理
* void UART_POS_Rx_IRQHandler(void)
* {
* U8_T byte;
* if (UART_POS_ReadByte(&byte))
* {
* pos_vtk_on_rx_byte(byte);
* }
* }
*
* 3)启动充电前预扣款
* void StartChargeByUser(U8_T gunNo)
* {
* U32_T preAuthAmountMinor = 5000U; // 预扣 50 元
*
* if (!pos_vtk_start_preauth(gunNo, preAuthAmountMinor))
* {
* // 预扣请求发送失败,直接报错
* return;
* }
*
* // 后台轮询会话状态,等待预扣结果
* POS_VTK_SESSION_T sess;
* pos_vtk_get_session(gunNo, &sess);
* if (sess.e_state == POS_VTK_STATE_PREAUTH_OK &&
* sess.e_lastResult == VTK_POS_RESULT_APPROVED)
* {
* // 预扣成功,允许进入充电流程
* }
* else if (sess.e_state == POS_VTK_STATE_ERROR)
* {
* // 预扣失败,提示用户
* }
* }
*
* 4)充电结束后结算
* void StopChargeAndSettle(U8_T gunNo, U32_T finalAmountMinor)
* {
* if (!pos_vtk_notify_charge_end(gunNo, finalAmountMinor))
* {
* // 结算请求未发出,记录异常
* return;
* }
*
* POS_VTK_SESSION_T sess;
* pos_vtk_get_session(gunNo, &sess);
* if (sess.e_state == POS_VTK_STATE_DONE &&
* sess.e_lastResult == VTK_POS_RESULT_APPROVED)
* {
* // 结算成功,写订单、上报平台
* }
* else if (sess.e_state == POS_VTK_STATE_ERROR)
* {
* // 结算失败,记录异常订单
* }
* }
*/
#endif /* POS_VTK_EN */
+160
View File
@@ -0,0 +1,160 @@
#ifndef POS_VTK_SERVICE_H
#define POS_VTK_SERVICE_H
#include "publicdata/public_define.h"
#if (POS_VTK_EN)
#ifdef __cplusplus
extern "C" {
#endif
#include "main.h"
#include "usart.h"
#include "pos_vtk/vtk_pos_protocol.h"
#include "pos_vtk/vtk_frame.h"
/* POS 串口端口号:
* - 默认使用 E_USART_1,若实际接线不同,可在其它头文件中重定义:
* #define POS_VTK_COM_ID E_USART_9
*/
#ifndef POS_VTK_COM_ID
#define POS_VTK_COM_ID E_USART_1
#endif
/* 串口发送宏:
* - 默认封装为 u16_usart_send(POS_VTK_COM_ID, ...)。
* - 若有特殊需要可以在工程其它位置重定义 POS_VTK_UART_SEND。
*/
#ifndef POS_VTK_UART_SEND
#define POS_VTK_UART_SEND(pData, dataLen) u16_usart_send(POS_VTK_COM_ID, (uint8_t *)(pData), (uint16_t)(dataLen))
#endif
/* 获取系统毫秒节拍的回调(用于会话超时管理):
* - 需在用户工程中实现,例如调用 get_current_millis() 或 HAL_GetTick()。
*/
extern U32_T pos_vtk_get_tick_ms(void);
/* 会话状态 */
typedef enum
{
POS_VTK_STATE_IDLE = 0,
POS_VTK_STATE_PREAUTH_PENDING,
POS_VTK_STATE_PREAUTH_OK,
POS_VTK_STATE_CHARGING,
POS_VTK_STATE_SETTLE_PENDING,
POS_VTK_STATE_DONE,
POS_VTK_STATE_ERROR
} POS_VTK_STATE_E;
typedef struct
{
POS_VTK_STATE_E e_state;
U32_T opNumber; /* VTK Operation number */
U32_T preAuthAmount; /* 预扣金额(分) */
U32_T finalAmount; /* 结算金额(分) */
VTK_POS_RESULT_E e_lastResult; /* 最近一次 POS 返回结果 */
U32_T lastUpdateTick; /* 最近状态更新时间(ms */
} POS_VTK_SESSION_T;
/* 初始化 POS-VTK 服务(需在系统启动时调用一次) */
void pos_vtk_init(void);
/* 充电前启动预扣款流程(CDP
* @param[in] gunNo 枪号(0 ~ GUN_MAX_CNT-1
* @param[in] amountMinor 预扣款金额(单位:分,例如 5000 = 50.00 元)
* @return U8_T 1=请求已发送;0=参数或状态不允许
*
* 使用示例:
* // 系统启动时
* pos_vtk_init();
*
* // 串口接收中断中逐字节喂入
* void UART_POS_Rx_IRQHandler_Byte(U8_T byte)
* {
* pos_vtk_on_rx_byte(byte);
* }
*
* // 用户启动充电时(以枪0为例),预扣 50 元
* if (!pos_vtk_start_preauth(0, 5000U))
* {
* // 预扣请求未正确发送,视为支付失败
* }
*
* // 后台任务轮询会话状态
* POS_VTK_SESSION_T sess;
* pos_vtk_get_session(0, &sess);
* if (sess.e_state == POS_VTK_STATE_PREAUTH_OK &&
* sess.e_lastResult == VTK_POS_RESULT_APPROVED)
* {
* // 预扣款成功,可以进入充电流程
* }
*/
U8_T pos_vtk_start_preauth(U8_T gunNo, U32_T amountMinor);
/* 充电结束后通知进行结算(CRD)
* @param[in] gunNo 枪号
* @param[in] amountMinor 实际消费金额(单位:分)
* @return U8_T 1=结算请求已发送;0=状态不允许
*
* 使用示例:
* // 计费模块算出本次消费 42 元(含服务费等)
* if (!pos_vtk_notify_charge_end(0, 4200U))
* {
* // 结算请求未正确发送,记录异常
* }
*
* // 稍后轮询
* POS_VTK_SESSION_T sess;
* pos_vtk_get_session(0, &sess);
* if (sess.e_state == POS_VTK_STATE_DONE &&
* sess.e_lastResult == VTK_POS_RESULT_APPROVED)
* {
* // 结算成功,写订单、上报平台
* }
* else if (sess.e_state == POS_VTK_STATE_ERROR)
* {
* // 结算失败,记录失败原因,可提示人工干预
* }
*/
U8_T pos_vtk_notify_charge_end(U8_T gunNo, U32_T amountMinor);
/* 串口接收入口(逐字节喂入),通常在 UART 接收中断或接收任务中调用
* @param[in] byte UART 接收到的 1 字节数据
*
* 示例1(使用中断逐字节喂入,需自行从 HAL/usart 中读取 1 字节):
* void UART_POS_IRQHandler(void)
* {
* U8_T byte;
* if (UART_ReadByte(&byte))
* {
* pos_vtk_on_rx_byte(byte);
* }
* }
*
* 示例2(使用 u16_usart_recv 轮询读取):
* void POS_VTK_PollRecv_Task(void)
* {
* U8_T rxBuf[64];
* U16_T len, i;
*
* len = u16_usart_recv(POS_VTK_COM_ID, rxBuf, sizeof(rxBuf));
* for (i = 0; i < len; i++)
* {
* pos_vtk_on_rx_byte(rxBuf[i]);
* }
* }
*/
void pos_vtk_on_rx_byte(U8_T byte);
/* 获取指定枪的会话状态快照,便于上层判断 POS 当前处理结果 */
void pos_vtk_get_session(U8_T gunNo, POS_VTK_SESSION_T *pOut);
#ifdef __cplusplus
}
#endif
#endif /* POS_VTK_EN */
#endif /* POS_VTK_SERVICE_H */
+241
View File
@@ -0,0 +1,241 @@
#include "pos_vtk/vtk_frame.h"
#include "publicdata/public_define.h"
#include <string.h>
#if (POS_VTK_EN)
/******************************************************************************
* 模块说明:VTK 串口帧封装与解析
*
* 本文件负责:
* - 按 VTK 协议格式,将应用层 TLV 数据封装为串口帧(起始字节、Length、协议号、CRC16)。
* - 从串口接收的字节流中拼出完整帧,并做 CRC 校验,输出应用层数据指针与长度。
*
* 对外主要函数:
* - vtk_build_frame() :打包一帧 VTK 串口报文。
* - vtk_parse_frame() :在拿到完整帧后,做 CRC 校验并解析协议号和 App 区段。
* - vtk_frame_parser_init() :初始化流式解析器。
* - vtk_frame_parser_input() :逐字节喂入数据,自动拼出完整帧。
*
* 串口接收侧通常的用法是:
* 1. 在系统初始化时调用 vtk_frame_parser_init() 初始化解析器实例。
* 2. 在 UART 接收中断或接收任务中对每个字节调用 vtk_frame_parser_input()。
* 3. 当返回值为 1 时,说明解析器内部缓冲中已经形成一帧完整报文。
******************************************************************************/
/* CRC16-CCITT (0xFFFF) 计算 */
static U16_T vtk_crc16_ccitt(const U8_T *pData, U16_T len)
{
U16_T crc = 0xFFFF;
U16_T i;
while (len--)
{
crc ^= (U16_T)(*pData++) << 8;
for (i = 0; i < 8; i++)
{
if (crc & 0x8000)
crc = (U16_T)((crc << 1) ^ 0x1021);
else
crc <<= 1;
}
}
return crc;
}
U16_T vtk_build_frame(const U8_T *pApp, U16_T appLen, U16_T protoDisc,
U8_T *pOut, U16_T outMax)
{
U16_T lengthField;
U16_T totalLen;
U16_T crc;
if (!pOut || !pApp || appLen == 0)
return 0;
/* Length 字段 = Protocol Disc(2) + App 数据长度 */
lengthField = (U16_T)(2U + appLen);
totalLen = (U16_T)(1U + 2U + lengthField + 2U); /* Start + Len + Body + CRC */
if (totalLen > outMax || totalLen > VTK_MAX_FRAME_LEN)
return 0;
/* 组帧 */
pOut[0] = VTK_FRAME_START_BYTE;
pOut[1] = (U8_T)(lengthField >> 8);
pOut[2] = (U8_T)(lengthField & 0xFFU);
pOut[3] = (U8_T)(protoDisc >> 8);
pOut[4] = (U8_T)(protoDisc & 0xFFU);
(void)memcpy(&pOut[5], pApp, appLen);
/* CRC 从起始字节到应用数据最后一字节 */
crc = vtk_crc16_ccitt(pOut, (U16_T)(1U + 2U + 2U + appLen));
pOut[5U + appLen] = (U8_T)(crc >> 8);
pOut[6U + appLen] = (U8_T)(crc & 0xFFU);
return totalLen;
}
U8_T vtk_parse_frame(const U8_T *pFrame, U16_T frameLen, VTK_FRAME_INFO_T *pInfo)
{
U16_T lengthField;
U16_T expectedTotalLen;
U16_T crcCalc;
U16_T crcInFrame;
if (!pFrame || !pInfo || frameLen < 1U + 2U + 2U + 2U)
return 0;
if (pFrame[0] != VTK_FRAME_START_BYTE)
return 0;
lengthField = (U16_T)(((U16_T)pFrame[1] << 8) | pFrame[2]);
expectedTotalLen = (U16_T)(1U + 2U + lengthField + 2U);
if (frameLen != expectedTotalLen || frameLen > VTK_MAX_FRAME_LEN)
return 0;
/* CRC 校验 */
crcCalc = vtk_crc16_ccitt(pFrame, (U16_T)(frameLen - 2U));
crcInFrame = (U16_T)(((U16_T)pFrame[frameLen - 2U] << 8) |
pFrame[frameLen - 1U]);
if (crcCalc != crcInFrame)
return 0;
/* Protocol Disc 紧跟在 Length 字段后面 */
pInfo->protoDisc = (U16_T)(((U16_T)pFrame[3] << 8) | pFrame[4]);
pInfo->pApp = &pFrame[5];
pInfo->appLen = (U16_T)(lengthField - 2U); /* 去掉协议号 2 字节 */
return 1;
}
void vtk_frame_parser_init(VTK_FRAME_PARSER_T *parser)
{
if (!parser)
return;
parser->state = VTK_PARSE_WAIT_START;
parser->lengthField = 0;
parser->index = 0;
parser->totalLen = 0;
}
U8_T vtk_frame_parser_input(VTK_FRAME_PARSER_T *parser, U8_T byte,
const U8_T **ppFrame, U16_T *pLen)
{
if (!parser || !ppFrame || !pLen)
return 0;
*ppFrame = NULL;
*pLen = 0;
switch (parser->state)
{
case VTK_PARSE_WAIT_START:
if (byte == VTK_FRAME_START_BYTE)
{
parser->buf[0] = byte;
parser->index = 1U;
parser->state = VTK_PARSE_LEN_H;
}
break;
case VTK_PARSE_LEN_H:
parser->buf[parser->index++] = byte;
parser->state = VTK_PARSE_LEN_L;
break;
case VTK_PARSE_LEN_L:
parser->buf[parser->index++] = byte;
parser->lengthField = (U16_T)(((U16_T)parser->buf[1] << 8) |
parser->buf[2]);
parser->totalLen = (U16_T)(1U + 2U + parser->lengthField + 2U);
if (parser->totalLen > VTK_MAX_FRAME_LEN)
{
/* 长度异常,重新等待起始字节 */
parser->state = VTK_PARSE_WAIT_START;
parser->index = 0;
}
else
{
parser->state = VTK_PARSE_BODY;
}
break;
case VTK_PARSE_BODY:
parser->buf[parser->index++] = byte;
if (parser->index >= parser->totalLen)
{
VTK_FRAME_INFO_T info;
if (vtk_parse_frame(parser->buf, parser->totalLen, &info))
{
*ppFrame = parser->buf;
*pLen = parser->totalLen;
/* 完成一帧后重置状态等待下一帧 */
parser->state = VTK_PARSE_WAIT_START;
parser->index = 0;
return 1;
}
/* CRC 或其他错误,丢弃本帧,重新等待起始字节 */
parser->state = VTK_PARSE_WAIT_START;
parser->index = 0;
}
break;
default:
parser->state = VTK_PARSE_WAIT_START;
parser->index = 0;
break;
}
return 0;
}
/*
* 外部使用示例:
*
* 1)发送一帧带 TLV 的 VTK 报文
*
* U8_T appBuf[VTK_MAX_APP_LEN];
* U16_T appLen = 0;
* U8_T frameBuf[VTK_MAX_FRAME_LEN];
* U16_T frameLen;
*
* // 此处省略 TLV 追加逻辑(建议使用 vtk_tlv_xxx 接口)。
* // appBuf/appLen 中存放 TLV 数据,例如 MessageName/OperationNo/Amount 等。
*
* frameLen = vtk_build_frame(appBuf, appLen, VTK_PD_VMC_TO_POS,
* frameBuf, sizeof(frameBuf));
* if (frameLen > 0U)
* {
* // 通过上层定义的 POS 串口发送宏发送出去
* POS_VTK_UART_SEND(frameBuf, frameLen);
* }
*
* 2)接收侧使用流式解析器:
*
* static VTK_FRAME_PARSER_T s_parser;
*
* void POS_Uart_Init(void)
* {
* vtk_frame_parser_init(&s_parser);
* }
*
* // 在 UART 接收中断 / 任务里调用
* void POS_Uart_Rx_Byte_Handler(U8_T rxByte)
* {
* const U8_T *pFrame;
* U16_T frameLen;
*
* if (vtk_frame_parser_input(&s_parser, rxByte, &pFrame, &frameLen))
* {
* // 这里获得完整帧 pFrame[0..frameLen-1],可继续调用 vtk_parse_frame
* // 或 vtk_parse_payment_response 进行业务解析。
* }
* }
*/
#endif /* POS_VTK_EN */
+134
View File
@@ -0,0 +1,134 @@
#ifndef VTK_FRAME_H
#define VTK_FRAME_H
#include "publicdata/public_define.h"
#if (POS_VTK_EN)
#ifdef __cplusplus
extern "C" {
#endif
#include "main.h"
/* VTK 基础协议号(串口模式) */
#define VTK_PD_VMC_TO_POS ((U16_T)0x96FB) /* CCU(VMC) → POS */
#define VTK_PD_POS_TO_VMC ((U16_T)0x97FB) /* POS → CCU(VMC) */
/* 串口帧相关常量 */
#define VTK_FRAME_START_BYTE ((U8_T)0x1F)
#define VTK_MAX_APP_LEN ((U16_T)512)
#define VTK_MAX_FRAME_LEN ((U16_T)(1 + 2 + 2 + VTK_MAX_APP_LEN + 2)) /* Start + Len + PD + App + CRC */
typedef struct
{
U16_T protoDisc; /* 协议号:VTK_PD_VMC_TO_POS / VTK_PD_POS_TO_VMC */
const U8_T *pApp; /* 指向应用层 TLV 数据起始地址 */
U16_T appLen; /* 应用层 TLV 数据长度 */
} VTK_FRAME_INFO_T;
/* 串口帧打包:把 TLV 应用数据封装为完整 VTK 串口帧
* @param[in] pApp 应用层 TLV 数据指针
* @param[in] appLen 应用层 TLV 数据长度
* @param[in] protoDisc 协议号(通常为 VTK_PD_VMC_TO_POS
* @param[out] pOut 输出缓冲区
* @param[in] outMax 输出缓冲区大小
* @return U16_T 串口帧总长度;0 表示失败(缓冲区不足等)
*/
U16_T vtk_build_frame(const U8_T *pApp, U16_T appLen, U16_T protoDisc,
U8_T *pOut, U16_T outMax);
/* 串口帧解析:对一整帧(含 0x1F 起始和 CRC)做合法性校验,并给出应用层 TLV 区段
* @param[in] pFrame 输入帧缓冲区
* @param[in] frameLen 输入帧长度
* @param[out] pInfo 输出解析结果(协议号、应用层指针与长度)
* @return U8_T 1=解析成功且 CRC 正确,0=失败
*/
U8_T vtk_parse_frame(const U8_T *pFrame, U16_T frameLen, VTK_FRAME_INFO_T *pInfo);
/* 流式解析器:用于从串口逐字节接收数据并拼出完整 VTK 帧 */
typedef enum
{
VTK_PARSE_WAIT_START = 0,
VTK_PARSE_LEN_H,
VTK_PARSE_LEN_L,
VTK_PARSE_BODY,
VTK_PARSE_CRC_H,
VTK_PARSE_CRC_L
} VTK_PARSE_STATE_E;
typedef struct
{
VTK_PARSE_STATE_E state;
U16_T lengthField; /* Length 字段值(Protocol Disc + App 长度) */
U16_T index; /* 当前已接收字节数 */
U16_T totalLen; /* 预期整帧长度(含起始和 CRC) */
U8_T buf[VTK_MAX_FRAME_LEN]; /* 内部缓冲区 */
} VTK_FRAME_PARSER_T;
/* 初始化帧解析器 */
void vtk_frame_parser_init(VTK_FRAME_PARSER_T *parser);
/* 向帧解析器输入 1 字节
* @param[in,out] parser 解析器状态
* @param[in] byte 串口接收到的 1 字节
* @param[out] ppFrame 当返回 1 时,指向完整帧起始地址(内部缓冲区)
* @param[out] pLen 当返回 1 时,给出完整帧长度
* @return U8_T 1=拼出完整合法帧;0=尚未完成或校验失败
*/
U8_T vtk_frame_parser_input(VTK_FRAME_PARSER_T *parser, U8_T byte,
const U8_T **ppFrame, U16_T *pLen);
/* 使用示例(发送 + 接收):
*
* // 发送侧:构造一帧带 TLV 的 VTK 串口报文并通过 UART 发送
* U8_T tlvBuf[VTK_MAX_APP_LEN];
* U16_T tlvLen = 0;
* U8_T frameBuf[VTK_MAX_FRAME_LEN];
* U16_T frameLen;
*
* // 追加 TLV 参数,例如 MessageName(Tag=0x01)="CDP"
* vtk_tlv_append_ascii(0x01, "CDP", tlvBuf, sizeof(tlvBuf), &tlvLen);
* // 追加 OperationNo(Tag=0x03)="12345"
* vtk_tlv_append_ascii_uint(0x03, 12345, 1, tlvBuf, sizeof(tlvBuf), &tlvLen);
* // 追加 Amount(Tag=0x04)="5000"
* vtk_tlv_append_ascii_uint(0x04, 5000, 1, tlvBuf, sizeof(tlvBuf), &tlvLen);
*
* // 封装为串口帧并发送
* frameLen = vtk_build_frame(tlvBuf, tlvLen, VTK_PD_VMC_TO_POS,
* frameBuf, sizeof(frameBuf));
* if (frameLen > 0U)
* {
* POS_VTK_UART_SEND(frameBuf, frameLen); // 由上层实现的串口发送函数
* }
*
* // 接收侧:在 UART 接收中断/任务中逐字节喂入解析器
* static VTK_FRAME_PARSER_T s_parser;
* static U8_T parserInited = 0;
*
* if (!parserInited)
* {
* vtk_frame_parser_init(&s_parser);
* parserInited = 1;
* }
*
* void UART_Rx_Handler_Byte(U8_T rxByte)
* {
* const U8_T *pFrame;
* U16_T frameLen;
*
* if (vtk_frame_parser_input(&s_parser, rxByte, &pFrame, &frameLen))
* {
* // 这里获得了一整帧 VTK 报文,可继续调用 vtk_parse_frame / vtk_parse_payment_response 解析
* }
* }
*/
#ifdef __cplusplus
}
#endif
#endif /* POS_VTK_EN */
#endif /* VTK_FRAME_H */
+191
View File
@@ -0,0 +1,191 @@
#include "pos_vtk/vtk_pos_protocol.h"
#include "pos_vtk/vtk_tlv.h"
#include "publicdata/public_define.h"
#include <string.h>
#if (POS_VTK_EN)
/******************************************************************************
* 模块说明:VTK 支付消息构造与解析
*
* 负责将业务参数映射为 VTK TLV 参数,并打包/解析串口帧:
* - vtk_build_preauth_request() :构造预扣款(CDP)请求。
* - vtk_build_settle_request() :构造结算(CRD)请求。
* - vtk_parse_payment_response(): 解析 POS 返回的 CDP/CRD 响应结果。
*
* 内部约定:
* - Tag 0x01: Message name ASCII,例如 "CDP"/"CRD"
* - Tag 0x03: Operation no. ASCII 十进制)
* - Tag 0x04: Amount ASCII 十进制,单位:分)
* - Tag 0x0D: Simple data block 的首字节为结果码:
* 0 = Approved, 1 = Declined, 2 = Timeout, 其它 = Unknown
******************************************************************************/
/* 内部约定:结果码放在 Simple data block(Tag=0x0D) 的首字节
* 0 = Approved, 1 = Declined, 2 = Timeout, 其它 = Unknown
*/
#define VTK_TAG_MESSAGE_NAME ((U8_T)0x01)
#define VTK_TAG_OPERATION_NO ((U8_T)0x03)
#define VTK_TAG_AMOUNT ((U8_T)0x04)
#define VTK_TAG_RESULT ((U8_T)0x0D)
static VTK_POS_RESULT_E vtk_result_from_code(U8_T code)
{
switch (code)
{
case 0: return VTK_POS_RESULT_APPROVED;
case 1: return VTK_POS_RESULT_DECLINED;
case 2: return VTK_POS_RESULT_TIMEOUT;
default: return VTK_POS_RESULT_UNKNOWN;
}
}
static U32_T vtk_ascii_to_u32(const U8_T *p, U16_T len)
{
U32_T v = 0;
U16_T i;
if (!p || len == 0)
return 0;
for (i = 0; i < len; i++)
{
if (p[i] < '0' || p[i] > '9')
break;
v = v * 10U + (U32_T)(p[i] - '0');
}
return v;
}
static U16_T vtk_build_common(const char *msgName,
U32_T opNumber,
U32_T amountMinor,
U8_T *pOutFrame,
U16_T outMax)
{
U8_T tlvBuf[VTK_MAX_APP_LEN];
U16_T tlvLen = 0U;
if (!msgName || !pOutFrame)
return 0;
/* 消息名,例如 "CDP" / "CRD" */
if (!vtk_tlv_append_ascii(VTK_TAG_MESSAGE_NAME, msgName,
tlvBuf, sizeof(tlvBuf), &tlvLen))
return 0;
/* 操作号:十进制 ASCII */
if (!vtk_tlv_append_ascii_uint(VTK_TAG_OPERATION_NO, opNumber, 1U,
tlvBuf, sizeof(tlvBuf), &tlvLen))
return 0;
/* 金额(分):十进制 ASCII */
if (!vtk_tlv_append_ascii_uint(VTK_TAG_AMOUNT, amountMinor, 1U,
tlvBuf, sizeof(tlvBuf), &tlvLen))
return 0;
/* 组装 VTK 串口帧(VMC→POS */
return vtk_build_frame(tlvBuf, tlvLen, VTK_PD_VMC_TO_POS,
pOutFrame, outMax);
}
U16_T vtk_build_preauth_request(U32_T opNumber, U32_T amountMinor,
U8_T *pOutFrame, U16_T outMax)
{
return vtk_build_common("CDP", opNumber, amountMinor,
pOutFrame, outMax);
}
U16_T vtk_build_settle_request(U32_T opNumber, U32_T amountMinor,
U8_T *pOutFrame, U16_T outMax)
{
return vtk_build_common("CRD", opNumber, amountMinor,
pOutFrame, outMax);
}
U8_T vtk_parse_payment_response(const U8_T *pFrame, U16_T frameLen,
VTK_POS_PAY_RESP_T *pResp)
{
VTK_FRAME_INFO_T info;
const U8_T *pVal;
U16_T valLen;
if (!pFrame || !pResp)
return 0;
if (!vtk_parse_frame(pFrame, frameLen, &info))
return 0;
/* 只接受来自 POS 的响应(0x97FB */
if (info.protoDisc != VTK_PD_POS_TO_VMC)
return 0;
(void)memset(pResp, 0, sizeof(*pResp));
pResp->eResult = VTK_POS_RESULT_UNKNOWN;
/* 消息名 */
if (vtk_tlv_find_first(info.pApp, info.appLen,
VTK_TAG_MESSAGE_NAME, &pVal, &valLen))
{
if (valLen > 3U) valLen = 3U;
(void)memcpy(pResp->messageName, pVal, valLen);
pResp->messageName[valLen] = '\0';
}
/* 操作号 */
if (vtk_tlv_find_first(info.pApp, info.appLen,
VTK_TAG_OPERATION_NO, &pVal, &valLen))
{
pResp->operationNumber = vtk_ascii_to_u32(pVal, valLen);
}
/* 金额 */
if (vtk_tlv_find_first(info.pApp, info.appLen,
VTK_TAG_AMOUNT, &pVal, &valLen))
{
pResp->amountMinor = vtk_ascii_to_u32(pVal, valLen);
}
/* 结果码(Simple data block 首字节) */
if (vtk_tlv_find_first(info.pApp, info.appLen,
VTK_TAG_RESULT, &pVal, &valLen))
{
if (valLen > 0U)
{
pResp->eResult = vtk_result_from_code(pVal[0]);
}
}
return 1;
}
/*
* 外部使用示例:
*
* 1)构造并发送预扣款请求(CDP)
*
* U8_T frameBuf[VTK_MAX_FRAME_LEN];
* U16_T frameLen;
*
* // 操作号 12345,预扣 50 元(5000 分)
* frameLen = vtk_build_preauth_request(12345U, 5000U,
* frameBuf, sizeof(frameBuf));
* if (frameLen > 0U)
* {
* POS_VTK_UART_SEND(frameBuf, frameLen);
* }
*
* 2)解析 POS 返回的响应(在串口接收处理里获得完整 VTK 帧后调用)
*
* VTK_POS_PAY_RESP_T resp;
* if (vtk_parse_payment_response(frameBuf, frameLen, &resp))
* {
* // resp.messageName 例如 "CDP"/"CRD"
* // resp.operationNumber 例如 12345
* // resp.amountMinor 例如 5000
* // resp.eResult = VTK_POS_RESULT_APPROVED / DECLINED / TIMEOUT / UNKNOWN
* }
*/
#endif /* POS_VTK_EN */
+69
View File
@@ -0,0 +1,69 @@
#ifndef VTK_POS_PROTOCOL_H
#define VTK_POS_PROTOCOL_H
#include "publicdata/public_define.h"
#if (POS_VTK_EN)
#ifdef __cplusplus
extern "C" {
#endif
#include "main.h"
#include "pos_vtk/vtk_frame.h"
/* 预扣款 / 结算结果枚举 */
typedef enum
{
VTK_POS_RESULT_UNKNOWN = 0,
VTK_POS_RESULT_APPROVED = 1,
VTK_POS_RESULT_DECLINED = 2,
VTK_POS_RESULT_TIMEOUT = 3
} VTK_POS_RESULT_E;
/* 支付类响应解析结果 */
typedef struct
{
char messageName[4]; /* TLV(0x01) 的消息名,3 字节,末尾补 '\0' */
U32_T operationNumber; /* TLV(0x03) 中的操作号(十进制 ASCII */
U32_T amountMinor; /* TLV(0x04) 中的金额(分) */
VTK_POS_RESULT_E eResult; /* TLV(0x0D) 中的结果码(简单定义:0/1/2/...) */
} VTK_POS_PAY_RESP_T;
/* 构造预扣款请求帧(CDP
* @param[in] opNumber 会话操作号(Operation number
* @param[in] amountMinor 预扣款金额(分)
* @param[out] pOutFrame 输出串口帧缓冲
* @param[in] outMax 缓冲区大小
* @return U16_T 串口帧长度;0=失败
*/
U16_T vtk_build_preauth_request(U32_T opNumber, U32_T amountMinor,
U8_T *pOutFrame, U16_T outMax);
/* 构造结算请求帧(CRD
* @param[in] opNumber 会话操作号(同预扣款)
* @param[in] amountMinor 最终金额(分)
* @param[out] pOutFrame 输出串口帧缓冲
* @param[in] outMax 缓冲区大小
* @return U16_T 串口帧长度;0=失败
*/
U16_T vtk_build_settle_request(U32_T opNumber, U32_T amountMinor,
U8_T *pOutFrame, U16_T outMax);
/* 解析来自 POS 的支付类响应(适用于 CDP/CRD)
* @param[in] pFrame 串口帧(包含 0x1F 起始和 CRC)
* @param[in] frameLen 帧长度
* @param[out] pResp 解析出的关键字段
* @return U8_T 1=解析成功 0=失败(CRC/TLV/字段缺失等)
*/
U8_T vtk_parse_payment_response(const U8_T *pFrame, U16_T frameLen,
VTK_POS_PAY_RESP_T *pResp);
#ifdef __cplusplus
}
#endif
#endif /* POS_VTK_EN */
#endif /* VTK_POS_PROTOCOL_H */
+167
View File
@@ -0,0 +1,167 @@
#include "pos_vtk/vtk_tlv.h"
#include "publicdata/public_define.h"
#include <string.h>
#if (POS_VTK_EN)
/******************************************************************************
* 模块说明:VTK TLV 编解码工具
*
* - 仅支持简单的 TLV 格式:1 字节 Tag + 1 字节 Length + Value。
* - 用于构造 VTK 应用层 TLV 消息(例如 Message name、Operation number、Amount)。
*
* 对外接口:
* - vtk_tlv_append() :追加任意二进制 TLV。
* - vtk_tlv_append_ascii() :追加 ASCII 字符串 TLV。
* - vtk_tlv_append_ascii_uint(): 以十进制 ASCII 形式追加整数 TLV。
* - vtk_tlv_find_first() :在 TLV 缓冲中查找首个指定 Tag。
******************************************************************************/
U8_T vtk_tlv_append(U8_T tag, const U8_T *pVal, U16_T len,
U8_T *pBuf, U16_T bufMax, U16_T *pOffset)
{
U16_T offset;
if (!pBuf || !pOffset)
return 0;
offset = *pOffset;
/* 仅支持短格式长度(1 字节,len < 128 */
if (len > 127U)
return 0;
if ((U16_T)(offset + 2U + len) > bufMax)
return 0;
pBuf[offset++] = tag;
pBuf[offset++] = (U8_T)len;
if (len > 0U && pVal)
{
(void)memcpy(&pBuf[offset], pVal, len);
offset = (U16_T)(offset + len);
}
*pOffset = offset;
return 1;
}
U8_T vtk_tlv_append_ascii(U8_T tag, const char *str,
U8_T *pBuf, U16_T bufMax, U16_T *pOffset)
{
U16_T len;
if (!str)
return 0;
len = (U16_T)strlen(str);
return vtk_tlv_append(tag, (const U8_T *)str, len, pBuf, bufMax, pOffset);
}
/* 把无符号整数转换为十进制 ASCII(不带 '\0'),右对齐或自然长度 */
static U8_T vtk_utoa_dec(U32_T value, char *out, U8_T minWidth)
{
char buf[16];
U8_T i = 0U;
U8_T len;
if (!out)
return 0;
/* 生成反向字符串 */
do
{
buf[i++] = (char)('0' + (value % 10U));
value /= 10U;
} while (value != 0U && i < sizeof(buf));
/* 实际长度至少为 minWidth */
while (i < minWidth && i < sizeof(buf))
{
buf[i++] = '0';
}
len = i;
/* 反转到输出缓冲 */
while (i > 0U)
{
*out++ = buf[--i];
}
return len;
}
U8_T vtk_tlv_append_ascii_uint(U8_T tag, U32_T value, U8_T minWidth,
U8_T *pBuf, U16_T bufMax, U16_T *pOffset)
{
char tmp[16];
U8_T len;
len = vtk_utoa_dec(value, tmp, minWidth);
if (len == 0U)
return 0;
return vtk_tlv_append(tag, (const U8_T *)tmp, len, pBuf, bufMax, pOffset);
}
U8_T vtk_tlv_find_first(const U8_T *pBuf, U16_T bufLen, U8_T tag,
const U8_T **ppVal, U16_T *pValLen)
{
U16_T offset = 0U;
if (!pBuf || !ppVal || !pValLen)
return 0;
while (offset + 2U <= bufLen)
{
U8_T t = pBuf[offset];
U8_T l = pBuf[offset + 1U];
U16_T next;
next = (U16_T)(offset + 2U + l);
if (next > bufLen)
return 0; /* TLV 结构损坏 */
if (t == tag)
{
*ppVal = &pBuf[offset + 2U];
*pValLen = l;
return 1;
}
offset = next;
}
return 0;
}
/*
* 外部使用示例:
*
* // 构造 TLV 缓冲用于预扣款请求(CDP)
* U8_T tlvBuf[VTK_MAX_APP_LEN];
* U16_T tlvLen = 0;
*
* // Tag 0x01: Message name = "CDP"
* vtk_tlv_append_ascii(0x01, "CDP", tlvBuf, sizeof(tlvBuf), &tlvLen);
*
* // Tag 0x03: Operation number = "12345"
* vtk_tlv_append_ascii_uint(0x03, 12345U, 1U, tlvBuf, sizeof(tlvBuf), &tlvLen);
*
* // Tag 0x04: Amount = "5000" (单位:分)
* vtk_tlv_append_ascii_uint(0x04, 5000U, 1U, tlvBuf, sizeof(tlvBuf), &tlvLen);
*
* // 解析响应时查找结果码(假设结果码放在 Tag 0x0D 的 Simple data block 首字节)
* const U8_T *pVal;
* U16_T valLen;
* if (vtk_tlv_find_first(tlvBuf, tlvLen, 0x0D, &pVal, &valLen) && valLen > 0U)
* {
* U8_T resultCode = pVal[0];
* // 0=Approved, 1=Declined, 2=Timeout...
* }
*/
#endif /* POS_VTK_EN */
+58
View File
@@ -0,0 +1,58 @@
#ifndef VTK_TLV_H
#define VTK_TLV_H
#include "publicdata/public_define.h"
#if (POS_VTK_EN)
#ifdef __cplusplus
extern "C" {
#endif
#include "main.h"
/* 简化的 TLV 工具:目前仅支持
* - 1 字节 Tag
* - 短格式 Length1 字节,长度 < 128
* 满足本项目中金额 / 操作号 / 消息名等中小长度字段需求。
*/
/* 在缓冲末尾追加一个 TLV
* @param[in] tag 参数标识
* @param[in] pVal 值缓冲区
* @param[in] len 值长度
* @param[in,out] pBuf 目标缓冲区
* @param[in] bufMax 目标缓冲区大小
* @param[in,out] pOffset 当前已使用长度,调用成功后自动累加
* @return U8_T 1=成功 0=失败(空间不足或长度超限)
*/
U8_T vtk_tlv_append(U8_T tag, const U8_T *pVal, U16_T len,
U8_T *pBuf, U16_T bufMax, U16_T *pOffset);
/* 追加 ASCII 字符串 TLV(不含终止符 '\0' */
U8_T vtk_tlv_append_ascii(U8_T tag, const char *str,
U8_T *pBuf, U16_T bufMax, U16_T *pOffset);
/* 以十进制 ASCII 形式追加无符号整数(长度可变,最小宽度由 minWidth 控制) */
U8_T vtk_tlv_append_ascii_uint(U8_T tag, U32_T value, U8_T minWidth,
U8_T *pBuf, U16_T bufMax, U16_T *pOffset);
/* 在 TLV 缓冲中查找第一个指定 Tag
* @param[in] pBuf TLV 缓冲区
* @param[in] bufLen 缓冲区长度
* @param[in] tag 要查找的 Tag
* @param[out] ppVal 找到时指向 Value 起始地址
* @param[out] pValLen 找到时的 Value 长度
* @return U8_T 1=找到 0=未找到或格式错误
*/
U8_T vtk_tlv_find_first(const U8_T *pBuf, U16_T bufLen, U8_T tag,
const U8_T **ppVal, U16_T *pValLen);
#ifdef __cplusplus
}
#endif
#endif /* POS_VTK_EN */
#endif /* VTK_TLV_H */