a truly horrifying autoconnect

This commit is contained in:
2022-04-18 21:52:31 +02:00
parent f790e5000e
commit 1fb62954aa
8 changed files with 282 additions and 42 deletions

View File

@@ -12,5 +12,11 @@
"plugin:import/electron",
"plugin:import/typescript"
],
"settings": {
"import/resolver": {
"typescript": {}
}
},
"parser": "@typescript-eslint/parser"
}

View File

@@ -9,7 +9,8 @@
"version": "0.0.1",
"license": "MIT",
"dependencies": {
"electron-squirrel-startup": "^1.0.0"
"electron-squirrel-startup": "^1.0.0",
"lowdb": "^3.0.0"
},
"devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.63",
@@ -25,6 +26,7 @@
"css-loader": "^6.7.1",
"electron": "18.0.4",
"eslint": "^8.13.0",
"eslint-import-resolver-typescript": "^2.7.1",
"eslint-plugin-import": "^2.26.0",
"fork-ts-checker-webpack-plugin": "^6.5.1",
"node-loader": "^2.0.0",
@@ -4368,6 +4370,26 @@
"ms": "^2.1.1"
}
},
"node_modules/eslint-import-resolver-typescript": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz",
"integrity": "sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"glob": "^7.2.0",
"is-glob": "^4.0.3",
"resolve": "^1.22.0",
"tsconfig-paths": "^3.14.1"
},
"engines": {
"node": ">=4"
},
"peerDependencies": {
"eslint": "*",
"eslint-plugin-import": "*"
}
},
"node_modules/eslint-module-utils": {
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz",
@@ -7000,6 +7022,20 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lowdb": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lowdb/-/lowdb-3.0.0.tgz",
"integrity": "sha512-9KZRulmIcU8fZuWiaM0d5e2/nPnrFyXkeXVpqT+MJS+vgbgOf1EbtvgQmba8HwUFgDl1oeZR6XqEJnkJmQdKmg==",
"dependencies": {
"steno": "^2.1.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
@@ -9642,6 +9678,17 @@
"node": ">= 0.6"
}
},
"node_modules/steno": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/steno/-/steno-2.1.0.tgz",
"integrity": "sha512-mauOsiaqTNGFkWqIfwcm3y/fq+qKKaIWf1vf3ocOuTdco9XoHCO2AGF1gFYXuZFSWuP38Q8LBHBGJv2KnJSXyA==",
"engines": {
"node": "^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -14547,6 +14594,19 @@
}
}
},
"eslint-import-resolver-typescript": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.7.1.tgz",
"integrity": "sha512-00UbgGwV8bSgUv34igBDbTOtKhqoRMy9bFjNehT40bXg6585PNIct8HhXZ0SybqB9rWtXj9crcku8ndDn/gIqQ==",
"dev": true,
"requires": {
"debug": "^4.3.4",
"glob": "^7.2.0",
"is-glob": "^4.0.3",
"resolve": "^1.22.0",
"tsconfig-paths": "^3.14.1"
}
},
"eslint-module-utils": {
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz",
@@ -16564,6 +16624,14 @@
"is-unicode-supported": "^0.1.0"
}
},
"lowdb": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lowdb/-/lowdb-3.0.0.tgz",
"integrity": "sha512-9KZRulmIcU8fZuWiaM0d5e2/nPnrFyXkeXVpqT+MJS+vgbgOf1EbtvgQmba8HwUFgDl1oeZR6XqEJnkJmQdKmg==",
"requires": {
"steno": "^2.1.0"
}
},
"lower-case": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
@@ -18565,6 +18633,11 @@
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"dev": true
},
"steno": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/steno/-/steno-2.1.0.tgz",
"integrity": "sha512-mauOsiaqTNGFkWqIfwcm3y/fq+qKKaIWf1vf3ocOuTdco9XoHCO2AGF1gFYXuZFSWuP38Q8LBHBGJv2KnJSXyA=="
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",

View File

@@ -53,7 +53,10 @@
{
"html": "./src/index.html",
"js": "./src/renderer.tsx",
"name": "main_window"
"name": "main_window",
"preload": {
"js": "./src/preload.ts"
}
}
]
}
@@ -76,6 +79,7 @@
"css-loader": "^6.7.1",
"electron": "18.0.4",
"eslint": "^8.13.0",
"eslint-import-resolver-typescript": "^2.7.1",
"eslint-plugin-import": "^2.26.0",
"fork-ts-checker-webpack-plugin": "^6.5.1",
"node-loader": "^2.0.0",
@@ -85,6 +89,7 @@
"typescript": "~4.5.4"
},
"dependencies": {
"electron-squirrel-startup": "^1.0.0"
"electron-squirrel-startup": "^1.0.0",
"lowdb": "^3.0.0"
}
}

View File

@@ -1,5 +1,6 @@
import { useState } from "preact/hooks";
import { useEffect, useState } from "preact/hooks";
const doorServiceUUIDString = "0x1811";
const doorServiceUUID: BluetoothServiceUUID = 0x1811;
const doorSwitchUUID: BluetoothCharacteristicUUID = 0x2ae2;
@@ -11,49 +12,93 @@ export function App() {
characteristic: BluetoothRemoteGATTCharacteristic;
} | null>(null);
const [doorOpen, setDoorOpen] = useState<boolean | null>(null);
const [autoConnect, setAutoConnect] = useState(false);
const onConnectClick = async () => {
try {
setConnecting(true);
const foundDevice = await navigator.bluetooth.requestDevice({
useEffect(() => {
window.api.send("read-setting", "autoConnect");
window.api.receive(
"read-setting-success",
(res: { key: string; value: any }) => {
if (res.key == "autoConnect") {
setAutoConnect(res.value);
}
},
);
window.api.receive(
"user-gesture-reply",
async (res: { type: string; promise: Promise<any> }) => {
if (res.type == "find-device") {
try {
const foundDevice = await window.newDevice;
console.log("Found device:");
console.log(foundDevice);
await foundDevice.gatt.connect();
foundDevice.ongattserverdisconnected = onDisconnect;
const doorService = await foundDevice.gatt.getPrimaryService(
doorServiceUUID,
);
const doorSwitch = await doorService.getCharacteristic(
doorSwitchUUID,
);
setDevice({
device: foundDevice,
service: doorService,
characteristic: doorSwitch,
});
const data = await (await doorSwitch.readValue()).getUint8(0);
setDoorOpen(Boolean(data));
doorSwitch.oncharacteristicvaluechanged = onDoorUpdate;
doorSwitch.startNotifications();
} catch (error) {
console.log("Error connecting:");
console.log(error);
} finally {
setConnecting(false);
window.newDevice = null;
}
}
},
);
}, []);
useEffect(() => {
if (autoConnect && !device && !connecting) {
tryConnect();
}
}, [autoConnect, device, connecting]);
function onAutoConnectChange(e: Event) {
const newVal = (e.currentTarget as any).checked;
setAutoConnect(newVal);
window.api.send("write-setting", { key: "autoConnect", value: newVal });
}
const tryConnect = async () => {
setConnecting(true);
//we actually need all this, because otherwise it gives "Must be handling a user gesture..." error
//and there doesn't seem to be a way to circumvent this yet
//todo: make this at least somewhat less ugly?
//similar issue: https://github.com/electron/electron/issues/27625
window.api.send("exec-user-gesture", {
type: "find-device",
function: `
window.newDevice = navigator.bluetooth.requestDevice({
filters: [
{
namePrefix: "Nano",
},
],
optionalServices: [doorServiceUUID],
});
console.log("Found device:");
console.log(foundDevice);
await foundDevice.gatt.connect();
foundDevice.ongattserverdisconnected = onDisconnect;
const doorService = await foundDevice.gatt.getPrimaryService(
doorServiceUUID,
);
const doorSwitch = await doorService.getCharacteristic(doorSwitchUUID);
setDevice({
device: foundDevice,
service: doorService,
characteristic: doorSwitch,
});
const data = await (await doorSwitch.readValue()).getUint8(0);
setDoorOpen(Boolean(data));
doorSwitch.oncharacteristicvaluechanged = onDoorUpdate;
doorSwitch.startNotifications();
} catch (error) {
console.log("Error connecting:");
console.log(error);
} finally {
setConnecting(false);
}
optionalServices: [${doorServiceUUIDString}],
})`,
});
};
const onDoorUpdate = async (ev: Event) => {
const characteristic = ev.target as BluetoothRemoteGATTCharacteristic;
const onDoorUpdate = async (e: Event) => {
const characteristic = e.target as BluetoothRemoteGATTCharacteristic;
const parsed = Boolean(await characteristic.value.getUint8(0));
setDoorOpen(parsed);
new Notification("Door update", { body: parsed ? "Open" : "Closed" });
@@ -76,9 +121,14 @@ export function App() {
return (
<div>
<input
onChange={onAutoConnectChange}
checked={autoConnect}
type="checkbox"
/>
{connecting && !device && <span>Trying to connect</span>}
{device == null && !connecting && (
<button onClick={onConnectClick}>Connect</button>
<button onClick={tryConnect}>Connect</button>
)}
{device && (
<>

View File

@@ -1,8 +1,59 @@
import { app, BrowserWindow } from "electron";
import { app, BrowserWindow, ipcMain } from "electron";
import * as path from "path";
import { JSONFile, Low } from "lowdb";
interface ISettings {
autoConnect: boolean;
}
const settingsPath = path.join(app.getPath("appData"), "settings.json");
const settings = new Low<ISettings>(new JSONFile(settingsPath));
const settingsRead = false;
async function initSettings() {
if (!settingsRead) {
await settings.read();
}
settings.data ||= {
autoConnect: false,
};
}
ipcMain.on("read-setting", async (event, arg: string) => {
await initSettings();
if (Object.keys(settings.data).includes(arg)) {
event.reply("read-setting-success", {
key: arg,
value: settings.data[arg as keyof ISettings],
});
} else {
event.reply("read-setting-error", {
key: arg,
error: "bad key",
});
}
});
ipcMain.on(
"write-setting",
async (event, arg: { key: keyof ISettings; value: any }) => {
await initSettings();
settings.data[arg.key] = arg.value;
try {
await settings.write();
event.reply("write-setting-success", arg.key);
} catch (e) {
event.reply("write-setting-fail", { key: arg.key, error: e });
}
},
);
// This allows TypeScript to pick up the magic constant that's auto-generated by Forge's Webpack
// plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on
// whether you're running in development or production).
declare const MAIN_WINDOW_WEBPACK_ENTRY: string;
declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: any;
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require("electron-squirrel-startup")) {
@@ -15,6 +66,9 @@ const createWindow = (): void => {
const mainWindow = new BrowserWindow({
height: 600,
width: 800,
webPreferences: {
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
},
});
mainWindow.webContents.on(
@@ -27,6 +81,17 @@ const createWindow = (): void => {
},
);
ipcMain.on(
"exec-user-gesture",
async (event, arg: { type: string; function: string }) => {
await mainWindow.webContents.executeJavaScript(arg.function, true);
event.reply("user-gesture-reply", {
type: arg.type,
promise: "aaaah",
});
},
);
// and load the index.html of the app.
mainWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY);

View File

@@ -0,0 +1,29 @@
import { contextBridge, ipcRenderer } from "electron";
//todo: renderer.d.ts
contextBridge.exposeInMainWorld("api", {
send: (channel: string, data: any) => {
// whitelist channels
const validChannels = [
"read-setting",
"write-setting",
"exec-user-gesture",
];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
receive: (channel: string, func: any) => {
const validChannels = [
"read-setting-success",
"read-setting-error",
"write-setting-success",
"write-setting-fail",
"user-gesture-reply",
];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
},
});

11
door-thing-app/src/renderer.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
export interface IElectronAPI {
send: (channel: string, data: any) => Promise<void>;
receive: (channel: string, func: any) => void;
}
declare global {
interface Window {
api: IElectronAPI;
newDevice: Promise<BluetoothDevice> | null;
}
}

View File

@@ -16,5 +16,6 @@
"jsx": "react-jsx",
"jsxImportSource": "preact"
},
"include": ["src/**/*"]
"include": ["src/**/*"],
"files": ["src/renderer.d.ts"]
}