Files
SecMPS/doc/整合方案/IoT设备实时点值显示与控制实施方案_v1.0.md
T
g82tt 6c76007249 V1.10+V1.10.1+V1.11: MC4 JSON反序列化大小写+IoT设备实时点值显示+空调控制
【V1.10 MC4 适配器 JSON 反序列化大小写修复】
- Mc4AuthHelper.cs: 新增 JsonOpts(PropertyNameCaseInsensitive+CamelCase) + EmptyJsonBody
- Mc4Adapter.cs: 新增统一 DeserializeData<T> 处理 {code,msg,data} 包装
- 所有 MC4 业务接口改用 DeserializeData<T> 解决 List 不能直接反序列化包装结构
- 解决 POST 无参 body=null 触发 400 的问题

【V1.10.1 GetObjectTreeAsync 修复】
- Mc4Adapter.cs: GetObjectTreeAsync 改用 EmptyJsonBody + DeserializeData<List<Mc4TreeNode>>
- 修复 A3 同步时报 400 和 List 反序列化错误

【V1.11 IoT 设备实时点值 + 空调控制】
- Mc4Adapter.cs: GetRealtimeValuesAsync 改用 DeserializeData(V1.10 漏改)
- web.vite/base_device.vue: fetchRealtime 增加 normalizePoint 字段映射(PascalCase→camelCase)
- web.vite/RealtimeDataPanel.vue: 组件层同样字段映射
- warehouse/DeviceInfo.vue: 改用 /api/base_device/getPageData 新接口
  + 按 DeviceCategory 分类展示(温度探头/湿度探头单卡片+空调控制器双卡片)
  + 30秒轮询实时点值(仅IoT设备)
  + 空调控制按钮 sourceDeviceId→deviceId 字段名修复
- tools/mc4_probe: 新增 MC4 设备探针工具(连接192.168.3.92抓取设备+点表+实时值)
- doc/整合方案/IoT设备实时点值显示与控制实施方案_v1.0.md: 详细方案文档
  + 设备三分类+点位映射表(温度探头/湿度探头index=2;空调index=5=湿度/6=温度/2-3-4=控制)
- 说明文档.md: 进度记录 V1.10/V1.10.1/V1.11 三条更新

【关键修复】
1. MC4 返回 JSON 字段全小写 → C# Model 大小写不匹配 (V1.10)
2. List 不能反序列化 {code,msg,data:[...]} 包装 (V1.10)
3. POST 无参 body=null 触发 400 (V1.10)
4. A3 同步 GetObjectTreeAsync 报 400+反序列化错误 (V1.10.1)
5. GetRealtimeValuesAsync 漏改 (V1.11)
6. 管理端实时数据弹窗 PascalCase 字段→camelCase 列名不匹配 (V1.11)
7. 仓库空调控制 sourceDeviceId 字段名与后端 ControlRequest.DeviceId 不匹配 (V1.11)
2026-07-24 05:08:37 +08:00

17 KiB
Raw Blame History

IoT 设备实时点值显示 + 设备控制实施方案

编写时间: 2026-07-24 编写人: 猫娘工程师 幽浮喵 适用版本: SecMPS 仓库大屏 (warehouse) + 管理端 (web.vite) + IntegrationGateway 目标: 解决 MC4 IoT 设备(温度探头/湿度探头/空调控制器)在管理端和大屏两个前端的实时点值显示与控制问题


一、问题背景

1.1 当前痛点

  1. 管理端base_device 列表中点击 IoT 设备的"实时数据"按钮,弹窗表格无任何数据显示(之前现场反馈过)
  2. 大屏端warehouse 地图点击 IoT 设备标签,实时点值不显示,空调控制器无法下发制冷/制热/关机命令
  3. 数据正确性:MC4 设备点位序号 1 是自动添加的"在线点",真实数据从序号 2 开始;空调控制器有多个有效点位(5=湿度、6=温度、2=制冷发射、3=制热发射、4=关机发射)

1.2 根因汇总

# 根因 文件
Mc4Adapter.GetRealtimeValuesAsyncJsonSerializer.Deserialize<List<Mc4PointValue>>(json)! 反序列化实时点值响应,没有传 JsonOptsMC4 返回的 {code, msg, data:[...]} 包装结构无法被 List 直接反序列化 IntegrationGateway.Adapters.MC4/Mc4Adapter.cs:154
base_device.vuefetchRealtime 直接 realtimeValues.value = await r.json()未做字段名映射(网关返回 PascalCasePointIndex/Value/UpdateTime/Interval,前端 table 列名是 camelCasepointIndex/value/updateTime/interval web.vite/src/views/warehouse/device_manager/base_device.vue:164-170
RealtimeDataPanel.vue 同样的问题,values.value = await r.json() 没做字段映射 web.vite/src/views/warehouse/device_manager/base_device/components/RealtimeDataPanel.vue:25
仓库 DeviceInfo.vue 调用的后端接口是老的(之前基于 warehouse_devicepoint 表),未切换到新的 /api/base_device/getPageData 接口 warehouse/src/view/DeviceInfo.vue
仓库空调控制按钮的下发参数 sourceDeviceId 与后端 ControlRequest 期望的 deviceId 字段名不一致 warehouse/src/view/DeviceInfo.vue:618
MC4 设备点位索引语义仅在主人本地知道(index=2 是真实数据/探头,空调 index=5=湿度/6=温度/2=制冷/3=制热/4=关机),未在代码/文档中固化,新人接手成本高 全局

二、设备分类与点位映射表

2.1 IoT 设备三分类

设备分类 DeviceCategory 字典值 真实数据点位 控制点位
温度探头 温度探头 index=2(温度值)
湿度探头 湿度探头 index=2(湿度值)
空调控制器 空调控制器 index=5(湿度)、index=6(温度) index=2(制冷发射)、index=3(制热发射)、index=4(关机发射)

说明:MC4 平台固定在每个设备的 index=1 位置自动添加"在线点"(bool 类型,1=在线),从 index=2 开始才是业务点位。

2.2 显示策略

设备类型 单卡片 双卡片 控制按钮
温度探头 1 个大数字(℃)
湿度探头 1 个大数字(%RH
空调控制器 温度(℃)+ 湿度(%RH 制冷/制热/关机

三、网关 B 组接口(已就绪)

3.1 实时点位值查询

  • 路径GET /api/gateway/realtime/{adapter}/{deviceId}
  • 入参adapter=MC4:33kudeviceId=1928MC4 设备 id 字符串)
  • 出参List<PointValue>PascalCase
    [
      {"sourceDeviceId":"1928","pointIndex":1,"value":1,"updateTime":"2026-07-24T10:00:00","interval":10},
      {"sourceDeviceId":"1928","pointIndex":2,"value":25.6,"updateTime":"2026-07-24T10:00:00","interval":10}
    ]
    
  • 实现位置IntegrationGateway.Host/Program.cs:302-307 路由 + Mc4Adapter.cs:145-164 调用

3.2 设备控制

  • 路径POST /api/gateway/realtime/{adapter}/control
  • 入参JSON Body):
    {
      "deviceId": "1928",      // 设备 sourceId(注意:是 deviceId 不是 sourceDeviceId
      "pointIndex": 2,         // 控制点索引
      "value": 1               // 1=发射
    }
    
  • 出参200 OK
  • 实现位置IntegrationGateway.Host/Program.cs:329-335 路由 + Mc4Adapter.cs:167-175 SetPointValueAsync

3.3 MC4 适配器 GetRealtimeValuesAsync 修复(V1.10.1

改动文件IntegrationGateway.Adapters.MC4/Mc4Adapter.cs:154

修改前

var values = JsonSerializer.Deserialize<List<Mc4PointValue>>(json)!;

修改后

// MC4 响应是 {code, msg, data:[...]} 包装结构,data 段才是真正的点位列表
var values = DeserializeData<List<Mc4PointValue>>(json, "/api/central/device/point/value/get", new List<Mc4PointValue>());

DeserializeData<T>Mc4Adapter.cs:49-56 定义,使用 JsonOptsPropertyNameCaseInsensitive=true + PropertyNamingPolicy=CamelCase)处理响应解析。


四、web.vite 管理端(已修复)

4.1 base_device.vue 实时数据弹窗

文件web.vite/src/views/warehouse/device_manager/base_device.vue

改动点fetchRealtime 函数,line 163-179):

// 网关 B4 返回 List<PointValue>,字段 PascalCaseSourceDeviceId, PointIndex, Value, UpdateTime, Interval
// 前端 table 字段是 camelCase,做一层字段映射
const normalizePoint = (v) => ({
    pointIndex: v.PointIndex ?? v.pointIndex ?? v.index ?? 0,
    value: v.Value ?? v.value ?? 0,
    updateTime: v.UpdateTime ?? v.updateTime ?? v.Time ?? v.time ?? '',
    interval: v.Interval ?? v.interval ?? 0
});
const fetchRealtime = async () => {
    if (!curDev.value) return; realtimeLoading.value = true;
    try {
        const r = await fetch(`${GW}/api/gateway/realtime/${(curDev.value.AdapterCode || curDev.value.adapterCode)}/${curDev.value.SourceId || curDev.value.sourceId}`);
        const list = await r.json();
        realtimeValues.value = Array.isArray(list) ? list.map(normalizePoint) : [];
    } catch {} finally { realtimeLoading.value = false }
}

核心修复:增加 normalizePoint 字段映射函数 + 数组类型判断(防止后端返回 404/500 时 await r.json() 解析失败)。

4.2 RealtimeDataPanel.vue 组件

文件web.vite/src/views/warehouse/device_manager/base_device/components/RealtimeDataPanel.vue

改动点:与 base_device.vue 完全一致的 normalizePoint 字段映射逻辑(line 19-26、line 35-37)。

4.3 IoT 设备操作列

文件web.vite/src/views/warehouse/device_manager/base_device.vueline 222-237

IoT 设备操作列已有 2 个按钮:

  • "实时数据":打开 RealtimeDataPanel 弹窗
  • "控制":打开 DeviceControlPanel 弹窗(通用点位控制)

五、warehouse 大屏端(已修复)

5.1 DeviceInfo.vue 接入新接口

文件warehouse/src/view/DeviceInfo.vue

改动点

  1. script 改用 /api/base_device/getPageData 接口line 524-582
  2. 优先用 MapModelId 查(地图点击场景),其次用 DeviceId
  3. 根据 DeviceCategory 字段判断设备分类:温度探头/湿度探头/空调控制器

5.2 实时数据展示(按分类)

文件warehouse/src/view/DeviceInfo.vue

设备分类 展示模板 关键计算属性
温度探头 <div class="single-value-card"> + 大数字 + ℃ primaryPointValueindex=2
湿度探头 <div class="single-value-card"> + 大数字 + %RH primaryPointValueindex=2
空调控制器 <div class="dual-value-card"> + 温度+湿度双卡片 acTempValueindex=6acHumValueindex=5

计算属性定义line 478-510):

const primaryPointValue = computed(() => {
  const p = realtimePoints.value.find(v => v.index === 2);
  return p ? p.value.toFixed(1) : '--';
});

const acTempValue = computed(() => {
  const p = realtimePoints.value.find(v => v.index === 6);
  return p ? p.value.toFixed(1) : '--';
});

const acHumValue = computed(() => {
  const p = realtimePoints.value.find(v => v.index === 5);
  return p ? p.value.toFixed(1) : '--';
});

5.3 空调控制

文件warehouse/src/view/DeviceInfo.vueline 607-631

const handleAirControl = async (mode: 'cool' | 'heat' | 'off') => {
  if (!deviceInfo.value.adapterCode || !deviceInfo.value.sourceId) {
    ElMessage.error('设备信息不完整,无法控制');
    return;
  }
  const pointIndex = mode === 'cool' ? 2 : mode === 'heat' ? 3 : 4;
  controlLoading.value = mode;
  try {
    await gwPost(`/api/gateway/realtime/${deviceInfo.value.adapterCode}/control`, {
      deviceId: deviceInfo.value.sourceId,  // ← 关键修复:原来是 sourceDeviceId,与后端 ControlRequest.DeviceId 不匹配
      pointIndex,
      value: 1  // 1 = 发射
    });
    ElMessage.success(`已下发${mode === 'cool' ? '制冷' : mode === 'heat' ? '制热' : '关机'}命令`);
    // 3 秒后刷新一次实时值
    setTimeout(fetchRealtimePoints, 3000);
  } catch (err: any) {
    console.error('控制失败:', err);
    ElMessage.error(`控制失败: ${err?.message || err}`);
  } finally {
    controlLoading.value = '';
  }
};

控制按钮 UItemplate line 115-125):

<el-tab-pane v-if="isAirConditionerController" label="设备控制">
  <div class="tab-content">
    <div class="control-buttons">
      <el-button type="primary" :loading="controlLoading === 'cool'" @click="handleAirControl('cool')">制冷</el-button>
      <el-button type="warning" :loading="controlLoading === 'heat'" @click="handleAirControl('heat')">制热</el-button>
      <el-button type="danger" :loading="controlLoading === 'off'" @click="handleAirControl('off')">关机</el-button>
    </div>
    <div class="control-tip">通过网关下发到 MC4 设备的 index=2/3/4 控制点</div>
  </div>
</el-tab-pane>

5.4 30 秒轮询实时点值

文件warehouse/src/view/DeviceInfo.vueline 647-655

onMounted(() => {
  setInterval(() => {
    if (isIotDevice.value && deviceInfo.value.sourceId && deviceInfo.value.adapterCode) {
      fetchRealtimePoints();
    }
  }, 30000);
});

仅 IoT 设备开启轮询(30 秒一次),避免不必要的网络请求

5.5 实时点值解析(兼容多种命名风格)

文件warehouse/src/view/DeviceInfo.vueline 584-611

const fetchRealtimePoints = async () => {
  try {
    const adapter = deviceInfo.value.adapterCode; // 例如 MC4:33ku
    const devId = deviceInfo.value.sourceId;      // MC4 设备 id(字符串)
    const data: any = await gwGet(`/api/gateway/realtime/${adapter}/${devId}`);

    // 网关 B4 返回 List<PointValue>,字段:SourceDeviceId, PointIndex, Value, UpdateTime, Interval
    // 同时兼容历史字段(items 包装 / 全小写 / 驼峰)
    const list: any[] = Array.isArray(data)
      ? data
      : (data?.items || data?.data?.items || data?.data || []);
    realtimePoints.value = list.map((v: any) => {
      const updateTime = v.updateTime || v.UpdateTime || v.Time || v.time;
      return {
        index: Number(v.pointIndex ?? v.PointIndex ?? v.index ?? v.Index ?? 0),
        value: Number(v.value ?? v.Value ?? 0),
        name: v.name || v.Name,
        unit: v.unit || v.Unit,
        updateTime: updateTime ? new Date(updateTime).toLocaleString('zh-CN') : ''
      };
    });
    console.log(`[DeviceInfo] 实时点值 ${realtimePoints.value.length} 条`, realtimePoints.value);
  } catch (err: any) {
    console.warn('获取实时点值失败:', err);
    realtimePoints.value = [];
  }
};

六、调试探针(可选工具)

6.1 目的

连真实 MC4 设备(192.168.3.92)抓取设备和点位数据,验证点位索引偏移假设。

6.2 工具位置

tools/mc4_probe/Mc4Probe/Program.cs(独立 .NET 控制台程序)

6.3 用法

cd d:\Code\SecMPS\tools\mc4_probe\Mc4Probe
dotnet run -- --base http://192.168.3.92:3000 --user admin --pwd admin

6.4 输出示例

[MC4] 登录成功, token=eyJhbGciOiJIUzI1NiJ9...
[MC4] 对象树节点数: 12
[MC4] 设备 #1: 温度探头-01, id=1928, type=1
  点表: index=1 在线点(bool), index=2 温度(℃)
  实时: [在线=1, 温度=25.6℃]
[MC4] 设备 #2: 湿度探头-01, id=1929, type=1
  点表: index=1 在线点(bool), index=2 湿度(%RH)
  实时: [在线=1, 湿度=58.2%]
[MC4] 设备 #3: 空调控制器-01, id=1930, type=1
  点表: index=1 在线点(bool), index=2 制冷发射, index=3 制热发射, index=4 关机发射, index=5 湿度, index=6 温度
  实时: [在线=1, 制冷=0, 制热=0, 关机=0, 湿度=60.0, 温度=22.5]

七、验证清单

7.1 后端网关验证

  • dotnet build gateway — 0 错误 0 警告
  • GetRealtimeValuesAsync 改用 DeserializeData<T>,处理 {code, msg, data:[...]} 包装
  • JsonOpts 大小写不敏感,能正确反序列化 MC4 全小写字段

7.2 web.vite 管理端验证

  • npm run build web.vite — 0 错误 0 警告
  • base_device 列表 → 选 IoT 设备 → 点"实时数据" → 弹窗表格有 4 列数据(点位/当前值/更新时间/采集间隔)
  • 点"控制" → 弹窗可输入点位索引 + 目标值 → 点"发送指令" → 设备响应

7.3 warehouse 大屏端验证

  • npm run build warehouse — 0 错误 0 警告
  • 地图 → 找温度探头 → 点标签 → 设备详情弹窗 → 实时数据选项卡显示温度大数字
  • 地图 → 找空调控制器 → 点标签 → 设备详情弹窗 → 实时数据显示温度+湿度双卡片
  • 设备控制选项卡 → 点"制冷"按钮 → 3 秒后实时数据刷新(验证控制生效)
  • 30 秒后实时数据自动刷新(验证轮询工作)

7.4 端到端联调验证

  • 主人用 192.168.3.92 实机测试:温度探头/湿度探头/空调控制器三分类全部正常
  • 空调制冷/制热/关机三个命令都能成功下发并反映到实时数据

八、关键文件改动清单

# 文件 改动 验证
1 gateway/src/IntegrationGateway.Adapters.MC4/Mc4Adapter.cs GetRealtimeValuesAsync 改用 DeserializeData dotnet build ✓
2 gateway/src/IntegrationGateway.Adapters.MC4/Mc4AuthHelper.cs 新增 JsonOpts + EmptyJsonBodyV1.10 前序修复) dotnet build ✓
3 web.vite/src/views/warehouse/device_manager/base_device.vue fetchRealtime 增加 normalizePoint 字段映射 需 build
4 web.vite/src/views/warehouse/device_manager/base_device/components/RealtimeDataPanel.vue 同样增加 normalizePoint 字段映射 需 build
5 warehouse/src/view/DeviceInfo.vue 改用 /api/base_device/getPageData + 分类展示 + 30s 轮询 + 空调控制 需 build
6 tools/mc4_probe/Mc4Probe/Program.cs 新增(调试探针,独立工具) 独立运行

九、风险与回滚

9.1 风险

风险 等级 缓解措施
MC4 设备实际点位索引与主人预期不一致 部署后用 mc4_probe 工具先验证
空调命令下发后实时数据刷新延迟 已设置 3 秒后强制刷新 + 30 秒轮询兜底
设备控制误操作 控制按钮加 loading 状态 + 弹 ElMessage 提示

9.2 回滚方案

  • 仓库 DeviceInfo.vue 改动可通过 git revert 回滚到上一版本
  • web.vite base_device.vue / RealtimeDataPanel.vue 改动可通过 git revert 回滚
  • 网关 Mc4Adapter.cs 改动只影响 MC4 设备,对 Owl/KMS 适配器完全无影响回滚风险极低

十、后续优化建议(非本次范围)

  1. 点位配置表:在 base_device 表加 pointIndexConfig 字段(JSON),让管理员能自定义每个设备的"哪个 index 是什么含义",避免硬编码在代码里
  2. 告警阈值:在 base_device 加 minThreshold/maxThreshold 字段,gateway 实时轮询时自动比对,超出阈值推送 iot_alarm
  3. 历史曲线:仓库大屏的温度/湿度曲线当前已用 SVG 简单实现,可改为 ECharts 实时刷新(30 秒一次)
  4. 设备控制日志:所有通过 /api/gateway/realtime/{adapter}/control 下发的命令记录到新表 iot_command_log(含下发人、时间、命令、响应)
  5. 批量控制:空调控制器分组管理时,支持"批量制冷"操作(一组设备同时下发 index=2 发射命令)

附录 A:网关 B4 响应示例(修复后)

请求

GET http://192.168.3.108:5100/api/gateway/realtime/MC4:33ku/1928

响应HTTP 200application/json):

[
  {"sourceDeviceId":"1928","pointIndex":1,"value":1,"updateTime":"2026-07-24T10:00:00","interval":10},
  {"sourceDeviceId":"1928","pointIndex":2,"value":25.6,"updateTime":"2026-07-24T10:00:00","interval":10}
]

附录 B:网关 B5 请求示例

请求

POST http://192.168.3.108:5100/api/gateway/realtime/MC4:33ku/control
Content-Type: application/json

{
  "deviceId": "1930",
  "pointIndex": 2,
  "value": 1
}

响应HTTP 200application/json):

true

或空 bodyminimal API Results.Ok())。


维护说明: 本文档作为 V1.10 IoT 设备实时点值 + 控制功能的实施记录,与 说明文档.md 的"进度记录"段保持同步更新。