diff --git a/.gitignore b/.gitignore index ace7598..808b830 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ build/ #VS Code config files. -.vscode/ +# .vscode/ Sort this out later. .gitignore-ing an already tracked file doesn't actually work. #settings Testing/ diff --git a/.gitmodules b/.gitmodules index 82971f5..b464b53 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,5 +3,5 @@ url = https://github.com/rerdavies/pipedal_p2pd [submodule "modules/websocketpp"] path = modules/websocketpp - url = https://github.com/zaphoyd/websocketpp.git + url = https://github.com/rerdavies/websocketpp.git branch = develop diff --git a/.vscode/launch.json b/.vscode/launch.json index 0ef6f21..3e7d49c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,6 +11,33 @@ "version": "0.2.0", "configurations": [ + { + "name": "Attach to Edge", + "port": 9222, + "request": "attach", + "type": "msedge", + "webRoot": "${workspaceFolder}" + }, + { + "name": "(gdb) Attach", + "type": "cppdbg", + "request": "attach", + "program": "enter program name, for example ${workspaceFolder}/a.out", + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "Set Disassembly Flavor to Intel", + "text": "-gdb-set disassembly-flavor intel", + "ignoreFailures": true + } + ] + }, + @@ -40,6 +67,32 @@ } ] }, + { + "name": "(gdb) makeRelease", + "type": "cppdbg", + "request": "launch", + // Resolved by CMake Tools: + "program": "${command:cmake.launchTargetPath}", + "args": [ ], + "stopAtEntry": false, + "cwd": "${workspaceFolder}/../..", + "environment": [ + { + "name": "PATH", + "value": "$PATH:${command:cmake.launchTargetDirectory}" + } + ], + "externalConsole": true, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + }, + { "name": "(gdb) pipedal_nm_p2pd", "type": "cppdbg", @@ -96,7 +149,13 @@ "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true + }, + { + "description": "Add alsa source directories.", + "text": "directory $cd:$cwd:/usr/src/alsa-lib-1.2.8", + "ignoreFailures": true } + ] }, @@ -179,7 +238,7 @@ // Resolved by CMake Tools: "program": "${command:cmake.launchTargetPath}", - "args": [ "Nam_Profile", "--no-profile","-w" ], + "args": [ "ToobNam_Profile", "--no-profile","-w" ], "stopAtEntry": false, "cwd": "${workspaceFolder}", @@ -289,10 +348,13 @@ // Resolved by CMake Tools: "program": "${command:cmake.launchTargetPath}", "args": [ + "--prefix", + "/usr/sbin", "--nosudo", // run without sudo (which will fail because of perms, but allow us to debug deeper into install code.) - "--prefix", "/usr", - //"--get-current-port" "--install" + + //"--get-current-port" + //"--install" //"--enable-p2p" , "CA", "PiPedalTest","12345678","14" //"--list-p2p-channels" ], diff --git a/.vscode/settings.json b/.vscode/settings.json index eb5ed79..efc34fd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -96,7 +96,8 @@ "nm-device-wifi-p2p.h": "c", "strstream": "cpp", "p2p_i.h": "c", - "p2p.h": "c" + "p2p.h": "c", + "hash_set": "cpp" }, "cSpell.words": [ "Guitarix", diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b46af9..86ef6dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,17 +1,17 @@ cmake_minimum_required(VERSION 3.16.0) project(pipedal - VERSION 1.2.47 + VERSION 1.2.51 DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi" HOMEPAGE_URL "https://rerdavies.github.io/pipedal" ) -set (DISPLAY_VERSION "PiPedal v1.2.47-Release") +set (DISPLAY_VERSION "PiPedal v1.2.51-Release") set (PACKAGE_ARCHITECTURE "arm64") set (CMAKE_INSTALL_PREFIX "/usr/") include(CTest) enable_testing() -add_subdirectory("submodules/pipedal_p2pd") +#add_subdirectory("submodules/pipedal_p2pd") add_subdirectory("PiPedalCommon") @@ -19,14 +19,14 @@ 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 @@ -50,6 +50,13 @@ install( install( FILES ${PROJECT_SOURCE_DIR}/debian/copyright DESTINATION /usr/share/doc/pipedal ) +#install ( +# DIRECTORY ${PROJECT_SOURCE_DIR}/src/polkit-1/rules/10-pipedal-networkmanager.rule DESTINATION /var/lib/polkit-1/localauthority/ +#) + +install ( + FILES ${PROJECT_SOURCE_DIR}/src/polkit-1/rules/10-pipedal-networkmanager.rules DESTINATION /etc/polkit-1/rules.d/ +) install ( DIRECTORY ${REACT_BUILD_DIRECTORY} DESTINATION /etc/pipedal/react ) @@ -75,8 +82,9 @@ install(CODE EXECUTABLES ${CMAKE_INSTALL_PREFIX}/sbin/pipedaladmind ${CMAKE_INSTALL_PREFIX}/sbin/pipedald - ${CMAKE_INSTALL_PREFIX}/sbin/pipedal_nm_p2pd - ${CMAKE_INSTALL_PREFIX}/sbin/pipedal_p2pd + ${CMAKE_INSTALL_PREFIX}/sbin/pipedal_update + #${CMAKE_INSTALL_PREFIX}/sbin/pipedal_nm_p2pd + #${CMAKE_INSTALL_PREFIX}/sbin/pipedal_p2pd ${CMAKE_INSTALL_PREFIX}/bin/pipedalconfig ) ]] @@ -84,7 +92,7 @@ install(CODE set(CPACK_GENERATOR "DEB") -set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Robin Davies") # required +set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Robin E. R. Davies") # required set(CPACK_PACKAGE_VENDOR "Robin Davies") set(CPACK_PACKAGE_DESCRIPTION "IoT guitar effect pedal for Raspberry Pi, with phone-friendly web interface.") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "IoT guitar effect pedal for Raspberry Pi") @@ -92,13 +100,13 @@ 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, authbind, gpg" ) #set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "arm64") set(CPACK_PACKAGING_INSTALL_PREFIX /usr) set(CPACK_PROJECT_NAME ${PROJECT_NAME}) set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) -set (CPACK_STRIP_FILES ON) +set (CPACK_STRIP_FILES true) set(CPACK_DEBIAN_PACKAGE_SIGN_ALGORITHM "detached") set(CPACK_DEBIAN_PACKAGE_SIGN_TYPE "origin") diff --git a/NetworkManagerP2P/src/DBusLog.cpp b/NetworkManagerP2P/src/DBusLog.cpp index 9c01c28..fb4a9f8 100644 --- a/NetworkManagerP2P/src/DBusLog.cpp +++ b/NetworkManagerP2P/src/DBusLog.cpp @@ -215,6 +215,11 @@ 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() { diff --git a/NetworkManagerP2P/src/DBusLog.hpp b/NetworkManagerP2P/src/DBusLog.hpp index bbbcb12..814b29c 100644 --- a/NetworkManagerP2P/src/DBusLog.hpp +++ b/NetworkManagerP2P/src/DBusLog.hpp @@ -42,6 +42,7 @@ void SetDBusConsoleLogger(); void AddDBusConsoleLogger(); void SetDBusSystemdLogger(); void SetDBusFileLogger(const std::filesystem::path &path); +void SetDBusLogger(std::unique_ptr&&logger); extern void LogError(const std::string&path,const char*method,const std::string&message); extern void LogInfo(const std::string&path,const char*method,const std::string&message); diff --git a/NetworkManagerP2P/src/NMP2pSettings.cpp b/NetworkManagerP2P/src/NMP2pSettings.cpp index 9caf9a6..3b1fb0b 100644 --- a/NetworkManagerP2P/src/NMP2pSettings.cpp +++ b/NetworkManagerP2P/src/NMP2pSettings.cpp @@ -193,7 +193,7 @@ void P2pSettings::Load() } -static void openWithPerms( +static void openWithRestrictedPerms( pipedal::ofstream_synced &f, const std::filesystem::path &path, std::filesystem::perms perms = @@ -201,19 +201,25 @@ static void openWithPerms( std::filesystem::perms::group_read | std::filesystem::perms::group_write) { auto directory = path.parent_path(); - std::filesystem::create_directories(directory); - // open and close to make an existing empty file. - // close it. + if (!std::filesystem::exists(directory)) { - std::ofstream f; - f.open(path); - f.close(); + std::filesystem::create_directories(directory); + // open and close to make an existing empty file. + // close it. + { + std::ofstream f; + f.open(path); + f.close(); + } + try { + // set the perms. + std::filesystem::permissions( + path, + perms, + std::filesystem::perm_options::replace); + } catch (const std::exception&) { + } } - // set the perms. - std::filesystem::permissions( - path, - perms, - std::filesystem::perm_options::replace); // open for re3al. f.open(path); @@ -223,7 +229,7 @@ void P2pSettings::Save() auto filename = config_filename(); try { pipedal::ofstream_synced f; - openWithPerms(f,filename); + openWithRestrictedPerms(f,filename); if (!f.is_open()) { diff --git a/PiPedalCommon/src/CMakeLists.txt b/PiPedalCommon/src/CMakeLists.txt index 295a63e..f95dc77 100644 --- a/PiPedalCommon/src/CMakeLists.txt +++ b/PiPedalCommon/src/CMakeLists.txt @@ -41,6 +41,34 @@ message(STATUS "NMPIPEDAL CMAKE_CXX_FLAGS: ${CMAKE_CXX_FLAGS}") # Use the sdbus-c++ target in SDBusCpp namespace add_library(PiPedalCommon STATIC + include/dbus/org.freedesktop.NetworkManager.Device.Statistics.hpp + include/dbus/org.freedesktop.NetworkManager.Settings.Connection.hpp + include/dbus/org.freedesktop.NetworkManager.IP6Config.hpp + include/dbus/org.freedesktop.NetworkManager.Connection.Active.hpp + include/dbus/org.freedesktop.NetworkManager.DnsManager.hpp + include/dbus/org.freedesktop.NetworkManager.IP4Config.hpp + include/dbus/org.freedesktop.NetworkManager.Settings.hpp + include/dbus/org.freedesktop.NetworkManager.Device.hpp + include/dbus/org.freedesktop.NetworkManager.DHCP6Config.hpp + include/dbus/org.freedesktop.NetworkManager.AccessPoint.hpp + include/dbus/org.freedesktop.NetworkManager.Device.WifiP2P.hpp + include/dbus/org.freedesktop.NetworkManager.WifiP2PPeer.hpp + include/dbus/org.freedesktop.NetworkManager.hpp + include/dbus/org.freedesktop.NetworkManager.Device.Wireless.hpp + include/dbus/org.freedesktop.NetworkManager.DHCP4Config.hpp + + NetworkManagerInterfaces.cpp + include/NetworkManagerInterfaces.hpp + Lv2Log.cpp include/Lv2Log.hpp + DBusEvent.cpp + DBusVariantHelper.cpp + DBusLog.cpp + DBusDispatcher.cpp + include/DBusDispatcher.hpp + include/DBusEvent.hpp + include/DBusLog.hpp + include/DBusVariantHelper.hpp + ofstream_synced.cpp include/ofstream_synced.hpp ConfigSerializer.cpp include/ConfigSerializer.hpp WifiRegs.cpp include/WifiRegs.hpp @@ -66,7 +94,7 @@ add_library(PiPedalCommon STATIC ss.hpp ) target_include_directories(PiPedalCommon PUBLIC "include" ${LIBNL3_INCLUDE_DIRS}) -target_link_libraries(PiPedalCommon PUBLIC ${LIBNL3_LIBRARIES} ) +target_link_libraries(PiPedalCommon PUBLIC ${LIBNL3_LIBRARIES} SDBusCpp::sdbus-c++ ) diff --git a/PiPedalCommon/src/ChannelInfo.cpp b/PiPedalCommon/src/ChannelInfo.cpp index dffbe1f..3195134 100644 --- a/PiPedalCommon/src/ChannelInfo.cpp +++ b/PiPedalCommon/src/ChannelInfo.cpp @@ -53,12 +53,12 @@ static std::vector opClasses = { * care of removing invalid channels. */ - // WiFi 5. + // Wi-Fi 5. // {WifiMode::IEEE80211A, 128, 36, 177, 4, WifiBandwidth::BW80, true}, // {WifiMode::IEEE80211A, 129, 36, 177, 4, WifiBandwidth::BW160, true}, // {WifiMode::IEEE80211A, 130, 36, 177, 4, WifiBandwidth::BW80P80, true}, - // WiFi 6. + // Wi-Fi 6. // {WifiMode::IEEE80211AX, 131, 1, 233, 4, WifiBandwidth::BW20, true}, // {WifiMode::IEEE80211AX, 132, 1, 233, 8, WifiBandwidth::BW40PLUS, true}, // {WifiMode::IEEE80211AX, 133, 1, 233, 16, WifiBandwidth::BW80, true}, diff --git a/PiPedalCommon/src/DBusDispatcher.cpp b/PiPedalCommon/src/DBusDispatcher.cpp new file mode 100644 index 0000000..234efdf --- /dev/null +++ b/PiPedalCommon/src/DBusDispatcher.cpp @@ -0,0 +1,275 @@ +#include "DBusDispatcher.hpp" +#include "DBusLog.hpp" +#include +#include +#include +#include "ss.hpp" +#include +#include +#include "util.hpp" + +DBusDispatcher::DBusDispatcher(bool systemBus) +{ + // SignalStop requires this to be true. + + if (systemBus) + { + busConnection = sdbus::createSystemBusConnection(); + } + else + { + busConnection = sdbus::createSessionBusConnection(); + } +} +DBusDispatcher::~DBusDispatcher() +{ + Stop(); + busConnection = nullptr; +} + +DBusDispatcher::PostHandle DBusDispatcher::Post(DBusDispatcher::PostCallback &&fn) +{ + auto nextHandle = NextHandle(); + { + std::lock_guard lock{postMutex}; + this->postedEvents.push_back(CallbackEntry{std::move(fn), clock::time_point::min(), nextHandle}); + } + WakeThread(); + return nextHandle; +} + +bool DBusDispatcher::CancelPost(DBusDispatcher::PostHandle handle) +{ + { + std::lock_guard lock{postMutex}; + for (auto i = postedEvents.begin(); i != postedEvents.end(); ++i) + { + if (i->handle == handle) + { + postedEvents.erase(i); + return true; + } + } + return false; + } +} + +DBusDispatcher::PostHandle DBusDispatcher::PostDelayed(const clock::duration &delay, PostCallback &&fn) +{ + PostHandle nextHandle = NextHandle(); + { + std::lock_guard lock{postMutex}; + + this->postedEvents.emplace_back( + CallbackEntry{ + std::move(fn), + clock::now() + delay, + nextHandle}); + } + WakeThread(); + return nextHandle; +} + +void DBusDispatcher::Run() +{ + this->eventFd = eventfd(0, 0); + this->serviceThread = std::thread( + [this]() + { this->ThreadProc(); }); +} + +void DBusDispatcher::WakeThread() +{ + uint64_t val = 1; + if (eventFd != -1) + { + std::ignore = write(eventFd, &val, sizeof(val)); + } +} +void DBusDispatcher::ThreadProc() +{ + pipedal::SetThreadName("dbusd"); + try + { + + while (true) + { + bool idle; + bool quitting; + while (true) + { + idle = true; + quitting = false; + + while (busConnection->processPendingRequest()) + { + idle = false; + } + { + // process one event. + PostCallback callback; + auto now = clock::now(); + { + std::lock_guard lock{postMutex}; + for (auto iter = postedEvents.begin(); iter != postedEvents.end(); ++iter) + { + if (now >= iter->time) + { + callback = std::move(iter->callback); + postedEvents.erase(iter); + idle = false; + break; + } + } + quitting = this->stopping; // transfer with the mutex held. + } + if (callback) + { + callback(); + } + } + if (idle) + { + break; + } + } + if (idle && signalStopRequested) + { + signalStopRequested = false; + if (!signalStopExecuted) // only ever do this once. + { + signalStopExecuted = true; + std::atomic_thread_fence(std::memory_order::acquire); + signalStopCallback(); + signalStopCallback = std::function(); + this->stopping = true; + idle = false; + } + } + if (idle) + { + if (quitting) + { + if (postedEvents.size() == 0) + { + break; + } + } + sdbus::IConnection::PollData pollData = busConnection->getEventLoopPollData(); + + struct pollfd pollFds[2]; + + pollFds[0].fd = pollData.fd; + pollFds[0].events = pollData.events; + pollFds[0].revents = 0; + pollFds[1].fd = this->eventFd; + pollFds[1].events = POLLIN; + pollFds[1].revents = 0; + int pollTimeout = pollData.getPollTimeout(); + int eventTimeout = GetEventTimeoutMs(); + if (eventTimeout != -1 && eventTimeout < pollTimeout) + { + pollTimeout = eventTimeout;/*+-*/ + } + if (pollTimeout > 250 || pollTimeout == -1) + { + pollTimeout = 250; // poll for quit every 250 ms. + } + if (pollTimeout != 0) + { + poll(pollFds, 2, pollTimeout); + + if (pollFds[1].revents & POLLIN) // received a wakup event? + { + uint64_t counter; + std::ignore = read(pollFds[1].fd, &counter, sizeof(counter)); // reset the wakeup event. + } + } + } + } + } + catch (const std::exception &e) + { + std::string msg = SS("Abnormal termination. " << e.what()); + LogError("DBusDispatcher", "ThreadProc", msg); + } + isFinished = true; + LogTrace("DBusDispatcher", "ThreadProc", "Service thread terminated."); +} + +void DBusDispatcher::Wait() +{ + try + { + if (!threadJoined) + { + threadJoined = true; + serviceThread.join(); + } + } + catch (const std::exception &) + { + } + if (eventFd != -1) + { + close(eventFd); + eventFd = -1; + } +} + +bool DBusDispatcher::IsFinished() +{ + return isFinished; +} +void DBusDispatcher::Stop() +{ + if (!closed) + { + closed = true; + + this->stopping = true; + WakeThread(); + Wait(); + } +} + +void DBusDispatcher::SignalStop(std::function &&callback) +{ + this->signalStopCallback = std::move(callback); + std::atomic_thread_fence(::std::memory_order::release); + this->signalStopRequested = true; +} + +int DBusDispatcher::GetEventTimeoutMs() +{ + std::lock_guard lock{postMutex}; + + clock::time_point nextTime = clock::time_point::max(); + for (const auto &event : this->postedEvents) + { + if (event.time == clock::time_point::min()) + return 0; + if (event.time < nextTime) + { + nextTime = event.time; + } + } + if (nextTime == clock::time_point::max()) + { + return -1; + } + auto clockDuration = nextTime - clock::now(); + auto durationMs = std::chrono::duration_cast(clockDuration); + auto ticks = durationMs.count(); + if (ticks > 250) + return 250; + if (ticks < 0) + return 0; + return (int)ticks; +} + +bool DBusDispatcher::IsStopping() +{ + std::lock_guard lock{postMutex}; + return stopping; +} diff --git a/PiPedalCommon/src/DBusEvent.cpp b/PiPedalCommon/src/DBusEvent.cpp new file mode 100644 index 0000000..a06f285 --- /dev/null +++ b/PiPedalCommon/src/DBusEvent.cpp @@ -0,0 +1,7 @@ +#include "DBusEvent.hpp" + + + +namespace impl { + std::atomic NextHandle; +} \ No newline at end of file diff --git a/PiPedalCommon/src/DBusLog.cpp b/PiPedalCommon/src/DBusLog.cpp new file mode 100644 index 0000000..46f04ba --- /dev/null +++ b/PiPedalCommon/src/DBusLog.cpp @@ -0,0 +1,253 @@ +#include "DBusLog.hpp" +#include +#include "ss.hpp" +#include +#include +#include +#include "ss.hpp" +#include +#include "ofstream_synced.hpp" + + +namespace impl { + std::mutex logMutex; + DBusLogLevel dbusLogLevel = DBusLogLevel::Info; + +} + +static std::string getDateTime() +{ + auto now = std::chrono::system_clock::now(); + time_t in_time_t = std::chrono::system_clock::to_time_t(now); + auto ms = std::chrono::duration_cast(now.time_since_epoch()) % 1000; + std::stringstream ss; + ss << std::put_time(std::localtime(&in_time_t),"%Y-%m-%d %H:%M:%S"); + ss << '.' << std::setfill('0') << std::setw(3) << ms.count(); + ss << " "; + return ss.str(); +} + +class ConsoleDBusLogger : public IDBusLogger { + +public: + ConsoleDBusLogger(bool showTime = true) + :showTime(showTime) + { + + } + void LogError(const std::string&message) { + std::cout << getDateTime() << "error: " << message << std::endl; + } + void LogWarning(const std::string&message){ + std::cout << getDateTime() << "warning: " << message << std::endl; + } + void LogInfo(const std::string&message) { + std::cout << getDateTime() << "info: " << message << std::endl; + } + void LogDebug(const std::string&message) { + std::cout << getDateTime() << "debug: " << message << std::endl; + } + void LogTrace(const std::string&message) { + std::cout << getDateTime() << " trace: " << message << std::endl; + } +private: + + std::string getDateTime() { + return (showTime) ? getDateTime(): ""; + } + bool showTime; +}; +class FileDBusLogger : public IDBusLogger { + +public: + FileDBusLogger(const std::filesystem::path&path) + { + f.open(path); + if (!f) + { + throw std::runtime_error(SS("Can't open file " << path)); + } + } + void LogError(const std::string&message) { + f << "error: " << message << std::endl; + } + void LogWarning(const std::string&message){ + f << "warning: " << message << std::endl; + } + void LogInfo(const std::string&message) { + f << "info: " << message << std::endl; + } + void LogDebug(const std::string&message) { + f << "debug: " << message << std::endl; + } + void LogTrace(const std::string&message) { + f << "trace: " << message << std::endl; + } +private: + pipedal::ofstream_synced f; +}; + +class SystemdDBusLogger : public IDBusLogger { + +public: + void LogError(const std::string&message) { + sd_journal_print(LOG_ERR,"%s",message.c_str()); + + } + void LogWarning(const std::string&message){ + sd_journal_print(LOG_WARNING,"%s",message.c_str()); + } + void LogInfo(const std::string&message) { + sd_journal_print(LOG_INFO,"%s",message.c_str()); + + } + void LogDebug(const std::string&message) { + sd_journal_print(LOG_DEBUG,"%s",message.c_str()); + } + void LogTrace(const std::string&message) { + // no trace level. just log as debug. + sd_journal_print(LOG_DEBUG,"%s",message.c_str()); + } + +}; + + + + +static std::vector> loggers; + + +void SetDBusLogLevel(DBusLogLevel level) +{ + impl::dbusLogLevel = level; +} + +void LogError(const std::string&path,const char*method,const std::string&message) +{ + if (impl::dbusLogLevel > DBusLogLevel::Error) return; + std::lock_guard lock{impl::logMutex}; + for (auto &logger: loggers) + { + logger->LogError(SS(path<< "::" << method << ": "<< message)); + } +} +void LogInfo(const std::string&path,const char*method,const std::string&message) +{ + if (impl::dbusLogLevel > DBusLogLevel::Info) return; + std::lock_guard lock{impl::logMutex}; + for (auto &logger: loggers) + { + logger->LogInfo(SS(path<< "::" << method<< ": " << message)); + } +} + +void LogDebug(const std::string&path,const char*method,const std::string&message) +{ + if (impl::dbusLogLevel > DBusLogLevel::Debug) return; + std::lock_guard lock{impl::logMutex}; + + for (auto &logger: loggers) + { + logger->LogDebug(SS(path<< "::" << method << ": "<< message)); + } +} + +void LogDebug(const std::string&path,const char*method,const std::function &text) +{ + if (impl::dbusLogLevel > DBusLogLevel::Debug) return; + std::lock_guard lock{impl::logMutex}; + + for (auto &logger: loggers) + { + logger->LogDebug(SS(path<< "::" << method << ": " << text())); + } +} + +void LogTrace(const std::string&path,const char*method,const std::function &text) +{ + if (impl::dbusLogLevel > DBusLogLevel::Trace) return; + std::lock_guard lock{impl::logMutex}; + for (auto &logger: loggers) + { + logger->LogTrace(SS(path<< "::" << method << ": " << text())); + } +} +void LogWarning(const std::string&path,const char*method,const std::function &text) +{ + if (impl::dbusLogLevel > DBusLogLevel::Warning) return; + std::lock_guard lock{impl::logMutex}; + std::string message = text(); + for (auto &logger: loggers) + { + logger->LogWarning(SS(path<< "::" << method << ": " << message)); + } +} +void LogInfo(const std::string&path,const char*method,const std::function &text) +{ + if (impl::dbusLogLevel > DBusLogLevel::Info) return; + std::lock_guard lock{impl::logMutex}; + std::string message = text();; + for (auto &logger: loggers) + { + logger->LogInfo(SS(path<< "::" << method << ": " << message)); + } +} +void LogError(const std::string&path,const char*method,const std::function &text) +{ + if (impl::dbusLogLevel > DBusLogLevel::Error) return; + std::lock_guard lock{impl::logMutex}; + for (auto &logger: loggers) + { + logger->LogError(SS(path<< "::" << method << ": " << text())); + } +} + + +void LogWarning(const std::string&path,const char*method,const std::string&message) +{ + if (impl::dbusLogLevel > DBusLogLevel::Warning) return; + std::lock_guard lock{impl::logMutex}; + for (auto &logger: loggers) + { + logger->LogWarning(SS(path<< "::" << method << ": " << message)); + } +} + + +void LogTrace(const std::string&path,const char*method,const std::string&message) +{ + if (impl::dbusLogLevel > DBusLogLevel::Trace) return; + + std::lock_guard lock{impl::logMutex}; + for (auto &logger: loggers) + { + logger->LogTrace(SS(path<< "::" << method << ": " << message)); + } +} + +void SetDBusLogger(std::unique_ptr && logger) +{ + loggers.clear(); + loggers.push_back(std::move(logger)); +} + + + +void SetDBusConsoleLogger() +{ + SetDBusLogger(std::make_unique()); +} +void AddDBusConsoleLogger() +{ + loggers.push_back(std::make_unique()); +} +void SetDBusSystemdLogger() +{ + SetDBusLogger(std::make_unique()); + +} +void SetDBusFileLogger(const std::filesystem::path &path) +{ + SetDBusLogger(std::make_unique(path)); +} + diff --git a/PiPedalCommon/src/DBusVariantHelper.cpp b/PiPedalCommon/src/DBusVariantHelper.cpp new file mode 100644 index 0000000..4cea124 --- /dev/null +++ b/PiPedalCommon/src/DBusVariantHelper.cpp @@ -0,0 +1,175 @@ +#include "DBusVariantHelper.hpp" +#include +#include + + +//std::ostream &operator<<(std::ostream &s, const std::map &properties) +std::ostream&operator<<(std::ostream&s, const sdbus::Variant&v) +{ + // clang-format off +#define HANDLE_VARIANT_TYPE(type) \ + if (v.containsValueOfType()) \ + { \ + s << v.get(); \ + } + + HANDLE_VARIANT_TYPE(std::string) + else if (v.containsValueOfType()) + { + uint8_t value = v.get(); + s << (int)value; + } + else HANDLE_VARIANT_TYPE(uint32_t) + else HANDLE_VARIANT_TYPE(int32_t) + else HANDLE_VARIANT_TYPE(uint64_t) + else HANDLE_VARIANT_TYPE(int64_t) + else HANDLE_VARIANT_TYPE(uint16_t) + else HANDLE_VARIANT_TYPE(int16_t) + else HANDLE_VARIANT_TYPE(bool) + else HANDLE_VARIANT_TYPE(sdbus::Variant) + else if (v.containsValueOfType()) + { + auto objectPath = v.get(); + s << '[' << objectPath.c_str() << ']'; + } + else if (v.containsValueOfType>()) + { + auto values = v.get>(); + { + s << '['; + bool first = true; + for (const sdbus::ObjectPath&value: values) + { + if (!first) s << ", "; + first = false; + s << value.c_str(); + } + s << ']'; + + } + + } + else if (v.containsValueOfType>()) + { + auto paths = v.get >(); + { + s << '['; + bool first = true; + for (const sdbus::ObjectPath&path: paths) + { + if (!first) s << ", "; + first = false; + s << path.c_str(); + } + s << ']'; + + } + } + else if (v.containsValueOfType>()) + { + std::vector bytes = v.get>(); + s << '['; + bool first = true; + for (uint8_t byte: bytes) + { + if (!first) s << ", "; + first = false; + s << (int)byte; + + } + s << ']'; + + } + else if (v.containsValueOfType>>()) + { + auto arrayOfArrays = v.get>>(); + s << '['; + bool first = true; + for (const auto& array: arrayOfArrays) + { + if (!first) s << ", "; + first = false; + bool first2 = true; + for (auto byte: array) + { + if (!first2) s << ", "; + first2 = false; + s << byte; + + } + } + s << ']'; + + } + else if (v.containsValueOfType>()) + { + std::map map = v.get>(); + s << "{"; + bool first = true; + for (const auto &[key, value]: map) + { + if (!first) s << ","; + first = false; + s << '\"' << key << "\": " << value; + } + s << "}"; + } else if (v.containsValueOfType>>()) + { + std::map> values + = v.get>>(); + s << '{'; + bool first = true; + for (const auto &[key,bytes]: values) + { + if (!first) s << ','; + first = false; + s << "{\"" << key << "\": ["; + bool first2 = true; + for (auto b : bytes) + { + if (!first2) s << ','; + first2 = false; + s << b; + } + s << "]"; + } + s << '}'; + + } + + // else HANDLE_VARIANT_TYPE(float) + // else HANDLE_VARIANT_TYPE(double) + // else HANDLE_VARIANT_TYPE(uint64_t) + // else HANDLE_VARIANT_TYPE(int64_t) + // else HANDLE_VARIANT_TYPE(char) + // else HANDLE_VARIANT_TYPE(uint8_t) + // else HANDLE_VARIANT_TYPE(int8_t) + else + { + s << "UNKNOWN VALUE OF TYPE " << v.peekValueType(); + } +#undef HANDLE_VARIANT_TYPE + // clang-format on + return s; +} +std::ostream&operator<<(std::ostream&s,const std::map& properties) +{ + + s << "{"; + bool first = true; + for (const auto &property : properties) + { + if (!first) + { + s << ", "; + } + first = false; + s << '"' << property.first << "\": "; + + const sdbus::Variant &v = property.second; + s << v; + } + + s << "}"; + return s; +} diff --git a/src/Lv2Log.cpp b/PiPedalCommon/src/Lv2Log.cpp similarity index 98% rename from src/Lv2Log.cpp rename to PiPedalCommon/src/Lv2Log.cpp index 7e189be..7b43bec 100644 --- a/src/Lv2Log.cpp +++ b/PiPedalCommon/src/Lv2Log.cpp @@ -17,7 +17,8 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -#include "pch.h" +#include +#include #include "Lv2Log.hpp" #include #include diff --git a/PiPedalCommon/src/NetworkManagerInterfaces.cpp b/PiPedalCommon/src/NetworkManagerInterfaces.cpp new file mode 100644 index 0000000..ff94fda --- /dev/null +++ b/PiPedalCommon/src/NetworkManagerInterfaces.cpp @@ -0,0 +1,46 @@ +#include "NetworkManagerInterfaces.hpp" +#include "ss.hpp" + +using namespace dbus::networkmanager; +using namespace dbus::networkmanager::nm_impl; + +namespace dbus::networkmanager::nm_impl +{ + std::string NetworkManagerStateToString(uint32_t state) + { + // STUB: Not clear which state enum we're getting. + std::stringstream s; + s << state; + return s.str(); + } + + std::string NetworkManagerDeviceStateToString(uint32_t state) + { +#define DEVICE_CASE(name, value) \ + case value: \ + return #name; + + switch (state) + { + DEVICE_CASE(NM_DEVICE_STATE_UNKNOWN, 0) + DEVICE_CASE(NM_DEVICE_STATE_UNMANAGED, 10) + DEVICE_CASE(NM_DEVICE_STATE_UNAVAILABLE, 20) + DEVICE_CASE(NM_DEVICE_STATE_DISCONNECTED, 30) + DEVICE_CASE(NM_DEVICE_STATE_PREPARE, 40) + DEVICE_CASE(NM_DEVICE_STATE_CONFIG, 50) + DEVICE_CASE(NM_DEVICE_STATE_NEED_AUTH, 60) + DEVICE_CASE(NM_DEVICE_STATE_IP_CONFIG, 70) + DEVICE_CASE(NM_DEVICE_STATE_IP_CHECK, 80) + DEVICE_CASE(NM_DEVICE_STATE_SECONDARIES, 90) + DEVICE_CASE(NM_DEVICE_STATE_ACTIVATED, 100) + DEVICE_CASE(NM_DEVICE_STATE_DEACTIVATING, 110) + DEVICE_CASE(NM_DEVICE_STATE_FAILED, 120) + default: + { + std::stringstream s; + s << state; + return s.str(); + } + } + } +} diff --git a/PiPedalCommon/src/WifiConfigSettings.cpp b/PiPedalCommon/src/WifiConfigSettings.cpp index 2ef58c5..399a6a6 100644 --- a/PiPedalCommon/src/WifiConfigSettings.cpp +++ b/PiPedalCommon/src/WifiConfigSettings.cpp @@ -21,30 +21,439 @@ #include #include "ss.hpp" #include "ChannelInfo.hpp" +#include +#include "ofstream_synced.hpp" +#include +#include +#include "SysExec.hpp" +#include "Lv2Log.hpp" +#include +#include +#include +#include "util.hpp" using namespace pipedal; using namespace std; +namespace fs = std::filesystem; -JSON_MAP_BEGIN(WifiConfigSettings) - JSON_MAP_REFERENCE(WifiConfigSettings,valid) - JSON_MAP_REFERENCE(WifiConfigSettings,wifiWarningGiven) - JSON_MAP_REFERENCE(WifiConfigSettings,rebootRequired) - JSON_MAP_REFERENCE(WifiConfigSettings,enable) - JSON_MAP_REFERENCE(WifiConfigSettings,hotspotName) - JSON_MAP_REFERENCE(WifiConfigSettings,mdnsName) - JSON_MAP_REFERENCE(WifiConfigSettings,hasPassword) - JSON_MAP_REFERENCE(WifiConfigSettings,password) - JSON_MAP_REFERENCE(WifiConfigSettings,countryCode) - JSON_MAP_REFERENCE(WifiConfigSettings,channel) -JSON_MAP_END() +static const fs::path CONFIG_PATH = "/var/pipedal/config/wifiConfig.json"; -bool WifiConfigSettings::ValidateCountryCode(const std::string&value) +WifiConfigSettings::WifiConfigSettings() { - if (value.size() < 2 || value.size() > 3) return false; - return true; + Load(); } -int32_t pipedal::ChannelToChannelNumber(const std::string&channel) +static std::string getWifiCountryCode() +{ + try + { + SysExecOutput result = sysExecForOutput("iw", "reg get"); + if (result.exitCode != EXIT_SUCCESS) + { + return ""; + } + const std::string &output = result.output; + size_t pos = output.find("country"); + if (pos != std::string::npos) + { + std::string result = output.substr(pos + 8, 2); + if (WifiConfigSettings::ValidateCountryCode(result)) + { + return result; + } + } + } + catch (const std::exception &e) + { + return ""; // unset. + } + return ""; +} + +static void setWifiCountryCode(const std::string &countryCode) +{ + if (!WifiConfigSettings::ValidateCountryCode(countryCode)) + { + throw std::runtime_error("Invalid country code."); + } + SysExecOutput result = sysExecForOutput("iw", SS("reg set " << countryCode)); + if (result.exitCode != EXIT_SUCCESS) + { + throw std::runtime_error(SS("Failed to set country code: " << result.output)); + } +} +bool WifiConfigSettings::ValidateCountryCode(const std::string &text) +{ + if (text.length() != 2) + return false; + return std::isalpha(text[0]) && std::isalpha(text[1]); +} + + +void WifiConfigSettings::Load() +{ + try + { + if (fs::exists(CONFIG_PATH)) + { + std::ifstream f(CONFIG_PATH); + if (!f) + { + throw std::runtime_error("Can't open file."); + } + json_reader reader(f); + reader.read(this); + } + this->countryCode_ = getWifiCountryCode(); + this->enable_ = this->autoStartMode_ != (uint16_t)HotspotAutoStartMode::Never; + this->mdnsName_ = GetHostName(); + } + catch (const std::exception &e) + { + throw std::runtime_error(SS("Can't load WifiConfigSettings. " << e.what())); + } +} + +static void openWithPerms( + pipedal::ofstream_synced &f, + const std::filesystem::path &path, + std::filesystem::perms perms = + std::filesystem::perms::owner_read | std::filesystem::perms::owner_write | + std::filesystem::perms::group_read | std::filesystem::perms::group_write) +{ + auto directory = path.parent_path(); + if (!std::filesystem::exists(directory)) + { + std::filesystem::create_directories(directory); + // open and close to make an existing empty file. + // close it. + { + std::ofstream f; + f.open(path); + f.close(); + } + try { + // set the perms. + std::filesystem::permissions( + path, + perms, + std::filesystem::perm_options::replace); + } catch (const std::exception&) { + Lv2Log::warning(SS("Failed to set permissions on" << path << ".")); + } + } + + // open for re3al. + f.open(path); +} + +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(&newSettings); + } + catch (const std::exception &e) + { + throw std::runtime_error(SS("Can't save WifiConfigSettings. " << e.what())); + } +} + +namespace pipedal::priv +{ + + class Device + { + public: + Device(const std::string &path) + : m_proxy(sdbus::createProxy( + sdbus::createSystemBusConnection(), + "org.freedesktop.NetworkManager", + path)) + { + } + + uint32_t GetDeviceType() const + { + sdbus::Variant device_type_variant; + m_proxy->callMethod("Get") + .onInterface("org.freedesktop.DBus.Properties") + .withArguments("org.freedesktop.NetworkManager.Device", "DeviceType") + .storeResultsTo(device_type_variant); + return device_type_variant.get(); + } + + std::string GetPath() const + { + return m_proxy->getObjectPath(); + } + + private: + std::unique_ptr m_proxy; + }; + class NetworkManager + { + public: + NetworkManager() + : m_proxy(sdbus::createProxy( + sdbus::createSystemBusConnection(), + "org.freedesktop.NetworkManager", + "/org/freedesktop/NetworkManager")), + m_settings_proxy(sdbus::createProxy( + sdbus::createSystemBusConnection(), + "org.freedesktop.NetworkManager", + "/org/freedesktop/NetworkManager/Settings")) + { + } + + struct HotspotInfo + { + std::string connection_path; + std::string active_connection_path; + }; + + std::string AddConnection(const std::map> &connection) + { + std::string path; + m_settings_proxy->callMethod("AddConnection") + .onInterface("org.freedesktop.NetworkManager.Settings") + .withArguments(connection) + .storeResultsTo(path); + return path; + } + std::string AddConnection(const std::string &ssid, const std::string &password) + { + // Create connection + std::map> connection; + connection["connection"]["type"] = sdbus::Variant("802-11-wireless"); + connection["connection"]["id"] = sdbus::Variant(ssid); + std::vector ssid_bytes(ssid.begin(), ssid.end()); + connection["802-11-wireless"]["ssid"] = sdbus::Variant(ssid_bytes); + connection["802-11-wireless"]["mode"] = sdbus::Variant(std::string("ap")); + connection["802-11-wireless-security"]["key-mgmt"] = sdbus::Variant(std::string("wpa-psk")); + connection["802-11-wireless-security"]["psk"] = sdbus::Variant(password); + connection["ipv4"]["method"] = sdbus::Variant(std::string("shared")); + connection["ipv6"]["method"] = sdbus::Variant(std::string("ignore")); + + return AddConnection(connection); + } + + std::string ActivateConnection(const std::string &connection_path, const std::string &device_path) + { + std::string active_connection_path; + m_proxy->callMethod("ActivateConnection") + .onInterface("org.freedesktop.NetworkManager") + .withArguments(connection_path, device_path, "/") + .storeResultsTo(active_connection_path); + return active_connection_path; + } + + void DeactivateConnection(const std::string &active_connection_path) + { + m_proxy->callMethod("DeactivateConnection") + .onInterface("org.freedesktop.NetworkManager") + .withArguments(active_connection_path); + } + + std::string GetWirelessDevicePath() + { + auto devices = GetAllDevices(); + for (const auto &device : devices) + { + if (device.GetDeviceType() == 2) + { // 2 is the value for Wi-Fi devices + return device.GetPath(); + } + } + throw std::runtime_error("No Wi-Fi device found"); + } + + std::string CreateHotspot(const std::string &ssid, const std::string &password) + { + // Create connection + std::map> connection; + connection["connection"]["type"] = sdbus::Variant("802-11-wireless"); + connection["connection"]["id"] = sdbus::Variant(ssid); + std::vector ssid_bytes(ssid.begin(), ssid.end()); + connection["802-11-wireless"]["ssid"] = sdbus::Variant(ssid_bytes); + connection["802-11-wireless"]["mode"] = sdbus::Variant(std::string("ap")); + connection["802-11-wireless-security"]["key-mgmt"] = sdbus::Variant(std::string("wpa-psk")); + connection["802-11-wireless-security"]["psk"] = sdbus::Variant(password); + connection["ipv4"]["method"] = sdbus::Variant(std::string("shared")); + connection["ipv6"]["method"] = sdbus::Variant(std::string("ignore")); + + auto connection_path = AddConnection(connection); + return connection_path; + } + + std::string EnableHotspot(const std::string &connection_path) + { + + // Enable hotspot + std::string device_path = GetWirelessDevicePath(); + std::string active_connection_path = ActivateConnection(connection_path, device_path); + + return active_connection_path; + } + + + void RemoveConnection(const std::string &connection) + { + auto connection_proxy = sdbus::createProxy( + m_proxy->getConnection(), + "org.freedesktop.NetworkManager", + connection); + connection_proxy->callMethod("Delete") + .onInterface("org.freedesktop.NetworkManager.Settings.Connection"); + } + + std::optional GetActiveConnection(const std::string &device_path) + { + std::vector active_connections; + m_proxy->callMethod("Get") + .onInterface("org.freedesktop.DBus.Properties") + .withArguments("org.freedesktop.NetworkManager", "ActiveConnections") + .storeResultsTo(active_connections); + + for (const auto &conn : active_connections) + { + auto conn_proxy = sdbus::createProxy( + m_proxy->getConnection(), + "org.freedesktop.NetworkManager", + conn); + + sdbus::ObjectPath conn_device; + conn_proxy->callMethod("Get") + .onInterface("org.freedesktop.DBus.Properties") + .withArguments("org.freedesktop.NetworkManager.Connection.Active", "Devices") + .storeResultsTo(conn_device); + + if (conn_device == device_path) + { + return conn; + } + } + + return std::nullopt; + } + std::string GetHotspotConnectionPath(const std::string &ssid) + { + std::vector connections; + m_settings_proxy->callMethod("ListConnections") + .onInterface("org.freedesktop.NetworkManager.Settings") + .storeResultsTo(connections); + + for (const auto &conn_path : connections) + { + auto conn_proxy = sdbus::createProxy(sdbus::createSystemBusConnection(), + "org.freedesktop.NetworkManager", + conn_path); + + sdbus::Variant settings_variant; + conn_proxy->callMethod("GetSettings") + .onInterface("org.freedesktop.NetworkManager.Settings.Connection") + .storeResultsTo(settings_variant); + + auto settings = settings_variant.get>>(); + + // Check if this is a Wi-Fi connection + if (settings["connection"]["type"].get() == "802-11-wireless") + { + // Check if it's an access point (hotspot) connection + if (settings["802-11-wireless"]["mode"].get() == "ap") + { + // Check if the SSID matches + std::vector conn_ssid = settings["802-11-wireless"]["ssid"].get>(); + std::string conn_ssid_str(conn_ssid.begin(), conn_ssid.end()); + if (conn_ssid_str == ssid) + { + return conn_path; + } + } + } + } + + return ""; // No matching hotspot connection found + } + + private: + std::vector GetAllDevices() + { + sdbus::Variant devices_variant; + m_proxy->callMethod("Get") + .onInterface("org.freedesktop.DBus.Properties") + .withArguments("org.freedesktop.NetworkManager", "AllDevices") + .storeResultsTo(devices_variant); + + std::vector device_paths = devices_variant.get>(); + std::vector devices; + for (const auto &path : device_paths) + { + devices.emplace_back(path); + } + return devices; + } + + std::unique_ptr m_proxy; + std::unique_ptr m_settings_proxy; + }; +} + + +JSON_MAP_BEGIN(WifiConfigSettings) +// v0 +JSON_MAP_REFERENCE(WifiConfigSettings, valid) +JSON_MAP_REFERENCE(WifiConfigSettings, wifiWarningGiven) +JSON_MAP_REFERENCE(WifiConfigSettings, rebootRequired) +JSON_MAP_REFERENCE(WifiConfigSettings, enable) +JSON_MAP_REFERENCE(WifiConfigSettings, hotspotName) +JSON_MAP_REFERENCE(WifiConfigSettings, mdnsName) +JSON_MAP_REFERENCE(WifiConfigSettings, hasPassword) +JSON_MAP_REFERENCE(WifiConfigSettings, password) +JSON_MAP_REFERENCE(WifiConfigSettings, countryCode) +JSON_MAP_REFERENCE(WifiConfigSettings, channel) + +// 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) +{ + std::string t = channel; + // remove deprecated band specs. + if (t.size() > 1 && t[0] == 'a' || t[0] == 'g') + { + t = t.substr(1); + } + int32_t channelNumber = 0; + std::stringstream ss(t); + ss >> channelNumber; + return channelNumber; +} + +static uint32_t ParseChannel(const std::string &channel) { std::string t = channel; // remove dprecated band specs. @@ -52,24 +461,9 @@ int32_t pipedal::ChannelToChannelNumber(const std::string&channel) { t = t.substr(1); } - int32_t channelNumber = 1; - std::stringstream ss(t); - ss >> channelNumber; - return channelNumber; - -} - -static uint32_t ParseChannel(const std::string & channel) -{ - std::string t = channel; - // remove dprecated band specs. - if (t.size() > 1 && t[0] == 'a' || t[0] == 'g') - { - t = t.substr(1); - } size_t size = t.length(); - unsigned long long lChannel = std::stoull(t,&size); + unsigned long long lChannel = std::stoull(t, &size); if (size != t.length()) { throw invalid_argument("Expecting a number: '" + t + "'."); @@ -92,7 +486,7 @@ uint32_t pipedal::ChannelToWifiFrequency(uint32_t channel) // 2.4GHz. if (channel >= 1 && channel <= 13) { - return 2412 + 5*(channel-1); + return 2412 + 5 * (channel - 1); } if (channel == 14) { @@ -101,26 +495,25 @@ uint32_t pipedal::ChannelToWifiFrequency(uint32_t channel) // 802.11y if (channel >= 131 && channel < 137) { - return 3660 + (channel-131)*5; + return 3660 + (channel - 131) * 5; } if (channel >= 32 && channel <= 68 && (channel & 1) == 0) { - return 5160 + (channel-32)/2*10; + return 5160 + (channel - 32) / 2 * 10; } - if (channel == 96) return 5480; + if (channel == 96) + return 5480; if (channel >= 100 && channel <= 196) { - return 5500 + (channel-100)/5; + return 5500 + (channel - 100) / 5; } - throw invalid_argument(SS("Invalid WiFi channel: " << channel)); - + throw invalid_argument(SS("Invalid Wi-Fi channel: " << channel)); } - -bool WifiConfigSettings::ValidateChannel(const std::string&countryCode,const std::string&value) +bool WifiConfigSettings::ValidateChannel(const std::string &countryCode, const std::string &value) { - if (value == "0") // = "Autoselect". + if (value == "0") // = "Autoselect". { return true; } @@ -134,15 +527,17 @@ bool WifiConfigSettings::ValidateChannel(const std::string&countryCode,const std { throw std::invalid_argument(SS("Invalid country code: " << countryCode)); } - auto regDom = getWifiRegClass(countryCode,ParseChannel(value),40); - if (regDom == -1) { - std::vector valid_channels = getValidChannels(countryCode,40); + auto regDom = getWifiRegClass(countryCode, ParseChannel(value), 40); + if (regDom == -1) + { + std::vector valid_channels = getValidChannels(countryCode, 40); std::stringstream ss; ss << "Channel " << value << " is not permitted in the selected country.\n Valid channels: "; bool first = true; - for (auto channel: valid_channels) + for (auto channel : valid_channels) { - if (!first) ss << ", "; + if (!first) + ss << ", "; first = false; ss << channel; } @@ -153,36 +548,280 @@ bool WifiConfigSettings::ValidateChannel(const std::string&countryCode,const std return true; } - void WifiConfigSettings::ParseArguments(const std::vector &argv) - { +static const char* trueValues[] { + "true", + "on", + "yes" + "1", + nullptr +}; +static const char* falseValues[] { + "false", + "off", + "no", + "0", + nullptr +}; + +static bool Matches(const std::string&value, const char**matches) +{ + while(*matches) + { + if (value == *matches) return true; + ++matches; + } + return false; +} + +static bool TryStringToBool(const std::string &value, bool*outputValue) +{ + if (Matches(value,trueValues)) + { + *outputValue = true; + return true; + } + if (Matches(value, falseValues)) { + *outputValue = false; + return true; + } + *outputValue = false; + return false; + +} + +static WifiConfigSettings::ssid_t readSsid(std::istream&ss) +{ + using ssid_t = WifiConfigSettings::ssid_t; + char c; + + ssid_t result; + while (ss.peek() == ' ') + { + ss >> c; + } + if (ss.peek() == '"') + { + ss >> c; + while (!ss.eof() && ss.peek() != '"') + { + ss >> c; + if (c == '\\') + { + if (ss.eof()) + { + break; + } + ss >> c; + switch (c) { + case 'n': + result.push_back((uint8_t)'\n'); + break; + case 'r': + result.push_back((uint8_t)'\r'); + break; + case 't': + result.push_back((uint8_t)'\t'); + break; + case 'b': + result.push_back((uint8_t)'\b'); + break; + default: + result.push_back((uint8_t)c); + break; + } + } else { + result.push_back((uint8_t)c); + } + } + } else { + while (!ss.eof()) + { + if (c == ':') + { + break; + } + ss >> c; + if (c == ':') + { + break; + } + result.push_back((uint8_t)c); + } + } + return result; +} +static std::vector stringToSsidArray(const std::string&value) +{ + using ssid_t = WifiConfigSettings::ssid_t; + std::istringstream ss(value); + + std::vector result; + while (true) + { + ssid_t ssid = readSsid(ss); + if (ssid.size() == 0) + { + break; + } + result.push_back(ssid); + if (ss.peek() == ':') + { + char c; + ss >> c; + } + } + return result; + +} +void WifiConfigSettings::ParseArguments( + const std::vector &argv, + HotspotAutoStartMode startMode, + const std::string homeNetworkSsid + + ) +{ this->valid_ = false; - if (argv.size() != 4) { + 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->mdnsName_ = GetHostName(); + 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->hasSavedPassword_ = oldSettings.hasSavedPassword_; + if (!ValidateCountryCode(this->countryCode_)) { throw invalid_argument("Invalid country code."); } - if (this->hotspotName_.length() > 31) 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->hotspotName_.length() > 31) + 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 (!ValidateChannel(this->countryCode_,this->channel_)) + 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."); } // validate that the channel number is supported for the given country code. - this->valid_ = true; +} + +bool WifiConfigSettings::ConfigurationChanged(const WifiConfigSettings&other) const +{ + return !( + this->valid_ == other.valid_ && + //this->rebootRequired_ == other.rebootRequired_ && + //this->enable_ == other.enable_ && + this->countryCode_ == other.countryCode_ && + this->hotspotName_ == other.hotspotName_ && + this->hasPassword_ == other.hasPassword_ && + this->password_ == other.password_ && + this->channel_ == other.channel_ && + + this->homeNetwork_ == other.homeNetwork_ && + this->autoStartMode_ == other.autoStartMode_ && + this->hasSavedPassword_ == other.hasSavedPassword_ + ); + +} +bool WifiConfigSettings::operator==(const WifiConfigSettings&other) const +{ + return !ConfigurationChanged(other) && this->wifiWarningGiven_ == other.wifiWarningGiven_; +} + +static bool CanSeeHomeNetwork(const std::string&home, const std::vector&availableNetworks) +{ + for (const auto &availableNetwork: availableNetworks) + { + if (availableNetwork == home) + { + return true; + } + } + 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) + { + 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; + } +} + +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/WifiDirectConfigSettings.cpp b/PiPedalCommon/src/WifiDirectConfigSettings.cpp index 049a8ee..5b674d2 100644 --- a/PiPedalCommon/src/WifiDirectConfigSettings.cpp +++ b/PiPedalCommon/src/WifiDirectConfigSettings.cpp @@ -123,7 +123,7 @@ void WifiDirectConfigSettings::ParseArguments(const std::vector &ar if (!WifiConfigSettings::ValidateChannel(this->countryCode_,channel_)) { - throw invalid_argument(SS("Invalid WiFi channel: " << channel_)); + throw invalid_argument(SS("Invalid Wi-Fi channel: " << channel_)); } } diff --git a/PiPedalCommon/src/include/DBusDispatcher.hpp b/PiPedalCommon/src/include/DBusDispatcher.hpp new file mode 100644 index 0000000..af4288c --- /dev/null +++ b/PiPedalCommon/src/include/DBusDispatcher.hpp @@ -0,0 +1,84 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +namespace sdbus { + class IConnection; +}; + + +class DBusDispatcher { + DBusDispatcher(const DBusDispatcher&) = delete; + DBusDispatcher(DBusDispatcher&&) = delete; + DBusDispatcher & operator=(const DBusDispatcher&) = delete; + DBusDispatcher & operator=(const DBusDispatcher&&) = delete; +public: + DBusDispatcher(bool systemBus = true); + ~DBusDispatcher(); + + void Run(); + // Stop everything, shutting down gracefully. + void Stop(); + void Wait(); + bool IsFinished(); + + // Stop, but don't wait. Suitable for use in a signal handler. + // The callback is executed by the dispatcher thread, and can therefore call non-signal-safe functions. + void SignalStop(std::function &&callback); + + bool IsStopping(); + + using PostHandle = uint64_t; + using PostCallback = std::function; + PostHandle Post(PostCallback&&fn); + + using clock = std::chrono::steady_clock; + + template + DBusDispatcher::PostHandle PostDelayed(const std::chrono::duration&delay,PostCallback&&fn) + { + return PostDelayed( + std::chrono::duration_cast(delay), + std::move(fn)); + } + + DBusDispatcher::PostHandle PostDelayed(const clock::duration&delay,PostCallback&&fn); + + bool CancelPost(DBusDispatcher::PostHandle handle); + + sdbus::IConnection&Connection() { return *busConnection;} + +private: + std::atomic isFinished; + std::atomic signalStopRequested; + bool signalStopExecuted = false; + std::function signalStopCallback; + uint64_t NextHandle() { + return nextHandle.fetch_add(1); + } + int GetEventTimeoutMs(); + std::atomic nextHandle; + int eventFd = -1; + bool closed = false; + void ThreadProc(); + void WakeThread(); + std::unique_ptr busConnection; + + struct CallbackEntry { + PostCallback callback; + clock::time_point time; + PostHandle handle; + }; + + std::atomic stopping; + + std::vector postedEvents; + bool threadJoined; + std::thread serviceThread; + std::mutex postMutex; +}; \ No newline at end of file diff --git a/PiPedalCommon/src/include/DBusEvent.hpp b/PiPedalCommon/src/include/DBusEvent.hpp new file mode 100644 index 0000000..35927eb --- /dev/null +++ b/PiPedalCommon/src/include/DBusEvent.hpp @@ -0,0 +1,62 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace impl { + extern std::atomic NextHandle; +} + +using DBusEventHandle=uint64_t; +constexpr DBusEventHandle INVALID_DBUS_EVENT_HANDLE = 0; + + +template +class DBusEvent { +public: + using Callback=std::function; + + + DBusEventHandle add(Callback&&cb) + { + auto handle = impl::NextHandle.fetch_add(1,std::memory_order::acq_rel); + { + std::lock_guard lock{m}; + callbacks[handle] = std::make_shared(std::move(cb)); + } + return handle; + } + void remove(DBusEventHandle h) + { + std::lock_guard lock{m}; + callbacks.erase(h); + } + + void fire(ARG_TYPES...args) + { + std::vector> cbs; + cbs.reserve(callbacks.size()); + { + std::lock_guard lock{m}; + for (const auto &cb: callbacks) + { + cbs.push_back(cb.second); + } + } + for (auto&cb: cbs) + { + (*cb)(args...); + } + { + // destruct shared_ptrs with mutex held. + std::lock_guard lock{m}; + cbs.clear(); + } + } +private: + std::map> callbacks; + std::mutex m; +}; diff --git a/PiPedalCommon/src/include/DBusLog.hpp b/PiPedalCommon/src/include/DBusLog.hpp new file mode 100644 index 0000000..814b29c --- /dev/null +++ b/PiPedalCommon/src/include/DBusLog.hpp @@ -0,0 +1,99 @@ +#pragma once +#include +#include +#include +#include +#include + +enum class DBusLogLevel { + Trace, + Debug, + Info, + Warning, + Error, + None +}; + +namespace impl { + extern std::mutex logMutex; + extern DBusLogLevel dbusLogLevel; +} +extern void SetDBusLogLevel(DBusLogLevel level); +inline DBusLogLevel GetDBusLogLevel() { + return impl::dbusLogLevel; +} +inline bool IsDBusLoggingEnabled(DBusLogLevel level) { + return level >= impl::dbusLogLevel; +} + +class IDBusLogger { +public: + IDBusLogger() {} + virtual ~IDBusLogger() { } + virtual void LogError(const std::string&message) = 0; + virtual void LogWarning(const std::string&message) = 0; + virtual void LogInfo(const std::string&message) = 0; + virtual void LogDebug(const std::string&message) = 0; + virtual void LogTrace(const std::string&message) = 0; + +}; + +void SetDBusConsoleLogger(); +void AddDBusConsoleLogger(); +void SetDBusSystemdLogger(); +void SetDBusFileLogger(const std::filesystem::path &path); +void SetDBusLogger(std::unique_ptr&&logger); + +extern void LogError(const std::string&path,const char*method,const std::string&message); +extern void LogInfo(const std::string&path,const char*method,const std::string&message); +extern void LogDebug(const std::string&path,const char*method,const std::string&message); +extern void LogWarning(const std::string&path,const char*method,const std::string&message); +extern void LogTrace(const std::string&path,const char*method,const std::string&message); + +extern void LogTrace(const std::string&path,const char*method,const std::function &text); +extern void LogDebug(const std::string&path,const char*method,const std::function &text); +extern void LogInfo(const std::string&path,const char*method,const std::function &text); +extern void LogWarning(const std::string&path,const char*method,const std::function &text); +extern void LogError(const std::string&path,const char*method,const std::function &text); + + + +class DBusLog { +protected: + DBusLog(const std::string &path) + :path(path) + { + } +protected: + void LogError(const char*tag,const std::string&message) + { + ::LogError(path,tag,message); + } + void LogWarning(const char *tag, const std::string&message) + { + ::LogWarning(path,tag,message); + } + void LogDebug(const char*tag, const std::string &text) + { + ::LogDebug(path,tag,text); + } + void LogDebug(const char*tag, const std::function &text) + { + if (impl::dbusLogLevel > DBusLogLevel::Debug) return; + ::LogDebug(path,tag,text()); + } + void LogTrace(const char*tag, const std::string &text) + { + ::LogTrace(path,tag,text); + } + void LogTrace(const char*tag, const std::function &text) + { + if (impl::dbusLogLevel > DBusLogLevel::Trace) return; + ::LogTrace(path,tag,text()); + } +private: + std::string path; +}; + + + diff --git a/PiPedalCommon/src/include/DBusVariantHelper.hpp b/PiPedalCommon/src/include/DBusVariantHelper.hpp new file mode 100644 index 0000000..1e71671 --- /dev/null +++ b/PiPedalCommon/src/include/DBusVariantHelper.hpp @@ -0,0 +1,11 @@ +#pragma once +#include +#include +#include + +namespace sdbus { + class Variant; +} + +extern std::ostream&operator<<(std::ostream&s, const sdbus::Variant&v); +extern std::ostream&operator<<(std::ostream&s,const std::map& properties); diff --git a/src/Lv2Log.hpp b/PiPedalCommon/src/include/Lv2Log.hpp similarity index 98% rename from src/Lv2Log.hpp rename to PiPedalCommon/src/include/Lv2Log.hpp index 721c1a5..d04c6f1 100644 --- a/src/Lv2Log.hpp +++ b/PiPedalCommon/src/include/Lv2Log.hpp @@ -150,7 +150,7 @@ namespace pipedal static void error(const char *format, ...) { - if (Lv2Log::log_level_ >= LogLevel::Warning) + if (Lv2Log::log_level_ >= LogLevel::Error) { va_list arglist; va_start(arglist, format); diff --git a/PiPedalCommon/src/include/NetworkManagerInterfaces.hpp b/PiPedalCommon/src/include/NetworkManagerInterfaces.hpp new file mode 100644 index 0000000..084ebc6 --- /dev/null +++ b/PiPedalCommon/src/include/NetworkManagerInterfaces.hpp @@ -0,0 +1,348 @@ +#pragma once + +#include "DBusEvent.hpp" +#include "DBusLog.hpp" +#include "DBusDispatcher.hpp" + +#include "dbus/org.freedesktop.NetworkManager.Device.WifiP2P.hpp" +#include "dbus/org.freedesktop.NetworkManager.Device.hpp" +#include "dbus/org.freedesktop.NetworkManager.Device.Wireless.hpp" +#include "dbus/org.freedesktop.NetworkManager.Settings.Connection.hpp" +#include "dbus/org.freedesktop.NetworkManager.WifiP2PPeer.hpp" +#include "dbus/org.freedesktop.NetworkManager.AccessPoint.hpp" +#include "dbus/org.freedesktop.NetworkManager.Connection.Active.hpp" + +#include "dbus/org.freedesktop.NetworkManager.hpp" + +#include "libnm/nm-dbus-interface.h" + +namespace dbus::networkmanager { +namespace nm_impl +{ + extern std::string NetworkManagerStateToString(uint32_t state); + std::string NetworkManagerDeviceStateToString(uint32_t state); +}; + +class NetworkManager : public sdbus::ProxyInterfaces +{ +public: + using ptr = std::unique_ptr; + NetworkManager(DBusDispatcher &dispatcher) + : sdbus::ProxyInterfaces( + dispatcher.Connection(), + INTERFACE_NAME, + "/org/freedesktop/NetworkManager") + { + registerProxy(); + } + virtual ~NetworkManager() + { + unregisterProxy(); + } + + static ptr Create(DBusDispatcher &dispatcher) { return std::make_unique(dispatcher); } + DBusEvent<> OnCheckPermissions; + DBusEvent OnStateChanged; + DBusEvent OnDeviceAdded; + DBusEvent OnDeviceRemoved; + +private: + void EventTrace(const char *method, const std::string &message) + { + LogTrace(getObjectPath(), method, message); + } + virtual void onStateChanged(const uint32_t &state) + { + EventTrace("onStateChanged", nm_impl::NetworkManagerStateToString(state)); + OnStateChanged.fire(state); + } + + virtual void onCheckPermissions() { OnCheckPermissions.fire(); } + + virtual void onDeviceAdded(const sdbus::ObjectPath &device_path) + { + EventTrace("onDeviceAdded", device_path.c_str()); + OnDeviceAdded.fire(device_path); + } + virtual void onDeviceRemoved(const sdbus::ObjectPath &device_path) + { + EventTrace("onDeviceRemoved", device_path.c_str()); + + OnDeviceRemoved.fire(device_path); + } +}; + +class Device : public sdbus::ProxyInterfaces +{ +public: + using ptr = std::unique_ptr; + Device(DBusDispatcher &dispatcher, const sdbus::ObjectPath &path) + : sdbus::ProxyInterfaces( + dispatcher.Connection(), + "org.freedesktop.NetworkManager", + path) + { + registerProxy(); + } + virtual ~Device() + { + unregisterProxy(); + } + + static ptr Create(DBusDispatcher &dispatcher, const sdbus::ObjectPath &path) + { + return std::make_unique(dispatcher, path); + } + DBusEvent OnStateChanged; + +private: + const uint32_t NM_DEVICE_TYPE_WIFI_P2P = 30; // from lbnm-dev package. +public: + bool IsP2pDevice() + { + return this->DeviceType() == NM_DEVICE_TYPE_WIFI_P2P; + } + +private: + void EventTrace(const char *method, const std::string &message) + { + LogTrace(getObjectPath(), method, message); + } + + virtual void onStateChanged(const uint32_t &new_state, const uint32_t &old_state, const uint32_t &reason) + { + EventTrace("onStateChanged", nm_impl::NetworkManagerDeviceStateToString(new_state)); + + OnStateChanged.fire(new_state, old_state, reason); + } +}; +class WifiP2P : public sdbus::ProxyInterfaces< + org::freedesktop::NetworkManager::Device::WifiP2P_proxy> +{ +public: + using ptr = std::unique_ptr; + using base = sdbus::ProxyInterfaces< + org::freedesktop::NetworkManager::Device::WifiP2P_proxy>; + + WifiP2P(DBusDispatcher &dispatcher, const std::string &objectPath) + : base(dispatcher.Connection(), "org.freedesktop.NetworkManager", objectPath) + { + registerProxy(); + } + virtual ~WifiP2P() + { + unregisterProxy(); + } + static ptr Create(DBusDispatcher &dispatcher, const std::string &objectPath) + { + return std::make_unique(dispatcher, objectPath); + } + DBusEvent OnPeerAdded; + DBusEvent OnPeerRemoved; + DBusEvent OnStateChanged; + +private: + void EventTrace(const char *method, const std::string &message) + { + LogTrace(getObjectPath(), method, message); + } + virtual void onStateChanged(const uint32_t &new_state, const uint32_t &old_state, const uint32_t &reason) + { + EventTrace("onStateChanged", nm_impl::NetworkManagerStateToString(new_state)); + + OnStateChanged.fire(new_state); + } + + virtual void onPeerAdded(const sdbus::ObjectPath &peer) + { + EventTrace("onPeerAdded", peer); + OnPeerAdded.fire(peer); + } + virtual void onPeerRemoved(const sdbus::ObjectPath &peer) + { + EventTrace("onPeerRemoved", peer); + OnPeerRemoved.fire(peer); + } +}; + +class Connection : public sdbus::ProxyInterfaces +{ +public: + using self = Connection; + using ptr = std::unique_ptr; + + Connection(DBusDispatcher &dispatcher, const sdbus::ObjectPath &objectPath) + : sdbus::ProxyInterfaces( + dispatcher.Connection(), "org.freedesktop.NetworkManager", objectPath) + { + registerProxy(); + } + + static ptr Create(DBusDispatcher &dispatcher, const sdbus::ObjectPath &objectPath) + { + return std::make_unique(dispatcher, objectPath); + } + + virtual ~Connection() + { + unregisterProxy(); + } + DBusEvent<> OnUpdated; + DBusEvent<> OnRemoved; + +private: + virtual void onUpdated() override + { + OnUpdated.fire(); + } + virtual void onRemoved() override + { + OnRemoved.fire(); + } +}; +class WifiP2PPeer : public sdbus::ProxyInterfaces +{ +public: + using self = WifiP2PPeer; + using ptr = std::unique_ptr; + + WifiP2PPeer(DBusDispatcher &dispatcher, const sdbus::ObjectPath &objectPath) + : sdbus::ProxyInterfaces( + dispatcher.Connection(), "org.freedesktop.NetworkManager", objectPath) + { + registerProxy(); + } + virtual ~WifiP2PPeer() + { + unregisterProxy(); + } + static ptr Create(DBusDispatcher &dispatcher, const sdbus::ObjectPath &objectPath) + { + return std::make_unique(dispatcher, objectPath); + } + DBusEvent OnPeerAdded; + DBusEvent OnPeerRemoved; + +private: + virtual void onPeerAdded(const sdbus::ObjectPath &peer) + { + OnPeerAdded.fire(peer); + } + virtual void onPeerRemoved(const sdbus::ObjectPath &peer) + { + OnPeerRemoved.fire(peer); + } +}; + +class DeviceWireless : public sdbus::ProxyInterfaces +{ +public: + using self = DeviceWireless; + using ptr = std::unique_ptr; + using proxy_t = org::freedesktop::NetworkManager::Device::Wireless_proxy; + DeviceWireless(DBusDispatcher &dispatcher, const sdbus::ObjectPath &path) + : sdbus::ProxyInterfaces( + dispatcher.Connection(), + "org.freedesktop.NetworkManager", + path) + { + registerProxy(); + } + virtual ~DeviceWireless() + { + unregisterProxy(); + } + + static ptr Create(DBusDispatcher &dispatcher, const sdbus::ObjectPath &path) + { + return std::make_unique(dispatcher, path); + } + DBusEvent OnAccessPointAdded; + DBusEvent OnAccessPointRemoved; +protected: + void EventTrace(const char *method, const std::string &message) + { + LogTrace(getObjectPath(), method, message); + } + + virtual void onAccessPointAdded(const sdbus::ObjectPath& access_point) override { + EventTrace("onAccessPointAdded", access_point.c_str()); + OnAccessPointAdded.fire(access_point); + } + virtual void onAccessPointRemoved(const sdbus::ObjectPath& access_point) { + EventTrace("onAccessPointRemoved",access_point.c_str()); + + OnAccessPointRemoved.fire(access_point); + + } + +}; + +class AccessPoint : public sdbus::ProxyInterfaces +{ +public: + using self = AccessPoint; + using ptr = std::unique_ptr; + using proxy_t = org::freedesktop::NetworkManager::AccessPoint_proxy; + + AccessPoint(DBusDispatcher &dispatcher, const sdbus::ObjectPath &path) + : sdbus::ProxyInterfaces( + dispatcher.Connection(), + "org.freedesktop.NetworkManager", + path) + { + registerProxy(); + } + virtual ~AccessPoint() + { + unregisterProxy(); + } + + static ptr Create(DBusDispatcher &dispatcher, const sdbus::ObjectPath &path) + { + return std::make_unique(dispatcher, path); + } +protected: + void EventTrace(const char *method, const std::string &message) + { + LogTrace(getObjectPath(), method, message); + } +}; +class ActiveConnection : public sdbus::ProxyInterfaces +{ +public: + using self = ActiveConnection; + using ptr = std::unique_ptr; + using proxy_t = org::freedesktop::NetworkManager::Connection::Active_proxy; + + ActiveConnection(DBusDispatcher &dispatcher, const sdbus::ObjectPath &path) + : sdbus::ProxyInterfaces( + dispatcher.Connection(), + "org.freedesktop.NetworkManager", + path) + { + registerProxy(); + } + virtual ~ActiveConnection() + { + unregisterProxy(); + } + + static ptr Create(DBusDispatcher &dispatcher, const sdbus::ObjectPath &path) + { + return std::make_unique(dispatcher, path); + } + DBusEvent OnStateChanged; +protected: + void EventTrace(const char *method, const std::string &message) + { + LogTrace(getObjectPath(), method, message); + } + virtual void onStateChanged(const uint32_t& state, const uint32_t& reason) { + OnStateChanged.fire(state,reason); + } + +}; + + +} // namespace diff --git a/PiPedalCommon/src/include/P2pConfigFiles.hpp b/PiPedalCommon/src/include/P2pConfigFiles.hpp index 6a29a4a..76be852 100644 --- a/PiPedalCommon/src/include/P2pConfigFiles.hpp +++ b/PiPedalCommon/src/include/P2pConfigFiles.hpp @@ -25,7 +25,7 @@ #pragma once -#define DEVICE_GUID_FILE "/etc/pipedal/config/device_uuid" +#define DEVICE_GUID_FILE "/var/pipedal/config/device_uuid" -#define PIPEDAL_P2PD_CONF_PATH "/etc/pipedal/config/pipedal_p2pd.conf" +#define PIPEDAL_P2PD_CONF_PATH "/var/pipedal/config/pipedal_p2pd.conf" diff --git a/PiPedalCommon/src/include/WifiConfigSettings.hpp b/PiPedalCommon/src/include/WifiConfigSettings.hpp index 6c1315d..44492aa 100644 --- a/PiPedalCommon/src/include/WifiConfigSettings.hpp +++ b/PiPedalCommon/src/include/WifiConfigSettings.hpp @@ -20,30 +20,92 @@ #pragma once #include "json.hpp" +#include +#include + + +#ifndef NEW_WIFI_CONFIG +// 0-> Old hostapd-based hotspot. Now dead code. Works with buster only. +// 1-> NetworkManager-based hotspot (supported on bookworm or later). +#define NEW_WIFI_CONFIG 1 +#endif + +// Wifi P2P broken completely on Debian 12 updates on raspberry pi. +// +// Setting it to enable uses current-orphaned dead code as of PiPedal v1.2.49 +// +// 0: disable. +// 1: enable. +#ifndef ENABLE_WIFI_P2P +#define ENABLE_WIFI_P2P 0 +#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: - bool valid_ = false; - bool wifiWarningGiven_ = false; - bool rebootRequired_ = false; - bool enable_ = false; + using ssid_t = std::vector; + WifiConfigSettings(); + + void Load(); + void Save(); + + 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"; bool hasPassword_ = false; std::string password_; - std::string channel_ = "g6"; + std::string channel_ = ""; + std::string mdnsName_ = "pipedal"; - 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. + 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 &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/WifiDirectConfigSettings.hpp b/PiPedalCommon/src/include/WifiDirectConfigSettings.hpp index dd15adb..fd590f3 100644 --- a/PiPedalCommon/src/include/WifiDirectConfigSettings.hpp +++ b/PiPedalCommon/src/include/WifiDirectConfigSettings.hpp @@ -37,7 +37,7 @@ namespace pipedal { std::string hotspotName_ = "pipedal"; bool pinChanged_ = false; std::string pin_; - std::string channel_ = "0"; // "0" -> select automatically. + std::string channel_ = "1"; // "0" -> select automatically. std::string wlan_ = "wlan0"; void ParseArguments(const std::vector &arguments); diff --git a/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.AccessPoint.hpp b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.AccessPoint.hpp new file mode 100644 index 0000000..9cd48ad --- /dev/null +++ b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.AccessPoint.hpp @@ -0,0 +1,87 @@ + +/* + * This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT! + */ + +#ifndef __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_AccessPoint_hpp__proxy__H__ +#define __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_AccessPoint_hpp__proxy__H__ + +#include +#include +#include + +namespace org { +namespace freedesktop { +namespace NetworkManager { + +class AccessPoint_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "org.freedesktop.NetworkManager.AccessPoint"; + +protected: + AccessPoint_proxy(sdbus::IProxy& proxy) + : proxy_(proxy) + { + } + + ~AccessPoint_proxy() = default; + +public: + uint32_t Flags() + { + return proxy_.getProperty("Flags").onInterface(INTERFACE_NAME); + } + + uint32_t WpaFlags() + { + return proxy_.getProperty("WpaFlags").onInterface(INTERFACE_NAME); + } + + uint32_t RsnFlags() + { + return proxy_.getProperty("RsnFlags").onInterface(INTERFACE_NAME); + } + + std::vector Ssid() + { + return proxy_.getProperty("Ssid").onInterface(INTERFACE_NAME); + } + + uint32_t Frequency() + { + return proxy_.getProperty("Frequency").onInterface(INTERFACE_NAME); + } + + std::string HwAddress() + { + return proxy_.getProperty("HwAddress").onInterface(INTERFACE_NAME); + } + + uint32_t Mode() + { + return proxy_.getProperty("Mode").onInterface(INTERFACE_NAME); + } + + uint32_t MaxBitrate() + { + return proxy_.getProperty("MaxBitrate").onInterface(INTERFACE_NAME); + } + + uint8_t Strength() + { + return proxy_.getProperty("Strength").onInterface(INTERFACE_NAME); + } + + int32_t LastSeen() + { + return proxy_.getProperty("LastSeen").onInterface(INTERFACE_NAME); + } + +private: + sdbus::IProxy& proxy_; +}; + +}}} // namespaces + +#endif diff --git a/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Connection.Active.hpp b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Connection.Active.hpp new file mode 100644 index 0000000..09b57d2 --- /dev/null +++ b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Connection.Active.hpp @@ -0,0 +1,126 @@ + +/* + * This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT! + */ + +#ifndef __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_Connection_Active_hpp__proxy__H__ +#define __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_Connection_Active_hpp__proxy__H__ + +#include +#include +#include + +namespace org { +namespace freedesktop { +namespace NetworkManager { +namespace Connection { + +class Active_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "org.freedesktop.NetworkManager.Connection.Active"; + +protected: + Active_proxy(sdbus::IProxy& proxy) + : proxy_(proxy) + { + proxy_.uponSignal("StateChanged").onInterface(INTERFACE_NAME).call([this](const uint32_t& state, const uint32_t& reason){ this->onStateChanged(state, reason); }); + } + + ~Active_proxy() = default; + + virtual void onStateChanged(const uint32_t& state, const uint32_t& reason) = 0; + +public: + sdbus::ObjectPath Connection() + { + return proxy_.getProperty("Connection").onInterface(INTERFACE_NAME); + } + + sdbus::ObjectPath SpecificObject() + { + return proxy_.getProperty("SpecificObject").onInterface(INTERFACE_NAME); + } + + std::string Id() + { + return proxy_.getProperty("Id").onInterface(INTERFACE_NAME); + } + + std::string Uuid() + { + return proxy_.getProperty("Uuid").onInterface(INTERFACE_NAME); + } + + std::string Type() + { + return proxy_.getProperty("Type").onInterface(INTERFACE_NAME); + } + + std::vector Devices() + { + return proxy_.getProperty("Devices").onInterface(INTERFACE_NAME); + } + + uint32_t State() + { + return proxy_.getProperty("State").onInterface(INTERFACE_NAME); + } + + uint32_t StateFlags() + { + return proxy_.getProperty("StateFlags").onInterface(INTERFACE_NAME); + } + + bool Default() + { + return proxy_.getProperty("Default").onInterface(INTERFACE_NAME); + } + + sdbus::ObjectPath Ip4Config() + { + return proxy_.getProperty("Ip4Config").onInterface(INTERFACE_NAME); + } + + sdbus::ObjectPath Dhcp4Config() + { + return proxy_.getProperty("Dhcp4Config").onInterface(INTERFACE_NAME); + } + + bool Default6() + { + return proxy_.getProperty("Default6").onInterface(INTERFACE_NAME); + } + + sdbus::ObjectPath Ip6Config() + { + return proxy_.getProperty("Ip6Config").onInterface(INTERFACE_NAME); + } + + sdbus::ObjectPath Dhcp6Config() + { + return proxy_.getProperty("Dhcp6Config").onInterface(INTERFACE_NAME); + } + + bool Vpn() + { + return proxy_.getProperty("Vpn").onInterface(INTERFACE_NAME); + } + + sdbus::ObjectPath Controller() + { + return proxy_.getProperty("Controller").onInterface(INTERFACE_NAME); + } + + sdbus::ObjectPath Master() + { + return proxy_.getProperty("Master").onInterface(INTERFACE_NAME); + } + +private: + sdbus::IProxy& proxy_; +}; + +}}}} // namespaces + +#endif diff --git a/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.DHCP4Config.hpp b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.DHCP4Config.hpp new file mode 100644 index 0000000..44baae0 --- /dev/null +++ b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.DHCP4Config.hpp @@ -0,0 +1,42 @@ + +/* + * This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT! + */ + +#ifndef __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_DHCP4Config_hpp__proxy__H__ +#define __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_DHCP4Config_hpp__proxy__H__ + +#include +#include +#include + +namespace org { +namespace freedesktop { +namespace NetworkManager { + +class DHCP4Config_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "org.freedesktop.NetworkManager.DHCP4Config"; + +protected: + DHCP4Config_proxy(sdbus::IProxy& proxy) + : proxy_(proxy) + { + } + + ~DHCP4Config_proxy() = default; + +public: + std::map Options() + { + return proxy_.getProperty("Options").onInterface(INTERFACE_NAME); + } + +private: + sdbus::IProxy& proxy_; +}; + +}}} // namespaces + +#endif diff --git a/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.DHCP6Config.hpp b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.DHCP6Config.hpp new file mode 100644 index 0000000..c13839c --- /dev/null +++ b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.DHCP6Config.hpp @@ -0,0 +1,42 @@ + +/* + * This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT! + */ + +#ifndef __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_DHCP6Config_hpp__proxy__H__ +#define __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_DHCP6Config_hpp__proxy__H__ + +#include +#include +#include + +namespace org { +namespace freedesktop { +namespace NetworkManager { + +class DHCP6Config_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "org.freedesktop.NetworkManager.DHCP6Config"; + +protected: + DHCP6Config_proxy(sdbus::IProxy& proxy) + : proxy_(proxy) + { + } + + ~DHCP6Config_proxy() = default; + +public: + std::map Options() + { + return proxy_.getProperty("Options").onInterface(INTERFACE_NAME); + } + +private: + sdbus::IProxy& proxy_; +}; + +}}} // namespaces + +#endif diff --git a/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Device.Statistics.hpp b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Device.Statistics.hpp new file mode 100644 index 0000000..6eb0830 --- /dev/null +++ b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Device.Statistics.hpp @@ -0,0 +1,58 @@ + +/* + * This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT! + */ + +#ifndef __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_Device_Statistics_hpp__proxy__H__ +#define __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_Device_Statistics_hpp__proxy__H__ + +#include +#include +#include + +namespace org { +namespace freedesktop { +namespace NetworkManager { +namespace Device { + +class Statistics_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "org.freedesktop.NetworkManager.Device.Statistics"; + +protected: + Statistics_proxy(sdbus::IProxy& proxy) + : proxy_(proxy) + { + } + + ~Statistics_proxy() = default; + +public: + uint32_t RefreshRateMs() + { + return proxy_.getProperty("RefreshRateMs").onInterface(INTERFACE_NAME); + } + + void RefreshRateMs(const uint32_t& value) + { + proxy_.setProperty("RefreshRateMs").onInterface(INTERFACE_NAME).toValue(value); + } + + uint64_t TxBytes() + { + return proxy_.getProperty("TxBytes").onInterface(INTERFACE_NAME); + } + + uint64_t RxBytes() + { + return proxy_.getProperty("RxBytes").onInterface(INTERFACE_NAME); + } + +private: + sdbus::IProxy& proxy_; +}; + +}}}} // namespaces + +#endif diff --git a/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Device.WifiP2P.hpp b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Device.WifiP2P.hpp new file mode 100644 index 0000000..8ff2ec9 --- /dev/null +++ b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Device.WifiP2P.hpp @@ -0,0 +1,64 @@ + +/* + * This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT! + */ + +#ifndef __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_Device_WifiP2P_hpp__proxy__H__ +#define __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_Device_WifiP2P_hpp__proxy__H__ + +#include +#include +#include + +namespace org { +namespace freedesktop { +namespace NetworkManager { +namespace Device { + +class WifiP2P_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "org.freedesktop.NetworkManager.Device.WifiP2P"; + +protected: + WifiP2P_proxy(sdbus::IProxy& proxy) + : proxy_(proxy) + { + proxy_.uponSignal("PeerAdded").onInterface(INTERFACE_NAME).call([this](const sdbus::ObjectPath& peer){ this->onPeerAdded(peer); }); + proxy_.uponSignal("PeerRemoved").onInterface(INTERFACE_NAME).call([this](const sdbus::ObjectPath& peer){ this->onPeerRemoved(peer); }); + } + + ~WifiP2P_proxy() = default; + + virtual void onPeerAdded(const sdbus::ObjectPath& peer) = 0; + virtual void onPeerRemoved(const sdbus::ObjectPath& peer) = 0; + +public: + void StartFind(const std::map& options) + { + proxy_.callMethod("StartFind").onInterface(INTERFACE_NAME).withArguments(options); + } + + void StopFind() + { + proxy_.callMethod("StopFind").onInterface(INTERFACE_NAME); + } + +public: + std::string HwAddress() + { + return proxy_.getProperty("HwAddress").onInterface(INTERFACE_NAME); + } + + std::vector Peers() + { + return proxy_.getProperty("Peers").onInterface(INTERFACE_NAME); + } + +private: + sdbus::IProxy& proxy_; +}; + +}}}} // namespaces + +#endif diff --git a/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Device.Wireless.hpp b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Device.Wireless.hpp new file mode 100644 index 0000000..e00de94 --- /dev/null +++ b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Device.Wireless.hpp @@ -0,0 +1,103 @@ + +/* + * This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT! + */ + +#ifndef __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_Device_Wireless_hpp__proxy__H__ +#define __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_Device_Wireless_hpp__proxy__H__ + +#include +#include +#include + +namespace org { +namespace freedesktop { +namespace NetworkManager { +namespace Device { + +class Wireless_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "org.freedesktop.NetworkManager.Device.Wireless"; + +protected: + Wireless_proxy(sdbus::IProxy& proxy) + : proxy_(proxy) + { + proxy_.uponSignal("AccessPointAdded").onInterface(INTERFACE_NAME).call([this](const sdbus::ObjectPath& access_point){ this->onAccessPointAdded(access_point); }); + proxy_.uponSignal("AccessPointRemoved").onInterface(INTERFACE_NAME).call([this](const sdbus::ObjectPath& access_point){ this->onAccessPointRemoved(access_point); }); + } + + ~Wireless_proxy() = default; + + virtual void onAccessPointAdded(const sdbus::ObjectPath& access_point) = 0; + virtual void onAccessPointRemoved(const sdbus::ObjectPath& access_point) = 0; + +public: + std::vector GetAccessPoints() + { + std::vector result; + proxy_.callMethod("GetAccessPoints").onInterface(INTERFACE_NAME).storeResultsTo(result); + return result; + } + + std::vector GetAllAccessPoints() + { + std::vector result; + proxy_.callMethod("GetAllAccessPoints").onInterface(INTERFACE_NAME).storeResultsTo(result); + return result; + } + + void RequestScan(const std::map& options) + { + proxy_.callMethod("RequestScan").onInterface(INTERFACE_NAME).withArguments(options); + } + +public: + std::string HwAddress() + { + return proxy_.getProperty("HwAddress").onInterface(INTERFACE_NAME); + } + + std::string PermHwAddress() + { + return proxy_.getProperty("PermHwAddress").onInterface(INTERFACE_NAME); + } + + uint32_t Mode() + { + return proxy_.getProperty("Mode").onInterface(INTERFACE_NAME); + } + + uint32_t Bitrate() + { + return proxy_.getProperty("Bitrate").onInterface(INTERFACE_NAME); + } + + std::vector AccessPoints() + { + return proxy_.getProperty("AccessPoints").onInterface(INTERFACE_NAME); + } + + sdbus::ObjectPath ActiveAccessPoint() + { + return proxy_.getProperty("ActiveAccessPoint").onInterface(INTERFACE_NAME); + } + + uint32_t WirelessCapabilities() + { + return proxy_.getProperty("WirelessCapabilities").onInterface(INTERFACE_NAME); + } + + int64_t LastScan() + { + return proxy_.getProperty("LastScan").onInterface(INTERFACE_NAME); + } + +private: + sdbus::IProxy& proxy_; +}; + +}}}} // namespaces + +#endif diff --git a/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Device.hpp b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Device.hpp new file mode 100644 index 0000000..bdb1c90 --- /dev/null +++ b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Device.hpp @@ -0,0 +1,233 @@ + +/* + * This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT! + */ + +#ifndef __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_Device_hpp__proxy__H__ +#define __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_Device_hpp__proxy__H__ + +#include +#include +#include + +namespace org { +namespace freedesktop { +namespace NetworkManager { + +class Device_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "org.freedesktop.NetworkManager.Device"; + +protected: + Device_proxy(sdbus::IProxy& proxy) + : proxy_(proxy) + { + proxy_.uponSignal("StateChanged").onInterface(INTERFACE_NAME).call([this](const uint32_t& new_state, const uint32_t& old_state, const uint32_t& reason){ this->onStateChanged(new_state, old_state, reason); }); + } + + ~Device_proxy() = default; + + virtual void onStateChanged(const uint32_t& new_state, const uint32_t& old_state, const uint32_t& reason) = 0; + +public: + void Reapply(const std::map>& connection, const uint64_t& version_id, const uint32_t& flags) + { + proxy_.callMethod("Reapply").onInterface(INTERFACE_NAME).withArguments(connection, version_id, flags); + } + + std::tuple>, uint64_t> GetAppliedConnection(const uint32_t& flags) + { + std::tuple>, uint64_t> result; + proxy_.callMethod("GetAppliedConnection").onInterface(INTERFACE_NAME).withArguments(flags).storeResultsTo(result); + return result; + } + + void Disconnect() + { + proxy_.callMethod("Disconnect").onInterface(INTERFACE_NAME); + } + + void Delete() + { + proxy_.callMethod("Delete").onInterface(INTERFACE_NAME); + } + +public: + std::string Udi() + { + return proxy_.getProperty("Udi").onInterface(INTERFACE_NAME); + } + + std::string Path() + { + return proxy_.getProperty("Path").onInterface(INTERFACE_NAME); + } + + std::string Interface() + { + return proxy_.getProperty("Interface").onInterface(INTERFACE_NAME); + } + + std::string IpInterface() + { + return proxy_.getProperty("IpInterface").onInterface(INTERFACE_NAME); + } + + std::string Driver() + { + return proxy_.getProperty("Driver").onInterface(INTERFACE_NAME); + } + + std::string DriverVersion() + { + return proxy_.getProperty("DriverVersion").onInterface(INTERFACE_NAME); + } + + std::string FirmwareVersion() + { + return proxy_.getProperty("FirmwareVersion").onInterface(INTERFACE_NAME); + } + + uint32_t Capabilities() + { + return proxy_.getProperty("Capabilities").onInterface(INTERFACE_NAME); + } + + uint32_t Ip4Address() + { + return proxy_.getProperty("Ip4Address").onInterface(INTERFACE_NAME); + } + + uint32_t State() + { + return proxy_.getProperty("State").onInterface(INTERFACE_NAME); + } + + sdbus::Struct StateReason() + { + return proxy_.getProperty("StateReason").onInterface(INTERFACE_NAME); + } + + sdbus::ObjectPath ActiveConnection() + { + return proxy_.getProperty("ActiveConnection").onInterface(INTERFACE_NAME); + } + + sdbus::ObjectPath Ip4Config() + { + return proxy_.getProperty("Ip4Config").onInterface(INTERFACE_NAME); + } + + sdbus::ObjectPath Dhcp4Config() + { + return proxy_.getProperty("Dhcp4Config").onInterface(INTERFACE_NAME); + } + + sdbus::ObjectPath Ip6Config() + { + return proxy_.getProperty("Ip6Config").onInterface(INTERFACE_NAME); + } + + sdbus::ObjectPath Dhcp6Config() + { + return proxy_.getProperty("Dhcp6Config").onInterface(INTERFACE_NAME); + } + + bool Managed() + { + return proxy_.getProperty("Managed").onInterface(INTERFACE_NAME); + } + + void Managed(const bool& value) + { + proxy_.setProperty("Managed").onInterface(INTERFACE_NAME).toValue(value); + } + + bool Autoconnect() + { + return proxy_.getProperty("Autoconnect").onInterface(INTERFACE_NAME); + } + + void Autoconnect(const bool& value) + { + proxy_.setProperty("Autoconnect").onInterface(INTERFACE_NAME).toValue(value); + } + + bool FirmwareMissing() + { + return proxy_.getProperty("FirmwareMissing").onInterface(INTERFACE_NAME); + } + + bool NmPluginMissing() + { + return proxy_.getProperty("NmPluginMissing").onInterface(INTERFACE_NAME); + } + + uint32_t DeviceType() + { + return proxy_.getProperty("DeviceType").onInterface(INTERFACE_NAME); + } + + std::vector AvailableConnections() + { + return proxy_.getProperty("AvailableConnections").onInterface(INTERFACE_NAME); + } + + std::string PhysicalPortId() + { + return proxy_.getProperty("PhysicalPortId").onInterface(INTERFACE_NAME); + } + + uint32_t Mtu() + { + return proxy_.getProperty("Mtu").onInterface(INTERFACE_NAME); + } + + uint32_t Metered() + { + return proxy_.getProperty("Metered").onInterface(INTERFACE_NAME); + } + + std::vector> LldpNeighbors() + { + return proxy_.getProperty("LldpNeighbors").onInterface(INTERFACE_NAME); + } + + bool Real() + { + return proxy_.getProperty("Real").onInterface(INTERFACE_NAME); + } + + uint32_t Ip4Connectivity() + { + return proxy_.getProperty("Ip4Connectivity").onInterface(INTERFACE_NAME); + } + + uint32_t Ip6Connectivity() + { + return proxy_.getProperty("Ip6Connectivity").onInterface(INTERFACE_NAME); + } + + uint32_t InterfaceFlags() + { + return proxy_.getProperty("InterfaceFlags").onInterface(INTERFACE_NAME); + } + + std::string HwAddress() + { + return proxy_.getProperty("HwAddress").onInterface(INTERFACE_NAME); + } + + std::vector Ports() + { + return proxy_.getProperty("Ports").onInterface(INTERFACE_NAME); + } + +private: + sdbus::IProxy& proxy_; +}; + +}}} // namespaces + +#endif diff --git a/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.DnsManager.hpp b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.DnsManager.hpp new file mode 100644 index 0000000..9cba4bb --- /dev/null +++ b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.DnsManager.hpp @@ -0,0 +1,52 @@ + +/* + * This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT! + */ + +#ifndef __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_DnsManager_hpp__proxy__H__ +#define __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_DnsManager_hpp__proxy__H__ + +#include +#include +#include + +namespace org { +namespace freedesktop { +namespace NetworkManager { + +class DnsManager_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "org.freedesktop.NetworkManager.DnsManager"; + +protected: + DnsManager_proxy(sdbus::IProxy& proxy) + : proxy_(proxy) + { + } + + ~DnsManager_proxy() = default; + +public: + std::string Mode() + { + return proxy_.getProperty("Mode").onInterface(INTERFACE_NAME); + } + + std::string RcManager() + { + return proxy_.getProperty("RcManager").onInterface(INTERFACE_NAME); + } + + std::vector> Configuration() + { + return proxy_.getProperty("Configuration").onInterface(INTERFACE_NAME); + } + +private: + sdbus::IProxy& proxy_; +}; + +}}} // namespaces + +#endif diff --git a/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.IP4Config.hpp b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.IP4Config.hpp new file mode 100644 index 0000000..7fdd631 --- /dev/null +++ b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.IP4Config.hpp @@ -0,0 +1,102 @@ + +/* + * This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT! + */ + +#ifndef __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_IP4Config_hpp__proxy__H__ +#define __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_IP4Config_hpp__proxy__H__ + +#include +#include +#include + +namespace org { +namespace freedesktop { +namespace NetworkManager { + +class IP4Config_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "org.freedesktop.NetworkManager.IP4Config"; + +protected: + IP4Config_proxy(sdbus::IProxy& proxy) + : proxy_(proxy) + { + } + + ~IP4Config_proxy() = default; + +public: + std::vector> Addresses() + { + return proxy_.getProperty("Addresses").onInterface(INTERFACE_NAME); + } + + std::vector> AddressData() + { + return proxy_.getProperty("AddressData").onInterface(INTERFACE_NAME); + } + + std::string Gateway() + { + return proxy_.getProperty("Gateway").onInterface(INTERFACE_NAME); + } + + std::vector> Routes() + { + return proxy_.getProperty("Routes").onInterface(INTERFACE_NAME); + } + + std::vector> RouteData() + { + return proxy_.getProperty("RouteData").onInterface(INTERFACE_NAME); + } + + std::vector Nameservers() + { + return proxy_.getProperty("Nameservers").onInterface(INTERFACE_NAME); + } + + std::vector> NameserverData() + { + return proxy_.getProperty("NameserverData").onInterface(INTERFACE_NAME); + } + + std::vector Domains() + { + return proxy_.getProperty("Domains").onInterface(INTERFACE_NAME); + } + + std::vector Searches() + { + return proxy_.getProperty("Searches").onInterface(INTERFACE_NAME); + } + + std::vector DnsOptions() + { + return proxy_.getProperty("DnsOptions").onInterface(INTERFACE_NAME); + } + + int32_t DnsPriority() + { + return proxy_.getProperty("DnsPriority").onInterface(INTERFACE_NAME); + } + + std::vector WinsServers() + { + return proxy_.getProperty("WinsServers").onInterface(INTERFACE_NAME); + } + + std::vector WinsServerData() + { + return proxy_.getProperty("WinsServerData").onInterface(INTERFACE_NAME); + } + +private: + sdbus::IProxy& proxy_; +}; + +}}} // namespaces + +#endif diff --git a/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.IP6Config.hpp b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.IP6Config.hpp new file mode 100644 index 0000000..94935fb --- /dev/null +++ b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.IP6Config.hpp @@ -0,0 +1,87 @@ + +/* + * This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT! + */ + +#ifndef __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_IP6Config_hpp__proxy__H__ +#define __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_IP6Config_hpp__proxy__H__ + +#include +#include +#include + +namespace org { +namespace freedesktop { +namespace NetworkManager { + +class IP6Config_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "org.freedesktop.NetworkManager.IP6Config"; + +protected: + IP6Config_proxy(sdbus::IProxy& proxy) + : proxy_(proxy) + { + } + + ~IP6Config_proxy() = default; + +public: + std::vector, uint32_t, std::vector>> Addresses() + { + return proxy_.getProperty("Addresses").onInterface(INTERFACE_NAME); + } + + std::vector> AddressData() + { + return proxy_.getProperty("AddressData").onInterface(INTERFACE_NAME); + } + + std::string Gateway() + { + return proxy_.getProperty("Gateway").onInterface(INTERFACE_NAME); + } + + std::vector, uint32_t, std::vector, uint32_t>> Routes() + { + return proxy_.getProperty("Routes").onInterface(INTERFACE_NAME); + } + + std::vector> RouteData() + { + return proxy_.getProperty("RouteData").onInterface(INTERFACE_NAME); + } + + std::vector> Nameservers() + { + return proxy_.getProperty("Nameservers").onInterface(INTERFACE_NAME); + } + + std::vector Domains() + { + return proxy_.getProperty("Domains").onInterface(INTERFACE_NAME); + } + + std::vector Searches() + { + return proxy_.getProperty("Searches").onInterface(INTERFACE_NAME); + } + + std::vector DnsOptions() + { + return proxy_.getProperty("DnsOptions").onInterface(INTERFACE_NAME); + } + + int32_t DnsPriority() + { + return proxy_.getProperty("DnsPriority").onInterface(INTERFACE_NAME); + } + +private: + sdbus::IProxy& proxy_; +}; + +}}} // namespaces + +#endif diff --git a/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Settings.Connection.hpp b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Settings.Connection.hpp new file mode 100644 index 0000000..0beb271 --- /dev/null +++ b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Settings.Connection.hpp @@ -0,0 +1,105 @@ + +/* + * This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT! + */ + +#ifndef __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_Settings_Connection_hpp__proxy__H__ +#define __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_Settings_Connection_hpp__proxy__H__ + +#include +#include +#include + +namespace org { +namespace freedesktop { +namespace NetworkManager { +namespace Settings { + +class Connection_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "org.freedesktop.NetworkManager.Settings.Connection"; + +protected: + Connection_proxy(sdbus::IProxy& proxy) + : proxy_(proxy) + { + proxy_.uponSignal("Updated").onInterface(INTERFACE_NAME).call([this](){ this->onUpdated(); }); + proxy_.uponSignal("Removed").onInterface(INTERFACE_NAME).call([this](){ this->onRemoved(); }); + } + + ~Connection_proxy() = default; + + virtual void onUpdated() = 0; + virtual void onRemoved() = 0; + +public: + void Update(const std::map>& properties) + { + proxy_.callMethod("Update").onInterface(INTERFACE_NAME).withArguments(properties); + } + + void UpdateUnsaved(const std::map>& properties) + { + proxy_.callMethod("UpdateUnsaved").onInterface(INTERFACE_NAME).withArguments(properties); + } + + void Delete() + { + proxy_.callMethod("Delete").onInterface(INTERFACE_NAME); + } + + std::map> GetSettings() + { + std::map> result; + proxy_.callMethod("GetSettings").onInterface(INTERFACE_NAME).storeResultsTo(result); + return result; + } + + std::map> GetSecrets(const std::string& setting_name) + { + std::map> result; + proxy_.callMethod("GetSecrets").onInterface(INTERFACE_NAME).withArguments(setting_name).storeResultsTo(result); + return result; + } + + void ClearSecrets() + { + proxy_.callMethod("ClearSecrets").onInterface(INTERFACE_NAME); + } + + void Save() + { + proxy_.callMethod("Save").onInterface(INTERFACE_NAME); + } + + std::map Update2(const std::map>& settings, const uint32_t& flags, const std::map& args) + { + std::map result; + proxy_.callMethod("Update2").onInterface(INTERFACE_NAME).withArguments(settings, flags, args).storeResultsTo(result); + return result; + } + +public: + bool Unsaved() + { + return proxy_.getProperty("Unsaved").onInterface(INTERFACE_NAME); + } + + uint32_t Flags() + { + return proxy_.getProperty("Flags").onInterface(INTERFACE_NAME); + } + + std::string Filename() + { + return proxy_.getProperty("Filename").onInterface(INTERFACE_NAME); + } + +private: + sdbus::IProxy& proxy_; +}; + +}}}} // namespaces + +#endif diff --git a/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Settings.hpp b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Settings.hpp new file mode 100644 index 0000000..1285c17 --- /dev/null +++ b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.Settings.hpp @@ -0,0 +1,112 @@ + +/* + * This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT! + */ + +#ifndef __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_Settings_hpp__proxy__H__ +#define __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_Settings_hpp__proxy__H__ + +#include +#include +#include + +namespace org { +namespace freedesktop { +namespace NetworkManager { + +class Settings_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "org.freedesktop.NetworkManager.Settings"; + +protected: + Settings_proxy(sdbus::IProxy& proxy) + : proxy_(proxy) + { + proxy_.uponSignal("NewConnection").onInterface(INTERFACE_NAME).call([this](const sdbus::ObjectPath& connection){ this->onNewConnection(connection); }); + proxy_.uponSignal("ConnectionRemoved").onInterface(INTERFACE_NAME).call([this](const sdbus::ObjectPath& connection){ this->onConnectionRemoved(connection); }); + } + + ~Settings_proxy() = default; + + virtual void onNewConnection(const sdbus::ObjectPath& connection) = 0; + virtual void onConnectionRemoved(const sdbus::ObjectPath& connection) = 0; + +public: + std::vector ListConnections() + { + std::vector result; + proxy_.callMethod("ListConnections").onInterface(INTERFACE_NAME).storeResultsTo(result); + return result; + } + + sdbus::ObjectPath GetConnectionByUuid(const std::string& uuid) + { + sdbus::ObjectPath result; + proxy_.callMethod("GetConnectionByUuid").onInterface(INTERFACE_NAME).withArguments(uuid).storeResultsTo(result); + return result; + } + + sdbus::ObjectPath AddConnection(const std::map>& connection) + { + sdbus::ObjectPath result; + proxy_.callMethod("AddConnection").onInterface(INTERFACE_NAME).withArguments(connection).storeResultsTo(result); + return result; + } + + sdbus::ObjectPath AddConnectionUnsaved(const std::map>& connection) + { + sdbus::ObjectPath result; + proxy_.callMethod("AddConnectionUnsaved").onInterface(INTERFACE_NAME).withArguments(connection).storeResultsTo(result); + return result; + } + + std::tuple> AddConnection2(const std::map>& settings, const uint32_t& flags, const std::map& args) + { + std::tuple> result; + proxy_.callMethod("AddConnection2").onInterface(INTERFACE_NAME).withArguments(settings, flags, args).storeResultsTo(result); + return result; + } + + std::tuple> LoadConnections(const std::vector& filenames) + { + std::tuple> result; + proxy_.callMethod("LoadConnections").onInterface(INTERFACE_NAME).withArguments(filenames).storeResultsTo(result); + return result; + } + + bool ReloadConnections() + { + bool result; + proxy_.callMethod("ReloadConnections").onInterface(INTERFACE_NAME).storeResultsTo(result); + return result; + } + + void SaveHostname(const std::string& hostname) + { + proxy_.callMethod("SaveHostname").onInterface(INTERFACE_NAME).withArguments(hostname); + } + +public: + std::vector Connections() + { + return proxy_.getProperty("Connections").onInterface(INTERFACE_NAME); + } + + std::string Hostname() + { + return proxy_.getProperty("Hostname").onInterface(INTERFACE_NAME); + } + + bool CanModify() + { + return proxy_.getProperty("CanModify").onInterface(INTERFACE_NAME); + } + +private: + sdbus::IProxy& proxy_; +}; + +}}} // namespaces + +#endif diff --git a/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.WifiP2PPeer.hpp b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.WifiP2PPeer.hpp new file mode 100644 index 0000000..80c607c --- /dev/null +++ b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.WifiP2PPeer.hpp @@ -0,0 +1,87 @@ + +/* + * This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT! + */ + +#ifndef __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_WifiP2PPeer_hpp__proxy__H__ +#define __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_WifiP2PPeer_hpp__proxy__H__ + +#include +#include +#include + +namespace org { +namespace freedesktop { +namespace NetworkManager { + +class WifiP2PPeer_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "org.freedesktop.NetworkManager.WifiP2PPeer"; + +protected: + WifiP2PPeer_proxy(sdbus::IProxy& proxy) + : proxy_(proxy) + { + } + + ~WifiP2PPeer_proxy() = default; + +public: + std::string Name() + { + return proxy_.getProperty("Name").onInterface(INTERFACE_NAME); + } + + uint32_t Flags() + { + return proxy_.getProperty("Flags").onInterface(INTERFACE_NAME); + } + + std::string Manufacturer() + { + return proxy_.getProperty("Manufacturer").onInterface(INTERFACE_NAME); + } + + std::string Model() + { + return proxy_.getProperty("Model").onInterface(INTERFACE_NAME); + } + + std::string ModelNumber() + { + return proxy_.getProperty("ModelNumber").onInterface(INTERFACE_NAME); + } + + std::string Serial() + { + return proxy_.getProperty("Serial").onInterface(INTERFACE_NAME); + } + + std::vector WfdIEs() + { + return proxy_.getProperty("WfdIEs").onInterface(INTERFACE_NAME); + } + + std::string HwAddress() + { + return proxy_.getProperty("HwAddress").onInterface(INTERFACE_NAME); + } + + uint8_t Strength() + { + return proxy_.getProperty("Strength").onInterface(INTERFACE_NAME); + } + + int32_t LastSeen() + { + return proxy_.getProperty("LastSeen").onInterface(INTERFACE_NAME); + } + +private: + sdbus::IProxy& proxy_; +}; + +}}} // namespaces + +#endif diff --git a/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.hpp b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.hpp new file mode 100644 index 0000000..a4e7746 --- /dev/null +++ b/PiPedalCommon/src/include/dbus/org.freedesktop.NetworkManager.hpp @@ -0,0 +1,320 @@ + +/* + * This file was automatically generated by sdbus-c++-xml2cpp; DO NOT EDIT! + */ + +#ifndef __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_hpp__proxy__H__ +#define __sdbuscpp__dbus_hpp_org_freedesktop_NetworkManager_hpp__proxy__H__ + +#include +#include +#include + +namespace org { +namespace freedesktop { + +class NetworkManager_proxy +{ +public: + static constexpr const char* INTERFACE_NAME = "org.freedesktop.NetworkManager"; + +protected: + NetworkManager_proxy(sdbus::IProxy& proxy) + : proxy_(proxy) + { + proxy_.uponSignal("CheckPermissions").onInterface(INTERFACE_NAME).call([this](){ this->onCheckPermissions(); }); + proxy_.uponSignal("StateChanged").onInterface(INTERFACE_NAME).call([this](const uint32_t& state){ this->onStateChanged(state); }); + proxy_.uponSignal("DeviceAdded").onInterface(INTERFACE_NAME).call([this](const sdbus::ObjectPath& device_path){ this->onDeviceAdded(device_path); }); + proxy_.uponSignal("DeviceRemoved").onInterface(INTERFACE_NAME).call([this](const sdbus::ObjectPath& device_path){ this->onDeviceRemoved(device_path); }); + } + + ~NetworkManager_proxy() = default; + + virtual void onCheckPermissions() = 0; + virtual void onStateChanged(const uint32_t& state) = 0; + virtual void onDeviceAdded(const sdbus::ObjectPath& device_path) = 0; + virtual void onDeviceRemoved(const sdbus::ObjectPath& device_path) = 0; + +public: + void Reload(const uint32_t& flags) + { + proxy_.callMethod("Reload").onInterface(INTERFACE_NAME).withArguments(flags); + } + + std::vector GetDevices() + { + std::vector result; + proxy_.callMethod("GetDevices").onInterface(INTERFACE_NAME).storeResultsTo(result); + return result; + } + + std::vector GetAllDevices() + { + std::vector result; + proxy_.callMethod("GetAllDevices").onInterface(INTERFACE_NAME).storeResultsTo(result); + return result; + } + + sdbus::ObjectPath GetDeviceByIpIface(const std::string& iface) + { + sdbus::ObjectPath result; + proxy_.callMethod("GetDeviceByIpIface").onInterface(INTERFACE_NAME).withArguments(iface).storeResultsTo(result); + return result; + } + + sdbus::ObjectPath ActivateConnection(const sdbus::ObjectPath& connection, const sdbus::ObjectPath& device, const sdbus::ObjectPath& specific_object) + { + sdbus::ObjectPath result; + proxy_.callMethod("ActivateConnection").onInterface(INTERFACE_NAME).withArguments(connection, device, specific_object).storeResultsTo(result); + return result; + } + + std::tuple AddAndActivateConnection(const std::map>& connection, const sdbus::ObjectPath& device, const sdbus::ObjectPath& specific_object) + { + std::tuple result; + proxy_.callMethod("AddAndActivateConnection").onInterface(INTERFACE_NAME).withArguments(connection, device, specific_object).storeResultsTo(result); + return result; + } + + std::tuple> AddAndActivateConnection2(const std::map>& connection, const sdbus::ObjectPath& device, const sdbus::ObjectPath& specific_object, const std::map& options) + { + std::tuple> result; + proxy_.callMethod("AddAndActivateConnection2").onInterface(INTERFACE_NAME).withArguments(connection, device, specific_object, options).storeResultsTo(result); + return result; + } + + void DeactivateConnection(const sdbus::ObjectPath& active_connection) + { + proxy_.callMethod("DeactivateConnection").onInterface(INTERFACE_NAME).withArguments(active_connection); + } + + void Sleep(const bool& sleep) + { + proxy_.callMethod("Sleep").onInterface(INTERFACE_NAME).withArguments(sleep); + } + + void Enable(const bool& enable) + { + proxy_.callMethod("Enable").onInterface(INTERFACE_NAME).withArguments(enable); + } + + std::map GetPermissions() + { + std::map result; + proxy_.callMethod("GetPermissions").onInterface(INTERFACE_NAME).storeResultsTo(result); + return result; + } + + void SetLogging(const std::string& level, const std::string& domains) + { + proxy_.callMethod("SetLogging").onInterface(INTERFACE_NAME).withArguments(level, domains); + } + + std::tuple GetLogging() + { + std::tuple result; + proxy_.callMethod("GetLogging").onInterface(INTERFACE_NAME).storeResultsTo(result); + return result; + } + + uint32_t CheckConnectivity() + { + uint32_t result; + proxy_.callMethod("CheckConnectivity").onInterface(INTERFACE_NAME).storeResultsTo(result); + return result; + } + + uint32_t state() + { + uint32_t result; + proxy_.callMethod("state").onInterface(INTERFACE_NAME).storeResultsTo(result); + return result; + } + + sdbus::ObjectPath CheckpointCreate(const std::vector& devices, const uint32_t& rollback_timeout, const uint32_t& flags) + { + sdbus::ObjectPath result; + proxy_.callMethod("CheckpointCreate").onInterface(INTERFACE_NAME).withArguments(devices, rollback_timeout, flags).storeResultsTo(result); + return result; + } + + void CheckpointDestroy(const sdbus::ObjectPath& checkpoint) + { + proxy_.callMethod("CheckpointDestroy").onInterface(INTERFACE_NAME).withArguments(checkpoint); + } + + std::map CheckpointRollback(const sdbus::ObjectPath& checkpoint) + { + std::map result; + proxy_.callMethod("CheckpointRollback").onInterface(INTERFACE_NAME).withArguments(checkpoint).storeResultsTo(result); + return result; + } + + void CheckpointAdjustRollbackTimeout(const sdbus::ObjectPath& checkpoint, const uint32_t& add_timeout) + { + proxy_.callMethod("CheckpointAdjustRollbackTimeout").onInterface(INTERFACE_NAME).withArguments(checkpoint, add_timeout); + } + +public: + std::vector Devices() + { + return proxy_.getProperty("Devices").onInterface(INTERFACE_NAME); + } + + std::vector AllDevices() + { + return proxy_.getProperty("AllDevices").onInterface(INTERFACE_NAME); + } + + std::vector Checkpoints() + { + return proxy_.getProperty("Checkpoints").onInterface(INTERFACE_NAME); + } + + bool NetworkingEnabled() + { + return proxy_.getProperty("NetworkingEnabled").onInterface(INTERFACE_NAME); + } + + bool WirelessEnabled() + { + return proxy_.getProperty("WirelessEnabled").onInterface(INTERFACE_NAME); + } + + void WirelessEnabled(const bool& value) + { + proxy_.setProperty("WirelessEnabled").onInterface(INTERFACE_NAME).toValue(value); + } + + bool WirelessHardwareEnabled() + { + return proxy_.getProperty("WirelessHardwareEnabled").onInterface(INTERFACE_NAME); + } + + bool WwanEnabled() + { + return proxy_.getProperty("WwanEnabled").onInterface(INTERFACE_NAME); + } + + void WwanEnabled(const bool& value) + { + proxy_.setProperty("WwanEnabled").onInterface(INTERFACE_NAME).toValue(value); + } + + bool WwanHardwareEnabled() + { + return proxy_.getProperty("WwanHardwareEnabled").onInterface(INTERFACE_NAME); + } + + bool WimaxEnabled() + { + return proxy_.getProperty("WimaxEnabled").onInterface(INTERFACE_NAME); + } + + void WimaxEnabled(const bool& value) + { + proxy_.setProperty("WimaxEnabled").onInterface(INTERFACE_NAME).toValue(value); + } + + bool WimaxHardwareEnabled() + { + return proxy_.getProperty("WimaxHardwareEnabled").onInterface(INTERFACE_NAME); + } + + uint32_t RadioFlags() + { + return proxy_.getProperty("RadioFlags").onInterface(INTERFACE_NAME); + } + + std::vector ActiveConnections() + { + return proxy_.getProperty("ActiveConnections").onInterface(INTERFACE_NAME); + } + + sdbus::ObjectPath PrimaryConnection() + { + return proxy_.getProperty("PrimaryConnection").onInterface(INTERFACE_NAME); + } + + std::string PrimaryConnectionType() + { + return proxy_.getProperty("PrimaryConnectionType").onInterface(INTERFACE_NAME); + } + + uint32_t Metered() + { + return proxy_.getProperty("Metered").onInterface(INTERFACE_NAME); + } + + sdbus::ObjectPath ActivatingConnection() + { + return proxy_.getProperty("ActivatingConnection").onInterface(INTERFACE_NAME); + } + + bool Startup() + { + return proxy_.getProperty("Startup").onInterface(INTERFACE_NAME); + } + + std::string Version() + { + return proxy_.getProperty("Version").onInterface(INTERFACE_NAME); + } + + std::vector VersionInfo() + { + return proxy_.getProperty("VersionInfo").onInterface(INTERFACE_NAME); + } + + std::vector Capabilities() + { + return proxy_.getProperty("Capabilities").onInterface(INTERFACE_NAME); + } + + uint32_t State() + { + return proxy_.getProperty("State").onInterface(INTERFACE_NAME); + } + + uint32_t Connectivity() + { + return proxy_.getProperty("Connectivity").onInterface(INTERFACE_NAME); + } + + bool ConnectivityCheckAvailable() + { + return proxy_.getProperty("ConnectivityCheckAvailable").onInterface(INTERFACE_NAME); + } + + bool ConnectivityCheckEnabled() + { + return proxy_.getProperty("ConnectivityCheckEnabled").onInterface(INTERFACE_NAME); + } + + void ConnectivityCheckEnabled(const bool& value) + { + proxy_.setProperty("ConnectivityCheckEnabled").onInterface(INTERFACE_NAME).toValue(value); + } + + std::string ConnectivityCheckUri() + { + return proxy_.getProperty("ConnectivityCheckUri").onInterface(INTERFACE_NAME); + } + + std::map GlobalDnsConfiguration() + { + return proxy_.getProperty("GlobalDnsConfiguration").onInterface(INTERFACE_NAME); + } + + void GlobalDnsConfiguration(const std::map& value) + { + proxy_.setProperty("GlobalDnsConfiguration").onInterface(INTERFACE_NAME).toValue(value); + } + +private: + sdbus::IProxy& proxy_; +}; + +}} // namespaces + +#endif diff --git a/PiPedalCommon/src/include/json.hpp b/PiPedalCommon/src/include/json.hpp index 57c37a0..23ef873 100644 --- a/PiPedalCommon/src/include/json.hpp +++ b/PiPedalCommon/src/include/json.hpp @@ -33,6 +33,7 @@ #include #include #include +#include #define DECLARE_JSON_MAP(CLASSNAME) \ static pipedal::json_map::storage_type jmap @@ -327,6 +328,22 @@ namespace pipedal bool allowNaN() const { return allowNaN_; } void allowNaN(bool allow) { allowNaN_ = allow; } + void write(uint8_t value) + { + os << value; + } + void write(int8_t value) + { + os << value; + } + void write(short value) + { + os << value; + } + void write(unsigned short value) + { + os << value; + } void write(long long value) { os << value; @@ -351,6 +368,7 @@ namespace pipedal { os << value; } + void write (const std::chrono::system_clock::time_point &time); private: static void throw_encoding_error(); @@ -882,6 +900,29 @@ namespace pipedal skip_whitespace(); *value = read_boolean(); } + void read(uint8_t*value) + { + skip_whitespace(); + is_ >> *value; + 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(); @@ -956,6 +997,8 @@ namespace pipedal if (is_.fail()) throw JsonException("Invalid format."); } + void read(std::chrono::system_clock::time_point *value); + template void read(std::vector *value) { diff --git a/PiPedalCommon/src/include/util.hpp b/PiPedalCommon/src/include/util.hpp index 9a3fa3c..93e6343 100644 --- a/PiPedalCommon/src/include/util.hpp +++ b/PiPedalCommon/src/include/util.hpp @@ -35,4 +35,7 @@ namespace pipedal { std::u32string ToUtf32(const std::string &s); inline bool isPathSeperator(char c) { return c == '/' || c == std::filesystem::path::preferred_separator;} + + std::string GetHostName(); + } diff --git a/PiPedalCommon/src/json.cpp b/PiPedalCommon/src/json.cpp index ee6b163..5b55fc6 100644 --- a/PiPedalCommon/src/json.cpp +++ b/PiPedalCommon/src/json.cpp @@ -632,6 +632,60 @@ void json_reader::throw_format_error(const char*error) } +static std::string timePointToISO8601(const std::chrono::system_clock::time_point &tp) +{ + auto tt = std::chrono::system_clock::to_time_t(tp); + std::tm tm = *std::gmtime(&tt); + std::stringstream ss; + ss << std::put_time(&tm,"%Y-%m-%dT%H:%M:%S") << "Z"; + return ss.str(); + +} +static std::chrono::system_clock::time_point ISO8601ToTimePoint(const std::string&timeString) +{ + if (timeString.empty()) + { + return std::chrono::system_clock::time_point(); + } + if (timeString.back() != 'Z') + { + throw std::runtime_error("Time string is not in UTC Z timezone. Time-zones are not supported."); + } + + std::tm tm = {}; + std::istringstream ss(timeString); + ss >> std::get_time(&tm,"%Y-%m-%dT%H:%M:%S"); + if (ss.fail()) + { + throw std::runtime_error("Failed to parse ISO 8601 time string."); + } + time_t tt; + #ifdef _WIN32 + tt = _mkgmtime(&tm); + #else + tt = timegm(&tm); + #endif + if (tt == -1) + { + throw std::runtime_error("Failed to conver ISO 8601 time string."); + } + return std::chrono::system_clock::from_time_t(tt); +} + +void json_writer::write (const std::chrono::system_clock::time_point &time) +{ + std::string timeString = timePointToISO8601(time); + write(timeString); +} + +void json_reader::read(std::chrono::system_clock::time_point *value) +{ + std::string timeString; + read(&timeString); + *value = ISO8601ToTimePoint(timeString); +} + + // void json_writer::write(const json_variant &value) // { diff --git a/PiPedalCommon/src/util.cpp b/PiPedalCommon/src/util.cpp index 4247a7b..6fbc1a9 100644 --- a/PiPedalCommon/src/util.cpp +++ b/PiPedalCommon/src/util.cpp @@ -100,3 +100,15 @@ std::u32string pipedal::ToUtf32(const std::string &s) } return result.str(); } + + +std::string pipedal::GetHostName() +{ + char buffer[1024]; + if (gethostname(buffer,1024) != 0) + { + buffer[0] = '\0'; + } + buffer[1023] = '\0'; + return buffer; +} diff --git a/README.md b/README.md index 00f5dc5..c22ec8e 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ -Download: v1.2.47 +Download: v1.2.51 Website: [https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal). Documentation: [https://rerdavies.github.io/pipedal/Documentation.html](https://rerdavies.github.io/pipedal/Documentation.html).   -#### NEW version 1.2.47 Release, providing support for Raspberry Pi OS Bookworm. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. +#### NEW version 1.2.51 Release, providing support for Raspberry Pi OS Bookworm. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.   @@ -21,7 +21,7 @@ PiPedal running on a Raspberry Pi 4 or Pi 5 provides stable super-low-latency au PiPedal's user interface has been specifically designed to work well on small form-factor touch devices like phones or tablets. Clip a phone or tablet on your microphone stand on stage, and you're ready to play! Or connect via a desktop browser, for a slightly more luxurious experience. The PiPedal user-interface adapts to the screen size and orientation of your device, providing easy control of your guitar effects across a broad variety devices and screen sizes. -Install the [PiPedal Remote Android app](https://play.google.com/store/apps/details?id=com.twoplay.pipedal) to get one-click access to PiPedal via Wi-Fi Direct. Or connect from PC or Laptop via a web browser. PiPedal provides a simple configuration tool that allows you to set up the Wi-Fi Direct access point on your Raspberry Pi. +Install the [PiPedal Remote Android app](https://play.google.com/store/apps/details?id=com.twoplay.pipedal) to get one-click access to PiPedal via Wi-Fi or via the PiPedal Wi-Fi hotspot. Or connect from PC or Laptop via a web browser. PiPedal provides a simple configuration tool that allows you to set up the PiPedal's Wi-Fi hotspot access point on your Raspberry Pi. PiPedal includes a pre-installed selection of LV2 plugins from the ToobAmp collection of plugins; but it works with most LV2 Audio plugins. There are literally hundreds of free high-quality LV2 audio plugins that will work with PiPedal. Just install them on your Raspberry Pi, and they will show up in PiPedal. diff --git a/config/updatekey.gpg b/config/updatekey.asc similarity index 100% rename from config/updatekey.gpg rename to config/updatekey.asc diff --git a/config/updatekey2.asc b/config/updatekey2.asc new file mode 100644 index 0000000..593c6f2 Binary files /dev/null and b/config/updatekey2.asc differ diff --git a/docs/Configuring.md b/docs/Configuring.md index a36239c..cd10a24 100644 --- a/docs/Configuring.md +++ b/docs/Configuring.md @@ -10,7 +10,7 @@ Before using PiPedal, you will need to configure settings for the audio device t {% include pageIconL.html %} -You will also want to configure PiPedal to provide a Wi-Fi Direct access point (hotspot) that you can connect to using your phone. It's fine to use your home Wi-Fi network to connect to PiPedal when you're at home; but don't forget that when you take PiPedal out to a gig, you will need to ensure that the Wi-Fi Direct access point is enabled before you do. +You will also want to configure PiPedal to provide a Wi-Fi auto-hotspot so that you can connect to using your using your Android phone or tablet. It's fine to use your home Wi-Fi network to connect to PiPedal when you're at home; but don't forget that when you take PiPedal out to a gig, you will need to ensure that PiPedal's Wi-Fi auto-hotspot is enabled before you do. PiPedal uses LV2 audio plugins. There are literally thousands of freely available high-quality LV2 plugins that are suitable for use as guitar effects. @@ -21,8 +21,31 @@ with PiPedal. ### Installing the PiPedal Remote Android app -Install [PiPedal Remote](https://play.google.com/store/apps/details?id=com.twoplay.pipedal) on your Android device. PiPepedal Remote can connect to -PiPedal on your Raspberry Pi device as long as your Android device and Raspberry Pi are connected to the same network via a normal Wi-Fi access point, or via an Ethernet connection. +If you are using an Android phone or table to connect to PiPedal, you should download and install the [PiPedal Android Client](https://play.google.com/store/apps/details?id=com.twoplay.pipedal) + +The Android PiPedal client provides three feature: + +1. It automatically locates running instances of PiPedal on the local Wi-Fi network using dns/Bonjour discovery. So you don't need to keep track of which IP address to connect with. + +2. It provides a web view of PiPedal user interface with none of the clutter associated with a web browser. No browser address bars; no additional window clutter from the browser. Just a view of the PiPedal UI. So you get to use every precious pixel of your display to control PiPedal. + +3. It happens to interoperate nicely with PiPedal's auto-hotspot feature, allowing one-click connection to PiPedal even when you're away from home and don't have access to a Wi-Fi router. + +In theory, you could do all of this using Chrome, running on your phone. And, in fact, you can. But it's a fiddly procedure. The Android PiPedal Client takes care of all of those details, and allows you to connect to PiPedal from your phone or tablet with just one click to launch the app. + +The PiPedal client will connect to the PiPedal server running on your Raspberry Pi automatically if they are both connected to the same Wi-Fi router. When you are away from home, you should configure PiPedal to automatically bring up a Wi-Fi hotspot when the Raspberry Pi is not able to connect to your home network. + +When you do, you can easily connect to PiPedal via the Android app. + +- You show up at a local venue to perform. + +- You power up your Raspberry pi. The PiPedal server boots up, and decides that it is not able to connect to your home network, so it starts up a Wi-Fi hotspot. + +- Your phone or tablet doesn't have a Wi-Fi connection, because you're not at home. But when it sees the PiPedal hotspot it will automatically connect. + +- You launch the PiPedal client on your Android device. It searches the network associated with the current active Wi-Fi connection (the PiPedal hotspot, in this instance), and finds a PiPedal server announcing itself via mDNS/Bonjour. And having located the pipedal server, it automatically opens up a Web View and connects to the PiPedal web user interface. + +None of this is particularly clever. But it does make things simple. ## First Connection @@ -30,14 +53,16 @@ You can complete the initial configuration procedure using any of the following 1. _Log on to the Raspberry Pi device_. Launch a web browser, and connect to http:://127.0.0.1/ -2. _Connect Raspberry Pi to a Wi-Fi or Ethernet network, and connect from your laptop or desktop._ Connect to http://raspberrypi.local/ (substitute the host name of your Raspberry Pi if you have changed it from the default). +2. _Connect your Raspberry Pi to a Wi-Fi or Ethernet network, and connect from your laptop or desktop._ Connect to http://raspberrypi/ (substitute the host name of your Raspberry Pi if you have changed it from the default). 3. _From your Android device._ Install PiPedal Remote on your Android device. Connect your Raspberry Pi to your home router using a Wi-Fi or Ethernet connection. Launch PiPedal Remote on your Android device. -4. _From a LINUX command prompt._ As a last resort, you can configure the Wi-Fi Direct access point using the `pipedalconfig` program. Type `pipedalconfig --help` at a command prompt on your Raspberry Pi. +4. _From a LINUX command prompt._ As a last resort, you can configure the Wi-Fi hotspotusing the `pipedalconfig` program. Type `pipedalconfig --help` at a command prompt on your Raspberry Pi. If you already have another web server on port 80, see [*How to Change the Web Server Port*](ChangingTheWebServerPort.md). +When you connect to PiPedal for the first time, you will be presented with an Onboarding dialog, which allows you to complete the setup of things that need to be configured before you use PiPedal. From the Onboarding dialog, you can select and configure the audio device, and (optionally) configure Pipedal's Wi-Fi auto hotspot. + ### Configuring Audio Once connected, select the Settings menu item on the Hamburger menu at the top left corner of the display. Click on Audio Device Settings to select and configure the audio device you want to use. @@ -70,47 +95,76 @@ LINUX Kernel version 5.16 includes fixes to ALSA audio that are supposed to dram PiPedal uses the ALSA audio stack; so (unlike Jack Audio) there is no performance penalty for using 44100Hz sample rates. However, using a 48000Hz sampling rates does provide significant improvements in high-frequency audio quality when performing digital audio signal processing. -### Activating the P2P (Wi-Fi Direct) Hotspot +### Activating the Wi-Fi Auto-Hotspot -The preferred way to connect to a PiPedal host device is via Wi-Fi Direct connections. Doing so allows you to use your PiPedal device when you are away from home. - -Enable PiPedal's Wi-Fi Direct hotspot connection from the Settings dialog (Click on the hamburger icon; click on *Settings*; click on *Wi-Fi Direct Hotspot*). You will need to select a PIN and enable the connection before you can use it. The PiPedal hotspot will be visible to anyone with Wi-Fi distance; so choose your PIN carefully. Do NOT use trivial PINs like 12345678, or 00000000! Note that when connecting via Wi-Fi direct, you only have to enter the PIN once on the device you are connecting from; and it will be remembered automatically for subsequent connections. +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. -Best practice would be to generate a random PIN (using the Generate Random Pin button in the setup dialog), and then tape a label to the bottom of your Raspberry PI -(not the top) with the PIN written on it, in case you ever need to connect to PiPedal with a device that hasn't been previously set up. - -Wi-Fi Direct connections differ in a couple of ways from normal Wi-Fi connections - -- You only have to enter the pin once; the device you connect from will remember the PIN and connect without a PIN after that. +Raspberry Pi devices 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. +How you configure PiPedal's auto-hostpot depends on how you normally connect to your Raspberry Pi when you are at home. -- You can have a simultaneous connection to your Wi-Fi router when using a Wi-Fi Direct connection to your Raspberry Pi. The device you - are connecting from will continue use the Internet over your Wi-Fi router, and only use the Wi-Fi Direct connection to communication - with your Raspberry Pi. +To configure Pipedal's Auto-Hotspot, open the Auto-Hotspot configuration dialog. You can access this dialog directly from the Onboarding page; or if +you have completed the onboarding procedure, you can open the dialog from the Auto Hotspot item in the PiPedal Settings dialog. -- You can use a Wi-Fi Direct connection even when you don't have a Wi-Fi router connection. Android phones will continue to communicate - with the Internet over their - data connections, even when a Wi-Fi Direct connection is active. +The Auto-Hotspot offers you several choices as to how and PiPedal should open up a Wi-Fi hotspot. Which option you choose depends on how you connect to +PiPedal when you are at home. -- More than one device can connect to the Wi-Fi Direct connection on the Raspberry Pi when it is active/ +If you normally connect to your Raspberry Pi using an ethernet connection, starting the Wi-Fi hotspot when there is 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. -- Your Raspberry Pi can also have simultaneous access to a Wi-Fi router access point when the Wi-Fi Direct connection is enabled. +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. -- Wi-Fi Direct connections are backward compatible with Wi-Fi access points if you are using an older device. Look for the DIRECT-xx-YourDeviceName access point. - But if you use a legacy connection, the connecting device cannot have a simultanous Wi-Fi router connection. +If there are multiple locations, and multiple Wi-Fi routers you use with PiPedal on a regular basis, you can choose to start the +PiPedal Wi-Fi hotspot when No remembered Wi-Fi connections are visible, 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. -If you use the PiPedal Android app, the Android app will manage discovery and setting up of Wi-Fi direct connections to your Raspberry Pi device automatically; but you must complete the initial configuration using a direct web connection first. - -PC support for Wi-Fi Direct connections varies dramatically. Modern PCs support Wi-Fi Direct; but you may find it easier to communicate with PiPedal via your Wi-Fi router. Configure PiPedal to use a Wi-Fi connection as well as a Wi-Fi Direct access point (or connect an ethernet cable to your Raspberry Pi). Just remember to configure your phone to use a Wi-Fi Diret connection _before_ you take PiPedal out on a gig, because you won't have a Wi-Fi router then. +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. -PiPedal advertises the address of the Raspberry Pi host machine via Multi-cast DNS (M-DNS). You should be able to establish web connections to the Raspberry Pi using the address `http://.local/`, no matter how you are connected. - -Older devices may not support Wi-Fi Direct connections; but they will be able to use the Wi-Fi Direct connection as an ordinary Wi-Fi Hotspot. But they won't have access to the Internet over your Wi-Fi router while they have a connection with your Raspberry Pi. +### Connecting to PiPedal Using a Laptop when you are Away From Home -You _must_ select the correct country when setting up your Wi-Fi Direct connection. Regulations for use of Wi-Fi vary greatly from coutry to country, and define both the channels you are allowed to use, and the features and signal strength of Wi-Fi connections on those channels. +If your laptop has Wi-Fi support, you can use a laptop to connect to Raspberry Pi when you are away from home. + +When you are at home, connect both the Raspberry Pi and your Laptop to the same Wi-Fi router. Out of the box, Raspberry Pi OS is +configured to announce itself via mDNS/Bonjour, and Window and Mac operating systems can both use mDNS/Bonjour name resolution. + +Simply launch + + http://raspberrypi + +(or supply the actual hostname of your Raspberry Pi if you have changed the default hostname). + +When you are away from home, you can use Wi-Fi hotspots to connect your laptop and your Raspberry Pi device. You can either configure +your laptop to provide a Wi-Fi hotspot, and then get your Raspberry Pi to connect to the hotspot on your laptop. Raspberry Pi OS will +automatically connect to your laptop hotspot whenever it sees it, after you have connected for the first time, and entered login +credentials on your Raspberry Pi. After the first time, all you need to do is turn on your laptop hotspot, and Raspberry Pi OS will +connect to it. + +Usually, you cannect from your laptop using the same web address: http://raspberrpi (or the hostname of your raspberry pi, if you have +changed it). Unlike Android phones (where mDNS name resolution doesn't work on Android-hosted hotspots), mDNS/Bonjour name resolution usually +works on laptop-hosted hotspots when using Windows or Mac OS. If are running Linux on your laptop, you may need to install and configure the +Avahi package to get mDNS/Bonjour name resolution to work. + +And if that doesn't work, you can configure PiPedal to launch an auto-hotspot, and then connect from your laptop to the PiPedal hotspot on your Raspberry Pi. +When the Raspberry Pi hosts the hotspot, mDNS discovery is definitely enabled; so you should be able to connect using http://raspberrypi. But if that doesn't work, PiPedal's IP address will always be 10.40.1, when the PiPedal Wi-Fi hotspot is running, so you can always connect using http://10.40.0.1. -For best results, you should select Wi-Fi channel 1, 6 or 11 (referred to as the "Wi-Fi Direct Social Channels"). Doing so reduces the time it takes for other devices to discover the Raspberry Pi. While it is possible to use 5Ghz channels for Wi-Fi Direct, it may take some time for connecting devices to find your PiPedal device. -Support for Apple/IOS devices: a client for Apple/IoS devices is in long-term development plans; but I don't own any Apple device on which to do development and testing. If you'd like to see an Apple/IOS client, your sponsorship would help. (The client performs automatic discovery and set up of Wi-Fi direct connections, and relies on the Web interface after that. Not difficult to implement. Just expensive.) -------- [<< Installing PiPedal](Installing.md) | [Up](Documentation.md) | | [Choosing a USB Audio Adapter >>](ChoosingAUsbAudioAdapter.md) diff --git a/docs/Debugging.md b/docs/Debugging.md index 623684b..60bd66f 100644 --- a/docs/Debugging.md +++ b/docs/Debugging.md @@ -68,8 +68,9 @@ The React app will display the message "Error: Failed to connect to the server", ... } -Setting socket_server_address to "*" configures the web app to reconnect using the host address the browser -request used to connect to the web app. (e.g. 127.0.0.1, pipedal.local, the address of the Wi-Fi Direct connection &c). If you choose to provide an explicit address, remember that it is to that address that the web browser will connect. +Setting socket_server_address to "*" configures the web app to using the host address of the current +web page. If you set it to a specific address, the web app will attempt to establish a websocket connection +son that specific address instead. (Not sure what that's useful for, but it's there if you ever need it). The original development for this app was done with Visual Studio Code. Open the root project directory in Visual Studio Code, and it will detect the CMake build files, and configure itself appropriately. Wait for diff --git a/docs/Installing.md b/docs/Installing.md index 6e3b6e2..2dfe4b0 100644 --- a/docs/Installing.md +++ b/docs/Installing.md @@ -13,17 +13,17 @@ page_icon: img/Install4.jpg Download the most recent Debian (.deb) package for your platform: -- [Raspberry Pi OS bookworm (64-bit) v1.2.47](https://github.com/rerdavies/pipedal/releases/download/) +- [Raspberry Pi OS bookworm (64-bit) v1.2.51](https://github.com/rerdavies/pipedal/releases/download/) - [Ubuntu/Raspberry Pi OS bullseyeye (64-bit) v1.2.31](https://github.com/rerdavies/pipedal/releases/download/v1.1.31/pipedal_1.1.31_arm64.deb) -Version 1.2.47 has not yet been tested on Ubuntu or Raspberry Pi OS bullseye. On these platforms, we recommend that you use version 1.1.31. +Version 1.2.51 has not yet been tested on Ubuntu or Raspberry Pi OS bullseye. On these platforms, we recommend that you use version 1.1.31. Install the package by running ``` sudo apt update cd ~/Downloads - sudo apt-get install pipedal_1.2.47_arm64.deb + sudo apt-get install pipedal_1.2.51_arm64.deb ``` Adjust accordingly if you have downloaded v1.1.31. diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index d97743c..d33b3d5 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,4 +1,39 @@ # Release Notes +## PiPedal 1.2.51 Release + +Important notice: Changes to the Auto-update code prevent previous versions from automatically updating to this version. You will have to install this version by downloading the [debian package](https://rerdavies.github.io/pipedal/download.html) and installing it manually. + +This release replaces Wi-Fi Direct connections with Wi-Fi hotspots. Support for Wi-Fi Direct on Linux and Android has been fragile for some time. An update to Raspberry Pi OS in early September broke Wi-Fi Direct support completely. As it turns out, Auto-hotspots work much better. + +The new Auto-Hostpot feature in PiPedal allows you to configure your Raspberry Pi so it automatically starts a Wi-Fi hotspot whenever you are away from home. The updated Android PiPedal Client will automatically detect and connect to your Raspberry PI whenever it is visible on the current Wi-Fi network. An updated Android [PiPedal Remote](https://play.google.com/store/apps/details?id=com.twoplay.pipedal&hl=en_US&pli=1) app has been posted on Google Play. Make sure you are using the updated version. + +New MIDI system bindings allow you to enable or disable the Wi-Fi hotspot, and to shut down or reboot your Raspberry Pi using MIDI-triggered events. + +NOTICE: PiPedal 1.2.47 fixed a significant and dangerous defect that may cause loss of presets, banks or configuration data if you remove power from your Raspberry Pi within up to five minutes of last saving data instead of performing an orderly shutdown or reboot. Users of PiPedal should upgrade to at least version 1.2.47 immediately, if they have not already done so. + + +Bug fixes: +- PiPedal Remote client theme native pages do not match the selected PiPedal UI theme. +- Unable to configure audio buffers properly for devices with minBufferSize=256. +- Release and Beta updates not visible if the most recent release is a development build. +- Revised auto-update procedure, including a new signing procedure. +- Ongoing improvements to TooB Neural Amp Modeler (bug fixes, performance improvements) +- pipedald service hangs and/or throws exceptions on shutdown. +- Update DNS/SD service announcements when the device name is changed. +- Unannounce DNS/DS services when the pipedal services shuts down. +- Auto-uprade checks may exceed the throttling rate of Github API calls when checking for updates. +- Memory corruption when ALSA device creation fails. +- MIDI input causing audio glitches and underruns. +- Unable to connect when using IPv6 connections and mDNS name resolution. +- Default web port configuration overwritten by upgrades. +- Remove dependencies on old pre-NetworkManager network services required by Wifi-Direct code. +- Refuse to install on versions of Raspberry Pi OS prior to bookworm. + +Known issues: +- TooB Neural Amp Modeler runs 5% slower than Mike Oliphant's Neural Amp Modeler, despite running virtually identical code from Steven Atkins' NeuralAmpModelerCore project. Investigation continues. + + + ## PiPedal 1.2.47 Release This version fixes a significant and dangerous defect that may cause loss of presets, banks or configuration data if you remove power from your Raspberry Pi within up to five minutes of last saving data instead of performing an orderly shutdown or reboot. Users of PiPedal should upgrade immediately. diff --git a/docs/download.md b/docs/download.md index 96df911..a11c636 100644 --- a/docs/download.md +++ b/docs/download.md @@ -4,17 +4,15 @@ Download the most recent Debian (.deb) package for your platform: -- Raspberry Pi OS Bookworm (64-bit) v1.2.47 -- Ubuntu 21.04 or Raspberry Pi OS bullseyeyeye (64-bit) v1.1.31 +- Raspberry Pi OS Bookworm (64-bit) v1.2.51 -v1.2.47 is does not currently support Ubuntu 21.04, or older versions of Raspberry Pi OS. Install the package by running ``` sudo apt update cd ~/Downloads - sudo apt-get install ./pipedal_1.2.47_arm64.deb + sudo apt-get install ./pipedal_1.2.51_arm64.deb ``` Follow the instructions in [_Configuring PiPedal After Installation_](https://rerdavies.github.io/pipedal/Configuring.html) to complete the installation. diff --git a/docs/index.md b/docs/index.md index 32d6163..2d77dea 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,7 @@ -v1.2.47 +v1.2.51   @@ -9,7 +9,7 @@ To download PiPedal, click [here](download.md). To view PiPedal documentation, click [here](Documentation.md). -#### NEW version 1.2.47 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. +#### NEW version 1.2.51 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.   @@ -18,9 +18,11 @@ PiPedal running on a Raspberry Pi 4 or Pi 5 provides stable super-low-latency au {% include gallery.html %} -PiPedal can be remotely controlled via a web interface over Ethernet, or from your phone or tablet via Wi-Fi Direct. Set up and configure the Wi-Fi Direct interface via the browser interface. +PiPedal can be remotely controlled via a web interface over Ethernet, or Wi-Fi. If you don't have access to a Wi-Fi router, PiPedal can be configured to +start a Wi-Fi hotspot automatically, whenever your Raspberry Pi can't connect to your home network. -Install the [PiPedal Remote Android app](https://play.google.com/store/apps/details?id=com.twoplay.pipedal) to get one-click access to PiPedal via Wi-Fi Direct. Or connect from PC, Laptop or your Raspberry Pi via a web browser over Ethernet. You can set up and configure the Wi-Fi Direct connection from the browser interface. +Install the [PiPedal Remote Android app](https://play.google.com/store/apps/details?id=com.twoplay.pipedal) to get one-click access to PiPedal via Wi-Fi networks, or Wi-Fi hotspots. If you are using PiPedal away from home, you can configure PiPedal to automatically start a Wi-Fi hotspot on your Raspberry Pi +whenever Pipedal is unable to detect your home network. The PiPedal Client Android app will allow to connect by simply launching the app, whether you are at home, or using a Wi-Fi auto-hotspot at a gig, when away from home. PiPedal's user interface has been specifically designed to work well on small form-factor touch devices like phones or tablets. Clip a phone or tablet on your microphone stand on stage, and you're ready to play! Or connect via a desktop browser, for a slightly more luxurious experience. The PiPedal user-interface adapts to the screen size and orientation of your device, providing easy control of your guitar effects across a broad variety devices and screen sizes. diff --git a/lv2/aarch64/ToobAmp.lv2/PowerStage2l.ttl b/lv2/aarch64/ToobAmp.lv2/PowerStage2l.ttl new file mode 100644 index 0000000..c50570a --- /dev/null +++ b/lv2/aarch64/ToobAmp.lv2/PowerStage2l.ttl @@ -0,0 +1,594 @@ +@prefix doap: . +@prefix lv2: . +@prefix rdf: . +@prefix rdfs: . +@prefix units: . +@prefix urid: . +@prefix atom: . +@prefix midi: . +@prefix epp: . +@prefix uiext: . +@prefix idpy: . +@prefix foaf: . +@prefix mod: . +@prefix param: . +@prefix work: . +@prefix pg: . + +@prefix atom: . +@prefix patch: . +@prefix plug: . +@prefix rdfs: . +@prefix state: . +@prefix urid: . +@prefix xsd: . +@prefix modgui: . +@prefix pstage: . + + +pstage:stage1 + a param:ControlGroup , + pg:InputGroup ; + lv2:name "Stage 1" ; + lv2:symbol "stage1" . + +pstage:stage2 + a param:ControlGroup , + pg:InputGroup ; + lv2:name "Stage 2" ; + lv2:symbol "stage2" . + + +pstage:stage3 + a param:ControlGroup , + pg:InputGroup ; + lv2:name "Stage 3" ; + lv2:symbol "stage3" . + + + + a lv2:Parameter ; + rdfs:label "uiState" ; + rdfs:range atom:Vector . + + + + a foaf:Person ; + foaf:name "Robin Davies" ; + foaf:mbox ; + foaf:homepage . + + + a lv2:Plugin , + lv2:SimulatorPlugin ; + + doap:name "TooB Power Stage"; + + doap:license ; + doap:maintainer ; + lv2:minorVersion 0 ; + lv2:microVersion 47 ; + + mod:brand "TooB"; + mod:label "Power Stage"; + lv2:requiredFeature urid:map ; + lv2:optionalFeature lv2:hardRTCapable; + patch:readable + ; + +rdfs:comment """ +The TooB Power Stage provides flexible and nuanced emulations of guitar amp power stages. +The goal of TooB Power Stage is emulate the best features of real tube amps without slavishly +reproducing the undesirable features as well. + +TooB Power Stage provides 3 consecutive gain stages, each of which can be overdriven to varying +degrees. Power transformer sag is emulated using the SAG and SAGD controls, which emulate the +volume and distortion effects introduced when power transformer voltage sags. + +Each gain section expects an input level that is nominally in the range of 12db to 0db. +It's ok if the levels are slightly over 0db, but they should not be massively over. Use the +Trim control to adjust input signal levels. + +The Gain controls use oversampled atan wave shaping to emulate the effect of overdriving +gain stages. Atan wave shaping provides rich even-order harmonics similar to those produced by a tube +amp power stage. A modest amount of gain will produce the characteristically "warm" sound of a tube +gain stage. More extreme settings produce harder overdrive. + +Judicious use of the LO CUT and HI CUT controls have interesting and subtle effects on the overall sound. +You are encouraged to play with these controls to see what happens. Tightening up low and high frequencies, +in early gain sections can allow you to drive the signal much harder in later gain sections without totally +breaking up. Real amps will usually cut low frequency response at somewhere between 80Hz (Fender) +and 140Hz (Marshall). + +General experience seems to suggest that gradually increasing the gain in each section allows you to drive +the signal harder without introducing undesirable effects in the output. However, there definitely interesting +and subtle differences in tone that can be acheived by not adhering to that rule of thumb. + +The SAG and SAGD controls the emulation of "forgiveness", and "compression" that are provided in a real tube +amp by sagging of power supply voltage. SAG controls how much +the overall volume of the output decreases as the transform sags. It provides the subtle "compression" that +tube amps provide, which is quite different from the compression provided by compressors. SAGD controls +the amount of additional distortion that occurs as the gain stages receive reduced voltage. It provides +the "forgiveness" of a good tube amp. Because SAGD operates in a feedback loop that runs through +all three gain stages. Increasing the distortion in the gain stages increases the power use of the gain stages; +increasing the power use of the gain stages increases the effect of SAG and SAGD which has an opposing effect +on power use. The net result is that SAGD causes signals to break up more gradually +(hence the perceived "forgiveness"). The effects are non-linear, so not easy to describe completely. You +are strongly urged to experiment with SAG and SAGD settings in order to get best tone results. Use very +moderate settings to emulate tube amp output; higher settings of SAG and SAGD will cause audible compression +and pumping of the outputs which may or may not be desireable. + +The TooB Power Stage design is based on an experimental design, that unexpectedly produced extraordinarily +good amp tones. We have not completely explored the sonic possibilities of the +TooB Power Stage. What's known so far: it produces exceptionally pretty clean tones; it can be driven really +hard, and seems to preserve an unusual amount of detail in the signal even when being driven hard; and it +can definitely produce signals that have many of the most prized features of tube amplifiers: warmth, detail, +subtle "compression" without coloration, and forgiving break up. + +Explore! + +The TooB Input Stage is part of the TooB Amp Emulation Toolkit. Typically, you would +use the following plugins in the following order. + +- TooB Input Stage + +- TooB Tone Stack + +- Toob Power Stage + +- Toob Cabinet Simulator +"""; + lv2:port + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 0 ; + pg:group pstage:stage1 ; + lv2:symbol "trim1" ; + lv2:name "Trim"; + lv2:default 0.0 ; + lv2:minimum -20.0 ; + lv2:maximum 20.0 ; + units:unit units:db ; + rdfs:comment "Trim input level (stage 1)" ; + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 1 ; + pg:group pstage:stage1 ; + lv2:symbol "locut1" ; + lv2:name "Lo Cut"; + lv2:default 30.0 ; + lv2:minimum 30.0 ; + lv2:maximum 300.0 ; + units:unit units:hz; + lv2:designation param:cutoffFrequency ; + + lv2:scalePoint [ + rdfs:label "OFF" ; + rdf:value 30.0 + ]; + rdfs:comment "Low frequency cutoff (stage 1). Set to minimum to disable." ; + + ], [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 2 ; + pg:group pstage:stage1 ; + lv2:symbol "hicut1" ; + lv2:name "Hi Cut"; + lv2:default 19000.0 ; + lv2:minimum 1000.0 ; + lv2:maximum 19000.0; + units:unit units:hz; + lv2:designation param:cutoffFrequency ; + lv2:scalePoint [ + rdfs:label "OFF" ; + rdf:value 19000.0 + ]; + rdfs:comment "High cutoff frequency (stage 1). Set to maximum to disable." ; + + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + pg:group pstage:stage1 ; + + lv2:index 3 ; + lv2:symbol "shape1" ; + lv2:name "Shape1"; + lv2:default 0.0 ; + lv2:minimum 0.0 ; + lv2:maximum 1.0; + lv2:rangeStep 2; + + lv2:portProperty lv2:enumeration ; + rdfs:comment "Wave Shape" ; + lv2:scalePoint [ + rdfs:label "atan" ; + rdf:value 0.0 + ], + [ + rdfs:label "triode" ; + rdf:value 1.0 + ]; + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 4 ; + pg:group pstage:stage1 ; + lv2:symbol "gain1" ; + lv2:name "Gain"; + lv2:default 0.0 ; + lv2:minimum 0.0 ; + lv2:maximum 1.0 ; + rdfs:comment "Gain (stage 1)" + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + pg:group pstage:stage1 ; + + lv2:index 5 ; + lv2:symbol "bias1" ; + lv2:name "Bias"; + lv2:default 0.0 ; + lv2:minimum -2.0 ; + lv2:maximum 2.0; + rdfs:comment "Waveshape bias (stage 1)" + ], + + + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 6 ; + pg:group pstage:stage2 ; + lv2:symbol "trim2" ; + lv2:name "Trim"; + lv2:default 0.0 ; + lv2:minimum -20.0 ; + lv2:maximum 20.0 ; + units:unit units:db ; + rdfs:comment "Trim input (stage 2)" ; + ], + + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 7 ; + pg:group pstage:stage2 ; + lv2:symbol "locut2" ; + lv2:name "Lo Cut"; + lv2:default 30.0 ; + lv2:minimum 30.0 ; + lv2:maximum 300.0 ; + units:unit units:hz; + lv2:designation param:cutoffFrequency ; + + lv2:scalePoint [ + rdfs:label "OFF" ; + rdf:value 30.0 + ] + ; + rdfs:comment "Low frequency cutoff (stage 2). Set to minimum to disable." ; + + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 8 ; + pg:group pstage:stage2 ; + lv2:symbol "hicut2" ; + lv2:name "Hi Cut"; + lv2:default 19000.0 ; + lv2:minimum 1000.0 ; + lv2:maximum 19000.0; + units:unit units:hz; + lv2:designation param:cutoffFrequency ; + lv2:scalePoint [ + rdfs:label "OFF" ; + rdf:value 19000.0 + ]; + rdfs:comment "High cutoff frequency (stage 2). Set to maximum to disable." ; + + ], + + [ + a lv2:InputPort , + lv2:ControlPort ; + pg:group pstage:stage2 ; + + lv2:index 9 ; + lv2:symbol "shape2" ; + lv2:name "Shape2"; + lv2:default 0.0 ; + lv2:minimum 0.0 ; + lv2:maximum 1.0; + lv2:rangeStep 2; + + lv2:portProperty lv2:enumeration ; + rdfs:comment "Wave Shape" ; + lv2:scalePoint [ + rdfs:label "atan" ; + rdf:value 0.0 + ], + [ + rdfs:label "triode" ; + rdf:value 1.0 + ] + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 10 ; + pg:group pstage:stage2 ; + lv2:symbol "gain2" ; + lv2:name "Gain"; + lv2:default 0.0 ; + lv2:minimum 0.0 ; + lv2:maximum 1.0 ; + rdfs:comment "Gain (stage 2)" + ], + + [ + a lv2:InputPort , + lv2:ControlPort ; + pg:group pstage:stage2 ; + + lv2:index 11 ; + lv2:symbol "bias2" ; + lv2:name "Bias"; + lv2:default 0.0 ; + lv2:minimum -2.0 ; + lv2:maximum 2.0; + rdfs:comment "Waveshape bias (stage 2)" + + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 12 ; + pg:group pstage:stage2 ; + lv2:symbol "gain2_enable" ; + lv2:name "Enable2"; + lv2:default 1 ; + lv2:minimum 0 ; + lv2:maximum 1; + lv2:portProperty lv2:toggled ; + rdfs:comment "Enable gain stage 2" ; + ], + + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 13 ; + pg:group pstage:stage3 ; + lv2:symbol "trim3" ; + lv2:name "Trim"; + lv2:default 0.0 ; + lv2:minimum -20.0 ; + lv2:maximum 20.0 ; + units:unit units:db ; + rdfs:comment "Trim input level (stage 3)." ; + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 14 ; + pg:group pstage:stage3 ; + lv2:symbol "locut3" ; + lv2:name "Lo Cut"; + lv2:default 30.0 ; + lv2:minimum 30.0 ; + lv2:maximum 300.0 ; + units:unit units:hz; + lv2:designation param:cutoffFrequency ; + + lv2:scalePoint [ + rdfs:label "OFF" ; + rdf:value 30.0 + ]; + rdfs:comment "Low frequency cutoff (stage 3). Set to minimum to disable." ; + + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 15 ; + pg:group pstage:stage3 ; + lv2:symbol "hicut3" ; + lv2:name "Hi Cut"; + lv2:default 19000.0 ; + lv2:minimum 1000.0 ; + lv2:maximum 19000.0; + units:unit units:hz; + lv2:designation param:cutoffFrequency ; + lv2:scalePoint [ + rdfs:label "OFF" ; + rdf:value 19000 + ]; + rdfs:comment "High cutoff frequency (stage 3). Set to minimum to disable." ; + + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + pg:group pstage:stage3 ; + + lv2:index 16 ; + lv2:symbol "shape3" ; + lv2:name "Shape3"; + lv2:default 0.0 ; + lv2:minimum 0.0 ; + lv2:maximum 1.0; + lv2:rangeStep 2; + + lv2:portProperty lv2:enumeration ; + rdfs:comment "Wave Shape" ; + lv2:scalePoint [ + rdfs:label "atan" ; + rdf:value 0.0 + ], + [ + rdfs:label "triode" ; + rdf:value 1.0 + ] + ], + + + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 17 ; + pg:group pstage:stage3 ; + lv2:symbol "gain3" ; + lv2:name "Gain"; + lv2:default 0.0 ; + lv2:minimum 0.0 ; + lv2:maximum 1.0 ; + rdfs:comment "Gain (stage 3)." + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + pg:group pstage:stage3 ; + + lv2:index 18 ; + lv2:symbol "bias3" ; + lv2:name "Bias"; + lv2:default 0.0 ; + lv2:minimum -2.0 ; + lv2:maximum 2.0; + rdfs:comment "Waveshape bias (stage 3)" + + ], + + + + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 19 ; + pg:group pstage:stage3 ; + lv2:symbol "gain3_enable" ; + lv2:name "Enable3"; + lv2:default 1 ; + lv2:minimum 0 ; + lv2:maximum 1; + lv2:portProperty lv2:toggled ; + rdfs:comment "Enable gain stage 3" ; + ], + + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 20 ; + lv2:symbol "sag" ; + lv2:name "Sag Vol"; + lv2:default 0.0 ; + lv2:minimum 0.0 ; + lv2:maximum 1.0; + rdfs:comment "Transformer sag (volume)." ; + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 21 ; + lv2:symbol "sagd" ; + lv2:name "Sag Dist"; + lv2:default 0.0 ; + lv2:minimum 0.0 ; + lv2:maximum 1.0; + rdfs:comment "Transformer sag (distortion)." ; + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 22 ; + lv2:symbol "master" ; + lv2:name "Master"; + lv2:default -0.0 ; + lv2:minimum -60.0 ; + lv2:maximum 30.0; + units:unit units:db; + lv2:scalePoint [ + rdfs:label "-60db" ; + rdf:value -60.0 + ] , [ + rdfs:label "0db" ; + rdf:value 0.0 + ], [ + rdfs:label "30db" ; + rdf:value -30.0 + ]; + rdfs:comment "Master volume." ; + + ], + [ + a lv2:AudioPort , + lv2:InputPort ; + lv2:index 23 ; + lv2:symbol "in" ; + lv2:name "In" + ] , [ + a lv2:AudioPort , + lv2:OutputPort ; + lv2:index 24 ; + lv2:symbol "out" ; + lv2:name "Out" + ],[ + a atom:AtomPort , + lv2:InputPort; + atom:bufferType atom:Sequence ; + # atom:supports patch:Message; + lv2:designation lv2:control ; + lv2:index 25 ; + lv2:symbol "control" ; + lv2:name "Control" ; + rdfs:comment "Plugin to GUI communication" ; + ] , [ + a atom:AtomPort , + lv2:OutputPort ; + atom:bufferType atom:Sequence ; + # atom:supports patch:Message; + lv2:designation lv2:control ; + lv2:index 26 ; + lv2:symbol "notify" ; + lv2:name "Notify" ; + rdfs:comment "Plugin to GUI communication" ; + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 27 ; + lv2:symbol "sagf" ; + lv2:name "SagF"; + lv2:default 13.0 ; + lv2:minimum 5.0 ; + lv2:maximum 25.0; + rdfs:comment "Sag filter Fc." ; + ] + . + + diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so deleted file mode 120000 index 98ec90e..0000000 --- a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so +++ /dev/null @@ -1 +0,0 @@ -ToobAmp.so.0 \ No newline at end of file diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so new file mode 100644 index 0000000..9112fa4 Binary files /dev/null and b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so differ diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.0 b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.0 deleted file mode 120000 index 476f52d..0000000 --- a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.0 +++ /dev/null @@ -1 +0,0 @@ -ToobAmp.so.1.1.47 \ No newline at end of file diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.1.1.47 b/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.1.1.47 deleted file mode 100644 index ba1c373..0000000 Binary files a/lv2/aarch64/ToobAmp.lv2/ToobAmp.so.1.1.47 and /dev/null differ diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so b/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so index ac1cea1..71a3482 100644 Binary files a/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so and b/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so differ diff --git a/modules/websocketpp b/modules/websocketpp index b9aeec6..a13b985 160000 --- a/modules/websocketpp +++ b/modules/websocketpp @@ -1 +1 @@ -Subproject commit b9aeec6eaf3d5610503439b4fae3581d9aff08e8 +Subproject commit a13b985bb5972fb6480eca2e8f7d09eef8a3f00d diff --git a/profiler_tools/.gitignore b/profiler_tools/.gitignore new file mode 100644 index 0000000..81b9e9d --- /dev/null +++ b/profiler_tools/.gitignore @@ -0,0 +1,2 @@ +# profiler out files +*.txt diff --git a/profiler_tools/ToobNam_profile b/profiler_tools/ToobNam_profile index 7c9fef7..c616390 100755 --- a/profiler_tools/ToobNam_profile +++ b/profiler_tools/ToobNam_profile @@ -1,3 +1,3 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -${SCRIPT_DIR}/../build/src/profilePlugin -w ToobNam_Profile -o /tmp/ToobNam.perf &&\ -google-pprof --text ${SCRIPT_DIR}/../build/src/profilePlugin --add_lib /usr/lib/lv2/ToobAmp.lv2/ToobAmp.so /tmp/ToobNam.perf >./ToobNam.txt \ No newline at end of file +${SCRIPT_DIR}/../build/src/profilePlugin -s 100 -w ToobNam_Profile -o /tmp/ToobNam.perf &&\ +google-pprof --text ${SCRIPT_DIR}/../build/src/profilePlugin --add_lib /usr/lib/lv2/ToobAmp.lv2/ToobAmp.so /tmp/ToobNam.perf >./ToobNam.txt diff --git a/profiler_tools/ToobNam_profile_web b/profiler_tools/ToobNam_profile_web new file mode 100755 index 0000000..11671f6 --- /dev/null +++ b/profiler_tools/ToobNam_profile_web @@ -0,0 +1,3 @@ +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +${SCRIPT_DIR}/../build/src/profilePlugin -w ToobNam_Profile -o /tmp/ToobNam.perf &&\ +google-pprof --web ${SCRIPT_DIR}/../build/src/profilePlugin --add_lib /usr/lib/lv2/ToobAmp.lv2/ToobAmp.so /tmp/ToobNam.perf >./ToobNam.txt diff --git a/profiler_tools/nam_profile b/profiler_tools/nam_profile index 9654887..5f24cc1 100755 --- a/profiler_tools/nam_profile +++ b/profiler_tools/nam_profile @@ -1,3 +1,3 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -${SCRIPT_DIR}/../build/src/profilePlugin -w Nam_Profile -o /tmp/nam.perf &&\ -google-pprof --text ${SCRIPT_DIR}/../build/src/profilePlugin --add_lib /usr/lib/lv2/neural_amp_modeler.lv2/neural_amp_modeler.so /tmp/nam.perf >./nam.txt \ No newline at end of file +${SCRIPT_DIR}/../build/src/profilePlugin -s 100 -w Nam_Profile -o /tmp/nam.perf &&\ +google-pprof --text ${SCRIPT_DIR}/../build/src/profilePlugin --add_lib /usr/lib/lv2/neural_amp_modeler.lv2/neural_amp_modeler.so /tmp/nam.perf >./nam.txt diff --git a/profiler_tools/nam_profile_web b/profiler_tools/nam_profile_web new file mode 100755 index 0000000..70827d4 --- /dev/null +++ b/profiler_tools/nam_profile_web @@ -0,0 +1,3 @@ +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +${SCRIPT_DIR}/../build/src/profilePlugin -w Nam_Profile -o /tmp/nam.perf &&\ +google-pprof --web ${SCRIPT_DIR}/../build/src/profilePlugin --add_lib /usr/lib/lv2/neural_amp_modeler.lv2/neural_amp_modeler.so /tmp/nam.perf >./nam.txt diff --git a/provisionLv2 b/provisionLv2 index d09ee04..dc8b9ea 100755 --- a/provisionLv2 +++ b/provisionLv2 @@ -2,3 +2,4 @@ rm -rf ./lv2/aarch64/* cp -R /usr/lib/lv2/ToobAmp.lv2/ ./lv2/aarch64/ +strip ./lv2/aarch64/ToobAmp.lv2/*.so diff --git a/react/public/index.html b/react/public/index.html index 0e138f2..7383ce2 100644 --- a/react/public/index.html +++ b/react/public/index.html @@ -67,10 +67,9 @@ useSystem = true; break; } - if (useSystem) - { + if (useSystem) { darkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; - } + } if (!darkMode) { let bgStyle = document.getElementById("bgStyle"); @@ -79,6 +78,22 @@ bgStyle.setAttribute('media', "max-width: 1px"); } } + function removeHashOnLoad() { + if (window.location.hash) { + // Store the hash value (without the '#' symbol) + var hash = window.location.hash.substring(1); + + // Replace the current URL without the hash + var newUrl = window.location.href.replace(window.location.hash, ''); + + // Use HTML5 history API to change the URL without reloading the page + history.replaceState(null, document.title, newUrl); + + } + } + + // Run the function when the window loads + window.addEventListener('load', removeHashOnLoad); diff --git a/react/public/var/config.json b/react/public/var/config.json index d16ec84..5711fff 100644 --- a/react/public/var/config.json +++ b/react/public/var/config.json @@ -1,8 +1,9 @@ { - "socket_server_port": 81, + "socket_server_port": 8080, "socket_server_address": "*", "debug": true, "max_upload_size": 536870912, "fakeAndroid": false, - "ui_plugins": [] + "ui_plugins": [], + "enable_auto_update": true } \ No newline at end of file diff --git a/react/src/AboutDialog.tsx b/react/src/AboutDialog.tsx index db3120e..8a65e02 100644 --- a/react/src/AboutDialog.tsx +++ b/react/src/AboutDialog.tsx @@ -6,7 +6,7 @@ import { PiPedalModel, PiPedalModelFactory, State } from './PiPedalModel'; import AppBar from '@mui/material/AppBar'; import Toolbar from '@mui/material/Toolbar'; import Divider from '@mui/material/Divider'; -import ArrowBackIcon from '@mui/icons-material//ArrowBack'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import JackHostStatus from './JackHostStatus'; import { PiPedalError } from './PiPedalError'; @@ -16,12 +16,9 @@ import { Theme, createStyles } from '@mui/material/styles'; import { withStyles, WithStyles } from '@mui/styles'; import Slide, { SlideProps } from '@mui/material/Slide'; - interface AboutDialogProps extends WithStyles { open: boolean; onClose: () => void; - - }; interface AboutDialogState { @@ -76,6 +73,15 @@ const styles = (theme: Theme) => createStyles({ }, secondaryItem: { + }, + can_select: { + userSelect: "text", + cursor: "text" + }, + no_select: { + userSelect: "none", + cursor: "text" + } @@ -173,7 +179,7 @@ const AboutDialog = withStyles(styles, { withTheme: true })( this.updateNotifications(); } componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot: any): void { - super.componentDidUpdate?.(prevProps,prevState,snapshot); + super.componentDidUpdate?.(prevProps, prevState, snapshot); this.updateNotifications(); } @@ -185,6 +191,11 @@ const AboutDialog = withStyles(styles, { withTheme: true })( render() { let classes = this.props.classes; let addressKey = 0; + let serverVersion = this.model.serverVersion?.serverVersion ?? ""; + let nPos = serverVersion.indexOf(' '); + if (nPos !== -1) { + serverVersion = serverVersion.substring(nPos + 1); + } return ( { this.props.onClose() }} TransitionComponent={Transition} @@ -210,13 +221,16 @@ const AboutDialog = withStyles(styles, { withTheme: true })( overflowX: "hidden", overflowY: "auto", userSelect: "text" }} > -
- - PiPedal - {(this.model.serverVersion ? this.model.serverVersion.serverVersion : "") - + (this.model.serverVersion?.debug ? " (Debug)" : "")} - - +
+
+ + PiPedal + {serverVersion + + (this.model.serverVersion?.debug ? " (Debug)" : "")} + + + +
Copyright © 2022-2024 Robin Davies. @@ -235,24 +249,34 @@ const AboutDialog = withStyles(styles, { withTheme: true })( ADDRESSES -
- { - this.model.serverVersion?.webAddresses.map((address) => - ( - - {address} - - )) +
+ { + this.model.serverVersion?.webAddresses.map((address) => + ( + + {address} + + )) - } + }
+ + + SERVER OS + +
+ + {this.model.serverVersion?.osVersion ?? ""} + +
+
LEGAL NOTICES -
+
diff --git a/react/src/AndroidHost.tsx b/react/src/AndroidHost.tsx index bd93f1a..f62c931 100644 --- a/react/src/AndroidHost.tsx +++ b/react/src/AndroidHost.tsx @@ -29,6 +29,8 @@ export interface AndroidHostInterface { chooseNewDevice() : void; setDisconnected(isDisconnected: boolean): void; launchExternalUrl(url:string): boolean; + setThemePreference(theme: number): void; + getThemePreference(): number; }; export class FakeAndroidHost implements AndroidHostInterface @@ -51,4 +53,11 @@ export class FakeAndroidHost implements AndroidHostInterface { return false; } + private theme = 1; + setThemePreference(theme: number): void{ + this.theme = theme; + } + getThemePreference(): number { + return this.theme; + } } diff --git a/react/src/App.tsx b/react/src/App.tsx index 29746d1..8e9637a 100644 --- a/react/src/App.tsx +++ b/react/src/App.tsx @@ -56,6 +56,19 @@ const theme = createTheme( { components: { MuiButton: { + styleOverrides: { + containedPrimary: { + borderRadius: '9999px', + paddingLeft: "16px", paddingRight: "16px", + textTransform: "none" + }, + containedSecondary: { + borderRadius: '9999px', + paddingLeft: "16px", paddingRight: "16px", + textTransform: "none" + } + + }, variants: [ { props: { variant: 'dialogPrimary' }, @@ -89,6 +102,19 @@ const theme = createTheme( { components: { MuiButton: { + styleOverrides: { + containedPrimary: { + borderRadius: '9999px', + paddingLeft: "16px", paddingRight: "16px", + textTransform: "none" + }, + containedSecondary: { + borderRadius: '9999px', + paddingLeft: "16px", paddingRight: "16px", + textTransform: "none" + } + + }, variants: [ { props: { variant: 'dialogPrimary' }, diff --git a/react/src/AppThemed.tsx b/react/src/AppThemed.tsx index fc526dc..9a05c74 100644 --- a/react/src/AppThemed.tsx +++ b/react/src/AppThemed.tsx @@ -48,7 +48,7 @@ import SettingsDialog from './SettingsDialog'; import AboutDialog from './AboutDialog'; import BankDialog from './BankDialog'; -import { PiPedalModelFactory, PiPedalModel, State, ZoomedControlInfo,wantsLoadingScreen } from './PiPedalModel'; +import { PiPedalModelFactory, PiPedalModel, State, ZoomedControlInfo, wantsLoadingScreen } from './PiPedalModel'; import ZoomedUiControl from './ZoomedUiControl' import MainPage from './MainPage'; import DialogContent from '@mui/material/DialogContent'; @@ -67,7 +67,7 @@ import { ReactComponent as SaveBankAsIcon } from './svg/ic_save_bank_as.svg'; import { ReactComponent as EditBanksIcon } from './svg/ic_edit_banks.svg'; import { ReactComponent as SettingsIcon } from './svg/ic_settings.svg'; import { ReactComponent as HelpOutlineIcon } from './svg/ic_help_outline.svg'; -import DialogEx from './DialogEx'; +import DialogEx, { DialogStackState } from './DialogEx'; const appStyles = (theme: Theme) => createStyles({ @@ -139,10 +139,11 @@ const appStyles = (theme: Theme) => createStyles({ marginTop: 0, fontWeight: 500, fontSize: "13pt", - maxWidth: 350, opacity: 1, zIndex: 2010, paddingTop: 12, + gravity: "center", + textAlign: "center" }, errorMessageBox: { @@ -163,10 +164,7 @@ const appStyles = (theme: Theme) => createStyles({ loadingBox: { position: "relative", top: "20%", - width: "240px", color: isDarkMode() ? theme.palette.text.secondary : "#888", - marginLeft: "auto", - marginRight: "auto", // border: "3px solid #888", borderRadius: "12px", padding: "12px", @@ -352,9 +350,40 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< this.handleCloseAlert = this.handleCloseAlert.bind(this); this.banksChangedHandler = this.banksChangedHandler.bind(this); this.showStatusMonitorHandler = this.showStatusMonitorHandler.bind(this); - + this.onPopStateHandler = this.onPopStateHandler.bind(this); } + showDrawer() { + if (!this.state.isDrawerOpen) { + this.setState({ isDrawerOpen: true }) + this.pushOpenMenuState(); + } + } + hideDrawer(loadingDialog: boolean = false) { + if (!loadingDialog && window.location.hash === "#menu") { + window.history.back(); + } else { + this.setState({ isDrawerOpen: false }) + } + } + + pushOpenMenuState() { + let stackState = { tag: "OpenMenu", previousState: null }; + window.history.replaceState(stackState, "", window.location.href); + window.history.pushState({}, "", "#menu"); + } + onPopStateHandler(ev: PopStateEvent) { + let stackState: DialogStackState | null = ev.state as (DialogStackState | null); + if (stackState && stackState.tag === "OpenMenu") { + if (this.state.isDrawerOpen) { + this.setState({ isDrawerOpen: false }); + } + ev.stopPropagation(); + window.history.replaceState(stackState.previousState, "", window.location.href); + return true; + } + return false; + } onOpenBank(bankId: number) { this.model_.openBank(bankId) @@ -525,9 +554,10 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< return undefined; } promptForUpdateHandler(newValue: boolean) { - if (this.state.updateDialogOpen !== newValue) - { - this.setState({updateDialogOpen: newValue}); + if (this.model_.enableAutoUpdate) { + if (this.state.updateDialogOpen !== newValue) { + this.setState({ updateDialogOpen: newValue }); + } } } componentDidMount() { @@ -535,6 +565,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< super.componentDidMount(); window.addEventListener("beforeunload", this.beforeUnloadListener); window.addEventListener("unload", this.unloadListener); + window.addEventListener("popstate", this.onPopStateHandler); this.model_.errorMessage.addOnChangedHandler(this.errorChangeHandler_); this.model_.state.addOnChangedHandler(this.stateChangeHandler_); @@ -544,6 +575,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< this.model_.showStatusMonitor.addOnChangedHandler(this.showStatusMonitorHandler); this.model_.promptForUpdate.addOnChangedHandler(this.promptForUpdateHandler); this.alertMessageChangedHandler(); + } @@ -566,6 +598,9 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< super.componentWillUnmount(); window.removeEventListener("beforeunload", this.beforeUnloadListener); + window.removeEventListener("popstate", this.onPopStateHandler); + + this.model_.promptForUpdate.removeOnChangedHandler(this.promptForUpdateHandler); this.model_.errorMessage.removeOnChangedHandler(this.errorChangeHandler_); this.model_.state.removeOnChangedHandler(this.stateChangeHandler_); @@ -632,12 +667,6 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< } } - showDrawer() { - this.setState({ isDrawerOpen: true }) - } - hideDrawer() { - this.setState({ isDrawerOpen: false }) - } shortBankList(banks: BankIndex): BankIndexEntry[] { let n = this.state.bankDisplayItems; let entries = banks.entries; @@ -682,6 +711,8 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< return "Downloading update..."; case State.InstallingUpdate: return "Installing update...."; + case State.HotspotChanging: + return "Network connection changing..." default: return "Reconnecting..."; } @@ -792,7 +823,11 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< { showBankSelectDialog && ( this.handleDrawerSelectBank()} + onClick={(ev) => { + ev.stopPropagation(); + this.hideDrawer(true); + this.handleDrawerSelectBank(); + }} > @@ -804,19 +839,34 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< - { this.handleDrawerRenameBank() }}> + { + ev.stopPropagation(); + this.hideDrawer(true); + this.handleDrawerRenameBank() + }}> - { this.handleDrawerSaveBankAs() }} > + { + ev.stopPropagation(); + this.hideDrawer(true); + this.handleDrawerSaveBankAs(); + }} > - { this.handleDrawerManageBanks(); }}> + { + ev.stopPropagation(); + this.hideDrawer(true); + this.handleDrawerManageBanks(); + }}> @@ -825,19 +875,34 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent< - { this.handleDrawerSettingsClick() }}> + { + ev.stopPropagation(); + this.hideDrawer(true); + this.handleDrawerSettingsClick() + }}> - { this.handleDrawerAboutClick() }}> + { + ev.stopPropagation(); + this.hideDrawer(true); + this.handleDrawerAboutClick(); + }}> - { this.handleDrawerDonationClick() }}> + { + ev.stopPropagation(); + this.hideDrawer(true); + this.handleDrawerDonationClick(); + }}> @@ -861,10 +926,10 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
this.setState({ bankDialogOpen: false })} /> - { (this.state.aboutDialogOpen)&& - ( - this.setState({ aboutDialogOpen: false })} /> - )} + {(this.state.aboutDialogOpen) && + ( + this.setState({ aboutDialogOpen: false })} /> + )} @@ -937,7 +1002,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
- + {this.getReloadingMessage()}
@@ -957,7 +1022,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<

- diff --git a/react/src/BankDialog.tsx b/react/src/BankDialog.tsx index 94ce27b..2b0940f 100644 --- a/react/src/BankDialog.tsx +++ b/react/src/BankDialog.tsx @@ -392,7 +392,7 @@ const BankDialog = withStyles(styles, { withTheme: true })( getSelectedBankName() { try { - return this.model.banks.get().getEntry(this.state.selectedItem)!.name; + return this.model.banks.get()?.getEntry(this.state.selectedItem)?.name??""; } catch (error) { return ""; } diff --git a/react/src/DarkMode.tsx b/react/src/DarkMode.tsx index 1c64522..f99496a 100644 --- a/react/src/DarkMode.tsx +++ b/react/src/DarkMode.tsx @@ -17,6 +17,8 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +import { AndroidHostInterface } from "./AndroidHost"; + export enum ColorTheme { Light, Dark, @@ -26,16 +28,20 @@ export enum ColorTheme { export function getColorScheme(): ColorTheme { - const androidHosted = !!((window as any).AndroidHost); - + let androidHost = (window as any).AndroidHost as AndroidHostInterface; + if (androidHost) { + switch (androidHost.getThemePreference() as number) + { + case 0: return ColorTheme.Light; + case 1: return ColorTheme.Dark; + default: + case 2: return ColorTheme.System; + } + } switch (localStorage.getItem('colorScheme')) { case null: default: - if (androidHosted) { - return ColorTheme.System; - } else { - return ColorTheme.Light; - } + return ColorTheme.Light; case "Light": return ColorTheme.Light; case "Dark": @@ -45,6 +51,23 @@ export function getColorScheme(): ColorTheme { } } export function setColorScheme(value: ColorTheme): void { + let androidHost = (window as any).AndroidHost as AndroidHostInterface; + if (androidHost) { + switch (value) { + case ColorTheme.Light: + androidHost.setThemePreference(0); + break; + case ColorTheme.Dark: + androidHost.setThemePreference(1); + break; + default: + case ColorTheme.System: + androidHost.setThemePreference(2); + break; + + } + } + var storageValue; switch (value) { default: diff --git a/react/src/DialogEx.tsx b/react/src/DialogEx.tsx index 3288327..a13e0ab 100644 --- a/react/src/DialogEx.tsx +++ b/react/src/DialogEx.tsx @@ -31,21 +31,17 @@ interface DialogExState { } class DialogStackEntry { - constructor(tag: string) + constructor(dialog: DialogEx) { - this.tag = tag; + this.tag = dialog.props.tag; + this.dialog = dialog; } tag: string; + dialog: DialogEx; }; let dialogStack: DialogStackEntry[] = []; -function peekDialogStack(): string { - if (dialogStack.length !== 0) { - return dialogStack[dialogStack.length-1].tag; - } - return ""; -} function popDialogStack(): void { if (dialogStack.length !== 0) @@ -53,12 +49,33 @@ function popDialogStack(): void { dialogStack.splice(dialogStack.length-1,1); } } -function pushDialogStack(tag: string): void { - dialogStack.push(new DialogStackEntry(tag)); +function pushDialogStack(dialog: DialogEx): void { + dialogStack.push(new DialogStackEntry(dialog)); } +export interface DialogStackState{ + tag: String; + previousState: DialogStackState | null; +}; +// Close all dialogs higher in the dialog stack than "tag". +export function removeDialogStackEntriesAbove(tag: string) +{ + for (let i = dialogStack.length-1; i >= 0; --i) + { + let entry = dialogStack[i]; + if (entry.tag === tag) + { + return; + } + popDialogStack(); + if (entry.dialog.props.open && entry.dialog.props.onClose) + { + entry.dialog.props.onClose({}, "backdropClick"); + } + } +} class DialogEx extends React.Component { constructor(props: DialogExProps) { @@ -75,22 +92,28 @@ class DialogEx extends React.Component { hasHooks: boolean = false; stateWasPopped: boolean = false; - handlePopState(e: any): any + handlePopState(ev: PopStateEvent): any { - let shouldClose = (peekDialogStack() === this.props.tag); - if (shouldClose) - { + let evTag: DialogStackState | null = ev.state as DialogStackState | null; + if (evTag && evTag.tag === this.props.tag) { + removeDialogStackEntriesAbove(this.props.tag); if (!this.stateWasPopped) { this.stateWasPopped = true; popDialogStack(); if (this.props.open) { - this.props.onClose?.(e,"backdropClick"); + this.props.onClose?.(ev,"backdropClick"); } } + window.history.replaceState(evTag.previousState,"",window.location.href); window.removeEventListener("popstate",this.handlePopState); - e.stopPropagation(); + + if (window.location.hash === "#menu") // The menu popstate is next? + { + window.history.back(); // then clear off the menu popstate too. + } + ev.stopPropagation(); } } @@ -105,19 +128,18 @@ class DialogEx extends React.Component { this.stateWasPopped = false; window.addEventListener("popstate",this.handlePopState); - pushDialogStack(this.props.tag); - let state: {tag: string} = {tag: this.props.tag}; - // eslint-disable-next-line no-restricted-globals - history.pushState( - state, + pushDialogStack(this); + let dialogStackState: DialogStackState = {tag: this.props.tag,previousState: window.history.state as DialogStackState | null}; + window.history.replaceState(dialogStackState,"",window.location.href); + window.history.pushState( + null, "", "#" + this.props.tag ); } else { if (!this.stateWasPopped) { - // eslint-disable-next-line no-restricted-globals - history.back(); + window.history.back(); } } } diff --git a/react/src/JackServerSettingsDialog.tsx b/react/src/JackServerSettingsDialog.tsx index 38c811b..d3d2e3a 100644 --- a/react/src/JackServerSettingsDialog.tsx +++ b/react/src/JackServerSettingsDialog.tsx @@ -42,11 +42,21 @@ import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; import AlsaDeviceInfo from './AlsaDeviceInfo'; +const MIN_BUFFER_SIZE = 16; +const MAX_BUFFER_SIZE = 2048; + + +interface BufferSetting { + bufferSize: number; + numberOfBuffers: number; +}; + const INVALID_DEVICE_ID = "_invalid_"; interface JackServerSettingsDialogState { latencyText: string; jackServerSettings: JackServerSettings; alsaDevices?: AlsaDeviceInfo[]; + okEnabled: boolean; } const styles = (theme: Theme) => @@ -66,12 +76,150 @@ export interface JackServerSettingsDialogProps extends WithStyles onApply: (jackServerSettings: JackServerSettings) => void; } -function getLatencyText(settings: JackServerSettings ): string { +function getLatencyText(settings?: JackServerSettings ): string { + if (!settings) + { + return "\u00A0"; + } + if (!settings.valid) return "\u00A0"; let ms = settings.bufferSize * settings.numberOfBuffers / settings.sampleRate * 1000; return ms.toFixed(1) + "ms"; } +function getValidBufferCounts(bufferSize: number, alsaDeviceInfo?: AlsaDeviceInfo): number[] +{ + if (!alsaDeviceInfo) + { + return []; + } + let result: number[] = []; + if (bufferSize * 2 >= alsaDeviceInfo.minBufferSize) + { + result = [2,3,4]; + } else { + let minBuffers = Math.ceil(alsaDeviceInfo.minBufferSize/bufferSize/2-0.0001); + result = [minBuffers*2,minBuffers*3, minBuffers*4]; + } + for (let i = 0; i < result.length; ++i) + { + let selectedSize = result[i]*bufferSize; + if (selectedSize < alsaDeviceInfo.minBufferSize || selectedSize > alsaDeviceInfo.maxBufferSize) + { + result.splice(i,1); + --i; + } + } + + return result; +} +function getValidBufferSizes(alsaDeviceInfo? : AlsaDeviceInfo): number[] +{ + if (!alsaDeviceInfo ) + { + return []; + } + let result: number[] = []; + for (let i = MIN_BUFFER_SIZE; i <= MAX_BUFFER_SIZE; i *= 2) + { + if (getValidBufferCounts(i,alsaDeviceInfo).length !== 0) + { + result.push(i); + } + } + return result; +} +function getBestBuffers( + alsaDeviceInfo: AlsaDeviceInfo | undefined, + bufferSize: number, + numberOfBuffers: number, + bufferSizeChanging: boolean = false +) : BufferSetting { + if (!alsaDeviceInfo) + { + return { bufferSize:bufferSize, numberOfBuffers: numberOfBuffers}; + } + + // If the numberOfbuffers is fine as is, don't change the number of Buffers. + // set default values. Otherwise, choose the best buffer count from available buffer counts. + let validBuffercounts = getValidBufferCounts(bufferSize,alsaDeviceInfo); + let ix = validBuffercounts.indexOf(numberOfBuffers); + if (ix !== -1) { + if (bufferSize*numberOfBuffers <= alsaDeviceInfo.maxBufferSize + && bufferSize*numberOfBuffers >= alsaDeviceInfo.minBufferSize) + { + return {bufferSize: bufferSize, numberOfBuffers: numberOfBuffers}; + } + } + // if numberOfBuffers is not valid, and the user has just selected a new buffers size, + // then choose from available buffer counts. + if (bufferSizeChanging) + { + if (validBuffercounts.length !== 0) + { + // prefer the one thats divisible by 3. + for (let i = 0; i < validBuffercounts.length; ++i) + { + if (validBuffercounts[i] % 3 === 0) + { + numberOfBuffers = validBuffercounts[i]; + return {bufferSize: bufferSize, numberOfBuffers:numberOfBuffers}; + } + } + numberOfBuffers = validBuffercounts[0]; + return {bufferSize: bufferSize, numberOfBuffers:numberOfBuffers}; + } + } + // otherwise select a sensible starting value. + + // favored default: 64x3. + if (64*3 >= alsaDeviceInfo.minBufferSize && 64*3 <= alsaDeviceInfo.maxBufferSize) + { + return { bufferSize: 64, numberOfBuffers: 3}; + } + // if that isn't possible then minBufferSize/2 x 4. + bufferSize = alsaDeviceInfo.minBufferSize/2; + numberOfBuffers = 4; + // otherwise, minBufferSize/2 x 2. + if (bufferSize*numberOfBuffers > alsaDeviceInfo.maxBufferSize) + { + numberOfBuffers = 2; + } + return { bufferSize: bufferSize, numberOfBuffers: numberOfBuffers}; + +}; + +function isOkEnabled(jackServerSettings: JackServerSettings, alsaDevices?: AlsaDeviceInfo[]) +{ + if (!jackServerSettings.valid) return false; + if (!alsaDevices) return false; + let alsaDevice: AlsaDeviceInfo | undefined = undefined; + for (let i = 0; i < alsaDevices.length; ++i) + { + if (alsaDevices[i].id === jackServerSettings.alsaDevice) + { + alsaDevice = alsaDevices[i]; + } + } + if (!alsaDevice) + { + return false; + } + let deviceBufferSize = jackServerSettings.bufferSize * jackServerSettings.numberOfBuffers; + if (deviceBufferSize < alsaDevice.minBufferSize || deviceBufferSize > alsaDevice.maxBufferSize) + { + return false; + } + let validBufferCounts = getValidBufferCounts(jackServerSettings.bufferSize, alsaDevice); + let ix = validBufferCounts.indexOf(jackServerSettings.numberOfBuffers); + if (ix === -1) + { + return false; + } + return true; +} + + const JackServerSettingsDialog = withStyles(styles)( class extends Component { @@ -83,8 +231,10 @@ const JackServerSettingsDialog = withStyles(styles)( this.state = { - latencyText: "\u00A0", - jackServerSettings: props.jackServerSettings.clone() // invalid, but not nullish + latencyText: getLatencyText(props.jackServerSettings), + jackServerSettings: props.jackServerSettings.clone(), // invalid, but not nullish + alsaDevices: undefined, + okEnabled: false }; } mounted: boolean = false; @@ -98,7 +248,8 @@ const JackServerSettingsDialog = withStyles(styles)( this.setState({ alsaDevices: devices, jackServerSettings: settings, - latencyText: getLatencyText(settings) + latencyText: getLatencyText(settings), + okEnabled: isOkEnabled(settings,devices) }); } else { this.setState({ alsaDevices: devices }); @@ -115,8 +266,8 @@ const JackServerSettingsDialog = withStyles(styles)( if (!alsaDevices) { return result; } + result.valid = false; if (alsaDevices.length === 0) { - result.valid = false; result.alsaDevice = INVALID_DEVICE_ID; return result; } @@ -134,27 +285,34 @@ const JackServerSettingsDialog = withStyles(styles)( } if (result.sampleRate === 0) result.sampleRate = 48000; - if (result.bufferSize === 0) result.bufferSize = 16; - if (result.numberOfBuffers === 0) result.numberOfBuffers = 3; result.sampleRate = selectedDevice.closestSampleRate(result.sampleRate); - result.bufferSize = selectedDevice.closestBufferSize(result.bufferSize); + + let bestBuffers = getBestBuffers(selectedDevice,result.bufferSize,result.numberOfBuffers); + result.bufferSize = bestBuffers.bufferSize; + result.numberOfBuffers = bestBuffers.numberOfBuffers; + result.valid = true; return result; } componentDidMount() { this.mounted = true; - this.requestAlsaInfo(); + if (this.props.open) { + this.requestAlsaInfo(); + } } componentDidUpdate(oldProps: JackServerSettingsDialogProps) { - if (this.props.open && !oldProps.open) { + if ((this.props.open && !oldProps.open) && this.mounted) { let settings = this.applyAlsaDevices(this.props.jackServerSettings.clone(), this.state.alsaDevices); this.setState({ jackServerSettings: settings, - latencyText: getLatencyText(settings) + latencyText: getLatencyText(settings), + okEnabled: isOkEnabled(settings,this.state.alsaDevices) }); - this.requestAlsaInfo(); + if (!this.state.alsaDevices) { + this.requestAlsaInfo(); + } } } @@ -180,12 +338,12 @@ const JackServerSettingsDialog = withStyles(styles)( if (!selectedDevice) return; let settings = this.state.jackServerSettings.clone(); settings.alsaDevice = device; - settings.sampleRate = selectedDevice.closestSampleRate(settings.sampleRate); - settings.bufferSize = selectedDevice.closestBufferSize(settings.bufferSize); + settings = this.applyAlsaDevices(settings,this.state.alsaDevices); this.setState({ jackServerSettings: settings, - latencyText: getLatencyText(settings) + latencyText: getLatencyText(settings), + okEnabled: isOkEnabled(settings,this.state.alsaDevices) }); } handleRateChanged(e: any) { @@ -198,7 +356,8 @@ const JackServerSettingsDialog = withStyles(styles)( this.setState({ jackServerSettings: settings, - latencyText: getLatencyText(settings) + latencyText: getLatencyText(settings), + okEnabled: isOkEnabled(settings,this.state.alsaDevices) }); } handleSizeChanged(e: any) { @@ -208,10 +367,14 @@ const JackServerSettingsDialog = withStyles(styles)( if (!selectedDevice) return; let settings = this.state.jackServerSettings.clone(); settings.bufferSize = size; + let bestBufferSetting = getBestBuffers(selectedDevice,settings.bufferSize,settings.numberOfBuffers,true); + settings.bufferSize = bestBufferSetting.bufferSize; + settings.numberOfBuffers = bestBufferSetting.numberOfBuffers; this.setState({ jackServerSettings: settings, - latencyText: getLatencyText(settings) + latencyText: getLatencyText(settings), + okEnabled: isOkEnabled(settings,this.state.alsaDevices) }); } handleNumberOfBuffersChanged(e: any) { @@ -224,7 +387,8 @@ const JackServerSettingsDialog = withStyles(styles)( this.setState({ jackServerSettings: settings, - latencyText: getLatencyText(settings) + latencyText: getLatencyText(settings), + okEnabled: isOkEnabled(settings,this.state.alsaDevices) }); } @@ -232,19 +396,6 @@ const JackServerSettingsDialog = withStyles(styles)( this.props.onApply(this.state.jackServerSettings.clone()); }; - getBufferSizes(alsaDevice: AlsaDeviceInfo): number[] { - let result: number[] = []; - let max = alsaDevice.maxBufferSize; - if (max > 2048) max = 2048; - - for (let i = 16; i <= max; i *= 2) { - if (i >= alsaDevice.minBufferSize) { - result.push(i); - } - } - return result; - } - render() { const classes = this.props.classes; @@ -265,10 +416,10 @@ const JackServerSettingsDialog = withStyles(styles)( } } } - let bufferSizes: number[] = []; - if (selectedDevice) { - bufferSizes = this.getBufferSizes(selectedDevice); - } + let bufferSizes: number[] = getValidBufferSizes(selectedDevice); + let bufferCounts = getValidBufferCounts(this.state.jackServerSettings.bufferSize,selectedDevice); + let bufferSizeDisabled = !selectedDevice; + let bufferCountDisabled = !selectedDevice; return ( @@ -305,6 +456,7 @@ const JackServerSettingsDialog = withStyles(styles)( this.handleSizeChanged(e)} value={this.state.jackServerSettings.bufferSize} + disabled={bufferSizeDisabled} inputProps={{ name: 'Buffer size', id: 'jsd_bufferSize', @@ -343,14 +496,21 @@ const JackServerSettingsDialog = withStyles(styles)(
@@ -365,8 +525,8 @@ const JackServerSettingsDialog = withStyles(styles)( - diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index f155618..0593afe 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -52,6 +52,7 @@ export enum State { ReloadingPlugins, DownloadingUpdate, InstallingUpdate, + HotspotChanging, }; class UpdatedError extends Error { @@ -65,6 +66,7 @@ export enum ReconnectReason { LoadingSettings, ReloadingPlugins, Updating, + HotspotChanging }; export type ControlValueChangedHandler = (key: string, value: number) => void; @@ -366,7 +368,8 @@ export class PiPedalModel //implements PiPedalModel clientId: number = -1; serverVersion?: PiPedalVersion; - countryCodes: Object = {}; + countryCodes: {[Name: string]: string} = {}; + socketServerUrl: string = ""; varServerUrl: string = ""; lv2Path: string = ""; @@ -465,6 +468,10 @@ export class PiPedalModel //implements PiPedalModel case ReconnectReason.Updating: this.setState(State.InstallingUpdate); break; + case ReconnectReason.HotspotChanging: + this.setState(State.HotspotChanging); + this.startHotspotReconnectTimer(); + break; } return true; @@ -642,9 +649,13 @@ export class PiPedalModel //implements PiPedalModel } else if (message === "onUpdateStatusChanged") { let updateStatus = new UpdateStatus().deserialize(body); this.onUpdateStatusChanged(updateStatus); + } else if (message === "onNetworkChanging") + { + this.onNetworkChanging(body as boolean); } } + private updateLaterTimeout?: NodeJS.Timeout = undefined; private clearPromptForUpdateTimer() { @@ -676,6 +687,10 @@ export class PiPedalModel //implements PiPedalModel private lastCanUpdateNow: boolean = false; private updatePromptForUpdate() { this.clearPromptForUpdateTimer(); + if (!this.enableAutoUpdate) + { + return; + } let stateEnabled = true; // must be present to accept alerts. this.state.get() === State.Ready; let timeEnabled = false; @@ -778,16 +793,46 @@ export class PiPedalModel //implements PiPedalModel return this.webSocket; } + androidReconnectTimeout?: NodeJS.Timeout = undefined; + + cancelAndroidReconnectTimer() + { + if (this.androidReconnectTimeout) { + clearTimeout(this.androidReconnectTimeout); + this.androidReconnectTimeout = undefined; + } + } + + startAndroidReconnectTimer() + { + this.cancelAndroidReconnectTimer(); + this.androidReconnectTimeout = setTimeout(()=>{ + this.androidReconnectTimeout = undefined; + this.androidHost?.setDisconnected(true); + },20*1000); + } onSocketConnectionLost() { // remove all the events and subscriptions we have. + if (this.isClosed) + { + return; // page unloading. do NOT change the UI. + } this.vuSubscriptions = []; this.monitorPatchPropertyListeners = []; if (this.isAndroidHosted()) { - this.androidHost?.setDisconnected(true); + // if unexpected, go back to the device browser immediately. + if (this.reconnectReason === ReconnectReason.Disconnected) + { + this.androidHost?.setDisconnected(true); + } else { + this.startAndroidReconnectTimer(); + } } } onSocketReconnected() { + this.cancelOnNetworkChanging(); + this.cancelAndroidReconnectTimer(); if (this.isAndroidHosted()) { this.androidHost?.setDisconnected(false); @@ -898,7 +943,7 @@ export class PiPedalModel //implements PiPedalModel maxFileUploadSize: number = 512 * 1024 * 1024; maxPresetUploadSize: number = 1024 * 1024; debug: boolean = false; - + enableAutoUpdate: boolean = false; requestConfig(): Promise { const myRequest = new Request(this.varRequest('config.json')); @@ -909,6 +954,7 @@ export class PiPedalModel //implements PiPedalModel } ) .then(data => { + this.enableAutoUpdate = !!data.enable_auto_update; if (data.max_upload_size) { this.maxPresetUploadSize = data.max_upload_size; } @@ -942,7 +988,7 @@ export class PiPedalModel //implements PiPedalModel return this.webSocket.connect(); }) .catch((error) => { - this.setError("Failed to connect to server."); + this.setError("Failed to connect to server. (" + this.socketServerUrl + ")"); return false; }) .then(() => { @@ -956,7 +1002,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 +2602,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 +2713,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) { @@ -2869,6 +2939,100 @@ export class PiPedalModel //implements PiPedalModel window.location.href = url; //window.location.reload(); } + + private networkChanging: boolean = false; + private networkChanging_expectHotspot = false; + private expectNetworkChangeTimeout?: NodeJS.Timeout = undefined; + private hotspotReconnectTimer?: NodeJS.Timeout = undefined; + + async detectServer(address: string) + { + let port = window.location.port; + let newUrl = new URL("http://" + address + ":" + port + "/manifest.json"); + + try { + let response = await fetch(newUrl); + if (response.ok) + { + return "http://" + address +":"+ port; + } + return ""; + } catch (error: any) + { + return ""; + } + } + async pollForLiveServer() { + let wifiConfigSettings = this.wifiConfigSettings.get(); + if (wifiConfigSettings.mdnsName.length !== 0) + { + let newUrl = await this.detectServer(wifiConfigSettings.mdnsName); + if (newUrl.length !== 0) + { + return newUrl; + } + } + if (this.networkChanging_expectHotspot) + { + let newUrl = await this.detectServer("10.40.0.1"); + if (newUrl.length !== 0) + { + return newUrl; + } + } + return ""; + } + cancelHotspotReconnectTimer() + { + if (this.hotspotReconnectTimer) + { + clearTimeout(this.hotspotReconnectTimer); + } + + } + startHotspotReconnectTimer() + { + // poll for access to a running pipedal server + this.hotspotReconnectTimer = setTimeout( + async () => { + let newUrl = await this.pollForLiveServer(); + if (newUrl.length === 0) + { + this.startHotspotReconnectTimer(); + } else { + this.cancelOnNetworkChanging(); + window.location.replace(newUrl); + } + }, + 5*1000); + + } + cancelOnNetworkChanging() + { + this.cancelHotspotReconnectTimer(); + if (this.expectNetworkChangeTimeout) + { + clearTimeout(this.expectNetworkChangeTimeout); + this.expectNetworkChangeTimeout = undefined; + } + this.networkChanging = false; + this.expectDisconnect(ReconnectReason.Disconnected); + } + onNetworkChanging(hotspotConnected: boolean) + { + this.cancelOnNetworkChanging(); + + this.networkChanging = true; + this.networkChanging_expectHotspot = hotspotConnected; + this.expectDisconnect(ReconnectReason.HotspotChanging); + + this.expectNetworkChangeTimeout = setTimeout( + () => { + this.cancelOnNetworkChanging(); + }, + 30*1000); + + } }; let instance: PiPedalModel | undefined = undefined; diff --git a/react/src/PiPedalSocket.tsx b/react/src/PiPedalSocket.tsx index 8a10bea..49315d7 100644 --- a/react/src/PiPedalSocket.tsx +++ b/react/src/PiPedalSocket.tsx @@ -26,7 +26,7 @@ export type ReconnectHandler = () => void; export type ReconnectingHandler = (retry: number, maxRetries: number) => void; const MAX_RETRIES = 6; -const MAX_RETRY_TIME = 90*1000; +const MAX_RETRY_TIME = 90 * 1000; export type PiPedalMessageHeader = { replyTo?: number; @@ -37,10 +37,10 @@ type ReplyHandler = (header: PiPedalMessageHeader, body: any | null) => void; export interface PiPedalSocketListener { - onMessageReceived : (header: PiPedalMessageHeader, body: any | null) => void; + onMessageReceived: (header: PiPedalMessageHeader, body: any | null) => void; onError: (message: string, exception?: Error) => void; onConnectionLost: () => void; - onReconnect: () => void; + onReconnect: () => void; onReconnecting: (retry: number, maxRetries: number) => boolean; }; @@ -60,7 +60,7 @@ class PiPedalSocket { constructor( url: string, listener: PiPedalSocketListener - ) { + ) { this.url = url; this.listener = listener; } @@ -103,8 +103,7 @@ class PiPedalSocket { _replyMap: Map = new Map(); - _discardReplyReservations() - { + _discardReplyReservations() { // it's ok. All pending reservations disappear into the GC. this._replyMap = new Map(); } @@ -193,8 +192,7 @@ class PiPedalSocket { return; } - if (this.canReconnect) - { + if (this.canReconnect) { this.listener.onConnectionLost(); this._reconnect(); } @@ -212,26 +210,23 @@ class PiPedalSocket { isBackground: boolean = false; - enterBackgroundState() - { + enterBackgroundState() { this.isBackground = true; this.close(); } - exitBackgroundState() - { + exitBackgroundState() { this.isBackground = false; this._reconnect(); } reconnect() { - if (this.socket) - { + if (this.socket) { this.close(); } - if (!this.listener.onReconnecting(this.retryCount,MAX_RETRIES)) { + if (!this.listener.onReconnecting(this.retryCount, MAX_RETRIES)) { return; } ++this.retryCount; @@ -243,8 +238,7 @@ class PiPedalSocket { this.listener.onReconnect(); }) .catch(error => { - if (this.totalRetryDelay === MAX_RETRY_TIME) - { + if (this.totalRetryDelay >= MAX_RETRY_TIME) { this.listener.onError("Server connection lost."); return; } else { @@ -258,8 +252,7 @@ class PiPedalSocket { close(): void { try { - if (this.socket) - { + if (this.socket) { this.socket.onclose = null; this.socket.onerror = null; this.socket.onmessage = null; @@ -267,50 +260,53 @@ class PiPedalSocket { this.socket.close(); this.socket = undefined; } - } catch (ignored) - { + } catch (ignored) { } this.socket = undefined; } connectInternal_(): Promise { return new Promise((resolve, reject) => { - let ws = new WebSocket(this.url); + try { + let ws = new WebSocket(this.url); - let self = this; + let self = this; - ws.onmessage = this.handleMessage.bind(this); - ws.onclose = (event: Event) => { - ws.onclose = null; - ws.onerror = null; - reject("Connection closed unexpectedly."); - }; - ws.onerror = (event: Event) => { - ws.onclose = null; - ws.onerror = null; - reject("Failed to connect."); - }; - ws.onopen = (event: Event) => { - ws.onerror = self.handleError.bind(self); - ws.onclose = self.handleClose.bind(self); - ws.onopen = null; - resolve(ws); + ws.onmessage = this.handleMessage.bind(this); + ws.onclose = (event: Event) => { + ws.onclose = null; + ws.onerror = null; + reject("Connection closed unexpectedly."); + }; + ws.onerror = (evt: Event) => { + ws.onclose = null; + ws.onerror = null; + reject("Failed to connect."); + }; + ws.onopen = (event: Event) => { + ws.onerror = self.handleError.bind(self); + ws.onclose = self.handleClose.bind(self); + ws.onopen = null; + resolve(ws); + }; + } catch (e: any){ + reject("Failed to connect. " + e.toString()); }; }); - } - connect(): Promise { - return new Promise((resolve, reject) => { +} +connect(): Promise < void> { + return new Promise((resolve, reject) => { - this.connectInternal_() - .then((socket) => { - this.socket = socket; - resolve(); - }) - .catch((reason) => { - reject(reason); - }); - }); - } + this.connectInternal_() + .then((socket) => { + this.socket = socket; + resolve(); + }) + .catch((reason) => { + reject(reason); + }); + }); +} } diff --git a/react/src/SettingsDialog.tsx b/react/src/SettingsDialog.tsx index c8481da..f983fdf 100644 --- a/react/src/SettingsDialog.tsx +++ b/react/src/SettingsDialog.tsx @@ -24,7 +24,7 @@ import ListSelectDialog from './ListSelectDialog'; import IconButton from '@mui/material/IconButton'; import Typography from '@mui/material/Typography'; import { PiPedalModel, PiPedalModelFactory, State } from './PiPedalModel'; -import {ColorTheme} from './DarkMode'; +import { ColorTheme } from './DarkMode'; import ButtonBase from "@mui/material/ButtonBase"; import AppBar from '@mui/material/AppBar'; import Button from '@mui/material/Button'; @@ -394,11 +394,10 @@ const SettingsDialog = withStyles(styles, { withTheme: true })( }); } - handleCheckForUpdates() - { + handleCheckForUpdates() { this.model.showUpdateDialog(); } - + handleMidiMessageSettings() { this.setState({ showSystemMidiBindingsDialog: true @@ -572,7 +571,8 @@ const SettingsDialog = withStyles(styles, { withTheme: true })( (
- Select and configure an audio device. You may optionally configure MIDI inputs, and set up a Wi-Fi Direct Hotspot now as well. + Select and configure an audio device. You may optionally configure MIDI inputs, and configure up a Wi-Fi Auto-Hotspot as well. + The Auto-Hotspot feature allows you to connect to Pipedal even if you don't have access to a Wi-Fi router. Access and modify these settings later by selecting the Settings menu item on the main menu. @@ -682,6 +682,20 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
CONNECTION + this.handleShowWifiConfigDialog()} > + +
+ + Wi-Fi auto-hotspot + + {this.state.wifiConfigSettings.getSummaryText()} + + +
+
+ { this.state.isAndroidHosted && ( @@ -700,33 +714,6 @@ const SettingsDialog = withStyles(styles, { withTheme: true })( ) } - this.handleShowWifiDirectConfigDialog()} > - -
- - Configure Wi-Fi Direct hotspot - - {this.state.wifiDirectConfigSettings.getSummaryText()} - - -
-
- - this.handleShowWifiConfigDialog()} > - -
- - Configure Wi-Fi hotspot - - {this.state.wifiConfigSettings.getSummaryText()} - - -
-
{(!this.props.onboarding) ? (
@@ -756,8 +743,8 @@ const SettingsDialog = withStyles(styles, { withTheme: true })( Color theme - { this.model.getTheme() === ColorTheme.Dark ? "Dark" : - (this.model.getTheme() === ColorTheme.Light ? "Light": "System")} + {this.model.getTheme() === ColorTheme.Dark ? "Dark" : + (this.model.getTheme() === ColorTheme.Light ? "Light" : "System")}
@@ -765,9 +752,9 @@ const SettingsDialog = withStyles(styles, { withTheme: true })( { + onClick={() => { this.model.setShowStatusMonitor(!this.state.showStatusMonitor) - }} > + }} >
- { this.handleCheckForUpdates(); }} > - -
- - Check for updates... -
-
+ { + this.model.enableAutoUpdate && ( + { this.handleCheckForUpdates(); }} > + +
+ + Check for updates... +
+
+ + ) + } + @@ -900,20 +893,26 @@ const SettingsDialog = withStyles(styles, { withTheme: true })( (this.state.showThemeSelectDialog) && ( { this.setState({showThemeSelectDialog: false});} } + onClose={() => { this.setState({ showThemeSelectDialog: false }); }} onOk={(selectedTheme: ColorTheme) => { this.model.setTheme(selectedTheme); - this.setState({showThemeSelectDialog: false}); + this.setState({ showThemeSelectDialog: false }); }} defaultTheme={this.model.getTheme()} /> ) } - this.setState({ showWifiConfigDialog: false })} - onOk={(wifiConfigSettings) => this.handleApplyWifiConfig(wifiConfigSettings)} - /> + { + (this.state.showWifiConfigDialog) && + ( + this.setState({ showWifiConfigDialog: false })} + onOk={(wifiConfigSettings) => this.handleApplyWifiConfig(wifiConfigSettings)} + /> + + ) + } this.setState({ showWifiDirectConfigDialog: false })} onOk={(wifiDirectConfigSettings: WifiDirectConfigSettings) => this.handleApplyWifiDirectConfig(wifiDirectConfigSettings)} diff --git a/react/src/SystemMidiBindingsDialog.tsx b/react/src/SystemMidiBindingsDialog.tsx index 33185bc..25112e6 100644 --- a/react/src/SystemMidiBindingsDialog.tsx +++ b/react/src/SystemMidiBindingsDialog.tsx @@ -131,7 +131,24 @@ export const SystemMidiBindingDialog = { displayName = "Next Preset"; instanceId = 2; + } else if (item.symbol === "startHotspot") + { + displayName = "Enable Hotspot"; + instanceId = 3; + } else if (item.symbol === "stopHotspot") + { + displayName = "Disable Hotspot"; + instanceId = 4; + } else if (item.symbol === "shutdown") + { + displayName = "Shutdown"; + instanceId = 5; + } else if (item.symbol === "reboot") + { + displayName = "Reboot"; + instanceId = 6; } + if (instanceId !== -1) { result.push(new BindingEntry(displayName,instanceId,item)); diff --git a/react/src/WifiConfigDialog.tsx b/react/src/WifiConfigDialog.tsx index f7f9760..1affb61 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,604 @@ 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 Auto-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 + }} + /> +
+
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.state.hasPassword && !this.state.showPassword ? + (this.props.wifiConfigSettings.hasSavedPassword? "(Unchanged)" : "(Required)") + : "" + , + endAdornment: ( + { this.handleTogglePasswordVisibility(); }} + > + { + (this.state.showPassword) ? + ( + + ) : + ( + + + ) + } + + ) + }} + /> + +
+ +
+
+
+ { if (value) { this.setState({ countryCode: value.id }) } }} + options={this.getCountryCodeOptions()} + renderInput={(params) => ()} + disabled={!enabled} + /> + {/* + + */} + +
+
+ + 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..7a80a4a 100644 --- a/react/src/WifiConfigSettings.tsx +++ b/react/src/WifiConfigSettings.tsx @@ -21,11 +21,14 @@ 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.mdnsName = input.mdnsName; this.hasPassword = input.hasPassword; this.password = input.password; this.countryCode = input.countryCode; @@ -35,29 +38,28 @@ export default class WifiConfigSettings { clone() : WifiConfigSettings { return new WifiConfigSettings().deserialize(this); } - valid: boolean = true; + autoStartMode: number = 0; + hasSavedPassword: boolean = false; + homeNetwork: string = ""; + mdnsName: 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/signPackage.sh b/signPackage.sh index 04fef7f..c26d857 100755 --- a/signPackage.sh +++ b/signPackage.sh @@ -1,10 +1,14 @@ #!/bin/bash - -#sign a package with rerdavies@gmail.com's private key. -cd build -for filename in *.deb; do - echo gpg --armor --output "$filename".asc -b "$filename" - - gpg --armor --output "$filename".asc -b "$filename" -done -cd .. \ No newline at end of file +# Sign the built .deb package with the private key used in the Auto-update process. +# +# Won't work for you, because you don't have MY private key. +# +# Auto-update requires that .deb files be distributed from the GitHub rerdavies/pipedal project. +# And auto-updates WILL update installs of your forked binary unless you take steps to prevent that. +# +# If you are distributing a fork, you should see the file UpdaterSecurity.hpp for details on how to +# modify pipedal to perform updates from an alternate github site. Or you could modify your fork +# to NOT do auto-updating (probably best). The build doesn't currently have a procedure for +# knocking out Auto-Update from UpdaterSecurity.hpp. But I would happily accept a pull request +# to bring such a feature back into mainline if you do it. +build/src/makeRelease diff --git a/src/AlsaDriver.cpp b/src/AlsaDriver.cpp index 56d257a..dd8f3a2 100644 --- a/src/AlsaDriver.cpp +++ b/src/AlsaDriver.cpp @@ -202,8 +202,16 @@ namespace pipedal { Close(); #ifdef ALSADRIVER_CONFIG_DBG - snd_output_close(snd_output); - snd_pcm_status_free(snd_status); + if (snd_output) + { + snd_output_close(snd_output); + snd_output = nullptr; + } + if (snd_status) + { + snd_pcm_status_free(snd_status); + snd_status = nullptr; + } #endif } @@ -248,17 +256,15 @@ namespace pipedal std::mutex terminateSync; - bool terminateAudio_ = false; + std::atomic terminateAudio_ = false; void terminateAudio(bool terminate) { - std::lock_guard lock{terminateSync}; this->terminateAudio_ = terminate; } bool terminateAudio() { - std::lock_guard lock{terminateSync}; return this->terminateAudio_; } @@ -1289,10 +1295,10 @@ namespace pipedal FillOutputBuffer(); err = snd_pcm_start(handle); - // if (err < 0) - // { - // throw PiPedalStateException(SS("Can't recover from ALSA underrun. (" << snd_strerror(err) << ")")); - // } + if (err < 0) + { + throw PiPedalStateException(SS("Can't recover from ALSA underrun. (" << snd_strerror(err) << ")")); + } return; } else if (err == -ESTRPIPE) @@ -1312,10 +1318,10 @@ namespace pipedal } return; } - throw PiPedalStateException(SS("ALSA error:" << snd_strerror(err))); + throw PiPedalStateException(SS("ALSA error: " << snd_strerror(err))); } - std::jthread *audioThread; + std::jthread *audioThread = nullptr; bool audioRunning; bool block = false; @@ -1422,45 +1428,58 @@ namespace pipedal } // snd_pcm_wait(captureHandle, 1); - - ssize_t framesRead; - if ((framesRead = ReadBuffer(captureHandle, this->rawCaptureBuffer, bufferSize)) < 0) + ssize_t framesToRead = bufferSize; + ssize_t framesRead = 0; + bool xrun = false; + while (framesToRead != 0) { - this->driverHost->OnUnderrun(); - auto state = snd_pcm_state(playbackHandle); - XrunRecoverInputUnderrun(captureHandle, framesRead); - continue; - } - else - { - cpuUse.AddSample(ProfileCategory::Read); - if (framesRead == 0) - continue; - if (framesRead != bufferSize) - { - throw PiPedalStateException("Invalid read."); - } - - (this->*copyInputFn)(framesRead); - cpuUse.AddSample(ProfileCategory::Driver); - - this->driverHost->OnProcess(framesRead); - - cpuUse.AddSample(ProfileCategory::Execute); - - (this->*copyOutputFn)(framesRead); - cpuUse.AddSample(ProfileCategory::Driver); - // process. - - ssize_t err = WriteBuffer(playbackHandle, rawPlaybackBuffer, framesRead); - - if (err < 0) + ssize_t thisTime = framesToRead; + ssize_t nFrames; + if ((nFrames = ReadBuffer( + captureHandle, + this->rawCaptureBuffer + this->captureFrameSize*framesRead, + bufferSize)) < 0) { this->driverHost->OnUnderrun(); - XrunRecoverOutputOverrun(playbackHandle, err); + auto state = snd_pcm_state(playbackHandle); + XrunRecoverInputUnderrun(captureHandle, nFrames); + xrun = true; + break; } - cpuUse.AddSample(ProfileCategory::Write); + framesRead += nFrames; + framesToRead -= nFrames; } + if (xrun) + { + continue; + } + cpuUse.AddSample(ProfileCategory::Read); + if (framesRead == 0) + continue; + if (framesRead != bufferSize) + { + throw PiPedalStateException("Invalid read."); + } + + (this->*copyInputFn)(framesRead); + cpuUse.AddSample(ProfileCategory::Driver); + + this->driverHost->OnProcess(framesRead); + + cpuUse.AddSample(ProfileCategory::Execute); + + (this->*copyOutputFn)(framesRead); + cpuUse.AddSample(ProfileCategory::Driver); + // process. + + ssize_t err = WriteBuffer(playbackHandle, rawPlaybackBuffer, framesRead); + + if (err < 0) + { + this->driverHost->OnUnderrun(); + XrunRecoverOutputOverrun(playbackHandle, err); + } + cpuUse.AddSample(ProfileCategory::Write); } } catch (const std::exception &e) @@ -1593,8 +1612,8 @@ namespace pipedal int dataLength = 0; int dataIndex = 0; size_t statusBytesRemaining = 0; - size_t data0; - size_t data1; + size_t data0 =0; + size_t data1 = 0; size_t eventCount = 0; MidiEvent events[MAX_MIDI_EVENT]; @@ -1862,14 +1881,21 @@ namespace pipedal { const auto &devices = channelSelection.GetInputMidiDevices(); - midiStates.resize(devices.size()); + midiStates.reserve(devices.size()); for (size_t i = 0; i < devices.size(); ++i) { const auto &device = devices[i]; - MidiState *state = new MidiState(); - midiStates[i] = state; - state->Open(device); + MidiState *midiState = nullptr; + try { + midiState = new MidiState(); + midiState->Open(device); + midiStates.push_back(midiState); + } catch (const std::exception &e) + { + // logged already. + delete midiState; + } } } @@ -2085,16 +2111,26 @@ namespace pipedal throw; } if (playbackHwParams) + { snd_pcm_hw_params_free(playbackHwParams); - if (captureHwParams) + playbackHwParams = nullptr; + } + + if (captureHwParams) { snd_pcm_hw_params_free(captureHwParams); + captureHwParams = nullptr; + } if (playbackHandle) { snd_pcm_close(playbackHandle); + playbackHandle = nullptr; } if (captureHandle) + { snd_pcm_close(captureHandle); + captureHandle = nullptr; + } return result; } diff --git a/src/AudioDriver.hpp b/src/AudioDriver.hpp index 6d8baae..7e051b2 100644 --- a/src/AudioDriver.hpp +++ b/src/AudioDriver.hpp @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2022 Robin E. R. Davies +* Copyright (c) 2022 Robin E. R. 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 diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index fb742db..9547e62 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -296,6 +296,10 @@ private: SystemMidiBinding nextMidiBinding; SystemMidiBinding prevMidiBinding; + SystemMidiBinding startHotspotMidiBinding; + SystemMidiBinding stopHotspotMidiBinding; + SystemMidiBinding rebootMidiBinding; + SystemMidiBinding shutdownMidiBinding; JackChannelSelection channelSelection; std::atomic active = false; @@ -345,17 +349,14 @@ private: virtual void Close() { - std::lock_guard guard{mutex}; - if (!isOpen) - return; - - isOpen = false; - - if (realtimeMonitorPortSubscriptions != nullptr) { - delete realtimeMonitorPortSubscriptions; - realtimeMonitorPortSubscriptions = nullptr; + std::lock_guard guard{mutex}; + if (!isOpen) + return; + + isOpen = false; } + if (active) { audioDriver->Deactivate(); @@ -367,6 +368,7 @@ private: StopReaderThread(); + // release any pdealboards owned by the process thread. this->activePedalboards.resize(0); this->realtimeActivePedalboard = nullptr; @@ -742,6 +744,34 @@ private: this->realtimeWriter.OnNextMidiProgram(++(this->midiProgramChangeId), -1); } } + else if (this->shutdownMidiBinding.IsMatch(event)) + { + if (shutdownMidiBinding.IsTriggered(event)) + { + this->realtimeWriter.OnRealtimeMidiEvent(RealtimeMidiEventType::Shutdown); + } + } + else if (this->rebootMidiBinding.IsMatch(event)) + { + if (rebootMidiBinding.IsTriggered(event)) + { + this->realtimeWriter.OnRealtimeMidiEvent(RealtimeMidiEventType::Reboot); + } + } + else if (this->startHotspotMidiBinding.IsMatch(event)) + { + if (startHotspotMidiBinding.IsTriggered(event)) + { + this->realtimeWriter.OnRealtimeMidiEvent(RealtimeMidiEventType::StartHotspot); + } + } + else if (this->stopHotspotMidiBinding.IsMatch(event)) + { + if (stopHotspotMidiBinding.IsTriggered(event)) + { + this->realtimeWriter.OnRealtimeMidiEvent(RealtimeMidiEventType::StopHotspot); + } + } else if (midiProgramChangePending) { // defer the message for processing after the program change has completed. @@ -783,7 +813,7 @@ private: virtual void OnAudioTerminated() override { this->active = false; - Lv2Log::info("Audio stopped."); + Lv2Log::info("Audio thread terminated."); } @@ -1182,6 +1212,12 @@ public: hostReader.read(&request); pNotifyCallbacks->OnNotifyNextMidiProgram(request); } + else if (command == RingBufferCommand::RealtimeMidiEvent) + { + RealtimeMidiEventRequest request; + hostReader.read(&request); + pNotifyCallbacks->OnNotifyMidiRealtimeEvent(request.eventType); + } else if (command == RingBufferCommand::Lv2ErrorMessage) { size_t size; @@ -1710,6 +1746,7 @@ void AudioHostImpl::SetSystemMidiBindings(const std::vector &bindin { for (auto i = bindings.begin(); i != bindings.end(); ++i) { + if (i->symbol() == "nextProgram") { this->nextMidiBinding.SetBinding(*i); @@ -1717,6 +1754,20 @@ void AudioHostImpl::SetSystemMidiBindings(const std::vector &bindin else if (i->symbol() == "prevProgram") { this->prevMidiBinding.SetBinding(*i); + } else if (i->symbol() == "startHotspot") + { + this->startHotspotMidiBinding.SetBinding(*i); + } else if (i->symbol() == "stopHotspot") + { + this->stopHotspotMidiBinding.SetBinding(*i); + } else if (i->symbol() == "reboot") + { + this->rebootMidiBinding.SetBinding(*i); + } else if (i->symbol() == "shutdown") + { + this->shutdownMidiBinding.SetBinding(*i); + } else { + Lv2Log::error(SS("Invalid system midi binding: " << i->symbol())); } } } diff --git a/src/AudioHost.hpp b/src/AudioHost.hpp index 2455f88..d5a6803 100644 --- a/src/AudioHost.hpp +++ b/src/AudioHost.hpp @@ -30,6 +30,7 @@ #include "PiPedalAlsa.hpp" #include "Promise.hpp" #include "json_variant.hpp" +#include "RealtimeMidiEventType.hpp" namespace pipedal { @@ -160,6 +161,7 @@ public: virtual void OnNotifyMidiProgramChange(RealtimeMidiProgramRequest&midiProgramRequest) = 0; virtual void OnNotifyNextMidiProgram(const RealtimeNextMidiProgramRequest&request) = 0; virtual void OnNotifyLv2RealtimeError(int64_t instanceId,const std::string &error) = 0; + virtual void OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType) = 0; }; diff --git a/src/AvahiService.cpp b/src/AvahiService.cpp index e0eefbf..02f54cc 100644 --- a/src/AvahiService.cpp +++ b/src/AvahiService.cpp @@ -1,18 +1,18 @@ /* * MIT License - * + * * Copyright (c) 2022 Robin E. R. 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 * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies * of the Software, and to permit persons to whom the Software is furnished to do * so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -24,6 +24,7 @@ #include "AvahiService.hpp" #include +#include #include #include #include @@ -35,6 +36,7 @@ #include #include "Lv2Log.hpp" #include "ss.hpp" +#include using namespace pipedal; @@ -43,37 +45,122 @@ void AvahiService::Announce( const std::string &name, const std::string &instanceId, const std::string &mdnsName, - bool addTestGroup) + bool wait) { - Unannounce(); - - this->portNumber = portNumber; - this->instanceId = instanceId; - this->mdnsName = mdnsName; - this->addTestGroup = addTestGroup; - - this->name = avahi_strdup(name.c_str()); - - Start(); -} - -void AvahiService::Unannounce() -{ - Stop(); - if (name) + if (terminated) + return; + // We should update an existing group if the name doesn't change. + // but in practice, the name is the only thing that's going to change without restarting the pipedal server. + if (this->serviceName == name) { - avahi_free(name); - this->name = nullptr; + return; + } + + // this->name = avahi_strdup(name.c_str()); + if (!started) + { + this->portNumber = portNumber; + this->instanceId = instanceId; + this->mdnsName = mdnsName; + this->serviceName = name; + + started = true; + makeAnnouncement = true; + createPending = true; + Start(); + if (wait) { + Wait(); + } + } + else + { + // already started, so we have to use poll_lock instead. + avahi_threaded_poll_lock(threadedPoll); + + // all state that we have to protect with the lock now that the avahi serv3ce is running. + this->portNumber = portNumber; + this->instanceId = instanceId; + this->mdnsName = mdnsName; + this->serviceName = name; + this->makeAnnouncement = true; + + // we've requested a start but have not created the group. + if (this->createPending) + { + // the first create will use our freshly updated parameters. + // no action required. + } + else + { + // otherwise we have to use a lock to effect the update. + if (group) + { + avahi_entry_group_reset(group); // unannounce the previous + SetState(ServiceState::Reset); + create_group(client); + } + else + { + Lv2Log::error("Failed to update mDNS service because of a synch problem."); + } + } + avahi_threaded_poll_unlock(threadedPoll); + if (wait) + { + Wait(); + } } - this->group = nullptr; - this->client = nullptr; - this->threadedPoll = nullptr; } +void AvahiService::Unannounce(bool wait) +{ + if (terminated) + return; + if (this->threadedPoll) + { + avahi_threaded_poll_lock(threadedPoll); + if (started) + { + if (this->createPending) + { + // if service is starting up, and we haven't yet made our first announcement, + // just signal that we don't want to announc. + this->makeAnnouncement = false; + } + else + { + this->makeAnnouncement = false; + // we have previously made a successful announcement. Retract it. + if (group) + { + int rc = avahi_entry_group_reset(group); + if (rc < 0) + { + Lv2Log::error("Avahi: failed to un-announce."); + } + } + } + } + avahi_threaded_poll_unlock(threadedPoll); + if (wait) + { + WaitForUnannounce(); + } + } +} + + + void AvahiService::entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userData) { ((AvahiService *)userData)->EntryGroupCallback(g, state); } + +void AvahiService::SetState(ServiceState serviceState) +{ + this->serviceState = serviceState; +} + void AvahiService::EntryGroupCallback(AvahiEntryGroup *g, AvahiEntryGroupState state) { group = g; @@ -81,29 +168,39 @@ void AvahiService::EntryGroupCallback(AvahiEntryGroup *g, AvahiEntryGroupState s switch (state) { case AVAHI_ENTRY_GROUP_ESTABLISHED: + SetState(ServiceState::Established); + Lv2Log::info(SS("DNS/SD group established.")); + /* The entry group has been established successfully */ - Lv2Log::debug(SS("Service " << name << " successfully established.")); break; case AVAHI_ENTRY_GROUP_COLLISION: { - char *n; /* A service name collision with a remote service - * happened. Let's pick a new name */ - n = avahi_alternative_service_name(name); - avahi_free(name); - name = n; - Lv2Log::error(SS("Service name collision, renaming service to '" << name << "'\n")); + * happened. Let's pick a new name */ + SetState(ServiceState::Collision); + char *n = avahi_alternative_service_name(avahiNameString); + avahi_free(avahiNameString); + avahiNameString = n; + Lv2Log::warning(SS("Service name collision, renaming service to '" << avahiNameString << "'\n")); /* And recreate the services */ create_group(avahi_entry_group_get_client(g)); break; } case AVAHI_ENTRY_GROUP_FAILURE: Lv2Log::error(SS("Entry group failure: " << avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g))) << "\n")); + Lv2Log::error(SS("DNS/SD service shutting down.")); /* Some kind of failure happened while we were registering our services */ + terminated = true; avahi_threaded_poll_quit(threadedPoll); + SetState(ServiceState::Failed); break; case AVAHI_ENTRY_GROUP_UNCOMMITED: + SetState(ServiceState::Uncommited); + break; case AVAHI_ENTRY_GROUP_REGISTERING:; + SetState(ServiceState::Registering); + break; + } } @@ -136,11 +233,14 @@ static int toRawDns(char *rawResult, size_t size, const std::string &name) } void AvahiService::create_group(AvahiClient *c) { + this->createPending = false; + char *n; int ret; assert(c); /* If this is the first time we're called, let's create a new * entry group if necessary */ + SetState(ServiceState::Requested); if (!group) { if (!(group = avahi_entry_group_new(c, entry_group_callback, (void *)this))) @@ -151,20 +251,26 @@ void AvahiService::create_group(AvahiClient *c) } /* If the group is empty (either because it was just created, or * because it was reset previously, add our entries. */ - if (avahi_entry_group_is_empty(group)) + if (this->makeAnnouncement && avahi_entry_group_is_empty(group)) { - Lv2Log::debug(SS("Adding service '" << name << "'")); + Lv2Log::debug(SS("Adding service '" << avahiNameString << "'")); std::string instanceTxtRecord = SS("id=" << this->instanceId); #define PIPEDAL_SERVICE_TYPE "_pipedal._tcp" + if (avahiNameString) + { + avahi_free(avahiNameString); + avahiNameString = nullptr; + } + avahiNameString = avahi_strdup(serviceName.c_str()); if ((ret = avahi_entry_group_add_service( group, AVAHI_IF_UNSPEC, - AVAHI_PROTO_UNSPEC, // IPv4 for now, until we figure out why dnsqmasq borks IPv6 routing.) + AVAHI_PROTO_INET, // IPv4 for now, until we figure out why dnsqmasq borks IPv6 routing.) (AvahiPublishFlags)0, - name, + avahiNameString, PIPEDAL_SERVICE_TYPE, NULL, NULL, @@ -180,55 +286,31 @@ void AvahiService::create_group(AvahiClient *c) goto fail; } - if (this->addTestGroup) - { - Lv2Log::info("Added tests DNS/SD service."); - std::string instanceTxtRecord = SS("id=" << "0a6045b0-1753-4104-b3e4-b9713b9cc360"); - - if ((ret = avahi_entry_group_add_service( - group, - AVAHI_IF_UNSPEC, - AVAHI_PROTO_INET, // IPv4 for now, until we figure out why dnsqmasq borks IPv6 routing.) - (AvahiPublishFlags)0, - "Ed's PiPedal", - PIPEDAL_SERVICE_TYPE, - NULL, - NULL, - portNumber, - - // txt records. - instanceTxtRecord.c_str(), - NULL)) < 0) - { - if (ret == AVAHI_ERR_COLLISION) - goto collision; - Lv2Log::error(SS("Failed to add _pipedal._tcp service: " << avahi_strerror(ret))); - goto fail; - } - } - /* Tell the server to register the service */ if ((ret = avahi_entry_group_commit(group)) < 0) { Lv2Log::error(SS("Failed to commit entry group: " << avahi_strerror(ret))); goto fail; } - Lv2Log::info(SS("DNS/SD service announced.")); - + Lv2Log::info(SS("DNS/SD service announced. (" << this->avahiNameString << ")")); } return; collision: /* A service name collision with a local service happened. Let's - * pick a new name */ - n = avahi_alternative_service_name(name); - avahi_free(name); - name = n; - Lv2Log::warning(SS("Service name collision, renaming service to '" << name << "'")); + * pick a new avahiNameString */ + n = avahi_alternative_service_name(avahiNameString); + avahi_free(avahiNameString); + avahiNameString = n; + Lv2Log::warning(SS("Service name collision, renaming service to '" << avahiNameString << "'")); avahi_entry_group_reset(group); create_group(c); return; fail: + terminated = true; avahi_threaded_poll_quit(threadedPoll); + Lv2Log::error("DNS/SD service shutting down."); + SetState(ServiceState::Failed); + } void AvahiService::client_callback(AvahiClient *c, AvahiClientState state, AVAHI_GCC_UNUSED void *userdata) @@ -244,7 +326,7 @@ void AvahiService::ClientCallback(AvahiClient *c, AvahiClientState state) { case AVAHI_CLIENT_S_RUNNING: /* The server has startup successfully and registered its host - * name on the network, so it's time to create our services */ + * name on the network, so it's time to create our services */ create_group(c); break; case AVAHI_CLIENT_FAILURE: @@ -253,11 +335,11 @@ void AvahiService::ClientCallback(AvahiClient *c, AvahiClientState state) if (this->clientErrno == AVAHI_ERR_DISCONNECTED) { // tear the client down and restart it - Lv2Log::info("Avahi connection lost. Reconnecting."); + Lv2Log::info("DNS/SD connection lost. Reconnecting."); if (group) { - avahi_entry_group_reset(group); + avahi_entry_group_free(group); group = nullptr; } @@ -272,28 +354,35 @@ void AvahiService::ClientCallback(AvahiClient *c, AvahiClientState state) if (!client) { Lv2Log::error(SS("Failed to create client: " << avahi_strerror(error))); + terminated = true; avahi_threaded_poll_quit(threadedPoll); + SetState(ServiceState::Failed); } } else { - Lv2Log::error(SS("Client failure: " << avahi_strerror(avahi_client_errno(c)))); + terminated = true; + Lv2Log::error(SS("DNS/SD client failure: " << avahi_strerror(avahi_client_errno(c)))); avahi_threaded_poll_quit(threadedPoll); + SetState(ServiceState::Failed); } break; case AVAHI_CLIENT_S_COLLISION: /* Let's drop our registered services. When the server is back - * in AVAHI_SERVER_RUNNING state we will register them - * again with the new host name. */ + * in AVAHI_SERVER_RUNNING state we will register them + * again with the new host name. */ case AVAHI_CLIENT_S_REGISTERING: /* The server records are now being established. This - * might be caused by a host name change. We need to wait - * for our own records to register until the host name is - * properly esatblished. */ + * might be caused by a host name change. We need to wait + * for our own records to register until the host name is + * properly esatblished. */ if (group) avahi_entry_group_reset(group); + SetState(ServiceState::Settling); + break; + case AVAHI_CLIENT_CONNECTING: + SetState(ServiceState::Settling); break; - case AVAHI_CLIENT_CONNECTING:; } } @@ -304,72 +393,159 @@ void AvahiService::Start() int ret = 1; struct timeval tv; this->clientErrno = 0; - while (true) + SetState(ServiceState::Initializing); + for (int retry = 0; retry < 3; ++retry) { /* Allocate main loop object */ - if (!(this->threadedPoll = avahi_threaded_poll_new())) + if (!this->threadedPoll) { - Lv2Log::error("Failed to create Avahi poll object."); - goto fail; + if (!(this->threadedPoll = avahi_threaded_poll_new())) + { + Lv2Log::error("Failed to create Avahi poll object."); + goto fail; + } + avahi_threaded_poll_start(threadedPoll); } - /* Allocate a new client */ - client = avahi_client_new(avahi_threaded_poll_get(threadedPoll), AvahiClientFlags::AVAHI_CLIENT_NO_FAIL, client_callback, (void *)this, &error); - /* Check wether creating the client object succeeded */ if (!client) { - Lv2Log::error(SS("Failed to create client: " << avahi_strerror(error))); - goto fail; + /* Allocate a new client */ + client = avahi_client_new(avahi_threaded_poll_get(threadedPoll), AvahiClientFlags::AVAHI_CLIENT_NO_FAIL, client_callback, (void *)this, &error); + /* Check wether creating the client object succeeded */ + if (!client) + { + Lv2Log::error(SS("Failed to create client: " << avahi_strerror(error))); + goto fail; + } } - avahi_threaded_poll_start(threadedPoll); ret = 0; return; fail: - /* Cleanup things */ - if (client) - { - avahi_client_free(client); - client = nullptr; - } - if (group) - { - avahi_entry_group_reset(group); - group = nullptr; - } - if (threadedPoll) - { - avahi_threaded_poll_free(threadedPoll); - threadedPoll = nullptr; - } + Stop(); + sleep(1); + SetState(ServiceState::Failed); } return; } void AvahiService::Stop() { + if (stopped) + return; + this->stopped = true; + + Unannounce(true); + sleep(1); // let traffic setting. avahi doens't seem to shut down cleany otherwise. if (threadedPoll) { + avahi_threaded_poll_lock(threadedPoll); + if (group) + { + avahi_entry_group_free(group); + group = nullptr; + } + avahi_threaded_poll_unlock(threadedPoll); + avahi_threaded_poll_lock(threadedPoll); + if (client) + { + avahi_client_free(client); + client = nullptr; + } + avahi_threaded_poll_unlock(threadedPoll); + avahi_threaded_poll_stop(threadedPoll); - } - /* Cleanup things */ - // client owns the group? - // if (group) - // { - // avahi_entry_group_reset(group); - // group = nullptr; - // }` - - if (client) - { - avahi_client_free(client); - client = nullptr; - } - if (threadedPoll) - { avahi_threaded_poll_free(threadedPoll); + SetState(ServiceState::Closed); threadedPoll = nullptr; } - group = nullptr; + + /* Cleanup things */ + if (avahiNameString) + { + avahi_free(avahiNameString); + avahiNameString = nullptr; + } + Lv2Log::info("DNS/SD service stopped."); } + +void AvahiService::Wait() +{ + using clock = std::chrono::steady_clock; + auto start = clock::now(); + auto waitTime = start + std::chrono::duration_cast(std::chrono::seconds(5)); + while (true) + { + bool done; + switch (serviceState) + { + default: + case ServiceState::Unitialized: + throw std::runtime_error("Invalid state"); + + case ServiceState::Initializing: + case ServiceState::Settling: + case ServiceState::Requested: + case ServiceState::Uncommited: + case ServiceState::Registering: + case ServiceState::Collision: + done = false; + break; + case ServiceState::Reset: + case ServiceState::Established: + case ServiceState::Failed: + case ServiceState::Closed: + done = true; + break; + }; + if (done) break; + if (clock::now() > waitTime) + { + Lv2Log::error("DNS/SD announcement timed out."); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } +} +void AvahiService::WaitForUnannounce() +{ + using clock = std::chrono::steady_clock; + auto start = clock::now(); + auto waitTime = start + std::chrono::duration_cast(std::chrono::seconds(5)); + + while (true) + { + bool done; + switch (serviceState) + { + default: + throw std::runtime_error("Invalid state"); + + case ServiceState::Initializing: + case ServiceState::Settling: + case ServiceState::Requested: + case ServiceState::Registering: + case ServiceState::Collision: + done = false; + break; + case ServiceState::Uncommited: + case ServiceState::Unitialized: + case ServiceState::Reset: + case ServiceState::Established: + case ServiceState::Failed: + case ServiceState::Closed: + done = true; + break; + }; + if (done) break; + if (clock::now() > waitTime) + { + Lv2Log::error("DNS/SD unannounce timed out."); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + +} + diff --git a/src/AvahiService.hpp b/src/AvahiService.hpp index e02db9e..ce7f607 100644 --- a/src/AvahiService.hpp +++ b/src/AvahiService.hpp @@ -26,10 +26,12 @@ #include #include +#include // forward declarations. class AvahiEntryGroup; class AvahiThreadedPoll; +#include #include @@ -38,13 +40,37 @@ namespace pipedal { class AvahiService { public: - ~AvahiService() { Unannounce(); } + ~AvahiService() { Stop(); } void Announce( - int portNumber, const std::string &name, const std::string &instanceId, const std::string&mdnsName,bool addTestGroup = false); - - void Unannounce(); + int portNumber, const std::string &name, const std::string &instanceId, const std::string&mdnsName, bool wait); + void Unannounce(bool wait); private: + enum class ServiceState { + Unitialized, + Initializing, + Settling, + Requested, + Uncommited, + Registering, + Collision, + Reset, + Established, + Failed, + Closed + }; + + std::atomic serviceState; + void Wait(); + void WaitForUnannounce(); + void SetState(ServiceState serviceState); + + std::atomic terminated = false; + bool stopped = false; + bool started = false; + bool createPending = false; + bool makeAnnouncement = false; + void Start(); void Stop(); static void entry_group_callback(AvahiEntryGroup*g, AvahiEntryGroupState state, void *userData); @@ -56,19 +82,17 @@ namespace pipedal { void create_group(AvahiClient *c); - void threadProc(); - int clientErrno = 0; int portNumber = -1; std::string instanceId; + std::string serviceName; std::string mdnsName; - bool addTestGroup = false; AvahiClient *client = NULL; AvahiEntryGroup *group = NULL; AvahiThreadedPoll *threadedPoll = NULL; - char*name = NULL; + char*avahiNameString = NULL; }; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7e58950..e0f61b3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -9,6 +9,8 @@ set(CXX_STANDARD 20) include(FindPkgConfig) +find_package(sdbus-c++ REQUIRED) + if(DEFINED ENV{GITHUB_ACTIONS}) message(STATUS "Building in GitHub Actions environment") @@ -94,7 +96,7 @@ endif() set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED True) -set (USE_SANITIZE False) +set (USE_SANITIZE true) if (!ENABLE_VST3) @@ -144,9 +146,12 @@ else() endif() set (PIPEDAL_SOURCES + RealtimeMidiEventType.hpp + DBusToLv2Log.cpp DBusToLv2Log.hpp + HotspotManager.cpp HotspotManager.hpp UpdateResults.cpp UpdateResults.hpp UpdaterSecurity.hpp - Updater.cpp Updater.hpp + Updater.cpp Updater.hpp UpdaterStatus.hpp Lv2PluginChangeMonitor.cpp Lv2PluginChangeMonitor.hpp WebServerConfig.cpp WebServerConfig.hpp Locale.hpp Locale.cpp @@ -190,7 +195,6 @@ set (PIPEDAL_SOURCES RequestHandler.hpp Scratch.cpp PluginHost.hpp PluginHost.cpp PluginType.hpp PluginType.cpp - Lv2Log.hpp Lv2Log.cpp PiPedalSocket.hpp PiPedalSocket.cpp PiPedalVersion.hpp PiPedalVersion.cpp PiPedalModel.hpp PiPedalModel.cpp @@ -252,6 +256,7 @@ set (PIPEDAL_INCLUDES ) set(PIPEDAL_LIBS libpipedald zip + PiPedalCommon icui18n icuuc icudata @@ -290,6 +295,16 @@ target_link_libraries(pipedald PRIVATE PiPedalCommon ) ################################# +add_executable(hotspotManagerTest + hotspotManagerTestMain.cpp + HotspotManager.cpp HotspotManager.hpp) + +target_link_libraries(hotspotManagerTest PRIVATE ${PIPEDAL_LIBS}) + +set_target_properties(hotspotManagerTest PROPERTIES EXCLUDE_FROM_ALL true) + + + add_executable(pipedaltest testMain.cpp InvertingMutexTest.cpp @@ -313,10 +328,12 @@ add_executable(pipedaltest testMain.cpp MemDebug.cpp MemDebug.hpp ) -target_link_libraries(pipedaltest PRIVATE PiPedalCommon) +target_link_libraries(pipedaltest PRIVATE ${PIPEDAL_LIBS}) target_include_directories(pipedaltest PRIVATE ${PIPEDAL_INCLUDES} ) +set_target_properties(pipedaltest PROPERTIES EXCLUDE_FROM_ALL true) + if (NOT GITHUB_ACTIONS) # Google perftools are not available on Github action servers. @@ -327,6 +344,9 @@ if (NOT GITHUB_ACTIONS) # Google perftools are not available on Github action se target_include_directories(profilePlugin PRIVATE ${PIPEDAL_INCLUDES} ) + + set_target_properties(profilePlugin PROPERTIES EXCLUDE_FROM_ALL true) + endif() add_executable(jsonTest testMain.cpp @@ -344,7 +364,12 @@ target_link_libraries(jsonTest PRIVATE PiPedalCommon) target_include_directories(jsonTest PRIVATE ${PIPEDAL_INCLUDES} ) +set_target_properties(jsonTest PROPERTIES EXCLUDE_FROM_ALL true) + + target_link_libraries(jsonTest PRIVATE pthread) + + add_test(NAME jsonTest COMMAND jsonTest) @@ -549,18 +574,18 @@ if(ENABLE_VST3) ) - add_executable(vst3test Vst3test.cpp + # add_executable(vst3test Vst3test.cpp - MemDebug.cpp - MemDebug.hpp + # MemDebug.cpp + # MemDebug.hpp - Vst3SdkRepro.cpp + # Vst3SdkRepro.cpp - ${VST3_FILES} + # ${VST3_FILES} - #vst3/Vst3PresetFile.hpp Vst3PresetFile.cpp - asan_options.cpp - ) + # #vst3/Vst3PresetFile.hpp Vst3PresetFile.cpp + # asan_options.cpp + # ) target_include_directories(vst3test PRIVATE ${PIPEDAL_INCLUDES} ) @@ -596,7 +621,6 @@ add_executable(pipedalconfig SystemConfigFile.hpp SystemConfigFile.cpp WifiChannelSelectors.cpp WifiChannelSelectors.hpp asan_options.cpp - Lv2Log.cpp Lv2Log.hpp ) @@ -609,7 +633,6 @@ add_executable(pipedal_latency_test PiLatencyMain.cpp PiPedalAlsa.hpp PiPedalAlsa.cpp asan_options.cpp - Lv2Log.cpp Lv2Log.hpp AlsaDriver.cpp AlsaDriver.hpp JackConfiguration.hpp JackConfiguration.cpp JackServerSettings.hpp JackServerSettings.cpp @@ -632,13 +655,23 @@ target_include_directories(capturepresets PRIVATE ${PIPEDAL_INCLUDES}) target_link_libraries(capturepresets ${PIPEDAL_LIBS}) +add_executable(makeRelease + makeReleaseMain.cpp + CommandLineParser.hpp +) +target_include_directories(makeRelease PRIVATE ${PIPEDAL_INCLUDES}) + +target_link_libraries(makeRelease ${PIPEDAL_LIBS}) + + + + add_executable(pipedal_update UpdateMain.cpp UpdateResults.cpp UpdateResults.hpp - Lv2Log.hpp Lv2Log.cpp Lv2SystemdLogger.cpp Lv2SystemdLogger.hpp - UpdateResults.cpp UpdateResults.hpp + TemporaryFile.cpp TemporaryFile.hpp AdminInstallUpdate.cpp AdminInstallUpdate.hpp ) @@ -652,7 +685,6 @@ add_executable(pipedaladmind AdminMain.cpp CommandLineParser.hpp JackServerSettings.hpp JackServerSettings.cpp Lv2SystemdLogger.hpp Lv2SystemdLogger.cpp - Lv2Log.cpp Lv2Log.hpp SystemConfigFile.hpp SystemConfigFile.cpp CpuGovernor.cpp CpuGovernor.hpp asan_options.cpp diff --git a/src/CapturePresetsMain.cpp b/src/CapturePresetsMain.cpp index 7f9dd4f..79d0bbe 100644 --- a/src/CapturePresetsMain.cpp +++ b/src/CapturePresetsMain.cpp @@ -1,3 +1,22 @@ +// 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 +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + #include "Storage.hpp" #include "CommandLineParser.hpp" #include diff --git a/src/ConfigMain.cpp b/src/ConfigMain.cpp index d330149..f3d860a 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 @@ -22,6 +22,8 @@ #include #include #include "CommandLineParser.hpp" +#include "SystemConfigFile.hpp" + #include #include #include "WriteTemplateFile.hpp" @@ -42,6 +44,7 @@ #include #include "ofstream_synced.hpp" +#define P2PD_DISABLED #if JACK_HOST #define INSTALL_JACK_SERVICE 1 @@ -64,6 +67,7 @@ namespace fs = std::filesystem; #define JACK_SERVICE_ACCOUNT_NAME "jack" #define AUDIO_SERVICE_GROUP_NAME "audio" #define JACK_SERVICE_GROUP_NAME AUDIO_SERVICE_GROUP_NAME +#define NETDEV_GROUP_NAME "netdev" #define SYSTEMCTL_BIN "/usr/bin/systemctl" #define GROUPADD_BIN "/usr/sbin/groupadd" @@ -151,6 +155,86 @@ void DisableService() #endif } +static void RestartAvahiService() +{ + silentSysExec(SS(SYSTEMCTL_BIN << " restart avahi-daemon.service").c_str()); +} +static void AvahiInstall() +{ + // disable IPv6 mdns broadcasts. Avahi broadcasts link-local IPV6 addresses which are unusually difficult to deal with. + + try + { + std::filesystem::path avahiConfig("/etc/avahi/avahi-daemon.conf"); + SystemConfigFile avahi(avahiConfig); + + bool changed = avahi.RemoveUndoableActions(); + int line = avahi.GetLineThatStartsWith("use-ipv6=yes"); + if (line != -1) + { + avahi.UndoableReplaceLine(line, "use-ipv6=no"); + changed = true; + } + else + { + if (avahi.GetLineThatStartsWith("use-ipv6=no") == -1) + { + line = avahi.GetLineThatStartsWith("[server]"); + if (line == 1) + { + throw std::runtime_error("Unable to find [server] section."); + } + { + // increment to end of section. + while (line < avahi.GetLineCount()) + { + const auto &txt = avahi.Get(line); + if (txt.empty()) + { + break; + } + if (txt.starts_with("[")) // start of next section. + { + break; + } + ++line; + } + } + avahi.UndoableAddLine(avahi.GetLineCount(), "use-ipv6=no"); + changed = true; + } + } + if (changed) + { + avahi.Save(); + RestartAvahiService(); + } + } + catch (const std::exception &e) + { + cout << "Warning: Unabled to disable Ipv6 mDNS announcements. " << e.what() << endl; + } +} + +static void AvahiUninstall() +{ + try + { + std::filesystem::path avahiConfig("/etc/avahi/avahi-daemon.conf"); + SystemConfigFile avahi(avahiConfig); + + if (avahi.RemoveUndoableActions()) + { + avahi.Save(); + RestartAvahiService(); + } + } + catch (const std::exception &e) + { + cout << " Warning: Unable to restore Avahi Daemon configuration. " << e.what() << endl; + } +} + void StopService(bool excludeShutdownService = false) { if (sysExec(SYSTEMCTL_BIN " stop " PIPEDALD_SERVICE ".service") != EXIT_SUCCESS) @@ -468,6 +552,11 @@ static bool IsP2pServiceEnabled() void Uninstall() { + // if NetworkManager isn't installed, Install will have failed. + // Do cleanup as if the Isntall had not failed. + + PretendNetworkManagerIsInstalled(); + try { OnWifiUninstall(true); @@ -526,6 +615,8 @@ void Uninstall() { } UninstallPamEnv(); + AvahiUninstall(); + // UninstallLimits(); sysExec(SYSTEMCTL_BIN " daemon-reload"); } @@ -676,7 +767,8 @@ static void FixPermissions() struct passwd *passwd; if ((passwd = getpwnam("pipedal_d")) == nullptr) { - cout << "Error: " << "User 'pipedal_d' does not exist." << endl; + cout << "Error: " + << "User 'pipedal_d' does not exist." << endl; return; } uid = passwd->pw_uid; @@ -771,7 +863,8 @@ void DeployVarConfig() void InstallPgpKey() { fs::path homeDir = "/var/pipedal/config/gpg"; - fs::path keyPath = "/etc/pipedal/config/updatekey.gpg"; + fs::path keyPath = "/etc/pipedal/config/updatekey.asc"; + fs::path keyPath2 = "/etc/pipedal/config/updatekey2.asc"; fs::create_directories(homeDir); std::ignore = chmod(homeDir.c_str(), 0700); @@ -781,15 +874,24 @@ void InstallPgpKey() s << (CHOWN_BIN " " SERVICE_GROUP_NAME ":" SERVICE_GROUP_NAME " ") << homeDir.c_str(); sysExec(s.str().c_str()); } - std::ostringstream ss; - ss << "/usr/bin/gpg --homedir " << homeDir.c_str() << " --import " << keyPath.c_str(); - - int rc = silentSysExec(ss.str().c_str()); - if (rc != EXIT_SUCCESS) { - cout << "Error: Failed to create update keyring." << endl; + std::ostringstream ss; + ss << "/usr/bin/gpg --homedir " << homeDir.c_str() << " --import " << keyPath.c_str(); + int rc = silentSysExec(ss.str().c_str()); + if (rc != EXIT_SUCCESS) + { + cout << "Error: Failed to create update keyring." << endl; + } + } + { + std::ostringstream ss; + ss << "/usr/bin/gpg --homedir " << homeDir.c_str() << " --import " << keyPath2.c_str(); + int rc = silentSysExec(ss.str().c_str()); + if (rc != EXIT_SUCCESS) + { + cout << "Error: Failed to add update key." << endl; + } } - { std::stringstream ss; ss << (CHOWN_BIN " -R " SERVICE_GROUP_NAME ":" SERVICE_GROUP_NAME " ") << homeDir.c_str(); @@ -800,8 +902,16 @@ void InstallPgpKey() void Install(const fs::path &programPrefix, const std::string endpointAddress) { cout << "Configuring pipedal" << endl; + + if (!UsingNetworkManager()) + { + throw std::runtime_error("The current OS is not using NetworkManager. Services not configured."); + } try { + // apply policy changes we dropped into the polkit configuration files (allows pipedal_d to use NetworkManager dbus apis). + silentSysExec(SYSTEMCTL_BIN " restart polkit"); + DeployVarConfig(); if (sysExec(GROUPADD_BIN " -f " AUDIO_SERVICE_GROUP_NAME) != EXIT_SUCCESS) @@ -813,6 +923,7 @@ void Install(const fs::path &programPrefix, const std::string endpointAddress) throw std::runtime_error("Failed to create pipedald service group."); } // defensively disable wifi p2p if some leftover config file left it enabled. +#ifdef P2PD_DISABLED try { if (IsP2pServiceEnabled()) @@ -826,6 +937,7 @@ void Install(const fs::path &programPrefix, const std::string endpointAddress) catch (const std::exception &) { } +#endif InstallAudioService(); auto endpos = endpointAddress.find_last_of(':'); @@ -876,6 +988,9 @@ void Install(const fs::path &programPrefix, const std::string endpointAddress) // Add to audio groups. sysExec(USERMOD_BIN " -a -G " AUDIO_SERVICE_GROUP_NAME " " SERVICE_ACCOUNT_NAME); + // add to netdev group + sysExec(USERMOD_BIN " -a -G " NETDEV_GROUP_NAME " " SERVICE_ACCOUNT_NAME); + // create and configure /var directory. @@ -951,7 +1066,7 @@ void Install(const fs::path &programPrefix, const std::string endpointAddress) } s << (programPrefix / "sbin" / PIPEDALD_SERVICE).string() - << " /etc/pipedal/config /etc/pipedal/react -port " << endpointAddress << " -systemd"; + << " /etc/pipedal/config /etc/pipedal/react -systemd"; map["COMMAND"] = s.str(); } @@ -995,12 +1110,16 @@ void Install(const fs::path &programPrefix, const std::string endpointAddress) sysExec(SYSTEMCTL_BIN " daemon-reload"); FixPermissions(); - RestartService(false); + + StopService(false); + AvahiInstall(); + InstallPgpKey(); + StartService(false); + EnableService(); - // Restart WiFi Direct if neccessary. + // Restart Wi-Fi Direct if neccessary. OnWifiReinstall(); - InstallPgpKey(); } catch (const std::exception &e) { @@ -1026,7 +1145,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 " @@ -1051,10 +1170,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" @@ -1063,68 +1182,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:" @@ -1138,28 +1243,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) @@ -1200,6 +1288,14 @@ static int ListP2PChannels(const std::vector &arguments) return EXIT_SUCCESS; } +void RequireNetworkManager() +{ + if (!UsingNetworkManager()) + { + throw std::runtime_error("The current OS is not using NetworkManager."); + } +} + int main(int argc, char **argv) { CommandLineParser parser; @@ -1209,7 +1305,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; @@ -1218,6 +1314,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); @@ -1231,22 +1330,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."); @@ -1260,7 +1354,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) { @@ -1268,6 +1362,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) { @@ -1277,7 +1388,7 @@ int main(int argc, char **argv) } if (helpError) { - cout << "\n"; + cout << endl; return EXIT_FAILURE; // don't scroll the error off the screen. } @@ -1317,104 +1428,165 @@ int main(int argc, char **argv) } if (install) { - fs::path prefix; - if (prefixOption.length() != 0) - { - prefix = fs::path(prefixOption); - } - else - { - prefix = fs::path(argv[0]).parent_path().parent_path(); - - fs::path pipedalPath = prefix / "sbin" / "pipedald"; - if (!fs::exists(pipedalPath)) + try { + fs::path prefix; + if (prefixOption.length() != 0) { - std::stringstream s; - s << "Can't find pipedald executable at " << pipedalPath << ". Try again using the -prefix option."; - throw std::runtime_error(s.str()); + prefix = fs::path(prefixOption); + } + else + { + prefix = fs::path(argv[0]).parent_path().parent_path(); + + fs::path pipedalPath = prefix / "sbin" / "pipedald"; + if (!fs::exists(pipedalPath)) + { + std::stringstream s; + s << "Can't find pipedald executable at " << pipedalPath << ". Try again using the -prefix option."; + throw std::runtime_error(s.str()); + } } - } - if (portOption == "") - { - portOption = GetCurrentWebServicePort(); if (portOption == "") { - portOption = "80"; + portOption = GetCurrentWebServicePort(); + if (portOption == "") + { + portOption = "80"; + } } - } - if (portOption.find(':') == string::npos) + if (portOption.find(':') == string::npos) + { + portOption = "0.0.0.0:" + portOption; + } + Install(prefix, portOption); + FileSystemSync(); + + } catch (const std::exception&e) { - portOption = "0.0.0.0:" + portOption; + cout << "ERROR: " << e.what() << endl; + FileSystemSync(); + return EXIT_SUCCESS; // say we succeeded so we don't put APT into a hellish state. } - Install(prefix, portOption); - FileSystemSync(); } else if (uninstall) { - Uninstall(); - FileSystemSync(); + try { + Uninstall(); + FileSystemSync(); + } catch (const std::exception &e) + { + cout << "ERROR: " << e.what() << endl; + FileSystemSync(); + return EXIT_SUCCESS; // Say we succeeds so that we don't put APT into a hellish state. + } } else if (stop) { + RequireNetworkManager(); StopService(); + } else if (start) { + RequireNetworkManager(); StartService(); } else if (restart) { + RequireNetworkManager(); RestartService(excludeShutdownService); } else if (enable) { + RequireNetworkManager(); EnableService(); + FileSystemSync(); + } else if (disable) { + RequireNetworkManager(); DisableService(); + FileSystemSync(); + } 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; - } + throw std::runtime_error("Wi-Fi p2p connections are no longer supported. Use hotspots instead."); + // 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) { + RequireNetworkManager(); WifiDirectConfigSettings settings; - settings.valid_ = true; + settings.Load(); settings.enable_ = false; SetWifiDirectConfig(settings); + + RestartService(true); + return EXIT_SUCCESS; } - else if (enable_ap) + else if (enable_hotspot) { + RequireNetworkManager(); + 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."); + } + FileSystemSync(); } - else if (disable_ap) + else if (disable_hotspot) { + RequireNetworkManager(); + 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."); + } + FileSystemSync(); } } catch (const std::exception &e) diff --git a/src/DBusToLv2Log.cpp b/src/DBusToLv2Log.cpp new file mode 100644 index 0000000..62b1c0e --- /dev/null +++ b/src/DBusToLv2Log.cpp @@ -0,0 +1,55 @@ +// Copyright (c) 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 +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#include "DBusToLv2Log.hpp" +#include "DBusLog.hpp" +#include "Lv2Log.hpp" + +using namespace pipedal; + +class DBus2ToLv2Logger : public IDBusLogger +{ + +public: + virtual void LogError(const std::string &message) override + { + Lv2Log::error(message); + } + virtual void LogWarning(const std::string &message) override + { + Lv2Log::warning(message); + } + virtual void LogInfo(const std::string &message) override + { + Lv2Log::info(message); + } + virtual void LogDebug(const std::string &message) override + { + Lv2Log::debug(message); + } + virtual void LogTrace(const std::string &message) override + { + Lv2Log::debug(message); + } +}; + +void pipedal::DbusLogToLv2Log() +{ + SetDBusLogger(std::make_unique()); +} diff --git a/src/DBusToLv2Log.hpp b/src/DBusToLv2Log.hpp new file mode 100644 index 0000000..5abfb9d --- /dev/null +++ b/src/DBusToLv2Log.hpp @@ -0,0 +1,25 @@ +// Copyright (c) 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 +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +#pragma once + +namespace pipedal { + void DbusLogToLv2Log(); +} \ No newline at end of file diff --git a/src/HotspotManager.cpp b/src/HotspotManager.cpp new file mode 100644 index 0000000..bb7b6a9 --- /dev/null +++ b/src/HotspotManager.cpp @@ -0,0 +1,989 @@ +// Copyright (c) 2022 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 +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#include "HotspotManager.hpp" +#include +#include "Lv2Log.hpp" +#include "DBusLog.hpp" +#include "DBusEvent.hpp" +#include "DBusDispatcher.hpp" +#include "NetworkManagerInterfaces.hpp" +#include "ss.hpp" +#include "WifiConfigSettings.hpp" +#include "ServiceConfiguration.hpp" +#include +#include +#include + +using namespace pipedal; +using namespace dbus::networkmanager; + +namespace pipedal::impl +{ + + constexpr std::string PIPEDAL_HOTSPOT_NAME = "PiPedal Hotspot"; + + class HotspotManagerImpl : public HotspotManager + { + + public: + using ssid_t = WifiConfigSettings::ssid_t; + + HotspotManagerImpl(); + virtual ~HotspotManagerImpl() noexcept; + + virtual void Open() override; + 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; + + virtual void SetNetworkChangingListener(NetworkChangingListener &&listener) override; + + private: + enum class State + { + Initial, + WaitingForNetworkManager, + Monitoring, + HotspotConnecting, + HotspotConnected, + Error, + Closed + }; + void SetState(State state); + State state = State::Initial; + + void onClose(); + void onError(const std::string &message); + void onInitialize(); + void onDevicesChanged(); + void onDisconnect(); + void onReload(); + + void DisableHotspot(); + void EnableHotspot(); + void StartHotspot(); + void StopHotspot(); + void onStartMonitoring(); + void onAccessPointChanged(); + void onAccessPointsChanged(); + void CancelAccessPointsChangedTimer(); + void WaitForNetworkManager(); + void StartWaitForNetworkManagerTimer(); + void CancelWaitForNetworkManagerTimer(); + void UpdateNetworkManagerStatus(); + void CancelDeviceChangedTimer(); + void ReleaseNetworkManager(); + void OnEthernetStateChanged(uint32_t state); + void OnWlanStateChanged(uint32_t state); + std::vector GetAllAccessPoints(); + std::vector GetAllAutoConnectSsids(); + std::vector GetKnownVisibleAccessPoints(const std::vector &allAccessPoints); + + void UpdateKnownNetworks( + std::vector &knownSsids, + std::vector &allAccessPoints); + + void FireNetworkChanging(); + + void MaybeStartHotspot(); + + void StartScanTimer(); + void StopScanTimer(); + void ScanNow(); + + Connection::ptr FindExistingConnection(); + + Device::ptr GetDevice(uint32_t nmDeviceType); + std::atomic closed; + DBusDispatcher dbusDispatcher; + + std::unique_ptr thread; + void ThreadProc(); + WifiConfigSettings wifiConfigSettings; + NetworkManager::ptr networkManager; + Device::ptr ethernetDevice; + Device::ptr wlanDevice; + DeviceWireless::ptr wlanWirelessDevice; + ActiveConnection::ptr activeConnection; + + std::recursive_mutex networkChangingListenerMutex; + NetworkChangingListener networkChangingListener; + + std::mutex knownWifiNetworksMutex; + std::vector knownWifiNetworks; + + bool ethernetConnected = true; + bool wlanConnected = true; + + DBusEventHandle onDeviceAddedHandle = INVALID_DBUS_EVENT_HANDLE; + DBusEventHandle onDeviceRemovedHandle = INVALID_DBUS_EVENT_HANDLE; + DBusDispatcher::PostHandle devicesChangedTimerHandle = 0; + DBusDispatcher::PostHandle networkManagerTimerHandle = 0; + DBusDispatcher::PostHandle accessPointsChangedTimerHandle = 0; + DBusDispatcher::PostHandle scanTimerHandle = 0; + }; +} +using namespace pipedal::impl; + +HotspotManagerImpl::HotspotManagerImpl() +{ +} + +void HotspotManagerImpl::Open() +{ + + dbusDispatcher.Run(); + dbusDispatcher.Post( + [this]() + { + UpdateNetworkManagerStatus(); // waits for valid network manager state, and calls onInitialize when ready. + }); +} + +void HotspotManagerImpl::onClose() +{ + this->closed = true; // avoids a memory barrier probelm. + + CancelDeviceChangedTimer(); + CancelWaitForNetworkManagerTimer(); + CancelAccessPointsChangedTimer(); + + + if (networkManager && activeConnection) + { + try { + networkManager->DeactivateConnection(activeConnection->getObjectPath()); + activeConnection = nullptr; + } catch (const std::exception&e) + { + // nothrow. + } + } + + ReleaseNetworkManager(); + + Lv2Log::debug("HotspotManager: state=Closed"); + SetState(State::Closed); +} +void HotspotManagerImpl::onError(const std::string &message) +{ + Lv2Log::error(message); + ReleaseNetworkManager(); + Lv2Log::debug("HotspotManager: state=Error"); + + SetState(State::Error); +} + +void HotspotManagerImpl::SetState(State state) +{ + this->state = state; +} +void HotspotManagerImpl::onInitialize() +{ + if (closed) + return; + if (state != State::Initial && state != State::WaitingForNetworkManager) + { + onError("Invalid state (onInitialize)."); + return; + } + try + { + wifiConfigSettings.Load(); + + onStartMonitoring(); + } + catch (const std::exception &e) + { + onError(SS("HotspotManager: " << e.what())); + } +} +void HotspotManagerImpl::onDisconnect() +{ + if (state != State::Initial && state != State::WaitingForNetworkManager) + { + WaitForNetworkManager(); + } +} + +void HotspotManagerImpl::UpdateNetworkManagerStatus() +{ + try + { + if (!networkManager) + { + networkManager = NetworkManager::Create(dbusDispatcher); + } + // turn on wifi if required. + auto ethernetDevice = GetDevice(NM_DEVICE_TYPE_ETHERNET); + auto wlanDevice = GetDevice(NM_DEVICE_TYPE_WIFI); + + if (ethernetDevice && wlanDevice) + { + if (state == State::Initial || state == State::WaitingForNetworkManager) + { + CancelWaitForNetworkManagerTimer(); + this->onInitialize(); + } + return; + } + + // check to see whether we have BOTH an eth0 device and a wlan0 device. + } + catch (const std::exception &e) + { + } + if (state != State::WaitingForNetworkManager) + { + this->WaitForNetworkManager(); + } + else + { + this->StartWaitForNetworkManagerTimer(); + } +} + +void HotspotManagerImpl::CancelDeviceChangedTimer() +{ + if (devicesChangedTimerHandle) + { + dbusDispatcher.CancelPost(devicesChangedTimerHandle); + devicesChangedTimerHandle = 0; + } +} +void HotspotManagerImpl::ReleaseNetworkManager() +{ + StopScanTimer(); + CancelAccessPointsChangedTimer(); + CancelWaitForNetworkManagerTimer(); + CancelDeviceChangedTimer(); + + DisableHotspot(); + + ethernetDevice = nullptr; + wlanDevice = nullptr; + wlanWirelessDevice = nullptr; + networkManager = nullptr; + + this->onDeviceRemovedHandle = INVALID_DBUS_EVENT_HANDLE; + this->onDeviceAddedHandle = INVALID_DBUS_EVENT_HANDLE; +} + +void HotspotManagerImpl::onDevicesChanged() +{ + UpdateNetworkManagerStatus(); +} +Device::ptr HotspotManagerImpl::GetDevice(uint32_t nmDeviceType) +{ + const auto &allDevices = networkManager->GetAllDevices(); + for (const auto &devicePath : allDevices) + { + auto device = Device::Create(dbusDispatcher, devicePath); + if (device->DeviceType() == nmDeviceType) + { + return device; + } + } + return nullptr; +} +void HotspotManagerImpl::OnEthernetStateChanged(uint32_t state) +{ + bool newValue = (state == 100); + if (newValue != this->ethernetConnected) + { + this->ethernetConnected = newValue; + Lv2Log::debug(SS("HotspotMonitor: ethernetConnected=" << ethernetConnected)); + FireNetworkChanging(); + MaybeStartHotspot(); + } +} +void HotspotManagerImpl::OnWlanStateChanged(uint32_t state) +{ + this->wlanConnected = (state == 100); + Lv2Log::debug(SS("HotspotMonitor: OnWlanStateChanged")); + MaybeStartHotspot(); +} + +void HotspotManagerImpl::onStartMonitoring() +{ + try + { + ReleaseNetworkManager(); + + this->networkManager = NetworkManager::Create(dbusDispatcher); + + if (!networkManager->WirelessEnabled()) + { + networkManager->WirelessEnabled(true); + } + + this->onDeviceAddedHandle = this->networkManager->OnDeviceAdded.add( + [this](const sdbus::ObjectPath &) + { + onDevicesChanged(); + }); + this->onDeviceRemovedHandle = this->networkManager->OnDeviceRemoved.add( + [this](const sdbus::ObjectPath &objectPath) + { + if (this->ethernetDevice && this->ethernetDevice->getObjectPath() == objectPath) + { + onDisconnect(); + } + else if (this->wlanDevice && this->wlanDevice->getObjectPath() == objectPath) + { + onDisconnect(); + } + }); + + ethernetDevice = GetDevice(NM_DEVICE_TYPE_ETHERNET); + if (!ethernetDevice) + { + throw std::runtime_error("eth0 device not found."); + } + this->ethernetDevice->OnStateChanged.add( + [this](uint32_t, uint32_t, uint32_t) + { + OnEthernetStateChanged(ethernetDevice->State()); + }); + + wlanDevice = GetDevice(NM_DEVICE_TYPE_WIFI); + if (!wlanDevice) + { + throw std::runtime_error("wlan0 device not found."); + } + wlanWirelessDevice = DeviceWireless::Create(dbusDispatcher, wlanDevice->getObjectPath()); + + wlanWirelessDevice->OnAccessPointAdded.add( + [this](const sdbus::ObjectPath &accessPoint) + { + onAccessPointChanged(); + }); + wlanWirelessDevice->OnAccessPointRemoved.add( + [this](const sdbus::ObjectPath &accessPoint) + { + onAccessPointChanged(); + }); + + this->wlanDevice->OnStateChanged.add( + [this](uint32_t, uint32_t, uint32_t) + { + OnWlanStateChanged(ethernetDevice->State()); + }); + Lv2Log::debug("HotspotManager: state=Monitoring"); + SetState(State::Monitoring); + + Lv2Log::info("HotspotManager: Monitoring network status."); + + OnEthernetStateChanged(ethernetDevice->State()); + OnWlanStateChanged(wlanDevice->State()); + + StartScanTimer(); + onAccessPointChanged(); + } + catch (const std::exception &e) + { + Lv2Log::error(SS("HotspotManager: " << e.what())); + WaitForNetworkManager(); + } +} + +void HotspotManagerImpl::StartWaitForNetworkManagerTimer() +{ + + CancelWaitForNetworkManagerTimer(); + networkManagerTimerHandle = dbusDispatcher.PostDelayed( + std::chrono::seconds(5), + [this]() + { + networkManagerTimerHandle = 0; + this->UpdateNetworkManagerStatus(); + }); +} +void HotspotManagerImpl::CancelWaitForNetworkManagerTimer() +{ + + if (networkManagerTimerHandle) + { + dbusDispatcher.CancelPost(networkManagerTimerHandle); + networkManagerTimerHandle = 0; + } +} + +void HotspotManagerImpl::WaitForNetworkManager() +{ + Lv2Log::debug("HotspotManager: state=WaitingForNetworkManager"); + SetState(State::WaitingForNetworkManager); + ReleaseNetworkManager(); + StartWaitForNetworkManagerTimer(); +} +void HotspotManagerImpl::onReload() +{ + if (closed) + return; + WifiConfigSettings oldSettings = this->wifiConfigSettings; + ; + wifiConfigSettings.Load(); + + switch (state) + { + case State::Initial: + case State::Error: + // ignore. + return; + default: + MaybeStartHotspot(); + return; + } +} + +HotspotManagerImpl::~HotspotManagerImpl() +{ + Close(); +} + +void HotspotManagerImpl::ThreadProc() +{ + try + { + } + catch (const std::exception &e) + { + Lv2Log::error(SS("Hotspot Manager thread terminated. " << e.what())); + } +} +void HotspotManagerImpl::Reload() +{ + dbusDispatcher.Post([this]() + { onReload(); }); +} +void HotspotManagerImpl::Close() +{ + if (!closed) + { + closed = true; + dbusDispatcher.Post([this] + { onClose(); }); + dbusDispatcher.Stop(); + } +} + +HotspotManager::ptr HotspotManager::Create() +{ + return std::make_unique(); +} + +template +class VectorHash +{ +public: + std::size_t operator()(const std::vector &vec) const + { + std::size_t hash = vec.size(); + for (auto &i : vec) + { + hash += static_cast(i); + hash = ((hash >> 32) ^ hash) * 0x119de1F3; + } + return hash; + } +}; + +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::map map; + + for (auto &knownSsid : knownSsids) + { + std::string ssid = ssidToString(knownSsid); + if (ssid.length() != 0) + { + NetworkSortRecord &record = map[ssid]; + record.ssid = ssid; + record.knownNetwork = true; + } + } + for (auto &accessPoint : allAccessPoints) + { + try { + 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; + } + } catch (const std::exception&ignored) + { + // race to get the info before it changes. np. + } + } + 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::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->GetAllAutoConnectSsids(); + + std::unordered_set, VectorHash> index{knownSsids.begin(), knownSsids.end()}; + + std::vector result; + std::vector accessPoints; + + for (const auto &accessPoint : allAccessPoints) + { + if (index.contains(accessPoint)) + { + result.push_back(accessPoint); + } + } + return result; +} + +std::vector> HotspotManagerImpl::GetAllAutoConnectSsids() +{ + auto availableConnections = wlanDevice->AvailableConnections(); + std::vector> ssids; + + for (const auto &connectionPath : availableConnections) + { + auto connection = Connection::Create(dbusDispatcher, connectionPath); + try + { + auto settings = connection->GetSettings(); + bool autoConnect = true; + if (settings["connect"].count("autoconnect") > 0) + { + autoConnect = settings["connect"]["autoconnect"]; + } + bool isInfrastructure = + settings["802-11-wireless"].count("mode") > 0 && settings["802-11-wireless"]["mode"].get() == "infrastructure"; + if (isInfrastructure && autoConnect) + { + std::vector ssid = + settings["802-11-wireless"]["ssid"]; + ssids.push_back(std::move(ssid)); + } + } + catch (const std::exception &ignored) + { + // not totally sure of structure of all connection types. + } + } + return ssids; +} + +void HotspotManagerImpl::onAccessPointsChanged() +{ + MaybeStartHotspot(); +} + +void HotspotManagerImpl::CancelAccessPointsChangedTimer() +{ + if (accessPointsChangedTimerHandle) + { + dbusDispatcher.CancelPost(accessPointsChangedTimerHandle); + accessPointsChangedTimerHandle = 0; + } +} +void HotspotManagerImpl::onAccessPointChanged() +{ + // coalesce large bursts of AccessPointChanged calls using a timer. + if (!accessPointsChangedTimerHandle) + { + accessPointsChangedTimerHandle = dbusDispatcher.PostDelayed( + std::chrono::milliseconds(512), + [this]() + { + accessPointsChangedTimerHandle = 0; + onAccessPointsChanged(); // coalesced handling of one or more changes. + }); + } +} + +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() +{ + if (this->state == State::Error) + return; + if (this->closed) + return; + + if (!wlanDevice || !wlanWirelessDevice) + { + // devices are transitioning. Do nothing. + return; + } + 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); + + bool wantsHotspot = this->wifiConfigSettings.WantsHotspot( + + this->ethernetConnected, connectableSsids, allAccessPointSsids); + + if (this->state == State::Monitoring && wantsHotspot) + { + StartHotspot(); + } + else + { + if ((!wantsHotspot) && (this->state == State::HotspotConnected || this->state == State::HotspotConnecting)) + { + StopHotspot(); + } + } +} + +void HotspotManagerImpl::EnableHotspot() +{ +} + +void HotspotManagerImpl::DisableHotspot() +{ + StopHotspot(); +} + +Connection::ptr HotspotManagerImpl::FindExistingConnection() +{ + if (wlanDevice) + { + for (const auto &connectionPath : wlanDevice->AvailableConnections()) + { + auto connection = Connection::Create(dbusDispatcher, connectionPath); + auto settings = connection->GetSettings(); + bool isHotspot = + settings["802-11-wireless"].count("mode") > 0 && settings["802-11-wireless"]["mode"].get() == "ap"; + if (isHotspot) + { + std::string id; + if (settings["connection"].count("id") > 0) + { + id = settings["connection"]["id"].get(); + } + if (id.starts_with(PIPEDAL_HOTSPOT_NAME)) + { + return connection; + } + } + } + } + + return nullptr; +} + +void HotspotManagerImpl::StartHotspot() +{ + if (this->state == State::Monitoring) + { + try + { + ServiceConfiguration serviceConfiguration; + serviceConfiguration.Load(); + + Lv2Log::debug("HotspotManager: state=HotspotConnecting"); + SetState(State::HotspotConnecting); + + Lv2Log::info("HotspotManager: Enabling PiPedal hotspot."); + + // do it ONLY if we're in monitoring state. + // Create a proxy for NetworkManager + // Create connection settings for the hotspot + std::map> settings; + + std::map &connection = settings["connection"]; + + connection["type"] = "802-11-wireless"; + connection["autoconnect"] = false; + connection["id"] = PIPEDAL_HOTSPOT_NAME; + connection["interface-name"] = wlanDevice->Interface(); + connection["uuid"] = serviceConfiguration.uuid; + + std::map &wireless = settings["802-11-wireless"]; + + std::string ssid = this->wifiConfigSettings.hotspotName_; + std::vector vSsid{ssid.begin(), ssid.end()}; + wireless["ssid"] = vSsid; + wireless["mode"] = "ap"; + wireless["band"] = "a"; + wireless["band"] = "bg"; + + uint32_t iChannel = 0; + auto channel = this->wifiConfigSettings.channel_; + if (channel.length() != 0) + { + std::stringstream ss{channel}; + ss >> iChannel; + } + if (iChannel != 0) + { + wireless["channel"] = iChannel; + } + + std::map &wirelessSecurity = settings["802-11-wireless-security"]; + wirelessSecurity["key-mgmt"] = "wpa-psk"; + wirelessSecurity["psk"] = wifiConfigSettings.password_; + + settings["ipv4"]["method"] = "shared"; + settings["ipv6"]["method"] = "shared"; + // settings["ipv6"]["addr-gen-mode"] = "stable-privacy"; + + std::map options; + options["persist"] = "disk"; + + Connection::ptr existingConnection = FindExistingConnection(); + if (existingConnection) + { + existingConnection->Update(settings); + auto activeConnectionPath = networkManager->ActivateConnection( + existingConnection->getObjectPath(), + wlanDevice->getObjectPath(), + "/"); + this->activeConnection = ActiveConnection::Create(dbusDispatcher, activeConnectionPath); + } + else + { + + // Call AddAndActivateConnection2 method to create and activate the hotspot + sdbus::ObjectPath nullPath("/"); + auto result = networkManager->AddAndActivateConnection2( + settings, wlanDevice->getObjectPath(), "/", options + + ); + auto connectionPath = std::get<0>(result); + auto activeConnectionPath = std::get<1>(result); + // auto resultArgs = std::get<2>(result); + + this->activeConnection = ActiveConnection::Create(dbusDispatcher, activeConnectionPath); + } + SetState(State::HotspotConnected); + Lv2Log::info("HotspotManager: Hotspot activated."); + + FireNetworkChanging(); + } + catch (const std::exception &e) + { + onError(SS("HotspotManager: Activation failed: " << e.what())); + } + } + else + { + onError("HotspotManager: Illegal state (StartHotspot)"); + } +} +void HotspotManagerImpl::StopHotspot() +{ + // do it regardless of state. + try + { + if (activeConnection) + { + FireNetworkChanging(); + networkManager->DeactivateConnection(activeConnection->getObjectPath()); + activeConnection = nullptr; + } + } + catch (const std::exception &e) + { + Lv2Log::error(SS("HotspotManager: Failed to deactivate hotspot. " << e.what())); + activeConnection = nullptr; + } + if (this->state == State::HotspotConnected || this->state == State::HotspotConnecting) + { + Lv2Log::debug("HotspotManager: state=HotspotMonitoring"); + SetState(State::Monitoring); + + Lv2Log::info("HotspotManager: PiPedal hotspot disabled."); + } +} + +static const std::chrono::seconds scanInterval{60}; + +void HotspotManagerImpl::StartScanTimer() +{ + StopScanTimer(); + ScanNow(); +} +void HotspotManagerImpl::StopScanTimer() +{ + if (this->scanTimerHandle) + { + dbusDispatcher.CancelPost(this->scanTimerHandle); + this->scanTimerHandle = 0; + } +} +void HotspotManagerImpl::ScanNow() +{ + this->scanTimerHandle = 0; + + if (wlanWirelessDevice) + { + + std::map options; + + try + { + Lv2Log::debug("Scanning"); + wlanWirelessDevice->RequestScan(options); + } + catch (const std::exception &e) + { + Lv2Log::error(SS("HotspotMonitor: Wi-Fi RequestScan failed." << e.what())); + return; + } + + this->scanTimerHandle = this->dbusDispatcher.PostDelayed( + std::chrono::duration_cast(scanInterval), + [this]() + { + ScanNow(); + }); + } +} + +HotspotManagerImpl::PostHandle HotspotManagerImpl::Post(PostCallback &&fn) +{ + return dbusDispatcher.Post(std::move(fn)); +} +HotspotManagerImpl::PostHandle HotspotManagerImpl::PostDelayed(const clock::duration &delay, PostCallback &&fn) +{ + return dbusDispatcher.PostDelayed(delay, std::move(fn)); +} +bool HotspotManagerImpl::CancelPost(PostHandle handle) +{ + return dbusDispatcher.CancelPost(handle); +} + +void HotspotManagerImpl::SetNetworkChangingListener(NetworkChangingListener &&listener) +{ + std::lock_guard lock{this->networkChangingListenerMutex}; + this->networkChangingListener = std::move(listener); +} +void HotspotManagerImpl::FireNetworkChanging() +{ + std::lock_guard lock{this->networkChangingListenerMutex}; + if (this->networkChangingListener) + { + this->networkChangingListener(this->ethernetConnected, !!activeConnection); + } +} + +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 new file mode 100644 index 0000000..cd4958e --- /dev/null +++ b/src/HotspotManager.hpp @@ -0,0 +1,76 @@ +// Copyright (c) 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 +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#pragma once +#include +#include +#include + +namespace pipedal { + + class HotspotManager { + + // no move, no delete. + HotspotManager(const HotspotManager&) = delete; + HotspotManager(HotspotManager&&) = delete; + HotspotManager & operator=(const HotspotManager&) = delete; + HotspotManager & operator=(const HotspotManager&&) = delete; + protected: + + HotspotManager() {} // use Create(). + public: + using clock = std::chrono::steady_clock; + + using ptr = std::unique_ptr; + + static ptr Create(); + + virtual ~HotspotManager() noexcept { } + + virtual void Open() = 0; + virtual void Reload() = 0; + virtual void Close() = 0; + + virtual std::vector GetKnownWifiNetworks() = 0; + + + using NetworkChangingListener = std::function; + + virtual void SetNetworkChangingListener(NetworkChangingListener &&listener) = 0; + + using PostHandle = uint64_t; + using PostCallback = std::function; + + virtual PostHandle Post(PostCallback&&fn) = 0; + virtual PostHandle PostDelayed(const clock::duration&delay,PostCallback&&fn) = 0; + virtual bool CancelPost(PostHandle handle) = 0; + + + template + PostHandle PostDelayed(const std::chrono::duration&delay,PostCallback&&fn) + { + return PostDelayed( + std::chrono::duration_cast(delay), + std::move(fn)); + } + + + + }; + +} \ No newline at end of file diff --git a/src/Ipv6Helpers.cpp b/src/Ipv6Helpers.cpp index a758a3a..7634bbd 100644 --- a/src/Ipv6Helpers.cpp +++ b/src/Ipv6Helpers.cpp @@ -30,7 +30,7 @@ using namespace pipedal; -std::string pipedal::GetLinkLocalAddress(const std::string fromAddress); +std::string pipedal::GetNonLinkLocalAddress(const std::string fromAddress); static bool IsIpv4MappedAddress(const struct in6_addr &inetAddr6) { @@ -302,6 +302,7 @@ static std::string GetInterfaceForIp6Address(const in6_addr inetAddr6) { // TODO: Add support for AF_INET6 struct sockaddr_in6 *pAddr = (struct sockaddr_in6 *)(p->ifa_addr); struct sockaddr_in6 *pNetMask = (struct sockaddr_in6 *)(p->ifa_netmask); + if (ipv6NetmaskCompare(inetAddr6, pAddr->sin6_addr, pNetMask->sin6_addr)) { result = p->ifa_name; @@ -313,7 +314,7 @@ static std::string GetInterfaceForIp6Address(const in6_addr inetAddr6) return result; } -static std::string GetLinkLocalAddressForInterface(const std::string &name) +static std::string GetNonLinkLocalAddressForInterface(const std::string &name) { struct ifaddrs *ifap = nullptr; if (getifaddrs(&ifap) != 0) @@ -324,7 +325,7 @@ static std::string GetLinkLocalAddressForInterface(const std::string &name) if (p->ifa_addr->sa_family == AF_INET6 && p->ifa_addr != nullptr && p->ifa_netmask != nullptr) { // TODO: Add support for AF_INET6 struct sockaddr_in6 *pAddr = (struct sockaddr_in6 *)(p->ifa_addr); - if (IN6_IS_ADDR_LINKLOCAL(&(pAddr->sin6_addr))) + if (!IN6_IS_ADDR_LINKLOCAL(&(pAddr->sin6_addr))) { if (name == p->ifa_name) { @@ -353,7 +354,7 @@ static std::string GetLinkLocalAddressForInterface(const std::string &name) freeifaddrs(ifap); return result; } -static std::string GetLinkLocalAddressForIp4Interface(const std::string &name) +static std::string GetNonLinkLocalAddressForIp4Interface(const std::string &name) { struct ifaddrs *ifap = nullptr; if (getifaddrs(&ifap) != 0) @@ -384,23 +385,13 @@ static std::string GetLinkLocalAddressForIp4Interface(const std::string &name) return result; } -std::string pipedal::GetLinkLocalAddress(const std::string fromAddress) +std::string pipedal::GetNonLinkLocalAddress(const std::string fromAddress) { - std::string address = StripPortNumber(fromAddress); - + std::string address = fromAddress; std::string result; if (address[0] != '[') { - // ipv4 - struct in_addr inetAddr; - memset(&inetAddr, 0, sizeof(inetAddr)); - - if (inet_pton(AF_INET, address.c_str(), &inetAddr) == 1) - { - uint32_t remoteAddress = htonl(inetAddr.s_addr); - std::string interfaceName = GetInterfaceForIp4Address(remoteAddress); - result = GetLinkLocalAddressForIp4Interface(interfaceName); - } + return address; } else { @@ -408,13 +399,14 @@ std::string pipedal::GetLinkLocalAddress(const std::string fromAddress) if (address[0] != '[' || address[address.length() - 1] != ']') throw std::invalid_argument("Bad address."); address = address.substr(1, address.length() - 2); - // strip scope if neccessary. - auto pos = address.find_last_of('%'); - if (pos != std::string::npos) - { - address = address.substr(0, pos); - } + auto nPos = address.find('%') ; + if (nPos != std::string::npos) + { + std::string ifName = address.substr(nPos+1); + return GetNonLinkLocalAddressForInterface(ifName); + + } struct in6_addr inetAddr6; memset(&inetAddr6, 0, sizeof(inetAddr6)); if (inet_pton(AF_INET6, address.c_str(), &inetAddr6) == 1) @@ -430,33 +422,25 @@ std::string pipedal::GetLinkLocalAddress(const std::string fromAddress) int8_t *pAddr = (int8_t *)&inetAddr6; uint32_t remoteAddress = htonl(*(int32_t *)(pAddr + 12)); std::string interfaceName = GetInterfaceForIp4Address(remoteAddress); - result = GetLinkLocalAddressForIp4Interface(interfaceName); + result = GetNonLinkLocalAddressForIp4Interface(interfaceName); } else { - std::string interfaceName; - if (IsIpv4MappedAddress(inetAddr6)) - { - uint32_t remoteAddress = GetIpv4MappedAddress(inetAddr6); - interfaceName = GetInterfaceForIp4Address(remoteAddress); - } - else - { - interfaceName = GetInterfaceForIp6Address(inetAddr6); - } - result = GetLinkLocalAddressForInterface(interfaceName); + + std::string interfaceName = GetInterfaceForIp6Address(inetAddr6); + result = GetNonLinkLocalAddressForInterface(interfaceName); } } } if (result == "") { - result = GetLinkLocalAddressForInterface(""); + result = GetNonLinkLocalAddressForInterface(""); } return result; } std::string pipedal::GetInterfaceIpv4Address(const std::string& interfaceName) { - return GetLinkLocalAddressForIp4Interface(interfaceName); + return GetNonLinkLocalAddressForIp4Interface(interfaceName); } diff --git a/src/Ipv6Helpers.hpp b/src/Ipv6Helpers.hpp index 68c788d..abfefed 100644 --- a/src/Ipv6Helpers.hpp +++ b/src/Ipv6Helpers.hpp @@ -27,7 +27,7 @@ namespace pipedal { std::string GetInterfaceIpv4Address(const std::string& interfaceName); - std::string GetLinkLocalAddress(const std::string fromAddress); + std::string GetNonLinkLocalAddress(const std::string fromAddress); bool IsOnLocalSubnet(const std::string&fromAddress); diff --git a/src/PiPedalAlsa.cpp b/src/PiPedalAlsa.cpp index c8ec346..63e200a 100644 --- a/src/PiPedalAlsa.cpp +++ b/src/PiPedalAlsa.cpp @@ -118,7 +118,7 @@ std::vector PiPedalAlsaDevices::GetAlsaDevices() if (err == 0) { unsigned int minRate = 0, maxRate = 0; - snd_pcm_uframes_t minBufferSize, maxBufferSize; + snd_pcm_uframes_t minBufferSize = 0, maxBufferSize = 0; int dir; err = snd_pcm_hw_params_get_rate_min(params, &minRate, &dir); if (err == 0) @@ -143,6 +143,10 @@ std::vector PiPedalAlsaDevices::GetAlsaDevices() info.sampleRates_.push_back(rate); } } + if (minBufferSize < 16) { + minBufferSize = 16; + } + info.minBufferSize_ = (uint32_t)minBufferSize; info.maxBufferSize_ = (uint32_t)maxBufferSize; cacheDevice(info.name_, info); @@ -169,15 +173,14 @@ std::vector PiPedalAlsaDevices::GetAlsaDevices() snd_config_update_free_global(); Lv2Log::debug("GetAlsaDevices --"); - for (auto& device: result) + for (auto &device : result) { Lv2Log::debug(SS(" " << device.name_ << " " << device.longName_ << " " << device.cardId_)); } return result; } - -static std::vector GetAlsaDevices(const char*devname, const char*direction) +static std::vector GetAlsaDevices(const char *devname, const char *direction) { std::vector result; { @@ -206,9 +209,9 @@ static std::vector GetAlsaDevices(const char*devname, const if (desc != nullptr) // skip virtual device { - if (ioid == nullptr || strcmp(ioid,direction) == 0) + if (ioid == nullptr || strcmp(ioid, direction) == 0) { - result.push_back(AlsaMidiDeviceInfo(name,desc)); + result.push_back(AlsaMidiDeviceInfo(name, desc)); } } if (name && strcmp("null", name) != 0) @@ -226,40 +229,37 @@ static std::vector GetAlsaDevices(const char*devname, const return result; } - std::vector pipedal::GetAlsaMidiInputDevices() { - return GetAlsaDevices("rawmidi","Input"); + return GetAlsaDevices("rawmidi", "Input"); } std::vector pipedal::GetAlsaMidiOutputDevices() { - return GetAlsaDevices("rawmidi","Output"); + return GetAlsaDevices("rawmidi", "Output"); } -AlsaMidiDeviceInfo::AlsaMidiDeviceInfo(const char*name, const char*description) -:name_(name) +AlsaMidiDeviceInfo::AlsaMidiDeviceInfo(const char *name, const char *description) + : name_(name) { // extract just the display name from description. // undocumented but e.g.: M2, M2\nM2 Raw Midi const char *p = description; const char *pEnd = p; // undocumented but e.g.: M2, M2\nM2 Raw Midi - while (pEnd != nullptr && *pEnd != 0 - && *pEnd != ',' - && *pEnd != '\n' - ) + while (pEnd != nullptr && *pEnd != 0 && *pEnd != ',' && *pEnd != '\n') { ++pEnd; } if (p != pEnd) { - description_ = std::string(p,pEnd); - } else { + description_ = std::string(p, pEnd); + } + else + { description = name; } } - JSON_MAP_BEGIN(AlsaDeviceInfo) JSON_MAP_REFERENCE(AlsaDeviceInfo, cardId) JSON_MAP_REFERENCE(AlsaDeviceInfo, id) diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index da2147d..8d256f2 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2022-2023 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 @@ -35,6 +35,13 @@ #include "PiPedalUI.hpp" #include "atom_object.hpp" #include "Lv2PluginChangeMonitor.hpp" +#include "HotspotManager.hpp" +#include "DBusToLv2Log.hpp" +#include "SysExec.hpp" +#include "Updater.hpp" +#include "util.hpp" +#include "DBusLog.hpp" +#include "AvahiService.hpp" #ifndef NO_MLOCK #include @@ -66,7 +73,9 @@ PiPedalModel::PiPedalModel() : pluginHost(), atomConverter(pluginHost.GetMapFeature()) { - this->currentUpdateStatus = updater.GetCurrentStatus(); + this->updater = Updater::Create(); + this->updater->Start(); + this->currentUpdateStatus = updater->GetCurrentStatus(); this->pedalboard = Pedalboard::MakeDefault(); #if JACK_HOST this->jackServerSettings = this->storage.GetJackServerSettings(); // to get onboarding flag. @@ -74,39 +83,71 @@ PiPedalModel::PiPedalModel() #else this->jackServerSettings = this->storage.GetJackServerSettings(); #endif - updater.SetUpdateListener( + updater->SetUpdateListener( [this](const UpdateStatus &updateStatus) { this->OnUpdateStatusChanged(updateStatus); }); + + + DbusLogToLv2Log(); + SetDBusLogLevel(DBusLogLevel::Info); + + hotspotManager = HotspotManager::Create(); + hotspotManager->SetNetworkChangingListener( + [this](bool ethernetConnected, bool hotspotEnabling) { + OnNetworkChanging(ethernetConnected,hotspotEnabling); + } + ); + // don't actuall start the hotspotManager until after LV2 is initialized (in order to avoid logging oddities) } void PiPedalModel::Close() { - std::lock_guard lock(mutex); + std::unique_ptr oldAudioHost; + { + std::lock_guard lock(mutex); + if (closed) + { + return; + } + closed = true; - // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) - IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()]; - for (size_t i = 0; i < subscribers.size(); ++i) - { - t[i] = this->subscribers[i]; - } - size_t n = this->subscribers.size(); - for (size_t i = 0; i < n; ++i) - { - t[i]->Close(); - } - delete[] t; + if (avahiService) { + this->avahiService = nullptr; // and close. + } - if (audioHost) + // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) + IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()]; + for (size_t i = 0; i < subscribers.size(); ++i) + { + t[i] = this->subscribers[i]; + } + size_t n = this->subscribers.size(); + for (size_t i = 0; i < n; ++i) + { + t[i]->Close(); + } + delete[] t; + + this->subscribers.resize(0); + + oldAudioHost = std::move(this->audioHost); + } // end lock. + + // lockless to avoid deadlocks while shutting down the audio thread. + if (oldAudioHost) { - audioHost->Close(); + oldAudioHost->Close(); } } PiPedalModel::~PiPedalModel() { - pluginChangeMonitor = nullptr; + CancelNetworkChangingTimer(); + hotspotManager = nullptr; // turn off the hotspot. + + pluginChangeMonitor = nullptr; // stop monitorin LV2 directories. try { adminClient.UnmonitorGovernor(); @@ -952,11 +993,24 @@ void PiPedalModel::SetWifiConfigSettings(const WifiConfigSettings &wifiConfigSet { std::lock_guard lock(mutex); - adminClient.SetWifiConfig(wifiConfigSettings); + +#if NEW_WIFI_CONFIG + if (this->storage.SetWifiConfigSettings(wifiConfigSettings)) + { + this->UpdateDnsSd(); + if (this->hotspotManager) + { + this->hotspotManager->Reload(); + } + } +#else this->storage.SetWifiConfigSettings(wifiConfigSettings); + adminClient.SetWifiConfig(wifiConfigSettings); +#endif { + // yyy: review locking semantics here. This is wrong. Convert to shared pointers? IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()]; for (size_t i = 0; i < subscribers.size(); ++i) { @@ -964,11 +1018,11 @@ void PiPedalModel::SetWifiConfigSettings(const WifiConfigSettings &wifiConfigSet } size_t n = this->subscribers.size(); - WifiConfigSettings tWifiConfigSettings = storage.GetWifiConfigSettings(); // (the passwordless version) + WifiConfigSettings settingsWithNoSecrets = storage.GetWifiConfigSettings(); // (the passwordless version) for (size_t i = 0; i < n; ++i) { - t[i]->OnWifiConfigSettingsChanged(tWifiConfigSettings); + t[i]->OnWifiConfigSettingsChanged(settingsWithNoSecrets); } delete[] t; } @@ -989,20 +1043,28 @@ static std::string GetP2pdName() void PiPedalModel::UpdateDnsSd() { - avahiService.Unannounce(); - + if (!avahiService) + { + throw std::runtime_error("Not ready."); + } ServiceConfiguration deviceIdFile; deviceIdFile.Load(); - - std::string p2pdName = GetP2pdName(); - if (p2pdName != "") + WifiConfigSettings wifiSettings; + wifiSettings.Load(); + std::string serviceName = wifiSettings.hotspotName_; + if (serviceName == "") { - deviceIdFile.deviceName = p2pdName; + serviceName = deviceIdFile.deviceName; + } + if (serviceName == "") + { + serviceName = "pipedal"; } - if (deviceIdFile.deviceName != "" && deviceIdFile.uuid != "") + std::string hostName = GetHostName(); + if (serviceName != "" && deviceIdFile.uuid != "") { - avahiService.Announce(webPort, deviceIdFile.deviceName, deviceIdFile.uuid, "pipedal"); + avahiService->Announce(webPort, serviceName, deviceIdFile.uuid, hostName,true); } else { @@ -1343,12 +1405,19 @@ void PiPedalModel::UpdateRealtimeVuSubscriptions() addedInstances.insert(activeVuSubscriptions[i].instanceid); } } - std::vector instanceids(addedInstances.begin(), addedInstances.end()); - audioHost->SetVuSubscriptions(instanceids); + if (audioHost) + { + std::vector instanceids(addedInstances.begin(), addedInstances.end()); + audioHost->SetVuSubscriptions(instanceids); + } } void PiPedalModel::UpdateRealtimeMonitorPortSubscriptions() { + if (!audioHost) + { + return; + } audioHost->SetMonitorPortSubscriptions(this->activeMonitorPortSubscriptions); } @@ -1451,7 +1520,10 @@ void PiPedalModel::SendSetPatchProperty( clientId, instanceId, urid, atomValue, nullptr, onError); outstandingParameterRequests.push_back(request); - this->audioHost->sendRealtimeParameterRequest(request); + if (this->audioHost) + { + this->audioHost->sendRealtimeParameterRequest(request); + } } void PiPedalModel::SendGetPatchProperty( @@ -1512,7 +1584,10 @@ void PiPedalModel::SendGetPatchProperty( std::lock_guard lock(mutex); outstandingParameterRequests.push_back(request); - this->audioHost->sendRealtimeParameterRequest(request); + if (this->audioHost) + { + this->audioHost->sendRealtimeParameterRequest(request); + } } BankIndex PiPedalModel::GetBankIndex() const @@ -1769,7 +1844,10 @@ void PiPedalModel::DeleteAtomOutputListeners(int64_t clientId) --i; } } - audioHost->SetListenForAtomOutput(atomOutputListeners.size() != 0); + if (audioHost) + { + audioHost->SetListenForAtomOutput(atomOutputListeners.size() != 0); + } } void PiPedalModel::DeleteMidiListeners(int64_t clientId) @@ -1783,7 +1861,10 @@ void PiPedalModel::DeleteMidiListeners(int64_t clientId) --i; } } - audioHost->SetListenForMidiEvent(midiEventListeners.size() != 0); + if (audioHost) + { + audioHost->SetListenForMidiEvent(midiEventListeners.size() != 0); + } } void PiPedalModel::OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom *atomValue) @@ -1929,7 +2010,7 @@ void PiPedalModel::CancelMonitorPatchProperty(int64_t clientId, int64_t clientHa atomOutputListeners.erase(atomOutputListeners.begin() + i); break; } - } + } if (midiEventListeners.size() == 0) { audioHost->SetListenForMidiEvent(false); @@ -2183,24 +2264,24 @@ void PiPedalModel::FireUpdateStatusChanged(const UpdateStatus &updateStatus) UpdateStatus PiPedalModel::GetUpdateStatus() { std::lock_guard lock(mutex); - return updater.GetCurrentStatus(); + return updater->GetCurrentStatus(); } void PiPedalModel::UpdateNow(const std::string &updateUrl) { std::lock_guard lock(mutex); std::filesystem::path fileName, signatureName; - updater.DownloadUpdate(updateUrl, &fileName, &signatureName); + updater->DownloadUpdate(updateUrl, &fileName, &signatureName); adminClient.InstallUpdate(fileName); } void PiPedalModel::ForceUpdateCheck() { - updater.ForceUpdateCheck(); + updater->ForceUpdateCheck(); } void PiPedalModel::SetUpdatePolicy(UpdatePolicyT updatePolicy) { - updater.SetUpdatePolicy(updatePolicy); + updater->SetUpdatePolicy(updatePolicy); } static bool HasAlsaDevice(const std::vector devices, const std::string &deviceId) @@ -2213,6 +2294,17 @@ static bool HasAlsaDevice(const std::vector devices, const std:: return false; } +void PiPedalModel::StartHotspotMonitoring() +{ + this->avahiService = std::make_unique(); + + SetThreadName("avahi"); // hack to name the avahi service thread. + UpdateDnsSd(); // now that the server is running, publish a DNS-SD announcement. + SetThreadName("main"); + + this->hotspotManager->Open(); +} + void PiPedalModel::WaitForAudioDeviceToComeOnline() { auto serverSettings = this->GetJackServerSettings(); @@ -2259,3 +2351,158 @@ void PiPedalModel::WaitForAudioDeviceToComeOnline() // pre-cache device info before we let audio services run. GetAlsaDevices(); } + +PiPedalModel::PostHandle PiPedalModel::Post(PostCallback&&fn) +{ + // I know. odd place to forward this to, but it's a very serviceable dispatcher implementation. + // Why? because it's there, and PiPedalModel has no thread of its own to do dispatching. + if (!hotspotManager) + { + throw std::runtime_error("Too early. It's not ready yet."); + } + return hotspotManager->Post(std::move(fn)); +} +PiPedalModel::PostHandle PiPedalModel::PostDelayed(const clock::duration&delay,PostCallback&&fn) +{ + if (!hotspotManager) + { + throw std::runtime_error("Too early. It's not ready yet."); + } + return hotspotManager->PostDelayed(delay,std::move(fn)); + +} +bool PiPedalModel::CancelPost(PostHandle handle) { + if (!hotspotManager) + { + throw std::runtime_error("Too early. It's not ready yet."); + } + return hotspotManager->CancelPost(handle); +} + +void PiPedalModel::CancelNetworkChangingTimer() +{ + if (networkChangingDelayHandle) + { + CancelPost(networkChangingDelayHandle); + networkChangingDelayHandle = 0; + } +} + + +std::vector PiPedalModel::GetKnownWifiNetworks() +{ + if (!this->hotspotManager) + { + return std::vector(); + } + return this->hotspotManager->GetKnownWifiNetworks(); +} + +void PiPedalModel::OnNetworkChanging(bool ethernetConnected,bool hotspotConnected) +{ + CancelNetworkChangingTimer(); + this->networkChangingDelayHandle = + PostDelayed(std::chrono::seconds(10), // takes a while for network configuration to be fully applied. + [this,ethernetConnected,hotspotConnected]() { + this->networkChangingDelayHandle = 0; + OnNetworkChanged(ethernetConnected,hotspotConnected); + } + ); + + // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) + std::vector t; + t.reserve(this->subscribers.size()); + + for (size_t i = 0; i < subscribers.size(); ++i) + { + t.push_back(this->subscribers[i]); + } + for (size_t i = 0; i < t.size(); ++i) + { + t[i]->OnNetworkChanging(hotspotConnected); + } + +} +void PiPedalModel::OnNetworkChanged(bool ethernetConnected, bool hotspotConnected) +{ + FireNetworkChanged(); +} + +void PiPedalModel::OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType) +{ + try { + switch (eventType) + { + case RealtimeMidiEventType::Shutdown: + { + this->RequestShutdown(false); + } + break; + case RealtimeMidiEventType::Reboot: + { + this->RequestShutdown(true); + } + break; + case RealtimeMidiEventType::StartHotspot: + { + WifiConfigSettings settings = storage.GetWifiConfigSettings(); + if (!settings.hasSavedPassword_) + { + throw std::runtime_error("Can't start Wi-Fi hotspot because no password has been configured."); + } + settings.autoStartMode_ = (uint16_t)HotspotAutoStartMode::Always; + this->SetWifiConfigSettings(settings); + } + break; + case RealtimeMidiEventType::StopHotspot: + { + WifiConfigSettings settings = storage.GetWifiConfigSettings(); + settings.autoStartMode_ = (uint16_t)HotspotAutoStartMode::Never; + this->SetWifiConfigSettings(settings); + } + break; + + default: + break; + } + } catch (const std::exception&e) + { + Lv2Log::error(SS("Failed to process realtime MIDI event. " << e.what())); + } +} + +void PiPedalModel::RequestShutdown(bool restart) +{ + if (GetAdminClient().CanUseAdminClient()) + { + GetAdminClient().RequestShutdown(restart); + } + else + { + // ONLY works when interactively logged in. + std::stringstream s; + s << "/usr/sbin/shutdown "; + if (restart) + { + s << "-r"; + } + else + { + s << "-P"; + } + s << " now"; + + if (sysExec(s.str().c_str()) != EXIT_SUCCESS) + { + Lv2Log::error("shutdown failed."); + if (restart) + { + throw new PiPedalStateException("Restart request failed."); + } + else + { + throw new PiPedalStateException("Shutdown request failed."); + } + } + } +} diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 8fc4104..042930b 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -19,6 +19,7 @@ #pragma once #include +#include "UpdaterStatus.hpp" #include "PluginHost.hpp" #include "GovernorSettings.hpp" #include "Pedalboard.hpp" @@ -30,13 +31,11 @@ #include #include #include "Banks.hpp" -#include "Updater.hpp" #include "PiPedalConfiguration.hpp" #include "JackServerSettings.hpp" #include "WifiConfigSettings.hpp" #include "WifiDirectConfigSettings.hpp" #include "AdminClient.hpp" -#include "AvahiService.hpp" #include #include "Promise.hpp" #include "AtomConverter.hpp" @@ -48,6 +47,8 @@ namespace pipedal struct RealtimeMidiProgramRequest; struct RealtimeNextMidiProgramRequest; class Lv2PluginChangeMonitor; + class Updater; + class AvahiService; class IPiPedalModelSubscriber { @@ -78,18 +79,31 @@ namespace pipedal virtual void OnFavoritesChanged(const std::map &favorites) = 0; virtual void OnShowStatusMonitorChanged(bool show) = 0; virtual void OnSystemMidiBindingsChanged(const std::vector&bindings) = 0; + //virtual void OnPatchPropertyChanged(int64_t clientId, int64_t instanceId,const std::string& propertyUri,const json_variant& value) = 0; virtual void OnErrorMessage(const std::string&message) = 0; virtual void OnLv2PluginsChanging() = 0; + virtual void OnNetworkChanging(bool hotspotConnected) = 0; virtual void Close() = 0; }; + class HotspotManager; + class PiPedalModel : private IAudioHostCallbacks { - private: + public: + using clock = std::chrono::steady_clock; + using PostHandle = uint64_t; + using PostCallback = std::function; + using NetworkChangedListener = std::function; + + + private: + std::unique_ptr hotspotManager; + + std::unique_ptr updater; UpdateStatus currentUpdateStatus; - Updater updater; void OnUpdateStatusChanged(const UpdateStatus&updateStatus); std::function restartListener; @@ -99,7 +113,7 @@ namespace pipedal std::vector systemMidiBindings; - AvahiService avahiService; + std::unique_ptr avahiService; uint16_t webPort; PiPedalAlsaDevices alsaDevices; @@ -140,6 +154,14 @@ namespace pipedal Storage storage; bool hasPresetChanged = false; + NetworkChangedListener networkChangedListener; + void FireNetworkChanged() { + if (networkChangedListener) + { + networkChangedListener(); + } + } + std::unique_ptr audioHost; JackConfiguration jackConfiguration; std::shared_ptr lv2Pedalboard; @@ -175,6 +197,7 @@ namespace pipedal std::vector outstandingParameterRequests; IPiPedalModelSubscriber *GetNotificationSubscriber(int64_t clientId); + std::atomic closed = false; private: // IAudioHostCallbacks virtual void OnNotifyLv2StateChanged(uint64_t instanceId) override; @@ -184,6 +207,8 @@ namespace pipedal virtual void OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, float value) override; virtual void OnNotifyMidiListen(bool isNote, uint8_t noteOrControl) override; virtual void OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom*atomValue) override; + virtual void OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType) override; + void OnNotifyPatchProperty(uint64_t instanceId, LV2_URID outputAtomProperty, const std::string &atomJson); @@ -193,6 +218,11 @@ namespace pipedal virtual void OnNotifyNextMidiProgram(const RealtimeNextMidiProgramRequest&request) override; virtual void OnNotifyLv2RealtimeError(int64_t instanceId,const std::string &error) override; + PostHandle networkChangingDelayHandle = 0; + void CancelNetworkChangingTimer(); + + void OnNetworkChanging(bool ethernetConnected,bool hotspotConnected); + void OnNetworkChanged(bool ethernetConnected, bool hotspotConnected); void UpdateVst3Settings(Pedalboard &pedalboard); @@ -203,6 +233,26 @@ namespace pipedal PiPedalModel(); virtual ~PiPedalModel(); + void RequestShutdown(bool restart); + + virtual PostHandle Post(PostCallback&&fn); + virtual PostHandle PostDelayed(const clock::duration&delay,PostCallback&&fn); + virtual bool CancelPost(PostHandle handle); + + template + PostHandle PostDelayed(const std::chrono::duration&delay,PostCallback&&fn) + { + return PostDelayed( + std::chrono::duration_cast(delay), + std::move(fn)); + } + + void SetNetworkChangedListener(NetworkChangedListener listener) { + networkChangedListener = listener; + } + + void StartHotspotMonitoring(); + void WaitForAudioDeviceToComeOnline(); UpdateStatus GetUpdateStatus(); @@ -291,6 +341,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..23a9f78 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -459,7 +459,6 @@ private: { return model.GetAdminClient(); } - void RequestShutdown(bool restart); std::recursive_mutex writeMutex; PiPedalModel &model; @@ -517,26 +516,47 @@ private: std::mutex activePortMonitorsMutex; std::vector> activePortMonitors; + std::atomic closed = false; public: virtual int64_t GetClientId() { return clientId; } virtual ~PiPedalSocketHandler() { + if (!closed) + { + FinalCleanup(); + } + } + + bool finalCleanup = false; + void FinalCleanup() + { + if (finalCleanup) return; + finalCleanup = true; + // avoid use after free. for (int i = 0; i < this->activePortMonitors.size(); ++i) { model.UnmonitorPort(activePortMonitors[i]->subscriptionHandle); } + activePortMonitors.resize(0); for (int i = 0; i < this->activeVuSubscriptions.size(); ++i) { model.RemoveVuSubscription(activeVuSubscriptions[i].subscriptionHandle); } + activeVuSubscriptions.resize(0); model.RemoveNotificationSubsription(this); + } virtual void Close() { + if (closed) return; + closed = true; + + FinalCleanup(); // do it while we can. &model will no longer be valid after this. ( :-( ) + SocketHandler::Close(); } @@ -936,6 +956,10 @@ public: } return; } + if (closed) + { + this->SendError(replyTo, "Server has shut down."); + } if (message == "setControl") { ControlChangedBody message; @@ -1025,6 +1049,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; @@ -1240,15 +1270,12 @@ public: } else if (message == "shutdown") { - PresetIndex newIndex; - - RequestShutdown(false); + model.RequestShutdown(false); this->Reply(replyTo, "shutdown"); } else if (message == "restart") { - PresetIndex newIndex; - RequestShutdown(true); + model.RequestShutdown(true); this->Reply(replyTo, "restart"); } else if (message == "deletePresetItem") @@ -1621,6 +1648,18 @@ private: Send("onLv2PluginsChanging",true); Flush(); } + virtual void OnNetworkChanging(bool hotspotConnected) override { + try { + Send("onNetworkChanging",hotspotConnected); + Flush(); + + } catch (const std::exception&ignored) + { + + } + + } + virtual void OnErrorMessage(const std::string&message) { @@ -1976,38 +2015,3 @@ std::shared_ptr pipedal::MakePiPedalSocketFactory(PiPedalModel & return std::make_shared(model); } -void PiPedalSocketHandler::RequestShutdown(bool restart) -{ - if (GetAdminClient().CanUseAdminClient()) - { - GetAdminClient().RequestShutdown(restart); - } - else - { - // ONLY works when interactively logged in. - std::stringstream s; - s << "/usr/sbin/shutdown "; - if (restart) - { - s << "-r"; - } - else - { - s << "-P"; - } - s << " now"; - - if (sysExec(s.str().c_str()) != EXIT_SUCCESS) - { - Lv2Log::error("shutdown failed."); - if (restart) - { - throw new PiPedalStateException("Restart request failed."); - } - else - { - throw new PiPedalStateException("Shutdown request failed."); - } - } - } -} diff --git a/src/PiPedalVersion.cpp b/src/PiPedalVersion.cpp index 54d3bb1..d8f47f4 100644 --- a/src/PiPedalVersion.cpp +++ b/src/PiPedalVersion.cpp @@ -107,7 +107,7 @@ PiPedalVersion::PiPedalVersion(PiPedalModel&model) std::string p2pAddr = GetInterfaceIpv4Address("p2p-wlan0-0"); if (p2pAddr.length() != 0) { - this->webAddresses_.push_back(MakeWebAddress(p2pAddr,port) + " (WiFi Direct)"); + this->webAddresses_.push_back(MakeWebAddress(p2pAddr,port) + " (Wi-Fi Direct)"); } 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/RealtimeMidiEventType.hpp b/src/RealtimeMidiEventType.hpp new file mode 100644 index 0000000..8867ec9 --- /dev/null +++ b/src/RealtimeMidiEventType.hpp @@ -0,0 +1,29 @@ +// Copyright (c) 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 +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#pragma once +namespace pipedal { + enum class RealtimeMidiEventType { + Shutdown, + Reboot, + StartHotspot, + StopHotspot + }; + +}; \ No newline at end of file diff --git a/src/RingBufferReader.hpp b/src/RingBufferReader.hpp index be51fe2..2e1cbf1 100644 --- a/src/RingBufferReader.hpp +++ b/src/RingBufferReader.hpp @@ -24,6 +24,7 @@ #include "VuUpdate.hpp" #include "AudioHost.hpp" #include "lv2/atom/atom.h" +#include "RealtimeMidiEventType.hpp" #include namespace pipedal { @@ -65,9 +66,15 @@ namespace pipedal SetOutputVolume, Lv2ErrorMessage, + RealtimeMidiEvent, + }; + + struct RealtimeMidiEventRequest { + RealtimeMidiEventType eventType; + }; struct RealtimeNextMidiProgramRequest { int64_t requestId; int32_t direction; @@ -371,6 +378,11 @@ namespace pipedal write(RingBufferCommand::MidiProgramChange,msg); } + void OnRealtimeMidiEvent(RealtimeMidiEventType eventType) + { + RealtimeMidiEventRequest msg { eventType}; + write(RingBufferCommand::RealtimeMidiEvent,msg); + } void OnNextMidiProgram(int64_t requestId,int32_t direction) { RealtimeNextMidiProgramRequest msg { requestId: requestId, direction:direction}; diff --git a/src/SetWifiConfig.cpp b/src/SetWifiConfig.cpp index 04f1d62..cc866c8 100644 --- a/src/SetWifiConfig.cpp +++ b/src/SetWifiConfig.cpp @@ -70,22 +70,56 @@ const std::string &pipedal::GetWifiConfigWlanAddress() return gWlanAddress; } +static bool gNetworkManagerTestExecuted = false; +static bool gUsingNetworkManager = false; + +void pipedal::PretendNetworkManagerIsInstalled() +{ + gNetworkManagerTestExecuted = true; + gUsingNetworkManager = true; +} bool pipedal::UsingNetworkManager() { + if (gNetworkManagerTestExecuted) + { + return gUsingNetworkManager; + } + bool bResult = false; + auto result = sysExecForOutput("systemctl","is-active NetworkManager"); + if (result.exitCode == EXIT_SUCCESS) + { + std::string text = result.output.erase(result.output.find_last_not_of(" \n\r\t")+1); + if (text == "active") + { + bResult = true; + } else if (text == "inactive") + { + bResult = false; + } else { + throw std::runtime_error(SS("UsingNetworkManager: unexpected result (" << text << ")")); + } + gNetworkManagerTestExecuted = true; + gUsingNetworkManager = bResult; + return bResult; + } + // does the neworkManager service path exists? return std::filesystem::exists(NETWORK_MANAGER_SERVICE_PATH); } +static bool IsP2pInstalled() +{ + return std::filesystem::exists(DNSMASQ_P2P_PATH) | std::filesystem::exists(NETWORK_MANAGER_DNSMASQ_P2P_PATH); +} + static bool IsApdInstalled() { return std::filesystem::exists(DNSMASQ_APD_PATH); } -static bool IsP2pInstalled() -{ - return std::filesystem::exists(DNSMASQ_P2P_PATH) | std::filesystem::exists(NETWORK_MANAGER_DNSMASQ_P2P_PATH); -} + +#if !NEW_WIFI_CONFIG static void restoreApdDhcpdConfFile() { // remove the interface wlan0 section. @@ -123,6 +157,7 @@ static void restoreApdDhcpdConfFile() dhcpcd.Save(dhcpcdConfig); } } +#endif static void restoreP2pDhcpdConfFile() { @@ -170,6 +205,7 @@ static void restoreP2pDhcpdConfFile() } } +#if !NEW_WIFI_CONFIG static void restoreApdDnsmasqConfFile() { std::filesystem::path path(DNSMASQ_APD_PATH); @@ -178,6 +214,7 @@ static void restoreApdDnsmasqConfFile() std::filesystem::remove(path); } } +#endif static void restoreP2pDnsmasqConfFile() { { @@ -196,6 +233,7 @@ static void restoreP2pDnsmasqConfFile() } } +#if !NEW_WIFI_CONFIG static void UninstallHostApd() { if (IsApdInstalled()) @@ -213,6 +251,7 @@ static void UninstallHostApd() sysExec(SYSTEMCTL_BIN " start wpa_supplicant"); } } +#endif static void InstallP2p(const WifiDirectConfigSettings &settings) { @@ -299,7 +338,8 @@ static void InstallP2p(const WifiDirectConfigSettings &settings) static void UninstallP2p(); -void pipedal::SetWifiConfig(const WifiConfigSettings &settings) +#if !NEW_WIFI_CONFIG +static void SetHostapdWifiConfig(const WifiConfigSettings &settings) { char band; if (!settings.enable_) @@ -476,6 +516,25 @@ void pipedal::SetWifiConfig(const WifiConfigSettings &settings) } ::sync(); } +#endif + +#if NEW_WIFI_CONFIG + +static void SetNetworkManagerWifiConfig(WifiConfigSettings &settings) +{ + settings.Save(); +} +#endif + +void pipedal::SetWifiConfig(WifiConfigSettings &settings) +{ +#if NEW_WIFI_CONFIG + SetNetworkManagerWifiConfig(settings); +#else + SetHostapdWifiConfig(const WifiConfigSettings &settings); +#endif +} + /********************************************************************************* @@ -559,10 +618,12 @@ void UninstallP2p() } - WifiDirectConfigSettings wifiDirectConfigSettings; - wifiDirectConfigSettings.Load(); - wifiDirectConfigSettings.enable_ = false; - SetWifiDirectConfig(wifiDirectConfigSettings); + } + if (UsingNetworkManager()) + { + // dhcpcd comes up enabled after an install, so stop it always. + sysExec(SYSTEMCTL_BIN " stop dhcpcd"); + sysExec(SYSTEMCTL_BIN " disable dhcpcd"); } ::sync(); @@ -655,7 +716,7 @@ static void ConfigDhcpcdForP2p() } void pipedal::SetWifiDirectConfig(const WifiDirectConfigSettings &settings) { - + settings.Save(); try { char band; @@ -675,7 +736,6 @@ void pipedal::SetWifiDirectConfig(const WifiDirectConfigSettings &settings) else { InstallP2p(settings); - sysExec(SYSTEMCTL_BIN " restart pipedald"); } } catch (const std::exception &e) @@ -685,6 +745,7 @@ void pipedal::SetWifiDirectConfig(const WifiDirectConfigSettings &settings) ::sync(); } void pipedal::OnWifiReinstall() { +#if ENABLE_WIFI_P2P // Do not enable P2P going forward. WifiDirectConfigSettings settings; settings.Load(); if (settings.enable_) @@ -692,14 +753,30 @@ void pipedal::OnWifiReinstall() { SetWifiDirectConfig(settings); ::sync(); } +#endif +#if NEW_WIFI_CONFIG + { + // no action required. + // pipedald will pick up the old settings at runtime. + } + +#endif } void pipedal::OnWifiUninstall(bool preserveState) { // intaller hook +#if NEW_WIFI_CONFIG + { + + // no action required. + // shutting down pipedald is sufficient. + } +#else if (IsApdInstalled()) { UninstallHostApd(); } +#endif if (IsP2pInstalled()) { WifiDirectConfigSettings settings; diff --git a/src/SetWifiConfig.hpp b/src/SetWifiConfig.hpp index 820b94b..b3a4c36 100644 --- a/src/SetWifiConfig.hpp +++ b/src/SetWifiConfig.hpp @@ -21,12 +21,13 @@ #include "WifiConfigSettings.hpp" #include "WifiDirectConfigSettings.hpp" + namespace pipedal { void SetWifiConfigWLanAddress(const std::string&wLanAddress); const std::string &GetWifiConfigWlanAddress(); - void SetWifiConfig(const WifiConfigSettings&settings); + void SetWifiConfig(WifiConfigSettings&settings); void SetWifiDirectConfig(const WifiDirectConfigSettings&settings); void OnWifiUninstall(bool preserveState = false); @@ -35,4 +36,6 @@ namespace pipedal { void OnWifiInstallComplete(); bool UsingNetworkManager(); + void PretendNetworkManagerIsInstalled(); + } // namespace. \ No newline at end of file diff --git a/src/Storage.cpp b/src/Storage.cpp index 9f47749..9e05424 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -38,7 +38,6 @@ using namespace pipedal; const char *BANK_EXTENSION = ".bank"; const char *BANKS_FILENAME = "index.banks"; -#define WIFI_CONFIG_SETTINGS_FILENAME "wifiConfigSettings.json"; #define USER_SETTINGS_FILENAME "userSettings.json"; Storage::Storage() @@ -953,43 +952,29 @@ std::string Storage::GetGovernorSettings() const return this->userSettings.governor_; } -void Storage::SetWifiConfigSettings(const WifiConfigSettings &wifiConfigSettings) +bool Storage::SetWifiConfigSettings(const WifiConfigSettings &wifiConfigSettings) { WifiConfigSettings copyToSave = wifiConfigSettings; - copyToSave.rebootRequired_ = false; - if (!copyToSave.enable_) + WifiConfigSettings previousValue; + previousValue.Load(); + if (!copyToSave.hasPassword_) { - copyToSave.hasPassword_ = false; - } - copyToSave.password_ = ""; - - std::filesystem::path path = this->dataRoot / WIFI_CONFIG_SETTINGS_FILENAME; - { - pipedal::ofstream_synced f(path); - if (!f.is_open()) + copyToSave.hasPassword_ = previousValue.hasPassword_; + copyToSave.password_ = previousValue.password_; + copyToSave.hasSavedPassword_ = previousValue.hasPassword_; + } else { + if (copyToSave.IsEnabled()) { - throw PiPedalException("Unable to write to " + ((std::string)path)); + copyToSave.hasSavedPassword_ = copyToSave.hasPassword_; } - json_writer writer(f, false); - writer.write(©ToSave); } - - WifiConfigSettings copyToStore = wifiConfigSettings; - if (copyToStore.enable_) - { - copyToStore.hasPassword_ = copyToStore.password_.length() != 0 || this->wifiConfigSettings.hasPassword_; - } - else - { - copyToStore.hasPassword_ = false; - } - copyToStore.password_ = ""; - copyToStore.rebootRequired_ = false; - if (copyToStore.enable_ && !copyToStore.hasPassword_) - { - copyToStore.hasPassword_ = this->wifiConfigSettings.hasPassword_; - } - this->wifiConfigSettings = copyToStore; + bool configChanged = copyToSave.ConfigurationChanged(previousValue); + copyToSave.Save(); + this->wifiConfigSettings = copyToSave; + this->wifiConfigSettings.hasPassword_ = false; + this->wifiConfigSettings.password_ = ""; + + return configChanged; } void Storage::SetWifiDirectConfigSettings(const WifiDirectConfigSettings &wifiDirectConfigSettings) @@ -1030,27 +1015,8 @@ void Storage::LoadUserSettings() } void Storage::LoadWifiConfigSettings() { - std::filesystem::path path = this->dataRoot / WIFI_CONFIG_SETTINGS_FILENAME; - - try - { - if (std::filesystem::is_regular_file(path)) - { - std::ifstream f(path); - if (!f.is_open()) - { - throw PiPedalException("Unable to write to " + ((std::string)path)); - } - json_reader reader(f); - WifiConfigSettings wifiConfigSettings; - reader.read(&wifiConfigSettings); - this->wifiConfigSettings = wifiConfigSettings; - } - } - catch (const std::exception &) - { - } - this->wifiConfigSettings.valid_ = true; + this->wifiConfigSettings.Load(); + this->wifiConfigSettings.valid_ = this->wifiConfigSettings.hasPassword_ && !this->wifiConfigSettings.countryCode_.empty(); } void Storage::LoadWifiDirectConfigSettings() { @@ -1065,7 +1031,10 @@ void Storage::LoadWifiDirectConfigSettings() WifiConfigSettings Storage::GetWifiConfigSettings() { - return this->wifiConfigSettings; + WifiConfigSettings result = this->wifiConfigSettings; + result.hasPassword_ = false; + result.password_ = ""; + return result; } WifiDirectConfigSettings Storage::GetWifiDirectConfigSettings() { @@ -1440,7 +1409,7 @@ void Storage::SetJackServerSettings(const pipedal::JackServerSettings &jackConfi void Storage::SetSystemMidiBindings(const std::vector &bindings) { - std::filesystem::path fileName = this->dataRoot / "SystemMidiBindings.json"; + std::filesystem::path fileName = this->dataRoot / "config" / "SystemMidiBindings.json"; pipedal::ofstream_synced f; f.open(fileName); if (f.is_open()) @@ -1453,19 +1422,39 @@ std::vector Storage::GetSystemMidiBindings() { std::vector result; - std::filesystem::path fileName = this->dataRoot / "SystemMidiBindings.json"; + std::filesystem::path fileName = this->dataRoot / "config" / "SystemMidiBindings.json"; + if (!std::filesystem::exists(fileName)) { + // pick up from legacy location? + fileName = this->configRoot / "config" / "SystemMidiBindings.json"; + } std::ifstream f; f.open(fileName); if (f.is_open()) { - json_reader reader(f); - reader.read(&result); - } - else - { - result.push_back(MidiBinding::SystemBinding("prevProgram")); - result.push_back(MidiBinding::SystemBinding("nextProgram")); + try { + json_reader reader(f); + reader.read(&result); + if (result.size() == 2) + { + result.push_back(MidiBinding::SystemBinding("stopHotspot")); + result.push_back(MidiBinding::SystemBinding("startHotspot")); + result.push_back(MidiBinding::SystemBinding("shutdown")); + result.push_back(MidiBinding::SystemBinding("reboot")); + } + return result; + } + catch (const std::exception&e) + { + Lv2Log::warning(SS("Can't read file " << fileName << ". " << e.what())); + } } + result.clear(); + result.push_back(MidiBinding::SystemBinding("prevProgram")); + result.push_back(MidiBinding::SystemBinding("nextProgram")); + result.push_back(MidiBinding::SystemBinding("stopHotspot")); + result.push_back(MidiBinding::SystemBinding("startHotspot")); + result.push_back(MidiBinding::SystemBinding("shutdown")); + result.push_back(MidiBinding::SystemBinding("reboot")); return result; } diff --git a/src/Storage.hpp b/src/Storage.hpp index 514686b..913c04d 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -158,7 +158,8 @@ public: void SetJackChannelSelection(const JackChannelSelection&channelSelection); const JackChannelSelection&GetJackChannelSelection(const JackConfiguration &jackConfiguration); - void SetWifiConfigSettings(const WifiConfigSettings & wifiConfigSettings); + // returns true if services needs to be updated. + bool SetWifiConfigSettings(const WifiConfigSettings & wifiConfigSettings); WifiConfigSettings GetWifiConfigSettings(); void SetWifiDirectConfigSettings(const WifiDirectConfigSettings & wifiDirectConfigSettings); diff --git a/src/SystemConfigFile.cpp b/src/SystemConfigFile.cpp index c82db9a..8bea50b 100644 --- a/src/SystemConfigFile.cpp +++ b/src/SystemConfigFile.cpp @@ -23,6 +23,7 @@ #include #include #include +#include "ss.hpp" using namespace pipedal; using namespace std; @@ -107,6 +108,7 @@ static inline std::string ValuePart(const std::string &line) throw PiPedalException("Value not found."); return line.substr(pos + 1); } + std::string SystemConfigFile::Get(const std::string &key) const { int64_t lineIndex = GetLine(key); @@ -346,3 +348,46 @@ void SystemConfigFile::AppendLine(const std::string &line) lines.push_back(line); } + +static const std::string REPLACE_SLUG = "# PiPedal replace:"; +static const std::string ADD_SLUG = "# PiPedal add:"; + +void SystemConfigFile::UndoableReplaceLine(int line,const std::string&text) +{ + std::string undoComment = SS(REPLACE_SLUG << lines[line]); + lines[line] = undoComment; + InsertLine(line+1,text); +} +int SystemConfigFile::UndoableAddLine(int line,const std::string&text) +{ + std::string undoComment = ADD_SLUG; + InsertLine(line,undoComment); + InsertLine(line+1,text); + return line+2; +} + +bool SystemConfigFile::RemoveUndoableActions() +{ + bool changed = false; + for (int i = 0; i < GetLineCount(); ++i) + { + if (lines[i].starts_with("# PiPedal")) + { + if (lines[i].starts_with(REPLACE_SLUG)) + { + std::string oldText = lines[i].substr(REPLACE_SLUG.length()); + lines[i] = oldText; + this->EraseLine(i+1); + changed = true; + } else if (lines[i].starts_with(ADD_SLUG)) + { + EraseLine(i); + EraseLine(i); + changed = true; + } + } + } + return changed; +} + + diff --git a/src/SystemConfigFile.hpp b/src/SystemConfigFile.hpp index f4842ae..a8622cb 100644 --- a/src/SystemConfigFile.hpp +++ b/src/SystemConfigFile.hpp @@ -47,6 +47,7 @@ public: bool HasValue(const std::string&key) const; bool Get(const std::string&key,std::string*pResult) const; std::string Get(const std::string&key) const; + const std::string& Get(int position) const { return lines[position]; } void Set(const std::string&key,const std::string &value); void Set(const std::string&key,const std::string &value, const std::string&comment); void SetDefault(const std::string&key, const std::string &value); @@ -66,6 +67,10 @@ public: void InsertLine(int position, const std::string&line); void AppendLine(const std::string&line); void SetLineValue(int index, const std::string&line) { lines[index] = line; } + + void UndoableReplaceLine(int line,const std::string&text); + int UndoableAddLine(int line,const std::string&text); + bool RemoveUndoableActions(); }; }; \ No newline at end of file diff --git a/src/TemporaryFile.cpp b/src/TemporaryFile.cpp index bababdd..26ad12f 100644 --- a/src/TemporaryFile.cpp +++ b/src/TemporaryFile.cpp @@ -18,6 +18,7 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #include "TemporaryFile.hpp" +#include using namespace pipedal; diff --git a/src/TemporaryFile.hpp b/src/TemporaryFile.hpp index dd0e1a5..7f1f2a9 100644 --- a/src/TemporaryFile.hpp +++ b/src/TemporaryFile.hpp @@ -28,6 +28,8 @@ namespace pipedal { TemporaryFile(const std::filesystem::path&parentDirectory); ~TemporaryFile(); const std::filesystem::path&Path()const { return path;} + std::string str() const { return path.c_str(); } + const char*c_str() const { return path.c_str(); } private: std::filesystem::path path; }; diff --git a/src/Updater.cpp b/src/Updater.cpp index 8c612cc..f13f15a 100644 --- a/src/Updater.cpp +++ b/src/Updater.cpp @@ -35,6 +35,9 @@ #include "UpdaterSecurity.hpp" #include "SysExec.hpp" #include "ofstream_synced.hpp" +#include +#include "TemporaryFile.hpp" +#include using namespace pipedal; namespace fs = std::filesystem; @@ -45,25 +48,129 @@ namespace fs = std::filesystem; #undef TEST_UPDATE // do NOT leat this leak into a production build! #endif +namespace pipedal +{ + class GithubResponseHeaders + { + public: + GithubResponseHeaders() {} + GithubResponseHeaders(const std::filesystem::path path); + int code_ = -1; + std::chrono::system_clock::time_point date_; + std::chrono::system_clock::time_point ratelimit_reset_; + uint64_t ratelimit_limit_ = 60; + uint64_t ratelimit_remaining_ = 60; + uint64_t ratelimit_used_ = 0; + std::string ratelimit_resource_; + bool limit_exceeded() const { return code_ != 200 && ratelimit_limit_ != 0 && ratelimit_limit_ == ratelimit_used_; } + void Load(const std::filesystem::path &filename); + void Save(const std::filesystem::path &filename); + DECLARE_JSON_MAP(GithubResponseHeaders); + + private: + static std::filesystem::path FILENAME; + }; +} + +class pipedal::UpdaterImpl : public Updater +{ +public: + UpdaterImpl(const std::filesystem::path &workingDirectory); + UpdaterImpl() : UpdaterImpl("/var/pipedal/updates") {} + + virtual ~UpdaterImpl() noexcept; + + virtual void SetUpdateListener(UpdateListener &&listener) override; + virtual void Start() override; + virtual void Stop() override; + + virtual void CheckNow() override; + + virtual UpdatePolicyT GetUpdatePolicy() override; + virtual void SetUpdatePolicy(UpdatePolicyT updatePolicy) override; + virtual void ForceUpdateCheck() override; + virtual void DownloadUpdate(const std::string &url, std::filesystem::path *file, std::filesystem::path *signatureFile) override; + virtual UpdateStatus GetCurrentStatus() const override { return this->currentResult; } + virtual UpdateStatus GetReleaseGeneratorStatus() override; + +private: + std::filesystem::path workingDirectory; + std::filesystem::path updateStatusCacheFile; + std::filesystem::path githubResponseHeaderFilename; + + GithubResponseHeaders githubResponseHeaders; + + using clock = std::chrono::steady_clock; + + void SetCachedUpdateStatus(UpdateStatus &updateStatus); + + UpdateStatus GetCachedUpdateStatus(); + + void RetryAfter(clock::duration delay); + + template + void RetryAfter(std::chrono::duration duration) + { + RetryAfter(std::chrono::duration_cast(duration)); + } + + void SaveRetryTime(const std::chrono::system_clock::time_point &time); + clock::time_point LoadRetryTime(); + + std::string GetUpdateFilename(const std::string &url); + std::string GetSignatureUrl(const std::string &url); + + UpdatePolicyT updatePolicy = UpdatePolicyT::ReleaseOrBeta; + using UpdateReleasePredicate = std::function; + + UpdateRelease getUpdateRelease( + const std::vector &githubReleases, + const std::string ¤tVersion, + const UpdateReleasePredicate &predicate); + + UpdateStatus cachedUpdateStatus; + bool stopped = false; + + int event_reader = -1; + int event_writer = -1; + void ThreadProc(); + UpdateStatus DoUpdate(bool forReleaseGenerator); + void CheckForUpdate(bool useCache); + UpdateListener listener; + + std::unique_ptr thread; + std::mutex mutex; + + bool hasInfo = false; + UpdateStatus currentResult; + static clock::duration updateRate; + clock::time_point updateRetryTime; +}; +Updater::ptr Updater::Create() +{ + return std::make_unique(); +} +Updater::ptr Updater::Create(const std::filesystem::path &workingDirectory) +{ + return std::make_unique(workingDirectory); +} + static constexpr uint64_t CLOSE_EVENT = 0; static constexpr uint64_t CHECK_NOW_EVENT = 1; static constexpr uint64_t UNCACHED_CHECK_NOW_EVENT = 2; -static std::filesystem::path WORKING_DIRECTORY = "/var/pipedal/updates"; -static std::filesystem::path UPDATE_STATUS_CACHE_FILE = WORKING_DIRECTORY / "updateStatus.json"; - -Updater::clock::duration Updater::updateRate = std::chrono::duration_cast(std::chrono::days(1)); +UpdaterImpl::clock::duration UpdaterImpl::updateRate = std::chrono::duration_cast(std::chrono::days(1)); static std::chrono::system_clock::duration CACHE_DURATION = std::chrono::duration_cast(std::chrono::minutes(30)); std::mutex cacheMutex; -static UpdateStatus GetCachedUpdateStatus() +UpdateStatus UpdaterImpl::GetCachedUpdateStatus() { std::lock_guard lock{cacheMutex}; try { - if (std::filesystem::exists(UPDATE_STATUS_CACHE_FILE)) + if (std::filesystem::exists(updateStatusCacheFile)) { - std::ifstream f{UPDATE_STATUS_CACHE_FILE}; + std::ifstream f{updateStatusCacheFile}; if (f.is_open()) { json_reader reader(f); @@ -83,13 +190,13 @@ static UpdateStatus GetCachedUpdateStatus() return UpdateStatus(); } -static void SetCachedUpdateStatus(UpdateStatus &updateStatus) +void UpdaterImpl::SetCachedUpdateStatus(UpdateStatus &updateStatus) { std::lock_guard lock{cacheMutex}; updateStatus.LastUpdateTime(std::chrono::system_clock::now()); try { - pipedal::ofstream_synced f{UPDATE_STATUS_CACHE_FILE}; + pipedal::ofstream_synced f{updateStatusCacheFile}; json_writer writer{f}; writer.write(updateStatus); } @@ -98,12 +205,22 @@ static void SetCachedUpdateStatus(UpdateStatus &updateStatus) Lv2Log::error(SS("Unable to write cached UpdateStatus. " << e.what())); } } -Updater::Updater() + +UpdaterImpl::UpdaterImpl(const std::filesystem::path &workingDirectory) + : workingDirectory(workingDirectory), + updateStatusCacheFile(workingDirectory / "updateStatus.json"), + githubResponseHeaderFilename(workingDirectory / "githubHeaders.json") { + this->githubResponseHeaders.Load(githubResponseHeaderFilename); cachedUpdateStatus = GetCachedUpdateStatus(); this->updatePolicy = cachedUpdateStatus.UpdatePolicy(); currentResult = cachedUpdateStatus; - +} +void UpdaterImpl::Start() +{ + #if !ENABLE_AUTO_UPDATE + return; + #endif int fds[2]; int rc = pipe(fds); if (rc != 0) @@ -117,16 +234,25 @@ Updater::Updater() { ThreadProc(); }); CheckNow(); } -Updater::~Updater() +UpdaterImpl::~UpdaterImpl() { - Stop(); + try + { + Stop(); + } + catch (const std::exception & /*ignored*/) + { + } } -void Updater::Stop() +void UpdaterImpl::Stop() { if (stopped) { return; } + #if !ENABLE_AUTO_UPDATE + return; + #endif stopped = true; if (event_writer != -1) @@ -149,13 +275,13 @@ void Updater::Stop() } } -void Updater::CheckNow() +void UpdaterImpl::CheckNow() { uint64_t value = CHECK_NOW_EVENT; write(this->event_writer, &value, sizeof(uint64_t)); } -void Updater::SetUpdateListener(UpdateListener &&listener) +void UpdaterImpl::SetUpdateListener(UpdateListener &&listener) { std::lock_guard lock{mutex}; this->listener = listener; @@ -165,75 +291,115 @@ void Updater::SetUpdateListener(UpdateListener &&listener) } } -void Updater::ThreadProc() +static void LogNextStartTime(const std::chrono::steady_clock::time_point &clockTime) +{ + using namespace std::chrono; + // convert to system_clock; + auto delay = clockTime - steady_clock::now(); + auto systemTime = system_clock::now() + duration_cast(delay); + auto t = system_clock::to_time_t(systemTime); + Lv2Log::info(SS("Updater: Next update check at " << std::put_time(std::localtime(&t), "%Y-%m-%d %H:%M:%S"))); +} +void UpdaterImpl::ThreadProc() { SetThreadName("UpdateMonitor"); struct pollfd pfd; pfd.fd = this->event_reader; pfd.events = POLLIN; - while (true) + auto lastRetryTime = clock::time_point(); + this->updateRetryTime = LoadRetryTime(); + try { - int ret = poll(&pfd, 1, std::chrono::duration_cast(updateRate).count()); // 1000 ms timeout - - if (ret == -1) + while (true) { - Lv2Log::error("Updater: Poll error."); - break; - } - else if (ret == 0) - { - CheckForUpdate(true); - } - else - { - // Event occurred - uint64_t value; - ssize_t s = read(event_reader, &value, sizeof(uint64_t)); - if (s == sizeof(uint64_t)) + if (updateRetryTime != lastRetryTime) // has retry time changed? { - if (value == CHECK_NOW_EVENT) + lastRetryTime = updateRetryTime; + if (updateRetryTime > clock::now()) + { + LogNextStartTime(updateRetryTime); + } + } + auto clockDelay = this->updateRetryTime - clock::now(); + auto delayMs = std::chrono::duration_cast(clockDelay).count(); + if (delayMs > std::numeric_limits::max()) // could be as little as 32 seconds on a 16-bit int system. + { + delayMs = std::numeric_limits::max(); + } + + int ret = poll(&pfd, 1, (int)delayMs); // 1000 ms timeout + + if (ret == -1) + { + Lv2Log::error("Updater: Poll error."); + break; + } + else if (ret == 0) + { + if (clock::now() >= this->updateRetryTime) { CheckForUpdate(true); } - else if (value == UNCACHED_CHECK_NOW_EVENT) + } + else + { + // Event occurred + uint64_t value; + ssize_t s = read(event_reader, &value, sizeof(uint64_t)); + if (s == sizeof(uint64_t)) { - CheckForUpdate(false); - } - else - { - break; + if (value == CHECK_NOW_EVENT) + { + + CheckForUpdate(true); + } + else if (value == UNCACHED_CHECK_NOW_EVENT) + { + CheckForUpdate(false); + } + else + { + break; + } } } } } + catch (const std::exception &e) + { + Lv2Log::error(SS("Updater: Service thread terminated abnormally: " << e.what())); + } } -class GithubAsset +namespace pipedal { -public: - GithubAsset(json_variant &v); - std::string name; - std::string browser_download_url; - std::string updated_at; -}; -class GithubRelease -{ -public: - GithubRelease(json_variant &v); + class GithubAsset + { + public: + GithubAsset(json_variant &v); + std::string name; + std::string browser_download_url; + std::string updated_at; + }; + class GithubRelease + { + public: + GithubRelease(json_variant &v); - const GithubAsset *GetDownloadForCurrentArchitecture() const; - const GithubAsset *GetGpgKeyForAsset(const std::string &name) const; + const GithubAsset *GetDownloadForCurrentArchitecture() const; + const GithubAsset *GetGpgKeyForAsset(const std::string &name) const; - bool draft = true; - bool prerelease = true; - std::string name; - std::string url; - std::string version; - std::string body; - std::vector assets; - std::string published_at; -}; + bool draft = true; + bool prerelease = true; + std::string name; + std::string url; + std::string version; + std::string body; + std::vector assets; + std::string published_at; + }; +} GithubAsset::GithubAsset(json_variant &v) { @@ -281,7 +447,7 @@ static std::string justTheVersion(const std::string &assetName) return t[1]; } -int compareVersions(const std::string &l, const std::string &r) +static int compareVersions(const std::string &l, const std::string &r) { std::stringstream sl(l); std::stringstream sr(r); @@ -341,19 +507,7 @@ static std::string normalizeReleaseName(const std::string &releaseName) return result; } -static bool IsCacheValid(const UpdateStatus &updateStatus) -{ - if (!updateStatus.IsValid() || !updateStatus.IsOnline()) - { - return false; - } - auto now = std::chrono::system_clock::now(); - auto validStart = updateStatus.LastUpdateTime(); - auto validEnd = validStart + CACHE_DURATION; - return now >= validStart && now < validEnd; -} - -UpdateRelease Updater::getUpdateRelease( +UpdateRelease UpdaterImpl::getUpdateRelease( const std::vector &githubReleases, const std::string ¤tVersion, const UpdateReleasePredicate &predicate) @@ -413,123 +567,183 @@ static void CheckUpdateHttpResponse(std::string errorCode) } } -void Updater::CheckForUpdate(bool useCache) +static std::chrono::system_clock::time_point http_date_to_time_point(const std::string &http_date) +{ + std::tm tm = {}; + std::istringstream ss(http_date); + + ss >> std::get_time(&tm, "%a, %d %b %Y %H:%M:%S GMT"); + + if (ss.fail()) + { + throw std::runtime_error("Failed to parse HTTP date: " + http_date); + } + auto tt = timegm(&tm); + return std::chrono::system_clock::from_time_t(tt); +} + +JSON_MAP_BEGIN(GithubResponseHeaders) +JSON_MAP_REFERENCE(GithubResponseHeaders, date) +JSON_MAP_REFERENCE(GithubResponseHeaders, ratelimit_reset) +JSON_MAP_REFERENCE(GithubResponseHeaders, ratelimit_limit) +JSON_MAP_REFERENCE(GithubResponseHeaders, ratelimit_remaining) +JSON_MAP_REFERENCE(GithubResponseHeaders, ratelimit_used) +JSON_MAP_REFERENCE(GithubResponseHeaders, ratelimit_resource) +JSON_MAP_END(); + +void GithubResponseHeaders::Load(const std::filesystem::path &FILENAME) +{ + std::ifstream f(FILENAME); + if (f.is_open()) + { + try + { + json_reader reader(f); + reader.read(this); + } + catch (const std::exception &e) + { + Lv2Log::error(SS("Invalid file format: " << FILENAME << ".")); + *this = GithubResponseHeaders(); + } + } +} +void GithubResponseHeaders::Save(const std::filesystem::path &FILENAME) +{ + std::filesystem::create_directories(FILENAME.parent_path()); + ofstream_synced f(FILENAME); + if (!f.is_open()) + { + Lv2Log::error(SS("Can't write to " << FILENAME << ".")); + } + else + { + json_writer writer(f); + writer.write(this); + } +} + +GithubResponseHeaders::GithubResponseHeaders(const std::filesystem::path path) +{ + // HTTP/2 403 + // date: Fri, 13 Sep 2024 17:31:22 GMT + //... + // x-ratelimit-limit: 60 + // x-ratelimit-remaining: 0 + // x-ratelimit-reset: 1726250807 + // x-ratelimit-resource: core + // x-ratelimit-used: 60 + std::ifstream f{path}; + if (!f.is_open()) + { + throw std::runtime_error(SS("Can't open file " << path << ".")); + } + std::string http; + f >> http >> code_; + std::string line; + std::getline(f, line); + + while (true) + { + std::getline(f, line); + auto trimPos = line.find_last_not_of("\r\n"); + if (trimPos != std::string::npos) + { + line = line.substr(0, trimPos + 1); + } + + if (!f) + break; + auto pos = line.find(':'); + if (pos != std::string::npos) + { + std::string tag = line.substr(0, pos); + ++pos; + while (pos < line.length() && line[pos] == ' ') + { + ++pos; + } + std::string value = line.substr(pos); + uint64_t *pResult = nullptr; + if (tag == "date") + { + this->date_ = http_date_to_time_point(value); + } + else if (tag == "x-ratelimit-limit") + { + pResult = &ratelimit_limit_; + } + else if (tag == "x-ratelimit-remaining") + { + pResult = &ratelimit_remaining_; + } + else if (tag == "x-ratelimit-reset") + { + std::istringstream ss{value}; + uint64_t reset = 0; + ss >> reset; + auto secs = std::chrono::seconds(reset); // value is seconds in UTC Unix epoch. + this->ratelimit_reset_ = std::chrono::system_clock::time_point(secs); + } + else if (tag == "x-ratelimit-resource") + { + ratelimit_resource_ = value; + } + else if (tag == "x-ratelimit-used") + { + pResult = &ratelimit_used_; + } + if (pResult) + { + std::istringstream ss{value}; + ss >> *pResult; + } + } + } +} + +UpdateStatus UpdaterImpl::GetReleaseGeneratorStatus() +{ + UpdateStatus result = DoUpdate(true); + return result; +} + +void UpdaterImpl::CheckForUpdate(bool useCache) { UpdateStatus updateResult; + // if we've used up too many actual github requests, used the cached version even if the request is non-cached. + if (githubResponseHeaders.ratelimit_remaining_ < 3 * githubResponseHeaders.ratelimit_limit_ / 4) { - std::lock_guard lock{mutex}; - if (useCache && IsCacheValid(cachedUpdateStatus)) - { - this->currentResult = cachedUpdateStatus; - this->currentResult.UpdatePolicy(this->updatePolicy); - if (listener) - { - listener(this->currentResult); - } - return; - } - updateResult = this->currentResult; + useCache = true; } - // const std::string responseOption = "-w \"%{response_code}\""; -#ifdef WIN32 - responseOption = "-w \"%%{response_code}\""; // windows shell requires doubling of the %%. -#endif + if (useCache) + { + if (clock::now() < this->updateRetryTime) + { + std::lock_guard lock{mutex}; + { + this->currentResult = cachedUpdateStatus; + this->currentResult.UpdatePolicy(this->updatePolicy); + if (listener) + { + listener(this->currentResult); + } + return; + } + } + } - std::string args = SS("-s -L " << GITHUB_RELEASES_URL); - - updateResult.errorMessage_ = ""; try { - auto result = sysExecForOutput("curl", args); - if (result.exitCode != EXIT_SUCCESS) - { - throw std::runtime_error("Server has no internet access."); - } - else - { - if (result.output.length() == 0) - { - throw std::runtime_error("Server has no internet access."); - } - std::stringstream ss(result.output); - json_reader reader(ss); - json_variant vResult(reader); - if (vResult.is_object()) - { - // an HTML error. - updateResult.isOnline_ = false; - auto o = vResult.as_object(); - std::string message = o->at("message").as_string(); - auto status_code = o->at("status_code").as_int64(); - throw std::runtime_error(SS("Service error. ()" << status_code << ": " << message << ")")); - } - else - { - json_variant::array_ptr vArray = vResult.as_array(); - - std::vector releases; - for (size_t i = 0; i < vArray->size(); ++i) - { - auto &el = vArray->at(0); - GithubRelease release{el}; - if (!release.draft && release.GetDownloadForCurrentArchitecture() != nullptr) - { - if (release.name.find("Experimental") == std::string::npos) // experimental releases do not participate in auto-updates (not even for dev stream) - { - releases.push_back(std::move(release)); - } - } - } - std::sort( - releases.begin(), - releases.end(), - [](const GithubRelease &left, const GithubRelease &right) - { - return left.published_at > right.published_at; // latest date first. - }); - updateResult.releaseOnlyRelease_ = getUpdateRelease( - releases, - updateResult.currentVersion_, - [](const GithubRelease &githubRelease) - { - return !githubRelease.prerelease && - githubRelease.name.find("Release") != std::string::npos; - }); - - updateResult.releaseOrBetaRelease_ = getUpdateRelease( - releases, - updateResult.currentVersion_, - [](const GithubRelease &githubRelease) - { - return !githubRelease.prerelease && - (githubRelease.name.find("Release") != std::string::npos || - githubRelease.name.find("Beta") != std::string::npos); - }); - updateResult.devRelease_ = getUpdateRelease( - releases, - updateResult.currentVersion_, - [](const GithubRelease &githubRelease) - { - return true; - }); -#ifdef TEST_UPDATE - updateResult.releaseOrBetaRelease_.upgradeVersionDisplayName_ = "PiPedal v1.2.41-Beta"; - updateResult.devRelease_.upgradeVersionDisplayName_ = "PiPedal v1.2.39-Experimental"; - updateResult.devRelease_.upgradeVersion_ = "1.2.39"; - updateResult.devRelease_.updateAvailable_ = false; -#endif - updateResult.isValid_ = true; - updateResult.isOnline_ = true; - } - } + updateResult = DoUpdate(false); } catch (const std::exception &e) { - Lv2Log::error(SS("Failed to fetch update info. " << e.what())); + Lv2Log::error(SS("Updater: Failed to fetch update info. " << e.what())); updateResult.errorMessage_ = e.what(); updateResult.isValid_ = false; updateResult.isOnline_ = false; @@ -537,7 +751,10 @@ void Updater::CheckForUpdate(bool useCache) { std::lock_guard lock{mutex}; updateResult.UpdatePolicy(this->updatePolicy); - this->currentResult = updateResult; + if (updateResult.isOnline_ && updateResult.isValid_) + { + this->currentResult = updateResult; + } SetCachedUpdateStatus(this->currentResult); if (listener) { @@ -545,6 +762,142 @@ void Updater::CheckForUpdate(bool useCache) } } } + +UpdateStatus UpdaterImpl::DoUpdate(bool forReleaseGenerator) +{ + UpdateStatus updateResult; + if (forReleaseGenerator) + { + updateResult.currentVersion_ = "0.0.0"; + } + + // set default next retry time. github failures may postpone even further. + + Lv2Log::info("Checking for updates."); + // const std::string responseOption = "-w \"%{response_code}\""; +#ifdef WIN32 + responseOption = "-w \"%%{response_code}\""; // windows shell requires doubling of the %%. +#endif + + TemporaryFile headerFile{workingDirectory}; + std::string args = SS("-s -L " << GITHUB_RELEASES_URL << " -D " << headerFile.str()); + + updateResult.errorMessage_ = ""; + + auto result = sysExecForOutput("curl", args); + if (result.exitCode != EXIT_SUCCESS) + { + RetryAfter(std::chrono::duration_cast(std::chrono::hours(4))); + throw std::runtime_error("Server has no internet access."); + } + else + { + // hard throttling for github. + RetryAfter(std::chrono::duration_cast(std::chrono::hours(8))); + + GithubResponseHeaders githubHeaders{headerFile.Path()}; + this->githubResponseHeaders = githubHeaders; + this->githubResponseHeaders.Save(githubResponseHeaderFilename); + + if (githubHeaders.code_ != 200) + { + if ( + githubHeaders.ratelimit_limit_ != 0 && + githubHeaders.ratelimit_limit_ == githubHeaders.ratelimit_used_) + { + std::time_t time = std::chrono::system_clock::to_time_t(githubHeaders.ratelimit_reset_); + std::string strTime = std::ctime(&time); + + this->RetryAfter(githubHeaders.ratelimit_reset_ - githubHeaders.date_); + throw std::runtime_error(SS("Github API rate limit exceeded. Retrying at " << strTime)); + } + } + + if (result.output.length() == 0) + { + throw std::runtime_error("Server has no internet access."); + } + std::stringstream ss(result.output); + json_reader reader(ss); + json_variant vResult(reader); + + if (vResult.is_object()) + { + // an HTML error. + updateResult.isOnline_ = false; + auto o = vResult.as_object(); + std::string message = "Unknown error."; + if (o->at("message").is_string()) + { + message = o->at("message").as_string(); + } + throw std::runtime_error(SS("Github Service error: " << message)); + } + else if (!vResult.is_array()) + { + throw std::runtime_error("Invalid file format error."); + } + else + { + json_variant::array_ptr vArray = vResult.as_array(); + + std::vector releases; + for (size_t i = 0; i < vArray->size(); ++i) + { + auto &el = vArray->at(i); + GithubRelease release{el}; + if (!release.draft && release.GetDownloadForCurrentArchitecture() != nullptr) + { + if (release.name.find("Experimental") == std::string::npos) // experimental releases do not participate in auto-updates (not even for dev stream) + { + releases.push_back(std::move(release)); + } + } + } + std::sort( + releases.begin(), + releases.end(), + [](const GithubRelease &left, const GithubRelease &right) + { + return left.published_at > right.published_at; // latest date first. + }); + updateResult.releaseOnlyRelease_ = getUpdateRelease( + releases, + updateResult.currentVersion_, + [](const GithubRelease &githubRelease) + { + return !githubRelease.prerelease && + githubRelease.name.find("Release") != std::string::npos; + }); + + updateResult.releaseOrBetaRelease_ = getUpdateRelease( + releases, + updateResult.currentVersion_, + [](const GithubRelease &githubRelease) + { + return !githubRelease.prerelease && + (githubRelease.name.find("Release") != std::string::npos || + githubRelease.name.find("Beta") != std::string::npos); + }); + updateResult.devRelease_ = getUpdateRelease( + releases, + updateResult.currentVersion_, + [](const GithubRelease &githubRelease) + { + return true; + }); +#ifdef TEST_UPDATE + updateResult.releaseOrBetaRelease_.upgradeVersionDisplayName_ = "PiPedal v1.2.41-Beta"; + updateResult.devRelease_.upgradeVersionDisplayName_ = "PiPedal v1.2.39-Experimental"; + updateResult.devRelease_.upgradeVersion_ = "1.2.39"; + updateResult.devRelease_.updateAvailable_ = false; +#endif + updateResult.isValid_ = true; + updateResult.isOnline_ = true; + } + } + return updateResult; +} bool UpdateRelease::operator==(const UpdateRelease &other) const { return (updateAvailable_ == other.updateAvailable_) && @@ -568,12 +921,12 @@ bool UpdateStatus::operator==(const UpdateStatus &other) const (devRelease_ == other.devRelease_); } -UpdatePolicyT Updater::GetUpdatePolicy() +UpdatePolicyT UpdaterImpl::GetUpdatePolicy() { std::lock_guard lock{mutex}; return updatePolicy; } -void Updater::SetUpdatePolicy(UpdatePolicyT updatePolicy) +void UpdaterImpl::SetUpdatePolicy(UpdatePolicyT updatePolicy) { std::lock_guard lock{mutex}; if (updatePolicy == this->updatePolicy) @@ -586,7 +939,7 @@ void Updater::SetUpdatePolicy(UpdatePolicyT updatePolicy) listener(currentResult); } } -void Updater::ForceUpdateCheck() +void UpdaterImpl::ForceUpdateCheck() { uint64_t value = UNCACHED_CHECK_NOW_EVENT; write(this->event_writer, &value, sizeof(uint64_t)); @@ -664,7 +1017,7 @@ UpdateRelease::UpdateRelease() { } -std::string Updater::GetSignatureUrl(const std::string &url) +std::string UpdaterImpl::GetSignatureUrl(const std::string &url) { // partialy whitelisting, partly avoiding having to parse a URL. @@ -683,7 +1036,7 @@ std::string Updater::GetSignatureUrl(const std::string &url) throw std::runtime_error("Permission denied. No signature URL."); } -std::string Updater::GetUpdateFilename(const std::string &url) +std::string UpdaterImpl::GetUpdateFilename(const std::string &url) { // partialy whitelisting, partly avoiding having to parse a URL. @@ -828,7 +1181,7 @@ void Updater::ValidateSignature(const std::filesystem::path &file, const std::fi << " --verify " << signatureFile << " " << file; - Lv2Log::info(SS("/usr/bin/gpg " << ss.str())); + Lv2Log::debug(SS("/usr/bin/gpg " << ss.str())); auto gpgOutput = sysExecForOutput("/usr/bin/gpg", ss.str()); if (gpgOutput.exitCode != EXIT_SUCCESS) { @@ -838,19 +1191,20 @@ void Updater::ValidateSignature(const std::filesystem::path &file, const std::fi if (!IsSignatureGood(gpgText)) { + Lv2Log::error(gpgOutput.output); throw std::runtime_error("Update signature is not valid."); } std::string keyId = getFingerprint(gpgText); - Lv2Log::info(unCRLF(gpgText)); // yyy delete me. - - if (keyId != UPDATE_GPG_FINGERPRINT) + if (keyId != UPDATE_GPG_FINGERPRINT && keyId != UPDATE_GPG_FINGERPRINT2) { - throw std::runtime_error(SS("Update signature has the wrong fingerprint: " << keyId)); + Lv2Log::error(gpgOutput.output); + throw std::runtime_error(SS("Update signature has the wrong id: " << keyId)); } std::string origin = getAddress(gpgText); - if (origin != UPDATE_GPG_ADDRESS) + if (origin != UPDATE_GPG_ADDRESS && origin != UPDATE_GPG_ADDRESS2) { + Lv2Log::error(gpgOutput.output); throw std::runtime_error(SS("Update signature has an incorrect address." << origin)); } } @@ -893,7 +1247,63 @@ static void checkCurlHttpResponse(std::string errorCode) throw std::runtime_error(message); } } -void Updater::DownloadUpdate(const std::string &url, std::filesystem::path *file, std::filesystem::path *signatureFile) +void UpdaterImpl::RetryAfter(clock::duration delay) +{ + namespace chron = std::chrono; + clock::duration hours24 = chron::duration_cast(chron::hours(24)); + if (delay.count() < 0 || delay >= hours24) // non-synced system clock (as Raspberry Pis are prone to without network connections) + { + delay = chron::duration_cast(chron::hours(6)); + } + SaveRetryTime(chron::system_clock::now() + std::chrono::duration_cast(delay)); + + clock::time_point myRetryTime = clock::now() + delay; + + if (this->updateRetryTime < myRetryTime) + { + this->updateRetryTime = myRetryTime; + } +} +void UpdaterImpl::SaveRetryTime(const std::chrono::system_clock::time_point &time) +{ + fs::path retryTimePath = workingDirectory / "retryTime.json"; + pipedal::ofstream_synced f{retryTimePath}; + if (f.is_open()) + { + json_writer writer(f); + auto rep = time.time_since_epoch().count(); + writer.write(rep); + } +} +UpdaterImpl::clock::time_point UpdaterImpl::LoadRetryTime() +{ + using namespace std::chrono; + + fs::path retryTimePath = workingDirectory / "retryTime.json"; + std::ifstream f{retryTimePath}; + if (f.is_open()) + { + system_clock::duration::rep tRep = 0; + json_reader reader(f); + reader.read(&tRep); + + system_clock::duration duration(tRep); + + system_clock::time_point systemTime = system_clock::time_point(duration); + + // now convert to stead_clock time with the understanding that system_clock time may be blown. + system_clock::duration systemDuration = systemTime - system_clock::now(); + clock::duration clockDuration = duration_cast(systemDuration); + if (clockDuration > duration_cast(hours(48))) // is system clock blown? + { + clockDuration = duration_cast(hours(6)); + } + return clock::now() + clockDuration; + } + return clock::now(); +} + +void UpdaterImpl::DownloadUpdate(const std::string &url, std::filesystem::path *file, std::filesystem::path *signatureFile) { std::string filename, signatureUrl; { @@ -913,7 +1323,7 @@ void Updater::DownloadUpdate(const std::string &url, std::filesystem::path *file { throw std::runtime_error(SS("Invalid update url. Downloads from this address are not permitted: " << signatureUrl)); } - auto downloadDirectory = WORKING_DIRECTORY / "downloads"; + auto downloadDirectory = workingDirectory / "downloads"; std::filesystem::create_directories(downloadDirectory); auto downloadFilePath = downloadDirectory / filename; @@ -929,7 +1339,6 @@ void Updater::DownloadUpdate(const std::string &url, std::filesystem::path *file responseOption = "-w \"%%{response_code}\""; // windows shell requires doubling of the %%. #endif std::string args = SS("-s -L " << responseOption << " " << url << " -o " << downloadFilePath.c_str()); - Lv2Log::info(SS("/usr/bin/curl " << args)); // yyy delete me. auto curlOutput = sysExecForOutput("/usr/bin/curl", args); if (curlOutput.exitCode != EXIT_SUCCESS || badOutput(downloadFilePath)) { @@ -939,7 +1348,6 @@ void Updater::DownloadUpdate(const std::string &url, std::filesystem::path *file checkCurlHttpResponse(curlOutput.output); args = SS("-s -L " << responseOption << " " << signatureUrl << " -o " << downloadSignaturePath.c_str()); - Lv2Log::info(SS("/usr/bin/curl " << args)); curlOutput = sysExecForOutput("/usr/bin/curl", args); if (curlOutput.exitCode != EXIT_SUCCESS || badOutput(downloadSignaturePath)) @@ -978,6 +1386,14 @@ void Updater::DownloadUpdate(const std::string &url, std::filesystem::path *file throw; } } + + +void UpdateStatus::AddRelease(const std::string&packageName) +{ + this->isValid_ = true; + this->errorMessage_ = ""; +} + /////////////////////////////////////////////////////////////////////////////////////////////// JSON_MAP_BEGIN(UpdateRelease) diff --git a/src/Updater.hpp b/src/Updater.hpp index e0c3325..201422b 100644 --- a/src/Updater.hpp +++ b/src/Updater.hpp @@ -26,133 +26,46 @@ #include #include "json.hpp" #include +#include "UpdaterStatus.hpp" -class GithubRelease; namespace pipedal { - class Updater; + class UpdaterImpl; + class GithubRelease; - enum class UpdatePolicyT + + class Updater { - // ordinal values must not change, as files depend on them. - // must match declaration in Updater.tsx - - ReleaseOnly = 0, - ReleaseOrBeta = 1, - Development = 2, - Disabled = 3 - - }; - class UpdateRelease - { - private: - friend class Updater; - bool updateAvailable_ = false; - std::string upgradeVersion_; // just the version. - std::string upgradeVersionDisplayName_; // display name for the version. - std::string assetName_; // filename only - std::string updateUrl_; // url from which to download the .deb file. - std::string gpgSignatureUrl_; // url from which to download the .deb.asc file. + protected: + Updater() {}; public: - UpdateRelease(); + virtual ~Updater() noexcept {} - bool UpdateAvailable() const { return updateAvailable_; } - const std::string &UpgradeVersion() const { return upgradeVersion_; } - const std::string &UpgradeVersionDisplayName() const { return upgradeVersionDisplayName_; } - const std::string &AssetName() const { return assetName_; } - const std::string &UpdateUrl() const { return updateUrl_; } - const std::string &GpgSignatureUrl() const { return gpgSignatureUrl_;} - bool operator==(const UpdateRelease &other) const; - DECLARE_JSON_MAP(UpdateRelease); - }; - - class UpdateStatus - { - private: - friend class Updater; - std::chrono::system_clock::time_point::rep lastUpdateTime_ = 0; - - bool isValid_ = false; - std::string errorMessage_; - bool isOnline_ = false; - std::string currentVersion_; - std::string currentVersionDisplayName_; - - int32_t updatePolicy_ = (int32_t)(UpdatePolicyT::ReleaseOrBeta); - UpdateRelease releaseOnlyRelease_; - UpdateRelease releaseOrBetaRelease_; - UpdateRelease devRelease_; - - public: - UpdateStatus(); - std::chrono::system_clock::time_point LastUpdateTime() const; - void LastUpdateTime(const std::chrono::system_clock::time_point &timePoint); - - void ResetCurrentVersion(); - bool IsValid() const { return isValid_; } - const std::string &ErrorMessage() const { return errorMessage_; } - bool IsOnline() const { return isOnline_; } - const std::string &CurrentVersion() const { return currentVersion_; } - const std::string &CurrentDisplayVersion() const { return currentVersionDisplayName_; } - UpdatePolicyT UpdatePolicy() const { return (UpdatePolicyT)updatePolicy_; } - void UpdatePolicy(UpdatePolicyT updatePreference) { this->updatePolicy_ = (int32_t)updatePreference; } - - const UpdateRelease &ReleaseOnlyRelease() const { return releaseOnlyRelease_; } - const UpdateRelease &ReleaseOrBetaRelease() const { return releaseOrBetaRelease_; } - const UpdateRelease &DevRelease() const { return devRelease_; } - bool operator==(const UpdateStatus &other) const; - - DECLARE_JSON_MAP(UpdateStatus); - }; - - class Updater - { - public: - Updater(); - ~Updater(); + using self = Updater; + using ptr = std::unique_ptr; + static Updater::ptr Create(); + static Updater::ptr Create(const std::filesystem::path & workingDirectory); static void ValidateSignature(const std::filesystem::path&file, const std::filesystem::path&signatureFile); using UpdateListener = std::function; + virtual void SetUpdateListener(UpdateListener &&listener) = 0; + virtual void Start() = 0; + virtual void Stop() = 0; - void SetUpdateListener(UpdateListener &&listener); - void CheckNow(); - void Stop(); + virtual void CheckNow() = 0; - UpdatePolicyT GetUpdatePolicy(); - void SetUpdatePolicy(UpdatePolicyT updatePolicy); - void ForceUpdateCheck(); - void DownloadUpdate(const std::string &url, std::filesystem::path*file, std::filesystem::path*signatureFile); - UpdateStatus GetCurrentStatus() const { return this->currentResult; } - private: - std::string GetUpdateFilename(const std::string &url); - std::string GetSignatureUrl(const std::string &url); + virtual UpdatePolicyT GetUpdatePolicy() = 0; + virtual void SetUpdatePolicy(UpdatePolicyT updatePolicy) = 0; + virtual void ForceUpdateCheck() = 0; + virtual void DownloadUpdate(const std::string &url, std::filesystem::path*file, std::filesystem::path*signatureFile) = 0; + virtual UpdateStatus GetCurrentStatus() const = 0; + virtual UpdateStatus GetReleaseGeneratorStatus() = 0; // only used when generating releases. Absolutely no caching. - UpdatePolicyT updatePolicy = UpdatePolicyT::ReleaseOrBeta; - using UpdateReleasePredicate = std::function; - UpdateRelease getUpdateRelease( - const std::vector &githubReleases, - const std::string ¤tVersion, - const UpdateReleasePredicate &predicate); - - UpdateStatus cachedUpdateStatus; - bool stopped = false; - using clock = std::chrono::steady_clock; - - int event_reader = -1; - int event_writer = -1; - void ThreadProc(); - void CheckForUpdate(bool useCache); - UpdateListener listener; - - std::unique_ptr thread; - std::mutex mutex; - - bool hasInfo = false; - UpdateStatus currentResult; - static clock::duration updateRate; }; + + } \ No newline at end of file diff --git a/src/UpdaterSecurity.hpp b/src/UpdaterSecurity.hpp index 774ee10..3fac951 100644 --- a/src/UpdaterSecurity.hpp +++ b/src/UpdaterSecurity.hpp @@ -24,6 +24,7 @@ #pragma once +#include /* PiPedal uses whitelisting and GPG signatures to verify that updates are valid. You must modify these defines appropriately if you want to host a fork. @@ -32,6 +33,9 @@ installs published on github/rerdavies/pipedal. */ +#ifndef ENABLE_AUTO_UPDATE +#define ENABLE_AUTO_UPDATE 1 +#endif #ifndef GITHUB_PROJECT #define GITHUB_PROJECT "rerdavies/pipedal" #endif @@ -42,10 +46,17 @@ #ifndef UPDATE_GPG_ADDRESS #define UPDATE_GPG_ADDRESS "Robin Davies " #endif +#ifndef UPDATE_GPG_ADDRESS2 +#define UPDATE_GPG_ADDRESS2 "PiPedal Project " +#endif + #ifndef UPDATE_GPG_FINGERPRINT #define UPDATE_GPG_FINGERPRINT "381124E2BB4478D225D2313B2AEF3F7BD53EAA59" #endif +#ifndef UPDATE_GPG_FINGERPRINT2 +#define UPDATE_GPG_FINGERPRINT2 "2D1F39DBB1F819412B678F88E9D7081E08E3D85C" +#endif // Configuration for downloading of updates. diff --git a/src/UpdaterStatus.hpp b/src/UpdaterStatus.hpp new file mode 100644 index 0000000..4d50326 --- /dev/null +++ b/src/UpdaterStatus.hpp @@ -0,0 +1,100 @@ +// Copyright (c) 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 +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#pragma once + +#include "json.hpp" + +namespace pipedal { + enum class UpdatePolicyT + { + // ordinal values must not change, as files depend on them. + // must match declaration in Updater.tsx + + ReleaseOnly = 0, + ReleaseOrBeta = 1, + Development = 2, + Disabled = 3 + + }; + class UpdateRelease + { + private: + friend class UpdaterImpl; + bool updateAvailable_ = false; + std::string upgradeVersion_; // just the version. + std::string upgradeVersionDisplayName_; // display name for the version. + std::string assetName_; // filename only + std::string updateUrl_; // url from which to download the .deb file. + std::string gpgSignatureUrl_; // url from which to download the .deb.asc file. + public: + UpdateRelease(); + + bool UpdateAvailable() const { return updateAvailable_; } + const std::string &UpgradeVersion() const { return upgradeVersion_; } + const std::string &UpgradeVersionDisplayName() const { return upgradeVersionDisplayName_; } + const std::string &AssetName() const { return assetName_; } + const std::string &UpdateUrl() const { return updateUrl_; } + const std::string &GpgSignatureUrl() const { return gpgSignatureUrl_;} + bool operator==(const UpdateRelease &other) const; + DECLARE_JSON_MAP(UpdateRelease); + }; + + class UpdateStatus + { + private: + friend class UpdaterImpl; + std::chrono::system_clock::time_point::rep lastUpdateTime_ = 0; + + bool isValid_ = false; + std::string errorMessage_; + bool isOnline_ = false; + std::string currentVersion_; + std::string currentVersionDisplayName_; + + int32_t updatePolicy_ = (int32_t)(UpdatePolicyT::ReleaseOrBeta); + UpdateRelease releaseOnlyRelease_; + UpdateRelease releaseOrBetaRelease_; + UpdateRelease devRelease_; + + public: + UpdateStatus(); + std::chrono::system_clock::time_point LastUpdateTime() const; + void LastUpdateTime(const std::chrono::system_clock::time_point &timePoint); + + void ResetCurrentVersion(); + bool IsValid() const { return isValid_; } + const std::string &ErrorMessage() const { return errorMessage_; } + bool IsOnline() const { return isOnline_; } + const std::string &CurrentVersion() const { return currentVersion_; } + const std::string &CurrentDisplayVersion() const { return currentVersionDisplayName_; } + UpdatePolicyT UpdatePolicy() const { return (UpdatePolicyT)updatePolicy_; } + void UpdatePolicy(UpdatePolicyT updatePreference) { this->updatePolicy_ = (int32_t)updatePreference; } + + const UpdateRelease &ReleaseOnlyRelease() const { return releaseOnlyRelease_; } + const UpdateRelease &ReleaseOrBetaRelease() const { return releaseOrBetaRelease_; } + const UpdateRelease &DevRelease() const { return devRelease_; } + bool operator==(const UpdateStatus &other) const; + + void AddRelease(const std::string&packageName); // Release Generator ONLY + + DECLARE_JSON_MAP(UpdateStatus); + }; + +} \ No newline at end of file diff --git a/src/UpdaterTest.cpp b/src/UpdaterTest.cpp index 7262ae9..2590492 100644 --- a/src/UpdaterTest.cpp +++ b/src/UpdaterTest.cpp @@ -29,10 +29,10 @@ using namespace pipedal; TEST_CASE( "updater test", "[updater]" ) { int nCalls = 0; cout << "------ upater test ----" << endl; - { Updater updater; + { Updater::ptr updater = Updater::Create(); - updater.SetUpdateListener( + updater->SetUpdateListener( [&nCalls](const UpdateStatus&updateStatus) mutable { @@ -43,6 +43,8 @@ TEST_CASE( "updater test", "[updater]" ) { ++nCalls; } ); + updater->Start(); + } REQUIRE(nCalls == 1); diff --git a/src/WebServer.cpp b/src/WebServer.cpp index 551d39d..5108545 100644 --- a/src/WebServer.cpp +++ b/src/WebServer.cpp @@ -41,6 +41,7 @@ #include "WebServer.hpp" #include "Uri.hpp" +#include "ss.hpp" #include @@ -556,16 +557,6 @@ pipedal::last_modified(const std::filesystem::path &path) } } -static std::string getHostName() -{ - char buff[512]; - if (gethostname(buff, sizeof(buff)) == 0) - { - buff[511] = '\0'; - return buff; - } - return ""; -} static std::string getIpv4Address(const std::string interface) { @@ -1160,27 +1151,11 @@ namespace pipedal m_endpoint.set_close_handler(bind(&WebServerImpl::on_close, this, _1)); m_endpoint.set_http_handler(bind(&WebServerImpl::on_http, this, _1)); - std::string hostName = getHostName(); - if (hostName.length() != 0) - { - std::stringstream ss; - ss << "Listening on " << hostName << ".local:" << this->port; - Lv2Log::info(ss.str()); - } - std::string ipv4Address = getIpv4Address("eth0"); - if (ipv4Address.length() != 0) - { - Lv2Log::info(SS("Listening on " << ipv4Address << ":" << this->port)); - } - std::string wifiAddress = getIpv4Address("wlan0"); - if (wifiAddress.length() != 0) - { - Lv2Log::info(SS("Listening on Wi-Fi address " << wifiAddress << ":" << this->port)); - } + DisplayIpAddresses(); std::stringstream ss; ss << port; - // m_endpoint.listen(this->address, ss.str()); + //m_endpoint.listen(this->address, ss.str()); m_endpoint.listen(tcp::v6(), (uint16_t)port); m_endpoint.start_accept(); @@ -1210,7 +1185,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) { @@ -1252,7 +1227,13 @@ namespace pipedal for (auto it = m_connections.begin(); it != m_connections.end(); ++it) { - m_endpoint.close(*it, websocketpp::close::status::normal, ""); + try + { + m_endpoint.close(*it, websocketpp::close::status::normal, ""); + } + catch (const std::exception &ignored) + { + } } } } @@ -1275,6 +1256,8 @@ namespace pipedal this->pBgThread = new std::thread(ThreadProc, this); } + virtual void DisplayIpAddresses() override; + WebServerImpl(const std::string &address, int port, const char *rootPath, int threads, size_t maxUploadSize); }; } // namespace pipedal @@ -1309,3 +1292,31 @@ std::shared_ptr pipedal::WebServer::create( { return std::shared_ptr(new WebServerImpl(address.to_string(), port, rootPath, threads, maxUploadSize)); } + +void WebServerImpl::DisplayIpAddresses() +{ + std::string hostName = GetHostName(); + if (hostName.length() != 0) + { + std::stringstream ss; + ss << "Listening on mDns address " << hostName << ":" << this->port; + Lv2Log::info(ss.str()); + } + std::string ipv4Address = getIpv4Address("eth0"); + if (ipv4Address.length() != 0) + { + Lv2Log::info(SS("Listening on eth0 address " << ipv4Address << ":" << this->port)); + } + std::string wifiAddress = getIpv4Address("wlan0"); + if (wifiAddress.length() != 0) + { + if (wifiAddress == "10.42.0.1") + { + Lv2Log::info(SS("Listening on Wi-Fi hotspot address " << wifiAddress << ":" << this->port)); + } + else + { + Lv2Log::info(SS("Listening on Wi-Fi address " << wifiAddress << ":" << this->port)); + } + } +} \ No newline at end of file diff --git a/src/WebServer.hpp b/src/WebServer.hpp index 8b86ba8..76b4268 100644 --- a/src/WebServer.hpp +++ b/src/WebServer.hpp @@ -222,6 +222,7 @@ public: virtual ~WebServer() { } virtual void SetLogHttpRequests(bool enableLogging) = 0; + virtual void DisplayIpAddresses() = 0; virtual void AddRequestHandler(std::shared_ptr requestHandler) = 0; virtual void AddSocketFactory(std::shared_ptr &socketHandler) = 0; diff --git a/src/WebServerConfig.cpp b/src/WebServerConfig.cpp index f88a4e1..45566b1 100644 --- a/src/WebServerConfig.cpp +++ b/src/WebServerConfig.cpp @@ -29,6 +29,7 @@ #include "ZipFile.hpp" #include "PiPedalUI.hpp" #include "ofstream_synced.hpp" +#include "UpdaterSecurity.hpp" #define PRESET_EXTENSION ".piPreset" @@ -401,7 +402,6 @@ public: if (patchProperty.length() == 0 && directory.length() == 0) { - // yyy no throwing! throw PiPedalException("Malformed request."); } @@ -473,6 +473,33 @@ public: } }; + +static std::string StripPortNumber(const std::string &fromAddress) +{ + std::string address = fromAddress; + + if (address.size() == 0) + return fromAddress; + + char lastChar = address[address.size() - 1]; + size_t pos = address.find_last_of(':'); + + // if ipv6, make sure we found an actual port address. + size_t posBracket = address.find_last_of(']'); + if (posBracket != std::string::npos && pos != std::string::npos) + { + if (posBracket > pos) + { + pos = std::string::npos; + } + } + if (pos != std::string::npos) + { + address = address.substr(0, pos); + } + return address; +} + /* When hosting a react app, replace /var/config.json with data that will connect the react app with our socket server. */ @@ -492,19 +519,16 @@ public: } std::string GetConfig(const std::string &fromAddress) { - #define LINK_LOCAL_WEB_SOCKET 1 - #if LINK_LOCAL_WEB_SOCKET - std::string webSocketAddress = GetLinkLocalAddress(fromAddress); + std::string webSocketAddress = GetNonLinkLocalAddress(StripPortNumber(fromAddress)); Lv2Log::info(SS("Web Socket Address: " << webSocketAddress << ":" << portNumber)); - #else - std::string webSocketAddress = "*"; - #endif std::stringstream s; s << "{ \"socket_server_port\": " << portNumber - << ", \"socket_server_address\": \"" << webSocketAddress << - "\", \"ui_plugins\": [ ], \"max_upload_size\": " << maxUploadSize << " }"; + << ", \"socket_server_address\": \"" << webSocketAddress + << "\", \"ui_plugins\": [ ], \"max_upload_size\": " << maxUploadSize + << ", \"enable_auto_update\": " << (ENABLE_AUTO_UPDATE ? " true": "false") + << " }"; return s.str(); } diff --git a/src/WebServerLog.hpp b/src/WebServerLog.hpp index 26a3d65..56ad8f8 100644 --- a/src/WebServerLog.hpp +++ b/src/WebServerLog.hpp @@ -117,7 +117,7 @@ public: /// Write a string message to the given channel /** - * @param channel The channel to write to + * @param channel The channel to write tosu * @param msg The message to write */ void write(level channel, std::string const & msg) { @@ -126,19 +126,19 @@ public: { case elevel::devel: case elevel::library: - Lv2Log::debug(msg); + Lv2Log::debug("WebServer: %s",msg.c_str()); break; case elevel::info: - Lv2Log::info(msg); + Lv2Log::info("WebServer: %s",msg.c_str()); break; case elevel::warn: - Lv2Log::warning(msg); + Lv2Log::warning("WebServer: %s",msg.c_str()); break; case elevel::rerror: - Lv2Log::error(msg); + Lv2Log::error("WebServer: %s",msg.c_str()); break; case elevel::fatal: - Lv2Log::error("Fatal error: " + msg); + Lv2Log::error("WebServer fatal error: %s",msg.c_str()); break; default: break; @@ -159,7 +159,7 @@ public: } bool dynamic_test(level channel) { - return (m_channels) & channel != 0; + return (m_channels & channel) != 0; } protected: diff --git a/src/hotspotManagerTestMain.cpp b/src/hotspotManagerTestMain.cpp new file mode 100644 index 0000000..3b1b4aa --- /dev/null +++ b/src/hotspotManagerTestMain.cpp @@ -0,0 +1,64 @@ +#include "HotspotManager.hpp" +#include "WifiConfigSettings.hpp" +#include +#include "Lv2Log.hpp" +#include "DBusLog.hpp" + +using namespace pipedal; + +int main(int argc, char**argv) +{ + WifiConfigSettings settings; + settings.Load(); + if (!settings.valid_ || !settings.hasPassword_) + { + settings.valid_ = true; + settings.channel_ = "1"; + settings.hotspotName_ = "pipedal"; + settings.password_ = "password"; + settings.hasPassword_ = true; + settings.Save(); + } + + Lv2Log::log_level(LogLevel::Debug); + SetDBusLogLevel(DBusLogLevel::Trace); + + HotspotManager::ptr hotspotManager = HotspotManager::Create(); + hotspotManager->Open(); + + while (true) + { + std::string line; + std::cout << "e=no-etthernet,a=always,n=never,q=quit > " << std::endl;; + std::getline(std::cin,line); + + if (line == "e") + { + settings.Load(); + settings.autoStartMode_ = (uint16_t)HotspotAutoStartMode::NoEthernetConnection; + settings.Save(); + hotspotManager->Reload(); + } else if (line == "a") + { + settings.Load(); + 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") { + break; + } else { + std::cout << "Invalid command." << std::endl; + } + } + hotspotManager->Close(); + hotspotManager = nullptr; + + return EXIT_SUCCESS; +} + diff --git a/src/main.cpp b/src/main.cpp index 43a845b..0987066 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -81,10 +81,18 @@ static bool isJackServiceRunning() return std::filesystem::exists(path); } + +static void AsanCheck() +{ + char *t = new char[5]; + t[5] = 'x'; + delete t; + exit(EXIT_FAILURE); +} + int main(int argc, char *argv[]) { - #ifndef WIN32 umask(002); // newly created files in /var/pipedal get 775-ish permissions, which improves debugging/live-service interaction. #endif @@ -117,7 +125,7 @@ int main(int argc, char *argv[]) if (help || parser.Arguments().size() == 0) { std::cout << "pipedald - Pipedal web socket server.\n" - "Copyright (c) 2022 Robin Davies.\n" + "Copyright (c) 2022-2024 Robin Davies.\n" "\n"; } } @@ -220,8 +228,36 @@ int main(int argc, char *argv[]) return EXIT_SUCCESS; // tell systemd not to auto-restart. } } + // only accept signals on the main thread. + // block now so that threads spawned by model inherit the block mask. + int sig; + sigset_t sigSet; + int s; + sigemptyset(&sigSet); + sigaddset(&sigSet, SIGINT); + sigaddset(&sigSet, SIGTERM); + sigaddset(&sigSet, SIGUSR1); + + s = pthread_sigmask(SIG_BLOCK, &sigSet, NULL); + if (s != 0) + { + throw std::logic_error("pthread_sigmask failed."); + } + + + PiPedalModel model; + + model.SetNetworkChangedListener( + [&server]() mutable { + if (server) + { + server->DisplayIpAddresses(); + } + } + ); + model.SetRestartListener( []() { @@ -243,20 +279,6 @@ int main(int argc, char *argv[]) model.WaitForAudioDeviceToComeOnline(); - // only accept signals on the main thread. - int sig; - sigset_t sigSet; - int s; - sigemptyset(&sigSet); - sigaddset(&sigSet, SIGINT); - sigaddset(&sigSet, SIGTERM); - sigaddset(&sigSet, SIGUSR1); - - s = pthread_sigmask(SIG_BLOCK, &sigSet, NULL); - if (s != 0) - { - throw std::logic_error("pthread_sigmask failed."); - } #if JACK_HOST if (systemd) @@ -302,9 +324,9 @@ int main(int argc, char *argv[]) { server->RunInBackground(-1); - SetThreadName("avahi"); - model.UpdateDnsSd(); // now that the server is running, publish a DNS-SD announcement. - SetThreadName("main"); + + model.StartHotspotMonitoring(); + { sigwait(&sigSet, &sig); diff --git a/src/makeReleaseMain.cpp b/src/makeReleaseMain.cpp new file mode 100644 index 0000000..e20bf2c --- /dev/null +++ b/src/makeReleaseMain.cpp @@ -0,0 +1,187 @@ +#include "CommandLineParser.hpp" +#include +#include "ss.hpp" +#include "config.hpp" +#include +#include +#include +#include +#include "unistd.h" +#include "UpdaterSecurity.hpp" +#include "Updater.hpp" +#include + +using namespace std; +namespace fs = std::filesystem; + +std::string getVersionString() +{ + std::string displayVersion = PROJECT_DISPLAY_VERSION; + auto npos = displayVersion.find_last_of('-'); + if (npos == std::string::npos) + { + throw std::runtime_error(SS("PROJECT_DISPLAY_VERSION does not contain a '-' character." << displayVersion)); + } + std::string releaseKind = displayVersion.substr(npos + 1); + + return SS(PROJECT_VER << "-" << releaseKind); +} + +std::string psystem(const std::string &command) +{ + std::string cmdRedirected = command + " 2>&1"; + std::array buffer; + std::string result; + std::unique_ptr pipe(popen(cmdRedirected.c_str(), "r"), pclose); + if (!pipe) + { + throw std::runtime_error("popen() failed!"); + } + while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) + { + result += buffer.data(); + } + return result; +} + +using namespace pipedal; + +void SignPackage() +{ + auto versionString = getVersionString(); + cout << "Processing release " << versionString << endl; + + // build/pipedal_1.2.49_arm64.deb + + fs::path packagePath = SS("build/pipedal_" << PROJECT_VER << "_arm64.deb"); + packagePath = fs::absolute(packagePath); + if (!fs::exists(packagePath)) + { + throw std::runtime_error(SS("File does not exist: " << packagePath)); + } + + + // sign the package. + // gpg --armor --output "$filename".asc -b "$filename" + cout << "--------------------------------------------------------------" << endl; + cout << "Signing the package" << endl; + cout << "--------------------------------------------------------------" << endl; + + std::string signCmd = + SS("/usr/bin/gpg --yes --default-key " << UPDATE_GPG_FINGERPRINT2 + << " --armor --output " << packagePath << ".asc" + << " --detach-sign " << packagePath.c_str()); + int result = system(signCmd.c_str()); + if (result != EXIT_SUCCESS) + { + throw std::runtime_error("Failed to sign package."); + } + + cout << "--------------------------------------------------------------" << endl; + cout << "Building a test keyring." << endl; + cout << "--------------------------------------------------------------" << endl; + + fs::path keyringPath = fs::absolute("build/gpg"); + fs::path keyPath = fs::absolute("config/updatekey2.asc"); + fs::remove_all(keyringPath); + fs::create_directories(keyringPath); + std::ostringstream importCommand; + + importCommand << "/usr/bin/gpg --homedir " << keyringPath.c_str() << " --import " << keyPath.c_str(); + int rc = system(importCommand.str().c_str()); + if (rc != EXIT_SUCCESS) + { + throw std::runtime_error("Failed to create test keyring."); + } + + cout << "--------------------------------------------------------------" << endl; + cout << "Verifying signature" << endl; + cout << "--------------------------------------------------------------" << endl; + + // verify the signature. + // warnings are unavoidable as there's no way to create a root certificate without + // an elaborate manual procedure. + cout << "Warnings are expected, since there's no convenient way to install a " << endl; + cout << "trusted certificate authority in the Updater keychain." << endl; + std::string verifyCommand = SS("/usr/bin/gpg --verify --no-default-keyring " + << "--homedir " << keyringPath.c_str() + << " --armor " << packagePath << ".asc " << packagePath.c_str()); + auto output = psystem(verifyCommand.c_str()); + cout << output << endl; + auto npos = output.find("Good signature from \"" UPDATE_GPG_ADDRESS2 "\""); + if (npos == std::string::npos) + { + throw std::runtime_error("Signature validation failed."); + } + npos == output.find("using RSA key " UPDATE_GPG_FINGERPRINT2); + if (npos == std::string::npos) + { + throw std::runtime_error("Signature validation failed."); + } + cout << "Signature check succeeded." << endl; + cout << "--------------------------------------------------------------" << endl; + cout << "Validating signing procedure" << endl; + cout << "--------------------------------------------------------------" << endl; + // Actually a test to make sure that signature checks fail on a modified binary. + // Calling the test "Testing signature failure" is a more disconcerting than it needs to be. + // Just one last sanity check before letting this procedure loose in the world. + + // create a bad test file. + fs::path badPackage = "build/gpgtest/bad_package.deb"; + fs::create_directories("build/gpgtest"); + fs::remove(badPackage); + fs::copy_file(packagePath, badPackage); + { + std::ofstream f; + f.open(badPackage, ios_base::out | ios_base::ate); + if (!f.is_open()) + { + throw std::runtime_error(SS("Can't open " << badPackage)); + } + f << "Bad stuff."; + } + + // verify the signature. + { + std::string verifyCommand = SS("/usr/bin/gpg --verify --no-default-keyring " + << "--homedir " << keyringPath.c_str() + << " --armor " << packagePath << ".asc " << badPackage.c_str()); + auto output = psystem(verifyCommand.c_str()); + auto npos = output.find("BAD signature from "); + if (npos == std::string::npos) + { + throw std::runtime_error("Signature validation succeeded when it should not have."); + } + } + cout << "Test succeeded." << endl; +} + +void GenerateUpdateJson() +{ + cout << "--------------------------------------------------------------" << endl; + cout << "Validating signing procedure" << endl; + cout << "--------------------------------------------------------------" << endl; + // Actually a test to make sure that signature checks fail on a modified binary. + // Calling the test "Testing signature failure" is a more disconcerting than it needs to be. + // Just one last sanity check in the + std::filesystem::path workingPath = fs::absolute("build/updater"); + Updater::ptr updater = Updater::Create(workingPath); // do NOT start it. We'll do the github request on the current thread. + std::string packageName = SS("build/pipedal_" << PROJECT_VER << "_arm64.deb"); + + // pipedal_1.2.3_arm64.deb + UpdateStatus updateStatus = updater->GetReleaseGeneratorStatus(); + updateStatus.AddRelease(packageName); +} +int main(int argc, char **argv) +{ + CommandLineParser commandLine; + try + { + SignPackage(); + } + catch (const std::exception &e) + { + cout << "ERROR: " << e.what() << endl; + return EXIT_FAILURE; + } +} \ No newline at end of file diff --git a/src/polkit-1/rules/10-pipedal-networkmanager.rules b/src/polkit-1/rules/10-pipedal-networkmanager.rules new file mode 100644 index 0000000..f5fe4d2 --- /dev/null +++ b/src/polkit-1/rules/10-pipedal-networkmanager.rules @@ -0,0 +1,6 @@ +// grant NetworkManager dbus permissions (no-prompts) to the pipedald systemd service. +polkit.addRule(function(action, subject) { + if (action.id.indexOf("org.freedesktop.NetworkManager.") == 0 && subject.isInGroup("pipedal_d")) { + return polkit.Result.YES; + } +}); \ No newline at end of file diff --git a/src/profilePluginMain.cpp b/src/profilePluginMain.cpp index d34acdc..e75eff4 100644 --- a/src/profilePluginMain.cpp +++ b/src/profilePluginMain.cpp @@ -153,6 +153,10 @@ void profilePlugin(const ProfileOptions &profileOptions) RingBufferSink ringBufferSink(writerRingbuffer); + + // run once to get memory allocations in NAM and ML out of the way. + lv2Pedalboard->Run(inputBuffers, outputBuffers, nFrames, &ringBufferWriter); + /* *** Pump the plugin for a bit if it is expected to do work on the scheduler thread when initializing */ if (profileOptions.waitForWork) { diff --git a/src/templates/pipedal_p2pd.conf.template b/src/templates/pipedal_p2pd.conf.template index 9253a1f..9b175ee 100644 --- a/src/templates/pipedal_p2pd.conf.template +++ b/src/templates/pipedal_p2pd.conf.template @@ -1,4 +1,4 @@ -# WiFi regdomain 2-letter country code. +# Wi-Fi regdomain 2-letter country code. # see: http://www.davros.org/misc/iso3166.txt country_code=${COUNTRY_CODE} diff --git a/todo.txt b/todo.txt index b68f42a..e1a64c1 100644 --- a/todo.txt +++ b/todo.txt @@ -1,20 +1,52 @@ -- feature: redo ./makepackage so that it runs without sudo. -- Bug: text Cursor hovering over the breadcrumb bar in file property dialog. -- Bug: Turning off wifi-direct sould re-enable NetworkManager. -- ToobML save/restore state. +- X test reconnect sequence in client. +-X remove 10-pipedal-networkmanager.rules before testing installer. -- make app use the same theme settings as the website. +- new signing procedure. + +- verify that scanning doesn't cause overruns. +- verify address change behaviour in client +- verify stripping in installer. +- verify update with no keyring. +-verify clean removal of dhcpcd and nm_p2p2d + +X Review docs changes once we go live. + +- Back button after reloads on Android. + +Localisation: support non-UTF8 code pages. +- unicode commandline arguments. +- review unicode filenames (this is probably ok) + +- make app use the same theme settings as the website. (make the website use the same theme as the app) - BUG: gcs when we have an animated output control -- Shutdown/reboot midi bindings - versioning. -- do we want to take a second crack at establishin multi-p2p connections with the driver_param? +Package: raspi-firmware +Version: 1:1.20240902-1 + +Package: raspi-firmware +Version: 1.20220830+ds-1 + + +Package: firmware-brcm80211 +Source: firmware-nonfree +Version: 1:20230625-2+rpt3 + +Package: f + + +downgrade: +sudo apt install raspi-firmware=1.20220830+ds-1 +sudo apt install firmware-brcm80211=1:20230625-2+rpt2 + + + Pri Description ----------------- 8 Migrate to Vite toolchain.