ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C#读取PI系统实时数据:Web API与SDK实战指南

C#读取PI系统实时数据:Web API与SDK实战指南

我做过不少工业数据接入的活儿,发现很多刚接触这一块的人,一听到“PI系统”就觉得是个高不可攀的庞然大物,尤其是要用C#去对接实时数据时,更是无从下手。有些人翻遍官方文档,结果被一堆SDK概念砸得头晕;有些人写的程序跑起来,死活读不到数据,折腾半天才发现是权限问题。

这篇东西就是把我自己踩过的坑、摸索出来的路子整理出来。不管你是要给MES系统供数,还是做设备监控大屏的数据源,或者是搞历史趋势分析,只要你是用C#去读PI实时数据,这篇文章都能给你一条能直接落地的路径。我会从系统原理讲到代码实现,再聊到性能优化和问题排查,尽量让你读完脑子里有一个清晰的地图。

1. 先搞清楚PI系统里的数据长什么样

没搞懂PI的数据模型之前,你写代码就像在黑暗里摸开关,所以咱们先花点时间把底层模型弄清楚,这是后面所有操作的基础。

1.1 PI所谓“实时数据”到底是什么

PI是一个专门用来采集海量时序数据的工业数据平台,核心部件是PI Data Archive,你可以把它想象成一个对“写”操作极度优化的时序数据库。工厂里的PLC、DCS把电流、温度、压力、流量这些信号通过采集器送进来,PI负责把这些数值按照时间戳归档存储。

它跟普通关系数据库最大的不同在于,它以时间为轴,一个测点(PI Tag/Point)就对应一整条随时间变化的数值曲线。你关心的不是某个瞬间某个字段的值,而是“温度这个测点在过去24小时内的变化轨迹”。所以PI里的数据不是一行一行的记录,而是一个一个按时间排序的“时间戳-数值-质量码”三元组。

我见过不少工程师一开始习惯性地想“用SQL去查PI”,但PI根本不认SQL。哪怕PI后来提供了SQL查询的接口,那也是一个时间序列函数,跟你理解的关系型SQL完全是两码事。所以,把思维从“查表”切换成“拿一条时间曲线”,这一步非常关键。

1.2 测点、快照和历史归档的关系

每一个PI测点(比如某条产线的冷却水温度T-101)其实包含两类数据:

  • 快照(Snapshot):测点最新一次的采集值。它能直接告诉你“阀门现在开到多少”“电机当前转速是多少”。
  • 历史归档(History/Archive):不断滚动写入的事后数据,存放了从采集至今的所有历史采样值,随着时间推进,这些数据会迁移到归档库。

在C#里读取的时候,这两类数据的访问逻辑差别明显。快照数据你直接取一次当前值,几毫秒就返回了,适合做监控界面;历史数据你得指定时间范围,交给PI服务器去压缩历史存储里边取数,耗时几秒甚至更久,适合做分析报表。写代码前先想清楚需求是哪一种,否则会走不少弯路。

PI还有一个很重要的概念——事件压缩与异常处理(Exception and Compression)。简单说,PI在存储时并不是把所有收到的原始点全存下来,它会按偏差和压缩算法的规则去判断,只有值得记录的变化才写入。因此你读取到的历史数据,在时间轴上是不均匀分布的——这也解释了为什么我需要专门在后面跟你聊“插值读取”和“按原始快照读取”的差别。

2. 两种主流的C#读取方案,怎么选

现在市面上用C#读PI,绕不开两条路:老牌的PI SDK,和后起的PI Web API。绝大部分困惑来源,就是在这两条路之间徘徊。

2.1 传统PI SDK:曾经的唯一选择,坑也不少

PI SDK是一套基于COM组件的开发包,C#里要引用DLL,走.NET Framework。它的优点是代码写起来非常直白——打开服务器、找到测点、取数据——所有逻辑都是对象化的,性能也相当不错,因为它走的是PI私有的高性能通讯协议(底层的消息传递机制是PI专用的)。

但它的缺点同样明显:

  1. 32位与64位油水分离。C#编译平台选错了,哪怕差一位,DLL注册不上那直接报错,非常折磨人。
  2. 强依赖Windows平台。你想在Linux容器里读取PI?这个方案直接出局。
  3. 排错黑盒。SDK报错时经常是一个笼统的错误代码,经常需要你靠经验去猜,排查相当痛苦。
  4. 踩坑成本高。每一次环境更换、系统重装,COM组件能不能正常注册就是个“看脸”的事情。

但说实话,如果你维护的是2010年左右就很成熟的老项目,而且不需要跨平台,那PI SDK仍然是可靠的选择,毕竟那些遗留系统已经跑得很稳定了。

2.2 现代PI Web API:跨平台与敏捷开发的首选

PI Web API是PI系统的RESTful风格接口,它把读数的能力完全封装成HTTP请求。你在C#里管它叫不叫“PI”都不重要,只要你会发HTTP GET,会解析JSON,就能把它搞定。

它有几个我非常看重的优势:

  • 跨平台:整条链路只要你网络能通,就能取数,不管是Windows,还是Linux,还是macOS。
  • 免安装:不需要部署任何COM依赖,只需要一个Base URL和合适的认证。
  • 调试方便:你甚至能直接在浏览器里输入URL测试返回JSON,这在排查问题的时候救命。
  • 语法清爽:数据读取的语义集中在URL路径和查询参数上,可读性和可维护性远胜于SDK那些复杂的对象模型。

如果你是全新项目,或者你的服务需要容器化部署,我强烈建议你选PI Web API。这也是目前业界新一代对接的主流方式。

2.3 我给选型的基本建议

我一般给团队的建议很直接:

  • 服务端批量拉历史数据、做大数据分析 → 选PI Web API,方便、干净、易排查。
  • 老项目的就地维护、且只能跑在Windows内网并追求极致响应速度 → 保留PI SDK。
  • 数据规模极大、对传输性能极度敏感、且项目是紧密型Windows集成 → PI SDK可能更合适;否则一律用PI Web API。

没有任何一种方案是“银弹”,想清楚你的部署环境和数据量需求,再动手,比什么都重要。

3. 实操记录:用PI Web API从零读取实时数据

接下来,我们看最核心的实战环节。我以一个具体例子带你走一遍,在这个例子里,我用一个虚拟的工厂PI服务器(假设地址是pi-server.yourcompany.local,这里仅作示例说明)来演示。所有的代码思路,你拿去换成自己环境里的地址就能跑。

3.1 确认你的PI Web API地址和认证方式

做任何事之前,先摸清楚PI Web API的入口。一般来说,系统管理员会给你一个形如以下的基础地址:

https://pi-server.yourcompany.local/piwebapi

拿到地址之后,一定要先在浏览器里测试一下。如果弹窗要求输入域账号,说明走的是Windows集成认证;如果不弹窗直接返回JSON,那多半是配置了匿名访问或独立账号。

我用一个非常小的C#控制台程序演示整个流程。这个程序用到了最基础的HttpClient,不需要引第三方库。代码如下:

using System.Net.Http.Headers; // 请配置你的PI Web API地址 string baseUrl = "https://pi-server.yourcompany.local/piwebapi"; // 测点全名,比如某工厂的压力测点 string piTag = "PLANT1.PRESSURE.P101"; // 输入时间范围,ISO 8601格式 string startTime = "2024-12-01T00:00:00+08:00"; string endTime = "2024-12-01T01:00:00+08:00";

这里有个重点,PI Web API对时间格式要求是严格的ISO 8601,并且必须带上时区偏移,如果你忽略这个细节,后面分分钟被各种时间偏差折磨。

3.2 封装一个通用读取方法

一个成熟的调用逻辑,最好封装成函数。我写了这么一段核心方法,负责去请求PI Web API的interpolated数据端点:

public static async Task<string> GetPiInterpolatedData( string baseUrl, string tag, DateTime startUtc, DateTime endUtc, string interval) { // 将时间统一转为UTC,再格式化为ISO 8601 string start = startUtc.ToUniversalTime().ToString("o"); string end = endUtc.ToUniversalTime().ToString("o"); // 构造读取URL,这里用了interpolated,你可以按需换成recorded等 string url = $"{baseUrl}/streams/{tag}/interpolated?startTime={start}&endTime={end}&interval={interval}"; using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String( System.Text.Encoding.ASCII.GetBytes("pi_user:pi_password"))); var response = await client.GetAsync(url); response.EnsureSuccessStatusCode(); string json = await response.Content.ReadAsStringAsync(); return json; }

注意这段代码里的test user和test password是示意用的,实际环境中,你可能会需要写一个服务账号。另外,HttpClient不建议在每次请求时都用using创建,你会发现连接不够用,端口耗尽的问题来了。正确的做法是在整个应用程序生命周期里只保留一个HttpClient实例——只要用static字段或者依赖注入的方式保持单例就行。

3.3 把JSON变成C#对象

接口返回的JSON长下面这样:

{ "Items": [ { "Timestamp": "2024-12-01T00:00:00+08:00", "Value": 2.51, "Good": true }, { "Timestamp": "2024-12-01T00:05:00+08:00", "Value": 2.55, "Good": true }, { "Timestamp": "2024-12-01T00:10:00+08:00", "Value": 2.42, "Good": false }, ... ] }

直接对JSON下手太累了,我先定义一个模型类:

public class PiStreamValue { public string Timestamp { get; set; } public object Value { get; set; } public bool Good { get; set; } } public class PiStreamResult { public List<PiStreamValue> Items { get; set; } = new List<PiStreamValue>(); }

然后反序列化:

using System.Text.Json; var data = JsonSerializer.Deserialize<PiStreamResult>(json); foreach (var item in data.Items) { Console.WriteLine($"{item.Timestamp} => {item.Value} (Quality: {item.Good})"); }

这里你一定要留意图中的Good字段,它不是摆设。如果某个测点处于检修状态或者采集异常,Good会是false,而Value可能是一个没有任何语义的数值,比如-9999。不判断质量码直接拿数值参与运算,是工业项目里最容易出事故的坑之一。

好,到这里,你已经能读数据了。但真实的工业项目远远不止“能读出来”,很多性能、数据完整性的问题还在后面等着你。

4. 深入PI SDK方案:老项目里的稳健选择

虽然Web API很好用,但老项目里你仍会碰到PI SDK。我做过的某老工厂能源管理系统,PI SDK跑得“骨头发黄”,但你完全替换掉它成本太高,不值当。所以我用一个简短的SDK示例,帮你快速捡回手感。

4.1 添加引用和环境准备

在Visual Studio里,右键项目引用,在COM分类下找到“PI SDK”的组件并勾选。它内部有几千个对象,但我们常用的就那么几个。

也要敲黑板提醒一下:SDK版本的注册,是全局COM注册,你必须要确保部署环境已经安装好PI SDK并注册成功。否则你代码写得没问题,运行时一样是“未能加载文件或程序集”。

4.2 核心代码:连服务器、找测点、读趋势

下面这段展示了PI SDK读取数据的完整流程:

// 这里的PISDK引用来自PI SDK COM组件 PISDKClass piSdk = new PISDKClass(); // 连接PI服务器,请改为你的实际服务器名 object connectOpts = null; PIServer server = piSdk.Servers["pi-server-name"]; server.Connect(connectOpts, true); // 获取测点对象 PIData dataPoint = server.PIData["PLANT1.PRESSURE.P101"]; // 定义时间范围 DateTime start = DateTime.Now.AddDays(-1); DateTime end = DateTime.Now; // 使用PITimeSeries读取带时间戳的数据 PITimeSeries series = dataPoint.RecordedValues(start, end, PITime.TimeZone.UTC, "") as PITimeSeries; foreach (object item in series) { var valueRec = (PIValue)item; DateTime ts = valueRec.TimeStamp; object val = valueRec.Value; Console.WriteLine($"{ts:G} => {val}"); } server.Disconnect();

这看着似乎比Web API还简单,是不是?但PI SDK的坑不在代码里,在环境里。如果你编译的目标平台是AnyCPU会导致COM组件载入时报错,你得显式改成X64或者把Prefer 32-bit关掉;还有运行时,如果你跑的环境根本装不了SDK,那程序直接整个废掉,这都让我碰到过不止一次。

4.3 SDK和Web API在性能上的体感差异

有一个实际的对比经验:在同时拉取24小时、1分钟插值的300个测点时,PI SDK直接在服务端把数据压缩后发给客户端,耗时基本在秒级;而PI Web API如果按单个测点一个个请求,那很可能会卡上十几秒。

但这个差距是可以被“并发拉取”弥补的——Web API构造成多个Task并发请求300个测点,性能一样能接受。所以不用盲目迷信旧技术,好的设计能缩小差距。

5. 性能优化与数据压缩的实战心得

读到数据只是起点,读得高效却是另一个层面的工作。真实生产环境每天产生的数据量惊人,如果代码写得粗糙,你的程序就会成为被人诟病的“取数慢”的替罪羊。

5.1 先把“插值读取”和“原始读取”分个清楚

PI Web API提供了几个不同的读取入口,最容易混淆的就是这两个:

  • Recorded(按原始事件):把所有经过压缩算法保留下来的历史采样点原样返回。点与点之间的时间间隔不一定均匀,但最能反映原始真实变化。
  • Interpolated(按周期插值):由PI服务器自动按你指定的时间间隔做线性插值,让数据变成均匀的时间序列。

我记得刚入手时吃过大亏。当时我要做一个报表需求,明明想用interpolated,结果手滑用了recorded,返回了几十万条高密度事件,把下游文件生成直接打挂了。所以,要先想清楚你的业务到底需要什么。画趋势曲线用interpolated;做精确回放、事故分析用recorded。

5.2 拉大时间跨度的正确姿势

PI Web API单次请求返回的数据数量是有限制的,超出限制就会被分页截断。官方默认的页面大小可能是几百条到几千条不等,具体由服务端配置决定。如果读取几天甚至几个月的数据,代码里必须处理分页机制。

细读返回的响应头,你会发现里面带了个链接指向下一页。确保你的拉取逻辑能跟随这个链接继续拿数据,拼装到一个统一的大列表里,而不是傻傻地只取第一页数据,否则你拿到的“完整数据”只是冰山一角。

5.3 多测点合批与并发控制

为了减少网络往返,PI Web API还支持在同一个请求路径里用逗号分隔多个测点去批量取数。这个方法能明显减少HTTP连接开销。另一个思路是并发——但不要无限开线程,通常我设置为8~16的并发度,拉取几百个测点的时候,效果比串行快了数倍。

经验值表明,并发拉取时,把每个请求的数据量控制住,别让一个任务长时间霸占连接,同时给httpClien.Timeout设置一个合理的上限,比如10分钟——否则大数据量查询超时的瞬间,你会看到一堆TimeoutException。

5.4 PI自身的存储压缩,是朋友不是敌人

值得强调的一点是,PI存储数据时会做异常压缩。你读到的点数是过滤后的“关键事件”,这大大节省了传输时间和流量。如果你非要原始高速采集数据,通过SDK取数可能是更合理的办法,因为SDK能直连数据源,而Web API走的是上层服务。

当你的数据量是真的大时,我个人建议尽可能把“计算”推向PI服务器,例如让PI Web API直接做聚合、求平均值、算最大值,而不必先把几十万条原始记录拉到C#里再自己用LINQ慢慢算。

6. 高频问题排查实录

最后这部分,是实时数据接入这条路上最让人头疼的环节。我整理了一些高频故障场景和处理思路,希望能帮你少走弯路。

6.1 “坏的请求”与URL编码问题

一上来就是Bad Request,多数是因为测点名没处理对。PI测点名往往含有点号或特殊字符,URL里点号本身是合法的,但如果你传带有中文或&符号,就必须进行URL编码。尤其注意:测点名的完整字段在某些PI配置里自带反斜杠或空格,编码这一环就容易翻车。

正确做法是使用Uri.EscapeDataString()对整个测点名做转义后再拼接URL。我见过有人不管三七二十一直接拿原始测点名拼URL,结果返回的始终是400,排查半天才恍然大悟。

6.2 认证问题:401、403与登录循环

这是最常见的拦路虎。有时浏览器打开正常,一到C#里就401/403,核心原因通常是:

  • 你没有正确处理默认凭证。调用Web API时,如果你希望走Windows集成认证,那C#客户端需要配置HttpClientHandler.UseDefaultCredentials = true,否则它不会主动带上你的Windows登录凭证。
  • 如果你使用Basic认证,请确保用户名密码是独立的服务账号,并确认该账号对目标测点有读取权限。
  • 如果是匿名访问,那BaseURL本身要确认IIS里确实开了匿名认证,而外部请求没有去凭猜测的认证头。

如果手边有Postman之类的工具,可以先在那里做连通测试。我在排查问题的时候,最怕的是“部署环境”和“测试工具”的认证机制不一致,导致代码在本地跑得很顺,一到服务器就各种401。

6.3 时区偏移不一致导致的时间盲区

时间问题在工业数据领域极其刺眼。PI服务端内部往往用UTC存储时间,但你的应用可能在东八区。返回的JSON时间戳如果选择带偏移格式,那没问题;如果你按ISO 8601解析却没注意+08:00的偏移,实际记录的UTC时间就是对不上号的。

经验是:客户端全链统一使用UTC时间,只在展示层做时区转换。按这个原则,所有请求参数、中间变量、存储逻辑都用UTC,只在finally输出到报表时转成当地时区,彻底根治时间混乱。

6.4 大量数据请求超时

很多小伙伴在读取数月历史数据时,代码写到socket读了一会儿就没下文。我前面的内容里也提过,一是单次查询数据量太大,二是HttpClient里的超时设置太小。

建议的排查顺序是:

  1. 先缩小时间范围,验证服务端响应时长。
  2. 确认是否翻页逻辑没有跟上,导致客户端以为数据没完。
  3. 合理设置超时时间;另外务必把HttpClient做单例,避免不断地挥手断开和新建连接。

下面我做了一个精简表格,方便你快速定位:

错误现象可能原因快速解法
HTTP 400URL未编码、时间参数格式错转义测点名,检查ISO8601格式
HTTP 401/403认证机制不匹配配置UseDefaultCredentials或Basic账号
返回数据稀疏用了Interpolated但周期没设对检查时间间隔参数
响应超时单次请求数据量过大分段拉取、增加分页处理、并发控制
内容不全未处理分页跟随NextPage链接
COM异常加载失败SDK平台位数不匹配统一X64或X86,重新注册DLL
偶发连接失败HttpClient频繁创建耗尽端口全局单例HttpClient

6.5 抖动数据与质量码的坑

最后一个严重但最少人提的:过滤数据时别只看数值范围,还得看质量码。PI中测点质量有Good、Questionable、Bad等多种状态。如果系统采集异常,PI会把历史上的数据标成Bad,但数值本身不会自动消失。你用常规“去极值”的逻辑过滤噪声,是根本剔不掉坏点的。

因此,写数据清洗逻辑时,第一步永远是把质量不好的数据标记或剔除,再谈统计计算。一套规范的工业数据接入服务,质量码判断这些细节,往往直接决定了推送给你的上层系统数据是否可靠。

我个人在实际项目里的做法是:在读取层就把质量码转换成统一的布尔标识,后续数据仓库只要识别这个标识就可以全局过滤,这样能保证数据流下游“一根筋”地干净,避免业务侧各自临时判断,造成口径不一致。

最后再分享一个小技巧:无论你用Web API还是SDK,先在浏览器或Postman里把返回JSON看明白再写解析代码。很多人写完解析逻辑才发现实际返回结构和预期不一致,白写一通。先对数据长什么样建立感知,再敲键盘,效率至少提高一半。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进