diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index ae84562..5732299 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,22 +1,22 @@
-import './globals.css'
-import type { Metadata } from 'next'
-import { Inter } from 'next/font/google'
+import "./globals.css";
+import type { Metadata } from "next";
+import { Inter } from "next/font/google";
-const inter = Inter({ subsets: ['latin'] })
+const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
- title: 'Create Next App',
- description: 'Generated by create next app',
-}
+ title: "Create Next App",
+ description: "Generated by create next app",
+};
export default function RootLayout({
children,
}: {
- children: React.ReactNode
+ children: React.ReactNode;
}) {
return (
{children}
- )
+ );
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 3f3b086..dc66abd 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,113 +1,326 @@
-import Image from 'next/image'
+"use client";
+
+import Image from "next/image";
+import {
+ TParsedDMMResponse,
+ debounceReadingFunc,
+ parseDMMBuffer,
+ startBluetoothCoonection as startBluetoothConnection,
+} from "@/helpers/parseDMM";
+import { startBuzz } from "@/helpers/buzzSound";
+import {
+ MutableRefObject,
+ useEffect,
+ useRef,
+ useState,
+ Dispatch,
+ SetStateAction,
+} from "react";
+
+export function SpeakerOnIcon() {
+ return (
+
+ );
+}
+
+export function SpeakerOffIcon() {
+ return (
+
+ );
+}
+
+export function EyeIcon() {
+ return (
+
+ );
+}
+
+export function MegaphoneIcon() {
+ return (
+
+ );
+}
+
+export function useBuzzSound() {}
+
+export function useStateWithRef(
+ initialValue: TState,
+): [TState, Dispatch>, MutableRefObject] {
+ const [value, setValue] = useState(initialValue);
+ const valueRef = useRef(initialValue);
+ valueRef.current = value;
+
+ return [value, setValue, valueRef];
+}
export default function Home() {
- return (
-
-
-
- Get started by editing
- src/app/page.tsx
-
-
-
+ const [printedValue, setPrintedValue] = useState(
+ undefined,
+ );
+ const [liveValue, setLiveValue] = useState(undefined);
+ const [isMute, setIsMute, isMuteRef] = useStateWithRef(false);
+ const stopBuzzRef = useRef<(() => void) | undefined>(undefined);
-
+ let [selectedVoice, setSelectedVoice, selectedVoiceRef] = useStateWithRef<
+ string | undefined
+ >(undefined);
+ let [allVoices, setAllVoices] = useState
();
+ let multimeterBluetoothDeviceRef = useRef(
+ undefined,
+ );
+ let multimeterDisconnectRef = useRef<(() => void) | undefined>(undefined);
+
+ const [showDisconnect, setShowDisconnect] = useState(false);
+ const showConnect = !showDisconnect;
+
+ function resetStatusAfterDisconnect() {
+ if (multimeterBluetoothDeviceRef.current !== undefined) {
+ // Only announce once
+ speak("Multimeter disconnected");
+ }
+ multimeterBluetoothDeviceRef.current = undefined;
+ multimeterDisconnectRef.current = undefined;
+ setLiveValue(undefined);
+ setPrintedValue(undefined);
+ setShowDisconnect(false);
+ }
+
+ function speak(value: string) {
+ let speech = new SpeechSynthesisUtterance();
+ speech.lang = "en";
+ speech.text = value;
+ speech.voice =
+ allVoices?.find(
+ ({ voiceURI }) => voiceURI === selectedVoiceRef.current,
+ ) || null;
+ speech.volume = isMuteRef.current ? 0 : 1;
+ window.speechSynthesis.speak(speech);
+ }
+
+ function handleConnectClick() {
+ const debouncedLogAndSpeak = debounceReadingFunc((value) => {
+ value = value as string;
+ setPrintedValue(value);
+ console.log(value);
+ speak(value.replace("-", "Minus "));
+ }, 700);
+
+ function handleBuzz(parsedDMMresponse: TParsedDMMResponse) {
+ const { isBuzz, isDiode } = parsedDMMresponse;
+ // If BUZZ sign & not DIODE sign & not mute
+ if (isBuzz && !isDiode && !isMuteRef.current) {
+ // Multimeter is buzzing, reciprocate
+ const { stop } = startBuzz();
+ stopBuzzRef.current = stop;
+ } else {
+ stopBuzzRef.current?.();
+ }
+ }
+
+ function onNotify(bufferAsArray: number[]) {
+ const parsedDMMresponse = parseDMMBuffer(bufferAsArray);
+ const printValue = parsedDMMresponse.toString();
+ // Set live value
+ setLiveValue(printValue);
+
+ // Debounced announce value
+ debouncedLogAndSpeak(printValue);
+
+ // Buzz, if needed
+ handleBuzz(parsedDMMresponse);
+ }
+ startBluetoothConnection({
+ onDisconnect: () => resetStatusAfterDisconnect(),
+ onNotify,
+ }).then((obj) => {
+ multimeterBluetoothDeviceRef.current = obj.multimeterBluetoothDevice;
+ multimeterDisconnectRef.current = obj.disconnect;
+ setShowDisconnect(true);
+ speak("Connected");
+ });
+ }
+
+ function handleDisconnectClick() {
+ if (multimeterDisconnectRef.current) {
+ console.log("Calling disconnect...");
+ multimeterDisconnectRef.current();
+
+ resetStatusAfterDisconnect();
+ } else {
+ console.log("Disconnect is not set");
+ }
+ }
+
+ useEffect(() => {
+ function _updateAllVoices() {
+ // Get List of Voices
+ const tempAllVoices = window.speechSynthesis.getVoices();
+ const allEnglishVoices = tempAllVoices.filter(({ lang }) =>
+ lang.startsWith("en"),
+ );
+ setAllVoices(allEnglishVoices);
+
+ if (allEnglishVoices.length > 0) {
+ // Initially set the First Voice in the Array.
+ setSelectedVoice(allEnglishVoices[0].name);
+ }
+ }
+
+ _updateAllVoices();
+ window.speechSynthesis.onvoiceschanged = () => {
+ _updateAllVoices();
+ };
+ }, [setSelectedVoice]);
+
+ function handleMuteClick() {
+ setIsMute(true);
+ }
+
+ function handleUnmuteClick() {
+ setIsMute(false);
+ }
+
+ return (
+
+
+
Multimeter Speaker
-
-
+
+ {showConnect ? (
+
+ ) : (
+
+ )}
+
+
+
+ {!isMute ? (
+
+ ) : (
+
+ )}
- )
+ );
}
diff --git a/src/helpers/bluetooth.ts b/src/helpers/bluetooth.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/helpers/buzzSound.ts b/src/helpers/buzzSound.ts
new file mode 100644
index 0000000..dd82c49
--- /dev/null
+++ b/src/helpers/buzzSound.ts
@@ -0,0 +1,57 @@
+// The browser will limit the number of concurrent audio contexts
+// So be sure to re-use them whenever you can
+const myAudioContext = new AudioContext();
+let lastOscillatorNode: OscillatorNode | undefined = undefined;
+
+/**
+ * Helper function to emit a beep sound in the browser using the Web Audio API.
+ *
+ * @param {number} duration - The duration of the beep sound in milliseconds.
+ * @param {number} frequency - The frequency of the beep sound.
+ * @param {number} volume - The volume of the beep sound.
+ *
+ * @returns {Promise} - A promise that resolves when the beep sound is finished.
+ */
+export function startBuzz({
+ duration = 500,
+ frequency = 2800,
+ volume = 70,
+ onEnd = () => {},
+} = {}) {
+ if (lastOscillatorNode !== undefined) {
+ lastOscillatorNode.stop(myAudioContext.currentTime + duration * 0.001);
+ return {
+ stop: () => lastOscillatorNode?.stop(),
+ };
+ } else {
+ let oscillatorNode = myAudioContext.createOscillator();
+ let gainNode = myAudioContext.createGain();
+ oscillatorNode.connect(gainNode);
+
+ // Set the oscillator frequency in hertz
+ oscillatorNode.frequency.value = frequency;
+
+ // Set the type of oscillator
+ oscillatorNode.type = "sine";
+ gainNode.connect(myAudioContext.destination);
+
+ // Set the gain to the volume
+ gainNode.gain.value = volume * 0.01;
+
+ // Start audio with the desired duration
+ oscillatorNode.start(myAudioContext.currentTime);
+ oscillatorNode.stop(myAudioContext.currentTime + duration * 0.001);
+
+ // Resolve the promise when the sound is finished
+ oscillatorNode.onended = () => {
+ lastOscillatorNode = undefined;
+ onEnd();
+ };
+
+ lastOscillatorNode = oscillatorNode;
+
+ return {
+ stop: () => oscillatorNode.stop(),
+ };
+ }
+}
diff --git a/src/helpers/constants.ts b/src/helpers/constants.ts
new file mode 100644
index 0000000..dbc5b7c
--- /dev/null
+++ b/src/helpers/constants.ts
@@ -0,0 +1,80 @@
+export const DEFAULT_SERVICE_UUID = "0000fff0-0000-1000-8000-00805f9b34fb";
+export const DEFAULT_CHARACTERISTIC_UUID =
+ "0000fff4-0000-1000-8000-00805f9b34fb";
+
+// eslint-disable-next-line max-len
+export const XOR_KEYS = [
+ parseInt("41", 16),
+ parseInt("21", 16),
+ parseInt("73", 16),
+ parseInt("55", 16),
+ parseInt("a2", 16),
+ parseInt("c1", 16),
+ parseInt("32", 16),
+ parseInt("71", 16),
+ parseInt("66", 16),
+ parseInt("aa", 16),
+ parseInt("3b", 16),
+];
+
+export const CHAR_MAP = {
+ "0": [1, 1, 1, 0, 1, 1, 1].join(""),
+ "1": [0, 0, 1, 0, 0, 1, 0].join(""),
+ "2": [1, 0, 1, 1, 1, 0, 1].join(""),
+ "3": [1, 0, 1, 1, 0, 1, 1].join(""),
+ "4": [0, 1, 1, 1, 0, 1, 0].join(""),
+ "5": [1, 1, 0, 1, 0, 1, 1].join(""),
+ "6": [1, 1, 0, 1, 1, 1, 1].join(""),
+ "7": [1, 0, 1, 0, 0, 1, 0].join(""),
+ "8": [1, 1, 1, 1, 1, 1, 1].join(""),
+ "9": [1, 1, 1, 1, 0, 1, 1].join(""),
+ A: [1, 1, 1, 1, 1, 1, 0].join(""),
+ U: [0, 0, 0, 0, 1, 1, 1].join(""),
+ T: [0, 1, 0, 1, 1, 0, 1].join(""),
+ O: [0, 0, 0, 1, 1, 1, 1].join(""),
+ L: [0, 1, 0, 0, 1, 0, 1].join(""), // Part of "OL" text
+};
+
+export type KNOWN_CHARS = keyof typeof CHAR_MAP;
+export type KNOWN_UNKNOWN_CHARS = KNOWN_CHARS | undefined;
+
+export const LCD_CHAR_BIT_POSITIONS = [
+ [24, 25, 36, 37, 26, 38, 39], // 0
+ [32, 33, 44, 45, 34, 46, 47], // 1
+ [40, 41, 52, 53, 42, 54, 55], // 2
+ [48, 49, 60, 61, 50, 62, 63], // 3
+];
+
+export const LCD_DECIMAL_POINT_BIT_POSITIONS = [35, 43, 51];
+
+export const LCD_SYMBOL_BIT_POSITION_MAP = {
+ Minus: 27,
+ Buzz: 28,
+ Relative: 30,
+ BatteryLow: 31,
+ Diode: 56,
+ Celsius: 57,
+ Fahrenheit: 58,
+ Hold: 59,
+ NanoFarad: 64,
+ MicroFarad: 65,
+ MilliFarad: 66,
+ Farad: 67,
+ AC: 68,
+ Percentage: 69,
+ Min: 70,
+ Max: 71,
+ Ampere: 72,
+ DC: 73,
+ MilliVolt: 74,
+ Volt: 75,
+ MegaOhm: 76,
+ KiloOhm: 77,
+ Ohm: 78,
+ Hertz: 79,
+ MilliAmp: 84,
+ MicroAmp: 85,
+ Auto: 87,
+};
+
+export type KNOWN_SYMBOLS = keyof typeof LCD_SYMBOL_BIT_POSITION_MAP;
diff --git a/src/helpers/parseDMM.ts b/src/helpers/parseDMM.ts
new file mode 100644
index 0000000..67088b1
--- /dev/null
+++ b/src/helpers/parseDMM.ts
@@ -0,0 +1,358 @@
+import {
+ CHAR_MAP,
+ DEFAULT_CHARACTERISTIC_UUID,
+ DEFAULT_SERVICE_UUID,
+ KNOWN_CHARS,
+ KNOWN_UNKNOWN_CHARS,
+ LCD_CHAR_BIT_POSITIONS,
+ LCD_DECIMAL_POINT_BIT_POSITIONS,
+ LCD_SYMBOL_BIT_POSITION_MAP,
+ XOR_KEYS,
+} from "./constants";
+import { getBit, getBitAsBoolean, zeroPad } from "./utils";
+
+export function parseDMMBuffer(bufferAsNumberArray: number[]) {
+ const partsXOR = bufferAsNumberArray.map(
+ (rawVal, index) => rawVal ^ XOR_KEYS[index],
+ );
+
+ const partsBin = partsXOR.map((val) => val.toString(2));
+ const partsBinPadded = partsBin.map((t) => zeroPad(t, 8));
+
+ const parsedBooleanString = partsBinPadded.join("");
+
+ return {
+ parsedBooleanString,
+ getBitAsBoolean(binIndex: number) {
+ return getBitAsBoolean(this.parsedBooleanString, binIndex);
+ },
+ char0: readCharacter(parsedBooleanString, 0),
+ char1: readCharacter(parsedBooleanString, 1),
+ char2: readCharacter(parsedBooleanString, 2),
+ char3: readCharacter(parsedBooleanString, 3),
+ decimalPointIndex: getDecimalPointIndex(parsedBooleanString),
+ isMinus: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.Minus,
+ ),
+ isBuzz: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.Buzz,
+ ),
+ isRelative: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.Relative,
+ ),
+ isBatteryLow: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.BatteryLow,
+ ),
+ isDiode: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.Diode,
+ ),
+ isHold: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.Hold,
+ ),
+ isAuto: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.Auto,
+ ),
+ isMin: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.Min,
+ ),
+ isMax: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.Max,
+ ),
+ isAC: getBitAsBoolean(parsedBooleanString, LCD_SYMBOL_BIT_POSITION_MAP.AC),
+ isDC: getBitAsBoolean(parsedBooleanString, LCD_SYMBOL_BIT_POSITION_MAP.DC),
+ isCelsius: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.Celsius,
+ ),
+ isFahrenheit: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.Fahrenheit,
+ ),
+ isNanoFarad: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.NanoFarad,
+ ),
+ isMicroFarad: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.MicroFarad,
+ ),
+ isMilliFarad: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.MilliFarad,
+ ),
+ isFarad: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.Farad,
+ ),
+ isPercentage: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.Percentage,
+ ),
+ isHertz: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.Hertz,
+ ),
+ isMilliVolt: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.MilliVolt,
+ ),
+ isVolt: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.Volt,
+ ),
+ isMegaOhm: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.MegaOhm,
+ ),
+ isKiloOhm: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.KiloOhm,
+ ),
+ isOhm: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.Ohm,
+ ),
+ isMilliAmp: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.MilliAmp,
+ ),
+ isMicroAmp: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.MicroAmp,
+ ),
+ isAmpere: getBitAsBoolean(
+ parsedBooleanString,
+ LCD_SYMBOL_BIT_POSITION_MAP.Ampere,
+ ),
+ get isFullAuto() {
+ return (
+ this.char0 === "A" &&
+ this.char1 === "U" &&
+ this.char2 === "T" &&
+ this.char3 === "O"
+ );
+ },
+ get unit() {
+ if (this.isCelsius) {
+ return "Celsius";
+ } else if (this.isFahrenheit) {
+ return "Fahrenheit";
+ } else if (this.isNanoFarad) {
+ return "NanoFarad";
+ } else if (this.isMicroFarad) {
+ return "MicroFarad";
+ } else if (this.isMilliFarad) {
+ return "MilliFarad";
+ } else if (this.isFarad) {
+ return "Farad";
+ } else if (this.isPercentage) {
+ return "Percentage";
+ } else if (this.isHertz) {
+ return "Hertz";
+ } else if (this.isMilliVolt) {
+ return "MilliVolt";
+ } else if (this.isVolt) {
+ return "Volt";
+ } else if (this.isMegaOhm) {
+ return "MegaOhm";
+ } else if (this.isKiloOhm) {
+ return "KiloOhm";
+ } else if (this.isOhm) {
+ return "Ohm";
+ } else if (this.isMilliAmp) {
+ return "MilliAmp";
+ } else if (this.isMicroAmp) {
+ return "MicroAmp";
+ } else if (this.isAmpere) {
+ return "Ampere";
+ }
+ },
+ get value() {
+ const parsedChars = [
+ this.char0 && parseInt(this.char0),
+ this.char1 && parseInt(this.char1),
+ this.char2 && parseInt(this.char2),
+ this.char3 && parseInt(this.char3),
+ ];
+ let value: number | undefined = undefined;
+ if (
+ parsedChars.every(
+ (tmpChar) => typeof tmpChar === "number" && !isNaN(tmpChar),
+ )
+ ) {
+ value =
+ (parsedChars[0] as number) * 1000 +
+ (parsedChars[1] as number) * 100 +
+ (parsedChars[2] as number) * 10 +
+ (parsedChars[3] as number) * 1;
+
+ if (this.decimalPointIndex !== undefined) {
+ value = value / 10 ** (3 - this.decimalPointIndex);
+ }
+
+ if (this.isMinus) {
+ value = -value;
+ }
+ }
+ return value;
+ },
+ get isOutOfLimit() {
+ return this.char1 == "0" && this.char2 == "L";
+ },
+ toString() {
+ if (this.value !== undefined && this.unit !== undefined) {
+ return `${this.value} ${this.unit} ${this.isAC ? "(AC)" : ""}${
+ this.isDC ? "(DC)" : ""
+ }`;
+ } else if (this.isOutOfLimit) {
+ return `Out of Limit (${this.unit})`;
+ }
+ },
+ };
+}
+
+export type TParsedDMMResponse = ReturnType;
+
+class MultimeterError extends Error {}
+
+export async function startBluetoothCoonection({
+ serviceUUID = DEFAULT_SERVICE_UUID,
+ characteristicUUID = DEFAULT_CHARACTERISTIC_UUID,
+ onNotify = printBLENotify,
+ onDisconnect = () => {},
+} = {}) {
+ let multimeterBluetoothDevice: BluetoothDevice | null = null;
+ multimeterBluetoothDevice = await navigator.bluetooth.requestDevice({
+ filters: [
+ {
+ services: [serviceUUID],
+ },
+ ],
+ });
+ if (multimeterBluetoothDevice === undefined) {
+ throw new MultimeterError("Multimeter not selected");
+ }
+ if (multimeterBluetoothDevice.gatt === undefined) {
+ throw new MultimeterError("BLE: Gatt is not available");
+ }
+ let gatt = await multimeterBluetoothDevice.gatt.connect();
+ multimeterBluetoothDevice.addEventListener("gattserverdisconnected", (ev) => {
+ console.log("Multimeter disconnected");
+ onDisconnect();
+ });
+
+ let service = await gatt.getPrimaryService(serviceUUID);
+ let characteristic = await service.getCharacteristic(characteristicUUID);
+
+ async function notifyCallback() {
+ const buffer: ArrayBuffer | undefined = characteristic.value?.buffer;
+ if (buffer == undefined) {
+ throw new MultimeterError("BLE: Notify buffer is undefined");
+ }
+ const bufferAsArray = [...new Uint8Array(buffer)];
+ onNotify(bufferAsArray);
+ }
+
+ if (characteristic.properties.notify) {
+ characteristic.addEventListener(
+ "characteristicvaluechanged",
+ notifyCallback,
+ );
+ await characteristic.startNotifications();
+ }
+
+ return {
+ multimeterBluetoothDevice,
+ service,
+ characteristic,
+ removeEventListener() {
+ characteristic.removeEventListener(
+ "characteristicvaluechanged",
+ notifyCallback,
+ );
+ },
+ disconnect() {
+ multimeterBluetoothDevice?.gatt?.disconnect();
+ onDisconnect();
+ },
+ };
+}
+
+const debouncedLog = debounceReadingFunc((value) => console.log(value));
+export function printBLENotify(bufferAsArray: number[]) {
+ const printValue = parseDMMBuffer(bufferAsArray).toString();
+ debouncedLog(printValue);
+}
+
+export function debounceReadingFunc(
+ callbackFn: (input: string | number) => void,
+ debounceMillis = 1000,
+) {
+ let timerId: ReturnType | undefined = undefined;
+ let lastReturnedValue: number | string | undefined = undefined;
+ let lastNewValue: number | string | undefined = undefined;
+
+ return function debouncedReading(newValue: string | number | undefined) {
+ if (newValue !== undefined && newValue !== lastNewValue) {
+ if (timerId !== undefined) {
+ clearTimeout(timerId);
+ timerId = undefined;
+ }
+
+ if (newValue !== lastReturnedValue) {
+ timerId = setTimeout(() => {
+ lastReturnedValue = newValue;
+ timerId = undefined;
+
+ callbackFn(newValue);
+ }, debounceMillis);
+ }
+ }
+ lastNewValue = newValue;
+ };
+}
+
+export function readCharacter(
+ binString: string,
+ charIndex: number,
+): KNOWN_UNKNOWN_CHARS {
+ if (charIndex >= LCD_CHAR_BIT_POSITIONS.length) {
+ throw new Error("Character index is invalid");
+ }
+ const charBitPositions = LCD_CHAR_BIT_POSITIONS[charIndex];
+ const receivedCharValueString = charBitPositions
+ .map((tmpBitPosition) => getBit(binString, tmpBitPosition))
+ .join("");
+
+ const foundKeyValue: [KNOWN_CHARS, string] | undefined = (
+ Object.entries(CHAR_MAP) as Array<[KNOWN_CHARS, string]>
+ ).find(([_, matchingCharValue]) => {
+ if (matchingCharValue == receivedCharValueString) {
+ return true;
+ }
+ });
+
+ if (foundKeyValue == undefined) {
+ return undefined;
+ }
+ return foundKeyValue[0];
+}
+
+export function getDecimalPointIndex(binString: string): number | undefined {
+ const maybeDecimalPointIndex = LCD_DECIMAL_POINT_BIT_POSITIONS.findIndex(
+ (tmpBitPosition) => getBitAsBoolean(binString, tmpBitPosition),
+ );
+ if (maybeDecimalPointIndex === -1) {
+ return undefined;
+ }
+ return maybeDecimalPointIndex;
+}
diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts
new file mode 100644
index 0000000..d1800e8
--- /dev/null
+++ b/src/helpers/utils.ts
@@ -0,0 +1,42 @@
+/**
+ * Pads number/string with zeros and returns string of required length.
+ * @example `zeroPad('101', 8)` - converts boolean (string) to 8-sized byte (string)
+ * @param num Number to pad
+ * @param length Total length after padding
+ * @returns zeropadded string
+ */
+export function zeroPad(num: number | string, length: number): string {
+ return String(num).padStart(length, "0");
+}
+
+/**
+ * Gets bit value from binary string
+ * @param str Binary string
+ * @param bitIndex Index of bit to get
+ * @returns bit value
+ */
+export function getBit(str: string, bitIndex: number): number {
+ const value = str[bitIndex];
+ if (value == undefined) {
+ throw new RangeError(`Index:${bitIndex} not found in ${str}`);
+ }
+ return Number(value);
+}
+
+/**
+ * Gets bit value as boolean from binary string
+ * @param str Binary string
+ * @param bitIndex Index of bit to get
+ * @returns bit value as boolean
+ */
+export function getBitAsBoolean(str: string, bitIndex: number): boolean {
+ return !!getBit(str, bitIndex);
+}
+
+export function hexStringToNumberArray(rawString: string) {
+ return rawString.split("-").map((val) => parseInt(val, 16));
+}
+
+export function hexArrayToNumberArray(rawStrings: string[]) {
+ return rawStrings.map((val) => parseInt(val, 16));
+}