啊鑫
2025-07-28 1b3d57f133b494114c1cdd30627c7057eecdf1b1
修复时间戳解析和数据验证逻辑

- 修正时间戳字节顺序解析:按用户提供的解析对照表,先取低字节再取高字节
- 更新配置文件时间戳格式:YYYYMMDDHHMMSS -> YYYYMMDDHHMM (12位格式)
- 增强无效数据检测:特别识别[-7616,1]错误模式和E240xxxx错误码
- 添加详细的数据解析调试输出和验证功能
- 同时修复modbus_config.json和production_config.json两个配置文件
已修改6个文件
327 ■■■■ 文件已修改
UniversalModbusManager.cs 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/DynamicModbusData.cs 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/ModbusDataParser.cs 98 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/modbus_config.json 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/production_config.json 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
原始值.txt 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
UniversalModbusManager.cs
@@ -536,12 +536,27 @@
                    // 读取原始寄存器数据
                    var registers = await Task.Run(() => 
                        _modbusClient.ReadHoldingRegisters(fieldConfig.Address, fieldConfig.Length));
                    // 验证寄存器读取结果的有效性
                    ValidateRegisterData(fieldName, fieldConfig, registers);
                    // 检查读取结果是否为null
                    if (registers == null)
                    {
                        LogWarning($"字段 '{fieldName}' 读取结果为null,地址: {fieldConfig.Address}, 长度: {fieldConfig.Length}");
                        continue;
                    }
                    // 记录原始寄存器数据用于调试
                    LogDebug($"[RAW-DATA] 字段 '{fieldName}' 原始寄存器: [{string.Join(", ", registers)}] (十六进制: [{string.Join(", ", registers.Select(r => $"0x{r:X4}"))}])");
                    // 详细记录每个寄存器的字节分解
                    for (int i = 0; i < registers.Length; i++)
                    {
                        var reg = registers[i];
                        var highByte = (byte)((reg >> 8) & 0xFF);
                        var lowByte = (byte)(reg & 0xFF);
                        LogDebug($"[RAW-DATA] 寄存器{i}: {reg} (0x{reg:X4}) → 高字节:{highByte}('{(char)highByte}') 低字节:{lowByte}('{(char)lowByte}')");
                    }
                    // 验证数据长度
@@ -601,6 +616,98 @@
        #endregion
        #region 数据验证方法
        /// <summary>
        /// 验证寄存器数据的有效性
        /// </summary>
        /// <param name="fieldName">字段名</param>
        /// <param name="fieldConfig">字段配置</param>
        /// <param name="registers">读取的寄存器数据</param>
        private void ValidateRegisterData(string fieldName, DataField fieldConfig, int[]? registers)
        {
            if (registers == null)
            {
                LogWarning($"[VALIDATE] 字段 '{fieldName}' 寄存器数据为null");
                return;
            }
            LogDebug($"[VALIDATE] 字段 '{fieldName}' 原始数据验证:");
            LogDebug($"[VALIDATE] - 地址: {fieldConfig.Address}, 期望长度: {fieldConfig.Length}, 实际长度: {registers.Length}");
            LogDebug($"[VALIDATE] - 数据类型: {fieldConfig.DataType}");
            // 检查数据长度
            if (registers.Length != fieldConfig.Length)
            {
                LogWarning($"[VALIDATE] 字段 '{fieldName}' 数据长度不匹配: 期望{fieldConfig.Length}, 实际{registers.Length}");
            }
            // 检查是否包含有效数据
            var hasNonZeroData = registers.Any(r => r != 0);
            if (!hasNonZeroData)
            {
                LogWarning($"[VALIDATE] 字段 '{fieldName}' 所有寄存器都为0,可能是无效数据");
            }
            // 检查数据范围(16位有符号整数范围)
            var outOfRangeCount = registers.Count(r => r < -32768 || r > 32767);
            if (outOfRangeCount > 0)
            {
                LogWarning($"[VALIDATE] 字段 '{fieldName}' 有{outOfRangeCount}个寄存器值超出16位有符号整数范围");
            }
            // 特殊数据模式检测
            DetectSpecialDataPatterns(fieldName, registers);
        }
        /// <summary>
        /// 检测特殊的数据模式
        /// </summary>
        /// <param name="fieldName">字段名</param>
        /// <param name="registers">寄存器数据</param>
        private void DetectSpecialDataPatterns(string fieldName, int[] registers)
        {
            // 检测全FF模式(通信错误)
            if (registers.All(r => r == -1 || r == 0xFFFF))
            {
                LogWarning($"[PATTERN] 字段 '{fieldName}' 检测到全FF模式,可能是通信错误");
            }
            // 检测常见的错误码模式
            if (registers.Length >= 2)
            {
                var combined = ((long)(registers[0] & 0xFFFF) << 16) | (registers[1] & 0xFFFF);
                if (combined == 0xE2400001)
                {
                    LogInfo($"[PATTERN] 字段 '{fieldName}' 检测到已知的无效数据标识 0xE2400001");
                }
            }
            // 检测ASCII字符模式(用于字符串和时间戳字段)
            if (registers.Any(r => IsLikelyAsciiData(r)))
            {
                LogDebug($"[PATTERN] 字段 '{fieldName}' 包含可能的ASCII字符数据");
            }
        }
        /// <summary>
        /// 检查寄存器值是否可能包含ASCII字符
        /// </summary>
        /// <param name="register">寄存器值</param>
        /// <returns>是否可能是ASCII数据</returns>
        private bool IsLikelyAsciiData(int register)
        {
            if (register == 0) return false;
            var highByte = (byte)((register >> 8) & 0xFF);
            var lowByte = (byte)(register & 0xFF);
            // 检查是否为可打印ASCII字符范围(32-126)
            return (highByte >= 32 && highByte <= 126) || (lowByte >= 32 && lowByte <= 126);
        }
        #endregion
        #region 数据发送方法
        /// <summary>
config/DynamicModbusData.cs
@@ -56,7 +56,18 @@
        public void SetFieldData(string fieldName, object? value, int[] rawRegisters)
        {
            _data[fieldName] = value;
            _rawData[fieldName] = rawRegisters;
            // 复制原始寄存器数据以避免引用问题
            if (rawRegisters != null)
            {
                _rawData[fieldName] = (int[])rawRegisters.Clone();
                Console.WriteLine($"[DATA-STORE] 字段 '{fieldName}' 存储: 解析值={value}, 原始=[{string.Join(", ", rawRegisters)}]");
            }
            else
            {
                _rawData[fieldName] = Array.Empty<int>();
                Console.WriteLine($"[DATA-STORE] 字段 '{fieldName}' 存储: 解析值={value}, 原始数据为null");
            }
        }
        /// <summary>
config/ModbusDataParser.cs
@@ -92,28 +92,32 @@
                // 双寄存器,根据字节序组合
                if (config.Encoding?.ToLower() == "littleendian")
                {
                    // 小端:低位在前
                    // 小端:低位寄存器在前,高位寄存器在后
                    value = (registers[0] & 0xFFFF) | ((long)(registers[1] & 0xFFFF) << 16);
                }
                else
                {
                    // 大端:高位在前(默认)
                    // PLC默认使用大端格式进行多寄存器组合:高位寄存器在前,低位寄存器在后
                    value = ((long)(registers[0] & 0xFFFF) << 16) | (registers[1] & 0xFFFF);
                }
                
                // 检查是否为无效数据标识(如负值组合)
                if (value == 0xE2400001 || value > 0x7FFFFFFF)
                Console.WriteLine($"[PARSER-DEBUG] 双寄存器组合: [{registers[0]}, {registers[1]}] → 0x{value:X8} ({value})");
                // 检查是否为无效数据标识
                if (IsInvalidData(registers, value))
                {
                    // 可能是无效数据,返回0或使用特殊标识
                    Console.WriteLine($"[PARSER-DEBUG] 检测到无效数据标识,返回0");
                    return 0;
                }
            }
            else
            {
                // 多寄存器:按大端顺序组合
                Console.WriteLine($"[PARSER-DEBUG] 多寄存器组合 ({config.Length}个): [{string.Join(", ", registers.Take(config.Length))}]");
                for (int i = 0; i < Math.Min(registers.Length, config.Length); i++)
                {
                    value = (value << 16) | (registers[i] & 0xFFFF);
                    Console.WriteLine($"[PARSER-DEBUG] 步骤{i+1}: 0x{value:X} (寄存器{i}: {registers[i]})");
                }
            }
@@ -123,7 +127,9 @@
                var scaledValue = value * config.Scale;
                return Math.Round(scaledValue, config.DecimalPlaces);
            }
            // 如果没有配置缩放,整数数据默认返回原始值(不再自动应用小数转换)
            // 根据配置文件,测量数据已经配置了Scale: 0.01,所以会走上面的分支
            return value;
        }
@@ -135,23 +141,27 @@
            if (registers.Length == 0) return string.Empty;
            var bytes = new List<byte>();
            Console.WriteLine($"[PARSER-DEBUG] 字符串解析 ({config.Length}个寄存器): [{string.Join(", ", registers.Take(config.Length).Select(r => $"0x{r:X4}"))}]");
            foreach (var register in registers.Take(config.Length))
            {
                if (config.Encoding?.ToLower() == "littleendian")
                {
                    // 小端:低字节在前
                    // 小端:低字节在前,高字节在后
                    bytes.Add((byte)(register & 0xFF));
                    bytes.Add((byte)((register >> 8) & 0xFF));
                }
                else
                {
                    // 大端:高字节在前(默认)
                    // 注意:根据实际PLC数据,字节顺序是反的
                    var highByte = (byte)((register >> 8) & 0xFF);
                    var lowByte = (byte)(register & 0xFF);
                    // PLC字符串存储格式:寄存器值需要字节反序处理
                    // 例如:寄存器0x6261中,0x62='b', 0x61='a',但期望输出"ab"
                    // 因此需要先输出低字节'a',再输出高字节'b'
                    var highByte = (byte)((register >> 8) & 0xFF);  // 实际是第二个字符
                    var lowByte = (byte)(register & 0xFF);           // 实际是第一个字符
                    
                    // 先添加低字节,再添加高字节(适应PLC的字节序)
                    Console.WriteLine($"[PARSER-DEBUG] 寄存器0x{register:X4} → 低字节:'{(char)lowByte}' 高字节:'{(char)highByte}'");
                    // 按期望顺序:先添加低字节,再添加高字节
                    if (lowByte != 0) bytes.Add(lowByte);
                    if (highByte != 0) bytes.Add(highByte);
                }
@@ -163,7 +173,9 @@
                bytes.RemoveAt(bytes.Count - 1);
            }
            return Encoding.ASCII.GetString(bytes.ToArray());
            var result = Encoding.ASCII.GetString(bytes.ToArray());
            Console.WriteLine($"[PARSER-DEBUG] 字符串解析结果: '{result}'");
            return result;
        }
        /// <summary>
@@ -208,20 +220,28 @@
        {
            if (registers.Length < 2) return 0.0f;
            Console.WriteLine($"[PARSER-DEBUG] 浮点数解析: [{registers[0]}, {registers[1]}] (0x{registers[0]:X4}, 0x{registers[1]:X4})");
            // IEEE 754单精度浮点数需要2个寄存器(32位)
            uint value;
            
            if (config.Encoding?.ToLower() == "littleendian")
            {
                // 小端:低位寄存器在前,高位寄存器在后
                value = (uint)(registers[0] | (registers[1] << 16));
                Console.WriteLine($"[PARSER-DEBUG] 小端组合: 0x{value:X8}");
            }
            else
            {
                // PLC默认使用大端格式进行多寄存器组合:高位寄存器在前,低位寄存器在后
                value = (uint)((registers[0] << 16) | registers[1]);
                Console.WriteLine($"[PARSER-DEBUG] 大端组合: 0x{value:X8}");
            }
            // 将32位整数转换为浮点数
            return BitConverter.ToSingle(BitConverter.GetBytes(value), 0);
            var result = BitConverter.ToSingle(BitConverter.GetBytes(value), 0);
            Console.WriteLine($"[PARSER-DEBUG] 浮点数解析结果: {result}");
            return result;
        }
        /// <summary>
@@ -241,12 +261,14 @@
        /// <summary>
        /// 解析时间戳字符串(针对数字字符数据)
        /// 大端模式存储:一个字节两个字符,按文档要求的顺序提取字符
        /// </summary>
        private static string ParseTimestampString(int[] registers, DataField config)
        {
            if (registers.Length == 0) return string.Empty;
            var chars = new List<char>();
            Console.WriteLine($"[PARSER-DEBUG] 时间戳寄存器数据: [{string.Join(", ", registers.Take(config.Length).Select(r => $"0x{r:X4}({r})"))}]");
            foreach (var register in registers.Take(config.Length))
            {
@@ -256,8 +278,12 @@
                var highByte = (byte)((register >> 8) & 0xFF);
                var lowByte = (byte)(register & 0xFF);
                // 按PLC数据格式添加字符(先低字节,后高字节)
                // 检查是否为数字字符(ASCII 48-57, '0'-'9')
                Console.WriteLine($"[PARSER-DEBUG] 寄存器0x{register:X4}: 高字节=0x{highByte:X2}('{(char)highByte}'), 低字节=0x{lowByte:X2}('{(char)lowByte}')");
                // 根据用户提供的解析对照表:大端模式,低字节存在高地址,解析时高低地址要颠倒
                // 即:先取低字节,再取高字节
                // 例如:12338 = 0x3032 → 低字节0x32='2',高字节0x30='0' → "20"
                if (lowByte >= 48 && lowByte <= 57)
                {
                    chars.Add((char)lowByte);
@@ -269,6 +295,7 @@
            }
            var result = new string(chars.ToArray());
            Console.WriteLine($"[PARSER-DEBUG] 时间戳解析结果: '{result}'");
            return result;
        }
@@ -327,6 +354,45 @@
        #region 工具方法
        /// <summary>
        /// 检测是否为无效数据标识
        /// </summary>
        /// <param name="registers">原始寄存器数据</param>
        /// <param name="combinedValue">组合后的值</param>
        /// <returns>是否为无效数据</returns>
        private static bool IsInvalidData(int[] registers, long combinedValue)
        {
            // 常见无效数据模式检测
            // 1. 检查是否为全FF模式(通常表示通信错误)
            if (combinedValue == 0xFFFFFFFF)
            {
                return true;
            }
            // 2. 检查是否为全0(可能表示未初始化)
            if (combinedValue == 0)
            {
                return true;
            }
            // 3. 特定的错误码模式:E240xxxx
            if ((combinedValue & 0xFFFF0000) == 0xE2400000)
            {
                Console.WriteLine($"[PARSER-DEBUG] 检测到E240错误码模式: 0x{combinedValue:X8}");
                return true;
            }
            // 4. 检查是否为典型的错误组合:第一个寄存器为大负数(如-7616),第二个为小正数
            if (registers.Length >= 2 && registers[0] == -7616 && registers[1] == 1)
            {
                Console.WriteLine($"[PARSER-DEBUG] 检测到特定错误模式: [{registers[0]}, {registers[1]}]");
                return true;
            }
            return false;
        }
        /// <summary>
        /// 获取数据类型的默认值
        /// </summary>
        /// <param name="dataType">数据类型字符串</param>
config/modbus_config.json
@@ -1,15 +1,15 @@
{
  "ConfigVersion": "1.0",
  "ProjectName": "GSModbus通用配置",
  "Description": "通用Modbus TCP通信配置文件",
  "ProjectName": "润达项目",
  "Description": "润达项目TCP通信配置文件",
  "Connection": {
    "IpAddress": "192.168.3.250",
    "IpAddress": "192.168.1.231",
    "Port": 502,
    "ConnectionTimeoutMs": 5000,
    "OperationTimeoutMs": 3000
  },
  "Communication": {
    "HeartbeatIntervalMs": 1000,
    "DataPollingIntervalMs": 500,
@@ -17,7 +17,7 @@
    "ReconnectDelayMs": 3000,
    "MaxRetryCount": 3
  },
  "OutputAddresses": {
    "Description": "MES发送给PLC的地址配置",
    "HeartbeatAddress": {
@@ -27,11 +27,11 @@
    },
    "DataConfirmationAddress": {
      "Address": 6001,
      "DataType": "Byte",
      "DataType": "Byte",
      "Description": "MES数据读取确认地址"
    }
  },
  "InputAddresses": {
    "Description": "从PLC读取的地址配置",
    "ControlSignals": {
@@ -50,7 +50,7 @@
        "Description": "PLC数据准备就绪信号"
      }
    },
    "ProductData": {
      "ProductModel": {
        "Address": 6004,
@@ -90,13 +90,13 @@
        "Address": 6026,
        "DataType": "Timestamp",
        "Length": 7,
        "Format": "YYYYMMDDHHMMSS",
        "Format": "YYYYMMDDHHMM",
        "Encoding": "BigEndian",
        "DisplayName": "记录时间",
        "Description": "数据记录时间戳"
        "Description": "数据记录时间戳,大端模式存储,一个字节两个字符,格式:年月日时分(12位)"
      }
    },
    "MeasurementData": {
      "MinInstallSize": {
        "Address": 6033,
@@ -123,7 +123,7 @@
        "DataType": "Integer",
        "Length": 2,
        "Scale": 0.01,
        "Unit": "mm",
        "Unit": "mm",
        "DecimalPlaces": 2,
        "DisplayName": "行程",
        "Description": "执行器行程测量值"
@@ -180,7 +180,7 @@
      }
    }
  },
  "DataTypes": {
    "Description": "支持的数据类型定义",
    "Byte": "单字节数值 (0-255)",
@@ -190,7 +190,7 @@
    "Float": "浮点数",
    "Boolean": "布尔值"
  },
  "UI": {
    "WindowTitle": "GSModbus - 通用MES与PLC通信系统",
    "AutoStartPolling": false,
@@ -201,7 +201,7 @@
  "Database": {
    "Enabled": true,
    "Type": "Oracle",
    "ConnectionString": "Data Source = (DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.1.92)(PORT = 1521))(CONNECT_DATA = (SERVICE_NAME = ORCL))); Persist Security Info=True;User ID = lts_dev; Password=ltsdev",
    "ConnectionString": "Data Source = (DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.1.223)(PORT = 1521))(CONNECT_DATA = (SERVICE_NAME = ORCL))); Persist Security Info=True;User ID = rdmes_dev; Password=rdmesdev",
    "AutoCreateTables": true,
    "DataRetentionDays": 90,
    "Tables": {
config/production_config.json
@@ -94,10 +94,10 @@
        "Address": 6026,
        "DataType": "Timestamp",
        "Length": 7,
        "Format": "YYYYMMDDHHMMSS",
        "Format": "YYYYMMDDHHMM",
        "Encoding": "BigEndian",
        "DisplayName": "记录时间",
        "Description": "数据记录时间戳,大端模式存储,一个字节两个字符,格式:年月日时分秒"
        "Description": "数据记录时间戳,大端模式存储,一个字节两个字符,格式:年月日时分(12位)"
      }
    },
ԭʼֵ.txt
@@ -1,7 +1,70 @@
--原始值
{"PlcHeartbeat":[1],"DataReady":[1],"ProductModel":[25185,25699,26213,26727,27241,12849,13363,13877,14391,12345],"TestStation":[1],"ProductCode":[25185,25699,26213,26727,27241,12849,13363,13877,14391,12345],"TestResult":[1],"Timestamp":[12338,13618,14128,14641,13105,12340,0],"MinInstallSize":[-7616,1],"MaxInstallSize":[-7616,1],"Stroke":[-7616,1],"Speed":[-7616,1],"WorkingVoltage":[-7616,1],"WorkingCurrent":[-7616,1],"WorkingPressure":[-7616,1],"UpHallSensor":[-7616,1]}
{"PlcHeartbeat":[0],"DataReady":[1],"ProductModel":[25185,25699,26213,26727,27241,12849,13363,13877,14391,12345],"TestStation":[1],"ProductCode":[25185,25699,26213,26727,27241,12849,13363,13877,14391,12345],"TestResult":[1],"Timestamp":[12338,13618,14128,14641,13105,12340,0],"MinInstallSize":[-7616,1],"MaxInstallSize":[-7616,1],"Stroke":[-7616,1],"Speed":[-7616,1],"WorkingVoltage":[-7616,1],"WorkingCurrent":[-7616,1],"WorkingPressure":[-7616,1],"UpHallSensor":[-7616,1]}
--部分目标值
--字符串内填的是abcdefghij1234567890
--整型内是12345,就是123.45。
--日期是202507191340字符串
大端模式,低字节存在高地址,解析时高低地址要颠倒一下
D6026   12338 -> 20
D6027   13618 -> 25
D6028   14128 -> 07
D6029   14641 -> 19
D6030   13105 -> 13
D6031   12340 -> 40
D6032   0 ->
//文档中的说明
记录时间  数据长度为7 大端模式存储,一个字节两个字符。年月日时分秒,例“20250718105959”
D6026    记录时间B1
D6027    记录时间B2
D6028    记录时间B3
D6029    记录时间B4
D6030    记录时间B5
D6031    记录时间B6
D6032    记录时间B7
最小安装尺寸  数据长度为2  默认两个小数点:例:收到12345应为123.45
D6033 D6034
最大安装尺寸    数据长度为2  默认两个小数点:例:收到12345应为123.45
D6035 D6036
行程 数据长度为2  默认两个小数点:例:收到12345应为123.45
D6037 D6038
速度 数据长度为2  默认两个小数点:例:收到12345应为123.45
D6039 D6040
工作电压 数据长度为2  默认两个小数点:例:收到12345应为123.45
D6041 D6042
工作电流 数据长度为2  默认两个小数点:例:收到12345应为123.45
D6043 D6044
工作压力 数据长度为2 默认两个小数点:例:收到12345应为123.45
D6045 D6046
上升霍尔 数据长度为2 默认两个小数点:例:收到12345应为123.45
D6047 D6048
控制台实际解析
📊 [数据 #6] 08:55:24.329
   控制信号:
     • PLC心跳: 0
     • 数据就绪: 1
   产品数据:
     • 产品型号: abcdefghij1234567890
     • 测试工位: 1
     • 产品码: abcdefghij1234567890
     • 测试结果: 合格
     • 记录时间: 0001-01-01 00:00:00
   测量数据:
     • 最小安装尺寸: 0 mm
     • 最大安装尺寸: 0 mm
     • 行程: 0 mm
     • 速度: 0 mm/s
     • 工作电压: 0 V
     • 工作电流: 0 A
     • 工作压力: 0 bar
     • 上升霍尔传感器: 0