This commit is contained in:
Tobias Hopp 2024-03-29 01:29:30 +01:00
parent db1731fde6
commit 8eb2d69e13
10 changed files with 408 additions and 83 deletions

46
package-lock.json generated
View File

@ -12,8 +12,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"
},
@ -1431,6 +1433,31 @@
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/icons-material": {
"version": "5.15.14",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.14.tgz",
"integrity": "sha512-vj/51k7MdFmt+XVw94sl30SCvGx6+wJLsNYjZRgxhS6y3UtnWnypMOsm3Kmg8TN+P0dqwsjy4/fX7B1HufJIhw==",
"dependencies": {
"@babel/runtime": "^7.23.9"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@mui/material": "^5.0.0",
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/material": {
"version": "5.15.14",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.14.tgz",
@ -2887,6 +2914,11 @@
"node": ">=8"
}
},
"node_modules/async": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
},
"node_modules/at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
@ -7548,7 +7580,6 @@
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash._reinterpolate": {
@ -8281,6 +8312,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/node-wifi-scanner": {
"version": "1.1.3",
"resolved": "git+https://git.gaminggeneration.de/tobiash/node-wifi-scanner#c5cbde1a3cd51687dd7fa887be8ae075417a0ca4",
"license": "MIT",
"dependencies": {
"async": "3.2.4",
"lodash": "4.17.21"
},
"engines": {
"node": ">= 4.0.0",
"npm": ">= 2.0.0"
}
},
"node_modules/nopt": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz",

46
scripts/addWifi.sh Normal file
View File

@ -0,0 +1,46 @@
#!/usr/bin/env bash
file="/etc/wpa_supplicant/wpa_supplicant-monopoly.conf"
ssid=""
psk=""
# Parse arguments
while getopts ":s:p:" opt; do
case ${opt} in
s )
ssid="$OPTARG"
;;
p )
psk="$OPTARG"
;;
: )
echo "Invalid option: $OPTARG" 1>&2
exit 1
;;
esac
done
shift $((OPTIND -1))
if [ ! -f "$file" ]; then
sudo mkdir -p /etc/wpa_supplicant/
echo "ctrl_interface=/run/wpa_supplicant
update_config=1
" | sudo tee "$file" >/dev/null
fi
# Validate arguments
if [ -z "$ssid" ] || [ -z "$psk" ]; then
echo "invalid-args"
exit 1
fi
echo "
network={
ssid=\"$ssid\"
psk=\"$psk\"
}
" | sudo tee -a "$file" >/dev/null
echo "ok"

View File

@ -1,12 +1,94 @@
#!/bin/bash
sudo ip addr flush dev wlan0
iface=$(iw dev | awk '$1=="Interface"{print $2}' | grep '^wlan')
file="/etc/wpa_supplicant/wpa_supplicant-monopoly.conf"
ssid="no_args_given"
# Parse arguments
while getopts ":s:p:" opt; do
case ${opt} in
s )
ssid="$OPTARG"
;;
: )
echo "Invalid option: $OPTARG" 1>&2
exit 1
;;
esac
done
shift $((OPTIND -1))
# Remove all disabled
sudo sed -i '/disabled=1/d' "$file"
# Temporary file to store modified content
temp_file=$(mktemp)
# Use awk to add disabled=1 within each network block
awk '
BEGIN { RS="\n\n"; FS="\n"; OFS="\n" }
{
found=0
for(i=1; i<=NF; i++) {
if ($i ~ /^network=/) {
for(j=i+1; j<=NF && $j !~ /^}/; j++) {
if ($j ~ /^disabled=1/) {
found=1
break
}
}
if (!found) {
$i = $i "\n disabled=1"
}
}
}
print $0 "\n"
}' "$file" > "$temp_file"
# Overwrite the original file with the modified content
sudo mv -f "$temp_file" "$file"
temp_file=$(mktemp)
while IFS= read -r line
do
if [[ "$line" == *"$ssid"* ]]; then
echo $line $ssid
remove_line=1
echo "remove_line=1"
fi
if [[ "$remove_line" -eq 1 && "$line" == *"disabled=1"* ]]; then
echo "skipped disabled line"
continue
fi
if [[ "$line" == *"}"* ]]; then
in_block=0
remove_line=0
fi
echo "$line" >> "$temp_file"
done < "$file"
# Overwrite the original file with the modified content
sudo mv -f "$temp_file" "$file"
cat $file
exit 0
sudo ip addr flush dev $iface
sudo killall wpa_supplicant
sudo truncate -s 0 /tmp/wifi_connection_status.txt
sudo wpa_supplicant -B -i wlan0 -f /tmp/wifi_connection_status.txt -c /etc/wpa_supplicant/wpa_supplicant.conf
sudo wpa_supplicant -B -i $iface -f /tmp/wifi_connection_status.txt -c $file
declare -i i=0
declare -i timeout=10
declare -i timeout=5
while [ $i -le $timeout ]; do
if grep -iq 'CTRL-EVENT-CONNECTED' /tmp/wifi_connection_status.txt; then
sudo dhclient wlan0

View File

@ -1,6 +1,13 @@
export type IPCChannel = 'WIFI_STATUS' | 'WIFI_SCAN' | 'WIFI_LIST' | 'WIFI_CONNECT' | 'FUNCTION_TEST';
export type IPCChannel =
'WIFI_STATUS'
| 'WIFI_SCAN'
| 'WIFI_LIST'
| 'WIFI_CONNECT'
| 'FUNCTION_TEST'
| 'CLOUD_STATUS'
| 'CLOUD_CONNECT'
export interface IPCAnswer {
@ -16,6 +23,7 @@ export interface WiFiNetwork {
ssid: string,
isSecured: boolean,
isKnown?: boolean,
psk?: string,
rssi?: number,
}

View File

@ -1,20 +1,80 @@
import {WiFiNetwork} from "./IPCConstants";
import {spawn, exec} from 'node:child_process';
import * as dns from "dns";
import * as process from "process";
import * as path from "path";
const wifiScan = require("node-wifi-scanner");
export default class OSHandler {
static getKnownWifis(): Promise<WiFiNetwork[]> {
return new Promise<WiFiNetwork[]>((resolve, reject) => {
exec("sudo touch /etc/wpa_supplicant/wpa_supplicant-monopoly.conf && sudo cat /etc/wpa_supplicant/wpa_supplicant-monopoly.conf", (err, stdout) => {
if(err)
return reject(err);
let lines = stdout.split("\n");
let wifis: WiFiNetwork[] = [];
let inBlock = false;
let currentNetwork: WiFiNetwork = {ssid: "", psk: "", isSecured: false};
for(let line of lines)
{
if(line.includes("network={"))
inBlock = true;
if(line.includes("}")) {
inBlock = false;
currentNetwork.isSecured = !!currentNetwork.psk;
if(currentNetwork.ssid)
wifis.push(currentNetwork);
currentNetwork.ssid = "";
currentNetwork.psk = "";
currentNetwork.isSecured = false;
}
if(inBlock && line.includes("ssid"))
currentNetwork.ssid = line.substring(line.indexOf('"')+1, line.lastIndexOf('"'));
if(inBlock && line.includes("psk"))
currentNetwork.psk = line.substring(line.indexOf('"')+1, line.lastIndexOf('"'));
}
resolve(wifis);
})
});
}
static addWifi(wifi: string, passkey: string | null) {
return new Promise<void>((resolve, reject) => {
let p = path.resolve(process.cwd(), "/scripts/addWifi.sh");
exec(p + ` -s "${wifi}" -p "${passkey}"`, (err, stdout) => {
if(err)
return reject(err);
if(stdout == "ok")
resolve();
else
reject("no-return");
})
});
}
static connectToWifi(ssid: string)
{
return new Promise<boolean>((resolve, reject) => {
let p = path.resolve(process.cwd(), "/scripts/connectToWifi.sh");
exec(p + ``)
});
}
static disableAllWifis() {
return new Promise<void>((resolve, reject) => {
});
}
static addWifi(wifi: WiFiNetwork, passkey: string|null) {
return new Promise<WiFiNetwork>((resolve, reject) => {
});
}
static scanWifis() {
return new Promise<WiFiNetwork[]>((resolve, reject) => {
@ -66,7 +126,6 @@ export default class OSHandler {
if (x.ssid == ele.ssid) {
return i == index;
}
i++;
}
return true;
@ -78,11 +137,6 @@ export default class OSHandler {
})
}
static disableAllWifis() {
return new Promise<void>((resolve, reject) => {
});
}
static checkForSudo() {
return new Promise<boolean>((resolve) => {
@ -102,12 +156,12 @@ export default class OSHandler {
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)
dns.lookup("google.de", 4, (err, address, family) => {
if (err)
resolve(false);
else
resolve(stdout == "1");
});
resolve(true);
})
});
}

View File

@ -12,7 +12,7 @@ const wifiScan = require("node-wifi-scanner");
export default class SmartMonopoly {
static run() {
this.setupIPCEvents();
OSHandler.checkForSudo().then(r => console.log("Wifistatus " + r))
OSHandler.getKnownWifis().then(console.log);
}
static setupIPCEvents() {
@ -30,7 +30,7 @@ export default class SmartMonopoly {
});
IPCHandler("WIFI_CONNECT", async (e, request, args) => {
let data = request.data as {wifi: WiFiNetwork, password: string}
let data = request.data as {wifi: string, password: string}
await OSHandler.addWifi(data.wifi, data.password);

View File

@ -9,18 +9,22 @@ import {
Typography,
Button,
Dialog,
DialogTitle, DialogContent, DialogContentText, DialogActions
DialogTitle, DialogContent, DialogContentText, DialogActions, Chip, FormControl
} from "@mui/material";
import {FunctionTest} from "../IPCConstants";
import SettingsIcon from '@mui/icons-material/Settings';
import PlayCircleIcon from '@mui/icons-material/PlayCircle';
import CloudOffIcon from '@mui/icons-material/CloudOff';
import CloudIcon from '@mui/icons-material/Cloud';
interface StartupState {
statusTxt: string,
open: boolean,
nextStep: boolean,
cloudConnect: boolean,
connectionIssue: boolean,
openCloudConnectModal: boolean,
showStartBtn: boolean,
isConnected: boolean,
isConnectionIssue: boolean,
openWifiQuestion: boolean,
startCounter: number,
}
export default class Startup extends Component<{}, StartupState> {
@ -28,19 +32,22 @@ export default class Startup extends Component<{}, StartupState> {
super(props);
this.state = {
statusTxt: "Smart-Monopoly wird gestartet...",
open: false,
nextStep: false,
cloudConnect: false,
connectionIssue: false,
openWifiQuestion: false
openCloudConnectModal: false,
showStartBtn: false,
isConnected: false,
isConnectionIssue: false,
openWifiQuestion: false,
startCounter: 10,
};
}
counterInterval: string | number | NodeJS.Timeout;
componentDidMount() {
setTimeout(() => {
this.setState((prevState) => ({
...prevState,
open: true,
openCloudConnectModal: true,
statusTxt: "Möchten Sie CloudConnect+ nutzen?"
}));
this.cloudDecision(true).then();
@ -51,25 +58,18 @@ export default class Startup extends Component<{}, StartupState> {
}
connectToCloud = () => {
}
checkForNext = () => {
if(this.state.cloudConnect)
{
// Connect to cloud
}
else
{
// Just start
connectToCloud = async (): Promise<boolean> => {
try {
let response = await window.api.request("CLOUD_CONNECT", {});
return response.status;
} catch (e) {
return false;
}
}
handleClose = () => this.setState((prevState) => ({
...prevState,
open: false
}));
startupBtnClick = () => {
// Startup handle
}
style = {
position: 'absolute',
@ -83,41 +83,80 @@ export default class Startup extends Component<{}, StartupState> {
p: 4,
};
async cloudDecision(decision: boolean) {
this.handleClose();
this.setState((prevState) => ({
...prevState,
cloudConnect: decision,
openWifiQuestion: false
openWifiQuestion: false,
openCloudConnectModal: false,
isConnectionIssue: false,
isConnected: false,
startCounter: 30
}));
clearInterval(this.counterInterval);
if (decision) {
this.setState((prevState) => ({
...prevState,
statusTxt: "WiFi-Verbindung wird hergestellt..."
statusTxt: "Internetverbindung wird geprüft..."
}));
let status = (await window.api.request("FUNCTION_TEST", {})).data as FunctionTest;
if(!status.hasInternet)
{
if (!status.hasInternet) {
this.setState((prevState) => ({
...prevState,
openWifiQuestion: true
openWifiQuestion: true, // Weiterleiten auf WiFiFrage
statusTxt: "Warten auf Netzwerkkonfiguration..."
}));
}
else
{
this.checkForNext();
}
}
else {
} else {
this.setState((prevState) => ({
...prevState,
statusTxt: "Ready to go!"
statusTxt: "Warten auf Cloud...",
}));
this.checkForNext();
this.connectToCloud().then((connected) => {
this.setState((prevState) => ({
...prevState,
statusTxt: "Bereit zum spielen?",
showStartBtn: true,
isConnectionIssue: !connected,
isConnected: connected
})
)
if(connected)
this.counterInterval = setInterval(() => {
this.setState((prevState) => ({
...prevState,
startCounter: prevState.startCounter-1
}));
if(this.state.startCounter == 0) {
clearInterval(this.counterInterval);
this.startupBtnClick();
}
}, 1000);
});
}
} else {
this.setState((prevState) => ({
...prevState,
statusTxt: "Bereit zum spielen?",
showStartBtn: true,
startCounter: 30
}));
this.counterInterval = setInterval(() => {
this.setState((prevState) => ({
...prevState,
startCounter: prevState.startCounter-1
}));
if(this.state.startCounter == 0) {
clearInterval(this.counterInterval);
this.startupBtnClick();
}
}, 1000);
}
}
render() {
@ -142,14 +181,41 @@ export default class Startup extends Component<{}, StartupState> {
<Button onClick={() => this.cloudDecision(false)}>
Offline nutzen
</Button>
<Button onClick={() => {window.app.toggleWiFiSettings(true)}} autoFocus>Einrichten</Button>
<Button onClick={() => {
window.app.toggleWiFiSettings(true)
}} autoFocus>Einrichten</Button>
</DialogActions>
</Dialog>
<Dialog
open={this.state.isConnectionIssue}
onClose={null}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
Cloud-Verbindung fehlgeschlagen!
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Die Cloud-Verbindung konnte nicht hergestellt werden.<br/>
Möglicherweise liegt ein Problem mit der Internetverbindung vor,<br/>
oder die Cloud ist derzeit in Wartung.<br/>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => this.cloudDecision(false)}>
Offline spielen
</Button>
<Button onClick={() =>
window.app.toggleWiFiSettings(true)} autoFocus>WiFi-Einstellungen</Button>
<Button onClick={() => this.cloudDecision(true)} autoFocus>Erneut versuchen</Button>
</DialogActions>
</Dialog>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={this.state.open}
open={this.state.openCloudConnectModal}
onClose={null}
closeAfterTransition
slots={{backdrop: Backdrop}}
@ -159,7 +225,7 @@ export default class Startup extends Component<{}, StartupState> {
},
}}
>
<Fade in={this.state.open}>
<Fade in={this.state.openCloudConnectModal}>
<Box sx={this.style}>
<Typography id="transition-modal-title" variant="h6" component="h2">
CloudConnect+ nutzen?
@ -171,19 +237,34 @@ export default class Startup extends Component<{}, StartupState> {
<i>Dafür wird eine WiFi-Verbindung hergestellt</i>
<br/>
<br/>
<Button variant="contained" onClick={() => this.cloudDecision(true)}>Ja</Button>
<Button variant="contained" sx={{mr: 1}} onClick={() => this.cloudDecision(true)}>Ja</Button>
<Button variant="contained" onClick={() => this.cloudDecision(false)}
color="error">Nein</Button>
</Typography>
</Box>
</Fade>
</Modal>
<Stack alignItems="center">
<Stack alignItems="center" sx={{width: '100%'}}>
<h1>Willkommen!</h1>
<br/>
<p>{this.state.statusTxt}</p>
<br/>
<CircularProgress/>
{!this.state.showStartBtn && <CircularProgress/>}
<Box alignItems="center" sx={{width: '100%', ml: 10}}>
<FormControl sx={{mr: 2, minWidth: '30%'}}>
{this.state.showStartBtn && <Button color="secondary" variant="contained">Setup <SettingsIcon/></Button>}
</FormControl>
<FormControl sx={{minWidth: '30%'}}>
{this.state.showStartBtn && <Button color="success" variant="contained">Start <Chip sx={{ml: 1}} size="small" label={this.state.startCounter} /> <PlayCircleIcon/></Button>}
</FormControl>
</Box>
<Chip sx={{mt: 3}} color={this.state.isConnected ? "success" : "default"} onClick={() => this.setState(prevState => ({
...prevState,
openCloudConnectModal: true
}))} label={this.state.isConnected ? <CloudIcon/> : <CloudOffIcon />}/>
</Stack>
</div>

View File

@ -23,6 +23,7 @@ import {IPCAnswer, WiFiNetwork} from "../IPCConstants";
interface WiFiState {
open: boolean,
currentSelection: string,
selectedSecured: boolean,
foundWiFis: WiFiNetwork[],
scanning: boolean,
status: status
@ -36,6 +37,7 @@ export default class WiFi extends Component<{}, WiFiState> {
this.state = {
open: true,
currentSelection: "pleaseSelect",
selectedSecured: false,
foundWiFis: [],
scanning: false,
status: "NONE",
@ -107,10 +109,20 @@ export default class WiFi extends Component<{}, WiFiState> {
}
onChange = (event: SelectChangeEvent) => {
let isSecured = false;
for(let x of this.state.foundWiFis)
{
if(x.ssid == event.target.value)
isSecured = x.isSecured;
}
this.setState((prevState) => ({
...prevState,
currentSelection: event.target.value
currentSelection: event.target.value,
selectedSecured: isSecured,
}));
}
render() {
@ -151,8 +163,7 @@ export default class WiFi extends Component<{}, WiFiState> {
labelId="wifi-select-label"
id="wifi-select"
value={this.state.currentSelection}
label="Age"
label="WiFi"
disabled={this.state.scanning}
onChange={this.onChange}
>
@ -182,7 +193,7 @@ export default class WiFi extends Component<{}, WiFiState> {
</FormControl>
<FormControl sx={{m: 1, minWidth: '70%'}}>
<TextField id="password" label="Passwort" disabled={this.state.scanning} variant="outlined" />
<TextField id="password" label="Passwort" disabled={this.state.scanning || !this.state.selectedSecured} variant="outlined" />
</FormControl>
@ -210,10 +221,6 @@ export default class WiFi extends Component<{}, WiFiState> {
onClick={() => this.handleClose()}
color="error">Abbrechen</Button>
</FormControl>
</Typography>
</Box>
</Fade>

View File

@ -43,5 +43,8 @@ body {
}
#root {
width: 100%;
height: 100%;
}

View File

@ -732,7 +732,7 @@
"@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"
resolved "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.14.tgz"
integrity sha512-vj/51k7MdFmt+XVw94sl30SCvGx6+wJLsNYjZRgxhS6y3UtnWnypMOsm3Kmg8TN+P0dqwsjy4/fX7B1HufJIhw==
dependencies:
"@babel/runtime" "^7.23.9"
@ -1639,7 +1639,7 @@ astral-regex@^2.0.0:
async@3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c"
resolved "https://registry.npmjs.org/async/-/async-3.2.4.tgz"
integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==
at-least-node@^1.0.0: