orderlyconnect/astm-e1394-91
Composer 安装命令:
composer require orderlyconnect/astm-e1394-91
包简介
Parse, build and transmit ASTM E1394-97 clinical laboratory messages over TCP, serial or files — including full LLP framing, ENQ/ACK handshake, and a fluent message builder.
关键字:
README 文档
README
A complete, production-ready PHP 8.1+ library for parsing, building, and transmitting ASTM E1394-97 messages — the standard used by clinical laboratory instruments (haematology analysers, chemistry analysers, coagulation systems, etc.) to exchange patient results with LIS/HIS systems.
Validated against real Sysmex XN-350 haematology analyser output.
Features
- Parse plain-text and LLP-framed ASTM messages into typed PHP objects
- Build messages with a fluent, type-safe builder API
- Send over TCP, RS-232 serial, or files with full ENQ/ACK/EOT handshake
- Receive inbound instrument connections with a byte-level state machine receiver
- TCP Server — accept multiple instrument connections in a dedicated process
- LLP framing — ASTM E1381-95 checksum, ETX/ETB frame splitting, streaming decoder
- EscapeCodec — encode/decode
&F& &R& &S& &E&sequences (§6.6) - MessageValidator — structural rule checking
- MessageCollection — batch processing with cross-session queries
- MessageDiff — field-by-field comparison for correction workflows
- JSON/array serialisation —
toArray()/toJson() - Zero runtime dependencies
Installation
composer require orderlyconnect/astm-e1394-91
Requirements: PHP 8.1+.
Serial port support requires stty (Linux/macOS) or mode.com (Windows).
Quick Start
use Astm\Astm; // ── Parse ────────────────────────────────────────────────────────────────── $message = Astm::parse(file_get_contents('/tmp/result.astm')); foreach ($message->getResultMap() as $test => $data) { printf("%-10s %6s %-10s flag=%s\n", $test, $data['value'], $data['units'], $data['flag']); } // ── Build ────────────────────────────────────────────────────────────────── $message = Astm::build() ->sender('MY-LIS', 'v2.0') ->patient(fn ($p) => $p->id('PAT-001')->name('Okafor', 'Ngozi')->sex('F')) ->order(fn ($o) => $o->specimenId('EDTA-001')->addTest('WBC')->addTest('HGB')) ->result(fn ($r) => $r ->test('WBC')->value('7.2')->units('10*3/uL') ->referenceRangeFromBounds(4.0, 11.0)->flag('N')) ->result(fn ($r) => $r ->test('HGB')->value('14.1')->units('g/dL')->flag('N')) ->build(); // ── Send over TCP ────────────────────────────────────────────────────────── Astm::sendTcp($message, '192.168.1.50', port: 3001); // ── Receive (blocking server) ────────────────────────────────────────────── Astm::listen(function (\Astm\Message $msg, string $from): void { echo "[{$from}] " . count($msg->getResults()) . " results\n"; file_put_contents('/var/log/astm/' . uniqid() . '.json', $msg->toJson()); }, host: '0.0.0.0', port: 3001);
Table of Contents
- Parsing
- Building Messages
- Cloning & Modifying
- Sending
- Receiving
- TCP Server
- Transports
- LLP Protocol
- Utility Classes
- Serialisation
- ASTM Record Reference
- Examples
- Testing
- Extending
- Publishing to Packagist
Parsing
// Single message from a string $message = Astm::parse($rawString); // All sessions from a plain-text file → MessageCollection $collection = Astm::parseFile('/path/to/session.astm'); // Decode an LLP-framed binary blob $collection = Astm::decodeLlp(file_get_contents('/captures/session.bin')); // Message accessors $message->getHeader(); // ?Header $message->getFirstPatient(); // ?Patient $message->getFirstOrder(); // ?Order $message->getResults(); // Result[] $message->getAbnormalResults(); // Result[] — flag ≠ '' and ≠ 'N' $message->getFinalResults(); // Result[] — status = 'F' $message->hasAbnormalities(); // bool $message->getComments(); // Comment[] $message->getTerminator(); // ?Terminator $message->getRecordsByType('R'); // AbstractRecord[] // Result map — keyed by test name // ['WBC' => ['value'=>'7.2','units'=>'10*3/uL','flag'=>'N','status'=>'F'], ...] $message->getResultMap();
Building Messages
use Astm\Astm; use Astm\Records\Result; $message = Astm::build() ->sender('MY-LIS', 'v2.0', processingId: 'P', receiverId: 'LAB-SYSTEM') ->patient(fn ($p) => $p ->id('PAT-001', 'LAB-001') ->name('Smith', 'John', 'A') // last, first, middle ->sex('M') // M / F / U ->birthdate('19850310') // YYYYMMDD ->address('123 Main St') ->phone('555-1234') ->physician('DR-JONES')) ->order(fn ($o) => $o ->specimenId('EDTA-001') ->addTest('WBC')->addTest('HGB')->addTest('PLT') ->collectionDateTime('20250329143000') ->priority('R') // R=routine, S=stat ->reportType('F')) ->result(fn ($r) => $r ->test('WBC') ->value('7.2') ->units('10*3/uL') ->referenceRangeFromBounds(4.0, 11.0) // produces "4-11" ->flag(Result::FLAG_NORMAL) ->status(Result::STATUS_FINAL) ->completedAt('20250329143330')) ->result(fn ($r) => $r ->test('PSA') ->value('2.1') ->units('ng/mL') ->referenceRangeLimit('<', 4.0) // produces "<4" ->flag('N')) ->comment('Sample slightly haemolysed') ->build();
Custom delimiters
use Astm\Delimiters; $d = new Delimiters(field: '!', component: '@', repeat: '#', escape: '$'); $message = Astm::build($d)->sender('MY-LIS')->result(...)->build();
Cloning & Modifying
// Clone a received message and enrich it $enriched = Astm::modify($received) ->comment('Critical values phoned to ward at 09:15', 'LIS', 'I') ->build(); // Sequence numbers continue correctly from the original // Compare the two messages $diff = Astm::diff($received, $enriched); if ($diff->hasDifferences()) { foreach ($diff->getSummary() as $line) { echo $line . "\n"; } }
Sending
// TCP — one-liner Astm::sendTcp($message, '192.168.1.50', port: 3001); // TCP — fine-grained use Astm\Sender; use Astm\Transport\TcpTransport; $transport = new TcpTransport('192.168.1.50', 3001, connectTimeout: 10, readTimeout: 30); $sender = new Sender($transport, maxEnqRetries: 3, maxFrameRetries: 3); $transport->connect(); try { $sender->send($message); } finally { $transport->disconnect(); } // Serial port use Astm\Transport\SerialTransport; $transport = new SerialTransport('/dev/ttyUSB0', baud: 9600); (new \Astm\Sender($transport))->send($message); // Write LLP-framed binary file Astm::writeFile($message, '/tmp/output.astm'); $collection = Astm::readFile('/tmp/output.astm');
Receiving
use Astm\Receiver; use Astm\Transport\TcpTransport; $transport = new TcpTransport('0.0.0.0', 3001); $transport->connect(); $receiver = new Receiver( transport: $transport, onMessage: function (\Astm\Message $msg): void { foreach ($msg->getAbnormalResults() as $r) { printf("⚠ %-12s %6s [%s]\n", $r->getTestName(), $r->getValue(), $r->getAbnormalFlag()); } }, ); // Non-blocking (event loop) while (true) { $receiver->tick(); while ($msg = $receiver->popMessage()) { handleMessage($msg); } usleep(1_000); } // Blocking $receiver->listen();
TCP Server
use Astm\TcpServer; $server = new TcpServer(host: '0.0.0.0', port: 3001, verifyChecksums: true); pcntl_signal(SIGTERM, fn () => $server->stop()); $server->serve(function (\Astm\Message $msg, string $remoteAddr): void { $sender = $msg->getHeader()->getSenderName(); $patient = $msg->getFirstPatient(); printf("[%s] %s — %s, %s — %d results\n", $remoteAddr, $sender, $patient?->getLastName(), $patient?->getFirstName(), count($msg->getResults())); // Persist file_put_contents('/var/log/astm/' . uniqid() . '.json', $msg->toJson()); });
Transports
| Class | Use Case |
|---|---|
TcpTransport |
Connect to an instrument over TCP/IP |
SerialTransport |
RS-232 via /dev/ttyUSBx or COM3 |
FileTransport |
Read / write LLP-framed binary files |
MemoryTransport |
Unit tests and simulation |
StreamTransport |
Wrap any open PHP stream resource |
Implement TransportInterface (6 methods) to add your own.
LLP Protocol
ENQ ← sender requests the line
ACK ← receiver grants
STX fn data ETX C1C2 CR LF ← one record frame
ACK
EOT ← sender ends session
- ETX — ends each ASTM record's last chunk
- ETB — splits records longer than
maxDataBytes(default 240) - Frame numbers cycle 1–7
- Checksum = sum(bytes of frameNum + data + ETX/ETB) mod 256, two uppercase hex digits
use Astm\Protocol\{LlpEncoder, LlpDecoder, Frame, Ascii}; $encoder = new LlpEncoder(maxDataBytes: 240); $frames = $encoder->encode($message); $decoder = new LlpDecoder(verifyChecksums: true); $decoder->feed($chunk); while ($raw = $decoder->popMessage()) { $parsed = (new \Astm\Parser())->parse($raw); } $checksum = Frame::checksum('1H|\\^&|||SENDER' . Ascii::ETX);
Utility Classes
EscapeCodec
$codec = Astm::escapeCodec(); $safe = $codec->encode('Result: 5.0 | note'); // "Result: 5.0 &F& note" $plain = $codec->decode($safe);
| Sequence | Literal |
|---|---|
&F& |
| field delimiter |
&R& |
\ repeat delimiter |
&S& |
^ component delimiter |
&E& |
& escape character |
MessageValidator
$errors = Astm::validate($message); // [] = valid
MessageCollection
$collection = Astm::parseFile('/batch/session.astm'); $collection->getAbnormalResults(); // all H/L/A across all sessions $collection->getHighResults(); // H/HH only $collection->getLowResults(); // L/LL only $collection->getResultsByTest('WBC'); // cross-session $collection->getMessagesWithAbnormalities(); // messages with any flag $collection->getAllResultsMapped(); // grouped by test name
MessageDiff
$diff = Astm::diff($original, $corrected); $diff->hasDifferences(); // bool $diff->getFieldChanges(); // [{type, seq, field, old, new}, ...] $diff->getChangedResults(); // [{test, old, new}, ...] $diff->getChangedTestNames(); // ['WBC', 'HGB'] $diff->getSummary(); // ['R[1] field 4: "7.2" → "99.9"', ...]
DateTimeHelper
use Astm\DateTimeHelper; DateTimeHelper::parse('20250329143330'); // DateTimeImmutable DateTimeHelper::parseDate('19920801'); // DateTimeImmutable DateTimeHelper::format($dt); // '20250329143330' DateTimeHelper::formatDate($dt); // '19920801' DateTimeHelper::now(); // current UTC as YYYYMMDDHHMMSS DateTimeHelper::today(); // current UTC date as YYYYMMDD DateTimeHelper::isValid('20250329143330'); // true
Serialisation
// Wire format $message->toString("\r"); // ASTM CR-terminated (instrument wire format) $message->toString("\n"); // newline-separated (logs / files) // PHP array $arr = $message->toArray(); // keys: sender, version, datetime, patient, order, results, comments // JSON $json = $message->toJson(); // pretty-printed $json = $message->toJson(JSON_THROW_ON_ERROR); // compact // LLP binary file Astm::writeFile($message, '/tmp/output.astm'); $collection = Astm::readFile('/tmp/output.astm');
ASTM Record Reference
Result flags
| Constant | Value | Meaning |
|---|---|---|
Result::FLAG_NORMAL |
N |
Within range |
Result::FLAG_ABOVE_NORMAL |
H |
High |
Result::FLAG_BELOW_NORMAL |
L |
Low |
Result::FLAG_CRITICAL_H |
HH |
Critically high |
Result::FLAG_CRITICAL_L |
LL |
Critically low |
Result::FLAG_ABNORMAL |
A |
Qualitative abnormal |
Result::FLAG_WARNING |
W |
Instrument warning |
Result statuses
| Constant | Value |
|---|---|
Result::STATUS_FINAL |
F |
Result::STATUS_CORRECTION |
C |
Result::STATUS_PRELIMINARY |
P |
Result::STATUS_INCOMPLETE |
I |
Exception hierarchy
AstmException
├── ParseException
│ └── UnknownRecordTypeException
└── ConnectionException
Examples
php examples/01_parse_file.php # parse + display with flags php examples/02_build_and_send.php [host] [port] # build, validate, JSON, send php examples/03_tcp_server.php [host] [port] # TCP server (Ctrl-C to stop) php examples/04_batch_collection.php # batch stats report php examples/05_modify_and_forward.php # clone + enrich + file round-trip php examples/06_serial_instrument.php /dev/ttyUSB0 9600 # RS-232 (hardware) php examples/07_escape_and_delimiters.php # EscapeCodec + custom delimiters
Testing
composer install
composer test
167 tests · 371 assertions across 5 test files covering the full stack.
Extending
Custom record type
use Astm\Records\AbstractRecord; final class ManufacturerRecord extends AbstractRecord { public const TYPE = 'M'; public function getType(): string { return self::TYPE; } public function getVendorData(): string { return $this->getField(2); } } \Astm\Parser::registerRecordType('M', ManufacturerRecord::class); $message = (new \Astm\Parser())->parse($raw); $mRecords = $message->getRecordsByType('M');
Custom transport
Implement Astm\Transport\TransportInterface and pass to Sender or Receiver.
Publishing to Packagist
See PACKAGIST.md for step-by-step instructions.
Security
Please report vulnerabilities via the process in SECURITY.md.
Changelog
See CHANGELOG.md.
Contributing
See CONTRIBUTING.md.
Licence
MIT — see LICENSE.
orderlyconnect/astm-e1394-91 适用场景与选型建议
orderlyconnect/astm-e1394-91 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 13 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 04 月 07 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「analyser」 「hl7」 「instrument」 「laboratory」 「LLP」 「astm」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 orderlyconnect/astm-e1394-91 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 orderlyconnect/astm-e1394-91 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 orderlyconnect/astm-e1394-91 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
Fast detection of composer dependency issues (dead dependencies, shadow dependencies, misplaced dependencies)
PHP performance tool analyser your script on time, memory usage and db query. Support Laravel and Composer for web, web console and command line interfaces.
A port of the PEAR Net_HL7 library to PHP5.3+
Pre-generated FHIR model classes for R4, R4B, and R5 versions
PSR-7 Middleware for instrumenting PHP applications
Shared interfaces, attributes and utilities for FHIR packages
统计信息
- 总下载量: 13
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 33
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-04-07