mirror of
https://github.com/bendtherules/multimeter-connect-web.git
synced 2026-08-18 22:03:12 +00:00
Merge pull request #2 from bendtherules/auto-retry
Add Auto-reconnect feature
This commit is contained in:
@@ -16,11 +16,12 @@
|
|||||||
- When multimeter's Buzz symbol is ON and DIODE symbol is OFF, that indicates multimeter is buzzing. The app also generates a tone of 2800 Hz with 70% volume to make the buzz more audible. (Actual multimeter uses 2150 Hz, but I like this one better.)
|
- When multimeter's Buzz symbol is ON and DIODE symbol is OFF, that indicates multimeter is buzzing. The app also generates a tone of 2800 Hz with 70% volume to make the buzz more audible. (Actual multimeter uses 2150 Hz, but I like this one better.)
|
||||||
- Doesn't work on IOS, because Web Bluetooth Api is not supported. Works on Android, Mac, Windows.
|
- Doesn't work on IOS, because Web Bluetooth Api is not supported. Works on Android, Mac, Windows.
|
||||||
- Supports PWA app installation and offline mode.
|
- Supports PWA app installation and offline mode.
|
||||||
|
- Added automatic BLE reconnect feature (if connection is lost from DMM side). If user clicks "Disconnect" button manually, reconnect is temporarily disabled till next reconnect.
|
||||||
|
|
||||||
## Todo
|
## Todo
|
||||||
|
|
||||||
- Support other size multimeters from Aneng
|
- Support other size multimeters from Aneng
|
||||||
- Allow configuring volume, pitch, buzz volume, buzz frequency
|
- Allow configuring volume, pitch, buzz volume, buzz frequency, auto re-connect
|
||||||
- Allow custom template
|
- Allow custom template
|
||||||
|
|
||||||
## Contribution
|
## Contribution
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function SpeakerOnIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-6 h-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function SpeakerOffIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-6 h-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M17.25 9.75L19.5 12m0 0l2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6l4.72-4.72a.75.75 0 011.28.531V19.94a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.506-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.395C2.806 8.757 3.63 8.25 4.51 8.25H6.75z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function EyeIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-6 h-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function MegaphoneIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-6 h-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M10.34 15.84c-.688-.06-1.386-.09-2.09-.09H7.5a4.5 4.5 0 110-9h.75c.704 0 1.402-.03 2.09-.09m0 9.18c.253.962.584 1.892.985 2.783.247.55.06 1.21-.463 1.511l-.657.38c-.551.318-1.26.117-1.527-.461a20.845 20.845 0 01-1.44-4.282m3.102.069a18.03 18.03 0 01-.59-4.59c0-1.586.205-3.124.59-4.59m0 9.18a23.848 23.848 0 018.835 2.535M10.34 6.66a23.847 23.847 0 008.835-2.535m0 0A23.74 23.74 0 0018.795 3m.38 1.125a23.91 23.91 0 011.014 5.395m-1.014 8.855c-.118.38-.245.754-.38 1.125m.38-1.125a23.91 23.91 0 001.014-5.395m0-3.46c.495.413.811 1.035.811 1.73 0 .695-.316 1.317-.811 1.73m0-3.46a24.347 24.347 0 010 3.46"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
+163
-204
@@ -1,214 +1,67 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import {
|
import { TParsedDMMResponse, parseDMMBuffer } from "@/helpers/parseDMM";
|
||||||
TParsedDMMResponse,
|
import { debounceReadingFunc } from "@/helpers/debounce";
|
||||||
debounceReadingFunc,
|
import { BluetoothConnection } from "@/helpers/bluetooth";
|
||||||
parseDMMBuffer,
|
|
||||||
startBluetoothCoonection as startBluetoothConnection,
|
|
||||||
} from "@/helpers/parseDMM";
|
|
||||||
import { startBuzz } from "@/helpers/buzzSound";
|
import { startBuzz } from "@/helpers/buzzSound";
|
||||||
import {
|
import { useEffect, useRef, useState } from "react";
|
||||||
MutableRefObject,
|
import { EyeIcon, MegaphoneIcon, SpeakerOnIcon, SpeakerOffIcon } from "./icons";
|
||||||
useEffect,
|
import { useStateWithRef } from "@/helpers/custom-hooks";
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
Dispatch,
|
|
||||||
SetStateAction,
|
|
||||||
} from "react";
|
|
||||||
|
|
||||||
function SpeakerOnIcon() {
|
function useBluetoothConnection() {
|
||||||
return (
|
let bluetoothConnectionRef = useRef<BluetoothConnection | null>(null);
|
||||||
<svg
|
if (bluetoothConnectionRef.current == null) {
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
bluetoothConnectionRef.current = new BluetoothConnection();
|
||||||
fill="none"
|
}
|
||||||
viewBox="0 0 24 24"
|
const bluetoothConnection = bluetoothConnectionRef.current;
|
||||||
strokeWidth={1.5}
|
const {
|
||||||
stroke="currentColor"
|
startConnection,
|
||||||
className="w-6 h-6"
|
startAutoReconnect,
|
||||||
>
|
addConnectHandler,
|
||||||
<path
|
addDisconnectHandler,
|
||||||
strokeLinecap="round"
|
addNotifyHandler,
|
||||||
strokeLinejoin="round"
|
disconnect,
|
||||||
d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
|
enableShouldAutoReconnect,
|
||||||
/>
|
disableShouldAutoReconnect,
|
||||||
</svg>
|
} = bluetoothConnection;
|
||||||
);
|
return {
|
||||||
|
startConnection,
|
||||||
|
startAutoReconnect,
|
||||||
|
addConnectHandler,
|
||||||
|
addDisconnectHandler,
|
||||||
|
addNotifyHandler,
|
||||||
|
disconnect,
|
||||||
|
enableShouldAutoReconnect,
|
||||||
|
disableShouldAutoReconnect,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function SpeakerOffIcon() {
|
function useSpeechApi() {
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="w-6 h-6"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M17.25 9.75L19.5 12m0 0l2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6l4.72-4.72a.75.75 0 011.28.531V19.94a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.506-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.395C2.806 8.757 3.63 8.25 4.51 8.25H6.75z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EyeIcon() {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="w-6 h-6"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MegaphoneIcon() {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="w-6 h-6"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M10.34 15.84c-.688-.06-1.386-.09-2.09-.09H7.5a4.5 4.5 0 110-9h.75c.704 0 1.402-.03 2.09-.09m0 9.18c.253.962.584 1.892.985 2.783.247.55.06 1.21-.463 1.511l-.657.38c-.551.318-1.26.117-1.527-.461a20.845 20.845 0 01-1.44-4.282m3.102.069a18.03 18.03 0 01-.59-4.59c0-1.586.205-3.124.59-4.59m0 9.18a23.848 23.848 0 018.835 2.535M10.34 6.66a23.847 23.847 0 008.835-2.535m0 0A23.74 23.74 0 0018.795 3m.38 1.125a23.91 23.91 0 011.014 5.395m-1.014 8.855c-.118.38-.245.754-.38 1.125m.38-1.125a23.91 23.91 0 001.014-5.395m0-3.46c.495.413.811 1.035.811 1.73 0 .695-.316 1.317-.811 1.73m0-3.46a24.347 24.347 0 010 3.46"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function useBuzzSound() {}
|
|
||||||
|
|
||||||
function useStateWithRef<TState>(
|
|
||||||
initialValue: TState,
|
|
||||||
): [TState, Dispatch<SetStateAction<TState>>, MutableRefObject<TState>] {
|
|
||||||
const [value, setValue] = useState<TState>(initialValue);
|
|
||||||
const valueRef = useRef<TState>(initialValue);
|
|
||||||
valueRef.current = value;
|
|
||||||
|
|
||||||
return [value, setValue, valueRef];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
const [printedValue, setPrintedValue] = useState<string | undefined>(
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
const [liveValue, setLiveValue] = useState<string | undefined>(undefined);
|
|
||||||
const [isMute, setIsMute, isMuteRef] = useStateWithRef<boolean>(false);
|
const [isMute, setIsMute, isMuteRef] = useStateWithRef<boolean>(false);
|
||||||
const stopBuzzRef = useRef<(() => void) | undefined>(undefined);
|
let [selectedVoiceURI, setSelectedVoiceURI, selectedVoiceURIRef] =
|
||||||
|
useStateWithRef<string | undefined>(undefined);
|
||||||
let [selectedVoice, setSelectedVoice, selectedVoiceRef] = useStateWithRef<
|
let [allVoices, setAllVoices, allVoicesRef] = useStateWithRef<
|
||||||
string | undefined
|
SpeechSynthesisVoice[] | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
let [allVoices, setAllVoices] = useState<SpeechSynthesisVoice[]>();
|
|
||||||
let multimeterBluetoothDeviceRef = useRef<BluetoothDevice | undefined>(
|
const speak = useCallback(
|
||||||
undefined,
|
function speak(value: string) {
|
||||||
|
let speech = new SpeechSynthesisUtterance();
|
||||||
|
speech.lang = "en";
|
||||||
|
speech.text = value;
|
||||||
|
speech.voice =
|
||||||
|
allVoicesRef.current?.find(
|
||||||
|
({ voiceURI }) => voiceURI == selectedVoiceURIRef.current,
|
||||||
|
) || null;
|
||||||
|
speech.volume = isMuteRef.current ? 0 : 1;
|
||||||
|
window.speechSynthesis.speak(speech);
|
||||||
|
},
|
||||||
|
[isMuteRef, selectedVoiceURIRef, allVoicesRef],
|
||||||
);
|
);
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Effect - Init all voices
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function _updateAllVoices() {
|
function _updateAllVoices() {
|
||||||
// Get List of Voices
|
// Get List of Voices
|
||||||
@@ -220,7 +73,7 @@ export default function Home() {
|
|||||||
|
|
||||||
if (allEnglishVoices.length > 0) {
|
if (allEnglishVoices.length > 0) {
|
||||||
// Initially set the First Voice in the Array.
|
// Initially set the First Voice in the Array.
|
||||||
setSelectedVoice(allEnglishVoices[0].name);
|
setSelectedVoiceURI(allEnglishVoices[0].voiceURI);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +81,113 @@ export default function Home() {
|
|||||||
window.speechSynthesis.onvoiceschanged = () => {
|
window.speechSynthesis.onvoiceschanged = () => {
|
||||||
_updateAllVoices();
|
_updateAllVoices();
|
||||||
};
|
};
|
||||||
}, [setSelectedVoice]);
|
}, [setSelectedVoiceURI, setAllVoices]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
allVoices,
|
||||||
|
selectedVoiceURI,
|
||||||
|
setSelectedVoiceURI,
|
||||||
|
isMute,
|
||||||
|
setIsMute,
|
||||||
|
speak,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const [printedValue, setPrintedValue] = useState<string | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
const [liveValue, setLiveValue] = useState<string | undefined>(undefined);
|
||||||
|
const stopBuzzRef = useRef<(() => void) | undefined>(undefined);
|
||||||
|
|
||||||
|
const [showDisconnect, setShowDisconnect] = useState(false);
|
||||||
|
const showConnect = !showDisconnect;
|
||||||
|
|
||||||
|
const {
|
||||||
|
startConnection,
|
||||||
|
startAutoReconnect,
|
||||||
|
addConnectHandler,
|
||||||
|
addDisconnectHandler,
|
||||||
|
addNotifyHandler,
|
||||||
|
disconnect,
|
||||||
|
enableShouldAutoReconnect,
|
||||||
|
disableShouldAutoReconnect,
|
||||||
|
} = useBluetoothConnection();
|
||||||
|
const {
|
||||||
|
allVoices,
|
||||||
|
selectedVoiceURI,
|
||||||
|
setSelectedVoiceURI,
|
||||||
|
isMute,
|
||||||
|
setIsMute,
|
||||||
|
speak,
|
||||||
|
} = useSpeechApi();
|
||||||
|
|
||||||
|
const debouncedLogAndSpeak = useMemo(
|
||||||
|
() =>
|
||||||
|
debounceReadingFunc((value) => {
|
||||||
|
value = value as string;
|
||||||
|
setPrintedValue(value);
|
||||||
|
console.log(value);
|
||||||
|
speak(value.replace("-", "Minus "));
|
||||||
|
}, 700),
|
||||||
|
[speak],
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleBuzz(parsedDMMresponse: TParsedDMMResponse) {
|
||||||
|
const { isBuzz, isDiode } = parsedDMMresponse;
|
||||||
|
// If BUZZ sign & not DIODE sign & not mute
|
||||||
|
if (isBuzz && !isDiode && !isMute) {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
addNotifyHandler(onNotify);
|
||||||
|
|
||||||
|
function onDisconnect() {
|
||||||
|
speak("Multimeter disconnected");
|
||||||
|
|
||||||
|
setLiveValue(undefined);
|
||||||
|
setPrintedValue(undefined);
|
||||||
|
setShowDisconnect(false);
|
||||||
|
|
||||||
|
// Has internal check for shouldAutoReconnect
|
||||||
|
startAutoReconnect();
|
||||||
|
}
|
||||||
|
addDisconnectHandler(onDisconnect);
|
||||||
|
|
||||||
|
function onConnect() {
|
||||||
|
// TODO: Check if autoReconnect settings enabled
|
||||||
|
enableShouldAutoReconnect();
|
||||||
|
setShowDisconnect(true);
|
||||||
|
speak("Connected");
|
||||||
|
}
|
||||||
|
addConnectHandler(onConnect);
|
||||||
|
|
||||||
|
function handleConnectClick() {
|
||||||
|
startConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDisconnectClick() {
|
||||||
|
console.log("Clicked disconnect");
|
||||||
|
disableShouldAutoReconnect();
|
||||||
|
disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
function handleMuteClick() {
|
function handleMuteClick() {
|
||||||
setIsMute(true);
|
setIsMute(true);
|
||||||
@@ -294,8 +253,8 @@ export default function Home() {
|
|||||||
<select
|
<select
|
||||||
name="voice"
|
name="voice"
|
||||||
className="mx-2 px-1 border-gray-200 border-2 border-solid rounded-sm bg-white text-black dark:bg-black dark:text-white"
|
className="mx-2 px-1 border-gray-200 border-2 border-solid rounded-sm bg-white text-black dark:bg-black dark:text-white"
|
||||||
value={selectedVoice}
|
value={selectedVoiceURI}
|
||||||
onChange={(e) => setSelectedVoice(e.target.value)}
|
onChange={(e) => setSelectedVoiceURI(e.target.value)}
|
||||||
>
|
>
|
||||||
{allVoices?.map(({ name, voiceURI }) => (
|
{allVoices?.map(({ name, voiceURI }) => (
|
||||||
<option key={voiceURI} value={voiceURI}>
|
<option key={voiceURI} value={voiceURI}>
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import {
|
||||||
|
DEFAULT_CHARACTERISTIC_UUID,
|
||||||
|
DEFAULT_SERVICE_UUID,
|
||||||
|
AUTO_RECONNECT_INTERVAL_MS,
|
||||||
|
} from "./constants";
|
||||||
|
import { MultimeterError } from "./custom-error";
|
||||||
|
|
||||||
|
export class BluetoothConnection {
|
||||||
|
multimeterBluetoothDevice: BluetoothDevice | null = null;
|
||||||
|
serviceUUID: string;
|
||||||
|
characteristicUUID: string;
|
||||||
|
gatt?: BluetoothRemoteGATTServer = undefined;
|
||||||
|
service?: BluetoothRemoteGATTService = undefined;
|
||||||
|
characteristic?: BluetoothRemoteGATTCharacteristic = undefined;
|
||||||
|
onNotifyExternal?: (bufferAsArray: number[]) => void = undefined;
|
||||||
|
onConnectExternal?: () => void = undefined;
|
||||||
|
onDisconnectExternal?: () => void = undefined;
|
||||||
|
autoReconnectTimer?: ReturnType<typeof setTimeout>;
|
||||||
|
shouldAutoReconnect = true;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
serviceUUID = DEFAULT_SERVICE_UUID,
|
||||||
|
characteristicUUID = DEFAULT_CHARACTERISTIC_UUID,
|
||||||
|
} = {}) {
|
||||||
|
this.serviceUUID = serviceUUID;
|
||||||
|
this.characteristicUUID = characteristicUUID;
|
||||||
|
|
||||||
|
// Bind methods
|
||||||
|
this.startConnection = this.startConnection.bind(this);
|
||||||
|
this.startAutoReconnect = this.startAutoReconnect.bind(this);
|
||||||
|
this.addConnectHandler = this.addConnectHandler.bind(this);
|
||||||
|
this.addDisconnectHandler = this.addDisconnectHandler.bind(this);
|
||||||
|
this.addNotifyHandler = this.addNotifyHandler.bind(this);
|
||||||
|
this.disconnect = this.disconnect.bind(this);
|
||||||
|
this.enableShouldAutoReconnect = this.enableShouldAutoReconnect.bind(this);
|
||||||
|
this.disableShouldAutoReconnect =
|
||||||
|
this.disableShouldAutoReconnect.bind(this);
|
||||||
|
this._onConnect = this._onConnect.bind(this);
|
||||||
|
this._onNotify = this._onNotify.bind(this);
|
||||||
|
this._onDisconnect = this._onDisconnect.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async startConnection() {
|
||||||
|
this.multimeterBluetoothDevice = await this.requestBluetoothDevice();
|
||||||
|
this.multimeterBluetoothDevice.removeEventListener(
|
||||||
|
"gattserverdisconnected",
|
||||||
|
this._onDisconnect,
|
||||||
|
);
|
||||||
|
this.multimeterBluetoothDevice.addEventListener(
|
||||||
|
"gattserverdisconnected",
|
||||||
|
this._onDisconnect,
|
||||||
|
);
|
||||||
|
this.connectGatt();
|
||||||
|
}
|
||||||
|
|
||||||
|
async autoReconnect() {
|
||||||
|
await this.connectGatt();
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestBluetoothDevice() {
|
||||||
|
const tmpBluetoothDevice = await navigator.bluetooth.requestDevice({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
services: [this.serviceUUID],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (tmpBluetoothDevice === undefined) {
|
||||||
|
throw new MultimeterError("Multimeter not selected");
|
||||||
|
}
|
||||||
|
return tmpBluetoothDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectGatt() {
|
||||||
|
if (this.multimeterBluetoothDevice == null) {
|
||||||
|
throw new MultimeterError("multimeterBluetoothDevice is not populated");
|
||||||
|
}
|
||||||
|
if (this.multimeterBluetoothDevice.gatt === undefined) {
|
||||||
|
throw new MultimeterError("BLE: Gatt is not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gatt = await this.multimeterBluetoothDevice.gatt.connect();
|
||||||
|
this.service = await this.gatt.getPrimaryService(this.serviceUUID);
|
||||||
|
this.characteristic = await this.service.getCharacteristic(
|
||||||
|
this.characteristicUUID,
|
||||||
|
);
|
||||||
|
if (this.characteristic.properties.notify) {
|
||||||
|
this.characteristic.removeEventListener(
|
||||||
|
"characteristicvaluechanged",
|
||||||
|
this._onNotify,
|
||||||
|
);
|
||||||
|
this.characteristic.addEventListener(
|
||||||
|
"characteristicvaluechanged",
|
||||||
|
this._onNotify,
|
||||||
|
);
|
||||||
|
await this.characteristic.startNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._onConnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onConnect() {
|
||||||
|
this.onConnectExternal?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onNotify() {
|
||||||
|
const buffer: ArrayBuffer | undefined = this.characteristic?.value?.buffer;
|
||||||
|
if (buffer == undefined) {
|
||||||
|
throw new MultimeterError("BLE: Notify buffer is undefined");
|
||||||
|
}
|
||||||
|
const bufferAsArray = [...new Uint8Array(buffer)];
|
||||||
|
this.onNotifyExternal?.(bufferAsArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _onDisconnect() {
|
||||||
|
console.log("Multimeter disconnected");
|
||||||
|
this.characteristic?.removeEventListener(
|
||||||
|
"characteristicvaluechanged",
|
||||||
|
this._onNotify,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.onDisconnectExternal?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
startAutoReconnect() {
|
||||||
|
// Stop, if already running
|
||||||
|
this.stopAutoReconnect();
|
||||||
|
if (this.multimeterBluetoothDevice !== null && this.shouldAutoReconnect) {
|
||||||
|
this.autoReconnectTimer = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
await this.autoReconnect();
|
||||||
|
} catch (_err) {
|
||||||
|
this.startAutoReconnect();
|
||||||
|
}
|
||||||
|
}, AUTO_RECONNECT_INTERVAL_MS);
|
||||||
|
} else {
|
||||||
|
this.autoReconnectTimer = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stopAutoReconnect() {
|
||||||
|
clearTimeout(this.autoReconnectTimer);
|
||||||
|
this.autoReconnectTimer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
addNotifyHandler(handler: (bufferAsArray: number[]) => void) {
|
||||||
|
this.onNotifyExternal = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
addConnectHandler(handler: () => void) {
|
||||||
|
this.onConnectExternal = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
addDisconnectHandler(handler: () => void) {
|
||||||
|
this.onDisconnectExternal = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.gatt?.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
enableShouldAutoReconnect() {
|
||||||
|
this.shouldAutoReconnect = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
disableShouldAutoReconnect() {
|
||||||
|
this.shouldAutoReconnect = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -78,3 +78,5 @@ export const LCD_SYMBOL_BIT_POSITION_MAP = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type KNOWN_SYMBOLS = keyof typeof LCD_SYMBOL_BIT_POSITION_MAP;
|
export type KNOWN_SYMBOLS = keyof typeof LCD_SYMBOL_BIT_POSITION_MAP;
|
||||||
|
|
||||||
|
export const AUTO_RECONNECT_INTERVAL_MS = 5000;
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export class MultimeterError extends Error {}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import {
|
||||||
|
MutableRefObject,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
|
export function useStateWithRef<TState>(
|
||||||
|
initialValue: TState,
|
||||||
|
): [TState, Dispatch<SetStateAction<TState>>, MutableRefObject<TState>] {
|
||||||
|
const [value, setValue] = useState<TState>(initialValue);
|
||||||
|
const valueRef = useRef<TState>(initialValue);
|
||||||
|
valueRef.current = value;
|
||||||
|
|
||||||
|
return [value, setValue, valueRef];
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export function debounceReadingFunc(
|
||||||
|
callbackFn: (input: string | number) => void,
|
||||||
|
debounceMillis = 1000,
|
||||||
|
) {
|
||||||
|
let timerId: ReturnType<typeof setTimeout> | 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
CHAR_MAP,
|
CHAR_MAP,
|
||||||
DEFAULT_CHARACTERISTIC_UUID,
|
|
||||||
DEFAULT_SERVICE_UUID,
|
|
||||||
KNOWN_CHARS,
|
KNOWN_CHARS,
|
||||||
KNOWN_UNKNOWN_CHARS,
|
KNOWN_UNKNOWN_CHARS,
|
||||||
LCD_CHAR_BIT_POSITIONS,
|
LCD_CHAR_BIT_POSITIONS,
|
||||||
@@ -222,105 +220,6 @@ export function parseDMMBuffer(bufferAsNumberArray: number[]) {
|
|||||||
|
|
||||||
export type TParsedDMMResponse = ReturnType<typeof parseDMMBuffer>;
|
export type TParsedDMMResponse = ReturnType<typeof parseDMMBuffer>;
|
||||||
|
|
||||||
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<typeof setTimeout> | 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(
|
export function readCharacter(
|
||||||
binString: string,
|
binString: string,
|
||||||
charIndex: number,
|
charIndex: number,
|
||||||
|
|||||||
Reference in New Issue
Block a user