Skip to content

Commit

Permalink
Fixes #132 - hopefully for the last time. Corrects badly formed datam…
Browse files Browse the repository at this point in the history
…atrix barcodes with invalid RS/GS encoding
  • Loading branch information
Michael Brown authored and replaysMike committed Apr 3, 2023
1 parent e8593fa commit a8d497d
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 39 deletions.
9 changes: 9 additions & 0 deletions Binner/Binner.Web/ClientApp/src/common/Utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { MD5 as encryptMD5 } from "crypto-js";

/**
* Copy/clone a string
* @param {string} str input string
* @returns copied string
*/
export const copyString = (str) => {
return (' ' + str).slice(1); // force clone string
};

/**
* Encode a number to resistance value
* @param {any} number the ohms value
Expand Down
131 changes: 102 additions & 29 deletions Binner/Binner.Web/ClientApp/src/components/BarcodeScannerInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import debounce from "lodash.debounce";
import { Events } from "../common/events";
import useSound from 'use-sound';
import boopSfx from '../audio/softbeep.mp3';
import { copyString } from "../common/Utils";

/**
* Handles generic barcode scanning input by listening for batches of key presses
Expand All @@ -16,6 +17,7 @@ export function BarcodeScannerInput({listening, minInputLength, onReceived, help
const [isKeyboardListening, setIsKeyboardListening] = useState(listening || true);
const [previousIsKeyboardListeningState, setPreviousIsKeyboardListeningState] = useState(listening || true);
const [playScanSound] = useSound(boopSfx, { soundEnabled: true, volume: 1 });
const [isReceiving, setIsReceiving] = useState(false);
const listeningRef = useRef();
listeningRef.current = isKeyboardListening;
const keyBufferRef = useRef();
Expand All @@ -28,7 +30,6 @@ export function BarcodeScannerInput({listening, minInputLength, onReceived, help
keyBufferRef.current.length = 0;
return; // drop and ignore input
}

const value = processKeyBuffer(buffer);
// reset key buffer
keyBufferRef.current.length = 0;
Expand All @@ -38,6 +39,7 @@ export function BarcodeScannerInput({listening, minInputLength, onReceived, help
if (enableSound) playScanSoundRef.current();
// fire an event that we received data
onReceived(e, input);
setIsReceiving(false);
};

const processKeyBuffer = (buffer) => {
Expand Down Expand Up @@ -95,7 +97,6 @@ export function BarcodeScannerInput({listening, minInputLength, onReceived, help
// 2D DotMatrix barcode. Process into value.
barcodeType = "datamatrix";
const parseResult = parseDataMatrix(value);
console.log('parseResult', parseResult);
parsedValue = parseResult.value;
gsDetected = parseResult.gsDetected;
rsDetected = parseResult.rsDetected;
Expand All @@ -121,9 +122,16 @@ export function BarcodeScannerInput({listening, minInputLength, onReceived, help

const parseDataMatrix = (value) => {
let parsedValue = {};
const gsCharCodes = ["\u001d", "\u005d", "\u241d"];
const rsCharCodes = ["\u001e", "\u005e", "\u241e"];
const eotCharCodes = ["\u0004", "^\u0044", "\u2404"];
// https://honeywellaidc.force.com/supportppr/s/article/What-do-Control-Characters-SOH-STX-etc-mean-when-scanning
const gsCharCodes = ["\u001d", "\u005d", "\u241d"]; // CTRL-], \u001d, GROUP SEPARATOR
const rsCharCodes = ["\u001e", "\u005e", "\u241e"]; // CTRL-^, \u001e, RECORD SEPARATOR
const eotCharCodes = ["\u0004", "^\u0044", "\u2404"]; // CTRL-D, \u0004, END OF TRANSMISSION
const crCharCodes = ["\r", "\u240d"]; // 13, line feed
const lfCharCodes = ["\n", "\u240a"]; // 10, carriage return
const fileSeparatorCharCodes = ["\u001c", "\u241c"]; // ctl-\, \u001c FILE SEPARATOR
const sohCharCodes = ["\u0001"]; // CTRL-A, \u0001 START OF HEADER
const stxCharCodes = ["\u0002"]; // CTRL-B, \u0002 START OF TEXT
const etxCharCodes = ["\u0003"]; // CTRL-C, \u0003 END OF TEXT
const header = "[)>";
const expectedFormatNumber = 6; /** 22z22 barcode */
const controlChars = ["P", "1P", "P1", "K", "1K", "10K", "11K", "4L", "Q", "11Z", "12Z", "13Z", "20Z"];
Expand All @@ -136,22 +144,32 @@ export function BarcodeScannerInput({listening, minInputLength, onReceived, help
let i;
let formatNumberIndex = 0;
let correctedValue = value.toString();
// normalize the control codes so we don't have multiple values to worry about
correctedValue = normalizeControlCharacters(correctedValue);

correctedValue = correctedValue.replaceAll("\u001d", "\u241d"); // GS
correctedValue = correctedValue.replaceAll("\u005d", "\u241d"); // GS

gsCodePresent = gsCharCodes.some(v => value.includes(v));
rsCodePresent = rsCharCodes.some(v => value.includes(v));
eotCodePresent = eotCharCodes.some(v => value.includes(v));
correctedValue = correctedValue.replaceAll("\u001e", "\u241e"); // RS
correctedValue = correctedValue.replaceAll("\u005e", "\u241e"); // RS
correctedValue = correctedValue.replaceAll("\u0004", "\u2404"); // EOT
correctedValue = correctedValue.replaceAll("^\u0044", "\u2404"); // EOT

gsCodePresent = gsCharCodes.some(v => correctedValue.includes(v));
rsCodePresent = rsCharCodes.some(v => correctedValue.includes(v));
eotCodePresent = eotCharCodes.some(v => correctedValue.includes(v));

// read in the format number first. For Digikey 2d barcodes, this should be 6 (expectedFormatNumber)
for (i = 0; i < value.length; i++) {
buffer += value[i];
for (i = 0; i < correctedValue.length; i++) {
buffer += correctedValue[i];
if (buffer === header) {
if (rsCharCodes.includes(value[i + 1])) {
if (rsCharCodes.includes(correctedValue[i + 1])) {
// read the character after the RS token (sometimes not present)
formatNumberIndex = i + 2;
} else {
formatNumberIndex = i + 1;
}
formatNumber = parseInt(value[formatNumberIndex] + value[formatNumberIndex + 1]);
formatNumber = parseInt(correctedValue[formatNumberIndex] + correctedValue[formatNumberIndex + 1]);
i += formatNumberIndex + 1;
break;
}
Expand All @@ -164,11 +182,11 @@ export function BarcodeScannerInput({listening, minInputLength, onReceived, help
}

let lastPosition = i;
const gsLines = [];
let gsLines = [];
let gsLine = '';
// break each group separator into an array
for (i = lastPosition; i < value.length; i++) {
const ch = value[i];
for (i = lastPosition; i < correctedValue.length; i++) {
const ch = correctedValue[i];
if (gsCharCodes.includes(ch)) {
// start of a new line. read until next gsCharCode or EOT
if (gsLine.length > 0)
Expand All @@ -181,26 +199,18 @@ export function BarcodeScannerInput({listening, minInputLength, onReceived, help
if (gsLine.length > 0)
gsLines.push(gsLine);

let readLength = gsLines.length;
let invalidBarcodeDetected = false;
// some older DigiKey barcodes are encoded incorrectly, and have a blank GSRS at the end. Filter them out.
// https://github.com/replaysMike/Binner/issues/132
if (correctedValue.endsWith("\u005e\u0044\r") || correctedValue.endsWith("\u005d\u005e\u0044")) {
if (isInvalidBarcode(gsLines)) {
gsLines = fixInvalidBarcode(gsLines);
invalidBarcodeDetected = true;
readLength--;
// apply correction to the raw value
if (correctedValue.endsWith("\r")){
correctedValue = correctedValue.substring(0, correctedValue.length - 4) + "\r";
} else {
correctedValue = correctedValue.substring(0, correctedValue.length - 3);
}
}
const filteredGsLines = [];
let readLength = gsLines.length;
// read each group separator
for (i = 0; i < readLength; i++) {
// read until we see a control char
const line = gsLines[i];
filteredGsLines.push(line);
let readCommandType = "";
let readValue = "";
let readControlChars = true;
Expand Down Expand Up @@ -263,18 +273,68 @@ export function BarcodeScannerInput({listening, minInputLength, onReceived, help
break;
}
}

correctedValue = buildBarcode(expectedFormatNumber, gsLines);
return {
rawValue: value,
value: parsedValue,
correctedValue: correctedValue,
gsDetected: gsCodePresent,
rsDetected: rsCodePresent,
eotDetected: eotCodePresent,
gsLines: filteredGsLines,
gsLines: gsLines,
invalidBarcodeDetected
};
};

const buildBarcode = (formatNumber, gsLines) => {
let barcode = `[)>\u241e${formatNumber.toString().padStart(2, '0')}`; // Header + RS + formatNumber
for(let i = 0; i < gsLines.length; i++){
barcode = barcode + "\u241d" + gsLines[i]; // GS
}
barcode = barcode + "\u2404\r"; // EOT + CR
return barcode;
};

const normalizeControlCharacters = (str) => {
// convert all variations of the control code to their equiv unicode value
let normalizedStr = copyString(str);
normalizedStr = normalizedStr.replaceAll("\u001d", "\u241d"); // GS
normalizedStr = normalizedStr.replaceAll("\u005d", "\u241d"); // GS

normalizedStr = normalizedStr.replaceAll("\u001e", "\u241e"); // RS
normalizedStr = normalizedStr.replaceAll("\u005e", "\u241e"); // RS
normalizedStr = normalizedStr.replaceAll("\u0004", "\u2404"); // EOT
normalizedStr = normalizedStr.replaceAll("^\u0044", "\u2404"); // EOT
return normalizedStr;
};

const isInvalidBarcode = (gsLines) => {
for(let i = 0; i < gsLines.length; i++){
if (gsLines[i].includes("\u241e")) { // RS
return true;
}
}
return false;
};

const fixInvalidBarcode = (gsLines) => {
const newGsLines = [];
for(let i = 0; i < gsLines.length; i++){
if (gsLines[i].includes("\u241e")) { // RS
// is there data before the RS character?
const rsIndex = gsLines[i].indexOf("\u241e");
if (rsIndex > 0) {
const data = gsLines[i].substring(0, rsIndex);
newGsLines.push(data);
}
continue;
}
newGsLines.push(gsLines[i]);
}
return newGsLines;
};

const scannerDebounced = useMemo(() => debounce(onReceivedBarcodeInput, BufferTimeMs), []);

const disableBarcodeInput = (e) => {
Expand Down Expand Up @@ -340,11 +400,24 @@ export function BarcodeScannerInput({listening, minInputLength, onReceived, help
&& !(e.ctrlKey && (e.key === "c" || e.key === "v" || e.key === "x"))
&& !(e.shiftKey && (e.key === "Insert"))
) {
// console.log('swallowing', e);
e.preventDefault();
e.stopPropagation();
}
// special case, swallow CTRL-SHIFT-D which changes the inspector dock window position
if (e.code === "KeyD" && e.shiftKey && e.ctrlKey) {
e.preventDefault();
e.stopPropagation();
return;
}
keyBufferRef.current.push(e);
// visual indicator of input received
setTimeout(() => {
if (keyBufferRef.current.length > 5) {
setIsReceiving(true);
} else {
setIsReceiving(false);
}
}, 500);
scannerDebounced(e, keyBufferRef.current);
} else {
// dropped key, not listening
Expand All @@ -362,7 +435,7 @@ export function BarcodeScannerInput({listening, minInputLength, onReceived, help
This page supports barcode scanning. <Link to={helpUrl}>More Info</Link>
</p>
}
trigger={<Image src="/image/barcode.png" width={35} height={35} className="barcode-support" />}
trigger={<Image src="/image/barcode.png" width={35} height={35} className={`barcode-support ${isReceiving ? "receiving" : ""}`} />}
/>
</div>
);
Expand Down
11 changes: 11 additions & 0 deletions Binner/Binner.Web/ClientApp/src/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,17 @@ code {
.barcode-support {
box-shadow: 2px 2px 3px 1px rgb(34 36 38 / 20%);
border-radius: 12px;
transition-delay: 0s;
transition-duration: 0.5s;
transition-property: filter;
transition-timing-function: ease-out;
outline: 4px solid transparent;
outline-offset: 4px;
filter: none;
}

.barcode-support.receiving {
filter: invert(76%) sepia(30%) saturate(3461%) hue-rotate(177deg) brightness(98%) contrast(91%);
}

.help-icon {
Expand Down
1 change: 1 addition & 0 deletions Binner/Binner.Web/ClientApp/src/pages/Inventory.js
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,7 @@ export function Inventory(props) {
setMetadataParts([]);
setDuplicateParts([]);
setPartMetadataIsSubscribed(false);
setInputPartNumber("");
const clearedPart = {
partId: 0,
partNumber: "",
Expand Down
25 changes: 18 additions & 7 deletions Binner/Binner.Web/ClientApp/src/pages/tools/BarcodeScanner.css
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,30 @@ pre {
background-color: rgb(128, 0, 0);
}

.gs {
.control {
font-size: 1.5em;
font-weight: 700;
}

.control.gs {
color:rgb(255, 0, 0);
}
.rs {
font-size: 1.5em;
font-weight: 700;
.control.rs {
color:rgb(50, 50, 150);
}

.eot {
font-size: 1.5em;
font-weight: 700;
.control.eot {
color:rgb(0, 100, 0);
}

.control.cr {
color:rgb(97, 0, 100);
}

.control.lf {
color:rgb(97, 0, 100);
}

.control.fs {
color:rgb(9, 215, 9);
}
12 changes: 9 additions & 3 deletions Binner/Binner.Web/ClientApp/src/pages/tools/BarcodeScanner.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export function BarcodeScanner(props) {
.replaceAll("\u005e","\u241e") // RS (94) ^
.replaceAll("\u001d", "\u241d") // GS (29)
.replaceAll("\u005d", "\u241d") // GS (93) ]
.replaceAll("\r", "\u240d") // CR
.replaceAll("\n", "\u240a") // LF
.replaceAll("\u001c", "\u241c") // FS
;
const json = {...input, rawValueFormatted: rawValueFormatted};
setBarcodeValue(JSON.stringify(json, null, 2));
Expand All @@ -45,9 +48,12 @@ export function BarcodeScanner(props) {
toast.info(`Barcode type ${input.type} received`);
};

let barcodeObject = reactStringReplace(barcodeValue, "\u241E", (match, i) => (<span key={i*2} className="rs">{match}</span>));
barcodeObject = reactStringReplace(barcodeObject, "\u241D", (match, i) => (<span key={i*3} className="gs">{match}</span>));
barcodeObject = reactStringReplace(barcodeObject, "\u2404", (match, i) => (<span key={i*4} className="eot">{match}</span>));
let barcodeObject = reactStringReplace(barcodeValue, "\u241E", (match, i) => (<span key={i*2} className="control rs">{match}</span>));
barcodeObject = reactStringReplace(barcodeObject, "\u241D", (match, i) => (<span key={i*3} className="control gs">{match}</span>));
barcodeObject = reactStringReplace(barcodeObject, "\u2404", (match, i) => (<span key={i*4} className="control eot">{match}</span>));
barcodeObject = reactStringReplace(barcodeObject, "\u240d", (match, i) => (<span key={i*5} className="control cr">{match}</span>));
barcodeObject = reactStringReplace(barcodeObject, "\u240a", (match, i) => (<span key={i*6} className="control lf">{match}</span>));
barcodeObject = reactStringReplace(barcodeObject, "\u241c", (match, i) => (<span key={i*7} className="control fs">{match}</span>));

return (
<div>
Expand Down

0 comments on commit a8d497d

Please sign in to comment.