PHPackages                             hamzi/portflow - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. hamzi/portflow

ActiveLibrary[API Development](/categories/api)

hamzi/portflow
==============

Neural bridge package connecting Laravel applications with local and IoT hardware using Web Serial and clean driver abstractions.

v0.7.0(1mo ago)464MITPHPPHP ^8.2CI passing

Since May 3Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/hamdyelbatal122/PortFlow)[ Packagist](https://packagist.org/packages/hamzi/portflow)[ RSS](/packages/hamzi-portflow/feed)WikiDiscussions master Synced 1w ago

READMEChangelog (9)Dependencies (15)Versions (11)Used By (0)

PortFlow
========

[](#portflow)

[![CI](https://github.com/hamdyelbatal122/PortFlow/actions/workflows/ci.yml/badge.svg)](https://github.com/hamdyelbatal122/PortFlow/actions/workflows/ci.yml)[![Latest Version on Packagist](https://camo.githubusercontent.com/5b891dce7d83fa60c16696eded9c19330654983eb0b87c57435815208c3e47d1/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f68616d7a692f706f7274666c6f772e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/hamzi/portflow)[![PHP Version](https://camo.githubusercontent.com/fca6a5abe8cb8ca5a09d7514f79421a5acfc883e66c5e71627c5051291b2c4ce/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e322532422d626c75653f7374796c653d666c61742d737175617265)](https://php.net)[![Laravel](https://camo.githubusercontent.com/f52ff4c0e480eee4dad30a33fe93a5ab5c51100fb19a743dcc97737417ca1db1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d3131253246313225324631332d7265643f7374796c653d666c61742d737175617265)](https://laravel.com)[![License: MIT](https://camo.githubusercontent.com/1b01ef0024ba0866c115986b895301f657c1b21fc29f05c4844b7f2e8d89204d/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d79656c6c6f772e7376673f7374796c653d666c61742d737175617265)](LICENSE)

> **Neural bridge for Laravel × Hardware.** Connect thermal printers, IoT sensors, RS-232 scales, barcode scanners, and any serial device to your Laravel application — all through a clean, driver-based architecture and the browser's Web Serial API.

---

Contents
--------

[](#contents)

- [What is PortFlow?](#what-is-portflow)
- [Architecture](#architecture)
- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Available Drivers](#available-drivers)
    - [RAW / JSON (ESP32, Arduino, IoT)](#raw--json-driver)
    - [Barcode Line Driver (USB/TTL Scanners)](#barcode-line-driver)
    - [RFID ASCII Driver (STX/ETX Readers)](#rfid-ascii-driver)
    - [Fingerprint Packet Driver (Binary UART)](#fingerprint-packet-driver)
    - [ESC/POS (Thermal Printers)](#escpos-driver)
    - [RS-232 (Scales, Legacy Devices)](#rs-232-driver)
- [Creating a Custom Driver](#creating-a-custom-driver)
- [Web Serial Integration](#web-serial-integration)
    - [Backend Direct Serial Mode](#backend-direct-serial-mode)
    - [Auto-Reconnect](#auto-reconnect)
    - [Browser Compatibility](#browser-compatibility)
- [Hardware → Events &amp; Eloquent](#hardware--events--eloquent)
    - [Queue-Based Routing](#queue-based-routing)
- [Thermal Printing Engine](#thermal-printing-engine)
- [IoT Frame Buffering](#iot-frame-buffering)
    - [Buffer Persistence Across Requests](#buffer-persistence-across-requests)
- [Blade &amp; Livewire Components](#blade--livewire-components)
- [Security](#security)
- [Configuration Reference](#configuration-reference)
- [Testing](#testing)
- [Contributing](#contributing)
- [License](#license)

---

What is PortFlow?
-----------------

[](#what-is-portflow)

Most Laravel packages live entirely in software. PortFlow does not.

It bridges **physical hardware** (printers, scales, sensors, microcontrollers) with Laravel's event system and database, translating raw serial bytes into typed, routable `SerialFrame` DTOs that your application can consume like any other event.

```
Browser (Web Serial API)
       │
       │  POST /portflow/ingest  { driver: "raw-json", chunk: "..." }
       ▼
Laravel (IngestController → PortFlowManager → DriverRegistry)
       │
       ├── parse inbound bytes → SerialFrame[]
       │
       ├── route synchronously or via Queue → Laravel Events
       │
       └── persist to Eloquent models

```

---

Architecture
------------

[](#architecture)

PortFlow follows Clean Architecture — domain logic is completely isolated from infrastructure:

```
src/
├── Domain/
│   ├── Contracts/
│   │   ├── SerialDriver.php          ← Interface all drivers implement
│   │   └── SerialEvent.php           ← Marker interface for domain events
│   ├── DTO/
│   │   └── SerialFrame.php           ← Immutable parsed-frame value object
│   ├── Events/
│   │   └── ProductScanned.php        ← Example domain event
│   └── Services/
│       └── IoTFrameBuffer.php        ← Byte-stream accumulation buffer
│
├── Application/
│   ├── Jobs/
│   │   └── RouteSerialFrameJob.php   ← Queueable frame routing job
│   └── Services/
│       ├── DriverRegistry.php        ← Resolves driver instances
│       ├── HardwareMessageService.php← Orchestrates ingest / encode
│       └── MessageRouter.php         ← Routes frames → Events / Eloquent
│
├── Infrastructure/
│   ├── Drivers/
│   │   ├── RawJsonDriver.php         ← ESP32, Arduino, MQTT payloads
│   │   ├── BarcodeLineDriver.php     ← Barcode scanners (ASCII line mode)
│   │   ├── RfidAsciiDriver.php       ← STX/ETX RFID ASCII readers
│   │   ├── FingerprintPacketDriver.php ← Binary UART fingerprint modules
│   │   ├── EscPosDriver.php          ← Thermal printers + barcode scanners
│   │   └── Rs232Driver.php           ← RS-232 scales and legacy devices
│   ├── Http/Controllers/
│   │   └── IngestController.php      ← POST endpoint consumed by JS bridge
│   ├── Livewire/
│   │   ├── PortFlowConnector.php     ← Tracks port connection state
│   │   └── PortFlowStatus.php        ← Real-time status display
│   └── Printing/
│       ├── EscPosBuilder.php         ← Fluent ESC/POS byte builder
│       └── BladeEscPosRenderer.php   ← Renders Blade → ESC/POS bytes
│
├── Console/Commands/
│   ├── MakeDriverCommand.php         ← php artisan portflow:make-driver
│   └── ListenSerialCommand.php       ← php artisan portflow:listen
│
├── Exceptions/
│   └── PortFlowException.php
├── Facades/
│   └── PortFlow.php
└── PortFlowServiceProvider.php

```

---

Requirements
------------

[](#requirements)

DependencyVersionNotesPHP`^8.2`PHP 8.3+ recommended for Laravel 13Laravel`^11.0 | ^12.0 | ^13.0`All actively supported versionsLivewire`^3.0`> **Browser support for Web Serial API:** Chrome 89+, Edge 89+. Not supported in Firefox or Safari.

---

Installation
------------

[](#installation)

```
composer require hamzi/portflow
```

Publish the config file:

```
php artisan vendor:publish --tag=portflow-config
```

Publish the JavaScript bridge (optional):

```
php artisan vendor:publish --tag=portflow-assets
```

Publish driver stubs (optional — for customising the `make:driver` template):

```
php artisan vendor:publish --tag=portflow-stubs
```

---

Quick Start
-----------

[](#quick-start)

**1. Add the JS bridge to your layout:**

```

```

**2. Drop in the Livewire connector component:**

```

```

The connector now bootstraps sensible defaults from `config/portflow.php` automatically, including the ingest URL, default driver, and baud rate. Override them from Blade when needed instead of rendering a select box for the end user.

If `:baud-rate` is explicitly passed in Blade, PortFlow gives it priority over remembered localStorage values.

Useful Blade props:

```

```

When `auto-connect-on-load` is enabled, PortFlow asks the browser for already-authorized serial devices with `navigator.serial.getPorts()` and reconnects automatically after reload when permission still exists.

**3. Listen for hardware events in your application:**

```
use Hamzi\PortFlow\Domain\Events\ProductScanned;

class HandleBarcodeScan
{
    public function handle(ProductScanned $event): void
    {
        $product = Product::where('barcode', $event->barcode)->firstOrFail();
        // process $product ...
    }
}
```

Register the listener in `AppServiceProvider::boot()`:

```
Event::listen(ProductScanned::class, HandleBarcodeScan::class);
```

---

Available Drivers
-----------------

[](#available-drivers)

### RAW / JSON Driver

[](#raw--json-driver)

**Use case:** ESP32, Arduino, MQTT-to-serial bridges, custom IoT sensors.

The driver accumulates bytes into an `IoTFrameBuffer` and emits complete JSON frames when a newline delimiter is detected. Invalid JSON is forwarded as `{ "raw": "..." }` so data is never silently dropped.

```
// config/portflow.php
'default_driver' => 'raw-json',

'driver_options' => [
    'raw-json' => [
        'delimiter' => "\n",
        'max_bytes'  => 16384,   // rolling buffer ceiling
    ],
],
```

**Inbound frame from an ESP32:**

```
{ "type": "barcode.scan", "barcode": "4006381333931" }
```

**Mapping it to a Laravel event:**

```
'mappings' => [
    [
        'driver'              => 'raw-json',
        'payload_field'       => 'type',
        'equals'              => 'barcode.scan',
        'event'               => ProductScanned::class,
        'event_payload_field' => 'barcode',
    ],
],
```

---

### ESC/POS Driver

[](#escpos-driver)

**Use case:** Thermal receipt printers and USB barcode scanners (which behave like keyboards).

The ESC/POS driver handles **both directions**:

- **Inbound** — scanner sends a barcode string that becomes a `SerialFrame`.
- **Outbound** — `PortFlow::encode('escpos', ['text' => $line])` returns bytes to send to the printer.

**Printing a Blade template:**

```
$bytes = PortFlow::print('receipts.order', ['order' => $order]);
// $bytes can be sent directly to the printer via Web Serial
```

**Building ESC/POS bytes manually:**

```
use Hamzi\PortFlow\Infrastructure\Printing\EscPosBuilder;

$bytes = (new EscPosBuilder)
    ->align('center')
    ->bold()
    ->text('ACME STORE')
    ->bold(false)
    ->divider()
    ->align('left')
    ->text('Item 1 ................. $9.99')
    ->text('Item 2 ................. $4.50')
    ->divider()
    ->bold()
    ->text('TOTAL ................. $14.49')
    ->bold(false)
    ->feed(3)
    ->cut()
    ->bytes();
```

**Available `EscPosBuilder` methods:**

MethodESC/POS CommandDescription`text(string $value)`—Append a line of text`bold(bool $on = true)``ESC E n`Toggle bold`underline(bool $on = true)``ESC - n`Toggle underline`align(string)``ESC a n``'left'`, `'center'`, `'right'``divider(int $width = 48)`—Print a dash line separator`feed(int $lines = 1)``LF`Feed blank lines`cut(bool $partial = false)``GS V`Cut paper (full or partial)`bytes()`—Return accumulated byte string---

### RS-232 Driver

[](#rs-232-driver)

**Use case:** Industrial scales, label printers, and legacy serial devices using semicolon-delimited records.

**Example record:**

```
12.500;kg;SCALE-A1

```

Parsed into:

```
$frame->payload['weight']   // "12.500"
$frame->payload['segments'] // ["12.500", "kg", "SCALE-A1"]
$frame->payload['raw']      // "12.500;kg;SCALE-A1"
```

**Encoding outbound commands:**

```
PortFlow::encode('rs232', ['TARE', '0', 'RESET']);
// → "TARE,0,RESET\n"
```

---

Creating a Custom Driver
------------------------

[](#creating-a-custom-driver)

### Using the Artisan Generator

[](#using-the-artisan-generator)

```
php artisan portflow:make-driver MyScale
# creates app/SerialDrivers/MyScaleDriver.php

php artisan portflow:make-driver Modbus --namespace="App\\Hardware\\Drivers"
# creates app/Hardware/Drivers/ModbusDriver.php
```

Then register it in your config:

```
// config/portflow.php
'drivers' => [
    'my-scale' => \App\SerialDrivers\MyScaleDriver::class,
],
```

### Manually Implementing the Interface

[](#manually-implementing-the-interface)

Implement `Hamzi\PortFlow\Domain\Contracts\SerialDriver` and, for type safety, also implement `SerialEvent` on any event classes you create:

```
use Hamzi\PortFlow\Domain\Contracts\SerialDriver;
use Hamzi\PortFlow\Domain\DTO\SerialFrame;

final class MyModbusDriver implements SerialDriver
{
    public function name(): string
    {
        return 'modbus';
    }

    public function configure(array $options = []): void
    {
        // Store $options for use in parse/encode
    }

    public function encodeOutbound(array|string $payload): string
    {
        // Serialize $payload to device-specific bytes
        return is_string($payload) ? $payload : json_encode($payload);
    }

    /** @return array */
    public function parseInbound(string $chunk, array $context = []): array
    {
        return [
            SerialFrame::now($this->name(), ['data' => $chunk], $context),
        ];
    }
}
```

Register in `AppServiceProvider` or config:

```
PortFlow::registerDriver('modbus', MyModbusDriver::class);
```

### Defining a Typed Domain Event

[](#defining-a-typed-domain-event)

```
use Hamzi\PortFlow\Domain\Contracts\SerialEvent;

final class WeightReceived implements SerialEvent
{
    public function __construct(
        public readonly string $value,
        public readonly array  $context = [],
    ) {}
}
```

> Events that do **not** implement `SerialEvent` will still be dispatched, but a `Log::warning` will be emitted to encourage type safety.

---

Web Serial Integration
----------------------

[](#web-serial-integration)

`portflow-serial.js` provides a thin wrapper around the [Web Serial API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API) that POSTs incoming chunks to Laravel automatically.

```
Connect Device

  const bridge = new PortFlowBridge({
    ingestUrl: '{{ route("portflow.ingest") }}',
    driver:    'raw-json',
    baudRate:  115200,
    autoConnectOnLoad: true,
    filters: [{ usbVendorId: 6790, usbProductId: 29987 }],
    browserChunkEvent: 'esp32-browser-frame',
    livewireChunkEvent: 'esp32-livewire-frame',
    csrfToken: document.head.querySelector('meta[name="csrf-token"]').content,
  });

  document.getElementById('connect').addEventListener('click', () => bridge.connect());

```

**Constructor options:**

OptionTypeDefaultDescription`baudRate``number``9600`Serial baud rate`autoConnectOnLoad``boolean``true`Reconnect automatically after reload when the browser already trusts the port`driver``string``'raw-json'`PortFlow driver name`ingestUrl``string``'/portflow/ingest'`Backend endpoint`csrfToken``string`auto-detectedCSRF token for POST`rememberBaudRate``boolean``true`Persist the last selected baud rate in `localStorage``filters``array``[]`Web Serial port filters used by `requestPort()``browserChunkEvent``string``'portflow-frame-received'`Browser event name emitted for received chunks`livewireChunkEvent``string``'portflow-frame-received'`Livewire event name emitted for received chunks`livewireStatusEvent``string``'portflow-status-updated'`Livewire event name emitted for connection status`livewireErrorEvent``string``'portflow-error'`Livewire event name emitted for bridge errors`autoReconnect``boolean``true`Auto-reconnect on disconnect`maxRetries``number``5`Max reconnect attempts`retryDelay``number``2000`Base delay in ms (exponential back-off)**Window events:**

```
// Fired on every status change (connect, disconnect, reconnect attempt)
window.addEventListener('portflow-status', (e) => {
  console.log('Connected:', e.detail.connected);
  console.log('Driver:', e.detail.driver);
  console.log('Frames received:', e.detail.frames);
  console.log('Retry count:', e.detail.retryCount);
});

// Fired for every raw chunk POSTed to the backend
window.addEventListener('portflow-frame-received', (e) => {
  console.log('Raw chunk:', e.detail.chunk);
});

// Fired on each reconnect attempt
window.addEventListener('portflow-reconnecting', (e) => {
  console.log(`Reconnect attempt ${e.detail.attempt} in ${e.detail.delay}ms`);
});

// Fired when max retries are exhausted
window.addEventListener('portflow-reconnect-failed', (e) => {
  console.error(`Failed after ${e.detail.retries} retries`);
});

// Fired immediately when the browser does not support Web Serial API
window.addEventListener('portflow-unsupported', (e) => {
  console.warn(e.detail.reason);
  console.info('Supported browsers:', e.detail.suggestedBrowsers);
});
```

### Auto-Reconnect

[](#auto-reconnect)

When a serial connection drops unexpectedly, the bridge automatically attempts to reconnect using **exponential back-off** (2 s, 4 s, 8 s, …) up to `maxRetries` attempts. This is enabled by default and requires no configuration.

### Backend Direct Serial Mode

[](#backend-direct-serial-mode)

Some devices are easier or safer to integrate from the backend (Linux service, kiosk daemon, headless station) rather than browser Web Serial. PortFlow includes an Artisan listener:

```
php artisan portflow:listen /dev/ttyUSB0 --driver=barcode-line --baud=115200
```

Windows example:

```
php artisan portflow:listen COM3 --driver=barcode-line --baud=115200
```

Advanced UART parameters:

```
php artisan portflow:listen /dev/ttyUSB1 \
  --driver=rfid-ascii \
  --baud=9600 \
  --parity=none \
  --data-bits=8 \
  --stop-bits=1 \
  --flow-control=none \
  --context='{"station":"gate-a"}'
```

Show incoming serial payloads in the console while still ingesting frames:

```
php artisan portflow:listen /dev/ttyUSB0 \
  --driver=raw-json \
  --baud=921600 \
  --show-data=1 \
  --show-data-format=json
```

Data preview options:

- `--show-data=1` enables payload preview logs.
- `--show-data-format=auto|raw|plain|json|hex|base64` controls rendering format.
- `--show-data-max=512` limits displayed bytes per chunk to keep logs readable.

Security and hardening notes:

- Device path validation supports `/dev/*` on Linux/macOS and `COMx` / `\\.\COMx` on Windows.
- Use `portflow.backend.allowed_devices` to allowlist exact/glob device paths.
- Listener configures serial parameters with platform-native tooling (`stty` on POSIX, `mode` on Windows).
- Ingest now validates decoded base64 chunk size against `max_chunk_bytes`.

`config/portflow.php` backend section:

```
'backend' => [
    'allowed_devices' => [
        '/dev/ttyUSB0',
        '/dev/ttyACM*',
        'COM*',
        '\\\\.\\COM*',
    ],
    'default_chunk_bytes' => 256,
    'default_read_sleep_us' => 20000,
],
```

To disable:

```
const bridge = new PortFlowBridge({ autoReconnect: false });
```

**Alpine.js integration (built-in):**

```

  window.portflowConfig = {
    ingestUrl: '{{ route("portflow.ingest") }}',
    driver:   'raw-json',
    baudRate:  115200,
  };

```

### Browser Compatibility

[](#browser-compatibility)

The Web Serial API is only available in **Chromium-based browsers** (Chrome 89+ / Edge 89+). Firefox and Safari do not support it.

**Check support in JavaScript:**

```
if (!PortFlowBridge.isSupported()) {
  // Show a fallback UI, redirect, or degrade gracefully
  console.warn('Web Serial not available in this browser.');
}
```

**Check support in Blade (no Alpine required):**

```
{{-- Wraps any content; shows a warning banner in unsupported browsers --}}

```

The component renders a yellow warning banner with a "Download Chrome" link in Firefox/Safari, and leaves the content untouched in supported browsers.

Customise the message and hide the download link:

```

```

BrowserWeb SerialStatusChrome 89+✅SupportedEdge 89+✅SupportedFirefox❌Not supported (flag-only, no stable release)Safari / iOS❌Not supportedOpera (Chromium)✅Supported---

Hardware → Events &amp; Eloquent
--------------------------------

[](#hardware--events--eloquent)

The `mappings` config key lets you automatically route frames to Laravel events or Eloquent models without writing controller code.

```
// config/portflow.php
'mappings' => [
    // Fire ProductScanned when a raw-json frame has type = "barcode.scan"
    [
        'driver'              => 'raw-json',
        'payload_field'       => 'type',
        'equals'              => 'barcode.scan',
        'event'               => ProductScanned::class,
        'event_payload_field' => 'barcode',
    ],

    // Match every ESC/POS frame (no payload filter)
    [
        'driver' => 'escpos',
        'event'  => ProductScanned::class,
        'event_payload_field' => 'barcode',
    ],

    // Persist weight readings directly to an Eloquent model
    [
        'driver'    => 'rs232',
        'model'     => \App\Models\WeightReading::class,
        'field_map' => [
            'value' => 'weight',   // model column => payload key
            'unit'  => 'segments.1',
        ],
    ],
],
```

If a mapping's event or model throws an exception, it is caught and written to `Log::error` so one bad handler never breaks other mappings or the HTTP response.

### Queue-Based Routing

[](#queue-based-routing)

For high-throughput or slow listeners, route frames asynchronously via the queue:

```
// config/portflow.php  (or .env)
'queue_routing' => env('PORTFLOW_QUEUE_ROUTING', false),
```

Or in `.env`:

```
PORTFLOW_QUEUE_ROUTING=true

```

When enabled, each `SerialFrame` is dispatched as a `RouteSerialFrameJob` (3 retries by default) on the configured queue connection. The HTTP response is returned immediately.

```
php artisan queue:work
```

---

Thermal Printing Engine
-----------------------

[](#thermal-printing-engine)

PortFlow ships a Blade-to-ESC/POS renderer so you can design receipts in familiar Blade syntax:

**`resources/views/receipts/order.blade.php`**

```
Order #{{ $order->id }}
Date: {{ $order->created_at->format('d/m/Y H:i') }}
------------------------------------------------
@foreach ($order->items as $item)
{{ str_pad($item->name, 38) }}{{ number_format($item->price, 2) }}
@endforeach
------------------------------------------------
TOTAL: {{ number_format($order->total, 2) }}
```

**In a controller or job:**

```
$bytes = PortFlow::print('receipts.order', ['order' => $order]);
// Send $bytes to the printer via Web Serial write() or a TCP socket
```

---

IoT Frame Buffering
-------------------

[](#iot-frame-buffering)

`IoTFrameBuffer` is a standalone utility that accumulates streaming bytes and emits complete frames when a delimiter is found. Use it independently for any stream protocol:

```
use Hamzi\PortFlow\Domain\Services\IoTFrameBuffer;

$buffer = new IoTFrameBuffer(delimiter: "\n", maxBytes: 8192);

$frames = $buffer->push("partial-");  // → []
$frames = $buffer->push("data\n");    // → ["partial-data"]

// Flush any incomplete frame before closing the connection
$remainder = $buffer->flushRemainder();
```

### Buffer Persistence Across Requests

[](#buffer-persistence-across-requests)

IoT devices may split a JSON packet across multiple HTTP requests. Pass a `session_id` in the request context and the `RawJsonDriver` will automatically persist the buffer state in the Laravel cache between requests (5-minute TTL):

**JavaScript (add `session_id` to the context):**

```
const bridge = new PortFlowBridge({
  driver: 'raw-json',
  ingestUrl: '/portflow/ingest',
});
// PortFlowBridge sets context.source automatically.
// To enable persistence, pass session_id from the server:
```

**Or POST directly from firmware:**

```
{
  "driver":  "raw-json",
  "chunk":   "{\"sensor\":\"temp\",\"val",
  "context": { "session_id": "device-esp32-A4:CF:12" }
}
```

A subsequent request with the same `session_id` will complete and emit the frame:

```
{
  "driver":  "raw-json",
  "chunk":   "ue\":22.5}\n",
  "context": { "session_id": "device-esp32-A4:CF:12" }
}
```

The backend will emit one complete `SerialFrame` with `{ "sensor": "temp", "value": 22.5 }`.

> Requires a cache driver other than `array` in production (e.g., `redis`, `database`).

---

Blade &amp; Livewire Components
-------------------------------

[](#blade--livewire-components)

### Blade

[](#blade)

```
{{-- Connection toggle button + port label --}}

{{-- Real-time driver status badge --}}

{{-- Browser compatibility warning (no Alpine required) --}}

```

`portflow-browser-check` wraps any slot content and injects a styled warning banner when the browser does not support the Web Serial API. See the [Browser Compatibility](#browser-compatibility) section for full options.

### Livewire

[](#livewire)

```

```

The `PortFlowStatus` component listens for the `portflow-status-updated` browser event dispatched by the JS bridge automatically.

---

Security
--------

[](#security)

ProtectionDetail**Rate limiting**`60` requests / minute per IP by default. Configurable via `portflow.ingest_rate_limit` or `PORTFLOW_RATE_LIMIT` env. Returns `429` when exceeded.**Chunk size limit**Inbound `chunk` field capped at `16 384` bytes (configurable). Returns `422` on violation.**CSRF**Ingest endpoint inherits the `web` middleware group (CSRF enforced). Switch to `['api']` if using token auth.**Event type safety**Events used in mappings should implement `SerialEvent`. A `Log::warning` is emitted for non-conforming classes.---

Configuration Reference
-----------------------

[](#configuration-reference)

```
// config/portflow.php

return [
    /*
    |--------------------------------------------------------------------------
    | Default Driver
    |--------------------------------------------------------------------------
    */
    'default_driver' => env('PORTFLOW_DEFAULT_DRIVER', 'raw-json'),

    /*
    |--------------------------------------------------------------------------
    | Ingest Endpoint
    |--------------------------------------------------------------------------
    */
    'ingest_path' => env('PORTFLOW_INGEST_PATH', '/portflow/ingest'),

    /*
    |--------------------------------------------------------------------------
    | Ingest Middleware
    |--------------------------------------------------------------------------
    | Do NOT add TrimStrings or ConvertEmptyStringsToNull — they corrupt
    | binary delimiters (\n, \r\n) in serial chunk data.
    */
    'ingest_middleware' => ['web'],

    /*
    |--------------------------------------------------------------------------
    | Security
    |--------------------------------------------------------------------------
    */
    'max_chunk_bytes'   => env('PORTFLOW_MAX_CHUNK_BYTES', 16384),
    'ingest_rate_limit' => env('PORTFLOW_RATE_LIMIT', 60),

    /*
    |--------------------------------------------------------------------------
    | Queue Routing
    |--------------------------------------------------------------------------
    | Set to true to dispatch SerialFrames via the queue instead of
    | processing them synchronously inside the HTTP request cycle.
    */
    'queue_routing' => env('PORTFLOW_QUEUE_ROUTING', false),

    /*
    |--------------------------------------------------------------------------
    | Drivers
    |--------------------------------------------------------------------------
    */
    'drivers' => [
        'raw-json' => \Hamzi\PortFlow\Infrastructure\Drivers\RawJsonDriver::class,
        'escpos'   => \Hamzi\PortFlow\Infrastructure\Drivers\EscPosDriver::class,
        'rs232'    => \Hamzi\PortFlow\Infrastructure\Drivers\Rs232Driver::class,
    ],

    /*
    |--------------------------------------------------------------------------
    | Driver Options
    |--------------------------------------------------------------------------
    */
    'driver_options' => [
        'raw-json' => [
            'delimiter' => "\n",
            'max_bytes' => 16384,
        ],
        'rs232' => [
            'delimiter' => "\n",
        ],
        'escpos' => [],
    ],

    /*
    |--------------------------------------------------------------------------
    | Mappings
    |--------------------------------------------------------------------------
    | Automatically route frames to Events or Eloquent models.
    | Supported keys per mapping:
    |   driver              — match only frames from this driver (optional)
    |   payload_field       — payload key to match on (optional)
    |   equals              — expected value of payload_field (optional)
    |   event               — fully-qualified event class to dispatch
    |   event_payload_field — payload key passed as first constructor arg
    |   model               — Eloquent model class to create
    |   field_map           — [ 'column' => 'payload_key' ]
    */
    'mappings' => [
        [
            'driver'              => 'raw-json',
            'payload_field'       => 'type',
            'equals'              => 'barcode.scan',
            'event'               => \Hamzi\PortFlow\Domain\Events\ProductScanned::class,
            'event_payload_field' => 'barcode',
        ],
        [
            'driver'              => 'escpos',
            'event'               => \Hamzi\PortFlow\Domain\Events\ProductScanned::class,
            'event_payload_field' => 'barcode',
        ],
    ],
];
```

### Barcode Line Driver

[](#barcode-line-driver)

**Use case:** common 1D/2D barcode scanners in serial mode (USB CDC, TTL UART, RS-232 adapters).

Most scanners send ASCII text followed by a line terminator (`\r`, `\n`, or both). `barcode-line` normalizes this into:

```
[
  'barcode' => '4006381333931',
  'raw' => '4006381333931\r',
  'length' => 13,
]
```

Recommended config:

```
'default_driver' => 'barcode-line',

'driver_options' => [
    'barcode-line' => [
        'delimiter' => "\n",
        'strip_prefix' => [],
        'strip_suffix' => ["\r", "\n", "\t"],
    ],
],
```

### RFID ASCII Driver

[](#rfid-ascii-driver)

**Use case:** 125kHz serial readers (for example ID-12/ID-20 family) that send framed ASCII.

A common frame format is:

- `STX (0x02)`
- ASCII tag bytes
- `CR` + `LF`
- `ETX (0x03)`

`rfid-ascii` parses this safely and emits payload like:

```
[
  'tag' => '7A005B0FF8D6',
  'raw' => '7A005B0FF8D6\r\n',
  'raw_hex' => '023741303035423046463844360D0A03',
  'format' => 'stx-etx-ascii',
]
```

Recommended config:

```
'driver_options' => [
    'rfid-ascii' => [
        'stx' => "\x02",
        'etx' => "\x03",
        'uppercase' => true,
    ],
],
```

### Fingerprint Packet Driver

[](#fingerprint-packet-driver)

**Use case:** optical/capacitive UART fingerprint modules that speak binary packet protocol (common start code `0xEF01`).

Unlike barcode/RFID text streams, fingerprint sensors are binary. PortFlow now supports binary chunks over web ingest using `chunk_encoding: base64` automatically in the JS bridge.

`fingerprint-packet` validates checksums and emits parsed packet metadata:

```
[
  'packet_type' => 7,
  'packet_type_name' => 'ack',
  'address_hex' => 'FFFFFFFF',
  'data_hex' => '00',
  'checksum' => 11,
  'checksum_calculated' => 11,
  'checksum_valid' => true,
  'raw_hex' => 'EF01FFFFFFFF07000300000B',
]
```

Recommended config:

```
'driver_options' => [
    'fingerprint-packet' => [
        'start_code_hex' => 'EF01',
    ],
],
```

### Device Patterns Supported Out-of-the-box

[](#device-patterns-supported-out-of-the-box)

Device TypeCommon Serial PatternRecommended DriverBarcode scannersASCII + CR/LF terminator`barcode-line`RFID serial readers`STX + TAG + CRLF + ETX``rfid-ascii`Fingerprint sensorsBinary framed packets (`EF01 ... checksum`)`fingerprint-packet`---

Testing
-------

[](#testing)

```
composer test
```

Run with coverage:

```
composer test -- --coverage
```

Lint &amp; style:

```
composer format              # fix in place
composer format -- --test    # check only (CI mode)
```

Static analysis:

```
composer analyse
```

---

Contributing
------------

[](#contributing)

Contributions are welcome. Please:

1. Fork the repository and create a feature branch.
2. Write tests for all new behaviour.
3. Run `composer format` and `composer analyse` before submitting.
4. Open a Pull Request with a clear description of the change.

See [CONTRIBUTING.md](CONTRIBUTING.md) for full guidelines.

---

License
-------

[](#license)

The MIT License. See [LICENSE](LICENSE) for details.

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance89

Actively maintained with recent releases

Popularity13

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity43

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 83.8% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~3 days

Total

9

Last Release

54d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/36019461?v=4)[Hamdy Elbatal](/maintainers/hamdyelbatal122)[@hamdyelbatal122](https://github.com/hamdyelbatal122)

---

Top Contributors

[![hamdyelbatal122](https://avatars.githubusercontent.com/u/36019461?v=4)](https://github.com/hamdyelbatal122 "hamdyelbatal122 (31 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (6 commits)")

---

Tags

laravellivewireposiotescposserialthermal-printerhardwarers232web-serial

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/hamzi-portflow/health.svg)

```
[![Health](https://phpackages.com/badges/hamzi-portflow/health.svg)](https://phpackages.com/packages/hamzi-portflow)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k15.1M136](/packages/laravel-pulse)[roots/acorn

Framework for Roots WordPress projects built with Laravel components.

9762.4M133](/packages/roots-acorn)[laravel/scout

Laravel Scout provides a driver based solution to searching your Eloquent models.

1.7k55.0M632](/packages/laravel-scout)[api-platform/laravel

API Platform support for Laravel

58174.6k17](/packages/api-platform-laravel)[flat3/lodata

OData v4.01 Producer for Laravel

99351.7k](/packages/flat3-lodata)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
