Практические методы взаимодействия с последовательными портами в PHP
Взаимодействие с последовательными портами из PHP открывает возможности управления периферийными устройствами: микроконтроллерами, весами, принтерами чеков и промышленным оборудованием. В статье рассмотрены различные способы работы с COM-портами: от готовых библиотек до встроенных функций.
Основное решение: библиотека PHP-Serial
Библиотека php-serial (Xowap/PHP-Serial) предоставляет удобный объектно-ориентированный интерфейс для работы с последовательными портами на Windows, Linux и macOS. Установка через Composer:
composer require xowap/php-serial
Пример открытия порта, настройки параметров и записи с чтением:
<?
require 'vendor/autoload.php';
use PhpSerial\Serial;
$serial = new Serial();
$serial->deviceSet('/dev/ttyUSB0');
$serial->confBaudRate(9600);
$serial->confParity('none');
$serial->confCharacterLength(8);
$serial->confStopBits(1);
$serial->deviceOpen();
$serial->sendMessage('AT\r\n');
$response = $serial->readPort();
echo 'Ответ: ' . $response;
$serial->deviceClose();
?>
Библиотека автоматически подбирает системные вызовы. Цель: быстрая разработка с минимальными усилиями. Случаи использования: создание шлюзов, управление Arduino, чтение показаний датчиков.
Возможные проблемы и решения
- Ошибка открытия порта – не хватает прав на устройство. Решение: добавить пользователя в группу dialout (Linux) или
sudo chmod 666 /dev/ttyUSB0. - Библиотека не найдена – не установлен Composer или не выполнен
composer install. - Порт занят – другое приложение (MobaXterm, screen) использует порт. Освободить порт.
Вариант 1. Как использовать встроенные функции fopen на Linux?
Цель: работа без внешних зависимостей. Случай: простые скрипты на сервере с ограниченными правами.
Открытие файла устройства и настройка через утилиту stty:
<?
$port = '/dev/ttyUSB0';
shell_exec("stty -F $port 9600 cs8 -cstopb -parenb 2>&1");
$fp = fopen($port, 'w+');
stream_set_blocking($fp, false);
fwrite($fp, 'AT\r\n');
usleep(100000);
$response = stream_get_contents($fp);
fclose($fp);
echo $response;
?>
Проблемы
- Неверная настройка stty – параметры могут не примениться. Проверить через
stty -F $port -a. - Блокировка – без
stream_set_blocking(false)скрипт зависнет при отсутствии данных.
Вариант 2. Как подключиться к COM-порту на Windows?
Цель: интеграция с Windows‑приложениями. Случай: унаследованные системы, торговое оборудование.
Использование команды mode и fopen:
<?
$port = 'COM3:';
shell_exec("mode $port BAUD=9600 DATA=8 PARITY=N STOP=1 &");
$fp = fopen($port, 'w+');
fwrite($fp, 'AT\r\n');
usleep(100000);
$response = fread($fp, 100);
fclose($fp);
echo $response;
?>
Типичные ошибки
- Порт не открывается – недостаточно прав или отсутствует драйвер. Запустить PHP от администратора.
- mode не возвращает ошибку – команда может выполниться «молча». Проверить
execс захватом вывода.
Вариант 3. Как использовать расширение DIO?
Цель: низкоуровневый контроль параметров линии. Случай: встроенные системы с поддержкой DIO.
Устанавливается через pecl install dio. Пример настройки и записи:
<?
$fd = dio_open('/dev/ttyS0', O_RDWR);
dio_tcsetattr($fd, array(
'baud' => 9600,
'bits' => 8,
'stop' => 1,
'parity' => 0
));
dio_write($fd, 'AT\r\n');
$data = dio_read($fd, 256);
dio_close($fd);
echo $data;
?>
Ограничения
- Не поддерживается на Windows – DIO доступен только *nix.
- Устаревшее расширение – нет поддержки PHP 8, возможны сбои.
Вариант 4. Как использовать библиотеку symfony/serial-port?
Цель: совместимость с экосистемой Symfony. Случай: проекты на Symfony, требующие инъекции зависимостей.
Установка composer require symfony/serial-port и пример:
<?
use Symfony\Component\SerialPort\SerialPort;
$port = SerialPort::create('/dev/ttyUSB0', 9600);
$port->write('AT\r\n');
$response = $port->read(1024);
echo $response;
?>
Возможные трудности
- Зависимости – библиотека требует php-process.
- Отсутствие поддержки Windows – на Windows может потребоваться дополнительный адаптер.
Расширенные примеры работы с COM-портами
Пример 1: Чтение данных с цифрового термометра (DS18B20) через php-serial на Linux.
<?
require 'vendor/autoload.php';
use PhpSerial\Serial;
$serial = new Serial();
$serial->deviceSet('/dev/ttyUSB0');
$serial->confBaudRate(9600);
$serial->confParity('none');
$serial->confCharacterLength(8);
$serial->confStopBits(1);
$serial->deviceOpen();
// Отправка запроса температуры
$serial->sendMessage("TEMP\r\n");
// Ожидание ответа (200 мс)
$serial->readPort(); // пропустить эхо
usleep(200000);
$temperature = trim($serial->readPort());
echo "Температура: $temperature °C\n";
$serial->deviceClose();
?>
Температура: 23.45 °C
Пример 2: Отправка команды на Arduino с подтверждением через fopen и stty.
<?
$port = '/dev/ttyACM0';
shell_exec("stty -F $port 115200 cs8 -cstopb -parenb 2>&1");
$fp = fopen($port, 'w+');
if (!$fp) die('Не удалось открыть порт');
stream_set_blocking($fp, false);
// Команда включения светодиода
fwrite($fp, "LED_ON\n");
// Ждём 100 мс и читаем ответ
usleep(100000);
$answer = stream_get_contents($fp);
echo "Ответ: $answer\n";
fclose($fp);
?>
Ответ: OK
Пример 3: Работа с COM-портом на Windows через exec и mode.
<?
$port = 'COM4:';
// Настройка порта командой mode
$output = shell_exec("mode $port BAUD=9600 DATA=8 PARITY=N STOP=1 2>&1");
echo "Настройка: $output\n";
$fp = fopen($port, 'w+');
if (!$fp) die('Ошибка открытия COM-порта');
fwrite($fp, "*IDN?\r\n");
usleep(200000);
$idn = fread($fp, 100);
echo "Идентификация: $idn\n";
fclose($fp);
?>
Настройка: Идентификация: Keysight 34461A, MY57201234, A.02.08-02.40
Пример 4: Настройка паритета и стоп-битов с помощью DIO.
<?
$fd = dio_open('/dev/ttyS1', O_RDWR | O_NOCTTY);
if (!$fd) die('Не удалось открыть порт');
$conf = [
'baud' => 19200,
'bits' => 7,
'stop' => 2,
'parity' => 1 // 1 - нечетность, 0 - четность
];
dio_tcsetattr($fd, $conf);
dio_write($fd, "DATA\n");
$reply = dio_read($fd, 50);
echo "Получено: $reply\n";
dio_close($fd);
?>
Получено: ACK
Пример 5: Неблокирующее чтение с помощью stream_select.
<?
$port = '/dev/ttyUSB0';
shell_exec("stty -F $port 9600 cs8 -cstopb -parenb");
$fp = fopen($port, 'r+');
stream_set_blocking($fp, false);
$read = [$fp];
$write = null;
$except = null;
// Отправить команду
fwrite($fp, "POLL\r\n");
// Ожидать данные до 1 секунды
if (stream_select($read, $write, $except, 1) > 0) {
$data = stream_get_contents($fp);
echo "Получены данные: $data\n";
} else {
echo "Таймаут - нет данных\n";
}
fclose($fp);
?>
Получены данные: 123456