diff --git a/CHANGELOG.md b/CHANGELOG.md index f03c658..e6cef4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## v5.1.0 + +### Added + +- Add `FluidXScanner`-class + ## v5.0.0 ### Added diff --git a/composer.lock b/composer.lock index 3834ec4..42f67e6 100644 --- a/composer.lock +++ b/composer.lock @@ -8253,26 +8253,26 @@ }, { "name": "orchestra/testbench", - "version": "v7.22.1", + "version": "v7.22.2", "source": { "type": "git", "url": "https://github.com/orchestral/testbench.git", - "reference": "987f5383f597a54f8d9868f98411899398348c02" + "reference": "2b46f51b61404313156f180a0b3ebda7233a309b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/orchestral/testbench/zipball/987f5383f597a54f8d9868f98411899398348c02", - "reference": "987f5383f597a54f8d9868f98411899398348c02", + "url": "https://api.github.com/repos/orchestral/testbench/zipball/2b46f51b61404313156f180a0b3ebda7233a309b", + "reference": "2b46f51b61404313156f180a0b3ebda7233a309b", "shasum": "" }, "require": { "fakerphp/faker": "^1.21", "laravel/framework": "^9.52.4", "mockery/mockery": "^1.5.1", - "orchestra/testbench-core": "^7.22.1", + "orchestra/testbench-core": "^7.22.2", "php": "^8.0", "phpunit/phpunit": "^9.5.10", - "spatie/laravel-ray": "^1.28", + "spatie/laravel-ray": "^1.32.4", "symfony/process": "^6.0.9", "symfony/yaml": "^6.0.9", "vlucas/phpdotenv": "^5.4.1" @@ -8306,22 +8306,22 @@ ], "support": { "issues": "https://github.com/orchestral/testbench/issues", - "source": "https://github.com/orchestral/testbench/tree/v7.22.1" + "source": "https://github.com/orchestral/testbench/tree/v7.22.2" }, - "time": "2023-02-24T01:07:22+00:00" + "time": "2023-03-23T09:07:28+00:00" }, { "name": "orchestra/testbench-core", - "version": "v7.22.1", + "version": "v7.22.2", "source": { "type": "git", "url": "https://github.com/orchestral/testbench-core.git", - "reference": "75b42f9130e7903ffa0ef8dbb466962ca6635261" + "reference": "c34bcaf4888cb680f76eaca11ec9c4a67c019a0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/orchestral/testbench-core/zipball/75b42f9130e7903ffa0ef8dbb466962ca6635261", - "reference": "75b42f9130e7903ffa0ef8dbb466962ca6635261", + "url": "https://api.github.com/repos/orchestral/testbench-core/zipball/c34bcaf4888cb680f76eaca11ec9c4a67c019a0b", + "reference": "c34bcaf4888cb680f76eaca11ec9c4a67c019a0b", "shasum": "" }, "require": { @@ -8333,9 +8333,9 @@ "laravel/pint": "^1.4", "mockery/mockery": "^1.5.1", "orchestra/canvas": "^7.0", - "phpstan/phpstan": "^1.9.14", + "phpstan/phpstan": "^1.10.7", "phpunit/phpunit": "^9.5.10", - "spatie/laravel-ray": "^1.28", + "spatie/laravel-ray": "^1.32.4", "symfony/process": "^6.0.9", "symfony/yaml": "^6.0.9", "vlucas/phpdotenv": "^5.4.1" @@ -8394,7 +8394,7 @@ "issues": "https://github.com/orchestral/testbench/issues", "source": "https://github.com/orchestral/testbench-core" }, - "time": "2023-02-23T12:15:23+00:00" + "time": "2023-03-23T08:52:15+00:00" }, { "name": "phar-io/manifest", @@ -9525,16 +9525,16 @@ }, { "name": "rector/rector", - "version": "0.15.21", + "version": "0.15.23", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "1cee8cc5d6d836e1bf9a3006d7b062adde3a6022" + "reference": "f4984ebd62b3613002869b0ddd6868261d62819e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/1cee8cc5d6d836e1bf9a3006d7b062adde3a6022", - "reference": "1cee8cc5d6d836e1bf9a3006d7b062adde3a6022", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/f4984ebd62b3613002869b0ddd6868261d62819e", + "reference": "f4984ebd62b3613002869b0ddd6868261d62819e", "shasum": "" }, "require": { @@ -9574,7 +9574,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/0.15.21" + "source": "https://github.com/rectorphp/rector/tree/0.15.23" }, "funding": [ { @@ -9582,7 +9582,7 @@ "type": "github" } ], - "time": "2023-03-06T11:44:29+00:00" + "time": "2023-03-22T15:22:45+00:00" }, { "name": "sanmai/later", @@ -10735,16 +10735,16 @@ }, { "name": "spatie/laravel-ray", - "version": "1.32.3", + "version": "1.32.4", "source": { "type": "git", "url": "https://github.com/spatie/laravel-ray.git", - "reference": "8c7ea86c8092bcfe7a046f640b6ac9e5d7ec98cd" + "reference": "2274653f0a90dd87fbb887437be1c1ea1388a47c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-ray/zipball/8c7ea86c8092bcfe7a046f640b6ac9e5d7ec98cd", - "reference": "8c7ea86c8092bcfe7a046f640b6ac9e5d7ec98cd", + "url": "https://api.github.com/repos/spatie/laravel-ray/zipball/2274653f0a90dd87fbb887437be1c1ea1388a47c", + "reference": "2274653f0a90dd87fbb887437be1c1ea1388a47c", "shasum": "" }, "require": { @@ -10804,7 +10804,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-ray/issues", - "source": "https://github.com/spatie/laravel-ray/tree/1.32.3" + "source": "https://github.com/spatie/laravel-ray/tree/1.32.4" }, "funding": [ { @@ -10816,7 +10816,7 @@ "type": "other" } ], - "time": "2023-03-03T13:37:21+00:00" + "time": "2023-03-23T08:04:54+00:00" }, { "name": "spatie/macroable", diff --git a/rector.php b/rector.php index a88fbd1..881e15c 100644 --- a/rector.php +++ b/rector.php @@ -2,6 +2,7 @@ use Rector\Config\RectorConfig; use Rector\Core\ValueObject\PhpVersion; +use Rector\Privatization\Rector\Class_\FinalizeClassesWithoutChildrenRector; use Rector\Set\ValueObject\SetList; use Rector\TypeDeclaration\Rector\Closure\AddClosureReturnTypeRector; @@ -18,4 +19,10 @@ ]); $config->paths([__DIR__ . '/src', __DIR__ . '/tests']); $config->phpVersion(PhpVersion::PHP_74); + + $config->skip([ + FinalizeClassesWithoutChildrenRector::class => [ + __DIR__ . '/src/FluidXPlate/FluidXScanner.php', // enabled for mocking + ], + ]); }; diff --git a/src/FluidXPlate/FluidXScanner.php b/src/FluidXPlate/FluidXScanner.php new file mode 100644 index 0000000..2305c70 --- /dev/null +++ b/src/FluidXPlate/FluidXScanner.php @@ -0,0 +1,103 @@ +<?php declare(strict_types=1); + +namespace Mll\LiquidHandlingRobotics\FluidXPlate; + +use Illuminate\Support\Str; +use Mll\Microplate\Coordinate; +use Mll\Microplate\CoordinateSystem96Well; +use MLL\Utils\StringUtil; + +/** + * Communicates with a FluidX scanner device and fetches results from it. + */ +class FluidXScanner +{ + private const READING = 'Reading...'; + private const XTR_96_CONNECTED = 'xtr-96 Connected'; + private const NO_READ = 'NO READ'; + private const NO_TUBE = 'NO TUBE'; + public const LOCALHOST = '127.0.0.1'; + + public function scanPlate(string $ip): FluidXPlate + { + if (self::LOCALHOST === $ip) { + return self::returnTestPlate(); + } + + if ('' === $ip) { + throw new ScanFluidXPlateException('Cannot start scan request without an IP address.'); + } + + try { + $socket = \Safe\fsockopen($ip, 8001, $errno, $errstr, 30); + } catch (\Throwable $e) { + throw new ScanFluidXPlateException("Cannot reach FluidX Scanner {$ip}: {$e->getMessage()}. Verify that the FluidX Scanner is turned on and the FluidX software is started.", 0, $e); + } + + \Safe\fwrite($socket, "get\r\n"); + + $answer = ''; + do { + $content = fgets($socket); + $answer .= $content; + } while (is_string($content) && ! Str::contains($content, 'H12')); + + \Safe\fclose($socket); + + return self::parseRawContent($answer); + } + + public static function parseRawContent(string $rawContent): FluidXPlate + { + if ('' === $rawContent) { + throw new ScanFluidXPlateException('Der Scanner lieferte ein leeres Ergebnis zurück.'); + } + + $lines = StringUtil::splitLines($rawContent); + $barcodes = []; + $id = null; + foreach ($lines as $line) { + if ('' === $line || self::READING === $line || self::XTR_96_CONNECTED === $line) { + continue; + } + $content = explode(', ', $line); + if (count($content) <= 3) { + continue; + } + + // All valid lines contain the same plate barcode + $id = $content[3]; + if (FluidXScanner::NO_READ === $id && isset($content[4])) { + $id = $content[4]; + } + + $barcodeScanResult = $content[1]; + $coordinateString = $content[0]; + if (self::NO_READ !== $barcodeScanResult && self::NO_TUBE !== $barcodeScanResult) { + $barcodes[$coordinateString] = $barcodeScanResult; + } + } + + if (is_null($id)) { + throw new ScanFluidXPlateException('Der Scanner lieferte keinen Plattenbarcode zurück.'); + } + + if (FluidXScanner::NO_READ === $id) { + throw new ScanFluidXPlateException([] === $barcodes + ? 'Weder Platten-Barcode noch Tube-Barcodes konnten gescannt werden. Bitte überprüfen Sie, dass die Platte korrekt in den FluidX-Scanner eingelegt wurde.' + : 'Platten-Barcode konnte nicht gescannt werden. Bitte überprüfen Sie, dass die Platte mit der korrekten Orientierung in den FluidX-Scanner eingelegt wurde.'); + } + + $plate = new FluidXPlate($id); + foreach ($barcodes as $coordinate => $barcode) { + $plate->addWell(Coordinate::fromString($coordinate, new CoordinateSystem96Well()), $barcode); + } + + return $plate; + } + + private static function returnTestPlate(): FluidXPlate + { + return self::parseRawContent(\Safe\file_get_contents(__DIR__ . '/TestPlate.txt')); + } +} diff --git a/src/FluidXPlate/ScanFluidXPlateException.php b/src/FluidXPlate/ScanFluidXPlateException.php new file mode 100644 index 0000000..4399603 --- /dev/null +++ b/src/FluidXPlate/ScanFluidXPlateException.php @@ -0,0 +1,7 @@ +<?php declare(strict_types=1); + +namespace Mll\LiquidHandlingRobotics\FluidXPlate; + +final class ScanFluidXPlateException extends FluidXPlateException +{ +} diff --git a/src/FluidXPlate/TestPlate.txt b/src/FluidXPlate/TestPlate.txt new file mode 100644 index 0000000..71b4edc --- /dev/null +++ b/src/FluidXPlate/TestPlate.txt @@ -0,0 +1,96 @@ +A1, FD20024619, 1d_rackid_1, SA00826894 +A2, FD20024698, 1d_rackid_1, SA00826894 +A3, FD20024711, 1d_rackid_1, SA00826894 +A4, NO READ, 1d_rackid_1, SA00826894 +A5, NO READ, 1d_rackid_1, SA00826894 +A6, NO READ, 1d_rackid_1, SA00826894 +A7, NO READ, 1d_rackid_1, SA00826894 +A8, NO READ, 1d_rackid_1, SA00826894 +A9, NO READ, 1d_rackid_1, SA00826894 +A10, NO READ, 1d_rackid_1, SA00826894 +A11, NO READ, 1d_rackid_1, SA00826894 +A12, NO READ, 1d_rackid_1, SA00826894 +B1, NO READ, 1d_rackid_1, SA00826894 +B2, NO READ, 1d_rackid_1, SA00826894 +B3, NO READ, 1d_rackid_1, SA00826894 +B4, NO READ, 1d_rackid_1, SA00826894 +B5, NO READ, 1d_rackid_1, SA00826894 +B6, NO READ, 1d_rackid_1, SA00826894 +B7, NO READ, 1d_rackid_1, SA00826894 +B8, NO READ, 1d_rackid_1, SA00826894 +B9, NO READ, 1d_rackid_1, SA00826894 +B10, NO READ, 1d_rackid_1, SA00826894 +B11, NO READ, 1d_rackid_1, SA00826894 +B12, NO READ, 1d_rackid_1, SA00826894 +C1, NO READ, 1d_rackid_1, SA00826894 +C2, NO READ, 1d_rackid_1, SA00826894 +C3, NO READ, 1d_rackid_1, SA00826894 +C4, NO READ, 1d_rackid_1, SA00826894 +C5, NO READ, 1d_rackid_1, SA00826894 +C6, NO READ, 1d_rackid_1, SA00826894 +C7, NO READ, 1d_rackid_1, SA00826894 +C8, NO READ, 1d_rackid_1, SA00826894 +C9, NO READ, 1d_rackid_1, SA00826894 +C10, NO READ, 1d_rackid_1, SA00826894 +C11, NO READ, 1d_rackid_1, SA00826894 +C12, NO READ, 1d_rackid_1, SA00826894 +D1, NO READ, 1d_rackid_1, SA00826894 +D2, NO READ, 1d_rackid_1, SA00826894 +D3, NO READ, 1d_rackid_1, SA00826894 +D4, NO READ, 1d_rackid_1, SA00826894 +D5, NO READ, 1d_rackid_1, SA00826894 +D6, NO READ, 1d_rackid_1, SA00826894 +D7, NO READ, 1d_rackid_1, SA00826894 +D8, NO READ, 1d_rackid_1, SA00826894 +D9, NO READ, 1d_rackid_1, SA00826894 +D10, NO READ, 1d_rackid_1, SA00826894 +D11, NO READ, 1d_rackid_1, SA00826894 +D12, NO READ, 1d_rackid_1, SA00826894 +E1, NO READ, 1d_rackid_1, SA00826894 +E2, NO READ, 1d_rackid_1, SA00826894 +E3, NO READ, 1d_rackid_1, SA00826894 +E4, NO READ, 1d_rackid_1, SA00826894 +E5, NO READ, 1d_rackid_1, SA00826894 +E6, NO READ, 1d_rackid_1, SA00826894 +E7, NO READ, 1d_rackid_1, SA00826894 +E8, NO READ, 1d_rackid_1, SA00826894 +E9, NO READ, 1d_rackid_1, SA00826894 +E10, NO READ, 1d_rackid_1, SA00826894 +E11, NO READ, 1d_rackid_1, SA00826894 +E12, NO READ, 1d_rackid_1, SA00826894 +F1, NO READ, 1d_rackid_1, SA00826894 +F2, NO READ, 1d_rackid_1, SA00826894 +F3, NO READ, 1d_rackid_1, SA00826894 +F4, NO READ, 1d_rackid_1, SA00826894 +F5, NO READ, 1d_rackid_1, SA00826894 +F6, NO READ, 1d_rackid_1, SA00826894 +F7, NO READ, 1d_rackid_1, SA00826894 +F8, NO READ, 1d_rackid_1, SA00826894 +F9, NO READ, 1d_rackid_1, SA00826894 +F10, NO READ, 1d_rackid_1, SA00826894 +F11, NO READ, 1d_rackid_1, SA00826894 +F12, NO READ, 1d_rackid_1, SA00826894 +G1, NO READ, 1d_rackid_1, SA00826894 +G2, NO READ, 1d_rackid_1, SA00826894 +G3, NO READ, 1d_rackid_1, SA00826894 +G4, NO READ, 1d_rackid_1, SA00826894 +G5, NO READ, 1d_rackid_1, SA00826894 +G6, NO READ, 1d_rackid_1, SA00826894 +G7, NO READ, 1d_rackid_1, SA00826894 +G8, NO READ, 1d_rackid_1, SA00826894 +G9, NO READ, 1d_rackid_1, SA00826894 +G10, NO READ, 1d_rackid_1, SA00826894 +G11, NO READ, 1d_rackid_1, SA00826894 +G12, NO READ, 1d_rackid_1, SA00826894 +H1, NO READ, 1d_rackid_1, SA00826894 +H2, NO READ, 1d_rackid_1, SA00826894 +H3, NO READ, 1d_rackid_1, SA00826894 +H4, NO READ, 1d_rackid_1, SA00826894 +H5, NO READ, 1d_rackid_1, SA00826894 +H6, NO READ, 1d_rackid_1, SA00826894 +H7, NO READ, 1d_rackid_1, SA00826894 +H8, NO READ, 1d_rackid_1, SA00826894 +H9, NO READ, 1d_rackid_1, SA00826894 +H10, NO READ, 1d_rackid_1, SA00826894 +H11, NO READ, 1d_rackid_1, SA00826894 +H12, NO READ, 1d_rackid_1, SA00826894 diff --git a/tests/Unit/FluidXPlate/FluidXScannerTest.php b/tests/Unit/FluidXPlate/FluidXScannerTest.php new file mode 100644 index 0000000..c6688aa --- /dev/null +++ b/tests/Unit/FluidXPlate/FluidXScannerTest.php @@ -0,0 +1,22 @@ +<?php declare(strict_types=1); + +namespace FluidXPlate; + +use Mll\LiquidHandlingRobotics\FluidXPlate\FluidXScanner; +use PHPUnit\Framework\TestCase; + +final class FluidXScannerTest extends TestCase +{ + public function testCreateFromStringEmpty(): void + { + $fluidXScanner = new FluidXScanner(); + $fluidXPlate = $fluidXScanner->scanPlate(FluidXScanner::LOCALHOST); + + self::assertSame('SA00826894', $fluidXPlate->rackId); + $filledWells = $fluidXPlate->filledWells(); + self::assertCount(3, $filledWells); + self::assertSame('FD20024619', $filledWells->get('A1')); + self::assertSame('FD20024698', $filledWells->get('A2')); + self::assertSame('FD20024711', $filledWells->get('A3')); + } +}