diff --git a/gui/public/i18n/en/translation.ftl b/gui/public/i18n/en/translation.ftl index ce4026ce68..802e4e9732 100644 --- a/gui/public/i18n/en/translation.ftl +++ b/gui/public/i18n/en/translation.ftl @@ -577,6 +577,9 @@ onboarding-wifi_creds-submit = Submit! onboarding-wifi_creds-ssid = .label = Wi-Fi name .placeholder = Enter Wi-Fi name +onboarding-wifi_creds-ssidSelect = + .placeholder = Select your Wi-Fi network +onboarding-wifi_creds-ssidSelect-label = Wi-Fi network onboarding-wifi_creds-password = .label = Password .placeholder = Enter password diff --git a/gui/src-tauri/capabilities/migrated.json b/gui/src-tauri/capabilities/migrated.json index 834fa4b619..ce6c1aa163 100644 --- a/gui/src-tauri/capabilities/migrated.json +++ b/gui/src-tauri/capabilities/migrated.json @@ -28,6 +28,56 @@ "store:allow-get", "store:allow-set", "store:allow-save", - "fs:allow-write-text-file" + "fs:allow-write-text-file", + { + "identifier": "shell:allow-execute", + "allow": [ + { + "name": "netsh-list", + "cmd": "netsh", + "args": [ + "wlan", + "show", + "profile" + ], + "sidecar": false + }, + { + "name": "netsh-scan", + "cmd": "netsh", + "args": [ + "wlan", + "show", + "network", + "mode=Bssid" + ], + "sidecar": false + }, + { + "name": "netsh-details", + "cmd": "netsh", + "args": [ + "wlan", + "show", + "profile", + { + "validator": "name=\\S+" + }, + "key=clear" + ], + "sidecar": false + }, + { + "name": "netsh-connected", + "cmd": "netsh", + "args": [ + "wlan", + "show", + "interfaces" + ], + "sidecar": false + } + ] + } ] } diff --git a/gui/src/components/onboarding/pages/WifiCreds.tsx b/gui/src/components/onboarding/pages/WifiCreds.tsx index b865d0fba1..bec50a8aff 100644 --- a/gui/src/components/onboarding/pages/WifiCreds.tsx +++ b/gui/src/components/onboarding/pages/WifiCreds.tsx @@ -1,8 +1,10 @@ +import { useMemo } from 'react'; import { Localized, useLocalization } from '@fluent/react'; import { useOnboarding } from '@/hooks/onboarding'; import { useWifiForm } from '@/hooks/wifi-form'; import { Button } from '@/components/commons/Button'; import { Input } from '@/components/commons/Input'; +import { Dropdown } from '@/components/commons/Dropdown'; import { Typography } from '@/components/commons/Typography'; import classNames from 'classnames'; import { useTrackers } from '@/hooks/tracker'; @@ -11,7 +13,14 @@ import { useBnoExists } from '@/hooks/imu-logic'; export function WifiCredsPage() { const { l10n } = useLocalization(); const { applyProgress, state } = useOnboarding(); - const { control, handleSubmit, submitWifiCreds, formState } = useWifiForm(); + const { + control, + handleSubmit, + submitWifiCreds, + formState, + wifiNetworks, + watch, + } = useWifiForm(); const { useConnectedIMUTrackers } = useTrackers(); const connectedIMUTrackers = useConnectedIMUTrackers(); @@ -19,6 +28,15 @@ export function WifiCredsPage() { const bnoExists = useBnoExists(connectedIMUTrackers); + const wifiNetworksDropdownItems = useMemo(() => { + const networksMapped = wifiNetworks.map((network) => ({ + label: network.ssid, + value: network.ssid, + })); + + return [...networksMapped, { label: 'Other', value: 'other' }]; + }, [wifiNetworks]); + return (
- - - + {wifiNetworks.length > 0 && ( + <> + + {l10n.getString('onboarding-wifi_creds-ssidSelect-label')} + + + + + + )} + + {(wifiNetworks.length === 0 || watch('ssidSelect') == 'other') && ( + + + + )} + ({ defaultValues: {}, reValidateMode: 'onSubmit', }); + const { wifiNetworks } = useWifiNetworks(); + + const ssidSelect = watch('ssidSelect'); + useEffect(() => { + if (ssidSelect === 'other') { + setValue('ssid', ''); + setValue('password', ''); + return; + } + + const network = wifiNetworks.find((network) => network.ssid === ssidSelect); + + if (!network) return; + + setValue('ssid', network.ssid); + setValue('password', network.password); + }, [ssidSelect]); + useEffect(() => { if (state.wifi) { reset({ @@ -38,7 +58,9 @@ export function useWifiForm() { handleSubmit, register, formState, + wifiNetworks, hasWifiCreds: !!state.wifi, control, + watch, }; } diff --git a/gui/src/hooks/wifi-scan.ts b/gui/src/hooks/wifi-scan.ts new file mode 100644 index 0000000000..df62d789ae --- /dev/null +++ b/gui/src/hooks/wifi-scan.ts @@ -0,0 +1,261 @@ +import { Command } from '@tauri-apps/plugin-shell'; +import * as os from '@tauri-apps/plugin-os'; +import { useWebsocketAPI } from './websocket-api'; +import { + RpcMessage, + SerialTrackerGetWifiScanRequestT, + SerialUpdateResponseT, + OpenSerialRequestT, +} from 'solarxr-protocol'; +import { useEffect, useState, useMemo } from 'react'; + +export interface WifiNetwork { + ssid: string; + source: string; + password: string; + connected: boolean; + signalStrength: number | null; +} + +async function getWifiNetworksLinux(): Promise { + // TODO + return []; +} + +async function getWifiNetworksMac(): Promise { + // TODO + return []; +} + +async function getWifiNetworksWindowsSaved(): Promise { + const ret: WifiNetwork[] = []; + + const networksResponse = await Command.create('netsh-list', [ + 'wlan', + 'show', + 'profile' + ]).execute(); + + networksResponse.stdout.split('\n').forEach(line => { + const ssidMatch = line.match(/All User Profile\s+:\s+(.+)/); + if(!ssidMatch) + return; + const ssid = ssidMatch[1]; + ret.push({ + ssid: ssid, + source: 'windows', + password: '', + connected: false, + signalStrength: null, + }); + }); + + ret.forEach(async (network) => { + const profileResponse = await Command.create('netsh-details', [ + 'wlan', + 'show', + 'profile', + `name=${network.ssid}`, + 'key=clear' + ]).execute(); + const passwordMatch = profileResponse.stdout.match(/Key Content\s+:\s+(.+)/); + network.password = passwordMatch ? passwordMatch[1] : ''; + }); + + return ret; +} + +async function getWifiNetworksWindowsScan(): Promise { + const ret: WifiNetwork[] = []; + + const scanResponse = await Command.create('netsh-scan', [ + 'wlan', + 'show', + 'network', + 'mode=Bssid', + ]).execute(); + + let lastSsid: string | null = null; + + scanResponse.stdout.split('\n').forEach((line) => { + const ssidMatch = line.match(/SSID\s+:\s+(.+)/); + if (ssidMatch) { + lastSsid = ssidMatch[1]; + } + + const signalMatch = line.match(/Signal\s+:\s+(.+)/); + if (!signalMatch) return; + + if (lastSsid) { + let network = ret.find((network) => network.ssid === lastSsid); + if (network === undefined) { + network = { + ssid: lastSsid, + source: 'windows', + password: '', + connected: false, + signalStrength: null, + }; + + ret.push(network); + } + + network.signalStrength = parseInt(signalMatch[1]) / 100; + } + }); + + const connectedResponse = await Command.create('netsh-connected', [ + 'wlan', + 'show', + 'interfaces', + ]).execute(); + + const connectedMatch = connectedResponse.stdout.match(/SSID\s+:\s+(.+)/); + if (connectedMatch) { + const connectedNetwork = ret.find((network) => network.ssid === connectedMatch[1]); + if (connectedNetwork) { + connectedNetwork.connected = true; + } + } + + return ret; +} + +function useWifiNetworksSlimes() { + const [slimeWifiNetworks, setSlimeWifiNetworks] = useState([]); + const { useRPCPacket, sendRPCPacket } = useWebsocketAPI(); + const [isSerialOpen, setSerialOpen] = useState(false); + const [isSerialOpening, setSerialOpening] = useState(false); + const [scanStarted, setScanStarted] = useState(false); + + useEffect(() => { + if(isSerialOpening === false) { + setSerialOpening(true); + // sendRPCPacket(RpcMessage.CloseSerialRequest, new CloseSerialRequestT()); + const req = new OpenSerialRequestT(); + req.port = 'Auto'; + req.auto = true; + sendRPCPacket(RpcMessage.OpenSerialRequest, req); + } + + if (isSerialOpen === true) { + if (scanStarted === false) { + setScanStarted(true); + setTimeout(() => { + sendRPCPacket( + RpcMessage.SerialTrackerGetWifiScanRequest, + new SerialTrackerGetWifiScanRequestT() + ); + }, 300); + } + } + }, [isSerialOpen]); + + useRPCPacket(RpcMessage.SerialUpdateResponse, (data: SerialUpdateResponseT) => { + if (data.closed) { + if (isSerialOpen !== false) setSerialOpen(false); + return; + } else { + if (isSerialOpen !== true) setSerialOpen(true); + } + + const logString: string = data.log; + + const regex = /\d+:\s+\d+\s+(.+)\t\(-\d+\)/gm; + const match = regex.exec(logString); + if (!match) return; + + const exists = slimeWifiNetworks.find((network) => network.ssid === match[1]); + if (!exists) { + setSlimeWifiNetworks([ + ...slimeWifiNetworks, + { + ssid: match[1], + source: 'slime', + password: '', + connected: false, + signalStrength: null, + }, + ]); + } + }); + + return slimeWifiNetworks; +} + +function populateNetworksWindows(setWifiNetworks: (networks: WifiNetwork[]) => void) +{ + let networksSaved: WifiNetwork[] = []; + let networksScanned: WifiNetwork[] = []; + + getWifiNetworksWindowsSaved().then((networks) => { + networksSaved = networks; + setWifiNetworks([...networksSaved, ...networksScanned]); + }); + + getWifiNetworksWindowsScan().then((networks) => { + networksScanned = networks; + setWifiNetworks([...networksSaved, ...networksScanned]); + }); +} + +function useWifiNetworksInternal() { + const [wifiNetworks, setWifiNetworks] = useState([]); + + useEffect(() => { + os.type().then(async (platformName) => { + switch (platformName) { + case 'windows': + populateNetworksWindows(setWifiNetworks); + break; + case 'linux': + getWifiNetworksLinux().then((networks) => { + setWifiNetworks(networks); + }); + break; + case 'macos': + getWifiNetworksMac().then((networks) => { + setWifiNetworks(networks); + }); + break; + default: + console.log('Unsupported platform: ', platformName); + } + }); + }, []); + + return wifiNetworks; +} + +export function useWifiNetworks() { + const wifiNetworksSlimes = useWifiNetworksSlimes(); + const wifiNetworksInternal = useWifiNetworksInternal(); + + const wifiNetworks = useMemo(() => { + const networksConcat = [...wifiNetworksInternal, ...wifiNetworksSlimes]; + + const ret = networksConcat.reduce((acc, network) => { + const exists = acc.find((accNetwork) => accNetwork.ssid === network.ssid); + + if (!exists) { + acc.push({ + ...network, + }); + } else { + if (!exists.password && network.password) { + exists.password = network.password; + } + } + + return acc; + }, [] as WifiNetwork[]); + + return ret.sort((a, b) => { + return a.ssid.localeCompare(b.ssid); + }); + }, [wifiNetworksInternal, wifiNetworksSlimes]); + + return { + wifiNetworks, + }; +}