PHP通过COM组件连接OPC DA服务器读取数据

Nora
Nora
发布时间:2026-10-24 | 7人浏览过
原创

在工业物联网与老旧产线数字化改造中,大量传感器、PLC和智能仪表的数据仍被集中汇聚在传统的 OPC DA(Data Access)服务器中。通过在 Windows 环境下启用 PHP 的 com_dotnet 扩展,并利用 OPC 基金会官方发布的自动化包装组件 OPCDAAuto.dll,开发者可以直接利用 PHP 的 COM 接口创建 OPC 客户端,实现对工控测点数据的同步或异步读取。本文将详细讲解该技术方案的架构原理、环境依赖准备、DCOM 安全策略配置、核心代码实现及高频异常排障方法。

一、技术架构与通信机理

OPC DA 规范是建立在微软 OLE/COM 和 DCOM 技术基础之上的工业通信标准。由于原生 OPC DA 规范主要定义的是 C/C++ 风格的自定义接口(Custom Interface),非托管高级语言或脚本语言难以直接映射调用。为此,OPC 基金会推出了自动化接口包装器(Automation Wrapper,即 OPCDAAuto.dll),该组件将底层的复杂接口转换为符合 OLE 自动化规范的双重接口(Dual Interface),暴露出标准的 OPCServer、OPCGroups 和 OPCItems 等调度对象。

PHP 解释器通过内置的 ext/com_dotnet 扩展模块,能够充当 OLE/COM 客户端,在 Windows 注册表中查找对应的 ProgID(通常为 OPC.Automation 或 OPC.Automation.1),实例化该自动化对象,再由该组件通过进程内调用或跨进程的 RPC 通信向目标 OPC DA 服务器(如 Kepware KEPServerEX、Matrikon OPC Server)请求测点数据。

PHP通过COM组件连接OPC DA服务器读取数据
图 1:PHP 与 OPC DA 服务器通信与系统架构层级

二、环境准备与核心依赖项

在开始编码之前,必须确保 Windows 操作系统中已正确部署了所有中间件,并且 PHP 解释器的架构配置与 COM 组件完全匹配。很多初学者在初次调用时遇到 Class not registered (0x80040154) 异常,根本原因就在于环境准备阶段的架构不一致。

1. 确认 PHP 运行环境与架构位数

OPC 基金会提供的标准 OPCDAAuto.dll 主要是 32 位(x86)编译的 COM 动态链接库。根据 Windows 进程模型,32 位的动态链接库无法直接加载进 64 位(x64)的进程地址空间中。因此,最稳妥的做法是安装 32 位 Windows 版本的 PHP(例如 PHP 7.4 或 PHP 8.x 的 x86 Thread Safe 或 Non Thread Safe 版本)。若生产环境必须采用 64 位 PHP,则必须在 Windows 组件服务中建立 COM+ 应用程序代理或配置 DCOM 包装器,增加了系统复杂性。

2. 启用 PHP 的 COM 扩展

打开当前 PHP 目录下的 php.ini 配置文件,搜索并定位到 extension=com_dotnet 配置项,移除行首的分号 ;。同时建议确认以下 COM 运行参数:

; 开启 COM 与 .NET 支持扩展
extension=com_dotnet

[COM]
; 允许调用 COM 对象的类型库常量
com.autoregister_typelib = true
; 遇到 COM 异常时抛出致命错误或 PHP 异常
com.throw_exception = true
; 字符编码设定,避免中文测点乱码
com.code_page = "UTF-8"

修改完成后,在命令行执行 php -m,若输出列表中包含 com_dotnet,则说明扩展已成功加载。

3. 注册 OPCDAAuto.dll 自动化包装组件

确保系统中存在 OPCDAAuto.dll。该文件通常随各类工业软件(如 Kepware、Matrikon)一同安装,也可从 OPC Core Components 分发包中提取。对于 64 位 Windows 系统,32 位的 COM 组件应存放在 C:\Windows\SysWOW64\ 目录下。

以管理员身份打开 Windows 命令提示符(CMD),执行注册命令:

cd C:\Windows\SysWOW64
regsvr32.exe OPCDAAuto.dll

当系统弹窗提示“DllRegisterServer in OPCDAAuto.dll succeeded.”时,说明组件已成功写入 Windows 注册表,ProgID OPC.Automation 和 OPC.Automation.1 正式生效。

PHP通过COM组件连接OPC DA服务器读取数据
图 2:从环境准备到数据提取的操作实施流程

三、DCOM 安全策略与访问控制配置

OPC DA 采用 DCOM 进行身份验证和权限校验。在局域网内或即使在同一台机器上以 Web 账户(如 IUSR、NETWORK SERVICE)运行时,若权限未放开,就会抛出 0x80070005 (Access is denied) 错误。配置 DCOM 是保障 PHP 稳定读取数据的前提:

  • 打开组件服务控制台:按下 Win + R,输入 dcomcnfg 并回车,展开“组件服务” -> “计算机” -> “我的电脑”。
  • 配置全局默认属性:右键“我的电脑”选择“属性”,在“默认属性”选项卡中,确保勾选“在此计算机上启用分布式 COM”;“默认身份验证级别”设为连接,“默认模拟级别”设为标识。
  • 配置全局安全:在“默认安全”选项卡中,分别进入“访问权限”和“启动和激活权限”的“编辑默认值”,添加用户 Everyone、Interactive、System 以及运行 Web 服务的账户,勾选全部允许。
  • 配置目标 OPC Server 的 DCOM 属性:在“DCOM 配置”分支下,找到对应的 OPC 服务端(例如 Kepware.KEPServerEX.V6)和 OPCEnum。右键选择“属性”:
    • 在“安全”选项卡中,将“启动和激活权限”、“访问权限”均改为“自定义”,并为当前运行用户赋予“本地启动”、“本地激活”、“本地访问”权限。
    • 在“标识”选项卡中,若 PHP 以 CLI 脚本在当前登录桌面运行,可选择交互式用户;若以 Windows 服务或后台任务运行,务必选择下列用户并指定具体的系统管理员账户和密码,切勿保持默认的“启动用户”,否则后台调用时会因没有桌面会话而挂起。

四、OPC 自动化对象模型与调用关系

在编写 PHP 代码前,理解 OPCDAAuto.dll 提供的对象树模型至关重要。整个通信结构呈现自上而下的树状容器关系:

PHP通过COM组件连接OPC DA服务器读取数据
图 3:OPCDAAuto 核心对象层次与方法调用拓扑

五、完整代码实现与关键步骤解析

下面给出一段完整的 PHP 脚本,演示如何从连接 OPC Server、建立 Group、添加 Item、同步读取数据、解析状态,到最终安全释放全部 COM 资源。代码具有生产级别的容错机制与显式异常捕获。

<?php
/**
 * PHP 通过 COM 组件连接 OPC DA 服务器读取数据示例
 * 运行环境:Windows x86 / 启用 ext-com_dotnet 扩展
 */

// 1. 设置运行参数与 OPC 目标信息
$serverProgId = "Kepware.KEPServerEX.V6"; // 或 "Matrikon.OPC.Simulation.1"
$serverNode   = "127.0.0.1";               // 本地服务器填 127.0.0.1 或留空,远程填目标主机 IP
$tagId        = "Channel1.Device1.Tag1";    // 目标测点完整 ItemID

// 数据源常量:1 代表 OPCCache(内存缓存),2 代表 OPCDevice(穿透到底层物理设备)
define('OPC_DS_CACHE', 1);
define('OPC_DS_DEVICE', 2);

$opcServer = null;
$groups    = null;
$group     = null;
$items     = null;
$item      = null;

try {
    echo "[1] 正在实例化 OPC.Automation 组件...\n";
    // 实例化自动化核心对象
    $opcServer = new COM("OPC.Automation") or die("无法创建 OPC.Automation 组件,请检查 DLL 是否注册!\n");

    echo "[2] 正在连接目标 OPC DA 服务器: {$serverProgId}...\n";
    // Connect 方法接受两个参数:Server 名称和 Node(节点名/IP)
    $opcServer->Connect($serverProgId, $serverNode);

    // 检查连接后的服务器状态:1 代表 OPC_STATUS_RUNNING
    $state = $opcServer->ServerState;
    echo "    连接成功,服务器当前运行状态码: {$state}\n";

    echo "[3] 正在创建测点逻辑组 (OPCGroup)...\n";
    $groups = $opcServer->OPCGroups;
    // 设置全局默认刷新率 (毫秒)
    $groups->DefaultGroupUpdateRate = 1000;
    
    // 添加一个自定义命名的组
    $group = $groups->Add("PhpCollectorGroup_" . uniqid());
    $group->IsActive = true;       // 激活组
    $group->IsSubscribed = false; // 同步轮询模式下关闭事件订阅以节省带宽

    echo "[4] 正在向组内添加测点 (OPCItem): {$tagId}...\n";
    $items = $group->OPCItems;
    // AddItem 接受 ItemID 和 ClientHandle (客户端句柄)
    $clientHandle = 1001;
    $item = $items->AddItem($tagId, $clientHandle);

    if (!$item) {
        throw new Exception("添加测点失败,请检查 ItemID 是否存在!");
    }

    echo "[5] 开始执行同步数据读取 (SyncRead)...\n";
    /**
     * 方式 A:直接通过 OPCItem 对象的属性直接访问(适合单点快速读取)
     * 此时由底层组件隐式读取缓存
     */
    $valueDirect     = $item->Value;
    $qualityDirect   = $item->Quality;
    $timestampDirect = $item->TimeStamp;

    echo "    [方式 A 读取] 结果:\n";
    echo "    测点值 (Value): " . (is_array($valueDirect) ? json_encode($valueDirect) : $valueDirect) . "\n";
    echo "    品质码 (Quality): {$qualityDirect} (" . getQualityDescription($qualityDirect) . ")\n";
    echo "    时间戳 (TimeStamp): " . date("Y-m-d H:i:s", (int)$timestampDirect) . "\n";

    /**
     * 方式 B:显式调用 Read 方法(可指定穿透到设备 OPCDevice 或读取缓存 OPCCache)
     */
    $val = null;
    $quality = null;
    $timeStamp = null;

    // 参数 1: Source (OPC_DS_DEVICE 强制从设备读取)
    $item->Read(OPC_DS_DEVICE, $val, $quality, $timeStamp);

    echo "    [方式 B 设备穿透读取] 结果:\n";
    echo "    测点值 (Value): " . (is_array($val) ? json_encode($val) : $val) . "\n";
    echo "    品质码 (Quality): {$quality} (" . getQualityDescription($quality) . ")\n";
    echo "    时间戳 (TimeStamp): " . date("Y-m-d H:i:s", (int)$timeStamp) . "\n";

} catch (com_exception $e) {
    echo "[!] COM 异常报错:\n";
    echo "    Code: " . dechex($e->getCode()) . "\n";
    echo "    Message: " . iconv("GBK", "UTF-8//IGNORE", $e->getMessage()) . "\n";
} catch (Exception $e) {
    echo "[!] 业务执行异常: " . $e->getMessage() . "\n";
} finally {
    echo "[6] 正在执行资源清理与 COM 句柄释放...\n";
    // 必须按照由子到父的逆序释放并断开连接,防止 COM 服务挂死
    try {
        if ($items && $item) {
            // 移除特定测点 (通过服务器分配的 ServerHandle)
            $serverHandles = new VARIANT([$item->ServerHandle]);
            $errors = null;
            // 亦可直接让 group->OPCItems 清空
        }
        if ($groups && $group) {
            $groups->RemoveAll(); // 移除所有组
        }
        if ($opcServer) {
            $opcServer->Disconnect(); // 断开与服务器的会话
        }
    } catch (Exception $ex) {
        // 捕获清理过程中的偶发连接断开异常
    }

    // 显式将 COM 对象赋值为 null,通知 Zend 引擎降低内部引用计数
    $item      = null;
    $items     = null;
    $group     = null;
    $groups    = null;
    $opcServer = null;

    echo "    资源释放完毕,通信会话已关闭。\n";
}

/**
 * 辅助函数:根据 OPC DA 标准品质位解析状态文本
 */
function getQualityDescription($qualityCode) {
    // 0xC0 (192) 代表 Good (数据良好)
    // 0x00 代表 Bad (通信失败或设备断开)
    // 0x40 (64) 代表 Uncertain (不确定/传感器超限)
    $majorQuality = $qualityCode & 0xC0;
    switch ($majorQuality) {
        case 192:
            return "Good (有效数据)";
        case 0:
            return "Bad (通信故障或测点离线)";
        case 64:
            return "Uncertain (数据不可信/超量程)";
        default:
            return "Unknown (品质码: {$qualityCode})";
    }
}

核心代码细节与语法要点说明

1. OPCCache 与 OPCDevice 的选择边界

在调用 $item->Read($source, ...) 时,参数 $source 的取值对系统吞吐量有直接影响。OPCCache (1) 读取的是 OPC Server 内部维护的最新内存镜像,耗时通常在 1ms 以内,不会给现场总线增加负荷;OPCDevice (2) 则会强迫 OPC Server 立即向底层 PLC 发起实际的总线轮询,在多测点、高并发调用下极易引起通信拥塞或超时。因此常规监控应优先使用 OPCCache,仅在校验瞬态控制动作时采用 OPCDevice。

2. COM 变体(VARIANT)与 SafeArray 的类型转换

PHP 的 com_dotnet 模块会自动把 COM 的基础标量类型(如 VT_I4、VT_R8、VT_BSTR)转换为 PHP 对应的 int、float、string。但对于数组型测点(如西门子 PLC 中的 Byte 数组或数据块),COM 返回的是 SafeArray,在 PHP 中会被映射为一个只读的 Array 或者 variant 对象。此时应使用 variant_get_type() 辅助判定,或者通过 foreach 迭代读取各个元素。

3. 必须由内到外显式释放 COM 引用

PHP 脚本在执行完毕后虽然会触发垃圾回收,但在 CLI 长进程(常驻守护进程)或多次 HTTP 请求中,若未显式调用 $opcServer->Disconnect() 并将全部 COM 变量赋值为 null,OPC Server 端维护的客户端连接数(Client Handles)与内存将无法释放,最终导致 OPC Server 句柄耗尽崩溃。

六、常见报错分析与排障决策树

在实际工程部署中,开发者常常会遇到各种十六进制错误码。下表列出了 PHP-OPC 通信中最典型的报错及其对策:

十六进制错误码 标准描述 引发根因 精准排查与修复步骤
0x80040154 Class not registered (类没有注册) PHP 位数与 DLL 位数冲突;或 OPCDAAuto.dll 根本未在当前系统中注册。 1. 检查 php -v 确认是 32位 还是 64位。
2. 若为 64位系统且运行 32位 DLL,需在 SysWOW64 目录下以管理员执行 regsvr32。
0x80070005 E_ACCESSDENIED (访问被拒绝) Windows DCOM 权限校验受阻。当前执行 PHP 的账户没有启动或访问 OPC Server 的权限。 1. 打开 dcomcnfg。
2. 将目标 OPC 服务与 OPCEnum 的启动、激活与访问权限授予 Everyone。
3. 在标识选项中指定具体管理员用户。
0x80004005 E_FAIL (未指明的错误) OPC Server 尚未启动,或指定的 ServerProgID 拼写错误。 1. 检查注册表 HKEY_CLASSES_ROOT 下是否存在该 ProgID。
2. 确保在连接前手动启动 OPC 服务端主程序排查。
0xC0040007 OPC_E_UNKNOWNITEMID (未知的测点名) 向组内添加 Item 时,指定的测点路径(ItemID)在 PLC 中不存在。 1. 使用 Kepware Quick Client 或 Matrikon Explorer 浏览测点树。
2. 核对通道名、设备名与点名层级(如 Channel1.Device1.Tag1)。
PHP通过COM组件连接OPC DA服务器读取数据
图 4:PHP-OPC DA 核心排错决策树与生产环境实践矩阵

七、总结与生产落地建议

通过 PHP 的 com_dotnet 扩展结合 OPCDAAuto.dll 自动化组件,开发者可以在无需引入 C# 或 C++ 中转网关的前提下,快速实现对工业 OPC DA 服务器的数据读取。在项目落地过程中,务必牢记以下三条核心实践准则:

  1. 统一架构位数:坚决保持 PHP 解释器与 OPCDAAuto.dll 同为 32 位(x86),杜绝混合位数导致的加载与注册表解析崩溃。
  2. DCOM 预先授权:在部署阶段将 OPCEnum 和目标 OPC Server 的 DCOM 标识固定为特定本地管理用户,规避系统后台服务运行时的无桌面会话挂起。
  3. 读写架构解耦:杜绝在 Web HTTP 响应中直接发起 OPC 同步连接;采用 PHP CLI 守护进程采集并入库 Redis,由 Web 端读取缓存的高可用架构。
相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表