Initial commit

This commit is contained in:
2025-03-20 22:40:01 +01:00
commit 3546cfde30
7 changed files with 602 additions and 0 deletions

View File

@ -0,0 +1,144 @@
<?php
namespace NoccyLabs\React\Serial;
use Evenement\EventEmitterTrait;
use React\EventLoop\Loop;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
use React\Stream\DuplexStreamInterface;
use React\Stream\ReadableStreamInterface;
use React\Stream\WritableStreamInterface;
class LineBufferedDuplexStream implements ReadableStreamInterface, WritableStreamInterface
{
use EventEmitterTrait;
const EVT_DATA = 'data';
const EVT_END = 'end';
const EVT_ERROR = 'error';
const EVT_CLOSE = 'close';
const EVT_DRAIN = 'drain';
const EVT_PIPE = 'pipe';
const EVT_LINE = 'line'; // when a full line has been received
const EVT_PROMPT = 'prompt'; // when a matching prompt has been received
const EVT_OUTPUT = 'output'; // every line event until a prompt event, if $bufferOutput
private string $buffer = '';
private array $output = [];
public function __construct(
private readonly DuplexStreamInterface $stream,
private readonly ?string $promptPattern = null,
private readonly string $eol = "\n",
private bool $bufferOutput = false,
)
{
$stream->on('data', $this->onData(...));
$stream->on('end', $this->onEnd(...));
$stream->on('error', $this->onError(...));
$stream->on('close', $this->onClose(...));
$stream->on('drain', $this->onDrain(...));
$stream->on('pipe', $this->onPipe(...));
}
private function onData($data): void
{
// Pass on the data event as is
$this->emit(self::EVT_DATA, [ $data ]);
$this->buffer .= $data;
// Parse out any lines and emit events
while (($pos = strpos($this->buffer, $this->eol)) !== false) {
$line = substr($this->buffer, 0, $pos);
$this->buffer = substr($this->buffer, $pos + 1);
$this->emit(self::EVT_LINE, [ $line ]);
if ($this->bufferOutput) {
$this->output[] = $line;
}
}
// Check if the buffer matches the prompt pattern
if ($this->promptPattern !== null) {
if (preg_match($this->promptPattern, $this->buffer)) {
$this->buffer = preg_replace($this->promptPattern, '', $this->buffer);
if ($this->bufferOutput) {
$this->emit(self::EVT_OUTPUT, [$this->output]);
$this->output = [];
}
$this->emit(self::EVT_PROMPT, []);
}
}
}
private function onEnd(): void
{
$this->emit(self::EVT_END, [ ]);
}
private function onError($error): void
{
$this->emit(self::EVT_ERROR, [ $error ]);
}
private function onClose(): void
{
$this->emit(self::EVT_CLOSE, []);
}
private function onDrain(): void
{
// FIXME check parameters
$this->emit(self::EVT_DRAIN, []);
}
private function onPipe(): void
{
// FIXME check parameters
$this->emit(self::EVT_PIPE, []);
}
public function isReadable(): bool
{
return $this->stream->isReadable();
}
public function pause(): void
{
$this->stream->pause();
}
public function resume(): void
{
$this->stream->resume();
}
public function pipe(WritableStreamInterface $dest, array $options = []): WritableStreamInterface
{
// TODO
}
public function close(): void
{
$this->stream->close();
}
public function isWritable(): bool
{
return $this->stream->isWritable();
}
public function write($data): bool
{
return $this->stream->write($data);
}
public function end($data = null): bool
{
return $this->stream->end($data);
}
}

17
src/Parity.php Normal file
View File

@ -0,0 +1,17 @@
<?php
namespace NoccyLabs\React\Serial;
use Evenement\EventEmitterTrait;
use React\EventLoop\Loop;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
use React\Stream\ReadableStreamInterface;
use React\Stream\WritableStreamInterface;
enum Parity:string {
case NONE = '-parenb';
case EVEN = 'parenb -parodd';
case ODD = 'parenb parodd';
};

43
src/SerialFactory.php Normal file
View File

@ -0,0 +1,43 @@
<?php
namespace NoccyLabs\React\Serial;
use Evenement\EventEmitterTrait;
use React\EventLoop\Loop;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
use React\Stream\DuplexResourceStream;
use React\Stream\ReadableStreamInterface;
use React\Stream\WritableStreamInterface;
class SerialFactory
{
/**
*
*
* @param string $device
* @param int $baud
* @param int $bits Bits per byte
* @param Parity $parity
* @param int $stop Stopbits
* @return PromiseInterface<DuplexResourceStream>
*/
public function open(string $device, int $baud = 9600, int $bits = 8, Parity $parity = Parity::NONE, int $stop = 1): PromiseInterface
{
$deferred = new Deferred();
Loop::futureTick(static function () use ($device, $baud, $bits, $parity, $stop, $deferred) {
$cmd = "stty -F ".escapeshellarg($device)." ".$baud." ".$parity->value." ".escapeshellarg("cs".$bits)." ".($stop==1?"-cstopb":"cstopb")." -echo cbreak min 0 time 0";
//echo $cmd."\n";
exec($cmd);
$fd = fopen($device, "a+");
$stream = new DuplexResourceStream($fd);
$deferred->resolve($stream);
});
return $deferred->promise();
}
}