【无标题】

目录


一、前言

大家好,这里是 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 字段不变

末尾三步(每次操作后均执行):

  1. write_cfg() --- 持久化,防重启丢失
  2. create_point_maps() --- 重建寄存器映射表
  3. modbus_write_point_maps() --- 下发映射到中控 H5

九、结尾

本篇完成了增删改点的完整情景分析:前台构造 JSON-RPC 请求,后台更新内存数组,再经由"写配置 → 建映射 → 下发中控"三步将变更同步到持久层和硬件层。

下一篇继续情景分析,学习读写点OTA 升级的实现流程,敬请期待~

Hello_Embed 继续带你从原理到实践,掌握嵌入式上位机开发的核心技能,敬请关注~

相关推荐
小陈phd1 小时前
多模态大模型学习笔记(三十八)——传统OCR技术机制:从DBNet到CRNN:吃透传统OCR两阶段范式的底层逻辑
笔记·学习·ocr
路溪非溪2 小时前
详解下DNS协议
网络·网络协议·tcp/ip·智能路由器
CHANG_THE_WORLD2 小时前
<Fluent Python > 2. 第二章:序列的数组
网络·windows·python
byoass2 小时前
企业云盘API集成指南:如何与CI/CD流水线打通
网络·安全·ci/cd·云计算
zhangrelay2 小时前
三分钟云课实践速通--工程制图基础-3D--FreeCAD
笔记·学习·3d
大卡片2 小时前
TCP、IP和TFTP协议
服务器·网络·tcp/ip
汽车仪器仪表相关领域2 小时前
Kvaser Memorator Professional HS/LS:高速 + 低速双通道 CAN 总线记录仪,跨系统诊断的专业级解决方案
网络·人工智能·功能测试·测试工具·安全·压力测试
kobesdu2 小时前
【ROS2实战笔记-13】Foxglove Studio:ROS可视化工具的另一条路
笔记·机器人·自动驾驶·ros
用户815577828212 小时前
连上WiFi 却打不开网页?一套常用命令帮你快速定位问题
网络协议