From 2e793e89fab30e854ef8f98ecfa8bd5d5065875c Mon Sep 17 00:00:00 2001 From: swdee Date: Fri, 16 Jan 2026 13:48:59 +1300 Subject: [PATCH] added handling of LCSC barcode decoding and part loading on Label Scanner --- src/Form/LabelSystem/ScanDialogType.php | 3 +- .../BarcodeScanner/BarcodeRedirector.php | 53 ++++++++ .../BarcodeScanner/BarcodeScanHelper.php | 13 ++ .../BarcodeScanner/BarcodeSourceType.php | 5 +- .../BarcodeScanner/LCSCBarcodeScanResult.php | 127 ++++++++++++++++++ translations/messages.en.xlf | 6 + 6 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 src/Services/LabelSystem/BarcodeScanner/LCSCBarcodeScanResult.php diff --git a/src/Form/LabelSystem/ScanDialogType.php b/src/Form/LabelSystem/ScanDialogType.php index 13ff8e6f..0a67467f 100644 --- a/src/Form/LabelSystem/ScanDialogType.php +++ b/src/Form/LabelSystem/ScanDialogType.php @@ -75,7 +75,8 @@ class ScanDialogType extends AbstractType BarcodeSourceType::INTERNAL => 'scan_dialog.mode.internal', BarcodeSourceType::IPN => 'scan_dialog.mode.ipn', BarcodeSourceType::USER_DEFINED => 'scan_dialog.mode.user', - BarcodeSourceType::EIGP114 => 'scan_dialog.mode.eigp' + BarcodeSourceType::EIGP114 => 'scan_dialog.mode.eigp', + BarcodeSourceType::LCSC => 'scan_dialog.mode.lcsc', }, ]); diff --git a/src/Services/LabelSystem/BarcodeScanner/BarcodeRedirector.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeRedirector.php index 2de7c035..1364e6c1 100644 --- a/src/Services/LabelSystem/BarcodeScanner/BarcodeRedirector.php +++ b/src/Services/LabelSystem/BarcodeScanner/BarcodeRedirector.php @@ -45,6 +45,7 @@ use App\Entity\LabelSystem\LabelSupportedElement; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\Part; use App\Entity\Parts\PartLot; +use App\Repository\Parts\PartRepository; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityNotFoundException; use InvalidArgumentException; @@ -77,6 +78,10 @@ final class BarcodeRedirector return $this->getURLVendorBarcode($barcodeScan); } + if ($barcodeScan instanceof LCSCBarcodeScanResult) { + return $this->getURLLCSCBarcode($barcodeScan); + } + throw new InvalidArgumentException('Unknown $barcodeScan type: '.get_class($barcodeScan)); } @@ -102,6 +107,54 @@ final class BarcodeRedirector } } + /** + * Gets the URL to a part from a scan of the LCSC Barcode + */ + private function getURLLCSCBarcode(LCSCBarcodeScanResult $barcodeScan): string + { + $part = $this->getPartFromLCSC($barcodeScan); + return $this->urlGenerator->generate('app_part_show', ['id' => $part->getID()]); + } + + /** + * Resolve LCSC barcode -> Part. + * Strategy: + * 1) Try providerReference.provider_id == pc (LCSC "Cxxxxxx") if you store it there + * 2) Fallback to manufacturer_product_number == pm (MPN) + * Returns first match (consistent with EIGP114 logic) + */ + private function getPartFromLCSC(LCSCBarcodeScanResult $barcodeScan): Part + { + // Try LCSC code (pc) as provider id if available + $pc = $barcodeScan->getPC(); // e.g. C138033 + if ($pc) { + $qb = $this->em->getRepository(Part::class)->createQueryBuilder('part'); + $qb->where($qb->expr()->like('LOWER(part.providerReference.provider_id)', 'LOWER(:vendor_id)')); + $qb->setParameter('vendor_id', $pc); + $results = $qb->getQuery()->getResult(); + if ($results) { + return $results[0]; + } + } + + // Fallback to MPN (pm) + $pm = $barcodeScan->getPM(); // e.g. RC0402FR-071ML + if (!$pm) { + throw new EntityNotFoundException(); + } + + $mpnQb = $this->em->getRepository(Part::class)->createQueryBuilder('part'); + $mpnQb->where($mpnQb->expr()->like('LOWER(part.manufacturer_product_number)', 'LOWER(:mpn)')); + $mpnQb->setParameter('mpn', $pm); + + $results = $mpnQb->getQuery()->getResult(); + if ($results) { + return $results[0]; + } + + throw new EntityNotFoundException(); + } + /** * Gets the URL to a part from a scan of a Vendor Barcode */ diff --git a/src/Services/LabelSystem/BarcodeScanner/BarcodeScanHelper.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanHelper.php index e5930b36..c9cce95a 100644 --- a/src/Services/LabelSystem/BarcodeScanner/BarcodeScanHelper.php +++ b/src/Services/LabelSystem/BarcodeScanner/BarcodeScanHelper.php @@ -92,6 +92,9 @@ final class BarcodeScanHelper if ($type === BarcodeSourceType::EIGP114) { return $this->parseEIGP114Barcode($input); } + if ($type === BarcodeSourceType::LCSC) { + return $this->parseLCSCBarcode($input); + } //Null means auto and we try the different formats $result = $this->parseInternalBarcode($input); @@ -117,6 +120,11 @@ final class BarcodeScanHelper return $result; } + // Try LCSC barcode + if (LCSCBarcodeScanResult::looksLike($input)) { + return $this->parseLCSCBarcode($input); + } + throw new InvalidArgumentException('Unknown barcode'); } @@ -125,6 +133,11 @@ final class BarcodeScanHelper return EIGP114BarcodeScanResult::parseFormat06Code($input); } + private function parseLCSCBarcode(string $input): LCSCBarcodeScanResult + { + return LCSCBarcodeScanResult::parse($input); + } + private function parseUserDefinedBarcode(string $input): ?LocalBarcodeScanResult { $lot_repo = $this->entityManager->getRepository(PartLot::class); diff --git a/src/Services/LabelSystem/BarcodeScanner/BarcodeSourceType.php b/src/Services/LabelSystem/BarcodeScanner/BarcodeSourceType.php index 40f707de..8f1dc72e 100644 --- a/src/Services/LabelSystem/BarcodeScanner/BarcodeSourceType.php +++ b/src/Services/LabelSystem/BarcodeScanner/BarcodeSourceType.php @@ -42,4 +42,7 @@ enum BarcodeSourceType * EIGP114 formatted barcodes like used by digikey, mouser, etc. */ case EIGP114; -} \ No newline at end of file + + /** For LCSC.com formatted QR codes */ + case LCSC; +} diff --git a/src/Services/LabelSystem/BarcodeScanner/LCSCBarcodeScanResult.php b/src/Services/LabelSystem/BarcodeScanner/LCSCBarcodeScanResult.php new file mode 100644 index 00000000..9a87951f --- /dev/null +++ b/src/Services/LabelSystem/BarcodeScanner/LCSCBarcodeScanResult.php @@ -0,0 +1,127 @@ + $fields + */ + public function __construct( + public readonly array $fields, + public readonly string $raw_input, + ) {} + + public function getSourceType(): BarcodeSourceType + { + return BarcodeSourceType::LCSC; + } + + /** + * @return string|null The manufactures part number + */ + public function getPM(): ?string + { + $v = $this->fields['pm'] ?? null; + $v = $v !== null ? trim($v) : null; + return ($v === '') ? null : $v; + } + + /** + * @return string|null The lcsc.com part number + */ + public function getPC(): ?string + { + $v = $this->fields['pc'] ?? null; + $v = $v !== null ? trim($v) : null; + return ($v === '') ? null : $v; + } + + /** + * @return array|float[]|int[]|null[]|string[] An array of fields decoded from the barcode + */ + public function getDecodedForInfoMode(): array + { + // Keep it human-friendly + return [ + 'Barcode type' => 'LCSC', + 'MPN (pm)' => $this->getPM() ?? '', + 'LCSC code (pc)' => $this->getPC() ?? '', + 'Qty' => $this->fields['qty'] ?? '', + 'Order No (on)' => $this->fields['on'] ?? '', + 'Pick Batch (pbn)' => $this->fields['pbn'] ?? '', + 'Warehouse (wc)' => $this->fields['wc'] ?? '', + 'Country/Channel (cc)' => $this->fields['cc'] ?? '', + ]; + } + + /** + * Parses the barcode data to see if the input matches the expected format used by lcsc.com + * @param string $input + * @return bool + */ + public static function looksLike(string $input): bool + { + $s = trim($input); + + // Your example: {pbn:...,on:...,pc:...,pm:...,qty:...} + if (!str_starts_with($s, '{') || !str_ends_with($s, '}')) { + return false; + } + + // Must contain at least pm: and pc: (common for LCSC labels) + return (stripos($s, 'pm:') !== false) && (stripos($s, 'pc:') !== false); + } + + /** + * Parse the barcode input string into the fields used by lcsc.com + * @param string $input + * @return self + */ + public static function parse(string $input): self + { + $raw = trim($input); + + if (!self::looksLike($raw)) { + throw new InvalidArgumentException('Not an LCSC barcode'); + } + + $inner = trim($raw); + $inner = substr($inner, 1, -1); // remove { } + + $fields = []; + + // This format is comma-separated pairs, values do not contain commas in your sample. + $pairs = array_filter(array_map('trim', explode(',', $inner))); + + foreach ($pairs as $pair) { + $pos = strpos($pair, ':'); + if ($pos === false) { + continue; + } + + $k = trim(substr($pair, 0, $pos)); + $v = trim(substr($pair, $pos + 1)); + + if ($k === '') { + continue; + } + + $fields[$k] = $v; + } + + if (!isset($fields['pm']) || trim($fields['pm']) === '') { + throw new InvalidArgumentException('LCSC barcode missing pm field'); + } + + return new self($fields, $raw); + } +} diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index f7f10146..0e79a6ed 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -12027,6 +12027,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g EIGP 114 barcode (e.g. the datamatrix codes on digikey and mouser orders) + + + scan_dialog.mode.lcsc + LCSC.com barcode + + scan_dialog.info_mode