// ═══════════════════════════════════════════════════════════════ // Mc4Probe — 临时探针工具 // // 用途:连接 192.168.3.92 MC4 设备,抓取完整对象树 + 每个设备的点表 + 实时点值 // 用于分析 MC4 实际数据结构,规划 web.vite/warehouse 的显示方案 // // 用法:cd tools\mc4_probe\Mc4Probe && dotnet run -- // 例如: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 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(); CollectDevices(tree, devices); Console.WriteLine($"\n[Probe] 共发现 {devices.Count} 台设备,开始抓点表+实时值..."); // 4. 对每台设备抓点表 + 实时值 var report = new List(); 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(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> 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>(data.GetRawText(), JsonOpts) ?? new(); } private static async Task> 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>(data.GetRawText(), JsonOpts) ?? new(); } private static async Task> 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>(data.GetRawText(), JsonOpts) ?? new(); } private static void CollectDevices(List nodes, List 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? Option { get; set; } public List? 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? 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? Option { get; set; } public List? Points { get; set; } public List? Realtime { get; set; } public string? Error { get; set; } }