news 2026/10/21 8:59:22

C#读取PI系统数据:PI SDK、AF SDK与REST API实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#读取PI系统数据:PI SDK、AF SDK与REST API实战指南

1. 先理清思路:PI系统到底提供了哪些数据访问方式

搞工业数据的人都知道,PI系统(Plant Information系统)在流程行业里几乎是标配,从DCS、PLC、SCADA采集上来的实时数据,最终都会汇到PI服务器的历史库里。我们做上位机开发、报表系统或者数据中台的时候,经常要用C#把PI里的数据拉出来,做趋势分析、报警统计、KPI计算,甚至是喂给机器学习模型做预测。

这里我先泼一盆冷水:PI系统的数据接口没有你想的那么“现代”。它不像MySQL、PostgreSQL那样给你一个标准的驱动,而是提供了好几套面向不同场景的API,有些人第一次接触就被这一堆SDK搞晕了。我见过不少同事在PI SDK和AF SDK之间犹豫半天,最后选错了方向,代码写了一堆却发现性能或者功能不匹配,返工重来。

简单梳理一下,目前C#读取PI数据的主流途径有四种:

  • PI SDK:老牌COM组件,封装了连接PI Server、读取点(Point)、读取快照值(Snapshot)、读取历史值(History)的基本能力。它依赖PI家的“点表”概念,适合传统的PI 3.x架构,很多老项目都在用它。
  • AF SDK:这是后来推出的、面向PI Asset Framework(AF)架构的SDK,比PI SDK“高级”很多。它把数据组织成资产模型(比如某条产线、某个设备节点下的测点集合),更贴近业务逻辑,性能也更好。AVEVA收购之后,AF SDK基本是官方主推的方向。
  • PI OLEDB / PI JDBC:这俩是给SQL系开发者准备的,把PI数据映射成关系型数据库的表结构,可以用SQL查询。但说实话,功能覆盖有限,性能也不是最优,适合临时做数据导出、报表工具,不适合高频实时读取。
  • PI Web API(RESTful API):通过HTTP方式暴露数据接口,用JSON返回结果,C#里用HttpClient就能调用。这个方案跨平台、跨语言,而且不需要在客户端装PI的SDK,现在越来越多人倾向于用它做轻量集成。

我个人的建议是:如果是新项目,优先用AF SDK,或者直接走PI Web API;如果公司老系统里大量逻辑都是围绕传统PI点表写的,那PI SDK还得继续用,毕竟兼容性摆在那里。下面我会把最常用的几种读取方式全部展开,附上我自己实际跑过的代码和踩坑记录。

2. 环境准备:先把PI SDK和AF SDK的依赖关系理顺

2.1 引用与安装,没你想的那么难

PI SDK是一个COM组件,安装PI SDK或者PI Server客户端工具之后,系统里会注册这个COM组件。C#项目里添加引用,选择COM选项卡,找到“PISDK”相关项即可。注意,PI SDK有32位和64位版本之分,如果你的程序集目标是x86(比如很多WinForm老项目为了兼容旧Excel组件),就必须确保PI SDK的32位版本已经安装;反过来,如果你的程序是x64,最好装64位的SDK,否则进程里加载COM会出现CLSID不匹配或者“类未注册”的尴尬错误。

AF SDK则是一个.NET程序集,安装完成后在GAC里注册。C#项目里直接添加引用,或者在NuGet上搜索“OSIsoft.AF”之类的包(实际包名以官方发布为准)。AF SDK内部也会依赖部分COM组件,但多数情况下你不需要关心,因为它已经帮你封装好了底层的连接细节。

我自己的习惯是:在开发机上把PI SDK和AF SDK全装上,这样写代码的时候想用哪个就用哪个,不用被环境卡住。但部署到生产服务器的时候,就要按程序实际用到的API来裁剪安装,避免服务器上出现不必要的组件和潜在的DLL冲突。

2.2 连接字符串和权限,80%的问题都出在这

不管是PI SDK还是AF SDK,连接PI数据库都需要三个核心信息:服务器名、用户名、密码。这里有个容易误会的点:PI Server的登录账密不一定等于Windows域账密,PI有自己的安全模型(PI Trust、映射表、身份验证),默认可以用“域\用户名”的格式登录,也可以配置成Windows集成认证。

PI SDK里面连接服务器的经典写法是:

using PISDK; PISDKCommon sdk = new PISDKCommon(); PISDKServer server = sdk.Servers.DefaultServer;

然后通过server.Open("服务器名", "用户名", "密码")进行登录。如果是域集成环境,用户名那栏可以传空串或者你当前登录的Windows用户,具体要看PI服务器上的Trust配置。

AF SDK里面则是用PISystems集合:

using OSIsoft.AF.PI; PISystems systems = new PISystems(); PISystem piServer = systems["我的PIServer"]; piServer.Connect();

如果连接失败,多半是账号在PI里没有对应的映射,或者映射的权限不够(只能读快照不能读历史之类的)。我的排查经验是先到PI的数据库管理工具里看这个用户有没有对应MAP,再看它关联的Identity有没有勾选“读”相关的权限,这两步能解决大部分登录失败问题。

2.3 64位/32位进程的选择

这里值得单独拎出来说。PI SDK这个老COM组件有个很烦人的特性:同一个服务器上如果同时注册了32位和64位两个版本,那么你的C#程序编译目标Platform(x86/x64/AnyCPU)会直接影响能否正确实例化SDK。

我踩过的坑是:开发的时候AnyCPU编译,本机跑得好好的,因为开发机是64位Windows,默认优先加载64位COM。但把程序放到32位环境下测试,直接报“Retrieving the COM class factory for component with CLSID {...} failed”,原因是客户机器上只装了32位SDK,而程序用了AnyCPU在64位系统上会以64位进程运行,导致CLSID找不到。

所以我的经验法则是:项目里用了PI SDK,就把编译目标固定成和你部署环境一致(x86就x86,x64就x64),不要用AnyCPU;如果客户环境不确定,宁可目标x86,因为绝大多数Windows Server上32位COM兼容层都能跑,而且x86进程里可以加载32位和64位两套COM(前提是SDK装了32位)。

AF SDK就没这么闹心,它是托管的.NET程序集,基本上AnyCPU和x64都能正常加载。

3. 核心实操:C#读取PI实时数据的五种典型写法

3.1 用PI SDK读取实时快照值

先说最传统的PI SDK读实时值,这是很多老项目里最基础也是最常见的需求:给一个PI点(PI Tag)名称,立刻返回当前值。

using PISDK; PISDKCommon sdk = new PISDKCommon(); PISDKServer server = sdk.Servers.DefaultServer; server.Open("PIServer01", "domain\\user", "password"); PIPoint point = server.PIPoints["TAG_UPS_POWER_01"]; PIPointValue value = point.CurrentValue; object rawValue = value.Value; DateTime timeStamp = value.TimeStamp; int quality = value.Quality // 或 value.Status Console.WriteLine($"当前值: {rawValue}, 时间戳: {timeStamp}, 质量: {quality}");

这段代码看起来简单,但有几个坑。point.CurrentValue返回的PIPointValue包含值、时间戳、质量戳(Quality)。质量戳很重要,很多DCS设备断线或手动置坏值的时候,PI里会存一个IMPUTED(推算)或BAD状态值,如果你不做质量判断就直接拿来做业务判断,很可能在设备故障的时候给出一个错误的“正常”数据。

我习惯的做法是拿到值之后,先判断value.IsGood或者对比value.Status是否为“Good”,不是Good就直接丢弃或打标。生产项目里我遇到过把PI里的Bad值当成真实值写入报表的情况,结果一条产线的关键指标被算偏了,排查了大半天,最后发现是某个仪表信号断了,PI做了零值保持,但质量戳是Bad,而我们的报表逻辑没过滤质量。

另外,PIPoint的名称要注意大小写和全名。PI点表里的名称可能是TAG_UPS_POWER_01也可能是CDE:MEASUREMENT.POWER这种带CDE前缀的。找不到点的时候,直接PIPoints["xxx"]会抛异常,所以取点前可以先PIPoint.FindPIPoint并捕获异常,或者用PIPoints集合的Contains方法判断一下,虽然会多一次网络交互,但提高健壮性还是值得的。

3.2 用PI SDK读取历史数据区间

读取历史数据是更常见的业务需求,比如查某个测点过去24小时的变化趋势。

using PISDK; PISDKCommon sdk = new PISDKCommon(); PISDKServer server = sdk.Servers.DefaultServer; server.Open("PIServer01", "domain\\user", "password"); PIPoint point = server.PIPoints["TAG_REACTOR_TEMP"]; DateTime startTime = DateTime.Now.AddHours(-24); DateTime endTime = DateTime.Now; // 按时间区间获取记录,RetrievalMode 可以控制是快照还是归档值 PIPointValues values = point.RetrieveByTime(startTime, endTime, singleExpTime, RetrievalModeConstants.rtmInterpolated, 0);

RetrieveByTime的参数值得掰开讲一下。第一个和第二个是起止时间;第三个参数singleExpTime是期望的时间分辨率,单位秒,主要配合插值模式使用;第四个参数RetrievalModeConstants有几种模式:

  • rtmSnapshot:返回区间内的所有快照(变化但不落盘的值变化记录)和归档数据点。
  • rtmInterpolated:按指定的时间间隔(第三个参数)做线性插值,返回规整后的数据序列。适合画趋势曲线,因为DCS数据采样间隔不固定,直接画点会参差不齐。
  • rtmPlot:按时间区间返回适合绘图的“优化”数据(类似趋势图画的点和线),这种模式下返回的数据量会比插值小,因为PI引擎会自动抽稀。
  • rtmRecorded:只返回归档记录,不包含插值和推算值。

我的建议:画趋势图优先用rtmPlot,做统计计算(比如求平均值、累积量)用rtmInterpolated,做离线分析拿原始快照用rtmRecorded。如果你拿rtmInterpolated去补点,第三个参数singleExpTime一定要跟你的业务需求匹配。比如你希望每5分钟一个点,那singleExpTime就是300秒;你希望每1秒一个点,就是1秒,但要注意,PI Server上内含的插值数据量会暴涨,如果区间过大、间隔过小,性能会显著下降,甚至返回超时。

还有一个容易被忽视的问题:RetrieveByTime返回的数据边界是“包括起止时间点”的,这个和很多人的直觉不一样。如果你要做“上月1号到上月月底”这种左闭右开区间,必须在代码里对结束时间做微调(比如减去1秒或1毫秒),否则容易把下个月的0点数据也算进来。我在做月报统计时就吃过这个亏,首月和次月的数据重叠了一个点,月度总量差了不少,追踪半天才发现是区间边界问题。

3.3 用AF SDK读取实时值

AF SDK的思路跟PI SDK差异很大。AF SDK强调“数据资产”的概念,不只是裸点读值,而是通过AFElement、AFAttribute这样的模型来组织数据。刚接触的人可能会觉得绕,但用顺手之后,你会发现它的API写起来更自然,尤其适合面向工厂对象建模的场景。

一个最简单的AF SDK读实时值例子:

using OSIsoft.AF.PI; using OSIsoft.AF.Asset; PISystems systems = new PISystems(); PISystem piServer = systems.DefaultPISystem; piServer.Connect(); AFDatabase database = piServer.Databases["我的数据库"]; AFElement element = database.Elements["生产线A"]; AFAttribute attribute = element.Attributes["反应釜温度"]; AFValue current = attribute.GetValue(); Console.WriteLine($"值: {current.Value}, 时间: {current.Timestamp}, 状态: {current.Status}");

注意,AF SDK里的GetValue()拿到的AFValue其实不只是“点值”,它可以关联到AF属性上的PI点数据源,也可以关联到手动录入的静态配置值。如果你的AF属性配置得比较完整,这里拿到的数据就是有业务语义的。比如你要展示“三号反应釜的温度”,你用PI SDK要先去查这个反应釜对应哪个PI Tag,但AF SDK直接按设备层级往下找属性就行,对业务人员友好很多。

AF SDK读取PI点对应的实时值也支持直接指定PIPoint去读:

PIPoint piPoint = PIPoint.FindPIPoint(piServer, "TAG_REACTOR_TEMP"); AFValue afVal = piPoint.CurrentValue();

这里CurrentValue()返回的也是AFValue,它的.Value属性可能是null,尤其是点存在但没有任何数据的时候。处理的时候要养成判空习惯,不要一上来就Convert.ToDouble(),我见过不少同事在这里被NullReferenceException折磨。

3.4 用AF SDK读取历史快照与时间区间

AF SDK读历史区间比PI SDK更直接,AFAttribute有一个RecordedValues方法,可以直接按时间窗口拿归档数据:

using OSIsoft.AF.Asset; using OSIsoft.AF.Time; AFTime start = new AFTime("2024-01-01 00:00:00"); AFTime end = new AFTime("2024-01-02 00:00:00"); AFValues recorded = attribute.RecordedValues(start, end, AFBoundaryType.Inside, null, false); foreach (AFValue v in recorded) { Console.WriteLine($"{v.Timestamp}: {v.Value}"); }

RecordedValues的边界类型AFBoundaryType有Inside、Outside、Interpolated等。Inside意思是不包含边界值,取区间内部的记录;Outside会外扩到区间前后的最近点;Interpolated则是按指定时间间隔插值。

AF SDK在插值这块跟PI SDK的差异也要注意。AF SDK的InterpolatedValues方法是独立于RecordedValues的:

AFTime start = new AFTime("2024-01-01 00:00:00"); AFTime end = new AFTime("2024-01-02 00:00:00"); TimeSpan interval = TimeSpan.FromMinutes(5); AFValues interp = attribute.InterpolatedValues(start, end, interval, AFInterpolationMode.Continuous, null, false);

AFInterpolationMode.Continuous用于连续变化量(流量、温度),Step用于离散量(开关状态、计数器)。选错模式会导致拐点处的数值不准确,尤其是处理开关量状态变化的时候——你用Continuous去插值一个0到1跳变的开关量,中间可能会插出0.4、0.7这种根本不存在的值,业务上完全没法用。

这个地方我专门给做报表系统的同事提过醒:统计某个阀门开关状态在一天内的累计开启时长,一定要用Step模式,而且最好用RecordedValues先拿原始状态变化记录,再在内存里做时间区间累加,别指望插值函数给你精确结果。

3.5 用REST API做跨平台轻量读取

如果你不想在目标机器上装一堆SDK,也不想处理COM和.NET程序集的版本纷争,PI Web API是最省心的方案。它基于OData协议,直接用HTTP请求就能读数据,C#端只需要System.Net.Http。

一个典型的读实时数据HTTP请求长这样:

using System.Net.Http; using System.Net.Http.Headers; string baseUrl = "https://piserver.example.com/piwebapi"; string user = "admin"; string password = "password"; string webIdOrPath = "/points/PIPE_TAG_123"; using (HttpClient client = new HttpClient()) { // Basic认证或者基于Token的认证 var authToken = Encoding.ASCII.GetBytes($"{user}:{password}"); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(authToken)); string url = $"{baseUrl}/streams/{webIdOrPath}/value"; HttpResponseMessage response = await client.GetAsync(url); string json = await response.Content.ReadAsStringAsync(); }

REST API响应是JSON格式,里面包含Timestamp、Value、Good等字段。解析JSON你既可以用System.Text.Json,也可以用Newtonsoft.Json,看项目现有依赖情况。

用PI Web API要注意几个事情:

  • URL里的路径可以传WebId(PI系统内部唯一标识),也可以传带反斜杠的路径表达式,但路径表达式的URL编码要做对,否则会返回404。
  • 认证方式有三种:Basic认证、Anonymous、基于Token的OAuth2。企业内部系统图省事,很多人直接上Basic,但Basic认证每次请求都会把密码明文编码发出去,尽量用HTTPS,别用HTTP明文。为了安全,其实更推荐申请一个ClientId和ClientSecret,用OAuth2拿Bearer Token,这样服务端流转会更安全。
  • PI Web API有默认的数据返回条数和性能限制,比如一次最多返回多少条历史记录、历史区间最大跨度是多少。如果查询区间太大,可以直接通过recordedValues的swa参数微调,或者把查询拆成多段并行请求。

我自己最近一年的新项目中,凡是做Web后端或者数据中台的项目,都直接用PI Web API了,因为不需要在每个容器或服务器上装SDK,镜像构建也简单,起一个HttpClient实例就能通。不过如果你要读高频率、大批量的数据(比如几千个点同时取实时值),REST的JSON序列化开销还是比AF SDK直连要大不少,这时候我会推荐回到AF SDK做批量读取。

4. 性能调优与稳定性:真实项目里怎么把读取速度提上去

4.1 连接对象别反复创建,会话要复用

这个问题我看到太多次了。有人写了个方法,每次调用都new PISDKCommon()或new PISystems(),用完不释放,或者释放了下次再new。PI Server端每个连接都会建立会话和授权上下文,频繁建立和断开连接不仅慢,还会给PI服务器造成无谓的负担,甚至会因为服务器端连接数限制而导致后续连接被拒。

正确做法是:进程生命周期内维护一个全局的连接单例。PI SDK的模式是PISDKCommon常驻,PISDKServer打开后保持;AF SDK里则是PISystem对象常驻,通过AddRef()和Release()或者依赖.NET的Dispose机制来管理。实际项目里我通常是写一个静态类来管理连接,并且在程序退出时显式调用server.Close()或piServer.Disconnect()。

还有一点,PI SDK和AF SDK的连接都是“会话级”的,如果你在ASP.NET这种多线程环境里使用,要注意并发访问。PI SDK的PISDKCommon不建议直接跨线程共享无锁调用,最好加锁或者用连接池。AF SDK线程安全性稍好一些,但PISystem内部某些操作也不是完全线程安好,稳妥做法是每个线程持有自己的PISystem实例,但连接开销会增大,所以一般建议使用单例+锁的折中方案。

我在一个上位机服务里就是用ConcurrentDictionary维护多个PI Server的连接实例,配合锁来控制并发读写,跑了大半年,PI服务器端没报过连接泄漏。

4.2 批量读取比循环单点读取快一个数量级

很多人写代码喜欢这样:一个for循环,遍历50个点,每个点分别调用ReadValue。这个习惯在PI数据读取上是非常糟糕的。

PI SDK提供了ReadData或者PIPointList配合Server.ReadData这种批量接口,AF SDK则提供了对AFValues的批量读取方法,比如AFListData或者AttributeList的同步读取。一次批量读取可以一次请求拿到多个点的数据,网络往返次数从N次降到1次,性能提升非常明显。

我之前做过一个对比:读取100个点每个点的24小时历史数据,循环单点读取大约需要50秒,用批量读取之后,只用了3秒。这个量级的差距在需要频繁刷新的大屏展示项目里,直接影响用户体验。

AF SDK里批量读取一个典型写法如下:

using OSIsoft.AF.Asset; IList<AFAttribute> attrs = new List<AFAttribute>(); attrs.Add(element.Attributes["温度1"]); attrs.Add(element.Attributes["温度2"]); attrs.Add(element.Attributes["压力1"]); AFTime time = AFTime.Now; IDictionary<AFAttribute, AFValue> values = AFAttribute.GetValue(attrs, time);

这种批量API返回一个字典,键是AFAttribute,值是AFValue,拿到之后直接按属性名去取值。

再强调一下,批量读取不是把代码写整齐就行,关键在减少PI Server端的请求上下文切换。PI数据库引擎对单请求的处理能力是有上限的,你把请求合并,服务器端就可以用内部的批量查询优化器统一处理,性能自然就上去了。

4.3 数据质量判断:别把坏值当宝

前面我提到了质量戳,这里再展开说一下。PI的数据质量戳不是只有Good和Bad两种,常见的还有:

  • Good:正常采集值。
  • Bad:传感器断线、超量程、手动置坏。
  • Uncertain:采集质量不确定,比如仪表刚重新上电。
  • IMputed:推算值,可能是PI引擎在数据缺失时用相邻值插值出来的。

如果你的业务对数据真实性要求高(比如做KPI考核、能耗审计),建议在读取后只保留Good质量的数据,对Uncertain和IMputed的数据做特殊标注。质量戳在PI SDK里对应value.Status或value.Quality,在AF SDK里对应value.Status或AFQuality的枚举。

我的一个客户案例是:设备状态分析系统常常把PI的推算值当成真实运行状态,导致停机时长统计虚高。最后我们在读取层加了一个质量过滤,只统计Good的状态,结果数字反而跟现场记录对上了。这个教训说明,数据质量过滤不是可选项,是必备项。

4.4 时区问题:PI的时间语义和你本地时间不一样

PI Server存储的时间戳是服务器本地时间还是UTC,取决于PI Server的配置。多数情况下,PI Server的时间是服务器本地时区时间,而你的客户端可能在另一台机器上、另一个时区。如果不做时区转换,查询历史数据你会遇到“差8小时”的怪异现象。

我的习惯是:查询前先读取PI服务器的当前时间和客户端当前时间的差值,也就是做一个时间偏移量校正。AF SDK里,PISystem.CurrentTime返回服务器时间,你可以据此计算偏移。有些项目直接用UTC时间做接口约定,那就更清晰了——客户端把本地时间转成UTC传给PI,拿回来的时间戳也按UTC解析,展示的时候再转回本地时区。

还有夏令时问题,如果你们公司有海外工厂,PI服务器在夏令时切换时会有“重复一小时”或“缺失一小时”,处理历史区间时要做容错。这个坑比较隐蔽,但真实存在。

5. 常见问题排查:我把踩过的坑都列出来,免得你再踩一遍

5.1 连接失败:报“Unable to open PI server”或者登录失败

先检查网络通不通,用PI SMT(PI System Management Tools)测试连接同一台服务器,能通说明网络和PI服务正常;再看账号在PI安全映射里是否存在,看看映射的Identity有没有对应权限;排查PI Server端的Trust列表有没有禁止你这个IP。很多时候是PI的Trust管理太严格,限制了某个网段的访问。

如果开发环境能连,生产环境不能连,多半是生产环境的PI服务器没有配置开发账号的映射,需要在PI数据库管理工具里手动添加。这类问题我遇到的次数比代码问题还多。

5.2 找不到点:报“PIPoint not found”或者AF属性取不到值

先确认点全名称和源服务器前缀,比如CDE:TAG这种带CDE的,不能只写TAG。检查PI点的类型,有的PI点被配置成String或者Blob,你拿数值类型去接收会报类型不匹配。用AF SDK时,先确认AF数据库名和元素路径,很多人搞混了“数据库(Database)”和“库元素(Element)”,路径写错也会导致找不到属性。

5.3 时间范围报错:查询时间超出PI Archiving范围

PI的归档数据不是无限期的,磁盘空间、归档策略、压缩归档都会限制可查时间范围。如果查询区间太远古,PI会返回空数据或直接报错。这种情况如果业务必须查,可以联系PI管理员确认是否有长期存储的辅助归档库,或者把数据导出为历史文件再做离线查询。

5.4 进程异常退出:PI SDK的COM对象没有正确释放

COM引用计数管理是很多人容易忽略的。PIPoint、PIPointValue这些COM对象,用完了要Marshal.ReleaseComObject,或者在finally块里做释放。AF SDK是托管对象,但内部也可能引用COM对象,长时间运行的程序如果发现内存不断增长,要检查是不是每隔一段时间创建了新的PISystem没有释放。

写Windows服务项目时尤其要小心,服务常驻运行,一点内存泄漏都会被放大。我在一个服务里就是每5分钟建一个PISystem连接,跑了半年之后,内存占用从300MB涨到2GB,最后加了Dispose和GC.Collect才控制住。

5.5 32位/64位引发的问题

我前面提过,PI SDK有32位和64位版本。报错信息里如果出现“Class not registered”或“CLSID”,基本可以判断是位数不匹配。解决办法是:要么在目标机器上安装对应位数的SDK,要么把程序编译目标改到和SDK一致的位数。注意,一台机器上32位和64位的PI SDK可以共存,但COM选择加载哪个版本取决于进程位数。

5.6 并发冲突:PI的锁和事务冲突

如果你在同一个进程里多线程操作同一个PIPoint或同一个PISystem,有时会碰到“数据库被锁”之类的错误。PI Server有内部的并发控制,但客户端自己如果不加锁,还是会出现一些诡异的异常。解决方案很简单:线程访问PI资源时加一个全局锁,或者用单独的读取线程队列来串行化所有PI请求。高并发场景下,我会用一个独立的PI访问模块,内部维护一个请求队列,主线程只管发请求,底层串行或者用轻量并发池处理。

6. 选型建议:SDK、AF SDK和REST API到底怎么选

我把我的选型逻辑总结成表格,供参考:

方案适用场景优缺点
PI SDK老项目维护、传统PI点表操作稳定、API直接,但架构老,文档晦涩,COM组件部署麻烦
AF SDK新项目、面向资产模型、高性能批量读建模灵活、性能好、官方主推,但学习成本稍高
PI Web API跨平台、轻量集成、前后端分离无需SDK、部署简单,但性能和批量能力弱于SDK
OLEDB临时SQL查询、报表工具对SQL开发者友好,但功能有限,不推荐高频调用

如果你是一个新工控项目的负责人,我建议优先用AF SDK配合PI Web API的组合:主数据通道用AF SDK,对外暴露服务或者做Web接口时用PI Web API。这样既有性能保障,又不至于把客户端绑定死。如果你是在老项目里做增量开发,那PI SDK还是绕不开,但你要做好COM环境管理的心理准备。

从团队角度说,选型的时候还要考虑团队的技能储备。如果你团队里的人都只会写SQL,那PI OLEDB很可能比SDK更落地;如果你的团队是.NET Core技术栈,那PI Web API几乎是唯一选择,因为PI SDK在.NET Core里兼容性真的不好,AF SDK在新版里其实也主要面向.NET Framework,跨平台支持很有限。这点在做Linux容器化部署时尤其关键——要么用PI Web API,要么就得用第三方兼容方案,千万不要在项目中期才发现这个坑。

7. 一个完整的C#示例:AF SDK批量读取多个测点的历史数据

我贴一个我自己项目里用过的、相对完整的代码片段,包含了连接、批量读取、质量过滤、时区处理,你可以直接当作脚手架去改。

using System; using System.Collections.Generic; using System.Linq; using OSIsoft.AF.PI; using OSIsoft.AF.Asset; using OSIsoft.AF.Time; public class PiDataReader { private PISystem _system; public PiDataReader(string serverName, string username, string password) { PISystems systems = new PISystems(); _system = systems[serverName]; _system.Server.User = username; _system.Server.Password = password; _system.Connect(); } public void Close() { if (_system != null) { _system.Disconnect(); } } public Dictionary<string, List<DataPoint>> ReadHistory(string elementPath, List<string> attrNames, DateTime startUtc, DateTime endUtc, TimeSpan interval) { AFElement element = AFElement.FindElement(_system, elementPath); if (element == null) { throw new Exception($"找不到元素: {elementPath}"); } List<AFAttribute> attrs = new List<AFAttribute>(); foreach (string attrName in attrNames) { AFAttribute attr = element.Attributes[attrName]; if (attr != null) { attrs.Add(attr); } } var result = new Dictionary<string, List<DataPoint>>(); AFTime start = new AFTime(startUtc); AFTime end = new AFTime(endUtc); // 批量插值查询,返回的字典按属性维度组织 IDictionary<AFAttribute, AFValues> interpValues = AFAttribute.InterpolatedValues(attrs, start, end, interval, AFInterpolationMode.Continuous, null, false); foreach (var kvp in interpValues) { var list = new List<DataPoint>(); foreach (AFValue v in kvp.Value) { // 过滤质量,只保留Good值 if (v.Status.IsGood) { list.Add(new DataPoint { Timestamp = v.Timestamp.LocalTime, Value = v.Value }); } } result[kvp.Key.Name] = list; } return result; } } public class DataPoint { public DateTime Timestamp { get; set; } public object Value { get; set; } }

这段代码有几个地方我特意做了处理:AFElement.FindElement用的是元素路径而不是数据库内的显示名,要求你明确给定如\\PIServer01\Database\LineA\Reactor这样的路径;批量插值查询一次性处理多属性,极大减少了请求次数;质量戳判断放在最里层循环,确保每个数据点都是可信的。

值得说明的是,AFAttribute.InterpolatedValues的重载版本很多,我用的这个签名是一次性传多个属性、统一时间范围和统一插值间隔,适合多个测点用同一个时间网格的场景。如果你每个测点需要不同的时间间隔,那就要分开查,或者按属性分组循环查。

另外,这里的AFTime既支持DateTime传参,也支持字符串,DateTime.Now和DateTime.UtcNow看你PI服务器时区匹配。我建议在仓储层统一用UTC时间传参,展示层再转本地时间,能有效避免时区混乱。

8. 从“能跑”到“好用”:我在生产环境里沉淀的经验

数据读取这事,看起来就是“连一下,取个值”,但真正到了生产环境,你会发现需要处理的全是边界问题:连接断了怎么重连、PI服务器重启了怎么自动恢复、点表改了名称怎么兼容、历史数据迁移了怎么适配。

关于重连机制,我强烈建议不要自己裸写循环重连,而是利用AF SDK提供的事件和状态自动恢复。PI SDK和AF SDK在连接断开时,访问对象的方法会抛出异常,你可以捕获特定异常类型,然后等待几秒重连。生产环境里,PI服务器偶尔会维护重启,如果你没有一个健壮的重连机制,整个服务可能就卡死在异常里了。

我实际用的重连策略是:每次请求前检查连接状态(IsConnected),如果断开则重新Connect(),并且做最多3次退避重试(间隔1秒、2秒、4秒)。如果仍然失败,就记录日志并抛出业务异常,让上层调度知道当前数据不可用。这样能避免长时间阻塞在连接等待上。

关于点表兼容,如果你读取的PI点名称是配置表里的字符串,当PI点被重命名或迁移到新服务器时,程序会找不到点。我的做法是:定义一个数据字典层,把“业务信号名(如:反应釜温度)”映射到“真实PI点全名(如:TAG_RCT_TEMP_XXX)”,当点表变化时,只改配置,不用改代码。这个映射可以放数据库、配置文件或者PI系统的AF模型里,我偏好放AF模型里,因为AF属性本身就是干这个用的。

关于日志,每笔PI读取请求都应该记录:读取的PI点/属性、时间范围、读到的记录条数、耗时、是否有质量异常。在正式排查问题时,这些日志简直是无价之宝。我见过太多项目,出了问题只知道“查不到数据”,但没有日志无法判断是权限问题、网络问题还是PI点本身无数据。

关于性能监控,你最好在PI读取模块里加一个计数器,比如每分钟读取次数、平均响应时间、失败率。这个在用PI Web API或者AF SDK批量读取时很容易实现。一旦PI服务器响应变慢,你能第一时间从指标图上看到异常,而不是等用户投诉了才去排查。

最后,如果你想长期维护这套PI读取程序,我建议你把数据读取层独立成一个类库,对外暴露简洁的接口,不要让业务代码到处直接调PI SDK。这样当PI系统升级(比如从PI 3.x升级到PI AF),或者你需要把数据源从PI换成另一套历史库时,只需要改内部实现,业务层完全不用动。这种抽象在工控系统里尤其重要,因为DCS品牌和PI系统版本升级是迟早的事。

我参与了不止一个工厂信息化项目,几乎每个项目都会经历“数据源变了,代码要改”的痛苦。但凡是当初把读取层做独立的,改造都很快;凡是业务代码里穿插着几十处PI SDK调用的,最后都改得痛不欲生。所以请务必把“数据读取模块”当成一个正经的上游接口来设计,而不是随手写几个工具类就完事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 8:59:02

打卡第二天为何最难?从最小动作到补救预案的习惯养成法

早上6点30分的闹钟准时响了。我睁开眼睛&#xff0c;脑子里的第一反应不是起床&#xff0c;而是一句极其诚实的自我对话&#xff1a;"要不今天不打卡了吧。"这时候我才反应过来&#xff0c;我的连续打卡计划才刚刚进行到第二天。第一天立flag时的那种热血已经凉了大半…

作者头像 李华
网站建设 2026/10/21 8:58:50

三相PWM整流器FCS-MPC电流控制双闭环Simulink仿真模型搭建与调试

项目正文: 三相PWM整流器有限集模型预测电流控制Simulink仿真模型。使用直流侧电压外环PI、电流内环FCS-MPC控制结构&#xff0c;输出电压可调&#xff0c;双闭环控制&#xff0c;采样频率20kHz&#xff0c;功率等级10kW。关键词: 三相PWM整流器, 模型预测控制, 有限集, 电流控…

作者头像 李华
网站建设 2026/10/21 8:58:50

Wireshark抓包从入门到实战:安装、过滤器与TCP三次握手全解

打开 Wireshark 那一刻&#xff0c;绝大多数新手的表情是懵的&#xff1a;满屏的英文、不断滚动的数据包列表、密密麻麻的十六进制字节&#xff0c;完全不知道从哪里下手。这个被网络工程师当作“显微镜”的工具&#xff0c;其实远没有看起来那么神秘。抓包这件事&#xff0c;说…

作者头像 李华
网站建设 2026/10/21 8:58:14

自考毕业论文AI平台清单:10个位置按需用,写作提速一倍

这两年带自考学生改毕业论文&#xff0c;我发现一个特别普遍的现象&#xff1a;大家不是不会用AI论文平台&#xff0c;而是把平台用歪了——要么让AI一口气生成整篇全文直接交差&#xff0c;要么只会拿对话式AI问几个名词解释。真正把AI论文平台用对路子的学生&#xff0c;写作…

作者头像 李华
网站建设 2026/10/21 8:58:08

STM32 HAL库驱动AT24C02 EEPROM:从I2C时序到跨页写入实战

1. 项目到底做什么&#xff1a;一颗256字节的I2C存储芯片&#xff0c;凭什么值得专门写驱动做嵌入式项目最烦的瞬间之一&#xff0c;就是设备一断电&#xff0c;攒了很久的参数全没了。你辛辛苦苦调好的PID系数、设备编号、累计计数&#xff0c;全部清零&#xff0c;每次上电都…

作者头像 李华
网站建设 2026/10/21 8:57:33

Solon AI v3.9:Skill爆发与Agent协作,坚守Java8兼容

标题里那句“全能 Skill 爆发&#xff0c;Agent 协作更专业&#xff01;仍然支持 java8&#xff01;”&#xff0c;我第一反应是&#xff1a;Solon AI 终于把重心从“能对话”转到“能干活”了。这版 v3.9 我从预览阶段就跟进&#xff0c;升完级后把手上两个老项目都跑了一遍。…

作者头像 李华