From f34ca1f0dadfa1f67fdce2ec1e14239c16755494 Mon Sep 17 00:00:00 2001 From: Robin Davies Date: Thu, 12 Sep 2024 19:12:07 -0400 Subject: [PATCH] Wi-Fi Hotspot UI --- CMakeLists.txt | 13 +- PiPedalCommon/src/DBusLog.cpp | 22 +- PiPedalCommon/src/WifiConfigSettings.cpp | 162 +++- .../src/include/WifiConfigSettings.hpp | 50 +- PiPedalCommon/src/include/json.hpp | 24 + react/src/PiPedalModel.tsx | 53 +- react/src/SettingsDialog.tsx | 41 +- react/src/WifiConfigDialog.tsx | 824 ++++++++++++------ react/src/WifiConfigSettings.tsx | 22 +- src/ConfigMain.cpp | 211 ++--- src/HotspotManager.cpp | 182 +++- src/HotspotManager.hpp | 4 +- src/PiPedalModel.cpp | 17 +- src/PiPedalModel.hpp | 3 + src/PiPedalSocket.cpp | 6 + src/PluginHost.cpp | 5 +- src/PrettyPrinter.hpp | 14 + src/Storage.cpp | 9 +- src/WebServer.cpp | 3 +- src/hotspotManagerTestMain.cpp | 17 +- src/main.cpp | 1 + todo.txt | 2 - 22 files changed, 1155 insertions(+), 530 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a6b89dc..dc5b55d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,14 +19,15 @@ add_subdirectory("react") add_subdirectory("src") -add_subdirectory("NetworkManagerP2P") +# Replaced with hotspot. +#add_subdirectory("NetworkManagerP2P") -install (TARGETS pipedal_p2pd DESTINATION ${CMAKE_INSTALL_PREFIX}/sbin - ) +# install (TARGETS pipedal_p2pd DESTINATION ${CMAKE_INSTALL_PREFIX}/sbin +# ) -install (TARGETS pipedal_nm_p2pd DESTINATION ${CMAKE_INSTALL_PREFIX}/sbin -) +# install (TARGETS pipedal_nm_p2pd DESTINATION ${CMAKE_INSTALL_PREFIX}/sbin +# ) # select LV2 source directory for the current build architecture @@ -99,7 +100,7 @@ set(CPACK_DEBIAN_PACKAGE_SECTION sound) set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) set(CPACK_DEBIAN_PACKAGE_CONTROL_STRICT_PERMISSION TRUE) #set(CPACK_DEBIAN_PACKAGE_DEPENDS "jackd2, hostapd, dhcpcd, dnsmasq" ) -set(CPACK_DEBIAN_PACKAGE_DEPENDS "lv2-dev, hostapd, dhcpcd,dnsmasq, authbind, gpg" ) +set(CPACK_DEBIAN_PACKAGE_DEPENDS "lv2-dev, hostapd, authbind, gpg" ) #set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "arm64") set(CPACK_PACKAGING_INSTALL_PREFIX /usr) set(CPACK_PROJECT_NAME ${PROJECT_NAME}) diff --git a/PiPedalCommon/src/DBusLog.cpp b/PiPedalCommon/src/DBusLog.cpp index 82c0e15..46f04ba 100644 --- a/PiPedalCommon/src/DBusLog.cpp +++ b/PiPedalCommon/src/DBusLog.cpp @@ -116,12 +116,6 @@ public: static std::vector> loggers; -void SetDBusLogger(std::unique_ptr && logger) -{ - loggers.clear(); - loggers.push_back(std::move(logger)); -} - void SetDBusLogLevel(DBusLogLevel level) { @@ -231,11 +225,17 @@ void LogTrace(const std::string&path,const char*method,const std::string&message } } +void SetDBusLogger(std::unique_ptr && logger) +{ + loggers.clear(); + loggers.push_back(std::move(logger)); +} + + void SetDBusConsoleLogger() { - loggers.clear(); - loggers.push_back(std::make_unique()); + SetDBusLogger(std::make_unique()); } void AddDBusConsoleLogger() { @@ -243,13 +243,11 @@ void AddDBusConsoleLogger() } void SetDBusSystemdLogger() { - loggers.clear(); - loggers.push_back(std::make_unique()); + SetDBusLogger(std::make_unique()); } void SetDBusFileLogger(const std::filesystem::path &path) { - loggers.clear(); - loggers.push_back(std::make_unique(path)); + SetDBusLogger(std::make_unique(path)); } diff --git a/PiPedalCommon/src/WifiConfigSettings.cpp b/PiPedalCommon/src/WifiConfigSettings.cpp index 60ef354..40e7919 100644 --- a/PiPedalCommon/src/WifiConfigSettings.cpp +++ b/PiPedalCommon/src/WifiConfigSettings.cpp @@ -102,6 +102,8 @@ void WifiConfigSettings::Load() reader.read(this); } this->countryCode_ = getWifiCountryCode(); + this->enable_ = this->autoStartMode_ != (uint16_t)HotspotAutoStartMode::Never; + this->mdnsName_ = this->hotspotName_; } catch (const std::exception &e) { @@ -143,12 +145,27 @@ static void openWithPerms( void WifiConfigSettings::Save() { + WifiConfigSettings newSettings {*this}; + + // sync legacy settings, just in case i don't know what. + newSettings.mdnsName_ = newSettings.hotspotName_; + newSettings.enable_ = newSettings.IsEnabled(); + // fill in the password, if required. + if (!newSettings.hasPassword_) + { + WifiConfigSettings oldSettings; + oldSettings.Load(); + newSettings.hasPassword_ = oldSettings.hasPassword_; + newSettings.password_ = oldSettings.password_; + } + newSettings.hasSavedPassword_ = newSettings.hasPassword_; + try { ofstream_synced f; openWithPerms(f,CONFIG_PATH); json_writer writer(f); - writer.write(this); + writer.write(&newSettings); } catch (const std::exception &e) { @@ -398,6 +415,7 @@ namespace pipedal::priv JSON_MAP_BEGIN(WifiConfigSettings) +// v0 JSON_MAP_REFERENCE(WifiConfigSettings, valid) JSON_MAP_REFERENCE(WifiConfigSettings, wifiWarningGiven) JSON_MAP_REFERENCE(WifiConfigSettings, rebootRequired) @@ -408,8 +426,13 @@ JSON_MAP_REFERENCE(WifiConfigSettings, hasPassword) JSON_MAP_REFERENCE(WifiConfigSettings, password) JSON_MAP_REFERENCE(WifiConfigSettings, countryCode) JSON_MAP_REFERENCE(WifiConfigSettings, channel) -JSON_MAP_REFERENCE(WifiConfigSettings, alwaysOn) -JSON_MAP_REFERENCE(WifiConfigSettings, homeNetworks) + +// v1: Auto-hotspot + +JSON_MAP_REFERENCE(WifiConfigSettings, autoStartMode) +JSON_MAP_REFERENCE(WifiConfigSettings, homeNetwork) +JSON_MAP_REFERENCE(WifiConfigSettings, hasSavedPassword) + JSON_MAP_END() int32_t pipedal::ChannelToChannelNumber(const std::string &channel) @@ -646,22 +669,33 @@ static std::vector stringToSsidArray(const std::stri return result; } -void WifiConfigSettings::ParseArguments(const std::vector &argv) +void WifiConfigSettings::ParseArguments( + const std::vector &argv, + HotspotAutoStartMode startMode, + const std::string homeNetworkSsid + + ) { this->valid_ = false; - if (argv.size() < 4 || argv.size() > 6) + if (argv.size() != 4) { throw invalid_argument("Invalid number of arguments."); } - this->enable_ = true; + WifiConfigSettings oldSettings; + oldSettings.Load(); + this->valid_ = false; + + this->autoStartMode_ = (int16_t)startMode; + this->enable_ = startMode != HotspotAutoStartMode::Never; + this->homeNetwork_ = homeNetworkSsid; + this->countryCode_ = argv[0]; this->hotspotName_ = argv[1]; this->mdnsName_ = this->hotspotName_; this->password_ = argv[2]; this->channel_ = argv[3]; this->hasPassword_ = this->password_.length() != 0; - this->homeNetworks_ = std::vector(); - this->alwaysOn_ = false; + this->hasSavedPassword_ = oldSettings.hasSavedPassword_; if (!ValidateCountryCode(this->countryCode_)) @@ -672,26 +706,20 @@ void WifiConfigSettings::ParseArguments(const std::vector &argv) throw invalid_argument("Hotspot name is too long."); if (this->hotspotName_.length() < 1) throw invalid_argument("Hotspot name is too short."); + if (this->password_.size() != 0 && this->password_.size() < 8) throw invalid_argument("Passphrase must be at least 8 characters long."); + if (this->password_.size() == 0 && !this->hasSavedPassword_) + { + throw invalid_argument("Passphrase required."); + } + if (!ValidateChannel(this->countryCode_, this->channel_)) { throw invalid_argument("Channel is not valid."); } - if (argv.size() >= 5) - { - if (!TryStringToBool(argv[4],&this->alwaysOn_)) - { - throw std::runtime_error(SS("Invalid boolean value: " << argv[4])); - } - } - if (argv.size() >= 6) - { - this->homeNetworks_ = stringToSsidArray(argv[5]); - } - // validate that the channel number is supported for the given country code. this->valid_ = true; @@ -701,15 +729,17 @@ bool WifiConfigSettings::ConfigurationChanged(const WifiConfigSettings&other) co { return !( this->valid_ == other.valid_ && - this->rebootRequired_ == other.rebootRequired_ && - this->enable_ == other.enable_ && + //this->rebootRequired_ == other.rebootRequired_ && + //this->enable_ == other.enable_ && this->countryCode_ == other.countryCode_ && - this->mdnsName_ == other.mdnsName_ && + this->hotspotName_ == other.hotspotName_ && this->hasPassword_ == other.hasPassword_ && this->password_ == other.password_ && this->channel_ == other.channel_ && - this->homeNetworks_ == other.homeNetworks_ && - this->alwaysOn_ == other.alwaysOn_ + + this->homeNetwork_ == other.homeNetwork_ && + this->autoStartMode_ == other.autoStartMode_ && + this->hasSavedPassword_ == other.hasSavedPassword_ ); } @@ -718,33 +748,75 @@ bool WifiConfigSettings::operator==(const WifiConfigSettings&other) const return !ConfigurationChanged(other) && this->wifiWarningGiven_ == other.wifiWarningGiven_; } - -bool WifiConfigSettings::WantsHotspot(bool ethernetConnected, const std::vector &availableNetworks) +static bool CanSeeHomeNetwork(const std::string&home, const std::vector&availableNetworks) { - if ((!this->valid_) || (!this->enable_)) - return false; - if (ethernetConnected) + for (const auto &availableNetwork: availableNetworks) { - return false; - } - if (!homeNetworks_.empty()) - { - for (auto &network: availableNetworks) + if (availableNetwork == home) { - for (auto&homeNetwork: homeNetworks_) - { - if (network == homeNetwork) - { - return false; - } - } + return true; } } - if (alwaysOn_) + return false; +} + +bool WifiConfigSettings::WantsHotspot( + bool ethernetConnected, + const std::vector &availableRememberedNetworks, + const std::vector &availableNetworks) +{ + if ((!this->valid_)) + return false; + + HotspotAutoStartMode autoStartMode = (HotspotAutoStartMode)this->autoStartMode_; + + switch (autoStartMode) { - return true; + case HotspotAutoStartMode::Never: + default: + return false; + + case HotspotAutoStartMode::NoEthernetConnection: + return !ethernetConnected; + case HotspotAutoStartMode::NotAtHome: + return !CanSeeHomeNetwork(this->homeNetwork_, availableNetworks); + case HotspotAutoStartMode::NoRememberedWifiConections: + return availableRememberedNetworks.size() == 0; + case HotspotAutoStartMode::Always: + return true; } - return availableNetworks.size() == 0; +} + +std::string pipedal::ssidToString(const std::vector &ssid) +{ + std::stringstream s; + for (auto v: ssid) + { + if (v == 0) break; // breaks for some arguably legal ssids, but I can live with that. + s << (char)v; + } + return s.str(); +} + +std::vector pipedal::ssidToStringVector(const std::vector> &ssids) +{ + std::vector result; + result.reserve(ssids.size()); + for (const std::vector &ssid: ssids) + { + result.push_back(ssidToString(ssid)); + } + return result; +} +bool WifiConfigSettings::WantsHotspot( + bool ethernetConnected, + const std::vector> &availableRememberedNetworks, // remembered networks that are currently visible + const std::vector> &availableNetworks // all visible networks. + ) +{ + std::vector sAvailableRememberedNetworks = ssidToStringVector(availableRememberedNetworks); + std::vector sAvailableNetworks = ssidToStringVector(availableNetworks); + return WantsHotspot(ethernetConnected,sAvailableRememberedNetworks,sAvailableNetworks); } diff --git a/PiPedalCommon/src/include/WifiConfigSettings.hpp b/PiPedalCommon/src/include/WifiConfigSettings.hpp index 7a059dd..3713661 100644 --- a/PiPedalCommon/src/include/WifiConfigSettings.hpp +++ b/PiPedalCommon/src/include/WifiConfigSettings.hpp @@ -20,6 +20,8 @@ #pragma once #include "json.hpp" +#include +#include #ifndef NEW_WIFI_CONFIG @@ -39,12 +41,23 @@ #endif namespace pipedal { + std::string ssidToString(const std::vector &ssid); + std::vector ssidToStringVector(const std::vector> &ssids); uint32_t ChannelToWifiFrequency(const std::string &channel); uint32_t ChannelToWifiFrequency(uint32_t channel); int32_t ChannelToChannelNumber(const std::string&channel); + + enum class HotspotAutoStartMode { + // saved to file. Do not remove or renumber enums. + Never = 0, + NoEthernetConnection = 1, + NotAtHome = 2, + NoRememberedWifiConections = 3, + Always = 4 + }; class WifiConfigSettings { public: using ssid_t = std::vector; @@ -53,26 +66,45 @@ namespace pipedal { void Load(); void Save(); - bool valid_ = false; - bool wifiWarningGiven_ = false; - bool rebootRequired_ = false; - bool enable_ = false; + int16_t autoStartMode_ = 0; // see HotspotAutoStartMode + bool hasSavedPassword_ = false; + std::string homeNetwork_; + std::string countryCode_ = "US"; // iso 3661 std::string hotspotName_ = "pipedal"; - std::string mdnsName_ = "pipedal"; - std::vector homeNetworks_; bool hasPassword_ = false; std::string password_; std::string channel_ = ""; - bool alwaysOn_ = false; - void ParseArguments(const std::vector &arguments); + bool wifiWarningGiven_ = false; // Do not use. Present only for backward compatibility. + bool valid_ = false; // Do not use. Present only for backward compatibility. + private: + bool rebootRequired_ = false; // Do not use. Present only for backward compatibility. + std::string mdnsName_ = "pipedal"; + bool enable_ = false; // Do not use. Present only for backward compatibility. + public: + bool IsEnabled() const { return autoStartMode_ != 0; } + // Initialize from commandline arguments (see ConfigMain.cpp) + void ParseArguments( + const std::vector &argv, + HotspotAutoStartMode startMode, + const std::string homeNetworkSsid + ); static bool ValidateCountryCode(const std::string&value); static bool ValidateChannel(const std::string&countryCode,const std::string&value); bool operator==(const WifiConfigSettings&other) const; bool ConfigurationChanged(const WifiConfigSettings&other) const; - bool WantsHotspot(bool ethernetConnected,const std::vector &availableNetworks); + bool WantsHotspot( + bool ethernetConnected, + const std::vector &availableRememberedNetworks, // remembered networks that are currently visible + const std::vector &availableNetworks // all visible networks. + ); + bool WantsHotspot( + bool ethernetConnected, + const std::vector> &availableRememberedNetworks, // remembered networks that are currently visible + const std::vector> &availableNetworks // all visible networks. + ); public: DECLARE_JSON_MAP(WifiConfigSettings); diff --git a/PiPedalCommon/src/include/json.hpp b/PiPedalCommon/src/include/json.hpp index a8007fe..e477411 100644 --- a/PiPedalCommon/src/include/json.hpp +++ b/PiPedalCommon/src/include/json.hpp @@ -335,6 +335,14 @@ namespace pipedal { os << value; } + void write(short value) + { + os << value; + } + void write(unsigned short value) + { + os << value; + } void write(long long value) { os << value; @@ -897,6 +905,22 @@ namespace pipedal if (is_.fail()) throw JsonException("Invalid format."); } + void read(short * value) + { + skip_whitespace(); + is_ >> *value; + if (is_.fail()) + throw JsonException("Invalid format."); + } + + void read(unsigned short * value) + { + skip_whitespace(); + is_ >> *value; + if (is_.fail()) + throw JsonException("Invalid format."); + } + void read(int *value) { skip_whitespace(); diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index f9c2d9b..738a949 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -366,7 +366,8 @@ export class PiPedalModel //implements PiPedalModel clientId: number = -1; serverVersion?: PiPedalVersion; - countryCodes: Object = {}; + countryCodes: {[Name: string]: string} = {}; + socketServerUrl: string = ""; varServerUrl: string = ""; lv2Path: string = ""; @@ -956,7 +957,7 @@ export class PiPedalModel //implements PiPedalModel return response.json(); }) .then((countryCodes) => { - this.countryCodes = countryCodes as Object; + this.countryCodes = countryCodes as {[Name: string]: string}; return this.getWebSocket().request("hello"); }) @@ -2556,27 +2557,33 @@ export class PiPedalModel //implements PiPedalModel let oldSettings = this.wifiConfigSettings.get(); wifiConfigSettings = wifiConfigSettings.clone(); - if ((!oldSettings.enable) && (!wifiConfigSettings.enable)) { + if ((!oldSettings.isEnabled()) && (!wifiConfigSettings.isEnabled())) { // no effective change. resolve(); return; } - if (!wifiConfigSettings.enable) { + if (!wifiConfigSettings.isEnabled()) { wifiConfigSettings.hasPassword = false; - wifiConfigSettings.hotspotName = oldSettings.hotspotName; + wifiConfigSettings.password = ""; } else { - if (wifiConfigSettings.countryCode === oldSettings.countryCode - && wifiConfigSettings.channel === oldSettings.channel - && wifiConfigSettings.hotspotName === oldSettings.hotspotName) { - if (!wifiConfigSettings.hasPassword) { - // no effective change. - resolve(); - return; + if (wifiConfigSettings.hasPassword) + { + wifiConfigSettings.hasSavedPassword = true; } - } } // save a version for the server (potentially carrying a password) - let serverConfigSettings = wifiConfigSettings.clone(); + let serverConfigSettings: WifiConfigSettings; + if (wifiConfigSettings.isEnabled()) { + serverConfigSettings = wifiConfigSettings.clone(); + } else { + // avoid leaking edits to the server + serverConfigSettings = oldSettings.clone(); + serverConfigSettings.autoStartMode = wifiConfigSettings.autoStartMode; + wifiConfigSettings = oldSettings.clone(); + wifiConfigSettings.autoStartMode = 0; + } + + wifiConfigSettings.hasPassword = false; wifiConfigSettings.password = ""; this.wifiConfigSettings.set(wifiConfigSettings); @@ -2661,6 +2668,24 @@ export class PiPedalModel //implements PiPedalModel return result; } + + getKnownWifiNetworks() : Promise { + let result = new Promise((resolve, reject) => { + if (!this.webSocket) { + reject("Connection closed."); + return; + } + this.webSocket.request("getKnownWifiNetworks") + .then((data) => { + resolve(data); + }) + .catch((err) => reject(err)); + }); + return result; + + } + + getWifiChannels(countryIso3661: string): Promise { let result = new Promise((resolve, reject) => { if (!this.webSocket) { diff --git a/react/src/SettingsDialog.tsx b/react/src/SettingsDialog.tsx index 9faa8f5..929951e 100644 --- a/react/src/SettingsDialog.tsx +++ b/react/src/SettingsDialog.tsx @@ -681,6 +681,20 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
CONNECTION + this.handleShowWifiConfigDialog()} > + +
+ + Wi-Fi auto hotspot + + {this.state.wifiConfigSettings.getSummaryText()} + + +
+
+ { this.state.isAndroidHosted && ( @@ -699,33 +713,6 @@ const SettingsDialog = withStyles(styles, { withTheme: true })( ) } - this.handleShowWifiConfigDialog()} > - -
- - Wi-Fi auto hotspot - - {this.state.wifiConfigSettings.getSummaryText()} - - -
-
- - this.handleShowWifiConfigDialog()} > - -
- - Configure Wi-Fi hotspot - - {this.state.wifiConfigSettings.getSummaryText()} - - -
-
{(!this.props.onboarding) ? (
diff --git a/react/src/WifiConfigDialog.tsx b/react/src/WifiConfigDialog.tsx index f7f9760..5d1f6d6 100644 --- a/react/src/WifiConfigDialog.tsx +++ b/react/src/WifiConfigDialog.tsx @@ -18,17 +18,19 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import React from 'react'; +import IconButton from '@mui/material/IconButton'; +import VisibilityIcon from '@mui/icons-material/Visibility'; +import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; +import FormHelperText from '@mui/material/FormHelperText'; import Button from '@mui/material/Button'; +import Autocomplete from '@mui/material/Autocomplete'; import TextField from '@mui/material/TextField'; import DialogEx from './DialogEx'; import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; -import Switch from '@mui/material/Switch'; -import FormControlLabel from '@mui/material/FormControlLabel'; import ResizeResponsiveComponent from './ResizeResponsiveComponent'; import WifiConfigSettings from './WifiConfigSettings'; -import NoChangePassword from './NoChangePassword'; import Typography from '@mui/material/Typography'; import { Theme } from '@mui/material/styles'; import { WithStyles } from '@mui/styles'; @@ -39,7 +41,8 @@ import FormControl from '@mui/material/FormControl'; import InputLabel from '@mui/material/InputLabel'; import MenuItem from '@mui/material/MenuItem'; import WifiChannel from './WifiChannel'; -import {PiPedalModel, PiPedalModelFactory} from './PiPedalModel'; +import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; +import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; const styles = (theme: Theme) => createStyles({ pgraph: { @@ -57,319 +60,602 @@ export interface WifiConfigProps extends WithStyles { } export interface WifiConfigState { + autoStartMode: number; + showPassword: boolean; fullScreen: boolean; + compactWidth: boolean; + compactHeight: boolean; showWifiWarningDialog: boolean; + showHelpDialog: boolean; wifiWarningGiven: boolean; - enabled: boolean; name: string; newPassword: string; + hasPassword: boolean; nameError: boolean; nameErrorMessage: string; passwordError: boolean; passwordErrorMessage: string; + homeNetworkSsid: string; + homeNetworkError: boolean; + homeNetworkErrorMessage: string; countryCode: string; channel: string; + knownWifiNetworks: string[]; wifiChannels: WifiChannel[]; } +let gCountryCodeOptions: { id: string, label: string }[] | undefined = undefined; -const WifiConfigDialog = withStyles(styles, { withTheme: true })( +const WifiConfigDialog = withStyles(styles, { withTheme: true })( class extends ResizeResponsiveComponent { - refName: React.RefObject; - refPassword: React.RefObject; - model: PiPedalModel; + refName: React.RefObject; + refPassword: React.RefObject; + model: PiPedalModel; - constructor(props: WifiConfigProps) { - super(props); - this.model = PiPedalModelFactory.getInstance(); - this.state = { - fullScreen: false, - enabled: this.props.wifiConfigSettings.enable, - name: this.props.wifiConfigSettings.hotspotName, - newPassword: "", - nameError: false, - nameErrorMessage: NBSP, - passwordError: false, - showWifiWarningDialog: false, - wifiWarningGiven: this.props.wifiConfigSettings.wifiWarningGiven, - passwordErrorMessage: NBSP, - countryCode: this.props.wifiConfigSettings.countryCode, - channel: this.props.wifiConfigSettings.channel, - wifiChannels: [] - }; - this.model.getWifiChannels(this.props.wifiConfigSettings.countryCode) - .then((wifiChannels_) => { - this.setState({wifiChannels: wifiChannels_}); - }).catch((err)=> {}); - - this.refName = React.createRef(); - this.refPassword = React.createRef(); - } - mounted: boolean = false; - - handleEnableChanged(e: any) { - this.setState({ enabled: e.target.checked }); - } - - - onWindowSizeChanged(width: number, height: number): void { - this.setState({ fullScreen: height < 200 }) - } - - - componentDidMount() { - super.componentDidMount(); - this.mounted = true; - } - componentWillUnmount() { - super.componentWillUnmount(); - this.mounted = false; - } - - componentDidUpdate(prevProps: WifiConfigProps) { - - if (this.props.open && !prevProps.open) - { - this.setState({ - enabled: this.props.wifiConfigSettings.enable, + constructor(props: WifiConfigProps) { + super(props); + this.model = PiPedalModelFactory.getInstance(); + this.state = { + showPassword: false, + fullScreen: false, + compactWidth: false, + compactHeight: false, + autoStartMode: this.props.wifiConfigSettings.autoStartMode, name: this.props.wifiConfigSettings.hotspotName, newPassword: "", + hasPassword: false, + nameError: false, + nameErrorMessage: NBSP, + passwordError: false, + homeNetworkSsid: this.props.wifiConfigSettings.homeNetwork, + homeNetworkError: false, + homeNetworkErrorMessage: NBSP, + showWifiWarningDialog: false, + showHelpDialog: false, wifiWarningGiven: this.props.wifiConfigSettings.wifiWarningGiven, + passwordErrorMessage: NBSP, countryCode: this.props.wifiConfigSettings.countryCode, - channel: this.props.wifiConfigSettings.channel - }); + channel: this.props.wifiConfigSettings.channel, + knownWifiNetworks: [], + wifiChannels: [] + }; + this.requestWifiChannels(this.props.wifiConfigSettings.countryCode); + this.requestKnownWifiNetworks(); - if (this.props.wifiConfigSettings.countryCode !== this.state.countryCode) - { - this.model.getWifiChannels(this.props.wifiConfigSettings.countryCode) - .then((wifiChannels) => { - this.setState({wifiChannels: wifiChannels}); - }) - .catch((error) => {}); + this.refName = React.createRef(); + this.refPassword = React.createRef(); + } + mounted: boolean = false; - } - + + + onWindowSizeChanged(width: number, height: number): void { + this.setState({ fullScreen: height < 200, compactWidth: width < 600, compactHeight: height < 450 }) } - } - handleOk(wifiWarningGiven: boolean) { - let hasError = false; - if (this.state.enabled) - { - let name = this.state.name; - if (name.length === 0) { - this.setState({nameError: true, nameErrorMessage: "* Required"}); - hasError = true; - } else if (name.length > 31) - { - this.setState({nameError: true, nameErrorMessage: "> 31 characters"}); + componentDidMount() { + super.componentDidMount(); + this.mounted = true; + this.applyDeferredWifiChannels(); + this.applyDeferredWifiNetworks(); + } + componentWillUnmount() { + super.componentWillUnmount(); + this.mounted = false; + } + + private deferredWifiNetworks?: string[] = undefined; + + applyDeferredWifiNetworks() { + if (this.mounted && this.deferredWifiNetworks) { + this.setState({ knownWifiNetworks: this.deferredWifiNetworks }); + this.deferredWifiNetworks = undefined; + } + } + requestKnownWifiNetworks() { + this.model.getKnownWifiNetworks() + .then((networks: string[]) => { + if (this.mounted) { + this.setState({ knownWifiNetworks: networks }); + } else { + this.deferredWifiNetworks = networks; + } + }).catch(() => { + + }); + } + + private lastChannelRequestCountryCode = ""; + private deferredWifiChannels?: WifiChannel[] = undefined; + + applyDeferredWifiChannels() { + if (this.mounted && this.deferredWifiChannels) { + this.setState({ wifiChannels: this.deferredWifiChannels }); + this.deferredWifiChannels = undefined; + } + } + + requestWifiChannels(countryCode: string) { + if (countryCode === this.lastChannelRequestCountryCode) { + return; + } + this.lastChannelRequestCountryCode = countryCode; + this.model.getWifiChannels(countryCode) + .then((wifiChannels) => { + if (this.mounted) { + this.setState({ wifiChannels: wifiChannels }); + } else { + } + }) + .catch((error) => { + this.lastChannelRequestCountryCode = ""; // try again later. :-/ + + }); + + } + + componentDidUpdate(prevProps: WifiConfigProps) { + + if (this.props.open && !prevProps.open) { + this.setState({ + name: this.props.wifiConfigSettings.hotspotName, + newPassword: "", + autoStartMode: this.props.wifiConfigSettings.autoStartMode, + homeNetworkSsid: this.props.wifiConfigSettings.homeNetwork, + wifiWarningGiven: this.props.wifiConfigSettings.wifiWarningGiven, + countryCode: this.props.wifiConfigSettings.countryCode, + channel: this.props.wifiConfigSettings.channel + }); + this.requestKnownWifiNetworks(); + this.requestWifiChannels(this.props.wifiConfigSettings.countryCode); + + } + this.applyDeferredWifiChannels(); + + } + + getDefaultPasswordValue(): string { + return (this.state.showPassword && this.state.newPassword === "") ? + "password" : + this.state.newPassword; + } + + handleOk(wifiWarningGiven: boolean) { + let hasError = false; + if (this.state.autoStartMode !== 0) { + let name = this.state.name; + if (name.length === 0) { + this.setState({ nameError: true, nameErrorMessage: "* Required" }); + hasError = true; + } else if (name.length > 31) { + this.setState({ nameError: true, nameErrorMessage: "> 31 characters" }); + hasError = true; + } + let password = this.state.newPassword; + if (!this.props.wifiConfigSettings.hasSavedPassword && password.length === 0) { + this.setState({ passwordError: true, passwordErrorMessage: "* required" }); + + } else if (password.length > 0) { + if (password.length < 8) { + this.setState({ passwordError: true, passwordErrorMessage: "Less than 8 characters" }); + hasError = true; + + } else if (password.length > 63) { + this.setState({ passwordError: true, passwordErrorMessage: "> 63 characters" }); + hasError = true; + + } + } + if (this.state.autoStartMode === 2 && this.state.homeNetworkSsid.length === 0) { + this.setState({ homeNetworkError: true, homeNetworkErrorMessage: "* Required" }); + hasError = true; + } + } + if (this.state.autoStartMode !== 0 && (!this.props.wifiConfigSettings.hasSavedPassword) && this.state.newPassword.length === 0) { + this.setState({ passwordError: true, passwordErrorMessage: "* Required" }); hasError = true; } - let password = this.state.newPassword; - if (password.length > 0) { - if (password.length < 8) { - this.setState({passwordError: true, passwordErrorMessage: "Less than 8 characters"}); - hasError = true; + if (!hasError) { + let wifiConfigSettings = new WifiConfigSettings(); + wifiConfigSettings.valid = true; - } else if (password.length > 63) { - this.setState({passwordError: true, passwordErrorMessage: "> 63 characters"}); - hasError = true; + wifiConfigSettings.autoStartMode = this.state.autoStartMode; + wifiConfigSettings.homeNetwork = this.state.homeNetworkSsid; + wifiConfigSettings.hotspotName = this.state.name; + wifiConfigSettings.countryCode = this.state.countryCode; + wifiConfigSettings.channel = this.state.channel; + if (this.state.newPassword.length === 0 || this.state.autoStartMode === 0) { + wifiConfigSettings.hasPassword = false; + } else { + wifiConfigSettings.hasPassword = true; + wifiConfigSettings.password = this.state.newPassword; } - } - } - if (this.state.enabled && (!this.props.wifiConfigSettings.hasPassword) && this.state.newPassword.length === 0) - { - this.setState({passwordError: true, passwordErrorMessage: "* Required"}); - hasError = true; - } - if (!hasError) - { - let wifiConfigSettings = new WifiConfigSettings(); - wifiConfigSettings.valid = true; - - wifiConfigSettings.enable = this.state.enabled; - wifiConfigSettings.hotspotName = this.state.name; - wifiConfigSettings.countryCode = this.state.countryCode; - wifiConfigSettings.channel = this.state.channel; - - if (this.state.newPassword.length === 0 || !this.state.enabled) - { - wifiConfigSettings.hasPassword = false; - } else { - wifiConfigSettings.hasPassword = true; - wifiConfigSettings.password = this.state.newPassword; - } - - if (!this.props.wifiConfigSettings.wifiWarningGiven && !wifiWarningGiven) { - this.setState({showWifiWarningDialog: true}); - } else { - wifiConfigSettings.wifiWarningGiven = true; - this.preventPasswordPrompt(); - // let preventPasswordPrompt changes settle (it's HARD to prevent a password prompt) - setTimeout(()=> { + if (!this.props.wifiConfigSettings.wifiWarningGiven && !wifiWarningGiven) { + this.setState({ showWifiWarningDialog: true }); + } else { + wifiConfigSettings.wifiWarningGiven = true; this.props.onOk(wifiConfigSettings); - },100); + } } + } - - } - preventPasswordPrompt() - { - let passwordInput = this.refPassword.current; - if (passwordInput) - { - passwordInput.type = "text"; - passwordInput.value = ""; + + handleChannelChange(e: any) { + let value = e.target.value as string; + this.setState({ channel: value }); } - this.setState({newPassword: ""}); - } - handleChannelChange(e: any) - { - let value = e.target.value as string; - this.setState({channel: value }); - } - handleCountryChanged(e: any) - { - let value = e.target.value as string; + handleCountryChanged(e: any) { + let value = e.target.value as string; - this.setState({countryCode: value}); - this.model.getWifiChannels(value) - .then(wifiChannels => { - this.setState({wifiChannels: wifiChannels}); - }) - .catch((error)=> { }); - } + this.setState({ countryCode: value }); + this.requestWifiChannels(value); + } + handleTogglePasswordVisibility() { + this.setState({ hasPassword: true, showPassword: !this.state.showPassword }); + } - render() { - let props = this.props; - let classes = this.props.classes; - let { open, onClose} = props; + private homeNetworkSsidElement?: HTMLInputElement = undefined; - const handleClose = () => { - this.preventPasswordPrompt(); - // let preventPasswordPrompt changes settle (it's HARD to prevent a password prompt) - setTimeout(()=> { + getCountryCodeValue(countryCode: string) { + let value = this.model.countryCodes[countryCode]; + if (!value) return undefined; + return { label: value, id: countryCode }; + } + + getCountryCodeOptions(): { id: string, label: string }[] { + if (gCountryCodeOptions) { + return gCountryCodeOptions; + } + let result: { label: string, id: string }[] = []; + for (let key in this.model.countryCodes) { + let value = this.model.countryCodes[key]; + result.push({ id: key, label: value }); + } + result.sort((left, right) => { + return left.label.localeCompare(right.label); + }); + if (result.length !== 0) { + gCountryCodeOptions = result; + } + return result; + } + render() { + let props = this.props; + let classes = this.props.classes; + let { open, onClose } = props; + + const handleClose = () => { onClose(); - },100); - }; - - return ( - - { this.state.fullScreen && ( - Wi-fi Hotspot - )} - - - this.handleEnableChanged(e)} - color="primary" - /> - )} - label="Enable" - /> - this.setState({name: e.target.value, nameError: false, nameErrorMessage: NBSP})} - inputRef={this.refName} - disabled={!this.state.enabled} - /> -
- - { this.setState({ newPassword: text, passwordError: false, passwordErrorMessage: NBSP }); }} - hasPassword={this.props.wifiConfigSettings.hasPassword} - label="WEP Passphrase" - error={this.state.passwordError} - helperText={this.state.passwordErrorMessage} - defaultValue={this.state.newPassword} - disabled={!this.state.enabled} - /> -
-
- - Country - - - -
-
- - Channel - - - -
- -
- - - - - + }; + let enabled = this.state.autoStartMode !== 0; + return ( + + {(this.state.fullScreen || !this.state.compactHeight) && ( + Wi-fi Hotspot + )} - - Enabling the Wi-Fi hotspot will disable regular Wi-Fi network access on the PiPedal device. Once - enabled, connect to the hotspot and launch http://172.23.0.2 or http://{this.state.name}.local to access the PiPedal web app again. - - - Are you sure you want to proceed? - +
+
+ + + Auto-start hotspot when + + {NBSP} + + {(this.state.compactWidth || this.state.autoStartMode !== 2) && ( + { this.setState({ showHelpDialog: true }); }} + > + + + )} +
+
+ { + this.setState({ homeNetworkSsid: value?.toString() ?? "", homeNetworkError: false, homeNetworkErrorMessage: NBSP }); + } + } + + renderInput={ + (params) => + { + this.setState({ homeNetworkSsid: event.target.value, homeNetworkError: false, homeNetworkErrorMessage: NBSP }); + } + + } + helperText={this.state.homeNetworkErrorMessage} + InputLabelProps={{ + shrink: true + }} + />} + /> + { this.setState({ showHelpDialog: true }); }} + > + + + +
+
+
+ + this.setState({ name: e.target.value, nameError: false, nameErrorMessage: NBSP })} + inputRef={this.refName} + InputLabelProps={{ + shrink: true + }} + disabled={!enabled} + /> +
+
false}> {/*Prevents chrome from saving passwords */} + { this.setState({ hasPassword: true }) }} + type={this.state.showPassword ? "text" : "password"} + onChange={(event): void => { + this.setState({ hasPassword: true, newPassword: event.target.value.toString(), passwordError: false, passwordErrorMessage: NBSP }); + } + } + helperText={this.state.passwordErrorMessage} + defaultValue={ + this.getDefaultPasswordValue() + } + disabled={!enabled} + InputLabelProps={{ + shrink: true + }} + InputProps={{ + startAdornment: + this.props.wifiConfigSettings.hasSavedPassword && !this.state.hasPassword && !this.state.showPassword ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : "" + , + endAdornment: ( + { this.handleTogglePasswordVisibility(); }} + > + { + (this.state.showPassword) ? + ( + + ) : + ( + + + ) + } + + ) + }} + /> + +
+ +
+
+
+ { if (value) { this.setState({ countryCode: value.id }) } }} + options={this.getCountryCodeOptions()} + renderInput={(params) => ()} + /> + {/* + + */} + +
+
+ + Channel + + + +
+
+ +
- - -
-
- ); - } -}); + {this.state.showHelpDialog && ( + + + + PiPedal Auto-Hotspot + + + + The PiPedal Auto-Hotspot feature allows you to connect to your Raspberry Pi even if you don't have + access to a Wi-Fi router. For example, if you are performing at a live venue, you probably will not + have access to a Wi-Fi router; but you can configure PiPedal so that your Raspberry Pi automatically + starts a Wi-Fi hotspot when you are not at home. The feature is primarily intended for use with the + PiPedal Android client, but you may find it useful for other purposes as well. + + + Raspberry Pi's are unable to run hotspots, and have another active Wi-Fi connection at the same time; so the auto-hotspot feature + automatically turns the hotspot on, when your Raspberry Pi cannot otherwise be connected to, and can be configured to + automatically turn the PiPedal hotspot off when you do want your Raspberry Pi to connect to another Wi-Fi access point. + Which auto-start option you should select depends on how you normally connect to your Raspberry Pi when you are at home. + + + If you normally connect to your Raspberry Pi using an ethernet connection, the No ethernet connection is a + good choice. The PiPedal Wi-Fi hotspot will be available whenever the ethernet cable is unplugged. Always on is + also a good choice, but may confuse your phone or tablet, since your Android device will now have to decide whether to auto-connect to your home Wi-Fi + router, or to the Raspberry Pi hotspot. If you use the No ethernet connection option, your phone or tablet will + never see the PiPedal hotspot and your Wi-Fi router at the same time. + + + If you normally connect to your Raspberry Pi through a Wi-Fi router, Not at home is a good choice. The + PiPedal hotspot will be automatically turned off whenever your home Wi-Fi router is in range, and automatically turned on + when you are out of range of your home Wi-Fi router. + + + If there are multiple locations, and multiple Wi-Fi routers you use with PiPedal on a regular basis, you can select + the No remembered Wi-Fi connections option, but this is a riskier option. The PiPedal hotspot will be automatically turned on if there are no + Wi-Fi access points in range that you have previously connected to from your Raspberry Pi, and will be automatically turned on otherwise. + The risk is that you could find yourself unable to connect to your Raspberry Pi when performing + at a local bar, after you have used your Rasberry Pi to connect to the Wi-Fi access point at the coffee shop nextdoor. (Public Wi-Fi access + points usually won't work because devices that are connected to a public access point can't connect to each other). + Will you ever do that? Probably not. But there is some risk that you might find yourself unable to connect at a live venue. Whether that's an + acceptable risk is up to you. + + + Typically, when you're away from home, there's no easy way to connect to your Raspberry Pi from a laptop in order to + correct the problem. So you should carefully test that your auto-hotspot configuration works as expected before you adventure + away from home with PiPedal. + + + + + + + + + + )} + {this.state.showWifiWarningDialog && ( + + + + Enabling the Wi-Fi hotspot will disable regular Wi-Fi network access on your Raspberry Pi when the PiPedal hotspot is active. + If you are relying on Wi-Fi access to connect to your Raspberry Pi, consider carefully whether your autostart options + will allow you to connect to your Raspberry Pi once applied. PiPedal + + online documentation provides a discussion of how to choose safe hotspot auto-start options. + + + When you are connected to the PiPedal hotspot, you can connect to the PiPedal web server at http://10.48.0.1. + + + Are you sure you want to continue? + + + + + + + + )} +
+ ); + } + }); export default WifiConfigDialog; \ No newline at end of file diff --git a/react/src/WifiConfigSettings.tsx b/react/src/WifiConfigSettings.tsx index b4f0c02..401e22f 100644 --- a/react/src/WifiConfigSettings.tsx +++ b/react/src/WifiConfigSettings.tsx @@ -21,10 +21,12 @@ export default class WifiConfigSettings { deserialize(input: any) : WifiConfigSettings{ + this.autoStartMode = input.autoStartMode; + this.hasSavedPassword = input.hasSavedPassword; + this.homeNetwork = input.homeNetwork; this.valid = input.valid; + this.wifiWarningGiven = input.wifiWarningGiven; - this.rebootRequired = input.rebootRequired; - this.enable = input.enable; this.hotspotName = input.hotspotName; this.hasPassword = input.hasPassword; this.password = input.password; @@ -35,29 +37,27 @@ export default class WifiConfigSettings { clone() : WifiConfigSettings { return new WifiConfigSettings().deserialize(this); } - valid: boolean = true; + autoStartMode: number = 0; + hasSavedPassword: boolean = false; + homeNetwork: string = ""; wifiWarningGiven: boolean = false; - enable: boolean = true; hasPassword: boolean = false; - rebootRequired = false; + valid: boolean = false; hotspotName: string = "pipedal"; password: string = ""; countryCode: string = "US"; - channel: string = "g6"; + channel: string = "0"; + isEnabled() { return this.autoStartMode !== 0;} getSummaryText() { let result: string; if (!this.valid) { result = "Not available."; - } else if (!this.enable) { + } else if (this.autoStartMode === 0) { result = "Disabled."; } else { result = this.hotspotName; } - if (this.rebootRequired) - { - result += " (Restart required)"; - } return result; } diff --git a/src/ConfigMain.cpp b/src/ConfigMain.cpp index 0a67f51..565f00a 100644 --- a/src/ConfigMain.cpp +++ b/src/ConfigMain.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Robin Davies +// Copyright (c) 2022-2024 Robin Davies // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the "Software"), to deal in @@ -1132,7 +1132,7 @@ static void PrintHelp() PrettyPrinter pp; pp << "pipedalconfig - Command-line post-install configuration for PiPedal" << "\n" - << "Copyright (c) 2022 Robin Davies. All rights reserved." + << "Copyright (c) 2022-2024 Robin Davies." << "\n" << "\n" << "See https://rerdavies.github.io/pipedal/Documentation.html for " @@ -1157,10 +1157,10 @@ static void PrintHelp() << "\n" << HangingIndent() << " --install [--port ]\t" - << "Install or re-install services and service accounts." + << "Install or re-install PiPedal services and service accounts." << "\n" << "\n" - << "The --port option controls which port the web server uses." + << "The --port option controls which TCP/IP port the web server uses." << "\n\n" << HangingIndent() << " --uninstall\t" @@ -1169,68 +1169,54 @@ static void PrintHelp() << "\n" << HangingIndent() << " --enable\t" - << "Start the pipedal service at boot time." + << "Start PiPedal services at boot time." << "\n" << "\n" - << HangingIndent() << " --disable\tDo not start the pipedal service at boot time." + << HangingIndent() << " --disable\tDo not start PiPedal services at boot time." << "\n" << "\n" - << HangingIndent() << " --start\tStart the pipedal services." + << HangingIndent() << " --start\tStart PiPedal services." << "\n" << "\n" - << HangingIndent() << " --stop\tStop the pipedal services." + << HangingIndent() << " --stop\tStop PiPedal services." << "\n" << "\n" - << HangingIndent() << " --restart\tRestart the pipedal services." + << HangingIndent() << " --restart\tRestart PiPedal services." << "\n" << "\n" - - << HangingIndent() << " --enable-p2p [ [[] ] ]\t" - << "Enable the P2P (Wi-Fi Direct) hotspot." + << HangingIndent() << " --enable-hotspot\t []" + << "\n\nEnable Wi-Fi hotspot." << "\n\n" - << "With no additional arguments, the P2P channel is enabled with most-recent settings." + << "PiPedal's Wi-Fi hotspot allows you to connect to your Raspberry Pi when you don't have a Wi-Fi router, for example, when you are " + << "performing away from home. It is most particularly useful when using the PiPedal Android client. Consult PiPedal's online documentation " + << "for guidance on configuring a Wi-Fi hotspot." << "\n\n" - << " is the 2-letter ISO-3166 country code for " - "the country you are in. see below for further notes." + << "One of the following hotspot options can be specifed. If no hotspot option is given, the hotspot will always be enabled. " + << "If one of the hotspot options are given, the PiPedal server will turn the hotspot on or off automatically, as conditions change." + << "\n\n" + << "--home-network \n" + << AddIndent(4) << "Hotspot is disabled if the specificed Wi-Fi network is detected.\n" + << AddIndent(-4) + << "--no-ethernet\n" + << AddIndent(4) << "Hotspot is disabled if an ethernet network is connected.\n" + << AddIndent(-4) + << "--no-wifi\n" + << AddIndent(4) << "Hotspot is disabled if a remembered Wi-Fi network is detected.\n" + << AddIndent(-4) + << "\n" + << "Caution: Wi-Fi connections are disabled when the hotspot is activated. If you currently access your Raspberry Pi using " + << "Wi-Fi, choose your hotspot options carefully, to ensure that you can still access your Raspberry Pi." << "\n\n" - << " is the name you see when connecting. " + << "country_code is the 2-letter ISO-3166 country code for " + << "the country you are in. see below for further notes." << "\n\n" - << " is an exactly-eight-digit pin number that you must " - << "enter when connecting to the hotspot. If you don't " - << "provide a pin, pipedalconfig will generate and " - << "display a random pin for you. The pin is a " - << "so-called \"label\" pin, which is the same every " - << "time you are asked to enter it (unlike a keypad pin " - << "which changes every time you need to enter it." - << "\n\n" - << "Consider attaching a label to the bottom of your device " - << "so you can can remember the pin if you wan't to connect a new " - << "device to PiPedal. (It's also available on the Settings page of PiPedal, if you have access to PiPedal UI on another device.)" - << "\n\n" - << "For best performance, the channel number should be 1, 6, or 11 (the Wifi Direct \"social\" channels). " - << "Channel number defaults to 1." + << "If the Wi-Fi channel is not specified, a Wi-Fi channel will be automatically selected." << "\n\n" - << HangingIndent() << " --disable-p2p\tDisable Wi-Fi Direct access." + << HangingIndent() << " --disable-hotspot\tDisabled the Wi-Fi hotspot." << "\n\n" - - << HangingIndent() << " --list-p2p-channels [] \tList valid p2p channels for the current/specified country." - << "\n\n" - - // << HangingIndent() << " --enable-legacy-ap\t \tEnable a legacy Wi-Fi access point." - // << "\n\n" - // << "Enable a legacy Wi-Fi access point. \n\n" - // << "country_code is the 2-letter ISO-3166 country code for " - // << "the country you are in. see below for further notes." - // << "\n\n" - // << "See below for an explanation of when you might want to use a legacy Wi-Fi access point instead of Wifi-Direct access." - // << "an explanation of when you might want to use a legacy Access Point instead of " - // << "a P2P (Wi-Fi Direct) connection. Generally, you should prefer a P2p connection " - // << "to an ordinary Hotspot connection." - // << "\n\n" - - << HangingIndent() << " --disable-legacy-ap\tDisabled the legacy Wi-Fi access point." + << HangingIndent() << " --list-wifi-channels [] \tList valid Wifi channels for the current/specified country." << "\n\n" << Indent(0) << "Country codes:" @@ -1244,28 +1230,11 @@ static void PrintHelp() << "with reduced amplitude and feature sets." << "\n\n" << "For the most part, Wi-Fi country codes are taken from the list of ISO 3661 " - << "2-letter country codes; although there are a handful of exceptions for small " + << "2-letter country codes; although there are a handful of exceptions for small " << "countries and islands. See the Alpha-2 code column of " << "\n\n" << Indent(8) << "https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes." - << "\n\n" - - << Indent(0) << "Legacy Wi-Fi Access Points:" - << "\n\n" - << Indent(4) - << "Some older devices may not be able to connect to PiPedal using Wi-Fi Direct connections. " - "Old Apple devices, for example, do not support Wi-Fi Direct. In theory, Wi-Fi Direct should " - "allow legacy Wi-Fi devices to connect to a Wi-Fi Direct access points as if it were an " - "ordinary Access Point. If this turns out " - "not to be the case, you can configure PiPedal to provide a legacy Wi-Fi access point instead. " - "\n\n" - "Unlike Wi-Fi Direct connections, legacy Access Points will prevent both the connecting device " - "and the PiPedal host machine from connecting to the Internet over a simultanous Wi-Fi connection Access " - "Point. On a connecting Android device, you won't be able to use the data connection either when a legacy " - "Wi-Fi connection is active." - "\n\n" - "Wi-Fi Direct connections are " - "therefore preferrable under almost all circumstances.\n\n"; + << "\n\n"; } static int ListP2PChannels(const std::vector &arguments) @@ -1315,7 +1284,7 @@ int main(int argc, char **argv) bool helpError = false; bool stop = false, start = false; bool enable = false, disable = false, restart = false; - bool enable_ap = false, disable_ap = false; + bool enable_hotspot = false, disable_hotspot = false; bool enable_p2p = false, disable_p2p = false; bool list_p2p_channels = false; bool get_current_port = false; @@ -1324,6 +1293,9 @@ int main(int argc, char **argv) bool excludeShutdownService = false; std::string prefixOption; std::string portOption; + std::string homeNetwork; + bool noEthernet = false; + bool noWifi = false; parser.AddOption("--nosudo", &nosudo); // strictly a debugging aid. Run without sudo, until we fail because of permissions. parser.AddOption("--install", &install); @@ -1337,22 +1309,17 @@ int main(int argc, char **argv) parser.AddOption("--help", &help); parser.AddOption("--prefix", &prefixOption); parser.AddOption("--port", &portOption); - // parser.AddOption("--enable-legacy-ap", &enable_ap); - parser.AddOption("--disable-ap", &disable_ap); - parser.AddOption("--enable-p2p", &enable_p2p); - parser.AddOption("--disable-p2p", &disable_p2p); - parser.AddOption("--list-p2p-channels", &list_p2p_channels); - parser.AddOption("--fix-permissions", &fix_permissions); - - parser.AddOption("--get-current-port", &get_current_port); // private. For debug use only. - - parser.AddOption("--excludeShutdownService", &excludeShutdownService); // private (unstable) option used by shutdown service. + parser.AddOption("--enable-hotspot", &enable_hotspot); + parser.AddOption("--disable-hotspot", &disable_hotspot); + parser.AddOption("--home-network",&homeNetwork); + parser.AddOption("--no-ethernet",&noEthernet); + parser.AddOption("--no-wifi",&noWifi); try { parser.Parse(argc, (const char **)argv); int actionCount = - help + get_current_port + install + uninstall + stop + start + enable + disable + enable_ap + disable_ap + restart + enable_p2p + disable_p2p + list_p2p_channels + fix_permissions; + help + get_current_port + install + uninstall + stop + start + enable + disable + enable_hotspot + disable_hotspot + restart + enable_p2p + disable_p2p + list_p2p_channels + fix_permissions; if (actionCount > 1) { throw std::runtime_error("Please provide only one action."); @@ -1366,7 +1333,7 @@ int main(int argc, char **argv) throw std::runtime_error("No action provided."); } - if ((!enable_p2p) && (!enable_ap) && (!list_p2p_channels)) + if ((!enable_p2p) && (!enable_hotspot) && (!list_p2p_channels)) { if (parser.Arguments().size() != 0) { @@ -1374,6 +1341,23 @@ int main(int argc, char **argv) helpError = true; } } + int hotspotOptionCount = + (homeNetwork.length() != 0 ? 1: 0) + + noEthernet + + noWifi; + + if (enable_hotspot) + { + if (hotspotOptionCount > 1) + { + throw std::runtime_error("Only one hotspot option at a time can be specified."); + } + } else { + if (hotspotOptionCount > 0) + { + throw std::runtime_error("Hotspot options only only valid when the --enable-hotspot option has been supplied."); + } + } } catch (const std::exception &e) { @@ -1383,7 +1367,7 @@ int main(int argc, char **argv) } if (helpError) { - cout << "\n"; + cout << endl; return EXIT_FAILURE; // don't scroll the error off the screen. } @@ -1483,21 +1467,23 @@ int main(int argc, char **argv) } else if (enable_p2p) { - try - { - auto argv = parser.Arguments(); - WifiDirectConfigSettings settings; - settings.ParseArguments(argv); - settings.valid_ = true; - settings.enable_ = true; - SetWifiDirectConfig(settings); - RestartService(true); // also have to retart web service so that it gets the correct device name. - } - catch (const std::exception &e) - { - cout << "ERROR: " << e.what() << endl; - return EXIT_FAILURE; - } + cout << "ERROR: Wi-Fi p2p connections are no longer supported. Use hotspots instead." << endl; + return EXIT_FAILURE; + // try + // { + // auto argv = parser.Arguments(); + // WifiDirectConfigSettings settings; + // settings.ParseArguments(argv); + // settings.valid_ = true; + // settings.enable_ = true; + // SetWifiDirectConfig(settings); + // RestartService(true); // also have to retart web service so that it gets the correct device name. + // } + // catch (const std::exception &e) + // { + // cout << "ERROR: " << e.what() << endl; + // return EXIT_FAILURE; + // } } else if (disable_p2p) { @@ -1508,20 +1494,45 @@ int main(int argc, char **argv) RestartService(true); return EXIT_SUCCESS; } - else if (enable_ap) + else if (enable_hotspot) { auto argv = parser.Arguments(); WifiConfigSettings settings; - settings.ParseArguments(argv); + HotspotAutoStartMode startMode = HotspotAutoStartMode::Always; + if (homeNetwork.length() != 0) + { + startMode = HotspotAutoStartMode::NotAtHome; + } else if (noEthernet) + { + startMode = HotspotAutoStartMode::NoEthernetConnection; + } else if (noWifi) + { + startMode = HotspotAutoStartMode::NoRememberedWifiConections; + } + + settings.ParseArguments(argv,startMode,homeNetwork); + + if (settings.hasPassword_) + { + settings.hasSavedPassword_ = true; + } SetWifiConfig(settings); + if (silentSysExec(SYSTEMCTL_BIN " restart " PIPEDALD_SERVICE ".service") != EXIT_SUCCESS) + { + throw std::runtime_error("Failed to restart the " PIPEDALD_SERVICE " service."); + } } - else if (disable_ap) + else if (disable_hotspot) { WifiConfigSettings settings; settings.valid_ = true; - settings.enable_ = false; + settings.autoStartMode_ = (uint16_t)HotspotAutoStartMode::Never; SetWifiConfig(settings); + if (silentSysExec(SYSTEMCTL_BIN " restart " PIPEDALD_SERVICE ".service") != EXIT_SUCCESS) + { + throw std::runtime_error("Failed to restart the " PIPEDALD_SERVICE " service."); + } } } catch (const std::exception &e) diff --git a/src/HotspotManager.cpp b/src/HotspotManager.cpp index 22dcaa5..2783ec7 100644 --- a/src/HotspotManager.cpp +++ b/src/HotspotManager.cpp @@ -29,6 +29,7 @@ #include "ServiceConfiguration.hpp" #include #include +#include using namespace pipedal; using namespace dbus::networkmanager; @@ -51,6 +52,8 @@ namespace pipedal::impl virtual void Reload() override; virtual void Close() override; + std::vector GetKnownWifiNetworks(); + virtual PostHandle Post(PostCallback&&fn) override; virtual PostHandle PostDelayed(const clock::duration&delay,PostCallback&&fn) override; virtual bool CancelPost(PostHandle handle) override; @@ -97,8 +100,14 @@ namespace pipedal::impl void ReleaseNetworkManager(); void OnEthernetStateChanged(uint32_t state); void OnWlanStateChanged(uint32_t state); - std::vector GetAutoConnectSsids(); - std::vector GetKnownVisibleAccessPoints(); + std::vector GetAllAccessPoints(); + std::vector GetAllAutoConnectSsids(); + std::vector GetKnownVisibleAccessPoints(const std::vector &allAccessPoints); + + void UpdateKnownNetworks( + std::vector &knownSsids, + std::vector &allAccessPoints + ); void FireNetworkChanging(); @@ -126,9 +135,11 @@ namespace pipedal::impl std::recursive_mutex networkChangingListenerMutex; NetworkChangingListener networkChangingListener; + std::mutex knownWifiNetworksMutex; + std::vector knownWifiNetworks; + bool ethernetConnected = true; bool wlanConnected = true; - bool isAutoWlanConnectionVisible = true; DBusEventHandle onDeviceAddedHandle = INVALID_DBUS_EVENT_HANDLE; DBusEventHandle onDeviceRemovedHandle = INVALID_DBUS_EVENT_HANDLE; @@ -190,7 +201,7 @@ void HotspotManagerImpl::onInitialize() { wifiConfigSettings.Load(); - if (wifiConfigSettings.valid_ && wifiConfigSettings.enable_) + if (wifiConfigSettings.valid_ && wifiConfigSettings.IsEnabled()) { onStartMonitoring(); } @@ -445,7 +456,7 @@ void HotspotManagerImpl::onReload() // ignore. return; case State::Disabled: - if (wifiConfigSettings.valid_ && wifiConfigSettings.enable_) + if (wifiConfigSettings.valid_ && wifiConfigSettings.IsEnabled()) { onStartMonitoring(); } @@ -487,6 +498,8 @@ void HotspotManagerImpl::Close() } } + + HotspotManager::ptr HotspotManager::Create() { return std::make_unique(); @@ -508,37 +521,134 @@ public: } }; -static std::string ssidToString(const std::vector &ssid) + +struct NetworkSortRecord { + std::string ssid; + bool connected = false; + bool knownNetwork = false; + bool visibleNetwork = false; + uint8_t strength; +}; +void HotspotManagerImpl::UpdateKnownNetworks( + std::vector &knownSsids, + std::vector & allAccessPoints +) { - std::stringstream s; - for (auto v: ssid) + std::map map; + + for (auto &knownSsid: knownSsids) { - if (v == 0) break; // breaks for some arguably legal ssids, but I can live with that. - s << (char)v; + std::string ssid = ssidToString(knownSsid); + if (ssid.length() != 0) + { + NetworkSortRecord &record = map[ssid]; + record.ssid = ssid; + record.knownNetwork = true; + } } - return s.str(); + for (auto&accessPoint: allAccessPoints) + { + uint8_t strength = accessPoint->Strength(); + auto vSsid = accessPoint->Ssid(); + std::string ssid = ssidToString(vSsid); + if (ssid.length() != 0) + { + NetworkSortRecord&record = map[ssid]; + record.ssid = ssid; + record.visibleNetwork = true; + record.strength = strength; + } + } + if (this->wlanDevice) + { + auto activeConnectionPath = this->wlanDevice->ActiveConnection(); + if (activeConnectionPath.length() > 2) // "/" -> no connection. Be paranoid about "". + { + auto activeConnection = ActiveConnection::Create(dbusDispatcher,activeConnectionPath); + std::string activeSsid = activeConnection->Id(); + auto it = map.find(activeSsid); + if (it != map.end()) + { + it->second.connected = true; + } + + } + } + std::vector records; + records.reserve(map.size()); + for (auto & mapEntry: map) + { + records.push_back(mapEntry.second); + } + std::sort(records.begin(),records.end(), [](const NetworkSortRecord&left, const NetworkSortRecord &right) + { + if (left.connected != right.connected) + { + return left.connected > right.connected; + } + if (left.knownNetwork != right.knownNetwork) + { + return left.knownNetwork > right.knownNetwork; + } + if (left.visibleNetwork != right.visibleNetwork) + { + return left.visibleNetwork > right.visibleNetwork; + } + if (left.strength != right.strength) + { + return left.strength > right.strength; + } + return false; + }); + if (records.size() > 10) + { + records.resize(10); + } + std::vector result; + result.reserve(records.size()); + for (auto &record: records) + { + result.push_back(std::move(record.ssid)); + } + { + std::lock_guard lock { this->knownWifiNetworksMutex }; + this->knownWifiNetworks = std::move(result); + } + } -std::vector> HotspotManagerImpl::GetKnownVisibleAccessPoints() + +std::vector HotspotManagerImpl::GetAllAccessPoints() { + std::vector accessPoints; + + for (const auto &accessPointPath : wlanWirelessDevice->AccessPoints()) + { + auto accessPoint = AccessPoint::Create(dbusDispatcher, accessPointPath); + accessPoints.push_back(std::move(accessPoint)); + } + return accessPoints; + +} +std::vector> HotspotManagerImpl::GetKnownVisibleAccessPoints(const std::vector&allAccessPoints) { - std::vector> knownSsids = this->GetAutoConnectSsids(); + std::vector> knownSsids = this->GetAllAutoConnectSsids(); std::unordered_set, VectorHash> index{knownSsids.begin(), knownSsids.end()}; std::vector result; - for (const auto &accessPointPath : wlanWirelessDevice->AccessPoints()) + std::vector accessPoints; + + for (const auto &accessPoint : allAccessPoints) { - auto accessPoint = AccessPoint::Create(dbusDispatcher, accessPointPath); - auto ssid = accessPoint->Ssid(); - if (index.contains(ssid)) + if (index.contains(accessPoint)) { - result.push_back(ssid); + result.push_back(accessPoint); } } return result; } -std::vector> HotspotManagerImpl::GetAutoConnectSsids() +std::vector> HotspotManagerImpl::GetAllAutoConnectSsids() { auto availableConnections = wlanDevice->AvailableConnections(); std::vector> ssids; @@ -599,13 +709,31 @@ void HotspotManagerImpl::onAccessPointChanged() } } +static std::vector> GetAccessPointSsids(std::vector&accessPoints) +{ + std::vector> result; + for (const auto&accessPoint: accessPoints) + { + try { + result.push_back(accessPoint->Ssid()); + } catch (const std::exception&ignored) + { + // race to get a disappearing ssid. Ignore the error. + } + } + return result; +} void HotspotManagerImpl::MaybeStartHotspot() { - std::vector> connectableSsids = GetKnownVisibleAccessPoints(); // all the ssids currently visible for which we have credentials. - bool wantsHotspot = this->wifiConfigSettings.WantsHotspot( - this->ethernetConnected,connectableSsids); + std::vector allAccessPoints = GetAllAccessPoints(); + std::vector> allAccessPointSsids = GetAccessPointSsids(allAccessPoints); + std::vector> connectableSsids = GetKnownVisibleAccessPoints(allAccessPointSsids); // all the ssids currently visible for which we have credentials. + + this->UpdateKnownNetworks(connectableSsids,allAccessPoints); - this->isAutoWlanConnectionVisible = connectableSsids.size() != 0; + bool wantsHotspot = this->wifiConfigSettings.WantsHotspot( + + this->ethernetConnected,connectableSsids,allAccessPointSsids); if (this->state == State::Monitoring && wantsHotspot) { @@ -849,3 +977,11 @@ void HotspotManagerImpl::FireNetworkChanging() { } } + +std::vector HotspotManagerImpl::GetKnownWifiNetworks() +{ + std::lock_guard lock(knownWifiNetworksMutex); // pushed off the service thread. + return this->knownWifiNetworks; + +} + diff --git a/src/HotspotManager.hpp b/src/HotspotManager.hpp index 5107177..cd4958e 100644 --- a/src/HotspotManager.hpp +++ b/src/HotspotManager.hpp @@ -30,7 +30,6 @@ namespace pipedal { HotspotManager(HotspotManager&&) = delete; HotspotManager & operator=(const HotspotManager&) = delete; HotspotManager & operator=(const HotspotManager&&) = delete; - protected: HotspotManager() {} // use Create(). @@ -47,6 +46,9 @@ namespace pipedal { virtual void Reload() = 0; virtual void Close() = 0; + virtual std::vector GetKnownWifiNetworks() = 0; + + using NetworkChangingListener = std::function; virtual void SetNetworkChangingListener(NetworkChangingListener &&listener) = 0; diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index cf4ad3b..75f49c7 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -91,7 +91,7 @@ PiPedalModel::PiPedalModel() OnNetworkChanging(ethernetConnected,hotspotEnabling); } ); - hotspotManager->Open(); + // don't actuall start the hotspotManager until after LV2 is initialized (in order to avoid logging oddities) } void PiPedalModel::Close() @@ -2241,6 +2241,11 @@ static bool HasAlsaDevice(const std::vector devices, const std:: return false; } +void PiPedalModel::StartHotspotMonitoring() +{ + this->hotspotManager->Open(); +} + void PiPedalModel::WaitForAudioDeviceToComeOnline() { auto serverSettings = this->GetJackServerSettings(); @@ -2324,6 +2329,16 @@ void PiPedalModel::CancelNetworkChangingTimer() } } + +std::vector PiPedalModel::GetKnownWifiNetworks() +{ + if (!this->hotspotManager) + { + return std::vector(); + } + return this->hotspotManager->GetKnownWifiNetworks(); +} + void PiPedalModel::OnNetworkChanging(bool ethernetConnected,bool hotspotConnected) { CancelNetworkChangingTimer(); diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 1b4f784..7f5ed31 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -245,6 +245,8 @@ namespace pipedal networkChangedListener = listener; } + void StartHotspotMonitoring(); + void WaitForAudioDeviceToComeOnline(); UpdateStatus GetUpdateStatus(); @@ -333,6 +335,7 @@ namespace pipedal void SetWifiConfigSettings(const WifiConfigSettings &wifiConfigSettings); WifiConfigSettings GetWifiConfigSettings(); + std::vector GetKnownWifiNetworks(); void SetWifiDirectConfigSettings(const WifiDirectConfigSettings &wifiConfigSettings); WifiDirectConfigSettings GetWifiDirectConfigSettings(); diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index bfdd22a..33d7f2e 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -1025,6 +1025,12 @@ public: std::vector devices = model.GetAlsaDevices(); this->Reply(replyTo, "getAlsaDevices", devices); } + else if (message == "getKnownWifiNetworks") + { + std::vector channels = this->model.GetKnownWifiNetworks(); + this->Reply(replyTo, "getWifiChannels", channels); + + } else if (message == "getWifiChannels") { std::string country; diff --git a/src/PluginHost.cpp b/src/PluginHost.cpp index b044fe8..f7d3f2d 100644 --- a/src/PluginHost.cpp +++ b/src/PluginHost.cpp @@ -458,7 +458,10 @@ void PluginHost::Load(const char *lv2Path) for (const std::string &s : messages) { - Lv2Log::info("lilv: " + s); + if (s.length() != 0) + { + Lv2Log::info("lilv: " + s); + } } auto collator = Locale::GetInstance()->GetCollator(); diff --git a/src/PrettyPrinter.hpp b/src/PrettyPrinter.hpp index f4edc34..284f21e 100644 --- a/src/PrettyPrinter.hpp +++ b/src/PrettyPrinter.hpp @@ -63,6 +63,11 @@ namespace pipedal this->indent = indent; return *this; } + PrettyPrinter&AddIndent(int64_t indent) + { + this->indent += indent; + return *this; + } void WriteLine() { lineBuffer.push_back('\n'); @@ -246,6 +251,15 @@ namespace pipedal return pp; }; } + pp_manip AddIndent(int n) + { + return [n] (PrettyPrinter&pp) ->PrettyPrinter& { + pp.AddIndent(n); + return pp; + }; + + } + pp_manip HangingIndent() { return [] (PrettyPrinter&pp) ->PrettyPrinter&{ diff --git a/src/Storage.cpp b/src/Storage.cpp index 0c2daa9..5d17c2c 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -961,12 +961,19 @@ bool Storage::SetWifiConfigSettings(const WifiConfigSettings &wifiConfigSettings { copyToSave.hasPassword_ = previousValue.hasPassword_; copyToSave.password_ = previousValue.hasPassword_; + copyToSave.hasSavedPassword_ = previousValue.hasPassword_; + } else { + if (copyToSave.IsEnabled()) + { + copyToSave.hasSavedPassword_ = copyToSave.hasPassword_; + } } - bool configChanged = wifiConfigSettings.ConfigurationChanged(previousValue); + bool configChanged = copyToSave.ConfigurationChanged(previousValue); copyToSave.Save(); this->wifiConfigSettings = copyToSave; this->wifiConfigSettings.hasPassword_ = false; this->wifiConfigSettings.password_ = ""; + return configChanged; } diff --git a/src/WebServer.cpp b/src/WebServer.cpp index 60e9eef..406b42a 100644 --- a/src/WebServer.cpp +++ b/src/WebServer.cpp @@ -41,6 +41,7 @@ #include "WebServer.hpp" #include "Uri.hpp" +#include "ss.hpp" #include @@ -1194,7 +1195,7 @@ namespace pipedal } catch (websocketpp::exception const &e) { - std::cout << e.what() << std::endl; + Lv2Log::error(SS("Web server: " << e.what())); } if (this->signalOnDone != -1) { diff --git a/src/hotspotManagerTestMain.cpp b/src/hotspotManagerTestMain.cpp index 8ac901d..3b1b4aa 100644 --- a/src/hotspotManagerTestMain.cpp +++ b/src/hotspotManagerTestMain.cpp @@ -29,19 +29,25 @@ int main(int argc, char**argv) while (true) { std::string line; - std::cout << "e=enable,x=disable,q=quit > " << std::endl;; + std::cout << "e=no-etthernet,a=always,n=never,q=quit > " << std::endl;; std::getline(std::cin,line); if (line == "e") { settings.Load(); - settings.enable_ = true; + settings.autoStartMode_ = (uint16_t)HotspotAutoStartMode::NoEthernetConnection; settings.Save(); hotspotManager->Reload(); - } else if (line == "x") + } else if (line == "a") { settings.Load(); - settings.enable_ = false; + settings.autoStartMode_ = (uint16_t)HotspotAutoStartMode::Always; + settings.Save(); + hotspotManager->Reload(); + } else if (line == "n") + { + settings.Load(); + settings.autoStartMode_ = (uint16_t)HotspotAutoStartMode::Never; settings.Save(); hotspotManager->Reload(); } else if (line == "q") { @@ -53,9 +59,6 @@ int main(int argc, char**argv) hotspotManager->Close(); hotspotManager = nullptr; - - - return EXIT_SUCCESS; } diff --git a/src/main.cpp b/src/main.cpp index 8108120..1e9cb82 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -269,6 +269,7 @@ int main(int argc, char *argv[]) (unsigned long)getpid()); } + model.StartHotspotMonitoring(); model.WaitForAudioDeviceToComeOnline(); diff --git a/todo.txt b/todo.txt index cef84c8..3172309 100644 --- a/todo.txt +++ b/todo.txt @@ -3,9 +3,7 @@ Installer: sudo systemctl restart polkitd.service -- Scanning. - verify that scanning doesn't cause overruns. -- UI changes. - test behaviour when Osgilliad is offline. - docs changes. - unicode commandline arguments.