From 2d7186c7c95e8100aaaff244c7a2dbd378742c2b Mon Sep 17 00:00:00 2001 From: Tobias Hopp Date: Thu, 28 Mar 2024 04:17:53 +0100 Subject: [PATCH] Add wifi scan ability and prepared connecting --- package.json | 2 + src/IPCConstants.ts | 17 ++++- src/IPCHandler.ts | 3 +- src/OSHandler.ts | 114 +++++++++++++++++++++++++++++ src/SmartMonopoly.ts | 33 ++++++++- src/preload.ts | 2 +- src/web/App.tsx | 4 +- src/web/Startup.tsx | 103 +++++++++++++++++++++----- src/web/WiFi.tsx | 167 +++++++++++++++++++++++++++++++++++++++---- src/web/global.ts | 2 +- src/web/index.css | 2 + yarn.lock | 21 +++++- 12 files changed, 429 insertions(+), 41 deletions(-) create mode 100644 src/OSHandler.ts diff --git a/package.json b/package.json index a854758..0b6da6d 100644 --- a/package.json +++ b/package.json @@ -47,8 +47,10 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", "@fontsource/roboto": "^5.0.12", + "@mui/icons-material": "^5.15.14", "@mui/material": "^5.15.14", "electron-squirrel-startup": "^1.0.0", + "node-wifi-scanner": "git+https://git.gaminggeneration.de/tobiash/node-wifi-scanner", "react": "^18.2.0", "react-dom": "^18.2.0" } diff --git a/src/IPCConstants.ts b/src/IPCConstants.ts index 03e6be7..c3a9334 100644 --- a/src/IPCConstants.ts +++ b/src/IPCConstants.ts @@ -1,13 +1,26 @@ -export type IPCChannel = 'WIFI_STATUS' | 'WIFI_SCAN' | 'WIFI_CONNECT'; +export type IPCChannel = 'WIFI_STATUS' | 'WIFI_SCAN' | 'WIFI_LIST' | 'WIFI_CONNECT' | 'FUNCTION_TEST'; export interface IPCAnswer { status: boolean, - data: any + data?: any } export interface IPCRequest { data?: any, +} + +export interface WiFiNetwork { + ssid: string, + isSecured: boolean, + isKnown?: boolean, + rssi?: number, +} + +export interface FunctionTest { + hasSudo: boolean, + hasWPASupplicant: boolean, + hasInternet: boolean } \ No newline at end of file diff --git a/src/IPCHandler.ts b/src/IPCHandler.ts index 2ede5fb..490cae3 100644 --- a/src/IPCHandler.ts +++ b/src/IPCHandler.ts @@ -3,14 +3,13 @@ import IpcMainInvokeEvent = Electron.IpcMainInvokeEvent; import {IPCAnswer, IPCChannel, IPCRequest} from "./IPCConstants"; - export const IPCHandler = ( channel: IPCChannel, listener: ( event: IpcMainInvokeEvent, request: IPCRequest, ...args: any[] - ) => Promise | IPCAnswer|any + ) => Promise | IPCAnswer ): void => { ipcMain.handle(channel, listener); }; diff --git a/src/OSHandler.ts b/src/OSHandler.ts new file mode 100644 index 0000000..50975d4 --- /dev/null +++ b/src/OSHandler.ts @@ -0,0 +1,114 @@ +import {WiFiNetwork} from "./IPCConstants"; +import {spawn, exec} from 'node:child_process'; +const wifiScan = require("node-wifi-scanner"); + + +export default class OSHandler { + static getKnownWifis(): Promise { + return new Promise((resolve, reject) => { + + }); + } + + static addWifi(wifi: WiFiNetwork) { + return new Promise((resolve, reject) => { + + }); + } + + static scanWifis() { + return new Promise((resolve, reject) => { + wifiScan.scan(true).then((result: { + ssid: string, + mac: string, + channel: number, + rssi: number, + encrypted: boolean + }[]) => { + if (!result) + return {status: false}; + + let networks: WiFiNetwork[] = []; + for (let wifi of result) { + networks.push({ + ssid: wifi.ssid, + isKnown: true, + isSecured: wifi.encrypted, + rssi: wifi.rssi + }) + } + + // Sort best rssi to top + networks.sort((a, b) => { + return b.rssi - a.rssi; + }); + + // Sort best rssi to top + networks.sort((a, b) => { + if (b.isSecured && !a.isSecured) + return 1; + else if (b.isSecured == a.isSecured) + return 0; + else + return -1; + }); + + console.log(networks); + + networks = networks.filter((ele, index) => { + // If empty ssid + if (!ele.ssid || ele.ssid == "") + return false; + + // Remove duplicates + let i = 0; + for (let x of networks) { + if (x.ssid == ele.ssid) { + return i == index; + } + + i++; + } + return true; + }); + + resolve(networks); + }).catch(reject); + + }) + } + + static disableAllWifis() { + return new Promise((resolve, reject) => { + + }); + } + + static checkForSudo() { + return new Promise((resolve) => { + const test = spawn("sudo", ["-v"], {detached: true, stdio: ["ignore", "pipe", "pipe"]}); + + test.on("close", (code: number) => { + if (code != 0) + resolve(false); + else + resolve(true); + }); + test.on("error", (err) => { + resolve(false); + }) + }) + } + + static checkForInternet() { + return new Promise((resolve) => { + exec("ping -q -w 1 -c 1 `ip r | grep default | cut -d ' ' -f 3` > /dev/null && echo ok || echo error", (error, stdout, stderr) => { + if (error) + resolve(false); + else + resolve(stdout == "1"); + }); + }); + } + +} \ No newline at end of file diff --git a/src/SmartMonopoly.ts b/src/SmartMonopoly.ts index e346d6e..a35a975 100644 --- a/src/SmartMonopoly.ts +++ b/src/SmartMonopoly.ts @@ -1,17 +1,46 @@ import {IPCHandler} from "./IPCHandler"; +import {FunctionTest, WiFiNetwork} from "./IPCConstants"; +import OSHandler from "./OSHandler"; + +const wifiScan = require("node-wifi-scanner"); /** * Background Class for doing stuff on the host */ -export default class SmartMonopoly{ +export default class SmartMonopoly { static run() { this.setupIPCEvents(); + OSHandler.checkForSudo().then(r => console.log("Wifistatus " + r)) } static setupIPCEvents() { + IPCHandler("FUNCTION_TEST", async (e, request, args) => { + let data: FunctionTest = { + hasSudo: await OSHandler.checkForSudo(), + hasWPASupplicant: false, + hasInternet: await OSHandler.checkForInternet(), + }; + + return { + status: true, + data: data + } + }); + IPCHandler("WIFI_CONNECT", (e, request, args) => { - console.log("Got wifi connect!") + + return {status: false} + }); + + IPCHandler("WIFI_SCAN", async (e, request, args) => { + try { + let networks = await OSHandler.scanWifis(); + return {status: true, data: networks}; + } catch(e) + { + return {status: false}; + } }) } } \ No newline at end of file diff --git a/src/preload.ts b/src/preload.ts index c3baf6c..a76b2a8 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -6,7 +6,7 @@ import {IPCAnswer, IPCChannel, IPCRequest} from "./IPCConstants"; import IpcRendererEvent = Electron.IpcRendererEvent; contextBridge.exposeInMainWorld('api', { - request: (channel: IPCChannel, request: IPCRequest, ...args: any) => { + request: (channel: IPCChannel, request: IPCRequest, ...args: any): Promise => { return ipcRenderer.invoke(channel, request, ...args); }, receive: (channel: IPCChannel, func: (event: IpcRendererEvent, message: IPCAnswer, ...args: any) => void) => { diff --git a/src/web/App.tsx b/src/web/App.tsx index 91468a8..2a82e99 100644 --- a/src/web/App.tsx +++ b/src/web/App.tsx @@ -88,10 +88,10 @@ export class App extends Component<{}, AppState> { } } - showWiFiSettings = () => { + toggleWiFiSettings = (state: boolean) => { this.setState((prevState) => ({ ...prevState, - showWiFi: true + showWiFi: state })); } diff --git a/src/web/Startup.tsx b/src/web/Startup.tsx index c2f23b1..4930a22 100644 --- a/src/web/Startup.tsx +++ b/src/web/Startup.tsx @@ -1,11 +1,26 @@ import React, {Component} from "react"; -import GameSetup from "./GameSetup"; -import {Backdrop, Box, CircularProgress, Fade, Modal, Stack, Typography, Button} from "@mui/material"; +import { + Backdrop, + Box, + CircularProgress, + Fade, + Modal, + Stack, + Typography, + Button, + Dialog, + DialogTitle, DialogContent, DialogContentText, DialogActions +} from "@mui/material"; +import {FunctionTest} from "../IPCConstants"; interface StartupState { statusTxt: string, open: boolean, + nextStep: boolean, + cloudConnect: boolean, + connectionIssue: boolean, + openWifiQuestion: boolean, } export default class Startup extends Component<{}, StartupState> { @@ -14,7 +29,10 @@ export default class Startup extends Component<{}, StartupState> { this.state = { statusTxt: "Smart-Monopoly wird gestartet...", open: false, - + nextStep: false, + cloudConnect: false, + connectionIssue: false, + openWifiQuestion: false }; } @@ -24,18 +42,29 @@ export default class Startup extends Component<{}, StartupState> { ...prevState, open: true, statusTxt: "Möchten Sie CloudConnect+ nutzen?" - })) - }, 2000) + })); + this.cloudDecision(true).then(); + }, 1) } componentWillUnmount() { } - handleOpen = () => this.setState((prevState) => ({ - ...prevState, - open: true - })); + connectToCloud = () => { + + } + + checkForNext = () => { + if(this.state.cloudConnect) + { + // Connect to cloud + } + else + { + // Just start + } + } handleClose = () => this.setState((prevState) => ({ ...prevState, @@ -43,19 +72,24 @@ export default class Startup extends Component<{}, StartupState> { })); style = { - position: 'absolute' as 'absolute', + position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', - width: 400, + width: 600, bgcolor: 'background.paper', - border: '2px solid #000', + border: '2px solid #0000', boxShadow: 24, p: 4, }; - wifiDecision(decision: boolean) { + async cloudDecision(decision: boolean) { this.handleClose(); + this.setState((prevState) => ({ + ...prevState, + cloudConnect: decision, + openWifiQuestion: false + })); if(decision) { @@ -63,18 +97,55 @@ export default class Startup extends Component<{}, StartupState> { ...prevState, statusTxt: "WiFi-Verbindung wird hergestellt..." })); - window.app.showWiFiSettings(); + + let status = (await window.api.request("FUNCTION_TEST", {})).data as FunctionTest; + if(!status.hasInternet) + { + this.setState((prevState) => ({ + ...prevState, + openWifiQuestion: true + })); + } + else + { + this.checkForNext(); + } } else { this.setState((prevState) => ({ ...prevState, statusTxt: "Ready to go!" })); + this.checkForNext(); } } render() { return
+ + + Keine Internetverbindung! + + + + Es wurde keine Internetverbindung erkannt.

+ Möchten Sie eine WLAN-Verbindung einrichten? +
Andernfalls können Sie SmartMonopoly offline nutzen. +
+
+ + + + +
+ { Dafür wird eine WiFi-Verbindung hergestellt

- - + diff --git a/src/web/WiFi.tsx b/src/web/WiFi.tsx index 19fd93e..2ef0a2d 100644 --- a/src/web/WiFi.tsx +++ b/src/web/WiFi.tsx @@ -1,42 +1,115 @@ -import {Component} from "react"; -import {Backdrop, Box, Button, CircularProgress, Fade, Modal, Stack, Typography} from "@mui/material"; +import React, {Component} from "react"; +import { + Alert, + Backdrop, + Box, + Button, + CircularProgress, + Fade, + FormControl, InputLabel, LinearProgress, + MenuItem, + Modal, + Select, SelectChangeEvent, + Stack, + Typography +} from "@mui/material"; + +import WifiPasswordIcon from "@mui/icons-material/WifiPassword"; +import WifiIcon from "@mui/icons-material/Wifi"; +import {IPCAnswer, WiFiNetwork} from "../IPCConstants"; interface WiFiState { open: boolean, + currentSelection: string, + foundWiFis: WiFiNetwork[], + scanning: boolean, + status: status } +type status = "NONE" | "SELECTION_FAILURE" | "CONNECTING" | "PASSWORD_NEEDED" | "FAILURE" | "CONNECTED" | 'SCAN_FAILURE'; + export default class WiFi extends Component<{}, WiFiState> { constructor(props: {}) { super(props); this.state = { - open: false, + open: true, + currentSelection: "pleaseSelect", + foundWiFis: [], + scanning: false, + status: "NONE", }; } componentDidMount() { - setTimeout( () => this.setState({open: true}), 500); + this.scanWifi(); } style = { - position: 'absolute' as 'absolute', + position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', - width: 400, + width: '50%', bgcolor: 'background.paper', border: '2px solid #000', boxShadow: 24, p: 4, }; - handleClose = () => { + scanWifi = () => { this.setState((prevState) => ({ ...prevState, - open: false + //currentSelection: "pleaseSelect", + //foundWiFis: [], + scanning: true, })); + window.api.request('WIFI_SCAN', {}).then((answer: IPCAnswer) => { + if(answer.status) + this.setState((prevState) => ({ + ...prevState, + foundWiFis: answer.data as WiFiNetwork[], + scanning: false + })); + else + this.setState((prevState) => ({ + ...prevState, + status: "SCAN_FAILURE", + scanning: false + })); + }); + } - window.api.request("WIFI_CONNECT", {}); + handleClose = () => { + window.app.toggleWiFiSettings(false); + } + + handleConnect = () => { + if(this.state.currentSelection == "pleaseSelect") + { + return this.setState((prevState) => ({ + ...prevState, + status: "SELECTION_FAILURE" + })); + } + + this.setState((prevState) => ({ + ...prevState, + status: "CONNECTING" + })); + window.api.request('WIFI_CONNECT', {data: this.state.currentSelection}).then((answer: IPCAnswer) => { + this.setState((prevState) => ({ + ...prevState, + status: answer.status ? "CONNECTED" : "FAILURE" + })); + }) + } + + onChange = (event: SelectChangeEvent) => { + this.setState((prevState) => ({ + ...prevState, + currentSelection: event.target.value + })); } render() { @@ -55,18 +128,84 @@ export default class WiFi extends Component<{}, WiFiState> { > + + {(this.state.scanning||this.state.status == "CONNECTING") && } + WiFi Verbindungsmanager - Folgende Netzwerke wurden gefunden... + + {this.state.status == "NONE" && "Bitte wählen Sie eins der folgenden Netzwerke aus"} + {this.state.status == "CONNECTING" && "Verbinden..." } + {this.state.status == "SCAN_FAILURE" && "Das Scannen ist fehlgeschlagen. - Möglicherweise fehlen Berechtigungen, um Netzwerke zu scannen, oder es befindet sich kein WLAN-Interface auf diesem Gerät." } + {this.state.status == "FAILURE" && "Verbindungsfehler!" } + {this.state.status == "SELECTION_FAILURE" && "Bitte zunächst ein Netzwerk auswählen!" } +
- Dafür wird eine WiFi-Verbindung hergestellt + + WiFi + + + + +

- - + + + + + + + + + + +
diff --git a/src/web/global.ts b/src/web/global.ts index e8528ba..6a5c7a9 100644 --- a/src/web/global.ts +++ b/src/web/global.ts @@ -6,7 +6,7 @@ export {} declare global { interface Window { "api": { - request: (channel: IPCChannel, request: IPCRequest, ...args: any) => Promise | IPCAnswer | any; + request: (channel: IPCChannel, request: IPCRequest, ...args: any) => Promise; listen: (channel: IPCChannel, func: (event: IpcRendererEvent, message: IPCAnswer, ...args: any) => void) => void; } app: App; diff --git a/src/web/index.css b/src/web/index.css index 1ce6c14..25740b0 100644 --- a/src/web/index.css +++ b/src/web/index.css @@ -7,6 +7,8 @@ body { padding-top: 0.3rem; /*background-color: #1F9598;*/ color: black; + user-select: none; + -moz-user-select: none; } diff --git a/yarn.lock b/yarn.lock index 6b41f7d..4a1c10e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -730,6 +730,13 @@ resolved "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.14.tgz" integrity sha512-on75VMd0XqZfaQW+9pGjSNiqW+ghc5E2ZSLRBXwcXl/C4YzjfyjrLPhrEpKnR9Uym9KXBvxrhoHfPcczYHweyA== +"@mui/icons-material@^5.15.14": + version "5.15.14" + resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.15.14.tgz#333468c94988d96203946d1cfeb8f4d7e8e7de34" + integrity sha512-vj/51k7MdFmt+XVw94sl30SCvGx6+wJLsNYjZRgxhS6y3UtnWnypMOsm3Kmg8TN+P0dqwsjy4/fX7B1HufJIhw== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/material@^5.15.14": version "5.15.14" resolved "https://registry.npmjs.org/@mui/material/-/material-5.15.14.tgz" @@ -1630,6 +1637,11 @@ astral-regex@^2.0.0: resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz" integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== +async@3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + at-least-node@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" @@ -4227,7 +4239,7 @@ lodash.templatesettings@^4.0.0: dependencies: lodash._reinterpolate "^3.0.0" -lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4: +lodash@4.17.21, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4: version "4.17.21" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -4626,6 +4638,13 @@ node-releases@^2.0.14: resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== +"node-wifi-scanner@git+https://git.gaminggeneration.de/tobiash/node-wifi-scanner": + version "1.1.3" + resolved "git+https://git.gaminggeneration.de/tobiash/node-wifi-scanner#c5cbde1a3cd51687dd7fa887be8ae075417a0ca4" + dependencies: + async "3.2.4" + lodash "4.17.21" + nopt@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz"