diff --git a/Binner/Binner.Web/ClientApp/package-lock.json b/Binner/Binner.Web/ClientApp/package-lock.json index 68053831..5219a74f 100644 --- a/Binner/Binner.Web/ClientApp/package-lock.json +++ b/Binner/Binner.Web/ClientApp/package-lock.json @@ -64,6 +64,7 @@ "smooth-scroll": "^16.1.3", "tiny-slider": "^2.9.4", "underscore": "^1.13.6", + "use-sound": "^4.0.1", "uuid": "^9.0.0", "yarn": "^1.22.19" }, @@ -12031,6 +12032,11 @@ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, + "node_modules/howler": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.3.tgz", + "integrity": "sha512-QM0FFkw0LRX1PR8pNzJVAY25JhIWvbKMBFM4gqk+QdV+kPXOhleWGCB6AiAF/goGjIHK2e/nIElplvjQwhr0jg==" + }, "node_modules/hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", @@ -22824,6 +22830,17 @@ "node": ">=0.10.0" } }, + "node_modules/use-sound": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/use-sound/-/use-sound-4.0.1.tgz", + "integrity": "sha512-hykJ86kNcu6y/FzlSHcQxhjSGMslZx2WlfLpZNoPbvueakv4OF3xPxEtGV2YmculrIaH0tPp9LtG4jgy17xMWg==", + "dependencies": { + "howler": "^2.1.3" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, "node_modules/util": { "version": "0.12.4", "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", @@ -33172,6 +33189,11 @@ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, + "howler": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.3.tgz", + "integrity": "sha512-QM0FFkw0LRX1PR8pNzJVAY25JhIWvbKMBFM4gqk+QdV+kPXOhleWGCB6AiAF/goGjIHK2e/nIElplvjQwhr0jg==" + }, "hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", @@ -41242,6 +41264,14 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, + "use-sound": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/use-sound/-/use-sound-4.0.1.tgz", + "integrity": "sha512-hykJ86kNcu6y/FzlSHcQxhjSGMslZx2WlfLpZNoPbvueakv4OF3xPxEtGV2YmculrIaH0tPp9LtG4jgy17xMWg==", + "requires": { + "howler": "^2.1.3" + } + }, "util": { "version": "0.12.4", "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", diff --git a/Binner/Binner.Web/ClientApp/package.json b/Binner/Binner.Web/ClientApp/package.json index c64b6e9e..64885288 100644 --- a/Binner/Binner.Web/ClientApp/package.json +++ b/Binner/Binner.Web/ClientApp/package.json @@ -59,6 +59,7 @@ "smooth-scroll": "^16.1.3", "tiny-slider": "^2.9.4", "underscore": "^1.13.6", + "use-sound": "^4.0.1", "uuid": "^9.0.0", "yarn": "^1.22.19" }, diff --git a/Binner/Binner.Web/ClientApp/src/App.js b/Binner/Binner.Web/ClientApp/src/App.js index 38dcf1a0..418183c7 100644 --- a/Binner/Binner.Web/ClientApp/src/App.js +++ b/Binner/Binner.Web/ClientApp/src/App.js @@ -29,6 +29,7 @@ import { Settings } from "./pages/Settings"; import { OhmsLawCalculator } from "./pages/tools/OhmsLawCalculator"; import { ResistorColorCodeCalculator } from "./pages/tools/ResistorColorCodeCalculator"; import { VoltageDividerCalculator } from "./pages/tools/VoltageDividerCalculator"; +import { BarcodeScanner } from "./pages/tools/BarcodeScanner"; import { Help } from './pages/help/Home'; import { Scanning } from './pages/help/Scanning'; import { ApiIntegrations } from './pages/help/ApiIntegrations'; @@ -85,6 +86,10 @@ export default class App extends Component { path="/tools/voltagedivider" element={} /> + } + /> } /> } /> } /> diff --git a/Binner/Binner.Web/ClientApp/src/components/BarcodeScannerInput.js b/Binner/Binner.Web/ClientApp/src/components/BarcodeScannerInput.js index 390848ee..54805e75 100644 --- a/Binner/Binner.Web/ClientApp/src/components/BarcodeScannerInput.js +++ b/Binner/Binner.Web/ClientApp/src/components/BarcodeScannerInput.js @@ -77,10 +77,15 @@ export function BarcodeScannerInput({listening, minInputLength, onReceived, help const processBarcodeInformation = (e, value) => { let barcodeType = "code128"; let parsedValue = {}; + let gsDetected = false; + let rsDetected = false; if (value.startsWith("[)>")) { // 2D DotMatrix barcode. Process into value. barcodeType = "datamatrix"; - parsedValue = parseDataMatrix(value); + const parseResult = parseDataMatrix(value); + parsedValue = parseResult.value; + gsDetected = parseResult.gsDetected; + rsDetected = parseResult.rsDetected; } else { // 1D barcode parsedValue = value.replace("\n", "").replace("\r", ""); @@ -89,7 +94,9 @@ export function BarcodeScannerInput({listening, minInputLength, onReceived, help return { type: barcodeType, value: parsedValue, - rawValue: value + rawValue: value, + rsDetected, + gsDetected }; }; @@ -102,6 +109,8 @@ export function BarcodeScannerInput({listening, minInputLength, onReceived, help const expectedFormatNumber = 6; /** 22z22 barcode */ const controlChars = ["P", "1P", "P1", "K", "1K", "10K", "11K", "4L", "Q", "11Z", "12Z", "13Z", "20Z"]; + let gsCodePresent = false; + let rsCodePresent = false; let formatNumber = ""; let buffer = ""; let i; @@ -111,6 +120,7 @@ export function BarcodeScannerInput({listening, minInputLength, onReceived, help if (buffer === header) { if (value.charCodeAt(i + 1) === rsCharCode) { // read the character after the RS token (sometimes not present) + rsCodePresent = true; formatNumberIndex = i + 2; } else { formatNumberIndex = i + 1; @@ -143,6 +153,9 @@ export function BarcodeScannerInput({listening, minInputLength, onReceived, help if (gsLine.length > 0) gsLines.push(gsLine); + if (gsLines.length > 0) + gsCodePresent = true; + // console.log('gsLines', gsLines); for (i = 0; i < gsLines.length; i++) { @@ -206,7 +219,11 @@ export function BarcodeScannerInput({listening, minInputLength, onReceived, help break; } } - return parsedValue; + return { + value: parsedValue, + gsDetected: gsCodePresent, + rsDetected: rsCodePresent + }; }; const scannerDebounced = useMemo(() => debounce(onReceivedBarcodeInput, BufferTimeMs), []); diff --git a/Binner/Binner.Web/ClientApp/src/pages/Inventory.js b/Binner/Binner.Web/ClientApp/src/pages/Inventory.js index bdda1130..d7dcc51b 100644 --- a/Binner/Binner.Web/ClientApp/src/pages/Inventory.js +++ b/Binner/Binner.Web/ClientApp/src/pages/Inventory.js @@ -152,6 +152,8 @@ export function Inventory(props) { bulkScanIsOpenRef.current = bulkScanIsOpen; const scannedPartsRef = useRef(); scannedPartsRef.current = scannedParts; + const partTypesRef = useRef(); + partTypesRef.current = partTypes; useEffect(() => { const partNumberStr = props.params.partNumber; @@ -174,12 +176,14 @@ export function Inventory(props) { }, [props.params.partNumber]); const fetchPartMetadata = async (input, part) => { + if (partTypesRef.current.length === 0) + console.error("There are no partTypes! This shouldn't happen and is a bug."); Inventory.infoAbortController.abort(); Inventory.infoAbortController = new AbortController(); setLoadingPartMetadata(true); setPartMetadataIsSubscribed(false); try { - const response = await fetchApi(`part/info?partNumber=${input}&partTypeId=${part.partTypeId}&mountingTypeId=${part.mountingTypeId}`, { + const response = await fetchApi(`part/info?partNumber=${input}&partTypeId=${part.partTypeId || "0"}&mountingTypeId=${part.mountingTypeId || "0"}`, { signal: Inventory.infoAbortController.signal }); const data = response.data; @@ -196,7 +200,7 @@ export function Inventory(props) { const suggestedPart = infoResponse.parts[0]; // populate the form with data from the part metadata - if(!isEditing) setPartFromMetadata(metadataParts, suggestedPart, partTypes); + if(!isEditing) setPartFromMetadata(metadataParts, suggestedPart); } else { // no part metadata available setPartMetadataIsSubscribed(true); @@ -217,7 +221,7 @@ export function Inventory(props) { } }; - const searchDebounced = useMemo(() => debounce(fetchPartMetadata, 1000), [partTypes, part]); + const searchDebounced = useMemo(() => debounce(fetchPartMetadata, 1000), []); const onUploadSubmit = async (uploadFiles, type) => { setUploading(true); @@ -388,7 +392,12 @@ export function Inventory(props) { // scan single part if (cleanPartNumber) { setPartMetadataIsSubscribed(false); - const newPart = {...part, partNumber: cleanPartNumber, quantity: input.value.quantity || "1"}; + const newPart = {...part, + partNumber: cleanPartNumber, + quantity: input.value.quantity || "1", + partTypeId: -1, + mountingTypeId: -1, + }; setPart(newPart); localStorage.setItem("viewPreferences", JSON.stringify({ ...viewPreferences, lastQuantity: newPart.quantity })); setShowBarcodeBeingScanned(false); @@ -702,15 +711,15 @@ export function Inventory(props) { updatedPart[control.name] = control.value; switch (control.name) { case "partNumber": - if (updatedPart.partNumber && updatedPart.partNumber.length > 0) searchDebounced(updatedPart.partNumber, updatedPart); + if (updatedPart.partNumber && updatedPart.partNumber.length > 0) searchDebounced(updatedPart.partNumber, updatedPart, partTypes); break; case "partTypeId": localStorage.setItem("viewPreferences", JSON.stringify({ ...viewPreferences, lastPartTypeId: control.value })); - if (updatedPart.partNumber && updatedPart.partNumber.length > 0) searchDebounced(updatedPart.partNumber, updatedPart); + if (updatedPart.partNumber && updatedPart.partNumber.length > 0) searchDebounced(updatedPart.partNumber, updatedPart, partTypes); break; case "mountingTypeId": localStorage.setItem("viewPreferences", JSON.stringify({ ...viewPreferences, lastMountingTypeId: control.value })); - if (updatedPart.partNumber && updatedPart.partNumber.length > 0) searchDebounced(updatedPart.partNumber, updatedPart); + if (updatedPart.partNumber && updatedPart.partNumber.length > 0) searchDebounced(updatedPart.partNumber, updatedPart, partTypes); break; case "lowStockThreshold": localStorage.setItem("viewPreferences", JSON.stringify({ ...viewPreferences, lowStockThreshold: control.value })); @@ -739,11 +748,14 @@ export function Inventory(props) { await fetchApi(`part/print?partNumber=${part.partNumber}&generateImageOnly=false`, { method: "POST" }); }; - const setPartFromMetadata = (metadataParts, suggestedPart, partTypes) => { + const setPartFromMetadata = (metadataParts, suggestedPart) => { + if (partTypesRef.current.length === 0) + console.error("There are no partTypes! This shouldn't happen and is a bug."); + const entity = { ...part }; const mappedPart = { partNumber: suggestedPart.basePartNumber, - partTypeId: getPartTypeId(suggestedPart.partType, partTypes), + partTypeId: getPartTypeId(suggestedPart.partType, partTypesRef.current), mountingTypeId: suggestedPart.mountingTypeId, packageType: suggestedPart.packageType, keywords: suggestedPart.keywords && suggestedPart.keywords.join(" ").toLowerCase(), @@ -812,7 +824,7 @@ export function Inventory(props) { }; const handleChooseAlternatePart = (e, part, partTypes) => { - setPartFromMetadata(metadataParts, part, partTypes); + setPartFromMetadata(metadataParts, part); setPartModalOpen(false); }; diff --git a/Binner/Binner.Web/ClientApp/src/pages/Tools.js b/Binner/Binner.Web/ClientApp/src/pages/Tools.js index 988fff97..fd1d92aa 100644 --- a/Binner/Binner.Web/ClientApp/src/pages/Tools.js +++ b/Binner/Binner.Web/ClientApp/src/pages/Tools.js @@ -34,6 +34,12 @@ export function Tools(props) { Voltage Divider Calculator + route(e, "/tools/barcodescanner")} style={{ cursor: "pointer" }}> + + + + Barcode Scanner + diff --git a/Binner/Binner.Web/ClientApp/src/pages/tools/BarcodeScanner.css b/Binner/Binner.Web/ClientApp/src/pages/tools/BarcodeScanner.css new file mode 100644 index 00000000..5034658e --- /dev/null +++ b/Binner/Binner.Web/ClientApp/src/pages/tools/BarcodeScanner.css @@ -0,0 +1,20 @@ +code { + background: #f4f4f4; + border: 1px solid #ddd; + border-left: 3px solid #f36d33; + color: #666; + page-break-inside: avoid; + font-family: monospace; + font-size: 15px; + line-height: 1.6; + margin-bottom: 1.6em; + max-width: 100%; + padding: 1em 1.5em; + display: block; + word-wrap: break-word !important; + white-space: pre-line !important; +} + +pre { + white-space: pre !important; +} \ No newline at end of file diff --git a/Binner/Binner.Web/ClientApp/src/pages/tools/BarcodeScanner.js b/Binner/Binner.Web/ClientApp/src/pages/tools/BarcodeScanner.js new file mode 100644 index 00000000..50fe39a8 --- /dev/null +++ b/Binner/Binner.Web/ClientApp/src/pages/tools/BarcodeScanner.js @@ -0,0 +1,35 @@ +import React, { useState, useEffect, useMemo } from "react"; +import { Link } from "react-router-dom"; +import _ from "underscore"; +import { Label, Button, Image, Form, Table, Segment, Dimmer, Checkbox, Loader, Popup } from "semantic-ui-react"; +import { toast } from "react-toastify"; +import { BarcodeScannerInput } from "../../components/BarcodeScannerInput"; +import "./BarcodeScanner.css"; + +export function BarcodeScanner(props) { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [message, setMessage] = useState(null); + const [isKeyboardListening, setIsKeyboardListening] = useState(true); + const [barcodeValue, setBarcodeValue] = useState('Waiting for input...'); + + // debounced handler for processing barcode scanner input + const handleBarcodeInput = (e, input) => { + // ignore single keypresses + setBarcodeValue(JSON.stringify(input, null, 2)); + toast.info(`Barcode type ${input.type} received`); + }; + + return ( +
+ +

Barcode Scanner

+

Test your barcode scanner to see what values it outputs.

+
+
+
{barcodeValue}
+
+
+
+ ); +} diff --git a/Binner/Library/Binner.Common/Integrations/DigikeyApi.cs b/Binner/Library/Binner.Common/Integrations/DigikeyApi.cs index 6e7a7e12..fe0196a6 100644 --- a/Binner/Library/Binner.Common/Integrations/DigikeyApi.cs +++ b/Binner/Library/Binner.Common/Integrations/DigikeyApi.cs @@ -437,7 +437,7 @@ private ICollection MapTaxonomies(string partType, MountingTypes pac { var taxonomies = new List(); var taxonomy = Taxonomies.None; - if (!string.IsNullOrEmpty(partType)) + if (!string.IsNullOrEmpty(partType) && partType != "-1") { if (Enum.TryParse(partType, true, out taxonomy)) {