Add wifi scan ability and prepared connecting

This commit is contained in:
Tobias Hopp 2024-03-28 04:17:53 +01:00
parent 900b07f088
commit 2d7186c7c9
12 changed files with 429 additions and 41 deletions

View File

@ -47,8 +47,10 @@
"@emotion/react": "^11.11.4", "@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@fontsource/roboto": "^5.0.12", "@fontsource/roboto": "^5.0.12",
"@mui/icons-material": "^5.15.14",
"@mui/material": "^5.15.14", "@mui/material": "^5.15.14",
"electron-squirrel-startup": "^1.0.0", "electron-squirrel-startup": "^1.0.0",
"node-wifi-scanner": "git+https://git.gaminggeneration.de/tobiash/node-wifi-scanner",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0"
} }

View File

@ -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 { export interface IPCAnswer {
status: boolean, status: boolean,
data: any data?: any
} }
export interface IPCRequest { export interface IPCRequest {
data?: any, data?: any,
} }
export interface WiFiNetwork {
ssid: string,
isSecured: boolean,
isKnown?: boolean,
rssi?: number,
}
export interface FunctionTest {
hasSudo: boolean,
hasWPASupplicant: boolean,
hasInternet: boolean
}

View File

@ -3,14 +3,13 @@ import IpcMainInvokeEvent = Electron.IpcMainInvokeEvent;
import {IPCAnswer, IPCChannel, IPCRequest} from "./IPCConstants"; import {IPCAnswer, IPCChannel, IPCRequest} from "./IPCConstants";
export const IPCHandler = ( export const IPCHandler = (
channel: IPCChannel, channel: IPCChannel,
listener: ( listener: (
event: IpcMainInvokeEvent, event: IpcMainInvokeEvent,
request: IPCRequest, request: IPCRequest,
...args: any[] ...args: any[]
) => Promise<IPCAnswer> | IPCAnswer|any ) => Promise<IPCAnswer> | IPCAnswer
): void => { ): void => {
ipcMain.handle(channel, listener); ipcMain.handle(channel, listener);
}; };

114
src/OSHandler.ts Normal file
View File

@ -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<WiFiNetwork[]> {
return new Promise<WiFiNetwork[]>((resolve, reject) => {
});
}
static addWifi(wifi: WiFiNetwork) {
return new Promise<WiFiNetwork>((resolve, reject) => {
});
}
static scanWifis() {
return new Promise<WiFiNetwork[]>((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<void>((resolve, reject) => {
});
}
static checkForSudo() {
return new Promise<boolean>((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<boolean>((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");
});
});
}
}

View File

@ -1,17 +1,46 @@
import {IPCHandler} from "./IPCHandler"; 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 * Background Class for doing stuff on the host
*/ */
export default class SmartMonopoly{ export default class SmartMonopoly {
static run() { static run() {
this.setupIPCEvents(); this.setupIPCEvents();
OSHandler.checkForSudo().then(r => console.log("Wifistatus " + r))
} }
static setupIPCEvents() { 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) => { 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};
}
}) })
} }
} }

View File

@ -6,7 +6,7 @@ import {IPCAnswer, IPCChannel, IPCRequest} from "./IPCConstants";
import IpcRendererEvent = Electron.IpcRendererEvent; import IpcRendererEvent = Electron.IpcRendererEvent;
contextBridge.exposeInMainWorld('api', { contextBridge.exposeInMainWorld('api', {
request: (channel: IPCChannel, request: IPCRequest, ...args: any) => { request: (channel: IPCChannel, request: IPCRequest, ...args: any): Promise<IPCAnswer> => {
return ipcRenderer.invoke(channel, request, ...args); return ipcRenderer.invoke(channel, request, ...args);
}, },
receive: (channel: IPCChannel, func: (event: IpcRendererEvent, message: IPCAnswer, ...args: any) => void) => { receive: (channel: IPCChannel, func: (event: IpcRendererEvent, message: IPCAnswer, ...args: any) => void) => {

View File

@ -88,10 +88,10 @@ export class App extends Component<{}, AppState> {
} }
} }
showWiFiSettings = () => { toggleWiFiSettings = (state: boolean) => {
this.setState((prevState) => ({ this.setState((prevState) => ({
...prevState, ...prevState,
showWiFi: true showWiFi: state
})); }));
} }

View File

@ -1,11 +1,26 @@
import React, {Component} from "react"; import React, {Component} from "react";
import GameSetup from "./GameSetup"; import {
import {Backdrop, Box, CircularProgress, Fade, Modal, Stack, Typography, Button} from "@mui/material"; Backdrop,
Box,
CircularProgress,
Fade,
Modal,
Stack,
Typography,
Button,
Dialog,
DialogTitle, DialogContent, DialogContentText, DialogActions
} from "@mui/material";
import {FunctionTest} from "../IPCConstants";
interface StartupState { interface StartupState {
statusTxt: string, statusTxt: string,
open: boolean, open: boolean,
nextStep: boolean,
cloudConnect: boolean,
connectionIssue: boolean,
openWifiQuestion: boolean,
} }
export default class Startup extends Component<{}, StartupState> { export default class Startup extends Component<{}, StartupState> {
@ -14,7 +29,10 @@ export default class Startup extends Component<{}, StartupState> {
this.state = { this.state = {
statusTxt: "Smart-Monopoly wird gestartet...", statusTxt: "Smart-Monopoly wird gestartet...",
open: false, open: false,
nextStep: false,
cloudConnect: false,
connectionIssue: false,
openWifiQuestion: false
}; };
} }
@ -24,18 +42,29 @@ export default class Startup extends Component<{}, StartupState> {
...prevState, ...prevState,
open: true, open: true,
statusTxt: "Möchten Sie CloudConnect+ nutzen?" statusTxt: "Möchten Sie CloudConnect+ nutzen?"
})) }));
}, 2000) this.cloudDecision(true).then();
}, 1)
} }
componentWillUnmount() { componentWillUnmount() {
} }
handleOpen = () => this.setState((prevState) => ({ connectToCloud = () => {
...prevState,
open: true }
}));
checkForNext = () => {
if(this.state.cloudConnect)
{
// Connect to cloud
}
else
{
// Just start
}
}
handleClose = () => this.setState((prevState) => ({ handleClose = () => this.setState((prevState) => ({
...prevState, ...prevState,
@ -43,19 +72,24 @@ export default class Startup extends Component<{}, StartupState> {
})); }));
style = { style = {
position: 'absolute' as 'absolute', position: 'absolute',
top: '50%', top: '50%',
left: '50%', left: '50%',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
width: 400, width: 600,
bgcolor: 'background.paper', bgcolor: 'background.paper',
border: '2px solid #000', border: '2px solid #0000',
boxShadow: 24, boxShadow: 24,
p: 4, p: 4,
}; };
wifiDecision(decision: boolean) { async cloudDecision(decision: boolean) {
this.handleClose(); this.handleClose();
this.setState((prevState) => ({
...prevState,
cloudConnect: decision,
openWifiQuestion: false
}));
if(decision) { if(decision) {
@ -63,18 +97,55 @@ export default class Startup extends Component<{}, StartupState> {
...prevState, ...prevState,
statusTxt: "WiFi-Verbindung wird hergestellt..." 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 { else {
this.setState((prevState) => ({ this.setState((prevState) => ({
...prevState, ...prevState,
statusTxt: "Ready to go!" statusTxt: "Ready to go!"
})); }));
this.checkForNext();
} }
} }
render() { render() {
return <div className="startup"> return <div className="startup">
<Dialog
open={this.state.openWifiQuestion}
onClose={null}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
Keine Internetverbindung!
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Es wurde keine Internetverbindung erkannt.<br/><br/>
<strong>Möchten Sie eine WLAN-Verbindung einrichten?</strong>
<br/><i>Andernfalls können Sie SmartMonopoly offline nutzen.</i>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => this.cloudDecision(false)}>
Offline nutzen
</Button>
<Button onClick={() => {window.app.toggleWiFiSettings(true)}} autoFocus>Einrichten</Button>
</DialogActions>
</Dialog>
<Modal <Modal
aria-labelledby="transition-modal-title" aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description" aria-describedby="transition-modal-description"
@ -100,8 +171,8 @@ export default class Startup extends Component<{}, StartupState> {
<i>Dafür wird eine WiFi-Verbindung hergestellt</i> <i>Dafür wird eine WiFi-Verbindung hergestellt</i>
<br/> <br/>
<br/> <br/>
<Button variant="contained" onClick={() => this.wifiDecision(true)}>Ja</Button> <Button variant="contained" onClick={() => this.cloudDecision(true)}>Ja</Button>
<Button variant="contained" onClick={() => this.wifiDecision(false)} <Button variant="contained" onClick={() => this.cloudDecision(false)}
color="error">Nein</Button> color="error">Nein</Button>
</Typography> </Typography>
</Box> </Box>

View File

@ -1,42 +1,115 @@
import {Component} from "react"; import React, {Component} from "react";
import {Backdrop, Box, Button, CircularProgress, Fade, Modal, Stack, Typography} from "@mui/material"; 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 { interface WiFiState {
open: boolean, 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> { export default class WiFi extends Component<{}, WiFiState> {
constructor(props: {}) { constructor(props: {}) {
super(props); super(props);
this.state = { this.state = {
open: false, open: true,
currentSelection: "pleaseSelect",
foundWiFis: [],
scanning: false,
status: "NONE",
}; };
} }
componentDidMount() { componentDidMount() {
setTimeout( () => this.setState({open: true}), 500); this.scanWifi();
} }
style = { style = {
position: 'absolute' as 'absolute', position: 'absolute',
top: '50%', top: '50%',
left: '50%', left: '50%',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
width: 400, width: '50%',
bgcolor: 'background.paper', bgcolor: 'background.paper',
border: '2px solid #000', border: '2px solid #000',
boxShadow: 24, boxShadow: 24,
p: 4, p: 4,
}; };
handleClose = () => { scanWifi = () => {
this.setState((prevState) => ({ this.setState((prevState) => ({
...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() { render() {
@ -55,18 +128,84 @@ export default class WiFi extends Component<{}, WiFiState> {
> >
<Fade in={this.state.open}> <Fade in={this.state.open}>
<Box sx={this.style}> <Box sx={this.style}>
<Box sx={{width: '100%', mb: 2}}>
{(this.state.scanning||this.state.status == "CONNECTING") && <LinearProgress/>}
</Box>
<Typography id="transition-modal-title" variant="h6" component="h2"> <Typography id="transition-modal-title" variant="h6" component="h2">
WiFi Verbindungsmanager WiFi Verbindungsmanager
</Typography> </Typography>
<Typography id="transition-modal-description" sx={{mt: 2}}> <Typography id="transition-modal-description" sx={{mt: 2}}>
Folgende Netzwerke wurden gefunden... <Alert variant={this.state.status == "FAILURE" ? "filled" : "standard"}
severity={this.state.status.includes("FAILURE") ? "error" : "info"}>
{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!" }
</Alert>
<br/> <br/>
<i>Dafür wird eine WiFi-Verbindung hergestellt</i> <FormControl sx={{m: 1, minWidth: '70%'}} error={this.state.status == "SELECTION_FAILURE"}>
<InputLabel id="wifi-select-label">WiFi</InputLabel>
<Select
labelId="wifi-select-label"
id="wifi-select"
value={this.state.currentSelection}
label="Age"
disabled={this.state.scanning}
onChange={this.onChange}
>
<MenuItem disabled={true} value="pleaseSelect">Bitte wählen...</MenuItem>
{this.state.foundWiFis.map((wifi: WiFiNetwork) => (
<MenuItem value={wifi.ssid}>{wifi.ssid} {wifi.isSecured ? <WifiPasswordIcon sx={{ml: 1}}/> :
<WifiIcon sx={{ml: 1}}/>}</MenuItem>
))};
{null}
</Select>
</FormControl>
<FormControl sx={{mt: 2, minWidth: '5%'}}>
<Button variant="contained" disabled={this.state.scanning || this.state.status == "CONNECTING"} onClick={() => this.scanWifi()}>Aktualisieren
{this.state.scanning && (
<CircularProgress
size={24}
sx={{
position: 'absolute',
top: '50%',
left: '50%',
marginTop: '-12px',
marginLeft: '-12px',
}}
/>
)}</Button>
</FormControl>
<br/> <br/>
<br/> <br/>
<Button variant="contained" onClick={() => this.handleClose()}>Ja !</Button>
<Button variant="contained" onClick={() => null} <FormControl sx={{mr: 2, minWidth: '20%'}}>
color="error">Nein</Button> <Button variant="contained" disabled={this.state.status == "CONNECTING" || this.state.scanning}
onClick={() => this.handleConnect()}>Verbinden
{this.state.status == "CONNECTING" && (
<CircularProgress
size={24}
sx={{
position: 'absolute',
top: '50%',
left: '50%',
marginTop: '-12px',
marginLeft: '-12px',
}}
/>
)}</Button>
</FormControl>
<FormControl sx={{ml: 2, minWidth: '20%'}}>
<Button variant="contained" disabled={this.state.status == "CONNECTING"}
onClick={() => this.handleClose()}
color="error">Abbrechen</Button>
</FormControl>
</Typography> </Typography>
</Box> </Box>
</Fade> </Fade>

View File

@ -6,7 +6,7 @@ export {}
declare global { declare global {
interface Window { interface Window {
"api": { "api": {
request: (channel: IPCChannel, request: IPCRequest, ...args: any) => Promise<IPCAnswer> | IPCAnswer | any; request: (channel: IPCChannel, request: IPCRequest, ...args: any) => Promise<IPCAnswer>;
listen: (channel: IPCChannel, func: (event: IpcRendererEvent, message: IPCAnswer, ...args: any) => void) => void; listen: (channel: IPCChannel, func: (event: IpcRendererEvent, message: IPCAnswer, ...args: any) => void) => void;
} }
app: App; app: App;

View File

@ -7,6 +7,8 @@ body {
padding-top: 0.3rem; padding-top: 0.3rem;
/*background-color: #1F9598;*/ /*background-color: #1F9598;*/
color: black; color: black;
user-select: none;
-moz-user-select: none;
} }

View File

@ -730,6 +730,13 @@
resolved "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.14.tgz" resolved "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.14.tgz"
integrity sha512-on75VMd0XqZfaQW+9pGjSNiqW+ghc5E2ZSLRBXwcXl/C4YzjfyjrLPhrEpKnR9Uym9KXBvxrhoHfPcczYHweyA== 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": "@mui/material@^5.15.14":
version "5.15.14" version "5.15.14"
resolved "https://registry.npmjs.org/@mui/material/-/material-5.15.14.tgz" 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" resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz"
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== 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: at-least-node@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" 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: dependencies:
lodash._reinterpolate "^3.0.0" 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" version "4.17.21"
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 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" resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz"
integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== 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: nopt@^6.0.0:
version "6.0.0" version "6.0.0"
resolved "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz" resolved "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz"