added handling of LCSC barcode decoding and part loading on Label Scanner

This commit is contained in:
swdee 2026-01-16 13:48:59 +13:00
parent 766ba07105
commit 2e793e89fa
6 changed files with 205 additions and 2 deletions

View file

@ -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',
},
]);

View file

@ -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
*/

View file

@ -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);

View file

@ -42,4 +42,7 @@ enum BarcodeSourceType
* EIGP114 formatted barcodes like used by digikey, mouser, etc.
*/
case EIGP114;
}
/** For LCSC.com formatted QR codes */
case LCSC;
}

View file

@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace App\Services\LabelSystem\BarcodeScanner;
use InvalidArgumentException;
/**
* This class represents the content of a lcsc.com barcode
* Its data structure is represented by {pbn:...,on:...,pc:...,pm:...,qty:...}
*/
class LCSCBarcodeScanResult implements BarcodeScanResultInterface
{
/**
* @param array<string, string> $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);
}
}

View file

@ -12027,6 +12027,12 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<target>EIGP 114 barcode (e.g. the datamatrix codes on digikey and mouser orders)</target>
</segment>
</unit>
<unit id="lAzDdr" name="scan_dialog.mode.lcsc">
<segment state="translated">
<source>scan_dialog.mode.lcsc</source>
<target>LCSC.com barcode</target>
</segment>
</unit>
<unit id="QSMS_Bd" name="scan_dialog.info_mode">
<segment state="translated">
<source>scan_dialog.info_mode</source>