diff --git a/README.md b/README.md
index c9dc046..c3216df 100644
--- a/README.md
+++ b/README.md
@@ -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.)
- Doesn't work on IOS, because Web Bluetooth Api is not supported. Works on Android, Mac, Windows.
- 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
- 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
## Contribution
diff --git a/src/app/icons.tsx b/src/app/icons.tsx
new file mode 100644
index 0000000..86c15f2
--- /dev/null
+++ b/src/app/icons.tsx
@@ -0,0 +1,80 @@
+"use client";
+import React from "react";
+
+export function SpeakerOnIcon() {
+ return (
+
+ );
+}
+export function SpeakerOffIcon() {
+ return (
+
+ );
+}
+export function EyeIcon() {
+ return (
+
+ );
+}
+export function MegaphoneIcon() {
+ return (
+
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 55eefbd..39f7f7b 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,214 +1,67 @@
"use client";
-import React from "react";
+import React, { useCallback, useMemo } from "react";
import Image from "next/image";
-import {
- TParsedDMMResponse,
- debounceReadingFunc,
- parseDMMBuffer,
- startBluetoothCoonection as startBluetoothConnection,
-} from "@/helpers/parseDMM";
+import { TParsedDMMResponse, parseDMMBuffer } from "@/helpers/parseDMM";
+import { debounceReadingFunc } from "@/helpers/debounce";
+import { BluetoothConnection } from "@/helpers/bluetooth";
import { startBuzz } from "@/helpers/buzzSound";
-import {
- MutableRefObject,
- useEffect,
- useRef,
- useState,
- Dispatch,
- SetStateAction,
-} from "react";
+import { useEffect, useRef, useState } from "react";
+import { EyeIcon, MegaphoneIcon, SpeakerOnIcon, SpeakerOffIcon } from "./icons";
+import { useStateWithRef } from "@/helpers/custom-hooks";
-function SpeakerOnIcon() {
- return (
-
- );
+function useBluetoothConnection() {
+ let bluetoothConnectionRef = useRef(null);
+ if (bluetoothConnectionRef.current == null) {
+ bluetoothConnectionRef.current = new BluetoothConnection();
+ }
+ const bluetoothConnection = bluetoothConnectionRef.current;
+ const {
+ startConnection,
+ startAutoReconnect,
+ addConnectHandler,
+ addDisconnectHandler,
+ addNotifyHandler,
+ disconnect,
+ enableShouldAutoReconnect,
+ disableShouldAutoReconnect,
+ } = bluetoothConnection;
+ return {
+ startConnection,
+ startAutoReconnect,
+ addConnectHandler,
+ addDisconnectHandler,
+ addNotifyHandler,
+ disconnect,
+ enableShouldAutoReconnect,
+ disableShouldAutoReconnect,
+ };
}
-function SpeakerOffIcon() {
- return (
-
- );
-}
-
-function EyeIcon() {
- return (
-
- );
-}
-
-function MegaphoneIcon() {
- return (
-
- );
-}
-
-function useBuzzSound() {}
-
-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() {
- const [printedValue, setPrintedValue] = useState(
- undefined,
- );
- const [liveValue, setLiveValue] = useState(undefined);
+function useSpeechApi() {
const [isMute, setIsMute, isMuteRef] = useStateWithRef(false);
- const stopBuzzRef = useRef<(() => void) | undefined>(undefined);
-
- let [selectedVoice, setSelectedVoice, selectedVoiceRef] = useStateWithRef<
- string | undefined
+ let [selectedVoiceURI, setSelectedVoiceURI, selectedVoiceURIRef] =
+ useStateWithRef(undefined);
+ let [allVoices, setAllVoices, allVoicesRef] = useStateWithRef<
+ SpeechSynthesisVoice[] | undefined
>(undefined);
- let [allVoices, setAllVoices] = useState();
- let multimeterBluetoothDeviceRef = useRef(
- undefined,
+
+ const speak = useCallback(
+ 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(() => {
function _updateAllVoices() {
// Get List of Voices
@@ -220,7 +73,7 @@ export default function Home() {
if (allEnglishVoices.length > 0) {
// 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 = () => {
_updateAllVoices();
};
- }, [setSelectedVoice]);
+ }, [setSelectedVoiceURI, setAllVoices]);
+
+ return {
+ allVoices,
+ selectedVoiceURI,
+ setSelectedVoiceURI,
+ isMute,
+ setIsMute,
+ speak,
+ };
+}
+
+export default function Home() {
+ const [printedValue, setPrintedValue] = useState(
+ undefined,
+ );
+ const [liveValue, setLiveValue] = useState(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() {
setIsMute(true);
@@ -294,8 +253,8 @@ export default function Home() {