6c76007249
【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)
242 lines
10 KiB
C#
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; }
|
|
}
|