目录
- 一、前言
- 二、增加点
- 三、local_add_point:写入全局数组
- 四、add_point_map:建立寄存器映射
- 五、modbus_write_point_maps:下发映射到中控
- 六、删除点
- 七、修改点
- 八、总结
- 九、结尾
一、前言
大家好,这里是 Hello_Embed 。上篇完成了配置文件读写的情景分析,了解了 control.cfg 的格式以及程序启动时如何从文件恢复运行状态。本篇继续情景分析,深入增加点、删除点、修改点三个操作的完整流程。
这三个操作的骨架是一致的:
前台 rpc_xxx_point() → JsonRPC 网络传输 → 后台 server_xxx_point()
↓
local_xxx_point() ← 更新内存
↓
write_cfg() ← 持久化到文件
↓
create_point_maps() ← 重建映射表
↓
modbus_write_point_maps() ← 下发中控
二、增加点
2.1 前台:rpc_add_point
前台在 GUI 上填完配置后调用 rpc_add_point,构造带命名参数的 JSON-RPC 请求并发出:
c
/* 返回值:新点的 point 编号(>=0),失败返回 -1 */
int rpc_add_point(int iSocketClient, char *port_info, int channel,
int dev_addr, int reg_addr, char *reg_type, int period)
{
char buf[300];
int iLen;
/* 构造带对象型 params 的 JSON-RPC 请求 */
sprintf(buf,
"{\"method\": \"add_point\","
"\"params\":"
"{\"port_info\": \"%s\","
"\"channel\": %d,"
"\"dev_addr\": %d,"
"\"reg_addr\": %d,"
"\"reg_type\": \"%s\","
"\"period\": %d},"
"\"id\": \"2\" }",
port_info, channel, dev_addr, reg_addr, reg_type, period);
iLen = send(iSocketClient, buf, strlen(buf), 0);
if (iLen == strlen(buf))
{
/* 接收响应,跳过孤立的换行符 */
while (1)
{
iLen = recv(iSocketClient, buf, sizeof(buf), 0);
buf[iLen] = '\0';
if (iLen == 1 && (buf[0] == '\r' || buf[0] == '\n'))
continue;
else
break;
}
if (iLen > 0)
{
cJSON *root = cJSON_Parse(buf);
cJSON *result = cJSON_GetObjectItem(root, "result");
if (result)
{
int point = result->valueint; // 后台返回的点编号
cJSON_Delete(root);
return point;
}
else
{
cJSON_Delete(root);
return -1;
}
}
else
{
DEBUG_PRINTF("read rpc reply err : %d\n", iLen);
return -1;
}
}
else
{
DEBUG_PRINTF("send rpc request err : %d, %s\n", iLen, strerror(errno));
return -1;
}
}
2.2 后台:server_add_point
后台收到请求后,执行五步操作:
cpp
bool IndustrialControlRpc::server_add_point(const Json::Value& root, Json::Value& response)
{
int point;
PointInfo tInfo;
Json::Value params = root["params"];
response["jsonrpc"] = "2.0";
response["id"] = root["id"];
if (params == Json::Value::null)
{
std::cout << "no params" << std::endl;
response["result"] = -1;
return true;
}
/* 第1步:从 JSON 取出所有字段,填充 tInfo */
strncpy_s(tInfo.port_info, params["port_info"].asString().c_str(), sizeof(tInfo.port_info) - 1);
tInfo.channel = params["channel"].asInt();
tInfo.dev_addr = params["dev_addr"].asInt();
tInfo.reg_addr = params["reg_addr"].asInt();
strncpy_s(tInfo.reg_type, params["reg_type"].asString().c_str(), sizeof(tInfo.reg_type) - 1);
tInfo.period = params["period"].asInt();
/* 第2步:写入全局点数组,分配 point 编号 */
point = local_add_point(&tInfo);
printf("add point %d: %s\r\n", point, tInfo.port_info);
response["result"] = point; // 把点编号返回给前台
/* 第3步:持久化到配置文件 */
write_cfg();
/* 第4步:重新计算寄存器映射表 */
create_point_maps();
/* 第5步:通过 Modbus 把映射表下发给中控 H5 */
modbus_write_point_maps();
return true;
}
解析 :每次添加点后,后台都会连续执行"写配置 → 建映射 → 下发中控"三步,缺一不可:
write_cfg保证重启不丢点;create_point_maps把新点的寄存器偏移算出来;modbus_write_point_maps让中控 H5 知道"哪个主控寄存器对应哪个传感器寄存器"。删除点和修改点的末尾也完全相同。
三、local_add_point:写入全局数组
local_add_point 负责在内存数组中找一个空槽,分配点编号并写入:
c
int local_add_point(PPointInfo pNewInfo)
{
int i;
PPointInfo pInfo;
int point = -1; // 当前已知的最大点编号,用于分配新编号
/* 遍历数组,找到第一个空槽(dev_addr == 0),同时记录已有点的最大编号 */
for (i = 0; i < MAX_POINTS; i++)
{
if (g_PointInfos[i].dev_addr == 0)
break; // 找到空槽,停止
if (point < g_PointInfos[i].point)
point = g_PointInfos[i].point; // 更新最大编号
}
if (i == MAX_POINTS)
return -1; // 数组已满,无法再添加
pInfo = &g_PointInfos[i];
/* 拷贝连接信息,顺便把 "COM" 统一转小写 "com"(跨平台兼容) */
strncpy_s(pInfo->port_info, pNewInfo->port_info, sizeof(pInfo->port_info) - 1);
if (pInfo->port_info[0] == 'C') pInfo->port_info[0] = 'c';
if (pInfo->port_info[1] == 'O') pInfo->port_info[1] = 'o';
if (pInfo->port_info[2] == 'M') pInfo->port_info[2] = 'm';
pInfo->channel = pNewInfo->channel;
pInfo->dev_addr = pNewInfo->dev_addr;
pInfo->reg_addr = pNewInfo->reg_addr;
strncpy_s(pInfo->reg_type, pNewInfo->reg_type, sizeof(pInfo->reg_type) - 1);
pInfo->period = pNewInfo->period;
pInfo->point = point + 1; // 新点编号 = 当前最大编号 + 1
return pInfo->point;
}
解析 :点编号的分配策略是"最大值 + 1"而非"当前数组下标"。这样即使中间有删除操作,编号也不会因移位而重复------已分配的编号不会被复用,前台持有的 point 句柄在删除之前始终有效。
"COM"转小写是为了在 Linux 下(/dev/ttyUSB0)和 Windows 下行为一致,避免因大小写差异导致 Modbus 连接失败。
四、add_point_map:建立寄存器映射
create_point_maps 遍历 g_PointInfos,对每个点调用 add_point_map,建立"主控寄存器 ↔ 传感器寄存器"的对应关系:
c
static int add_point_map(PHostPointMap ptHostPointMap, PPointInfo ptPointInfo)
{
int reg_addr_master = 0;
int i, b;
PHostPointMap ptHostPointMapTmp;
/* 第一步:扫描所有已用的主控寄存器地址,找出最大值 + 1 作为新分配地址 */
for (b = 0; b < MAX_MODBUS; b++)
{
ptHostPointMapTmp = &g_HostPointMaps[b];
for (i = 0;
i < MAX_POINT_COUNT && ptHostPointMapTmp->tPointMaps[i].reg_type[0];
i++)
{
/* 只在相同寄存器类型(HR/CO 等)内比较,不同类型地址空间互不干扰 */
if (!strcmp(ptHostPointMapTmp->tPointMaps[i].reg_type, ptPointInfo->reg_type))
{
if (reg_addr_master <= ptHostPointMapTmp->tPointMaps[i].reg_addr_master)
reg_addr_master = ptHostPointMapTmp->tPointMaps[i].reg_addr_master + 1;
}
}
}
/* 第二步:在当前 HostPointMap 里找到一个空闲的 tPointMaps 槽 */
for (i = 0; i < MAX_POINT_COUNT && ptHostPointMap->tPointMaps[i].reg_type[0]; i++)
{
}
if (i >= MAX_POINT_COUNT)
return -1; // 槽已满
/* 第三步:填写映射条目 */
strncpy_s(ptHostPointMap->port_info,
ptPointInfo->port_info,
sizeof(ptHostPointMap->port_info) - 1);
strncpy_s(ptHostPointMap->tPointMaps[i].reg_type,
ptPointInfo->reg_type,
sizeof(ptHostPointMap->tPointMaps[i].reg_type) - 1);
ptHostPointMap->tPointMaps[i].reg_addr_master = reg_addr_master; // 主控侧地址(自动分配)
ptHostPointMap->tPointMaps[i].channel = ptPointInfo->channel;
ptHostPointMap->tPointMaps[i].dev_addr = ptPointInfo->dev_addr;
ptHostPointMap->tPointMaps[i].reg_addr_salve = ptPointInfo->reg_addr; // 传感器侧地址
return 0;
}
解析:主控寄存器地址由程序自动分配(当前最大地址 + 1),不需要人工指定。同一寄存器类型(如 HR 保持寄存器)内地址连续递增,不同类型(HR/CO)各自有独立的地址空间,互不干扰。这样中控 H5 拿到映射表后,就能通过读写自己的寄存器来透明地访问后端各传感器。
五、modbus_write_point_maps:下发映射到中控
modbus_write_point_maps 把内存中所有 PointMap 条目打包,通过 Modbus 写文件命令发给中控:
c
int modbus_write_point_maps(void)
{
PHostPointMap ptHostPointMap;
int file_size = 0;
modbus_t *ctx = NULL;
int rc = 0;
int i, j, cnt = 0;
PPointMap pNewMap;
/* 第一遍:统计所有映射条目数,计算总 buf 大小 */
for (i = 0; i < MAX_MODBUS; i++)
{
if (!g_HostPointMaps[i].port_info[0])
break; // port_info 为空 → 哨兵,停止
ptHostPointMap = &g_HostPointMaps[i];
for (j = 0; j < MAX_POINT_COUNT && ptHostPointMap->tPointMaps[j].reg_type[0]; j++);
file_size += j * sizeof(PointMap);
}
if (file_size == 0)
return 0; // 没有任何映射,直接退出
/* 第二遍:分配总映射表,把所有条目展平到线性 buf */
pNewMap = (PPointMap)malloc(file_size);
for (i = 0; i < MAX_MODBUS; i++)
{
if (!g_HostPointMaps[i].port_info[0])
break;
ptHostPointMap = &g_HostPointMaps[i];
for (j = 0; j < MAX_POINT_COUNT && ptHostPointMap->tPointMaps[j].reg_type[0]; j++)
pNewMap[cnt++] = ptHostPointMap->tPointMaps[j]; // 逐条拷贝
}
/* 第三遍:通过每个 Modbus 上下文把总映射表写入中控 */
for (i = 0; i < MAX_MODBUS; i++)
{
if (!g_HostPointMaps[i].port_info[0])
break;
ptHostPointMap = &g_HostPointMaps[i];
ctx = get_modbus_ctx(ptHostPointMap->port_info); // 获取对应接口的 Modbus 上下文
if (!ctx)
continue;
modbus_set_slave(ctx, 1); // 中控 H5 的 Modbus 地址固定为 1
rc = modbus_write_file(ctx, 0,
(uint8_t *)"reg_map",
(uint8_t *)pNewMap,
file_size); // 以文件形式写入映射表
put_modbus_ctx(ctx); // 归还上下文
}
free(pNewMap);
return (rc == 1) ? 0 : -1;
}
解析 :函数分三遍扫描:第一遍算大小 → 第二遍拷贝展平 → 第三遍发送 。"为保险起见对每个上下文都发送一次"------因为不同接口(COM1、COM2)可能连接到同一台中控的不同通道,全量下发保证每个通道的中控副本都是最新的。
modbus_write_file是对标准 Modbus "写文件记录"功能码的封装,把整块二进制映射表以文件名"reg_map"写入中控。
六、删除点
6.1 前台:rpc_remove_point
删除只需要传 point 编号,用数组型 params(单个整数):
c
/* 删除指定点,返回 0 表示成功,-1 失败 */
int rpc_remove_point(int iSocketClient, int point)
{
char buf[300];
int iLen;
/* params 为位置数组,只有一个元素:point 编号 */
sprintf(buf,
"{\"method\": \"remove_point\","
"\"params\": [%d], \"id\": \"2\" }",
point);
iLen = send(iSocketClient, buf, strlen(buf), 0);
if (iLen == strlen(buf))
{
while (1)
{
iLen = recv(iSocketClient, buf, sizeof(buf), 0);
buf[iLen] = '\0';
if (iLen == 1 && (buf[0] == '\r' || buf[0] == '\n'))
continue;
else
break;
}
if (iLen > 0)
{
cJSON *root = cJSON_Parse(buf);
cJSON *result = cJSON_GetObjectItem(root, "result");
if (result)
{
cJSON_Delete(root);
return result->valueint; // 0 = 成功
}
else
{
cJSON_Delete(root);
return -1;
}
}
else
{
DEBUG_PRINTF("read rpc reply err : %d\n", iLen);
return -1;
}
}
else
{
DEBUG_PRINTF("send rpc request err : %d, %s\n", iLen, strerror(errno));
return -1;
}
}
6.2 后台:server_remove_point
cpp
bool IndustrialControlRpc::server_remove_point(const Json::Value& root, Json::Value& response)
{
Json::Value params = root["params"];
int point, i;
response["jsonrpc"] = "2.0";
response["id"] = root["id"];
if (params == Json::Value::null)
{
response["result"] = -1;
return true;
}
point = params[0u].asInt(); // 从位置数组取第 0 个元素
/* 遍历数组,按 point 编号查找对应槽 */
for (i = 0; i < MAX_POINTS; i++)
{
if (g_PointInfos[i].point == point)
break;
}
if (i == MAX_POINTS) // 未找到
{
response["result"] = -1;
return true;
}
printf("remove point %d: %s\r\n", point, g_PointInfos[i].port_info);
/* 删除:把 i+1 之后的所有元素向前移一位,填补空位 */
for (; i < MAX_POINTS; i++)
g_PointInfos[i] = g_PointInfos[i + 1];
response["result"] = 0;
/* 删除后同步三步 */
write_cfg();
create_point_maps();
modbus_write_point_maps();
return true;
}
解析 :删除采用前移覆盖 :找到目标槽后,把它后面所有元素依次向前移一格,末尾自然变成上一个元素的副本,但由于哨兵位(
dev_addr == 0)跟着移到了倒数第二位,遍历终止条件不受影响。这个方法简单高效,无需维护"空洞"或链表结构。
七、修改点
修改点的逻辑是"先按 point 找槽,再原地覆盖所有字段":
cpp
bool IndustrialControlRpc::server_modify_point(const Json::Value& root, Json::Value& response)
{
Json::Value params = root["params"];
int point, i;
PPointInfo pInfo;
response["jsonrpc"] = "2.0";
response["id"] = root["id"];
if (params == Json::Value::null)
{
response["result"] = -1;
return true;
}
point = params["point"].asInt(); // 修改用命名参数,需要先取出 point 字段
/* 按 point 编号定位槽 */
for (i = 0; i < MAX_POINTS; i++)
{
if (g_PointInfos[i].point == point)
break;
}
if (i == MAX_POINTS)
{
response["result"] = -1;
return true;
}
pInfo = &g_PointInfos[i];
printf("modify point %d: %s => %s\r\n",
point,
g_PointInfos[i].port_info,
params["port_info"].asString().c_str());
/* 原地覆盖所有字段,port_info 顺便转小写 */
strncpy_s(pInfo->port_info, params["port_info"].asString().c_str(), sizeof(pInfo->port_info) - 1);
if (pInfo->port_info[0] == 'C') pInfo->port_info[0] = 'c';
if (pInfo->port_info[1] == 'O') pInfo->port_info[1] = 'o';
if (pInfo->port_info[2] == 'M') pInfo->port_info[2] = 'm';
pInfo->channel = params["channel"].asInt();
pInfo->dev_addr = params["dev_addr"].asInt();
pInfo->reg_addr = params["reg_addr"].asInt();
strncpy_s(pInfo->reg_type, params["reg_type"].asString().c_str(), sizeof(pInfo->reg_type) - 1);
pInfo->period = params["period"].asInt();
// 注意:不修改 pInfo->point,点编号不变
response["result"] = 0;
/* 修改后同步三步 */
write_cfg();
create_point_maps();
modbus_write_point_maps();
return true;
}
解析 :修改时
pInfo->point保持不变------point 编号是点的"身份证",一旦分配就不应改变,前台持有的句柄才不会失效。修改只改变点的物理属性(端口、通道、寄存器地址等),编号不动。
八、总结
| 操作 | params 类型 | 前台传参 | 后台内存操作 | 末尾三步 |
|---|---|---|---|---|
| 增加点 | 命名对象 | 所有字段 | local_add_point:找空槽,编号 = 最大值+1 |
✓ |
| 删除点 | 位置数组 | point 编号 | 前移覆盖:g_PointInfos[i] = g_PointInfos[i+1] |
✓ |
| 修改点 | 命名对象 | point + 新字段 | 原地覆盖,point 字段不变 | ✓ |
末尾三步(每次操作后均执行):
write_cfg()--- 持久化,防重启丢失create_point_maps()--- 重建寄存器映射表modbus_write_point_maps()--- 下发映射到中控 H5
九、结尾
本篇完成了增删改点的完整情景分析:前台构造 JSON-RPC 请求,后台更新内存数组,再经由"写配置 → 建映射 → 下发中控"三步将变更同步到持久层和硬件层。
下一篇继续情景分析,学习读写点 与 OTA 升级的实现流程,敬请期待~
Hello_Embed 继续带你从原理到实践,掌握嵌入式上位机开发的核心技能,敬请关注~