本教程详细探讨了在 php 中使用 lepiafSerialPort 库进行串口通信时,read 方法可能导致的阻塞问题。通过分析库的内部实现,我们发现即使在非阻塞模式下,read 方法仍会无限等待分隔符。文章提供了一种修改库源代码以引入超时机制的解决方案,并指导如何在应用层优雅地处理串口读取超时,确保系统稳定性。
理解 PHP 串口通信中的阻塞问题
在使用 php 进行串口通信,特别是与外部硬件(如树莓派上的按钮输入)交互时,一个常见且关键的需求是等待特定数据的到来。然而,如果外部设备长时间没有响应,或者数据格式不符合预期,程序可能会无限期地等待串口数据,从而导致整个进程阻塞,甚至因为达到 php 的最大执行时间而终止。
原始代码尝试通过一个 while (true) 循环结合 time() 函数来手动实现超时机制:
// ... 串口初始化代码 ... $timeoutStart = time(); $timeout = $timeoutStart + 15; // 15 秒超时 $aborted = false; while (true) { $data2 = $this->serialPort->read(); // 关键的阻塞点 if (Str::contains($data2, "Whatever I want to check for")) { // 处理数据并跳出循环 break; } // 检查是否超时 if (time() >= $timeout) { $this->serialPort->write("C,STOP "); // 发送停止命令 $aborted = true; $this->alert("vending sequence stopped"); break; // 超时跳出循环 } }
尽管代码中包含了超时检查逻辑,但实际运行中发现,即使超时时间已到,循环也可能“突然停止”或根本不执行超时逻辑。这通常是因为 $this->serialPort->read() 方法本身是一个阻塞调用,它会在没有数据到来或未检测到分隔符时无限期地等待,从而阻止了 time() 检查的执行。当 read() 方法被阻塞时,PHP 脚本会暂停,直到数据到达或达到 PHP 的 max_execution_time 限制而被系统强制终止。
深入分析 lepiafSerialPort 库的 read 方法
要解决这个问题,我们需要理解 lepiafSerialPort 库的内部工作机制。通过查看其源代码,特别是 SerialPort.php 文件中的 read 方法,我们可以发现:
public function read() { $this->ensuredeviceOpen(); $chars = []; do { $char = fread($this->fd, 1); // 从文件描述符读取一个字符 // ... 其他逻辑 ... } while ($char !== $this->getParser()->getSeparator()); // 循环直到找到分隔符 return $this->getParser()->parse($chars); }
尽管 lepiafSerialPort 库可能将串口流设置为非阻塞模式,但其 read 方法内部的 do…while 循环却是一个阻塞点。它会不断地从串口读取单个字符,直到遇到配置的分隔符为止。这意味着,如果串口长时间没有数据,或者没有发送分隔符,这个 read 方法将永远不会返回,从而导致外部的超时逻辑无法执行。
立即学习“PHP免费学习笔记(深入)”;
解决方案:修改 read 方法以引入超时机制
最直接有效的解决方案是修改 lepiafSerialPort 库的 read 方法,使其能够接受一个超时参数,并在指定时间内未接收到分隔符时返回。
请编辑 vendor/lepiaf/serialport/src/lepiaf/SerialPort/SerialPort.php 文件,找到 read 方法(通常在第 107 行左右),并将其修改为以下形式:
public function read($maxElapsed = 'infinite') { $this->ensureDeviceOpen(); $chars = []; // 计算超时时间点,使用 microtime(true) 获取微秒级时间戳 $timeout = $maxElapsed == 'infinite' ? 1.7976931348623E+308 : (microtime(true) + $maxElapsed); do { $char = fread($this->fd, 1); // 尝试读取一个字符 if ($char === '') { // 如果没有读取到字符(非阻塞模式下) if (microtime(true) > $timeout) { return false; // 超时,返回 false } usleep(100); // 短暂休眠,避免 CPU 占用过高 continue; // 继续循环等待 } $chars[] = $char; // 读取到字符,添加到缓冲区 } while ($char !== $this->getParser()->getSeparator()); // 循环直到找到分隔符 return $this->getParser()->parse($chars); // 解析并返回数据 }
修改说明:
- $maxElapsed 参数: 新增一个 $maxElapsed 参数,用于指定最大等待时间(秒)。默认值为 ‘infinite’,表示无限等待。
- $timeout 计算: 根据 $maxElapsed 计算一个精确的超时时间点。microtime(true) 用于获取当前的微秒级时间戳,以实现更精确的超时控制。
- 超时判断: 在 fread($this->fd, 1) 返回空字符串(表示当前没有数据可读,因为流是非阻塞的)时,会立即检查当前时间是否已超过 $timeout。如果超时,则立即返回 false。
- usleep(100): 当没有数据可读且未超时时,程序会短暂休眠 100 微秒。这可以有效降低 CPU 占用,避免空转。
- 返回 false: 超时时返回 false,作为一种明确的超时信号。
在应用代码中实现带超时的读取
修改库文件后,您就可以在自己的应用代码中调用带有超时参数的 read 方法,并根据其返回值进行相应的处理:
// ... 串口初始化代码 ... // 尝试读取串口数据,设置 15 秒的超时时间 $data2 = $this->serialPort->read(15); if ($data2 === false) { // 发生了超时,可以执行相应的错误处理或回滚操作 $this->serialPort->write("C,STOP "); // 发送停止命令 $aborted = true; $this->alert("vending sequence stopped due to timeout"); // 这里可以进行 API 调用来回滚之前的操作 // 例如:$this->apiClient->revertSomeAction(); } elseif (Str::contains($data2, "Whatever I want to check for")) { // 成功读取到数据,并且包含了期望的字符串 // 处理数据并继续流程 $this->alert("Received expected data: " . $data2); } else { // 成功读取到数据,但未包含期望的字符串 $this->alert("Received data, but not expected: " . $data2); // 可以选择继续等待或采取其他措施 }
通过这种方式,您的应用程序可以更健壮地处理串口通信,避免因外部设备无响应而导致的程序阻塞。
注意事项与总结
- 修改第三方库的风险: 直接修改 vendor 目录下的库文件存在风险。当您运行 composer update 时,这些修改可能会被覆盖。为了避免此问题,您可以考虑:
- Fork 库: 将 lepiafSerialPort 库 Fork 到自己的仓库,进行修改,然后在 composer.json 中引用您的 Fork 版本。
- 扩展类: 如果库的设计允许,可以创建一个新的类来继承 lepiafSerialPortSerialPort 并覆盖 read 方法。然而,对于这种底层的文件描述符操作,直接覆盖可能更复杂。
- 补丁文件: 使用 cweagans/composer-patches 等工具来管理对 vendor 文件的补丁。 鉴于 lepiafSerialPort 库的简单性,并且此处修改是核心功能增强,直接修改并记录下来是一个快速解决方案,但长期项目应考虑更健壮的维护策略。
- usleep() 的作用: usleep(100) 的目的是在没有数据可读时,让 CPU 短暂休眠,而不是进行忙等待,从而降低 CPU 占用。100 微秒是一个经验值,可以根据实际情况调整。
- 错误处理: 始终检查 read 方法的返回值。false 表示超时,其他值表示成功读取到的数据。
- PHP 的限制: 尽管引入了超时机制,PHP 仍然不是进行硬实时通信的最佳选择。对于对时间精度要求极高的应用,可能需要考虑使用 C/c++ 等语言。
- 分隔符的重要性: lepiafSerialPort 库依赖于分隔符来判断一条消息的结束。确保您的硬件设备在发送数据时包含正确的分隔符,或者调整解析器配置。
通过上述修改和实践,您可以在 PHP 应用中实现一个可靠的串口读取超时机制,有效提升系统的稳定性和用户体验。