hamzi / portflow
Neural bridge package connecting Laravel applications with local and IoT hardware using Web Serial and clean driver abstractions.
Requires
- php: ^8.2
- illuminate/bus: ^11.0|^12.0|^13.0
- illuminate/cache: ^11.0|^12.0|^13.0
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/events: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/queue: ^11.0|^12.0|^13.0
- illuminate/routing: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- livewire/livewire: ^3.0
Requires (Dev)
- laravel/pint: ^1.18
- nunomaduro/collision: ^8.0
- orchestra/testbench: ^9.0|^10.0
- phpstan/phpstan: ^1.11|^2.0
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2026-05-04 23:09:02 UTC
README
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
- What is PortFlow?
- Architecture
- Requirements
- Installation
- Quick Start
- Available Drivers
- Creating a Custom Driver
- Web Serial Integration
- Hardware → Events & Eloquent
- Thermal Printing Engine
- IoT Frame Buffering
- Blade & Livewire Components
- Security
- Configuration Reference
- Testing
- Contributing
- License
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
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
| Dependency | Version | Notes |
|---|---|---|
| PHP | ^8.2 |
PHP 8.3+ recommended for Laravel 13 |
| Laravel | ^11.0 | ^12.0 | ^13.0 |
All actively supported versions |
| Livewire | ^3.0 |
Browser support for Web Serial API: Chrome 89+, Edge 89+. Not supported in Firefox or Safari.
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
1. Add the JS bridge to your layout:
<script src="{{ asset('vendor/portflow/portflow-serial.js') }}"></script>
2. Drop in the Livewire connector component:
<livewire:portflow-connector :baud-rate="115200" :auto-connect-on-load="true" />
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:
<livewire:portflow-connector :baud-rate="115200" driver="raw-json" :auto-connect-on-load="true" :context="['device' => 'esp32-line-a']" :filters="[['usbVendorId' => 6790, 'usbProductId' => 29987]]" browser-chunk-event="esp32-browser-frame" livewire-chunk-event="esp32-livewire-frame" />
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
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
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:
| Method | ESC/POS Command | Description |
|---|---|---|
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
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
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
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<int, SerialFrame> */ 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
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
SerialEventwill still be dispatched, but aLog::warningwill be emitted to encourage type safety.
Web Serial Integration
portflow-serial.js provides a thin wrapper around the Web Serial API that POSTs incoming chunks to Laravel automatically.
<button id="connect">Connect Device</button> <script src="{{ asset('vendor/portflow/portflow-serial.js') }}"></script> <script> 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()); </script>
Constructor options:
| Option | Type | Default | Description |
|---|---|---|---|
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-detected | CSRF 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
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
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=1enables payload preview logs.--show-data-format=auto|raw|plain|json|hex|base64controls rendering format.--show-data-max=512limits displayed bytes per chunk to keep logs readable.
Security and hardening notes:
- Device path validation supports
/dev/*on Linux/macOS andCOMx/\\.\COMxon Windows. - Use
portflow.backend.allowed_devicesto allowlist exact/glob device paths. - Listener configures serial parameters with platform-native tooling (
sttyon POSIX,modeon 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):
<div x-data="portflowConnector()" x-init="init()"> <button @click="connect()" :disabled="connecting"> <span x-text="connected ? 'Connected' : (connecting ? 'Connecting…' : 'Connect Device')"></span> </button> <span x-show="connected" x-text="'Frames: ' + frames"></span> <span x-show="retryCount > 0" x-text="'Reconnecting… attempt ' + retryCount"></span> </div> <script> window.portflowConfig = { ingestUrl: '{{ route("portflow.ingest") }}', driver: 'raw-json', baudRate: 115200, }; </script>
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 --}} <x-portflow::portflow-browser-check> <livewire:portflow-connector /> </x-portflow::portflow-browser-check>
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:
<x-portflow::portflow-browser-check message="Serial device features require Google Chrome or Microsoft Edge." :show-download-link="false" > <livewire:portflow-connector /> </x-portflow::portflow-browser-check>
| Browser | Web Serial | Status |
|---|---|---|
| Chrome 89+ | ✅ | Supported |
| Edge 89+ | ✅ | Supported |
| Firefox | ❌ | Not supported (flag-only, no stable release) |
| Safari / iOS | ❌ | Not supported |
| Opera (Chromium) | ✅ | Supported |
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
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
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
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
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
arrayin production (e.g.,redis,database).
Blade & Livewire Components
Blade
{{-- Connection toggle button + port label --}} <x-portflow::connector /> {{-- Real-time driver status badge --}} <x-portflow::status /> {{-- Browser compatibility warning (no Alpine required) --}} <x-portflow::portflow-browser-check> <livewire:portflow-connector /> </x-portflow::portflow-browser-check>
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 section for full options.
Livewire
<livewire:portflow-connector /> <livewire:portflow-status />
The PortFlowStatus component listens for the portflow-status-updated browser event dispatched by the JS bridge automatically.
Security
| Protection | Detail |
|---|---|
| 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
// 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
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
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+LFETX (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
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 Type | Common Serial Pattern | Recommended Driver |
|---|---|---|
| Barcode scanners | ASCII + CR/LF terminator | barcode-line |
| RFID serial readers | STX + TAG + CRLF + ETX |
rfid-ascii |
| Fingerprint sensors | Binary framed packets (EF01 ... checksum) |
fingerprint-packet |
Testing
composer test
Run with coverage:
composer test -- --coverage
Lint & style:
composer format # fix in place composer format -- --test # check only (CI mode)
Static analysis:
composer analyse
Contributing
Contributions are welcome. Please:
- Fork the repository and create a feature branch.
- Write tests for all new behaviour.
- Run
composer formatandcomposer analysebefore submitting. - Open a Pull Request with a clear description of the change.
See CONTRIBUTING.md for full guidelines.
License
The MIT License. See LICENSE for details.