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)
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 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; }
|
||||
}
|
||||
Reference in New Issue
Block a user