首页 / 资讯中心 / 文章详情

C#调用AF SDK高效读取PI实时数据实战指南

C#调用AF SDK高效读取PI实时数据实战指南 ★ FEATURED ARTICLE
上个月给某化工厂做MES二期报表对方IT负责人把数据库账号一丢“你要的数据都在PI里自己连吧。”我拿ODBC驱动怼了三天查询效率惨不忍睹。后来换成AF SDK几十个测点的实时曲线秒出。这篇文章就把这次实战踩出来的经验整理一遍给同样要和PI实时数据打交道的C#工程师做个参考。你在工厂里一定会遇到这类需求大屏要显示当前温度、电流、流量报表要拉最近一个月的趋势MES系统要对PI里的测点做越限报警。PI系统里存的是带时间戳、带质量码的时序数据不是传统关系型数据库里的业务表。想用C#读PI实时数据教科书式的做法已经不够用了关键是把访问通道选对、把模型理解透、把常见坑避开。1. 先把PI的访问通道摸清楚再动手写代码1.1 四种常见读取方式别一上来就查ODBC我见过太多人拿到PI服务器地址后的第一反应是写个SQL语句连过去查表。这确实是最容易想到的办法但不是好办法。PI对外提供的访问入口主要有四条路。访问方式技术路径适合场景需要注意的问题ODBC/JDBC直连以SQL查询方式读PI归档数据临时查数、简单历史报表本质是关系型查询读实时快照非常吃力频繁查询会把PI服务器搞得很累老版PI SDK以PI点Point/Tag为中心COM/.NET接口老项目维护、现有脚本改造面向点模型没有资产对象概念新项目不推荐再重新学AF SDK以资产模型AF为中心连接PI Server大多数新项目的主力方案强烈推荐有学习曲线得先理解AF模型和PI点模型的关系PI Web API基于RESTful的JSON接口走HTTP/HTTPS跨平台、前端可视化、轻量集成高频小点并发时HTTP开销大不适合大量点循环拉取ODBC方式为什么不适合做实时数据因为PI内部的数据组织方式和SQL Server、Oracle完全不一样。PI存储的是“时间戳值质量码”的事件流数据写入时按时间顺序归档读取靠的是针对时间序列优化的底层引擎而不是索引加扫描的关系表。你拿SQL查询去模拟“取当前最新值”这种操作每次都要跑到归档文件里找边界性能天然就差。加上ODBC驱动又是额外收费组件网络策略上还得单独开端口很多工厂的IT根本不愿意给ODBC端口开放。1.2 数据从现场设备到C#应用到底经过了哪些环节要理解读取接口先看PI整个数据链路。现场DCS、PLC或传感器先把信号送给采集服务器常见方案是用OPC或Modbus协议然后PI的采集接口组件负责把实时值写入PI Server的Data Archive归档库。PI Server内部维护了两套数据一套是“快照”保存每个测点最新的当前值内存驻留、读取极快另一套是“归档”把历史事件按压缩算法存到磁盘文件里。C#应用去读PI实时数据本质就是两个动作读快照拿当前值读归档拿历史区间记录。而AF SDK同时把这两件事封装成了简洁的对象接口。理解这一点特别重要很多测试脚本读起来慢就是因为没搞清楚自己应该在快照里拿实时值却非要去归档里“查最新一条”。1.3 为什么主力方案是AF SDK老版PI SDK也能读当前值接口也很简单我为什么劝你用AF SDK核心原因有四个。第一AF SDK能利用资产模型。PI里的点通常叫“测点”或“Tag”命名往往带着区域、设备、变量比如“反应釜K101.温度”。老SDK直接面对这一串字符串而AF SDK可以建模成“反应釜”这个对象下挂“温度”“压力”“液位”多个属性读取时按对象、按属性导航大规模项目里可维护性差太多了。第二AF SDK对.NET支持更现代。老SDK带一大堆COM互操作部署时容易出权限问题跑在.NET 6及以上环境里更是别扭。AF SDK可以在.NET Framework或现代.NET运行时下工作遇到新老项目都能落地。第三批量读取能力强。AF SDK提供按点集合一次取值的接口底层会做数据打包传输比我写循环一个个点调用快一个数量级。报表场景里这个差距非常明显。第四除了实时值AF SDK还能读历史归档、事件帧、数字状态、计算属性将来做报警统计、设备效率分析都会用到学习一次长期受益。2. 环境和首个连接让AF SDK在C#里跑起来2.1 准备清单比想象的多写代码之前先把环境确认清楚。很多人卡住不是代码写错是前置条件没满足。你至少需要四样东西。第一PI客户端的AF SDK组件从PI系统官方下载渠道拿对应版本安装包安装时选你用的开发语言对应组件。第二PI服务器名称和通信端口AF SDK默认走TCP 5450端口连接Data Archive部分部署还会用到其他动态端口要提前在PI服务器的防火墙规则里确认。第三一个可用的PI账号权限至少包含读取对应测点的权限很多工厂用域账号集成认证代码里不用传密码但也有用基本认证的情况。第四客户端机器和PI服务器在同一个网络域DNS能解析到PI服务器名。我遇到过一种很典型的“半小时没连上”问题数据库连接串写的是IP地址但PI Server名称解析用的是主机名AF SDK内部通过PI服务器名去查找IP写对了也白搭。所以连接代码里指定的服务器名应该和PI系统管理员给你的服务器名完全一致。2.2 最小连接代码先证明链路是通的环境准备好后写一个最简单的连接测试。下面的示例基于AF SDK 2.10及以上版本的API命名如果你的版本较老类名和重载可能会有细微差别。using OSIsoft.AF; using OSIsoft.AF.PI; public class PiConnectionTest { public static void Test(string serverName) { // 从本机已知的PI服务器集合中找到目标服务器 PISystem piSystem new PISystems()[serverName]; // 连接PI Server piSystem.Connect(); Console.WriteLine($连接状态: {piSystem.ConnectionInfo.Status}); Console.WriteLine($服务器描述: {piSystem.ConnectionInfo.ServerVersion}); } }这段代码背后发生的事情比表面看起来多。new PISystems()会扫描客户端本机已注册的PI服务器列表然后按名称找到PISystemConnect()会完成身份认证、建立到Data Archive的通信链路、初始化连接池。如果连接成功你可以继续访问piSystem.PIDataServers拿到具体的Data Archive对象。有一个容易忽略的细节AF SDK连接是“重量级”操作首次连接可能需要几秒甚至十几秒中间还会有超时判断。这不是程序卡死也不要在一段代码里频繁new和Dispose连接对象。正确用法是进程内复用同一个PISystem实例线程安全访问任务结束后再释放。2.3 连不上时从三个方向排查如果Connect()抛了异常或者状态是Faulted不要急着改代码先按下面表格排查。现象大概率原因处理方式找不到名为XXX的PI服务器服务器名拼错、DNS不解析、客户端未注册服务器用new PISystems()遍历列表确认注册名用ping和nslookup验证网络连接超时防火墙拦截了5450端口或网络隔离策略找PI系统管理员确认端口是否放行在客户端用telnet测试端口连通性Access Denied / 无权限当前Windows账号没有对应PI用户权限切换账号或用基本认证请管理员在PI系统里给账号分配测点读取权限3. 三类高频实时读取场景的C#实现3.1 读取当前实时值拿快照不要拿归档读取某一个测点的最新值是“实时读取”最核心的操作。PI的Data Archive会为每个点维护一个“快照值”也就是最近一次写入产生的值。这个值存在内存中读取速度极快。using OSIsoft.AF; using OSIsoft.AF.PI; using OSIsoft.AF.Asset; public class PiReader { private readonly PISystem _piSystem; public PiReader(string serverName) { _piSystem new PISystems()[serverName]; _piSystem.Connect(); } public AFValue GetCurrentValue(string dataServerName, string pointName) { PIDataServer dataServer _piSystem.PIDataServers[dataServerName]; PIPoint piPoint PIPoint.FindPIPoint(dataServer, pointName); return piPoint.CurrentValue(); } }调用方式var reader new PiReader(PISRV01); AFValue v reader.GetCurrentValue(PISRV01, REACTOR_K101.TEMPERATURE); Console.WriteLine($时间: {v.Timestamp}); Console.WriteLine($数值: {v.Value}); Console.WriteLine($质量码: {v.Status}); Console.WriteLine($单位: {v.UOM?.ToString() ?? 无});这里面有三个要点。AFValue.Value动态类型数值点返回double或float字符串点返回string数字状态点返回数字状态对象。做报表输出时一定要先判类型再格式化不能想当然ToString。AFValue.Timestamp返回的时间是UTC时间不是本地时间。如果你直接绑定给前端图表不转本地时区显示出来的曲线会整整偏8个小时。后面第4章再展开。AFValue.Status是质量码里面藏着大坑。你看到PI里读出一个值不代表这个值就是可靠的生产数据质量码为坏质量时数值可能是估算出来的也可能是设备停运时的残值。怎么处理也放到第4章。如果只是测试用也可以直接读AF模型里的属性AFDatabase database _piSystem.Databases[MES_DB]; AFElement element database.Elements[反应K101]; AFAttribute temperatureAttr element.Attributes[反应温度]; AFValue attrValue temperatureAttr.GetValue();这两种方式读到的都是实时快照。区别在于PIPoint.CurrentValue()更直接适合你只知道测点名的情况AFAttribute适合你知道资产路径、想按对象层级遍历的情况。实际项目里后者更优雅因为它天然带着模型结构读出来的值还附带单位的元数据。3.2 读取历史区间搞懂边界类型再取数做趋势曲线、做日报表都需要读取一段历史区间。这里最大的坑是“边界类型”。using OSIsoft.AF.Time; public ListAFValue GetHistory(string dataServerName, string pointName, DateTime startUtc, DateTime endUtc) { PIDataServer dataServer _piSystem.PIDataServers[dataServerName]; PIPoint piPoint PIPoint.FindPIPoint(dataServer, pointName); AFTimeRange timeRange new AFTimeRange(startUtc, endUtc); IListAFValue values piPoint.RecordedValues(timeRange, AFBoundaryType.Inside, , false); return values.ToList(); }注意RecordedValues的重载参数很多核心第一个参数是时间区间第二个参数就是AFBoundaryType。它决定查询结果的边界行为。AFBoundaryType.Inside只返回完全落在区间内部的事件记录。做统计报表时这个选项最接近“这段时间真实发生了哪些数据变化”。AFBoundaryType.Interpolated区间两端自动补一个插值点保证返回的第一个点和最后一个点正好落在边界时间上。做趋势曲线视觉效果好但它可能生成“实际上不存在”的数据。AFBoundaryType.Inside和Outside的差别我举个直观例子某温度点上午10点整有个事件你要查9点到10点半的区间用Inside就不包含10点整这跳数据用Outside就把区间左右最近的事件也带进来。我在做报表时基本默认用Inside因为报表里每一行都要对应真实发生的生产过程事件。只有做曲线展示时才考虑用Interpolated补点图形更好看。还有一个性能习惯读取很长时间范围时不要一次查一个月。PI归档数据本身压缩过但一次查询跨度太大服务器端要做大量的盘区扫描很容易把PI服务器的性能拖下来。我现在的做法是把一个月拆成一天一天查后台线程分桶并行拉取最后在内存里合并。速度不慢对PI服务器压力也小得多。3.3 批量读取多个测点循环调用是要挨打的新手最容易写出来的代码是foreach (var tagName in tagNames) { var value piPoint.CurrentValue(); // 循环里一个个查 }这样写点一多就把PI服务器打满。AF SDK提供了批量读取机制核心思路是把一组PIPoint装进集合一次请求发过去。public Dictionarystring, AFValue BatchGetCurrent(string dataServerName, Liststring pointNames) { PIDataServer dataServer _piSystem.PIDataServers[dataServerName]; PIPointList pointList new PIPointList(); foreach (var name in pointNames) { pointList.Add(PIPoint.FindPIPoint(dataServer, name)); } // 按点在集合中的顺序返回对应的当前值集合 IListAFValue values dataServer.CurrentValues(pointList); var result new Dictionarystring, AFValue(); for (int i 0; i pointNames.Count; i) { result[pointNames[i]] values[i]; } return result; }IDataServer.CurrentValues()这个批量接口返回的集合顺序和传入的点集合顺序是一致的所以可以靠索引对齐。批量读取的实际收益在点数量超过20个后非常明显一次网络往返搞定效率是循环方式的几十倍。关于异步再补一句。AF SDK的Connect()和CurrentValue()是同步阻塞接口在WinForm/WPF里直接调界面会卡住。正确姿势是放到后台线程或Task里再把结果通过事件、Invoke或数据绑定推回UI。如果你写的是ASP.NET Core服务更要注意不要为每个请求创建独立PISystem连接应该用依赖注入做成单例服务。4. 生产环境里最容易踩的五个坑4.1 时间戳UTC和当地时间一定分清楚这个坑我踩了三回才算长记性。AF SDK返回的时间是UTC很多开发者在本地调试时没感觉因为调试机器时区恰好和PI服务器一致。一旦部署到云服务器或跨时区环境时间差马上暴露。举个具体例子。某设备操作员在系统里看到“14:00报警”但数据库里存的时间戳显示“06:00”于是误判报警时间。这不是数据丢了而是时间展示层没做时区转换。处理原则两条内部存储、查询参数统一用UTC时间。构造查询区间时用DateTime.UtcNow或者用AFTime字符串带后缀“Z”例如new AFTime(2025-02-11T08:00:00Z)。界面展示时再转本地时间。前端图表组件如果支持时区属性可以直接传本地时区名如果是自绘文本用TimeZoneInfo.ConvertTimeFromUtc。4.2 质量码读到的数值不等于可靠的生产数据PI数据的“质量码”Quality Code极其重要。PI的数值不只包含大小还带一个状态描述常见状态包括好质量、坏质量、清零、替换值、估算值、停机值等。默认状态下很多PI点即使现场设备已经断线也会在历史归档里留下“最后已知值”配合坏质量标记。如果你只读取Value字段完全不管Status报表里统计到的可能就是一堆断电瞬间的残值。我在项目里养成了一个习惯数据落入业务库之前先做质量码过滤。逻辑大致是if (!afValue.Status.IsGood) { // 记录日志或者标记该值不可用 continue; }注意Status.IsGood只是一个布尔简化判断实际生产中还可能存在“可疑”“置换”等中间状态对应特定的质量码数值。稳妥的做法是把原始质量码也存进业务表需要时再回溯分析。4.3 归档查询的“无中生有”问题用AFBoundaryType.Interpolated查历史数据时如果某个时间段里根本没有事件记录PI会按照前后事件做插值硬生生造出一个时间点。这对趋势曲线没问题画出来是连续的但对统计报表就是灾难。比如某台泵实际运行了10分钟中间没有事件用Interpolated拉出来的每分钟一个点会让你以为它一直稳定运行而用Inside拉出来可能只有起点和终点两条事件记录才能反应真实启停。结论很简单历史统计一律用Inside趋势展示视情况用Interpolated。如果数值变化频繁、事件很密两者差别不大如果测点变化稀疏差别就非常大。4.4 单位换算别想当然地乘系数PI点本身可以配置工程单位Engineering Units比如摄氏度、兆帕、立方米每小时。但这个单位更多是“标注性”的PI存储的数值通常是采集上来时的原始工程值比如DCS给的是0到100的量程百分比那PI里存的就是0到100的小数并不自动转成你想要的工艺量。我做DCS数据对接时就见过工艺人员抱怨“PI里温度明明是50为什么换算后变成500”。原因出在PI点是按“百分比”存储的而DCS的量程是0到500度转换公式是实际值 量程下限 采集百分比 * (量程上限 - 量程下限)。这种换算关系AF SDK不会替你算必须自己从PI点的属性里读量程范围和单位元数据再按公式处理。所以建议里有一条建点的时候尽量让PI采集接口直接存储工程单位下的实际值避免在应用层反复换算。如果你无法控制建点规则也要把换算规则写进配置中心不要让每个报表各自理解。4.5 连接对象和内存服务跑几天就卡多半是连接泄漏AF SDK对象模型里PISystem、PIDataServer这些对象默认实现了IDisposable。很多开发者只记得new不记得Dispose在循环或高频请求里反复创建连接最终把进程的句柄数拉爆服务越来越卡。我踩过一次印象很深的故障某数据同步服务上线两周后内存逐步上涨最后整个Windows服务假死。排查后发现问题出在我每次处理一批测点时就new了一个PISystem虽然局部变量会被GC回收但内部连接资源并没有及时释放。正确做法是这样的长时间运行的服务全局只创建一个PISystem连接作为单例复用。程序退出时再Dispose。短命令行的工具程序用using块包裹确保连接随作用域释放。多线程环境同一个PISystem实例在AF SDK内部通常做了线程安全处理但在拿值和批处理时不建议多个线程并发写同一个集合对象要有明确的边界。如果非要频繁建立短连接至少做成连接池复用已建立的连接对象不要裸new。5. AF SDK与PI Web API的选型建议5.1 PI Web API真香但别拿它硬扛高频轮询PI Web API是基于HTTP的RESTful接口最大的优点是跨平台、不需要装SDK、防火墙策略好做。如果你写的是前端JavaScript应用或者用Python做数据分析走Web API顺手得多。C#里用HttpClient也能轻松调用。读取当前值的HTTP请求大概长这样GET /piwebapi/streams?path\\PISRV01\REACTOR_K101.TEMPERATUREvaluecurrent返回JSON{ Timestamp: 2025-02-11T08:00:00Z, Value: 92.5, UnitsAbbreviation: deg C, Good: true, Questionable: false, Substituted: false }C#侧用System.Text.Json反序列化即可public class StreamValue { public DateTime Timestamp { get; set; } public double Value { get; set; } public string UnitsAbbreviation { get; set; } public bool Good { get; set; } public bool Questionable { get; set; } public bool Substituted { get; set; } } public async TaskStreamValue GetCurrentViaWebApi(string baseUrl, string pointPath) { using var client new HttpClient(); client.BaseAddress new Uri(baseUrl); string url $/piwebapi/streams?path{Uri.EscapeDataString(pointPath)}valuecurrent; var json await client.GetStringAsync(url); return JsonSerializer.DeserializeStreamValue(json, new JsonSerializerOptions { PropertyNameCaseInsensitive true }); }但Web API的短板也很明显HTTP协议本身有消息头、连接握手、序列化开销高频小点轮询时会把时间和带宽都消耗在协议上。我见过一个看板项目前端每5秒拉20个点走Web API直接把PI Web API服务拖到超时。后来改成边缘服务用AF SDK把数据回传到本地内存前端走后端接口问题立刻消失。5.2 混合架构是我目前最推荐的姿势我现在做这类项目的标准架构是“三层分层”第一层边缘采集服务C#AF SDK负责从PI批量读取实时值和历史数据按业务需要的频率写入本地缓存比如Redis或轻量数据库。第二层业务API层按照看板、报表、告警等业务场景向第一层请求数据做单位换算、质量码过滤、时间片聚合。第三层前端展示层只调业务API不直接碰PI系统。这套架构的好处是PI系统的访问点被收敛到一个服务里权限、流量、缓存都容易控制换PI服务器、升级SDK版本时只需要改一个服务前端完全无感。如果你只是临时做个小工具不想搭三层架构那就根据点数评估点少小于20个、并发低每秒几次用Web API省事点数多、需要历史区间、需要批量处理老老实实上AF SDK。最后分享一个经验无论用哪种接口拿到PI数据后一定要先打印一条日志把时间戳、数值、质量码、单位全部记录下来。这个习惯帮我排查过无数次数据对不上的情况——很多时候数据没错是读取端在时区、单位或者质量判断上出了偏差。数据链路越复杂日志越值钱。
阅读完成 · 觉得有帮助?
咨询建站