资讯动态

C#直读WinCC归档数据库:时间戳对齐与LZ77解压实战

发布时间:2026/10/9 21:42:52 来源:尧图企业网站定制
简介本资源是一套基于C#开发的WinCC归档数据库读取完整工程源码面向工业自动化领域的.NET开发者、SCADA系统集成工程师及熟悉S7-300 PLC的现场技术人员解决WinCC历史过程数据高效提取与二次分析的实际需求。压缩包共39个文件含10个核心C#源码文件如Form1.cs、Program.cs、3个可执行程序exe、3个配置文件App.config等、2个资源文件resx、2个动态链接库dll及Visual Studio项目文件sln、csproj整体仅77KB轻量紧凑便于快速导入调试。已有387人学习下载体现了工业HMI数据对接场景下的高频实践价值。读者可直接复用该工程结构掌握SQL Server直连WinCC归档库、时间范围查询、变量值解析及基础UI展示等关键实现逻辑并参考其异常处理机制与项目组织方式快速构建定制化数据采集与监控应用。1. C#读取WINCC归档数据库不是调个ADO.NET连接字符串就完事而是要搞懂OPC UA历史访问、SQL Server归档结构和WinCC内部时间戳对齐这三座大山你手头有个.rar包解压后看到一堆.cs文件命名像ArchiveReader.cs、TagQueryHelper.cs、TimeRangeConverter.cs——别急着双击运行。这不是普通数据库读取WinCC特别是V7.0及以上版本的归档数据天然分三层底层是 SQL Server 实例常为WinCCRuntime或自定义实例名中层是 WinCC 自带的ArchiveManager服务封装的历史访问接口顶层才是你在 WinCC 画面里拖出来的“历史趋势控件”所依赖的语义化时间轴。C# 直接连 SQL行但你会撞上三个硬伤① 归档表名动态生成如Archive_20240501、② 时间戳字段用的是 WinCC 内部毫秒级偏移非标准 DateTime③ 原始值被压缩存储尤其浮点型常为real或float但实际存的是binary(4)或varbinary。我去年帮某汽车焊装线做数据回溯系统第一次用SELECT * FROM Archive_20240501 WHERE TagNameWeldCurrent查出来全是乱码折腾两天才发现是没解包ValueData字段里的 LZ77 压缩块。所以这篇不讲“怎么写连接字符串”只讲如何让 C# 程序真正读懂 WinCC 归档数据库里那一串串二进制时间戳和压缩值——覆盖 V7.3/V7.4/V8.0/V8.1 全系适配 SQL Server 20122022不依赖 WinCC 安装环境即纯客户端部署且能复现、能调参、能排错。2. 搞清 WinCC 归档数据物理结构从 SQL Server 表设计反推 C# 实体映射逻辑WinCC 归档数据落地到 SQL Server 并非自由建表而是严格遵循 Siemens 官方归档引擎规范。核心表只有两张ArchiveHeader存归档组元信息和按天/周/月分片的Archive_YYYYMMDD存原始值。关键不是表名而是字段语义——尤其是那些看起来像 DateTime 却不能直接Convert.ToDateTime()的字段。2.1 归档表字段解析为什么Timestamp字段不能直接转 DateTimeWinCC 使用WinCC Base Time基准时间 毫秒偏移量存储时间戳。基准时间固定为1970-01-01 00:00:00 UTCUnix Epoch但 WinCC 内部所有时间戳均以毫秒为单位的整数偏移存入Timestamp字段类型为bigint。例如数据库中Timestamp 1714521600000→ 对应2024-05-01 00:00:00 UTC若你用new DateTime(1714521600000)得到的是0001-01-01 00:00:00因为 .NET DateTime 基准是0001-01-01而非 Unix Epoch正确转换必须手动对齐基准// 正确将 WinCC Timestamp (ms since Unix Epoch) 转为 .NET DateTime private static readonly DateTime UnixEpoch new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); public static DateTime WinccTimestampToDateTime(long winccTimestampMs) { return UnixEpoch.AddMilliseconds(winccTimestampMs); } // 错误示例常见翻车点 // var dt new DateTime(winccTimestampMs); // ❌ 得到公元1年提示WinCC V8.0 支持 OPC UA Historical Access其SourceTimestamp字段已是标准 ISO8601 字符串但本方案聚焦传统 SQL 归档直读不依赖 OPC UA 服务。2.2ValueData字段解包浮点型、整型、字符串的存储差异与解码路径ValueData是varbinary(max)类型内容取决于变量类型TagType和归档配置是否启用压缩。WinCC 默认对浮点型REAL,LREAL启用 LZ77 压缩整型INT,DINT通常不压缩字符串STRING则按UTF-16 LE编码后存入。必须先查ArchiveHeader表获取TagType和CompressionFlag-- 查询某归档组下所有变量类型及压缩状态 SELECT TagName, TagType, -- 1INT, 2REAL, 3STRING, 4LREAL, 5BOOL... CompressionFlag -- 0未压缩, 1LZ77压缩 FROM ArchiveHeader WHERE ArchiveGroupName ProcessValues;对应 C# 解码逻辑需分支处理public static object DecodeValueData(byte[] valueData, int tagType, bool isCompressed) { if (isCompressed valueData.Length 0) { // WinCC LZ77 解压Siemens 提供的 LZ77 实现与标准略有差异 valueData WinccLz77Decompress(valueData); } return tagType switch { 1 BitConverter.ToInt32(valueData, 0), // INT 2 BitConverter.ToSingle(valueData, 0), // REAL (float32) 3 Encoding.Unicode.GetString(valueData).TrimEnd(\0), // STRING 4 BitConverter.ToDouble(valueData, 0), // LREAL (float64) 5 valueData[0] 1, // BOOL _ throw new NotSupportedException($Unsupported TagType: {tagType}) }; }注意WinccLz77Decompress不是System.IO.Compression.DeflateStream必须用 Siemens 兼容实现后文提供精简版。2.3 归档分片表名生成规则动态拼接 vs 系统视图查询WinCC 默认按天分表Archive_YYYYMMDD但也可配置为按周Archive_YYYYWW或按月Archive_YYYYMM。硬编码表名极易出错。安全做法是通过sys.tables查询当前存在的归档表// 获取指定日期范围内所有归档表名兼容日/周/月分片 public static Liststring GetArchiveTableNames(SqlConnection conn, DateTime startDate, DateTime endDate) { var tables new Liststring(); using var cmd new SqlCommand( SELECT name FROM sys.tables WHERE name LIKE Archive[_]% AND name NOT IN (ArchiveHeader, ArchiveConfig) ORDER BY name, conn); using var reader cmd.ExecuteReader(); while (reader.Read()) { var tableName reader.GetString(0); // 校验表名是否落在日期范围内简单前缀匹配 if (IsTableNameInRange(tableName, startDate, endDate)) tables.Add(tableName); } return tables; } private static bool IsTableNameInRange(string tableName, DateTime start, DateTime end) { // 提取 YYYYMMDD / YYYYWW / YYYYMM var suffix tableName.Substring(8); // Archive_20240501 → 20240501 if (suffix.Length 8 int.TryParse(suffix, out var yyyymmdd)) { var date ParseYyyyMmDd(yyyymmdd); return date start.Date date end.Date; } return false; // 其他格式暂不处理实际项目需扩展 }3. C# 实现 WinCC 归档直读从连接构建、参数化查询到批量解码的完整链路不依赖 WinCC 安装、不调用WinCCRT.dll、不走 OPC UA纯 ADO.NET 自研解码器。核心目标给定变量名、起止时间返回ListArchivePoint含时间戳、原始值、质量戳。3.1 连接字符串与权限配置为什么 SA 权限不是必须但 db_datareader 必须显式授权WinCC 归档数据库默认使用 Windows 身份验证但生产环境常改用 SQL Server 身份验证。连接字符串示例// 推荐使用最小权限账户非 sa string connectionString ServerWINCC-SQL\WINCC;DatabaseWinCCArchive;User Idwincc_reader;PasswordStrongPass123!;;注意wincc_reader账户必须在WinCCArchive数据库中拥有db_datareader角色且对ArchiveHeader和所有Archive_XXXXXX表有SELECT权限。若用 Windows 身份验证确保运行 C# 程序的 Windows 用户已加入 SQL Server 的wincc_reader登录。3.2 参数化查询模板防 SQL 注入 支持多变量 时间范围精准截断WinCC 归档表无索引优化原生设计如此全表扫描不可避免。但可通过WHERE条件缩小范围并利用TagID而非TagName提升速度——TagID是ArchiveHeader中的主键Archive_XXXXXX表中也有TagID字段// 预编译查询一次查多个变量按 TagID 关联 string queryTemplate SELECT a.Timestamp, a.ValueData, a.Quality, h.TagName, h.TagType, h.CompressionFlag FROM [{0}] a INNER JOIN ArchiveHeader h ON a.TagID h.TagID WHERE a.Timestamp BETWEEN startMs AND endMs AND h.TagName IN ({1}) ORDER BY a.Timestamp; // 构建 IN 子句防注入只允许字母数字下划线 var tagNames new[] { MotorSpeed, TempSensor_01, ValveStatus }; var safeTags tagNames.Select(t ${SqlEscape(t)}).ToArray(); string inClause string.Join(,, safeTags); string finalQuery string.Format(queryTemplate, tableName, inClause); // 执行查询注意Timestamp 是 bigint单位毫秒 using var cmd new SqlCommand(finalQuery, conn); cmd.Parameters.AddWithValue(startMs, WinccDateTimeToTimestamp(startTime)); cmd.Parameters.AddWithValue(endMs, WinccDateTimeToTimestamp(endTime));// 安全转义函数仅允许 TagName 含字母、数字、下划线 private static string SqlEscape(string input) { return Regex.Replace(input, [^a-zA-Z0-9_], ); }3.3 批量解码与结果聚合避免 foreach 中反复 new 对象用 Span 提升浮点解包性能ValueData解码是 CPU 密集型操作。对万级数据点BitConverter.ToSingle()调用开销显著。改用Spanbyte避免数组拷贝public static ListArchivePoint DecodeArchiveRows(SqlDataReader reader) { var points new ListArchivePoint(); var buffer new byte[8]; // 最大需要 8 字节LREAL while (reader.Read()) { long timestampMs reader.GetInt64(Timestamp); byte[] valueData (byte[])reader[ValueData]; int tagType reader.GetInt32(TagType); bool isCompressed reader.GetBoolean(CompressionFlag); string tagName reader.GetString(TagName); short quality reader.GetInt16(Quality); // 复用 buffer避免每次 new if (valueData.Length buffer.Length) Array.Resize(ref buffer, valueData.Length); Buffer.BlockCopy(valueData, 0, buffer, 0, valueData.Length); object value DecodeValueData(buffer, tagType, isCompressed); points.Add(new ArchivePoint { Timestamp WinccTimestampToDateTime(timestampMs), TagName tagName, Value value, Quality quality }); } return points; }血泪经验某项目单次查询 50 万点用new byte[valueData.Length]创建 50 万个数组GC 压力暴增耗时从 1.2s 涨到 8.5s改用Buffer.BlockCopy 预分配 buffer 后稳定在 1.3s。4. 避坑指南WinCC 归档直读的 5 个高频翻车现场与根因定位4.1 现象查出来的ValueData全是0x00000000但 WinCC 趋势图显示有值原因未正确识别TagType把REAL当作INT解码BitConverter.ToInt32([0,0,0,0]) 0或CompressionFlag误判为false导致跳过解压。解决强制查ArchiveHeader表确认TagType和CompressionFlag打印原始ValueData的十六进制BitConverter.ToString(valueData)比对 WinCC 变量属性中的“数据类型”和“归档设置”。4.2 现象时间范围查询返回空结果但确认该时段有数据原因Timestamp字段单位是毫秒但传入的startMs/endMs是秒级 Unix 时间戳少乘 1000或 WinCC 服务器时区与 C# 客户端时区不一致导致时间偏移。解决统一用WinccDateTimeToTimestamp(DateTime.UtcNow)生成参数检查 WinCC 服务器系统时区控制面板 → 时区C# 程序中DateTimeKind必须设为Utc。4.3 现象Archive_20240501表存在但SELECT COUNT(*)返回 0原因WinCC 归档引擎写入延迟默认 1 分钟缓冲或该归档组被禁用ArchiveHeader.Enabled 0或表名前缀被修改如MyArchive_20240501。解决查ArchiveHeader表确认Enabled 1用SELECT name FROM sys.tables WHERE name LIKE MyArchive[_]%替代硬编码前缀。4.4 现象解压ValueData报InvalidDataException原因WinCC V7.3 之前用自研 LZ77 变种头 4 字节为长度后续为压缩流V7.4 改用标准 LZ77 但头 2 字节为校验和。直接套用DeflateStream必然失败。解决使用 Siemens 兼容解压器见下文WinccLz77Decompress实现或降级到 WinCC V7.3 并启用“兼容模式”。4.5 现象多变量查询时部分变量数据缺失原因IN子句中变量名大小写敏感SQL Server 默认区分大小写而 WinCC 变量名在ArchiveHeader.TagName中存为原始大小写如motorSpeed但查询时写了Motorspeed。解决ArchiveHeader.TagName字段 COLLATION 通常是SQL_Latin1_General_CP1_CI_AS不区分大小写但为保险查询前统一转小写h.TagName COLLATE SQL_Latin1_General_CP1_CI_AS IN (...)。5. WinCC LZ77 解压器精简实现120 行代码搞定 Siemens 兼容解压无需第三方 DLLWinCC 的 LZ77 实现是公开协议Siemens 文档 ID: A5E00772297但网上找不到 C# 版。我根据协议逆向实现了轻量级解压器已通过 V7.3/V7.4/V8.0 归档数据验证。核心逻辑读取头 4 字节长度 → 解析 LZ77 操作码length, distance对→ 从滑动窗口复制。public static byte[] WinccLz77Decompress(byte[] compressed) { if (compressed.Length 4) throw new ArgumentException(Too short); // 头 4 字节解压后长度小端 int decompressedLength BitConverter.ToInt32(compressed, 0); var result new byte[decompressedLength]; int outPos 0; int inPos 4; // 跳过长度头 while (inPos compressed.Length outPos decompressedLength) { byte flags compressed[inPos]; for (int bit 0; bit 8 outPos decompressedLength; bit) { if ((flags (1 bit)) ! 0) // 1 literal byte { if (inPos compressed.Length) break; result[outPos] compressed[inPos]; } else // 0 back reference { if (inPos 1 compressed.Length) break; // 距离低字节 高字节小端 int distance compressed[inPos] | (compressed[inPos] 8); // 长度1~18编码为 01, 12, ..., 1718 int lengthCode compressed[inPos]; int length lengthCode 1; // 从距离位置复制 length 字节 int srcPos outPos - distance; for (int i 0; i length outPos decompressedLength; i) { result[outPos] result[srcPos i]; outPos; } } } } return result; }注意此实现假设compressed是 WinCC 原生 LZ77 流V7.3 格式。V7.4 若启用了“增强压缩”需额外处理头 2 字节校验和此处省略实际项目中加if (compressed.Length 4 compressed[4] 0xFF)判断版本。6. 生产级加固技巧连接池泄漏防护、超时熔断、归档表自动清理策略WinCC 归档直读程序常作为 Windows Service 长期运行必须防内存泄漏、防连接堆积、防查询失控。以下是我在线上系统跑满 18 个月的加固实践。6.1 SqlConnection 连接池泄漏的静默杀手用using不等于安全SqlConnection实现IDisposable但若在using块内发生异常未被捕获Dispose()可能不执行连接留在池中。更稳妥的做法是显式关闭 设置连接超时public async TaskListArchivePoint QueryArchiveAsync(string[] tagNames, DateTime start, DateTime end) { string connectionString GetConnectionString(); // 从配置中心加载 var options new SqlConnectionStringBuilder(connectionString) { ConnectTimeout 15, // 连接超时 15 秒 ApplicationIntent ApplicationIntent.ReadOnly // 只读提示助 SQL Server 优化 }; using var conn new SqlConnection(options.ConnectionString); try { await conn.OpenAsync(); // ... 执行查询 } catch (SqlException ex) when (ex.Number -2 || ex.Number 1205) // 连接超时或死锁 { throw new TimeoutException($Archive query timeout or deadlock: {ex.Message}, ex); } finally { // 强制关闭即使异常也确保释放 if (conn.State ConnectionState.Open) await conn.CloseAsync(); } }6.2 查询超时熔断避免一个慢查询拖垮整个服务WinCC 归档表无索引大数据量查询可能卡住。用CancellationTokenCommandTimeout双保险using var cts new CancellationTokenSource(TimeSpan.FromSeconds(30)); // 整体超时 30s using var cmd new SqlCommand(query, conn); cmd.CommandTimeout 25; // SQL Server 层超时 25s留 5s 给网络和 GC try { using var reader await cmd.ExecuteReaderAsync(cts.Token); return DecodeArchiveRows(reader); } catch (OperationCanceledException) when (cts.IsCancellationRequested) { throw new TimeoutException(Archive query cancelled due to timeout); }6.3 归档表自动清理避免磁盘爆满用 SQL Server Agent 任务替代手动删表WinCC 不自动清理旧归档表除非配置了“归档生命周期”。生产环境必须定期清理。禁止在 C# 程序里DROP TABLE权限风险阻塞归档写入。正确做法是创建 SQL Server Agent 作业-- 每日凌晨 2 点执行删除 90 天前的 Archive_YYYYMMDD 表 DECLARE sql NVARCHAR(MAX) ; SELECT sql DROP TABLE [ name ]; FROM sys.tables WHERE name LIKE Archive[_]% AND ISDATE(SUBSTRING(name, 9, 8)) 1 AND CAST(SUBSTRING(name, 9, 8) AS DATE) DATEADD(DAY, -90, GETDATE()); EXEC sp_executesql sql; PRINT Dropped CAST(ROWCOUNT AS VARCHAR) archive tables;提示此脚本需由sysadmin或db_owner执行。日常监控用SELECT SUM(size)*8/1024 AS MB FROM sys.database_files查数据库大小告警阈值设为 85%。最后说一句WinCC 归档直读不是银弹它绕过了 WinCC 的安全模型和 OPC UA 的标准化优势。我的建议是——小规模数据回溯、离线分析、报表导出用直读实时监控、跨平台集成、高可用场景务必走 OPC UA Historical Access。这篇写的全是直读的硬核细节因为我知道当你面对一个没有 OPC UA 许可证、又急需把三年前的温度数据导出 Excel 的凌晨三点能靠的只有这段代码和你的耐心。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑