Files
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

242 lines
10 KiB
C#

// ═══════════════════════════════════════════════════════════════
// Mc4Probe — 临时探针工具
//
// 用途:连接 192.168.3.92 MC4 设备,抓取完整对象树 + 每个设备的点表 + 实时点值
// 用于分析 MC4 实际数据结构,规划 web.vite/warehouse 的显示方案
//
// 用法:cd tools\mc4_probe\Mc4Probe && dotnet run -- <ip> <user> <pwd>
// 例如:dotnet run -- 192.168.3.92 admin admin
// ═══════════════════════════════════════════════════════════════
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Mc4Probe;
internal class Program
{
// 大小写不敏感的 JSON 配置
private static readonly JsonSerializerOptions JsonOpts = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
private static readonly StringContent EmptyJsonBody
= new("{}", Encoding.UTF8, "application/json");
private static string _token = "";
static async Task<int> Main(string[] args)
{
var ip = args.Length > 0 ? args[0] : "192.168.3.92";
var user = args.Length > 1 ? args[1] : "admin";
var pwd = args.Length > 2 ? args[2] : "admin";
var baseUrl = $"http://{ip}:3000";
Console.WriteLine($"[Probe] 目标: {baseUrl}, 账号: {user}");
Console.WriteLine(new string('=', 80));
using var http = new HttpClient { BaseAddress = new Uri(baseUrl) };
http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
try
{
// 1. 登录拿 Token
await LoginAsync(http, user, pwd);
// 2. 拉对象树
var tree = await GetObjectTreeAsync(http);
Console.WriteLine($"\n[Probe] 对象树根节点数: {tree.Count}");
foreach (var n in tree) PrintNode(n, 0);
// 3. 收集所有设备节点(type=2)
var devices = new List<Mc4TreeNode>();
CollectDevices(tree, devices);
Console.WriteLine($"\n[Probe] 共发现 {devices.Count} 台设备,开始抓点表+实时值...");
// 4. 对每台设备抓点表 + 实时值
var report = new List<DeviceReport>();
foreach (var dev in devices)
{
var r = new DeviceReport
{
DeviceId = dev.Id,
DeviceName = dev.Name ?? $"设备{dev.Id}",
ObjectType = dev.ObjectType,
Tag = dev.Tag,
Option = dev.Option
};
try
{
r.Points = await GetDevicePointsAsync(http, dev.Id);
r.Realtime = await GetRealtimeValuesAsync(http, dev.Id);
}
catch (Exception ex)
{
r.Error = ex.Message;
}
report.Add(r);
}
// 5. 输出 JSON 报告
var json = JsonSerializer.Serialize(report, JsonOpts);
var outFile = $"mc4_report_{ip.Replace(".", "_")}_{DateTime.Now:yyyyMMdd_HHmmss}.json";
await File.WriteAllTextAsync(outFile, json);
Console.WriteLine($"\n[Probe] 报告已写入: {outFile} ({new FileInfo(outFile).Length} 字节)");
// 6. 控制台摘要
Console.WriteLine("\n" + new string('=', 80));
Console.WriteLine("设备摘要:");
foreach (var r in report)
{
var ptCnt = r.Points?.Count ?? 0;
var rtCnt = r.Realtime?.Count ?? 0;
var onlineIdx = r.Realtime?.FirstOrDefault(v => v.Index == 1)?.Value;
Console.WriteLine($" [{r.DeviceId}] {r.DeviceName,-20} ObjectType={r.ObjectType,-5} 点表={ptCnt} 实时值={rtCnt} 在线点(index=1)={onlineIdx?.ToString() ?? "N/A"}");
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"\n[Probe] 错误: {ex.Message}");
return 1;
}
return 0;
}
private static async Task LoginAsync(HttpClient http, string user, string pwd)
{
// 1) 查询是否需要 MD5
var confResp = await http.PostAsync("/api/central/auth/conf/get", EmptyJsonBody);
confResp.EnsureSuccessStatusCode();
var confJson = await confResp.Content.ReadAsStringAsync();
var conf = JsonSerializer.Deserialize<JsonElement>(confJson, JsonOpts);
var needMd5 = conf.GetProperty("data").GetProperty("encrypt").GetBoolean();
var pwdFinal = needMd5 ? ComputeMd5(pwd) : pwd;
Console.WriteLine($"[Probe] 加密配置: encrypt={needMd5}, MD5={pwdFinal}");
// 2) 登录
var loginBody = JsonSerializer.Serialize(new { account = user, password = pwdFinal }, JsonOpts);
var resp = await http.PostAsync("/api/central/auth/login", new StringContent(loginBody, Encoding.UTF8, "application/json"));
var rawJson = await resp.Content.ReadAsStringAsync();
Console.WriteLine($"[Probe] 登录响应({(int)resp.StatusCode}): {rawJson}");
resp.EnsureSuccessStatusCode();
var doc = JsonDocument.Parse(rawJson, new JsonDocumentOptions { AllowTrailingCommas = true });
if (!doc.RootElement.TryGetProperty("data", out var dataElem) || dataElem.ValueKind == JsonValueKind.Null)
throw new Exception($"登录响应 data 为空 (HTTP {(int)resp.StatusCode}): {rawJson}");
_token = dataElem.GetProperty("token").GetString()
?? throw new Exception($"登录响应 data.token 为空: {rawJson}");
http.DefaultRequestHeaders.Remove("token");
http.DefaultRequestHeaders.Add("token", _token);
Console.WriteLine($"[Probe] 登录成功, Token={_token[..Math.Min(8, _token.Length)]}...");
}
private static async Task<List<Mc4TreeNode>> GetObjectTreeAsync(HttpClient http)
{
var resp = await http.PostAsync("/api/central/object/tree", EmptyJsonBody);
resp.EnsureSuccessStatusCode();
var json = await resp.Content.ReadAsStringAsync();
var doc = JsonDocument.Parse(json);
var data = doc.RootElement.GetProperty("data");
return JsonSerializer.Deserialize<List<Mc4TreeNode>>(data.GetRawText(), JsonOpts) ?? new();
}
private static async Task<List<Mc4PointDef>> GetDevicePointsAsync(HttpClient http, int deviceId)
{
var body = JsonSerializer.Serialize(new { id = deviceId }, JsonOpts);
var resp = await http.PostAsync("/api/central/device/point/get", new StringContent(body, Encoding.UTF8, "application/json"));
resp.EnsureSuccessStatusCode();
var json = await resp.Content.ReadAsStringAsync();
var doc = JsonDocument.Parse(json);
if (!doc.RootElement.TryGetProperty("data", out var data)) return new();
return JsonSerializer.Deserialize<List<Mc4PointDef>>(data.GetRawText(), JsonOpts) ?? new();
}
private static async Task<List<Mc4PointValue>> GetRealtimeValuesAsync(HttpClient http, int deviceId)
{
var body = JsonSerializer.Serialize(new { id = deviceId }, JsonOpts);
var resp = await http.PostAsync("/api/central/device/point/value/get", new StringContent(body, Encoding.UTF8, "application/json"));
resp.EnsureSuccessStatusCode();
var json = await resp.Content.ReadAsStringAsync();
var doc = JsonDocument.Parse(json);
if (!doc.RootElement.TryGetProperty("data", out var data)) return new();
return JsonSerializer.Deserialize<List<Mc4PointValue>>(data.GetRawText(), JsonOpts) ?? new();
}
private static void CollectDevices(List<Mc4TreeNode> nodes, List<Mc4TreeNode> bag)
{
foreach (var n in nodes)
{
if (n.Type == 2) bag.Add(n);
if (n.Children != null && n.Children.Count > 0) CollectDevices(n.Children, bag);
}
}
private static void PrintNode(Mc4TreeNode n, int depth)
{
var indent = new string(' ', depth * 2);
var typeStr = n.Type == 1 ? "[区域]" : "[设备]";
var otStr = n.ObjectType != 0 ? $" ObjectType={n.ObjectType}" : "";
var tagStr = !string.IsNullOrEmpty(n.Tag) ? $" Tag={n.Tag}" : "";
Console.WriteLine($"{indent}{typeStr} id={n.Id} {n.Name}{otStr}{tagStr}");
if (n.Children != null)
foreach (var c in n.Children) PrintNode(c, depth + 1);
}
private static string ComputeMd5(string input)
{
var bytes = System.Security.Cryptography.MD5.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
}
// ═══════════════════════════════════════════
// 数据模型
// ═══════════════════════════════════════════
public class Mc4TreeNode
{
public int Id { get; set; }
public string? Name { get; set; }
public int Type { get; set; }
public int ObjectType { get; set; }
public string? Tag { get; set; }
public Dictionary<string, object?>? Option { get; set; }
public List<Mc4TreeNode>? Children { get; set; }
}
public class Mc4PointDef
{
public int Index { get; set; }
public int Type { get; set; }
public string? Tag { get; set; }
public string? Name { get; set; }
public string? Desc { get; set; }
public string? Unit { get; set; }
public Dictionary<string, object?>? Option { get; set; }
}
public class Mc4PointValue
{
public int Id { get; set; }
public int Index { get; set; }
public double Value { get; set; }
public string? Time { get; set; }
public int Interval { get; set; }
}
public class DeviceReport
{
public int DeviceId { get; set; }
public string DeviceName { get; set; } = "";
public int ObjectType { get; set; }
public string? Tag { get; set; }
public Dictionary<string, object?>? Option { get; set; }
public List<Mc4PointDef>? Points { get; set; }
public List<Mc4PointValue>? Realtime { get; set; }
public string? Error { get; set; }
}