mirror of
https://github.com/usatiuk/door-thing.git
synced 2025-10-26 11:17:47 +01:00
a truly horrifying autoconnect
This commit is contained in:
@@ -12,5 +12,11 @@
|
||||
"plugin:import/electron",
|
||||
"plugin:import/typescript"
|
||||
],
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"typescript": {}
|
||||
}
|
||||
},
|
||||
|
||||
"parser": "@typescript-eslint/parser"
|
||||
}
|
||||
|
||||
75
door-thing-app/package-lock.json
generated
75
door-thing-app/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
29
door-thing-app/src/preload.ts
Normal file
29
door-thing-app/src/preload.ts
Normal 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
11
door-thing-app/src/renderer.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -16,5 +16,6 @@
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
"include": ["src/**/*"],
|
||||
"files": ["src/renderer.d.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user