Initial commit: CCU621_M firmware project with BLE debug link support.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
# CCU–POS VTK 串口结算功能设计说明(草案)
|
||||
|
||||
版本:v0.1
|
||||
协议参考:`vtk-protocol-en-1.23.pdf`(VTK protocol 1.23)
|
||||
|
||||
---
|
||||
|
||||
## 1. 应用场景概述
|
||||
|
||||
- **角色**
|
||||
- **VMC(Vending 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 name(ASCII 3)—— 如 `"CDP"`, `"CRD"`, `"INF"`, `"PRS"` 等。
|
||||
- `0x03`:Operation number(ASCII 数字,8 位)—— 可做一次充电会话的 ID。
|
||||
- `0x04`:Amount in minor currency unit(ASCII,金额的“最小货币单位”,例如分)。
|
||||
- `0x11`:Local time(可选,用于时钟同步)。
|
||||
- `0x12`:System information(可选,用于 POS 侧展示桩信息)。
|
||||
|
||||
> 后续业务实现只需先支持必要的 Tag,留出扩展空间即可。
|
||||
|
||||
---
|
||||
|
||||
## 3. 业务消息与状态机设计(草案)
|
||||
|
||||
### 3.1 关键业务实体
|
||||
|
||||
- **ChargeSession(充电会话)**
|
||||
- `sessionId`:对应 VTK 的 Operation number(Tag 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 name(CDP/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=0x01,Len=0x03,Value="CDP"
|
||||
- `03 05 31 32 33 34 35`
|
||||
- Tag=0x03,Len=0x05,Value="12345"
|
||||
- `04 04 35 30 30 30`
|
||||
- Tag=0x04,Len=0x04,Value="5000"
|
||||
|
||||
- 串口帧结构:
|
||||
- `1F` 起始字节
|
||||
- `00 LL` Length(2 字节,大端,= ProtocolDisc + TLV 长度)
|
||||
- `96 FB` Protocol discriminator(VMC→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"`
|
||||
- Result:Tag 0x0D,Length=1,Value=0x00(Approved)
|
||||
|
||||
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 错误等)的处理策略。
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
* - 短格式 Length(1 字节,长度 < 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 */
|
||||
|
||||
Reference in New Issue
Block a user