Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate mll-lab/microplate and mll-lab/liquid-handling-robotics #16

Merged
merged 16 commits into from
Apr 24, 2024
4 changes: 4 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
php-version:
- "7.4"
Expand Down Expand Up @@ -74,6 +75,9 @@ jobs:
extensions: mbstring
php-version: "${{ matrix.php-version }}"

- if: "! startsWith(matrix.php-version, 8)"
run: composer remove --dev --no-update mll-lab/graphql-php-scalars

- run: composer require "illuminate/support:${{ matrix.illuminate }}" --no-interaction --no-update

- if: matrix.dependencies == 'lowest'
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ See [GitHub releases](https://github.com/mll-lab/php-utils/releases).

## Unreleased

## v1.13.0

### Added

- Integrate `mll-lab/microplate` and `mll-lab/liquid-handling-robotics`

## v1.12.0

### Added
Expand Down
8 changes: 7 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,19 @@
"php": "^7.4 || ^8",
"ext-calendar": "*",
"illuminate/support": "^8.73 || ^9 || ^10",
"mll-lab/microplate": "^6",
"mll-lab/str_putcsv": "^1",
"nesbot/carbon": "^2.62.1",
"thecodingmachine/safe": "^1 || ^2"
},
"require-dev": {
"ergebnis/composer-normalize": "^2",
"illuminate/database": "^8.73 || ^9 || ^10",
simbig marked this conversation as resolved.
Show resolved Hide resolved
"infection/infection": "^0.26 || ^0.27",
"jangregor/phpstan-prophecy": "^1",
"mll-lab/graphql-php-scalars": "^6",
"mll-lab/php-cs-fixer-config": "^5",
"nunomaduro/larastan": "^1 || ^2",
"orchestra/testbench": "^5 || ^6 || ^7 || ^8",
"phpstan/extension-installer": "^1",
"phpstan/phpstan": "^1",
"phpstan/phpstan-deprecation-rules": "^1",
Expand All @@ -37,6 +40,9 @@
"rector/rector": "^0.17",
"thecodingmachine/phpstan-safe-rule": "^1.2"
},
"suggest": {
"mll-lab/graphql-php-scalars": "To use the provided scalar types for GraphQL servers, requires version ^6"
},
"autoload": {
"psr-4": {
"MLL\\Utils\\": "src/"
Expand Down
6 changes: 2 additions & 4 deletions src/CSVArray.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@

use Illuminate\Support\Arr;

/**
* @phpstan-type CSVPrimitive bool|float|int|string|\Stringable|null
*/
final class CSVArray
/** @phpstan-type CSVPrimitive bool|float|int|string|\Stringable|null */
class CSVArray
{
/**
* TODO: fix parsing multiline-content in csv.
Expand Down
68 changes: 68 additions & 0 deletions src/FluidXPlate/FluidXPlate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php declare(strict_types=1);

namespace MLL\Utils\FluidXPlate;

use Illuminate\Support\Collection;
use MLL\Utils\Microplate\Coordinates;
use MLL\Utils\Microplate\CoordinateSystem96Well;
use MLL\Utils\Microplate\Enums\FlowDirection;
use MLL\Utils\Microplate\Microplate;

class FluidXPlate
{
public const FLUIDX_BARCODE_REGEX = /* @lang RegExp */ '/' . self::FLUIDX_BARCODE_REGEX_WITHOUT_DELIMITER . '/';
public const FLUIDX_BARCODE_REGEX_WITHOUT_DELIMITER = '[A-Z]{2}(\d){8}';

public string $rackID;

/** @var Microplate<string, CoordinateSystem96Well> */
private Microplate $microplate;

public function __construct(string $rackID)
{
if (\Safe\preg_match(self::FLUIDX_BARCODE_REGEX, $rackID) === 0) {
throw new InvalidRackIDException($rackID);
}
$this->rackID = $rackID;
$this->microplate = new Microplate(self::coordinateSystem());
}

public static function coordinateSystem(): CoordinateSystem96Well
{
return new CoordinateSystem96Well();
}

/** @param Coordinates<CoordinateSystem96Well> $coordinates */
public function addWell(Coordinates $coordinates, string $barcode): void
{
if (\Safe\preg_match(self::FLUIDX_BARCODE_REGEX, $barcode) === 0) {
throw new InvalidTubeBarcodeException($barcode);
}

$this->microplate->addWell($coordinates, $barcode);
}

/** @return Coordinates<CoordinateSystem96Well> */
public function addToNextFreeWell(string $content, FlowDirection $flowDirection): Coordinates
{
return $this->microplate->addToNextFreeWell($content, $flowDirection);
}

/** @return Collection<string, string|null> */
public function wells(): Collection
{
return $this->microplate->wells();
}

/** @return Collection<string, null> */
public function freeWells(): Collection
{
return $this->microplate->freeWells();
}

/** @return Collection<string, string> */
public function filledWells(): Collection
{
return $this->microplate->filledWells();
}
}
5 changes: 5 additions & 0 deletions src/FluidXPlate/FluidXPlateException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php declare(strict_types=1);

namespace MLL\Utils\FluidXPlate;

abstract class FluidXPlateException extends \Exception {}
101 changes: 101 additions & 0 deletions src/FluidXPlate/FluidXScanner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php declare(strict_types=1);

namespace MLL\Utils\FluidXPlate;

use Illuminate\Support\Str;
use MLL\Utils\Microplate\Coordinates;
use MLL\Utils\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 ($ip === self::LOCALHOST) {
return $this->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 === '' || $line === self::READING || $line === self::XTR_96_CONNECTED) {
continue;
}
$content = explode(', ', $line);
if (count($content) <= 3) {
continue;
}

// All valid lines contain the same plate barcode
$id = $content[3];
if ($id === FluidXScanner::NO_READ && isset($content[4])) {
$id = $content[4];
}

$barcodeScanResult = $content[1];
$coordinatesString = $content[0];
if ($barcodeScanResult !== self::NO_READ && $barcodeScanResult !== self::NO_TUBE) {
$barcodes[$coordinatesString] = $barcodeScanResult;
}
}

if (is_null($id)) {
throw new ScanFluidXPlateException('Der Scanner lieferte keinen Plattenbarcode zurück.');
}

if ($id === FluidXScanner::NO_READ) {
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 $coordinates => $barcode) {
$plate->addWell(Coordinates::fromString($coordinates, new CoordinateSystem96Well()), $barcode);
}

return $plate;
}

private function returnTestPlate(): FluidXPlate
{
return self::parseRawContent(\Safe\file_get_contents(__DIR__ . '/TestPlate.txt'));
}
}
11 changes: 11 additions & 0 deletions src/FluidXPlate/InvalidRackIDException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php declare(strict_types=1);

namespace MLL\Utils\FluidXPlate;

class InvalidRackIDException extends FluidXPlateException
{
public function __construct(string $rackID)
{
parent::__construct("Invalid rack ID: {$rackID}");
}
}
11 changes: 11 additions & 0 deletions src/FluidXPlate/InvalidTubeBarcodeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php declare(strict_types=1);

namespace MLL\Utils\FluidXPlate;

class InvalidTubeBarcodeException extends FluidXPlateException
{
public function __construct(string $tubeBarcode)
{
parent::__construct("Invalid tube barcode: {$tubeBarcode}");
}
}
16 changes: 16 additions & 0 deletions src/FluidXPlate/Scalars/FluidXBarcode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php declare(strict_types=1);

namespace MLL\Utils\FluidXPlate\Scalars;

use MLL\GraphQLScalars\Regex;
use MLL\Utils\FluidXPlate\FluidXPlate;

class FluidXBarcode extends Regex
{
public ?string $description = 'A valid barcode for FluidX-Tubes or FluidX-Plates represented as a string, e.g. `XR12345678`.';

public static function regex(): string
{
return FluidXPlate::FLUIDX_BARCODE_REGEX;
}
}
17 changes: 17 additions & 0 deletions src/FluidXPlate/Scalars/FrameStarBarcode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php declare(strict_types=1);

namespace MLL\Utils\FluidXPlate\Scalars;

use MLL\GraphQLScalars\Regex;

class FrameStarBarcode extends Regex
{
public const FRAME_STAR_BARCODE_REGEX = /* @lang RegExp */ '/[A-Z]{2}(\d){6}/';

public ?string $description = 'A valid barcode for a FrameStar plate represented as a string, e.g. `WD123456`.';

public static function regex(): string
{
return self::FRAME_STAR_BARCODE_REGEX;
}
}
5 changes: 5 additions & 0 deletions src/FluidXPlate/ScanFluidXPlateException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php declare(strict_types=1);

namespace MLL\Utils\FluidXPlate;

class ScanFluidXPlateException extends FluidXPlateException {}
Loading
Loading