Vendored
+4
-3
@@ -112,7 +112,7 @@
|
|||||||
//"[json_variants]" // subtest of your choice, or none to run all of the tests.
|
//"[json_variants]" // subtest of your choice, or none to run all of the tests.
|
||||||
//"[inverting_mutex_test]"
|
//"[inverting_mutex_test]"
|
||||||
// "[utf8_to_utf32]"
|
// "[utf8_to_utf32]"
|
||||||
"[wifi_channels_test]"
|
"[updater]"
|
||||||
],
|
],
|
||||||
|
|
||||||
"stopAtEntry": false,
|
"stopAtEntry": false,
|
||||||
@@ -258,10 +258,11 @@
|
|||||||
"program": "${command:cmake.launchTargetPath}",
|
"program": "${command:cmake.launchTargetPath}",
|
||||||
"args": [
|
"args": [
|
||||||
"--nosudo", // run without sudo (which will fail because of perms, but allow us to debug deeper into install code.)
|
"--nosudo", // run without sudo (which will fail because of perms, but allow us to debug deeper into install code.)
|
||||||
|
"--prefix", "/usr",
|
||||||
//"--get-current-port"
|
//"--get-current-port"
|
||||||
// "--uninstall"
|
"--install"
|
||||||
//"--enable-p2p" , "CA", "PiPedalTest","12345678","14"
|
//"--enable-p2p" , "CA", "PiPedalTest","12345678","14"
|
||||||
"--list-p2p-channels"
|
//"--list-p2p-channels"
|
||||||
],
|
],
|
||||||
"stopAtEntry": false,
|
"stopAtEntry": false,
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
|
|||||||
+6
-7
@@ -1,19 +1,16 @@
|
|||||||
cmake_minimum_required(VERSION 3.16.0)
|
cmake_minimum_required(VERSION 3.16.0)
|
||||||
project(pipedal
|
project(pipedal
|
||||||
VERSION 1.2.41
|
VERSION 1.2.44
|
||||||
DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi"
|
DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi"
|
||||||
HOMEPAGE_URL "https://rerdavies.github.io/pipedal"
|
HOMEPAGE_URL "https://rerdavies.github.io/pipedal"
|
||||||
)
|
)
|
||||||
set (DISPLAY_VERSION "v1.2.41")
|
set (DISPLAY_VERSION "PiPedal v1.2.42-Experimental")
|
||||||
|
set (PACKAGE_ARCHITECTURE "arm64")
|
||||||
set (CMAKE_INSTALL_PREFIX "/usr/")
|
set (CMAKE_INSTALL_PREFIX "/usr/")
|
||||||
|
|
||||||
include(CTest)
|
include(CTest)
|
||||||
enable_testing()
|
enable_testing()
|
||||||
|
|
||||||
# Frameworks
|
|
||||||
# catch
|
|
||||||
|
|
||||||
add_subdirectory("submodules/pipedal_p2pd")
|
add_subdirectory("submodules/pipedal_p2pd")
|
||||||
|
|
||||||
add_subdirectory("PiPedalCommon")
|
add_subdirectory("PiPedalCommon")
|
||||||
@@ -95,7 +92,7 @@ set(CPACK_DEBIAN_PACKAGE_SECTION sound)
|
|||||||
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
||||||
set(CPACK_DEBIAN_PACKAGE_CONTROL_STRICT_PERMISSION TRUE)
|
set(CPACK_DEBIAN_PACKAGE_CONTROL_STRICT_PERMISSION TRUE)
|
||||||
#set(CPACK_DEBIAN_PACKAGE_DEPENDS "jackd2, hostapd, dhcpcd, dnsmasq" )
|
#set(CPACK_DEBIAN_PACKAGE_DEPENDS "jackd2, hostapd, dhcpcd, dnsmasq" )
|
||||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "lv2-dev, hostapd, dhcpcd,dnsmasq, authbind" )
|
set(CPACK_DEBIAN_PACKAGE_DEPENDS "lv2-dev, hostapd, dhcpcd,dnsmasq, authbind, gpg" )
|
||||||
#set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "arm64")
|
#set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "arm64")
|
||||||
set(CPACK_PACKAGING_INSTALL_PREFIX /usr)
|
set(CPACK_PACKAGING_INSTALL_PREFIX /usr)
|
||||||
set(CPACK_PROJECT_NAME ${PROJECT_NAME})
|
set(CPACK_PROJECT_NAME ${PROJECT_NAME})
|
||||||
@@ -103,6 +100,8 @@ set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
|
|||||||
set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT)
|
set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT)
|
||||||
set (CPACK_STRIP_FILES ON)
|
set (CPACK_STRIP_FILES ON)
|
||||||
|
|
||||||
|
set(CPACK_DEBIAN_PACKAGE_SIGN_ALGORITHM "detached")
|
||||||
|
set(CPACK_DEBIAN_PACKAGE_SIGN_TYPE "origin")
|
||||||
|
|
||||||
set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${CMAKE_CURRENT_SOURCE_DIR}/debian/postinst;${CMAKE_CURRENT_SOURCE_DIR}/debian/prerm")
|
set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${CMAKE_CURRENT_SOURCE_DIR}/debian/postinst;${CMAKE_CURRENT_SOURCE_DIR}/debian/prerm")
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,14 @@
|
|||||||
#include "WifiRegs.hpp"
|
#include "WifiRegs.hpp"
|
||||||
#include "ChannelInfo.hpp"
|
#include "ChannelInfo.hpp"
|
||||||
#include "DBusLog.hpp"
|
#include "DBusLog.hpp"
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
using namespace pipedal;
|
using namespace pipedal;
|
||||||
P2pSettings::P2pSettings(const std::filesystem::path&configDirectoryPath)
|
P2pSettings::P2pSettings(const std::filesystem::path&configDirectoryPath, const std::filesystem::path&varDirectoryPath)
|
||||||
|
: configDirectoryPath(configDirectoryPath),
|
||||||
|
varDirectoryPath(varDirectoryPath)
|
||||||
|
|
||||||
{
|
{
|
||||||
this->configDirectoryPath = configDirectoryPath;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,12 +191,40 @@ void P2pSettings::Load()
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void openWithPerms(
|
||||||
|
std::ofstream &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();
|
||||||
|
std::filesystem::create_directories(directory);
|
||||||
|
// open and close to make an existing empty file.
|
||||||
|
// close it.
|
||||||
|
{
|
||||||
|
std::ofstream f;
|
||||||
|
f.open(path);
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
// set the perms.
|
||||||
|
std::filesystem::permissions(
|
||||||
|
path,
|
||||||
|
perms,
|
||||||
|
std::filesystem::perm_options::replace);
|
||||||
|
|
||||||
|
// open for re3al.
|
||||||
|
f.open(path);
|
||||||
|
}
|
||||||
void P2pSettings::Save()
|
void P2pSettings::Save()
|
||||||
{
|
{
|
||||||
auto filename = config_filename();
|
auto filename = config_filename();
|
||||||
try {
|
try {
|
||||||
std::ofstream f(filename);
|
std::ofstream f;
|
||||||
if (!f)
|
openWithPerms(f,filename);
|
||||||
|
|
||||||
|
if (!f.is_open())
|
||||||
{
|
{
|
||||||
throw std::runtime_error("Can't write to file.");
|
throw std::runtime_error("Can't write to file.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
class P2pSettings {
|
class P2pSettings {
|
||||||
// adapter between nm p2p settings, and legacy p2p
|
// adapter between nm p2p settings, and legacy p2p
|
||||||
public:
|
public:
|
||||||
P2pSettings(const std::filesystem::path&configDirectory = "/etc/pipedal/config");
|
P2pSettings(
|
||||||
|
const std::filesystem::path&configDirectory = "/etc/pipedal/config",
|
||||||
|
const std::filesystem::path&varDirectory = "/var/pipedal/config" );
|
||||||
|
|
||||||
void Load();
|
void Load();
|
||||||
void Save();
|
void Save();
|
||||||
@@ -16,6 +18,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
bool valid_ = false;
|
bool valid_ = false;
|
||||||
std::filesystem::path configDirectoryPath;
|
std::filesystem::path configDirectoryPath;
|
||||||
|
std::filesystem::path varDirectoryPath;
|
||||||
bool auth_changed_ = true;
|
bool auth_changed_ = true;
|
||||||
std::vector<uint8_t> device_type_{
|
std::vector<uint8_t> device_type_{
|
||||||
0x00,0x01, 0x00,0x50,0xF2,0x04, 0x00,0x02 /*"1-0050F204-2";*/
|
0x00,0x01, 0x00,0x50,0xF2,0x04, 0x00,0x02 /*"1-0050F204-2";*/
|
||||||
@@ -59,7 +62,7 @@ public:
|
|||||||
};
|
};
|
||||||
std::filesystem::path config_filename() const {
|
std::filesystem::path config_filename() const {
|
||||||
// "/etc/pipedal/config/wpa_supplicant/wpa_supplicant-pipedal.conf"; }
|
// "/etc/pipedal/config/wpa_supplicant/wpa_supplicant-pipedal.conf"; }
|
||||||
return (configDirectoryPath / "NetworkManagerP2P.json").string();
|
return (varDirectoryPath / "NetworkManagerP2P.json").string();
|
||||||
};
|
};
|
||||||
|
|
||||||
const std::string& bssid() const { return bssid_; }
|
const std::string& bssid() const { return bssid_; }
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "ServiceConfiguration.hpp"
|
#include "ServiceConfiguration.hpp"
|
||||||
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <grp.h>
|
#include <grp.h>
|
||||||
@@ -35,36 +36,39 @@ using namespace pipedal;
|
|||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace config_serializer;
|
using namespace config_serializer;
|
||||||
|
|
||||||
|
const char ServiceConfiguration::DEVICEID_FILE_NAME[] = "/var/pipedal/config/service.conf";
|
||||||
const char ServiceConfiguration::DEVICEID_FILE_NAME[] = "/etc/pipedal/config/service.conf";
|
|
||||||
|
|
||||||
#define SERIALIZER_ENTRY(MEMBER_NAME) \
|
#define SERIALIZER_ENTRY(MEMBER_NAME) \
|
||||||
new ConfigSerializer<ServiceConfiguration, decltype(ServiceConfiguration::MEMBER_NAME)>(#MEMBER_NAME, &ServiceConfiguration::MEMBER_NAME, "")
|
new ConfigSerializer<ServiceConfiguration, decltype(ServiceConfiguration::MEMBER_NAME)>(#MEMBER_NAME, &ServiceConfiguration::MEMBER_NAME, "")
|
||||||
|
|
||||||
|
|
||||||
static p2p::autoptr_vector<ConfigSerializerBase<ServiceConfiguration>>
|
static p2p::autoptr_vector<ConfigSerializerBase<ServiceConfiguration>>
|
||||||
deviceIdSerializers
|
deviceIdSerializers{
|
||||||
{
|
|
||||||
SERIALIZER_ENTRY(uuid),
|
SERIALIZER_ENTRY(uuid),
|
||||||
SERIALIZER_ENTRY(deviceName),
|
SERIALIZER_ENTRY(deviceName),
|
||||||
SERIALIZER_ENTRY(server_port)
|
SERIALIZER_ENTRY(server_port)};
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
ServiceConfiguration::ServiceConfiguration()
|
ServiceConfiguration::ServiceConfiguration()
|
||||||
: base(deviceIdSerializers)
|
: base(deviceIdSerializers)
|
||||||
{
|
{
|
||||||
|
filename = "/var/pipedal/config/service.conf";
|
||||||
}
|
}
|
||||||
|
|
||||||
void ServiceConfiguration::Load()
|
void ServiceConfiguration::Load(std::filesystem::path filename)
|
||||||
{
|
{
|
||||||
base::Load(DEVICEID_FILE_NAME,false);
|
this->filename = filename;
|
||||||
|
base::Load(filename, false);
|
||||||
}
|
}
|
||||||
void ServiceConfiguration::Save()
|
void ServiceConfiguration::Save()
|
||||||
{
|
{
|
||||||
base::Save(DEVICEID_FILE_NAME);
|
|
||||||
{
|
{
|
||||||
|
auto directory = filename.parent_path();
|
||||||
|
std::filesystem::create_directories(directory);
|
||||||
|
|
||||||
|
// make sure the file has correct permissions.
|
||||||
|
std::ofstream t;
|
||||||
|
t.open(filename);
|
||||||
|
t.close();
|
||||||
|
|
||||||
struct group *group_;
|
struct group *group_;
|
||||||
group_ = getgrnam("pipedal_d");
|
group_ = getgrnam("pipedal_d");
|
||||||
if (group_ == nullptr)
|
if (group_ == nullptr)
|
||||||
@@ -72,6 +76,9 @@ void ServiceConfiguration::Save()
|
|||||||
throw logic_error("Group not found: pipedal_d");
|
throw logic_error("Group not found: pipedal_d");
|
||||||
}
|
}
|
||||||
std::ignore = chown(DEVICEID_FILE_NAME, -1, group_->gr_gid);
|
std::ignore = chown(DEVICEID_FILE_NAME, -1, group_->gr_gid);
|
||||||
std::ignore = chmod(DEVICEID_FILE_NAME, S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH);
|
std::ignore = chmod(DEVICEID_FILE_NAME, S_IWUSR | S_IRUSR | S_IRGRP | S_IWGRP | S_IROTH);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
base::Save(filename);
|
||||||
}
|
}
|
||||||
@@ -267,3 +267,42 @@ int pipedal::sysExecWait(ProcessId pid_)
|
|||||||
return exitStatus;
|
return exitStatus;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SysExecOutput pipedal::sysExecForOutput(const std::string &program, const std::string &args)
|
||||||
|
{
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
fs::path fullPath = findOnSystemPath(program);
|
||||||
|
if (!fs::exists(fullPath))
|
||||||
|
{
|
||||||
|
throw std::runtime_error(SS("Path does not exist. " << fullPath));
|
||||||
|
}
|
||||||
|
std::stringstream s;
|
||||||
|
s << fullPath.c_str() << " " << args << " 2>&1";
|
||||||
|
|
||||||
|
std::string fullCommand = s.str();
|
||||||
|
FILE *output = popen(fullCommand.c_str(), "r");
|
||||||
|
if (output)
|
||||||
|
{
|
||||||
|
char buffer[512];
|
||||||
|
std::stringstream ssOutput;
|
||||||
|
|
||||||
|
while (!feof(output))
|
||||||
|
{
|
||||||
|
size_t nRead = fread(buffer, sizeof(char),sizeof(buffer), output);
|
||||||
|
ssOutput.write(buffer,nRead);
|
||||||
|
if (nRead == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
int rc = pclose(output);
|
||||||
|
SysExecOutput result
|
||||||
|
{
|
||||||
|
.exitCode = rc,
|
||||||
|
.output = ssOutput.str()
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
throw std::runtime_error(SS("Failed to execute command. " << fullCommand ));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -158,12 +158,12 @@ void WifiDirectConfigSettings::Load()
|
|||||||
{
|
{
|
||||||
this->enable_ = false;
|
this->enable_ = false;
|
||||||
std::string strEnable;
|
std::string strEnable;
|
||||||
if (ConfigUtil::GetConfigLine("/etc/pipedal/config/pipedal_p2pd.conf","enabled",&strEnable))
|
if (ConfigUtil::GetConfigLine("/var/pipedal/config/pipedal_p2pd.conf","enabled",&strEnable))
|
||||||
{
|
{
|
||||||
this->enable_ = (strEnable == "true" || strEnable == "1");
|
this->enable_ = (strEnable == "true" || strEnable == "1");
|
||||||
}
|
}
|
||||||
std::string strWlan;
|
std::string strWlan;
|
||||||
if (ConfigUtil::GetConfigLine("/etc/pipedal/config/pipedal_p2pd.conf","wlan",&strWlan))
|
if (ConfigUtil::GetConfigLine("/var/pipedal/config/pipedal_p2pd.conf","wlan",&strWlan))
|
||||||
{
|
{
|
||||||
this->wlan_ = strWlan;
|
this->wlan_ = strWlan;
|
||||||
} else {
|
} else {
|
||||||
@@ -172,20 +172,20 @@ void WifiDirectConfigSettings::Load()
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (!ConfigUtil::GetConfigLine("/etc/pipedal/config/pipedal_p2pd.conf","p2p_device_name",&this->hotspotName_))
|
if (!ConfigUtil::GetConfigLine("/var/pipedal/config/pipedal_p2pd.conf","p2p_device_name",&this->hotspotName_))
|
||||||
{
|
{
|
||||||
this->hotspotName_ = "PiPedal";
|
this->hotspotName_ = "PiPedal";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ConfigUtil::GetConfigLine("/etc/pipedal/config/pipedal_p2pd.conf","country_code",&this->countryCode_))
|
if (!ConfigUtil::GetConfigLine("/var/pipedal/config/pipedal_p2pd.conf","country_code",&this->countryCode_))
|
||||||
{
|
{
|
||||||
this->countryCode_ = "US";
|
this->countryCode_ = "US";
|
||||||
}
|
}
|
||||||
if (!ConfigUtil::GetConfigLine("/etc/pipedal/config/pipedal_p2pd.conf","p2p_pin",&this->pin_))
|
if (!ConfigUtil::GetConfigLine("/var/pipedal/config/pipedal_p2pd.conf","p2p_pin",&this->pin_))
|
||||||
{
|
{
|
||||||
this->pin_ = "12345678";
|
this->pin_ = "12345678";
|
||||||
}
|
}
|
||||||
if (!ConfigUtil::GetConfigLine("/etc/pipedal/config/pipedal_p2pd.conf","wifiChannel",&this->channel_))
|
if (!ConfigUtil::GetConfigLine("/var/pipedal/config/pipedal_p2pd.conf","wifiChannel",&this->channel_))
|
||||||
{
|
{
|
||||||
this->channel_ = "6";
|
this->channel_ = "6";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include "ConfigSerializer.hpp"
|
#include "ConfigSerializer.hpp"
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
namespace pipedal {
|
namespace pipedal {
|
||||||
using namespace config_serializer;
|
using namespace config_serializer;
|
||||||
@@ -38,12 +39,16 @@ namespace pipedal {
|
|||||||
|
|
||||||
static const char DEVICEID_FILE_NAME[];
|
static const char DEVICEID_FILE_NAME[];
|
||||||
|
|
||||||
void Load();
|
void Load(
|
||||||
|
std::filesystem::path path = "/var/pipedal/config/service.conf"
|
||||||
|
);
|
||||||
void Save();
|
void Save();
|
||||||
|
|
||||||
|
|
||||||
std::string uuid;
|
std::string uuid;
|
||||||
std::string deviceName = "PiPedal";
|
std::string deviceName = "PiPedal";
|
||||||
uint32_t server_port = 80;
|
uint32_t server_port = 80;
|
||||||
|
private:
|
||||||
|
std::filesystem::path filename;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -20,13 +20,21 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace pipedal {
|
namespace pipedal
|
||||||
|
{
|
||||||
// exec a command, returning the actual exit code (unlike execXX() or system() )
|
// exec a command, returning the actual exit code (unlike execXX() or system() )
|
||||||
int sysExec(const char *szCommand);
|
int sysExec(const char *szCommand);
|
||||||
|
|
||||||
// execute a command, suppressing output.
|
// execute a command, suppressing output.
|
||||||
int silentSysExec(const char *szCommand);
|
int silentSysExec(const char *szCommand);
|
||||||
|
|
||||||
|
struct SysExecOutput
|
||||||
|
{
|
||||||
|
int exitCode;
|
||||||
|
std::string output;
|
||||||
|
};
|
||||||
|
|
||||||
|
SysExecOutput sysExecForOutput(const std::string& command, const std::string&args);
|
||||||
|
|
||||||
using ProcessId = int64_t; // platform-agnostic wrapper for pid_t;
|
using ProcessId = int64_t; // platform-agnostic wrapper for pid_t;
|
||||||
// Returns a pid or -1 on errror.
|
// Returns a pid or -1 on errror.
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ namespace pipedal
|
|||||||
public:
|
public:
|
||||||
~json_variant();
|
~json_variant();
|
||||||
json_variant();
|
json_variant();
|
||||||
|
json_variant(json_reader &reader);
|
||||||
json_variant(json_variant &&);
|
json_variant(json_variant &&);
|
||||||
json_variant(const json_variant &);
|
json_variant(const json_variant &);
|
||||||
|
|
||||||
@@ -145,6 +146,22 @@ namespace pipedal
|
|||||||
return content.double_value;
|
return content.double_value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int8_t as_int8() const { return (int8_t)as_number(); }
|
||||||
|
|
||||||
|
uint8_t as_uint8() const { return (uint8_t)as_number(); }
|
||||||
|
|
||||||
|
int16_t as_int16() const { return (int16_t)as_number(); }
|
||||||
|
|
||||||
|
uint16_t as_uint16() const { return (uint16_t)as_number(); }
|
||||||
|
|
||||||
|
int32_t as_int32() const { return (int32_t)as_number(); }
|
||||||
|
|
||||||
|
uint32_t as_uint32() const { return (uint32_t)as_number(); }
|
||||||
|
|
||||||
|
int64_t as_int64() const { return (int64_t)as_number(); }
|
||||||
|
|
||||||
|
uint64_t as_uint64() const { return (uint64_t)as_number(); }
|
||||||
|
|
||||||
const std::string &as_string() const;
|
const std::string &as_string() const;
|
||||||
std::string &as_string();
|
std::string &as_string();
|
||||||
|
|
||||||
@@ -154,9 +171,6 @@ namespace pipedal
|
|||||||
const std::shared_ptr<json_array> &as_array() const;
|
const std::shared_ptr<json_array> &as_array() const;
|
||||||
std::shared_ptr<json_array> &as_array();
|
std::shared_ptr<json_array> &as_array();
|
||||||
|
|
||||||
template <typename U>
|
|
||||||
U &as() { static_assert("Invalid type."); }
|
|
||||||
|
|
||||||
// convenience methods for object and array manipulation.
|
// convenience methods for object and array manipulation.
|
||||||
static json_variant make_object();
|
static json_variant make_object();
|
||||||
static json_variant make_array();
|
static json_variant make_array();
|
||||||
@@ -179,6 +193,7 @@ namespace pipedal
|
|||||||
bool operator!=(const json_variant &other) const;
|
bool operator!=(const json_variant &other) const;
|
||||||
|
|
||||||
std::string to_string() const;
|
std::string to_string() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void free();
|
void free();
|
||||||
void write_double_value(json_writer &writer, double value) const;
|
void write_double_value(json_writer &writer, double value) const;
|
||||||
@@ -285,7 +300,6 @@ namespace pipedal
|
|||||||
bool operator!=(const json_object &other) const { return (!((*this) == other)); }
|
bool operator!=(const json_object &other) const { return (!((*this) == other)); }
|
||||||
bool contains(const std::string &index) const;
|
bool contains(const std::string &index) const;
|
||||||
|
|
||||||
|
|
||||||
using values_t = std::vector<std::pair<std::string, json_variant>>;
|
using values_t = std::vector<std::pair<std::string, json_variant>>;
|
||||||
using iterator = values_t::iterator;
|
using iterator = values_t::iterator;
|
||||||
using const_iterator = values_t::const_iterator;
|
using const_iterator = values_t::const_iterator;
|
||||||
@@ -298,12 +312,12 @@ namespace pipedal
|
|||||||
iterator find(const std::string &key);
|
iterator find(const std::string &key);
|
||||||
const_iterator find(const std::string &key) const;
|
const_iterator find(const std::string &key) const;
|
||||||
|
|
||||||
|
|
||||||
// strictly for testing purposes. Not thread-safe.
|
// strictly for testing purposes. Not thread-safe.
|
||||||
static int64_t allocation_count()
|
static int64_t allocation_count()
|
||||||
{
|
{
|
||||||
return allocation_count_;
|
return allocation_count_;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
virtual void read_json(json_reader &reader);
|
virtual void read_json(json_reader &reader);
|
||||||
virtual void write_json(json_writer &writer) const;
|
virtual void write_json(json_writer &writer) const;
|
||||||
@@ -317,27 +331,6 @@ namespace pipedal
|
|||||||
inline std::string &json_variant::memString() { return *(std::string *)content.mem; }
|
inline std::string &json_variant::memString() { return *(std::string *)content.mem; }
|
||||||
inline const std::string &json_variant::memString() const { return *(const std::string *)content.mem; }
|
inline const std::string &json_variant::memString() const { return *(const std::string *)content.mem; }
|
||||||
|
|
||||||
template <>
|
|
||||||
inline json_null &json_variant::as<json_null>() { return as_null(); }
|
|
||||||
|
|
||||||
template <>
|
|
||||||
inline bool &json_variant::as<bool>() { return as_bool(); }
|
|
||||||
|
|
||||||
template <>
|
|
||||||
inline double &json_variant::as<double>() { return as_number(); }
|
|
||||||
|
|
||||||
template <>
|
|
||||||
inline std::string &json_variant::as<std::string>() { return as_string(); }
|
|
||||||
|
|
||||||
template <>
|
|
||||||
inline std::shared_ptr<json_object> &json_variant::as<std::shared_ptr<json_object>>() { return as_object(); }
|
|
||||||
|
|
||||||
template <>
|
|
||||||
inline std::shared_ptr<json_array> &json_variant::as<std::shared_ptr<json_array>>() { return as_array(); }
|
|
||||||
|
|
||||||
template <>
|
|
||||||
inline json_variant &json_variant::as<json_variant>() { return *this; }
|
|
||||||
|
|
||||||
inline json_variant::object_ptr &json_variant::memObject()
|
inline json_variant::object_ptr &json_variant::memObject()
|
||||||
{
|
{
|
||||||
return *(object_ptr *)content.mem;
|
return *(object_ptr *)content.mem;
|
||||||
@@ -525,9 +518,9 @@ namespace pipedal
|
|||||||
return !(*this == other);
|
return !(*this == other);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Holds a string but is json_read and json_written as an unqoted json object.
|
// Holds a string but is json_read and json_written as an unqoted json object.
|
||||||
class raw_json_string: public JsonSerializable {
|
class raw_json_string : public JsonSerializable
|
||||||
|
{
|
||||||
public:
|
public:
|
||||||
raw_json_string() {}
|
raw_json_string() {}
|
||||||
raw_json_string(const std::string &value) : value(value) {}
|
raw_json_string(const std::string &value) : value(value) {}
|
||||||
@@ -536,11 +529,12 @@ namespace pipedal
|
|||||||
void Set(const std::string &value) { this->value = value; }
|
void Set(const std::string &value) { this->value = value; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
virtual void write_json(json_writer &writer) const
|
||||||
virtual void write_json(json_writer &writer) const {
|
{
|
||||||
writer.write_raw(value.c_str());
|
writer.write_raw(value.c_str());
|
||||||
}
|
}
|
||||||
virtual void read_json(json_reader &reader) {
|
virtual void read_json(json_reader &reader)
|
||||||
|
{
|
||||||
throw std::logic_error("Not implemented.");
|
throw std::logic_error("Not implemented.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -552,6 +552,11 @@ json_variant::json_variant(const char*sz)
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
json_variant::json_variant(json_reader&reader)
|
||||||
|
{
|
||||||
|
this->read_json(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*static*/ json_null json_null::instance;
|
/*static*/ json_null json_null::instance;
|
||||||
|
|||||||
@@ -6,13 +6,13 @@
|
|||||||
<a href="https://rerdavies.github.io/pipedal/LicensePiPedal.html"><img src="https://img.shields.io/badge/MIT-MIT?label=license&color=%23808080"/></a>
|
<a href="https://rerdavies.github.io/pipedal/LicensePiPedal.html"><img src="https://img.shields.io/badge/MIT-MIT?label=license&color=%23808080"/></a>
|
||||||
<a href="https://github.com/rerdavies/pipedal/actions"><img src="https://img.shields.io/github/actions/workflow/status/rerdavies/pipedal/cmake.yml?branch=main"/></a>
|
<a href="https://github.com/rerdavies/pipedal/actions"><img src="https://img.shields.io/github/actions/workflow/status/rerdavies/pipedal/cmake.yml?branch=main"/></a>
|
||||||
|
|
||||||
Download: <a href='https://rerdavies.github.io/pipedal/download.html'>v1.2.41</a>
|
Download: <a href='https://rerdavies.github.io/pipedal/download.html'>v1.2.42</a>
|
||||||
Website: [https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal).
|
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).
|
Documentation: [https://rerdavies.github.io/pipedal/Documentation.html](https://rerdavies.github.io/pipedal/Documentation.html).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### NEW version 1.2.41 Release, providing support for Raspberry Pi OS Bookworm. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.
|
#### NEW version 1.2.44 Release, providing support for Raspberry Pi OS Bookworm. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+20
-14
@@ -1,33 +1,39 @@
|
|||||||
{
|
{
|
||||||
|
|
||||||
|
// DO NOT EDIT THIS FILE
|
||||||
|
//
|
||||||
|
// THIS FILE CONTAINS DEFAULT VALUES AS SET BY THE INSTALLER, AND IS OVERWRITTEN BY
|
||||||
|
// UPGRADES.
|
||||||
|
//
|
||||||
|
// If you wish to customize any of these values, see /var/pipedal/config/config.json
|
||||||
|
|
||||||
|
/* A writable directory in which state files can be stored, preferrably only by the server process. */
|
||||||
|
"local_storage_path": "/var/pipedal",
|
||||||
|
|
||||||
/* One or more directories containing LV2 plugins, seperated by ':' */
|
/* One or more directories containing LV2 plugins, seperated by ':' */
|
||||||
"lv2_path": "/usr/lib/lv2:/usr/local/lib/lv2:/usr/modep/lv2",
|
"lv2_path": "/usr/lib/lv2:/usr/local/lib/lv2:/usr/modep/lv2",
|
||||||
|
|
||||||
|
|
||||||
/* A writable directory in which state files can be stored, preferrably only by the server process. */
|
/* whether to lock a process pages into memor. should be true unless running on a very memory constrained system.
|
||||||
"local_storage_path": "/var/pipedal",
|
Setting to false may cause unpredictable audio dropouts.
|
||||||
/* whether to lock a process pages into memroy. should be true unless running on a very memory constrained system.
|
|
||||||
Setting to false will cause unpredictable audio dropouts.
|
|
||||||
*/
|
*/
|
||||||
"mlock": true,
|
"mlock": true,
|
||||||
|
|
||||||
/* Address on which Piddle listens for websocket connections */
|
|
||||||
"socketServerAddress": "0.0.0.0:80",
|
|
||||||
|
|
||||||
/* Number of threads to use for servicing websockets */
|
/* Number of threads to use for servicing websockets */
|
||||||
"threads" : 5,
|
"threads" : 5,
|
||||||
|
|
||||||
|
|
||||||
|
/* Address on which the web server listens for http requests. */
|
||||||
|
"socketServerAddress": "0.0.0.0:80",
|
||||||
|
|
||||||
|
/* Whether to log individual http requests. (inadvisable) */
|
||||||
"logHttpRequests": false,
|
"logHttpRequests": false,
|
||||||
|
/* Log level for the web server */
|
||||||
/* { None=0,Error =1,Warning =2,Info = 3, Debug=4} */
|
/* { None=0,Error =1,Warning =2,Info = 3, Debug=4} */
|
||||||
"logLevel": 3,
|
"logLevel": 3,
|
||||||
|
|
||||||
/* Maximum filesize to allow when uploading */
|
/* Maximum filesize to allow when uploading */
|
||||||
"maxUploadSize": 536870912,
|
"maxUploadSize": 536870912 // 512MiB
|
||||||
|
|
||||||
/* Provide access point capture redirects on this gateway. -- provides automatic browser launching on Access Point access.
|
|
||||||
(not implemented)
|
|
||||||
*/
|
|
||||||
"accessPointGateway": "172.24.1.0/24",
|
|
||||||
"accessPointServerAddress": "172.24.1.1"
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
mQGNBGbOVk8BDAD31asK3tIVqn66DiedrambXgA+DIua4aaruI/dyUiTr4vtF2r3
|
||||||
|
aAeZwkG3Wk01DYIEUXZpMG2CKUKV8hJlfIaR5SZxigHFo/tp4kGYk1UjIN12C8XO
|
||||||
|
qyDLsgEftsOimU6UZpxVaMN6GrhR8i2efygMQpC/6wsDJfwsqrUyM/Z/SyQeW/Cj
|
||||||
|
LSxUn+wkmsDq0HmITtoa2kKLSAVSyngmvDwIdYANdBBvq97Xaw27mDWt47K/qLwv
|
||||||
|
tT2b+yCRTTqMD/IWTCX1VsAuiglBJBqbdH/w9tdeTJzXNmmPcI5jFk/ad0YGvgo3
|
||||||
|
8xjmN/YaueIiegJpIiOOv3X5/hlRNjKWITaiPTbzkVTfkggw9QBl0liolia7EfLR
|
||||||
|
TVxTa2j+F0G0+oWtTe8CiyejIgOqJjmEVARUjuerG+Ezy6HEWcz0e0vM+lkYzEVq
|
||||||
|
IvgPDzQ3lilYUSRvxFw95D1xZmTo87nC++Kw7NGC+Jd9GANHgrxJWhDo+l/DOK/E
|
||||||
|
/MpWhwwfUnyTBwUAEQEAAbQiUm9iaW4gRGF2aWVzIDxyZXJkYXZpZXNAZ21haWwu
|
||||||
|
Y29tPokBzgQTAQoAOBYhBDgRJOK7RHjSJdIxOyrvP3vVPqpZBQJmzlZPAhsDBQsJ
|
||||||
|
CAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJECrvP3vVPqpZxkcMAIN+XQqhkhIQpOIR
|
||||||
|
RW38pYGvf9kG6eQfWDzZYnnatBUdsO2soipzqtOjyRutESG1vayJ2fuSowaoI+kV
|
||||||
|
kVpjfS7rVE31uCM9pOYzjwZfTpj82AXku7aqLRg3cgQMnZuvrjbQ0WyjA+h2Nu0h
|
||||||
|
IBvBUUxFMNuaDVsGq5kx4D+ncWdiDuc7H8pR6pAiltnpUYEXJuU+lrBeMYKnz8f4
|
||||||
|
htj73fl7fSlkeecssHz3rC6txil3bfgzNN9bIS+MfGteRE4JmBd0c4eyArQuqHKm
|
||||||
|
INgIz7bySQwPBVmJO/26f/ANMoMJc3c8ukGTR5pzNESxYlzugUATf6H7KYXBOkGG
|
||||||
|
RgsFqoa9H/i/eHUWIZVAhXi9FIUgdqQhyKzkM/bG18muBieZpKjTquTvnf1Wj88L
|
||||||
|
4GJzT01vwO/gWAYawf4FEMGyfadZ7KT0L5O+8BcLrhhKvg/VuWGSgtRcgJZDeWdd
|
||||||
|
mxUpHZAHb2/0YqVprXL18TUD8dfCIXSa6BlRtPogZlxduzrCzrkBjQRmzlZPAQwA
|
||||||
|
vfBhU09D3WrakekpbMmCzOcKFQ+CGJ0OxisHWgkb9iLJ+bae3HIvy6WQ/cmIM6BG
|
||||||
|
/e9/mhOFpMbZ+xDl7lpuT4qZZwTtDGWLIlxYBRu9b3kKWEP2+/F6GZDjjJVVidEe
|
||||||
|
bko7qYPUgSXwX3XkwT8T5HiTFesY0sPAo0fEcTEyVjg2thcnuCtlj7oOxQO/+k9O
|
||||||
|
Dmc4rdGgghMXTX6zWHaTWk0CGHInaFQ08qmiFz7/DDW3SkrdK6l1BMg+ReGhybM+
|
||||||
|
lzUMmlc5cFj8OyMZpVF7g8Vy2/4V8i4PneCwzVmHT1ShdNvdLUg6U6vG1/eHL5uS
|
||||||
|
32FfprdwlfALDv9MIdS7uTTfTThgODCSf5BwC4S+ZLnKlstRMH4Z1NzH/VRtRj8R
|
||||||
|
rpGgOLsYcy3Q3CTuinKGxe/BdyRgvmSgtdcrJEKClyMwHtBZIWaE2/t2lipeWllZ
|
||||||
|
NOz9+3rMy003GqzrlV+bIRBFp4JA9+mtROQn4frY/kMiaUo+Q12VxxyL7XeoeeUR
|
||||||
|
ABEBAAGJAbYEGAEKACAWIQQ4ESTiu0R40iXSMTsq7z971T6qWQUCZs5WTwIbDAAK
|
||||||
|
CRAq7z971T6qWXk/DAC6vryv/mxICrOCmJTL19+AO7J/94Q9Yo0TEVLDr5xEGgpP
|
||||||
|
FulrHKMEa8k09yK7TJaFRCIfdu4PGztLn28XL7psY+NjMwRlqCMSY5ppjJKujVzR
|
||||||
|
kYI2YSStfnO7y8bdzPNKEXhyFO5j9gaR/Xi7c1UXnLULOY07d8TRE66SNhyJVBTH
|
||||||
|
fQMoCldFjd/Um7lYNjEY9REO0m+ZWSaWIUYI5LXDhVdPh8wIsFgW33kDy12w/vZE
|
||||||
|
DauYStxFHflpdQec8DB9bmFLyG5Ov9g8x+BzJyg3fELb+9sBPcEA787+XDT8PMO2
|
||||||
|
fie7Qxpq3DAJW9KK236WPHP3oWprAAzcyumwPq5m47sVSCFolhbg3r4iAzwdCsuv
|
||||||
|
TSH2rpsGfBWJNs4qZSuYbtv1ZesE44+Qgh1Y/iAEJLqAT23QcIe1ZowOJB3bbXIV
|
||||||
|
JrqWVJ6BzDOy/yllbSFmrM44LASP5C6kGwddx3R4RDLccYKs/NkuucZJc+sL3k0a
|
||||||
|
QOsNQMsqIgaFJgS/9Yc=
|
||||||
|
=d5rj
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
+3
-3
@@ -13,17 +13,17 @@ page_icon: img/Install4.jpg
|
|||||||
|
|
||||||
Download the most recent Debian (.deb) package for your platform:
|
Download the most recent Debian (.deb) package for your platform:
|
||||||
|
|
||||||
- [Raspberry Pi OS bookworm (64-bit) v1.2.41](https://github.com/rerdavies/pipedal/releases/download/)
|
- [Raspberry Pi OS bookworm (64-bit) v1.2.42](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)
|
- [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.41 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.44 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
|
Install the package by running
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo apt update
|
sudo apt update
|
||||||
cd ~/Downloads
|
cd ~/Downloads
|
||||||
sudo apt-get install pipedal_1.2.41_arm64.deb
|
sudo apt-get install pipedal_1.2.42_arm64.deb
|
||||||
```
|
```
|
||||||
Adjust accordingly if you have downloaded v1.1.31.
|
Adjust accordingly if you have downloaded v1.1.31.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,24 @@
|
|||||||
# Release Notes
|
# Release Notes
|
||||||
|
|
||||||
|
## PiPedal 1.2.44 Release
|
||||||
|
|
||||||
|
This version includes the following new features:
|
||||||
|
|
||||||
|
- Double-tap (or double-click) dial controls to reset them to default values.
|
||||||
|
- Update PiPedal to the latest version from within the PiPedal web UI or PiPedal client. PiPedal monitors the PiPedal github repository for new releases,
|
||||||
|
and prompts you to update when new releases become available.
|
||||||
|
- Choose whether to monitor Release, Beta or Developer update streams, or disable update monitoring altogether.
|
||||||
|
- All PiPedal configuration settings now preserved when upgrading.
|
||||||
|
- Advanced configuration options are now stored in /var/pipedal/config/config.json so that they are preserved when upgrading.
|
||||||
|
|
||||||
|
<img src="img/Updates-sshot.png" width="80%"/>
|
||||||
|
|
||||||
|
Bug fixes:
|
||||||
|
|
||||||
|
- Fixed a bug which prevents the PiPedal UI from updating properly when using TooB Convolution Reverb and CabIR effects.
|
||||||
|
- Plugin controls now work when there's no audio device running, or when the audio device stops.
|
||||||
|
- Minor style and theming issues.
|
||||||
|
|
||||||
## PiPedal 1.2.41 Release
|
## PiPedal 1.2.41 Release
|
||||||
|
|
||||||
This version includes the following new features:
|
This version includes the following new features:
|
||||||
|
|||||||
+3
-3
@@ -4,17 +4,17 @@
|
|||||||
|
|
||||||
Download the most recent Debian (.deb) package for your platform:
|
Download the most recent Debian (.deb) package for your platform:
|
||||||
|
|
||||||
- <a href="https://github.com/rerdavies/pipedal/releases/download/v1.2.41/pipedal_1.2.41_arm64.deb">Raspberry Pi OS Bookworm (64-bit) v1.2.41</a>
|
- <a href="https://github.com/rerdavies/pipedal/releases/download/v1.2.42/pipedal_1.2.42_arm64.deb">Raspberry Pi OS Bookworm (64-bit) v1.2.42</a>
|
||||||
- <a href="https://github.com/rerdavies/pipedal/releases/download/v1.1.31/pipedal_1.1.31_arm64.deb">Ubuntu 21.04 or Raspberry Pi OS bullseyeyeye (64-bit) v1.1.31</a>
|
- <a href="https://github.com/rerdavies/pipedal/releases/download/v1.1.31/pipedal_1.1.31_arm64.deb">Ubuntu 21.04 or Raspberry Pi OS bullseyeyeye (64-bit) v1.1.31</a>
|
||||||
|
|
||||||
v1.2.41 is does not currently support Ubuntu 21.04, or older versions of Raspberry Pi OS.
|
v1.2.42 is does not currently support Ubuntu 21.04, or older versions of Raspberry Pi OS.
|
||||||
|
|
||||||
Install the package by running
|
Install the package by running
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo apt update
|
sudo apt update
|
||||||
cd ~/Downloads
|
cd ~/Downloads
|
||||||
sudo apt-get install ./pipedal_1.2.41_arm64.deb
|
sudo apt-get install ./pipedal_1.2.42_arm64.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
Follow the instructions in [_Configuring PiPedal After Installation_](https://rerdavies.github.io/pipedal/Configuring.html) to complete the installation.
|
Follow the instructions in [_Configuring PiPedal After Installation_](https://rerdavies.github.io/pipedal/Configuring.html) to complete the installation.
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<img src="GithubBanner.png" width="100%"/>
|
<img src="GithubBanner.png" width="100%"/>
|
||||||
<a href="Installing.html"><i>v1.2.41</i></a>
|
<a href="Installing.html"><i>v1.2.42</i></a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ To download PiPedal, click [here](download.md).
|
|||||||
To view PiPedal documentation, click [here](Documentation.md).
|
To view PiPedal documentation, click [here](Documentation.md).
|
||||||
|
|
||||||
|
|
||||||
#### NEW version 1.2.41 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.
|
#### NEW version 1.2.44 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ cabir:impulseFile3
|
|||||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||||
lv2:minorVersion 0 ;
|
lv2:minorVersion 0 ;
|
||||||
lv2:microVersion 36 ;
|
lv2:microVersion 43 ;
|
||||||
rdfs:comment """
|
rdfs:comment """
|
||||||
|
|
||||||
TooB Cab IR is a convolution-based guitar cabinet impulse response simulator.
|
TooB Cab IR is a convolution-based guitar cabinet impulse response simulator.
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ toob:frequencyResponseVector
|
|||||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||||
lv2:minorVersion 0 ;
|
lv2:minorVersion 0 ;
|
||||||
lv2:microVersion 36 ;
|
lv2:microVersion 43 ;
|
||||||
|
|
||||||
mod:brand "TooB";
|
mod:brand "TooB";
|
||||||
mod:label "TooB CabSim";
|
mod:label "TooB CabSim";
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ toobimpulse:impulseFile
|
|||||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||||
lv2:minorVersion 0 ;
|
lv2:minorVersion 0 ;
|
||||||
lv2:microVersion 36 ;
|
lv2:microVersion 43 ;
|
||||||
rdfs:comment """
|
rdfs:comment """
|
||||||
|
|
||||||
Convolution reverb is a notoriously compute-intensive effect. If you are having performance issues, use the Max T control to constrain the length of the impulse file to
|
Convolution reverb is a notoriously compute-intensive effect. If you are having performance issues, use the Max T control to constrain the length of the impulse file to
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ toobimpulse:impulseFile
|
|||||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||||
lv2:minorVersion 0 ;
|
lv2:minorVersion 0 ;
|
||||||
lv2:microVersion 36 ;
|
lv2:microVersion 43 ;
|
||||||
rdfs:comment """
|
rdfs:comment """
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ inputStage:filterGroup
|
|||||||
doap:license <https://two-play.com/TooB/licenses/isc> ;
|
doap:license <https://two-play.com/TooB/licenses/isc> ;
|
||||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||||
lv2:minorVersion 0 ;
|
lv2:minorVersion 0 ;
|
||||||
lv2:microVersion 36 ;
|
lv2:microVersion 43 ;
|
||||||
|
|
||||||
mod:brand "TooB";
|
mod:brand "TooB";
|
||||||
mod:label "TooB Input";
|
mod:label "TooB Input";
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||||
lv2:minorVersion 0 ;
|
lv2:minorVersion 0 ;
|
||||||
lv2:microVersion 36 ;
|
lv2:microVersion 43 ;
|
||||||
rdfs:comment "TooB spectrum analyzer" ;
|
rdfs:comment "TooB spectrum analyzer" ;
|
||||||
|
|
||||||
mod:brand "TooB";
|
mod:brand "TooB";
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ tonestack:eqGroup
|
|||||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||||
lv2:minorVersion 0 ;
|
lv2:minorVersion 0 ;
|
||||||
lv2:microVersion 36 ;
|
lv2:microVersion 43 ;
|
||||||
|
|
||||||
uiext:ui <http://two-play.com/plugins/toob-tone-stack-ui>;
|
uiext:ui <http://two-play.com/plugins/toob-tone-stack-ui>;
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
ToobAmp.so.1.1.36
|
ToobAmp.so.1.1.43
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -40,7 +40,7 @@
|
|||||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||||
lv2:minorVersion 0 ;
|
lv2:minorVersion 0 ;
|
||||||
lv2:microVersion 36 ;
|
lv2:microVersion 43 ;
|
||||||
rdfs:comment """
|
rdfs:comment """
|
||||||
Emulation of a Boss CE-2 Chorus.
|
Emulation of a Boss CE-2 Chorus.
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||||
lv2:minorVersion 0 ;
|
lv2:minorVersion 0 ;
|
||||||
lv2:microVersion 36 ;
|
lv2:microVersion 43 ;
|
||||||
rdfs:comment """
|
rdfs:comment """
|
||||||
A straightforward no-frills digital delay.
|
A straightforward no-frills digital delay.
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||||
lv2:minorVersion 0 ;
|
lv2:minorVersion 0 ;
|
||||||
lv2:microVersion 36 ;
|
lv2:microVersion 43 ;
|
||||||
rdfs:comment """
|
rdfs:comment """
|
||||||
Emulation of a Boss BF-2 Flanger, based on circuit analysis and simulation of the original.
|
Emulation of a Boss BF-2 Flanger, based on circuit analysis and simulation of the original.
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||||
lv2:minorVersion 0 ;
|
lv2:minorVersion 0 ;
|
||||||
lv2:microVersion 36 ;
|
lv2:microVersion 43 ;
|
||||||
rdfs:comment """
|
rdfs:comment """
|
||||||
Digital emulation of a Boss BF-2 Flanger.
|
Digital emulation of a Boss BF-2 Flanger.
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||||
lv2:minorVersion 0 ;
|
lv2:minorVersion 0 ;
|
||||||
lv2:microVersion 36 ;
|
lv2:microVersion 43 ;
|
||||||
rdfs:comment """
|
rdfs:comment """
|
||||||
Toob Freeverb is an Lv2 implementation of the famous Freeverb reverb effect. FreeVerb delivers a well-balanced reverb with very little tonal coloration.
|
Toob Freeverb is an Lv2 implementation of the famous Freeverb reverb effect. FreeVerb delivers a well-balanced reverb with very little tonal coloration.
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ toobml:sagGroup
|
|||||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||||
lv2:minorVersion 0 ;
|
lv2:minorVersion 0 ;
|
||||||
lv2:microVersion 36 ;
|
lv2:microVersion 43 ;
|
||||||
rdfs:comment """
|
rdfs:comment """
|
||||||
The TooB ML Amplifier plugin provides emulation of a variety of amplifiers and overdrive pedals that are implemented
|
The TooB ML Amplifier plugin provides emulation of a variety of amplifiers and overdrive pedals that are implemented
|
||||||
using neural-network-based machine learning models of real amplifiers.
|
using neural-network-based machine learning models of real amplifiers.
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ toobNam:eqGroup
|
|||||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||||
lv2:minorVersion 0 ;
|
lv2:minorVersion 0 ;
|
||||||
lv2:microVersion 36 ;
|
lv2:microVersion 43 ;
|
||||||
rdfs:comment """
|
rdfs:comment """
|
||||||
A port of Steven Atkinson's Neural Amp Modeler to LV2.
|
A port of Steven Atkinson's Neural Amp Modeler to LV2.
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
|
||||||
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
doap:maintainer <http://two-play.com/rerdavies#me> ;
|
||||||
lv2:minorVersion 0 ;
|
lv2:minorVersion 0 ;
|
||||||
lv2:microVersion 36 ;
|
lv2:microVersion 43 ;
|
||||||
rdfs:comment """
|
rdfs:comment """
|
||||||
TooB Tuner is a chromatic guitar tuner.
|
TooB Tuner is a chromatic guitar tuner.
|
||||||
""" ;
|
""" ;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ add_custom_command(
|
|||||||
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/react
|
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/react
|
||||||
|
|
||||||
DEPENDS
|
DEPENDS
|
||||||
|
src/Updater.tsx
|
||||||
src/OkCancelDialog.tsx
|
src/OkCancelDialog.tsx
|
||||||
src/PluginControl.tsx
|
src/PluginControl.tsx
|
||||||
src/svg/ic_save_bank_as.svg
|
src/svg/ic_save_bank_as.svg
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"socket_server_port": 80,
|
"socket_server_port": 81,
|
||||||
"socket_server_address": "*",
|
"socket_server_address": "*",
|
||||||
"debug": true,
|
"debug": true,
|
||||||
"max_upload_size": 536870912,
|
"max_upload_size": 536870912,
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ const AboutDialog = withStyles(styles, { withTheme: true })(
|
|||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
// ok in debug builds. File doesn't get placed until install time.
|
// ok in debug builds. File doesn't get placed until install time.
|
||||||
console.log("Failed to fetch open-source notices. " + err);
|
console.log("Failed to fetch open-source notices. " + err.toString());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,7 +184,7 @@ const AboutDialog = withStyles(styles, { withTheme: true })(
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
let classes = this.props.classes;
|
let classes = this.props.classes;
|
||||||
|
let addressKey = 0;
|
||||||
return (
|
return (
|
||||||
<DialogEx tag="about" fullScreen open={this.props.open}
|
<DialogEx tag="about" fullScreen open={this.props.open}
|
||||||
onClose={() => { this.props.onClose() }} TransitionComponent={Transition}
|
onClose={() => { this.props.onClose() }} TransitionComponent={Transition}
|
||||||
@@ -228,7 +228,7 @@ const AboutDialog = withStyles(styles, { withTheme: true })(
|
|||||||
|
|
||||||
{this.model.isAndroidHosted() && (
|
{this.model.isAndroidHosted() && (
|
||||||
<Typography noWrap display="block" variant="body2" style={{ marginBottom: 12 }} >
|
<Typography noWrap display="block" variant="body2" style={{ marginBottom: 12 }} >
|
||||||
Copyright © 2022 Robin Davies.
|
Copyright © 2022-2024 Robin Davies.
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
<Divider />
|
<Divider />
|
||||||
@@ -239,7 +239,7 @@ const AboutDialog = withStyles(styles, { withTheme: true })(
|
|||||||
{
|
{
|
||||||
this.model.serverVersion?.webAddresses.map((address) =>
|
this.model.serverVersion?.webAddresses.map((address) =>
|
||||||
(
|
(
|
||||||
<Typography display="block" variant="body2" style={{ marginBottom: 0, marginLeft: 24 }} >
|
<Typography key={addressKey++} display="block" variant="body2" style={{ marginBottom: 0, marginLeft: 24 }} >
|
||||||
{address}
|
{address}
|
||||||
</Typography>
|
</Typography>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export interface AndroidHostInterface {
|
|||||||
getHostVersion() : string;
|
getHostVersion() : string;
|
||||||
chooseNewDevice() : void;
|
chooseNewDevice() : void;
|
||||||
setDisconnected(isDisconnected: boolean): void;
|
setDisconnected(isDisconnected: boolean): void;
|
||||||
|
launchExternalUrl(url:string): boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class FakeAndroidHost implements AndroidHostInterface
|
export class FakeAndroidHost implements AndroidHostInterface
|
||||||
@@ -47,4 +47,8 @@ export class FakeAndroidHost implements AndroidHostInterface
|
|||||||
}
|
}
|
||||||
showSponsorship() : void { }
|
showSponsorship() : void { }
|
||||||
|
|
||||||
|
launchExternalUrl(url:string): boolean
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+48
-20
@@ -48,7 +48,7 @@ import SettingsDialog from './SettingsDialog';
|
|||||||
import AboutDialog from './AboutDialog';
|
import AboutDialog from './AboutDialog';
|
||||||
import BankDialog from './BankDialog';
|
import BankDialog from './BankDialog';
|
||||||
|
|
||||||
import { PiPedalModelFactory, PiPedalModel, State, ZoomedControlInfo } from './PiPedalModel';
|
import { PiPedalModelFactory, PiPedalModel, State, ZoomedControlInfo,wantsLoadingScreen } from './PiPedalModel';
|
||||||
import ZoomedUiControl from './ZoomedUiControl'
|
import ZoomedUiControl from './ZoomedUiControl'
|
||||||
import MainPage from './MainPage';
|
import MainPage from './MainPage';
|
||||||
import DialogContent from '@mui/material/DialogContent';
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
@@ -60,6 +60,7 @@ import RenameDialog from './RenameDialog';
|
|||||||
import JackStatusView from './JackStatusView';
|
import JackStatusView from './JackStatusView';
|
||||||
import { Theme } from '@mui/material/styles';
|
import { Theme } from '@mui/material/styles';
|
||||||
import { isDarkMode } from './DarkMode';
|
import { isDarkMode } from './DarkMode';
|
||||||
|
import UpdateDialog from './UpdateDialog';
|
||||||
|
|
||||||
import { ReactComponent as RenameOutlineIcon } from './svg/drive_file_rename_outline_black_24dp.svg';
|
import { ReactComponent as RenameOutlineIcon } from './svg/drive_file_rename_outline_black_24dp.svg';
|
||||||
import { ReactComponent as SaveBankAsIcon } from './svg/ic_save_bank_as.svg';
|
import { ReactComponent as SaveBankAsIcon } from './svg/ic_save_bank_as.svg';
|
||||||
@@ -270,6 +271,7 @@ type AppState = {
|
|||||||
tinyToolBar: boolean;
|
tinyToolBar: boolean;
|
||||||
alertDialogOpen: boolean;
|
alertDialogOpen: boolean;
|
||||||
alertDialogMessage: string;
|
alertDialogMessage: string;
|
||||||
|
updateDialogOpen: boolean;
|
||||||
isSettingsDialogOpen: boolean;
|
isSettingsDialogOpen: boolean;
|
||||||
onboarding: boolean;
|
onboarding: boolean;
|
||||||
isDebug: boolean;
|
isDebug: boolean;
|
||||||
@@ -324,6 +326,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
alertDialogMessage: "",
|
alertDialogMessage: "",
|
||||||
presetName: this.model_.presets.get().getSelectedText(),
|
presetName: this.model_.presets.get().getSelectedText(),
|
||||||
isSettingsDialogOpen: false,
|
isSettingsDialogOpen: false,
|
||||||
|
updateDialogOpen: false,
|
||||||
onboarding: false,
|
onboarding: false,
|
||||||
isDebug: true,
|
isDebug: true,
|
||||||
presetChanged: this.model_.presets.get().presetChanged,
|
presetChanged: this.model_.presets.get().presetChanged,
|
||||||
@@ -339,8 +342,10 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.promptForUpdateHandler = this.promptForUpdateHandler.bind(this);
|
||||||
this.errorChangeHandler_ = this.setErrorMessage.bind(this);
|
this.errorChangeHandler_ = this.setErrorMessage.bind(this);
|
||||||
this.unmountListener = this.unmountListener.bind(this);
|
this.beforeUnloadListener = this.beforeUnloadListener.bind(this);
|
||||||
|
this.unloadListener = this.unloadListener.bind(this);
|
||||||
this.stateChangeHandler_ = this.setDisplayState.bind(this);
|
this.stateChangeHandler_ = this.setDisplayState.bind(this);
|
||||||
this.presetChangedHandler = this.presetChangedHandler.bind(this);
|
this.presetChangedHandler = this.presetChangedHandler.bind(this);
|
||||||
this.alertMessageChangedHandler = this.alertMessageChangedHandler.bind(this);
|
this.alertMessageChangedHandler = this.alertMessageChangedHandler.bind(this);
|
||||||
@@ -353,7 +358,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
|
|
||||||
onOpenBank(bankId: number) {
|
onOpenBank(bankId: number) {
|
||||||
this.model_.openBank(bankId)
|
this.model_.openBank(bankId)
|
||||||
.catch((error) => this.model_.showAlert(error));
|
.catch((error) => this.model_.showAlert(error.toString()));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,7 +383,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
});
|
});
|
||||||
this.model_.saveBankAs(this.model_.banks.get().selectedBank, newName)
|
this.model_.saveBankAs(this.model_.banks.get().selectedBank, newName)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.model_.showAlert(error);
|
this.model_.showAlert(error.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -402,7 +407,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
});
|
});
|
||||||
this.model_.renameBank(this.model_.banks.get().selectedBank, newName)
|
this.model_.renameBank(this.model_.banks.get().selectedBank, newName)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.model_.showAlert(error);
|
this.model_.showAlert(error.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -510,18 +515,26 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
this.setState({ isFullScreen: !this.state.isFullScreen });
|
this.setState({ isFullScreen: !this.state.isFullScreen });
|
||||||
}
|
}
|
||||||
|
|
||||||
private unmountListener(e: Event) {
|
private unloadListener(e: Event) {
|
||||||
if (this.model_.state.get() === State.Ready && !this.model_.isAndroidHosted()) {
|
this.model_.close();
|
||||||
e.preventDefault();
|
|
||||||
(e as any).returnValue = "Are you sure you want to leave this page?";
|
|
||||||
return "Are you sure you want to leave this page?";
|
|
||||||
}
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private beforeUnloadListener(e: Event) {
|
||||||
|
this.model_.close();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
promptForUpdateHandler(newValue: boolean) {
|
||||||
|
if (this.state.updateDialogOpen !== newValue)
|
||||||
|
{
|
||||||
|
this.setState({updateDialogOpen: newValue});
|
||||||
|
}
|
||||||
|
}
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
|
||||||
super.componentDidMount();
|
super.componentDidMount();
|
||||||
window.addEventListener("beforeunload", this.unmountListener);
|
window.addEventListener("beforeunload", this.beforeUnloadListener);
|
||||||
|
window.addEventListener("unload", this.unloadListener);
|
||||||
|
|
||||||
this.model_.errorMessage.addOnChangedHandler(this.errorChangeHandler_);
|
this.model_.errorMessage.addOnChangedHandler(this.errorChangeHandler_);
|
||||||
this.model_.state.addOnChangedHandler(this.stateChangeHandler_);
|
this.model_.state.addOnChangedHandler(this.stateChangeHandler_);
|
||||||
@@ -529,6 +542,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
this.model_.alertMessage.addOnChangedHandler(this.alertMessageChangedHandler);
|
this.model_.alertMessage.addOnChangedHandler(this.alertMessageChangedHandler);
|
||||||
this.model_.banks.addOnChangedHandler(this.banksChangedHandler);
|
this.model_.banks.addOnChangedHandler(this.banksChangedHandler);
|
||||||
this.model_.showStatusMonitor.addOnChangedHandler(this.showStatusMonitorHandler);
|
this.model_.showStatusMonitor.addOnChangedHandler(this.showStatusMonitorHandler);
|
||||||
|
this.model_.promptForUpdate.addOnChangedHandler(this.promptForUpdateHandler);
|
||||||
this.alertMessageChangedHandler();
|
this.alertMessageChangedHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,14 +564,17 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
super.componentWillUnmount();
|
super.componentWillUnmount();
|
||||||
window.removeEventListener("beforeunload", this.unmountListener);
|
window.removeEventListener("beforeunload", this.beforeUnloadListener);
|
||||||
|
|
||||||
|
this.model_.promptForUpdate.removeOnChangedHandler(this.promptForUpdateHandler);
|
||||||
this.model_.errorMessage.removeOnChangedHandler(this.errorChangeHandler_);
|
this.model_.errorMessage.removeOnChangedHandler(this.errorChangeHandler_);
|
||||||
this.model_.state.removeOnChangedHandler(this.stateChangeHandler_);
|
this.model_.state.removeOnChangedHandler(this.stateChangeHandler_);
|
||||||
this.model_.pedalboard.removeOnChangedHandler(this.presetChangedHandler);
|
this.model_.pedalboard.removeOnChangedHandler(this.presetChangedHandler);
|
||||||
this.model_.banks.removeOnChangedHandler(this.banksChangedHandler);
|
this.model_.banks.removeOnChangedHandler(this.banksChangedHandler);
|
||||||
this.model_.banks.removeOnChangedHandler(this.showStatusMonitorHandler);
|
this.model_.banks.removeOnChangedHandler(this.showStatusMonitorHandler);
|
||||||
|
|
||||||
|
this.model_.close();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
alertMessageChangedHandler() {
|
alertMessageChangedHandler() {
|
||||||
@@ -661,6 +678,10 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
return "Applying\u00A0changes...";
|
return "Applying\u00A0changes...";
|
||||||
case State.ReloadingPlugins:
|
case State.ReloadingPlugins:
|
||||||
return "Reloading\u00A0plugins...";
|
return "Reloading\u00A0plugins...";
|
||||||
|
case State.DownloadingUpdate:
|
||||||
|
return "Downloading update...";
|
||||||
|
case State.InstallingUpdate:
|
||||||
|
return "Installing update....";
|
||||||
default:
|
default:
|
||||||
return "Reconnecting...";
|
return "Reconnecting...";
|
||||||
}
|
}
|
||||||
@@ -787,19 +808,19 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
<ListItemIcon >
|
<ListItemIcon >
|
||||||
<RenameOutlineIcon color='inherit' className={classes.menuIcon} />
|
<RenameOutlineIcon color='inherit' className={classes.menuIcon} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary='Rename Bank' />
|
<ListItemText primary='Rename bank' />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem button key='SaveBank' onClick={() => { this.handleDrawerSaveBankAs() }} >
|
<ListItem button key='SaveBank' onClick={() => { this.handleDrawerSaveBankAs() }} >
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<SaveBankAsIcon color="inherit" className={classes.menuIcon} />
|
<SaveBankAsIcon color="inherit" className={classes.menuIcon} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary='Save As New Bank' />
|
<ListItemText primary='Save as new bank' />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem button key='EditBanks' onClick={() => { this.handleDrawerManageBanks(); }}>
|
<ListItem button key='EditBanks' onClick={() => { this.handleDrawerManageBanks(); }}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<EditBanksIcon color="inherit" className={classes.menuIcon} />
|
<EditBanksIcon color="inherit" className={classes.menuIcon} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary='Manage Banks...' />
|
<ListItemText primary='Manage banks...' />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
<Divider />
|
<Divider />
|
||||||
@@ -840,7 +861,10 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<BankDialog show={this.state.bankDialogOpen} isEditDialog={this.state.editBankDialogOpen} onDialogClose={() => this.setState({ bankDialogOpen: false })} />
|
<BankDialog show={this.state.bankDialogOpen} isEditDialog={this.state.editBankDialogOpen} onDialogClose={() => this.setState({ bankDialogOpen: false })} />
|
||||||
|
{ (this.state.aboutDialogOpen)&&
|
||||||
|
(
|
||||||
<AboutDialog open={this.state.aboutDialogOpen} onClose={() => this.setState({ aboutDialogOpen: false })} />
|
<AboutDialog open={this.state.aboutDialogOpen} onClose={() => this.setState({ aboutDialogOpen: false })} />
|
||||||
|
)}
|
||||||
<SettingsDialog
|
<SettingsDialog
|
||||||
open={this.state.isSettingsDialogOpen}
|
open={this.state.isSettingsDialogOpen}
|
||||||
onboarding={this.state.onboarding}
|
onboarding={this.state.onboarding}
|
||||||
@@ -872,6 +896,11 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
onDialogClosed={() => { this.model_.zoomedUiControl.set(undefined); }
|
onDialogClosed={() => { this.model_.zoomedUiControl.set(undefined); }
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<UpdateDialog open={this.state.updateDialogOpen} />
|
||||||
|
{this.state.showStatusMonitor && (<JackStatusView />)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<DialogEx
|
<DialogEx
|
||||||
tag="Alert"
|
tag="Alert"
|
||||||
open={this.state.alertDialogOpen}
|
open={this.state.alertDialogOpen}
|
||||||
@@ -893,14 +922,13 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</DialogEx>
|
</DialogEx>
|
||||||
{this.state.showStatusMonitor && (<JackStatusView />)}
|
|
||||||
|
|
||||||
<div className={classes.errorContent} style={{
|
<div className={classes.errorContent} style={{
|
||||||
display: (
|
display: (
|
||||||
this.state.displayState === State.Reconnecting
|
wantsLoadingScreen(this.state.displayState)
|
||||||
|| this.state.displayState === State.ApplyingChanges
|
|
||||||
|| this.state.displayState === State.ReloadingPlugins)
|
|
||||||
? "block" : "none"
|
? "block" : "none"
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={classes.errorContentMask} />
|
<div className={classes.errorContentMask} />
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ import DialogContent from '@mui/material/DialogContent';
|
|||||||
import DialogActions from '@mui/material/DialogActions';
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
import { ReactComponent as DownloadIcon } from './svg/file_download_black_24dp.svg';
|
import { ReactComponent as DownloadIcon } from './svg/file_download_black_24dp.svg';
|
||||||
import { ReactComponent as UploadIcon } from './svg/file_upload_black_24dp.svg';
|
import { ReactComponent as UploadIcon } from './svg/file_upload_black_24dp.svg';
|
||||||
|
import { ReactComponent as BankIcon } from './svg/ic_bank.svg';
|
||||||
|
//import PublishIcon from '@mui/icons-material/Publish';
|
||||||
|
// import FileDownloadIcon from '@mui/icons-material/FileDownload';
|
||||||
|
// import AppsIcon from '@mui/icons-material/Apps';
|
||||||
|
|
||||||
interface BankDialogProps extends WithStyles<typeof styles> {
|
interface BankDialogProps extends WithStyles<typeof styles> {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@@ -182,7 +186,7 @@ const BankDialog = withStyles(styles, { withTheme: true })(
|
|||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
e.target.value = ""; // clear the file list so we can get another change notice.
|
e.target.value = ""; // clear the file list so we can get another change notice.
|
||||||
this.model.showAlert(err);
|
this.model.showAlert(err.toString());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
handleUploadBank() {
|
handleUploadBank() {
|
||||||
@@ -266,7 +270,7 @@ const BankDialog = withStyles(styles, { withTheme: true })(
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.model.showAlert(error);
|
this.model.showAlert(error.toString());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,11 +304,11 @@ const BankDialog = withStyles(styles, { withTheme: true })(
|
|||||||
>
|
>
|
||||||
<SelectHoverBackground selected={bankEntry.instanceId === selectedItem} showHover={true} />
|
<SelectHoverBackground selected={bankEntry.instanceId === selectedItem} showHover={true} />
|
||||||
<div className={classes.itemFrame}>
|
<div className={classes.itemFrame}>
|
||||||
<div className={classes.iconFrame}>
|
<div className={classes.ListItemIcon}>
|
||||||
<img src="img/ic_bank.svg" className={classes.itemIcon} alt="" />
|
<BankIcon className={classes.listIcon}/>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.itemLabel}>
|
<div className={classes.itemLabel}>
|
||||||
<Typography noWrap>
|
<Typography noWrap variant="body2" color="textPrimary">
|
||||||
{bankEntry.name}
|
{bankEntry.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
@@ -324,7 +328,7 @@ const BankDialog = withStyles(styles, { withTheme: true })(
|
|||||||
});
|
});
|
||||||
this.model.moveBank(from, to)
|
this.model.moveBank(from, to)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.model.showAlert(error);
|
this.model.showAlert(error.toString());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,7 +486,7 @@ const BankDialog = withStyles(styles, { withTheme: true })(
|
|||||||
>
|
>
|
||||||
<MenuItem onClick={() => { this.handleDownloadBank(); }} >
|
<MenuItem onClick={() => { this.handleDownloadBank(); }} >
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<DownloadIcon className="listIcon"
|
<DownloadIcon className={classes.listIcon}
|
||||||
/>
|
/>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>
|
<ListItemText>
|
||||||
@@ -495,8 +499,7 @@ const BankDialog = withStyles(styles, { withTheme: true })(
|
|||||||
|
|
||||||
|
|
||||||
<ListItemIcon >
|
<ListItemIcon >
|
||||||
<UploadIcon className="listIcon"
|
<UploadIcon className={classes.listIcon} />
|
||||||
/>
|
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>
|
<ListItemText>
|
||||||
Upload bank
|
Upload bank
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ export default withStyles(styles, { withTheme: true })(
|
|||||||
}
|
}
|
||||||
).catch(
|
).catch(
|
||||||
(e: any) => {
|
(e: any) => {
|
||||||
this.model.showAlert(e + "");
|
this.model.showAlert(e.toString());
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export default class JackHostStatus {
|
|||||||
<Typography variant="caption" color="inherit">{label}</Typography>
|
<Typography variant="caption" color="inherit">{label}</Typography>
|
||||||
|
|
||||||
<span style={{ color: RED_COLOR }}>
|
<span style={{ color: RED_COLOR }}>
|
||||||
<Typography variant="caption" color="inherit">{status.errorMessage === "" ? "Stopped" : status.errorMessage} </Typography>
|
<Typography variant="caption" color="inherit">{status.errorMessage === "" ? "Audio\u00A0Stopped" : status.errorMessage} </Typography>
|
||||||
</span>
|
</span>
|
||||||
{
|
{
|
||||||
status.temperaturemC > -100000 &&
|
status.temperaturemC > -100000 &&
|
||||||
|
|||||||
+269
-75
@@ -20,7 +20,7 @@
|
|||||||
import { UiPlugin, UiControl, PluginType, UiFileProperty } from './Lv2Plugin';
|
import { UiPlugin, UiControl, PluginType, UiFileProperty } from './Lv2Plugin';
|
||||||
|
|
||||||
import { PiPedalArgumentError, PiPedalStateError } from './PiPedalError';
|
import { PiPedalArgumentError, PiPedalStateError } from './PiPedalError';
|
||||||
|
import { UpdateStatus, UpdatePolicyT } from './Updater';
|
||||||
import { ObservableProperty } from './ObservableProperty';
|
import { ObservableProperty } from './ObservableProperty';
|
||||||
import { Pedalboard, PedalboardItem, ControlValue } from './Pedalboard'
|
import { Pedalboard, PedalboardItem, ControlValue } from './Pedalboard'
|
||||||
import PluginClass from './PluginClass';
|
import PluginClass from './PluginClass';
|
||||||
@@ -49,13 +49,20 @@ export enum State {
|
|||||||
Background,
|
Background,
|
||||||
Reconnecting,
|
Reconnecting,
|
||||||
ApplyingChanges,
|
ApplyingChanges,
|
||||||
ReloadingPlugins
|
ReloadingPlugins,
|
||||||
|
DownloadingUpdate,
|
||||||
|
InstallingUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function wantsLoadingScreen(state: State) {
|
||||||
|
return state >= State.Reconnecting;
|
||||||
|
}
|
||||||
|
|
||||||
export enum ReconnectReason {
|
export enum ReconnectReason {
|
||||||
Disconnected,
|
Disconnected,
|
||||||
LoadingSettings,
|
LoadingSettings,
|
||||||
ReloadingPlugins,
|
ReloadingPlugins,
|
||||||
|
Updating,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ControlValueChangedHandler = (key: string, value: number) => void;
|
export type ControlValueChangedHandler = (key: string, value: number) => void;
|
||||||
@@ -369,6 +376,9 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
state: ObservableProperty<State> = new ObservableProperty<State>(State.Loading);
|
state: ObservableProperty<State> = new ObservableProperty<State>(State.Loading);
|
||||||
visibilityState: ObservableProperty<VisibilityState> = new ObservableProperty<VisibilityState>(VisibilityState.Visible);
|
visibilityState: ObservableProperty<VisibilityState> = new ObservableProperty<VisibilityState>(VisibilityState.Visible);
|
||||||
|
|
||||||
|
promptForUpdate: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
|
||||||
|
updateStatus: ObservableProperty<UpdateStatus> = new ObservableProperty<UpdateStatus>(new UpdateStatus());
|
||||||
|
|
||||||
errorMessage: ObservableProperty<string> = new ObservableProperty<string>("");
|
errorMessage: ObservableProperty<string> = new ObservableProperty<string>("");
|
||||||
alertMessage: ObservableProperty<string> = new ObservableProperty<string>("");
|
alertMessage: ObservableProperty<string> = new ObservableProperty<string>("");
|
||||||
|
|
||||||
@@ -431,11 +441,15 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
this.onSocketConnectionLost = this.onSocketConnectionLost.bind(this);
|
this.onSocketConnectionLost = this.onSocketConnectionLost.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSocketReconnecting(retry: number, maxRetries: number): void {
|
expectDisconnect(reason: ReconnectReason) {
|
||||||
if (this.visibilityState.get() === VisibilityState.Hidden) return;
|
this.reconnectReason = reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSocketReconnecting(retry: number, maxRetries: number): boolean {
|
||||||
|
if (this.isClosed) return false;
|
||||||
|
if (this.visibilityState.get() === VisibilityState.Hidden) return false;
|
||||||
//if (retry !== 0) {
|
//if (retry !== 0) {
|
||||||
switch (this.reconnectReason)
|
switch (this.reconnectReason) {
|
||||||
{
|
|
||||||
case ReconnectReason.Disconnected:
|
case ReconnectReason.Disconnected:
|
||||||
default:
|
default:
|
||||||
this.setState(State.Reconnecting);
|
this.setState(State.Reconnecting);
|
||||||
@@ -446,8 +460,12 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
case ReconnectReason.ReloadingPlugins:
|
case ReconnectReason.ReloadingPlugins:
|
||||||
this.setState(State.ReloadingPlugins);
|
this.setState(State.ReloadingPlugins);
|
||||||
break;
|
break;
|
||||||
|
case ReconnectReason.Updating:
|
||||||
|
this.setState(State.InstallingUpdate);
|
||||||
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -613,18 +631,113 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
} else if (message === "onSystemMidiBindingsChanged") {
|
} else if (message === "onSystemMidiBindingsChanged") {
|
||||||
let bindings = MidiBinding.deserialize_array(body);
|
let bindings = MidiBinding.deserialize_array(body);
|
||||||
this.systemMidiBindings.set(bindings);
|
this.systemMidiBindings.set(bindings);
|
||||||
} else if (message === "onErrorMessage")
|
} else if (message === "onErrorMessage") {
|
||||||
{
|
|
||||||
this.showAlert(body as string);
|
this.showAlert(body as string);
|
||||||
|
|
||||||
}
|
}
|
||||||
else if (message === "onLv2PluginsChanging")
|
else if (message === "onLv2PluginsChanging") {
|
||||||
{
|
|
||||||
this.onLv2PluginsChanging();
|
this.onLv2PluginsChanging();
|
||||||
|
} else if (message === "onUpdateStatusChanged") {
|
||||||
|
let updateStatus = new UpdateStatus().deserialize(body);
|
||||||
|
this.onUpdateStatusChanged(updateStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateLaterTimeout?: NodeJS.Timeout = undefined;
|
||||||
|
|
||||||
|
private clearPromptForUpdateTimer() {
|
||||||
|
if (this.updateLaterTimeout) {
|
||||||
|
clearTimeout(this.updateLaterTimeout);
|
||||||
|
this.updateLaterTimeout = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setPromptForUpdateTimer(when: Date) {
|
||||||
|
let ms = when.getTime() - Date.now();
|
||||||
|
this.updateLaterTimeout = setTimeout(
|
||||||
|
() => {
|
||||||
|
this.updateLaterTimeout = undefined;
|
||||||
|
// make the server do a fresh check
|
||||||
|
this.getUpdateStatus()
|
||||||
|
.then(
|
||||||
|
() => {
|
||||||
|
this.updatePromptForUpdate();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
|
||||||
|
});
|
||||||
|
},
|
||||||
|
ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private lastCanUpdateNow: boolean = false;
|
||||||
|
private updatePromptForUpdate() {
|
||||||
|
this.clearPromptForUpdateTimer();
|
||||||
|
|
||||||
|
let stateEnabled = true; // must be present to accept alerts. this.state.get() === State.Ready;
|
||||||
|
let timeEnabled = false;
|
||||||
|
|
||||||
|
let updateLaterTime = this.getUpdateTime();
|
||||||
|
if (updateLaterTime == null) {
|
||||||
|
timeEnabled = true;
|
||||||
|
} else {
|
||||||
|
let nDate = Date.now();
|
||||||
|
|
||||||
|
let now: Date = new Date(nDate);
|
||||||
|
let maxDate: Date = new Date(nDate + 86400000*2); // sanity check for systems with unstable system clock
|
||||||
|
|
||||||
|
timeEnabled = (updateLaterTime < now || updateLaterTime >= maxDate)
|
||||||
|
}
|
||||||
|
let updateStatus = this.updateStatus.get();
|
||||||
|
let statusEnabled = updateStatus.isOnline && updateStatus.isValid && updateStatus.getActiveRelease().updateAvailable;
|
||||||
|
|
||||||
|
let canUpdateNow: boolean = (stateEnabled && timeEnabled && statusEnabled);
|
||||||
|
if (updateStatus.updatePolicy === UpdatePolicyT.Disable)
|
||||||
|
{
|
||||||
|
canUpdateNow = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canUpdateNow && canUpdateNow !== this.lastCanUpdateNow)
|
||||||
|
{
|
||||||
|
this.showUpdateDialogValue = true; // make the dialog sticky so it can show OK button
|
||||||
|
}
|
||||||
|
this.lastCanUpdateNow = canUpdateNow;
|
||||||
|
this.promptForUpdate.set(this.showUpdateDialogValue || canUpdateNow);
|
||||||
|
|
||||||
|
if (stateEnabled && statusEnabled && !timeEnabled && updateLaterTime) {
|
||||||
|
this.setPromptForUpdateTimer(updateLaterTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private showUpdateDialogValue: boolean = false;
|
||||||
|
|
||||||
|
showUpdateDialog(show: boolean = true) {
|
||||||
|
if (this.showUpdateDialogValue !== show) {
|
||||||
|
this.showUpdateDialogValue = show;
|
||||||
|
if (show) {
|
||||||
|
this.forceUpdateCheck();
|
||||||
|
}
|
||||||
|
this.updatePromptForUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdateStatusChanged(updateStatus: UpdateStatus): void {
|
||||||
|
let current = this.updateStatus.get();
|
||||||
|
if (!current.equals(updateStatus)) {
|
||||||
|
this.updateStatus.set(updateStatus);
|
||||||
|
if (current.currentVersion.length !== 0 && current.currentVersion !== updateStatus.currentVersion) {
|
||||||
|
// !! Server has been updated!!!
|
||||||
|
this.reloadPage();
|
||||||
|
throw new Error("Reloading...");
|
||||||
|
}
|
||||||
|
if (updateStatus.getActiveRelease().updateAvailable) {
|
||||||
|
this.promptForUpdate.set(true);
|
||||||
|
}
|
||||||
|
this.updatePromptForUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onLv2PluginsChanging(): void {
|
onLv2PluginsChanging(): void {
|
||||||
this.reconnectReason = ReconnectReason.ReloadingPlugins;
|
this.expectDisconnect(ReconnectReason.ReloadingPlugins);
|
||||||
// this.webSocket?.reconnect(); // let the server do it for us.
|
// this.webSocket?.reconnect(); // let the server do it for us.
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -642,16 +755,17 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
validatePlugins(plugin: PluginClass) {
|
validatePluginClasses(plugin: PluginClass) {
|
||||||
if (plugin.plugin_type === PluginType.None) {
|
if (plugin.plugin_type === PluginType.None) {
|
||||||
console.log("Error: No plugin type for uri '" + plugin.uri + "'");
|
console.log("Error: No plugin type for uri '" + plugin.uri + "'");
|
||||||
}
|
}
|
||||||
for (let i = 0; i < plugin.children.length; ++i) {
|
for (let i = 0; i < plugin.children.length; ++i) {
|
||||||
this.validatePlugins(plugin.children[i]);
|
this.validatePluginClasses(plugin.children[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private reconnectReason: ReconnectReason = ReconnectReason.Disconnected;
|
private reconnectReason: ReconnectReason = ReconnectReason.Disconnected;
|
||||||
|
|
||||||
isReloading(): boolean {
|
isReloading(): boolean {
|
||||||
@@ -667,7 +781,6 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
|
|
||||||
onSocketConnectionLost() {
|
onSocketConnectionLost() {
|
||||||
// remove all the events and subscriptions we have.
|
// remove all the events and subscriptions we have.
|
||||||
// yyy
|
|
||||||
this.vuSubscriptions = [];
|
this.vuSubscriptions = [];
|
||||||
this.monitorPatchPropertyListeners = [];
|
this.monitorPatchPropertyListeners = [];
|
||||||
|
|
||||||
@@ -681,13 +794,17 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
this.androidHost?.setDisconnected(false);
|
this.androidHost?.setDisconnected(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reconnectReason = ReconnectReason.Disconnected;
|
this.expectDisconnect(ReconnectReason.Disconnected); // the next expected disconnect will be an actual disconnect.
|
||||||
if (this.visibilityState.get() === VisibilityState.Hidden) return;
|
if (this.visibilityState.get() === VisibilityState.Hidden) return;
|
||||||
|
|
||||||
// reload state, but not configuration.
|
// reload state, but not configuration.
|
||||||
this.getWebSocket().request<number>("hello")
|
this.getWebSocket().request<number>("hello")
|
||||||
.then(clientId => {
|
.then(clientId => {
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
|
return this.getUpdateStatus(); // detects whether server has been upgraded.
|
||||||
|
})
|
||||||
|
.then((updateStatus) => {
|
||||||
|
|
||||||
return this.getWebSocket().request<any>("plugins");
|
return this.getWebSocket().request<any>("plugins");
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
@@ -762,7 +879,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
this.setState(State.Ready);
|
this.setState(State.Ready);
|
||||||
})
|
})
|
||||||
.catch((what) => {
|
.catch((what) => {
|
||||||
this.onError(what);
|
this.onError(what.toString());
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
makeSocketServerUrl(hostName: string, port: number): string {
|
makeSocketServerUrl(hostName: string, port: number): string {
|
||||||
@@ -825,6 +942,9 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
return this.getUpdateStatus();
|
||||||
|
})
|
||||||
|
.then((updateStatus) => {
|
||||||
const isoRequest = new Request('iso_codes.json');
|
const isoRequest = new Request('iso_codes.json');
|
||||||
return fetch(isoRequest);
|
return fetch(isoRequest);
|
||||||
})
|
})
|
||||||
@@ -846,7 +966,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.setError("Failed to connect to server. " + error);
|
this.setError("Failed to connect to server. " + error.toString());
|
||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
.then((succeeded) => {
|
.then((succeeded) => {
|
||||||
@@ -868,7 +988,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
|
||||||
this.plugin_classes.set(new PluginClass().deserialize(data));
|
this.plugin_classes.set(new PluginClass().deserialize(data));
|
||||||
this.validatePlugins(this.plugin_classes.get());
|
this.validatePluginClasses(this.plugin_classes.get());
|
||||||
|
|
||||||
return this.getWebSocket().request<PresetIndex>("getPresets");
|
return this.getWebSocket().request<PresetIndex>("getPresets");
|
||||||
})
|
})
|
||||||
@@ -941,7 +1061,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.setError("Failed to fetch server state.\n\n" + error);
|
this.setError("Failed to fetch server state.\n\n" + error.toString());
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1025,7 +1145,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.setError("Failed to get server state. \n\n" + error);
|
this.setError("Failed to get server state. \n\n" + error.toString());
|
||||||
});
|
});
|
||||||
let t = this.onVisibilityChanged;
|
let t = this.onVisibilityChanged;
|
||||||
|
|
||||||
@@ -1198,15 +1318,13 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
let changed: boolean = false;
|
let changed: boolean = false;
|
||||||
|
|
||||||
let pedalboard = this.pedalboard.get();
|
let pedalboard = this.pedalboard.get();
|
||||||
if (pedalboard.input_volume_db !== volume_db)
|
if (pedalboard.input_volume_db !== volume_db) {
|
||||||
{
|
|
||||||
let newPedalboard = pedalboard.clone();
|
let newPedalboard = pedalboard.clone();
|
||||||
newPedalboard.input_volume_db = volume_db;
|
newPedalboard.input_volume_db = volume_db;
|
||||||
this.pedalboard.set(newPedalboard);
|
this.pedalboard.set(newPedalboard);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
if (changed)
|
if (changed) {
|
||||||
{
|
|
||||||
if (notifyServer) {
|
if (notifyServer) {
|
||||||
nullCast(this.webSocket).send("setInputVolume", volume_db);
|
nullCast(this.webSocket).send("setInputVolume", volume_db);
|
||||||
}
|
}
|
||||||
@@ -1220,8 +1338,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setOutputVolume(volume_db: number, notifyServer: boolean) : void
|
setOutputVolume(volume_db: number, notifyServer: boolean): void {
|
||||||
{
|
|
||||||
this._setOutputVolume(volume_db, true);
|
this._setOutputVolume(volume_db, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1229,15 +1346,13 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
let changed: boolean = false;
|
let changed: boolean = false;
|
||||||
|
|
||||||
let pedalboard = this.pedalboard.get();
|
let pedalboard = this.pedalboard.get();
|
||||||
if (pedalboard.output_volume_db !== volume_db)
|
if (pedalboard.output_volume_db !== volume_db) {
|
||||||
{
|
|
||||||
let newPedalboard = pedalboard.clone();
|
let newPedalboard = pedalboard.clone();
|
||||||
newPedalboard.output_volume_db = volume_db;
|
newPedalboard.output_volume_db = volume_db;
|
||||||
this.pedalboard.set(newPedalboard);
|
this.pedalboard.set(newPedalboard);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
if (changed)
|
if (changed) {
|
||||||
{
|
|
||||||
if (notifyServer) {
|
if (notifyServer) {
|
||||||
nullCast(this.webSocket).send("setOutputVolume", volume_db);
|
nullCast(this.webSocket).send("setOutputVolume", volume_db);
|
||||||
}
|
}
|
||||||
@@ -1256,12 +1371,10 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready.");
|
if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready.");
|
||||||
let newPedalboard = pedalboard.clone();
|
let newPedalboard = pedalboard.clone();
|
||||||
|
|
||||||
if (instanceId === Pedalboard.START_CONTROL && key === "volume_db")
|
if (instanceId === Pedalboard.START_CONTROL && key === "volume_db") {
|
||||||
{
|
|
||||||
this._setInputVolume(value, notifyServer);
|
this._setInputVolume(value, notifyServer);
|
||||||
return;
|
return;
|
||||||
} else if (instanceId === Pedalboard.END_CONTROL)
|
} else if (instanceId === Pedalboard.END_CONTROL) {
|
||||||
{
|
|
||||||
this._setOutputVolume(value, notifyServer);
|
this._setOutputVolume(value, notifyServer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1395,12 +1508,10 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
// mouse is down. Don't update EVERYBODY, but we must change
|
// mouse is down. Don't update EVERYBODY, but we must change
|
||||||
// the control on the running audio plugin.
|
// the control on the running audio plugin.
|
||||||
// TODO: respect "expensive" port attribute.
|
// TODO: respect "expensive" port attribute.
|
||||||
if (instanceId === Pedalboard.START_CONTROL && key === "volume_db")
|
if (instanceId === Pedalboard.START_CONTROL && key === "volume_db") {
|
||||||
{
|
|
||||||
this.previewInputVolume(value);
|
this.previewInputVolume(value);
|
||||||
return;
|
return;
|
||||||
} else if (instanceId === Pedalboard.END_CONTROL)
|
} else if (instanceId === Pedalboard.END_CONTROL) {
|
||||||
{
|
|
||||||
this.previewOutputVolume(value);
|
this.previewOutputVolume(value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1530,12 +1641,10 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
addPedalboardItem(instanceId: number, append: boolean): number {
|
addPedalboardItem(instanceId: number, append: boolean): number {
|
||||||
let pedalboard = this.pedalboard.get();
|
let pedalboard = this.pedalboard.get();
|
||||||
if (instanceId === Pedalboard.START_CONTROL && append)
|
if (instanceId === Pedalboard.START_CONTROL && append) {
|
||||||
{
|
|
||||||
instanceId = pedalboard.items[0].instanceId;
|
instanceId = pedalboard.items[0].instanceId;
|
||||||
append = false;
|
append = false;
|
||||||
} else if (instanceId === Pedalboard.END_CONTROL && !append)
|
} else if (instanceId === Pedalboard.END_CONTROL && !append) {
|
||||||
{
|
|
||||||
instanceId = pedalboard.items[pedalboard.items.length - 1].instanceId;
|
instanceId = pedalboard.items[pedalboard.items.length - 1].instanceId;
|
||||||
append = true;
|
append = true;
|
||||||
}
|
}
|
||||||
@@ -1556,12 +1665,10 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
addPedalboardSplitItem(instanceId: number, append: boolean): number {
|
addPedalboardSplitItem(instanceId: number, append: boolean): number {
|
||||||
let pedalboard = this.pedalboard.get();
|
let pedalboard = this.pedalboard.get();
|
||||||
|
|
||||||
if (instanceId === Pedalboard.START_CONTROL && append)
|
if (instanceId === Pedalboard.START_CONTROL && append) {
|
||||||
{
|
|
||||||
instanceId = pedalboard.items[0].instanceId;
|
instanceId = pedalboard.items[0].instanceId;
|
||||||
append = false;
|
append = false;
|
||||||
} else if (instanceId === Pedalboard.END_CONTROL && !append)
|
} else if (instanceId === Pedalboard.END_CONTROL && !append) {
|
||||||
{
|
|
||||||
instanceId = pedalboard.items[pedalboard.items.length - 1].instanceId;
|
instanceId = pedalboard.items[pedalboard.items.length - 1].instanceId;
|
||||||
append = true;
|
append = true;
|
||||||
}
|
}
|
||||||
@@ -1615,7 +1722,6 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
saveCurrentPresetAs(newName: string, saveAfterInstanceId = -1): Promise<number> {
|
saveCurrentPresetAs(newName: string, saveAfterInstanceId = -1): Promise<number> {
|
||||||
// default behaviour is to save after the currently selected preset.
|
// default behaviour is to save after the currently selected preset.
|
||||||
if (saveAfterInstanceId === -1) {
|
if (saveAfterInstanceId === -1) {
|
||||||
@@ -1636,6 +1742,83 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUpdateStatus(): Promise<UpdateStatus> {
|
||||||
|
return new Promise<UpdateStatus>(
|
||||||
|
(accept, reject) => {
|
||||||
|
nullCast(this.webSocket)
|
||||||
|
.request<UpdateStatus>('getUpdateStatus')
|
||||||
|
.then((result) => {
|
||||||
|
let updateStatus = new UpdateStatus().deserialize(result);
|
||||||
|
this.onUpdateStatusChanged(updateStatus);
|
||||||
|
accept(result);
|
||||||
|
}).catch(
|
||||||
|
(e) => {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
launchExternalUrl(url: string) {
|
||||||
|
if (this.isAndroidHosted()) {
|
||||||
|
try {
|
||||||
|
this.androidHost?.launchExternalUrl(url);
|
||||||
|
} catch (e) {
|
||||||
|
// if they haven't updated their client yet, just don't do it.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} else {
|
||||||
|
window.open(url, "_blank")?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLater(delayMs: number) {
|
||||||
|
let futureDate = new Date(Date.now() + delayMs);
|
||||||
|
localStorage.setItem('nextUpdateTime', futureDate.toISOString());
|
||||||
|
|
||||||
|
this.updatePromptForUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNow(): Promise<void> {
|
||||||
|
return new Promise<void>(
|
||||||
|
(accept, reject) => {
|
||||||
|
let updateStatus = this.updateStatus.get();
|
||||||
|
if (updateStatus.isOnline && updateStatus.isValid && updateStatus.getActiveRelease().updateAvailable) {
|
||||||
|
this.setState(State.DownloadingUpdate);
|
||||||
|
this.expectDisconnect(ReconnectReason.Updating);
|
||||||
|
let url = updateStatus.getActiveRelease().updateUrl;
|
||||||
|
|
||||||
|
nullCast(this.webSocket)
|
||||||
|
.request<void>('updateNow', url)
|
||||||
|
.then(() => {
|
||||||
|
this.setState(State.InstallingUpdate);
|
||||||
|
accept();
|
||||||
|
})
|
||||||
|
.catch(
|
||||||
|
(e: any) => {
|
||||||
|
this.expectDisconnect(ReconnectReason.Disconnected);
|
||||||
|
|
||||||
|
this.setState(State.Ready); // TODO: hopefully we haven't had an intermediate disconnect.
|
||||||
|
reject(e);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reject(new Error("Invalid update request."));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUpdateTime(): Date | null {
|
||||||
|
let item = localStorage.getItem('nextUpdateTime');
|
||||||
|
if (item) {
|
||||||
|
return new Date(item);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
// deprecated.
|
// deprecated.
|
||||||
requestFileList(piPedalFileProperty: UiFileProperty): Promise<string[]> {
|
requestFileList(piPedalFileProperty: UiFileProperty): Promise<string[]> {
|
||||||
return nullCast(this.webSocket)
|
return nullCast(this.webSocket)
|
||||||
@@ -1648,8 +1831,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteUserFile(fileName: string) : Promise<boolean>
|
deleteUserFile(fileName: string): Promise<boolean> {
|
||||||
{
|
|
||||||
return nullCast(this.webSocket).request<boolean>('deleteUserFile', fileName);
|
return nullCast(this.webSocket).request<boolean>('deleteUserFile', fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1712,13 +1894,14 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
return this.copyPreset(instanceId, -1);
|
return this.copyPreset(instanceId, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
showAlert(message: string): void {
|
showAlert(message: string| Error): void {
|
||||||
this.alertMessage.set(message);
|
let m = message.toString();
|
||||||
|
this.alertMessage.set(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
setJackSettings(jackSettings: JackChannelSelection): void {
|
setJackSettings(jackSettings: JackChannelSelection): void {
|
||||||
this.reconnectReason = ReconnectReason.LoadingSettings;
|
this.expectDisconnect(ReconnectReason.LoadingSettings);
|
||||||
this.webSocket?.send("setJackSettings", jackSettings);
|
this.webSocket?.send("setJackSettings", jackSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1823,7 +2006,14 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
private isClosed = false;
|
||||||
|
close() {
|
||||||
|
if (!this.isClosed) {
|
||||||
|
this.isClosed = true;
|
||||||
|
this.webSocket?.close();
|
||||||
|
this.webSocket = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean> {
|
setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean> {
|
||||||
let result = new Promise<boolean>((resolve, reject) => {
|
let result = new Promise<boolean>((resolve, reject) => {
|
||||||
if (this.webSocket) {
|
if (this.webSocket) {
|
||||||
@@ -2117,14 +2307,12 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
let parsedUrl = new URL(url);
|
let parsedUrl = new URL(url);
|
||||||
let fileNameOnly = file.name.split('/').pop()?.split('\\')?.pop();
|
let fileNameOnly = file.name.split('/').pop()?.split('\\')?.pop();
|
||||||
let query = parsedUrl.search;
|
let query = parsedUrl.search;
|
||||||
if (query.length === 0)
|
if (query.length === 0) {
|
||||||
{
|
|
||||||
query += '?';
|
query += '?';
|
||||||
} else {
|
} else {
|
||||||
query += '&';
|
query += '&';
|
||||||
}
|
}
|
||||||
if (!fileNameOnly)
|
if (!fileNameOnly) {
|
||||||
{
|
|
||||||
reject("Invalid filename.");
|
reject("Invalid filename.");
|
||||||
}
|
}
|
||||||
query += "filename=" + encodeURIComponent(fileNameOnly ?? "");
|
query += "filename=" + encodeURIComponent(fileNameOnly ?? "");
|
||||||
@@ -2195,7 +2383,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
resolve(json as number);
|
resolve(json as number);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
reject("Upload failed. " + error);
|
reject("Upload failed. " + error.toString());
|
||||||
})
|
})
|
||||||
;
|
;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -2254,8 +2442,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
|
||||||
let ws = this.webSocket;
|
let ws = this.webSocket;
|
||||||
if (!ws)
|
if (!ws) {
|
||||||
{
|
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2271,8 +2458,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
createNewSampleDirectory(relativePath: string, uiFileProperty: UiFileProperty) : Promise<string>
|
createNewSampleDirectory(relativePath: string, uiFileProperty: UiFileProperty): Promise<string> {
|
||||||
{
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
|
||||||
let ws = this.webSocket;
|
let ws = this.webSocket;
|
||||||
@@ -2313,8 +2499,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
renameFilePropertyFile(oldRelativePath: string, newRelativePath: string, uiFileProperty: UiFileProperty) : Promise<string>
|
renameFilePropertyFile(oldRelativePath: string, newRelativePath: string, uiFileProperty: UiFileProperty): Promise<string> {
|
||||||
{
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
|
||||||
let ws = this.webSocket;
|
let ws = this.webSocket;
|
||||||
@@ -2444,8 +2629,8 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
resolve();
|
resolve();
|
||||||
|
|
||||||
});
|
});
|
||||||
this.reconnectReason = ReconnectReason.LoadingSettings;
|
this.expectDisconnect(ReconnectReason.LoadingSettings);
|
||||||
this.webSocket?.reconnect(); // close immediately, and wait for recoonnect.
|
// this.webSocket?.reconnect(); // avoid races by letting the server do it for us.
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2524,8 +2709,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
|
|
||||||
++ix;
|
++ix;
|
||||||
if (ix >= uiPlugin.controls.length) return;
|
if (ix >= uiPlugin.controls.length) return;
|
||||||
while (!uiPlugin.controls[ix].is_input)
|
while (!uiPlugin.controls[ix].is_input) {
|
||||||
{
|
|
||||||
++ix;
|
++ix;
|
||||||
if (ix >= uiPlugin.controls.length) return;
|
if (ix >= uiPlugin.controls.length) return;
|
||||||
}
|
}
|
||||||
@@ -2558,8 +2742,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
|
|
||||||
--ix;
|
--ix;
|
||||||
if (ix < 0) return;
|
if (ix < 0) return;
|
||||||
while (!uiPlugin.controls[ix].is_input)
|
while (!uiPlugin.controls[ix].is_input) {
|
||||||
{
|
|
||||||
--ix;
|
--ix;
|
||||||
if (ix < 0) return;
|
if (ix < 0) return;
|
||||||
}
|
}
|
||||||
@@ -2590,6 +2773,20 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setUpdatePolicy(updatePolicy: UpdatePolicyT): void {
|
||||||
|
let iPolicy = updatePolicy as number;
|
||||||
|
if (this.webSocket) {
|
||||||
|
this.webSocket.send("setUpdatePolicy", iPolicy);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
forceUpdateCheck() {
|
||||||
|
if (this.webSocket) {
|
||||||
|
this.webSocket.send("forceUpdateCheck");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
preloadImages(imageList: string): void {
|
preloadImages(imageList: string): void {
|
||||||
let imageNames = imageList.split(';');
|
let imageNames = imageList.split(';');
|
||||||
for (let i = 0; i < imageNames.length; ++i) {
|
for (let i = 0; i < imageNames.length; ++i) {
|
||||||
@@ -2619,8 +2816,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
// returns the ID of the new preset.
|
// returns the ID of the new preset.
|
||||||
newPresetItem(createAfter: number): Promise<number>
|
newPresetItem(createAfter: number): Promise<number> {
|
||||||
{
|
|
||||||
return nullCast(this.webSocket).request<number>("newPreset");
|
return nullCast(this.webSocket).request<number>("newPreset");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2630,8 +2826,7 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
|
|
||||||
setTheme(value: ColorTheme) {
|
setTheme(value: ColorTheme) {
|
||||||
|
|
||||||
if (this.getTheme() !== value)
|
if (this.getTheme() !== value) {
|
||||||
{
|
|
||||||
setColorScheme(value);
|
setColorScheme(value);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.reloadPage();
|
this.reloadPage();
|
||||||
@@ -2647,7 +2842,6 @@ export class PiPedalModel //implements PiPedalModel
|
|||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
//window.location.reload();
|
//window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let instance: PiPedalModel | undefined = undefined;
|
let instance: PiPedalModel | undefined = undefined;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export interface PiPedalSocketListener {
|
|||||||
onError: (message: string, exception?: Error) => void;
|
onError: (message: string, exception?: Error) => void;
|
||||||
onConnectionLost: () => void;
|
onConnectionLost: () => void;
|
||||||
onReconnect: () => void;
|
onReconnect: () => void;
|
||||||
onReconnecting: (retry: number, maxRetries: number) => void;
|
onReconnecting: (retry: number, maxRetries: number) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
class PiPedalSocket {
|
class PiPedalSocket {
|
||||||
@@ -183,6 +183,7 @@ class PiPedalSocket {
|
|||||||
|
|
||||||
handleClose(_event: any): any {
|
handleClose(_event: any): any {
|
||||||
if (this.retrying) {
|
if (this.retrying) {
|
||||||
|
this.close();
|
||||||
// treat this as a fatal error.
|
// treat this as a fatal error.
|
||||||
if (this.listener) {
|
if (this.listener) {
|
||||||
this.listener.onError("Server connection lost.");
|
this.listener.onError("Server connection lost.");
|
||||||
@@ -230,7 +231,9 @@ class PiPedalSocket {
|
|||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.listener.onReconnecting(this.retryCount,MAX_RETRIES);
|
if (!this.listener.onReconnecting(this.retryCount,MAX_RETRIES)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
++this.retryCount;
|
++this.retryCount;
|
||||||
|
|
||||||
this.connectInternal_()
|
this.connectInternal_()
|
||||||
@@ -262,6 +265,7 @@ class PiPedalSocket {
|
|||||||
this.socket.onmessage = null;
|
this.socket.onmessage = null;
|
||||||
this.socket.onopen = null;
|
this.socket.onopen = null;
|
||||||
this.socket.close();
|
this.socket.close();
|
||||||
|
this.socket = undefined;
|
||||||
}
|
}
|
||||||
} catch (ignored)
|
} catch (ignored)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -257,6 +257,9 @@ const PluginControl =
|
|||||||
touchDown: boolean = false;
|
touchDown: boolean = false;
|
||||||
touchIdentifier: number = -1;
|
touchIdentifier: number = -1;
|
||||||
|
|
||||||
|
private lastclickTimeMs: number = 0;
|
||||||
|
private isTap: boolean = false;
|
||||||
|
|
||||||
onTouchStart(e: TouchEvent<SVGSVGElement>) {
|
onTouchStart(e: TouchEvent<SVGSVGElement>) {
|
||||||
}
|
}
|
||||||
onTouchMove(e: TouchEvent<SVGSVGElement>) {
|
onTouchMove(e: TouchEvent<SVGSVGElement>) {
|
||||||
@@ -272,6 +275,7 @@ const PluginControl =
|
|||||||
let e = e_ as PointerEvent;
|
let e = e_ as PointerEvent;
|
||||||
if (this.isExtraTouch(e))
|
if (this.isExtraTouch(e))
|
||||||
{
|
{
|
||||||
|
this.isTap = false;
|
||||||
this.captureElement!.setPointerCapture(e.pointerId);
|
this.captureElement!.setPointerCapture(e.pointerId);
|
||||||
this.capturedPointers.push(e.pointerId);
|
this.capturedPointers.push(e.pointerId);
|
||||||
++this.pointersDown;
|
++this.pointersDown;
|
||||||
@@ -305,6 +309,7 @@ const PluginControl =
|
|||||||
{
|
{
|
||||||
if (this.props.uiControl?.isDial()??false)
|
if (this.props.uiControl?.isDial()??false)
|
||||||
{
|
{
|
||||||
|
this.isTap = false;
|
||||||
this.showZoomedControl();
|
this.showZoomedControl();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -312,8 +317,14 @@ const PluginControl =
|
|||||||
|
|
||||||
++this.pointersDown;
|
++this.pointersDown;
|
||||||
|
|
||||||
|
|
||||||
this.mouseDown = true;
|
this.mouseDown = true;
|
||||||
|
if (this.pointersDown === 1)
|
||||||
|
{
|
||||||
|
this.isTap = true;
|
||||||
|
this.tapStartMs = Date.now();
|
||||||
|
} else {
|
||||||
|
this.isTap = false;
|
||||||
|
}
|
||||||
|
|
||||||
this.pointerId = e.pointerId;
|
this.pointerId = e.pointerId;
|
||||||
this.pointerType = e.pointerType;
|
this.pointerType = e.pointerType;
|
||||||
@@ -342,6 +353,7 @@ const PluginControl =
|
|||||||
if (this.isExtraTouch(e))
|
if (this.isExtraTouch(e))
|
||||||
{
|
{
|
||||||
++this.pointersDown;
|
++this.pointersDown;
|
||||||
|
this.isTap = false;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,6 +366,7 @@ const PluginControl =
|
|||||||
onPointerLostCapture(e: PointerEvent<SVGSVGElement>) {
|
onPointerLostCapture(e: PointerEvent<SVGSVGElement>) {
|
||||||
if (this.isCapturedPointer(e)) {
|
if (this.isCapturedPointer(e)) {
|
||||||
--this.pointersDown;
|
--this.pointersDown;
|
||||||
|
this.isTap = false;
|
||||||
|
|
||||||
|
|
||||||
this.releaseCapture(e);
|
this.releaseCapture(e);
|
||||||
@@ -396,6 +409,35 @@ const PluginControl =
|
|||||||
this.lastX = e.clientX;
|
this.lastX = e.clientX;
|
||||||
return this.dRange;
|
return this.dRange;
|
||||||
}
|
}
|
||||||
|
private lastTapMs = 0;
|
||||||
|
|
||||||
|
resetToDefaultValue(uiControl: UiControl): void
|
||||||
|
{
|
||||||
|
let value = uiControl.default_value;
|
||||||
|
this.model.setPedalboardControl(this.props.instanceId,uiControl.symbol,value);
|
||||||
|
}
|
||||||
|
onPointerDoubleTap() {
|
||||||
|
let uiControl = this.props.uiControl;
|
||||||
|
if (uiControl)
|
||||||
|
{
|
||||||
|
if (uiControl.isDial())
|
||||||
|
{
|
||||||
|
this.resetToDefaultValue(uiControl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onPointerTap() {
|
||||||
|
let tapTime = Date.now();
|
||||||
|
let dT = tapTime-this.lastTapMs;
|
||||||
|
this.lastTapMs = tapTime;
|
||||||
|
|
||||||
|
if (dT < 500)
|
||||||
|
{
|
||||||
|
this.onPointerDoubleTap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private tapStartMs: number = 0;
|
||||||
|
|
||||||
onPointerUp(e: PointerEvent<SVGSVGElement>) {
|
onPointerUp(e: PointerEvent<SVGSVGElement>) {
|
||||||
|
|
||||||
if (this.isCapturedPointer(e)) {
|
if (this.isCapturedPointer(e)) {
|
||||||
@@ -407,6 +449,14 @@ const PluginControl =
|
|||||||
this.previewRange(dRange, true);
|
this.previewRange(dRange, true);
|
||||||
|
|
||||||
this.releaseCapture(e);
|
this.releaseCapture(e);
|
||||||
|
if (this.isTap)
|
||||||
|
{
|
||||||
|
let ms = Date.now()-this.tapStartMs;
|
||||||
|
if (ms < 200)
|
||||||
|
{
|
||||||
|
this.onPointerTap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
--this.pointersDown;
|
--this.pointersDown;
|
||||||
@@ -437,11 +487,24 @@ const PluginControl =
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clickSlop() {
|
||||||
|
return 3.5; // maybe larger on touch devices.
|
||||||
|
}
|
||||||
onPointerMove(e: PointerEvent<SVGSVGElement>): void {
|
onPointerMove(e: PointerEvent<SVGSVGElement>): void {
|
||||||
if (this.isCapturedPointer(e)) {
|
if (this.isCapturedPointer(e)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let dRange = this.updateRange(e)
|
let dRange = this.updateRange(e)
|
||||||
this.previewRange(dRange, false);
|
this.previewRange(dRange, false);
|
||||||
|
|
||||||
|
let x = e.clientX;
|
||||||
|
let y = e.clientY;
|
||||||
|
let dx = x - this.startX;
|
||||||
|
let dy = y-this.startY;
|
||||||
|
let distance = Math.sqrt(dx*dx+dy*dy);
|
||||||
|
if (distance >= this.clickSlop())
|
||||||
|
{
|
||||||
|
this.isTap = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -393,6 +393,12 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
|
|||||||
showThemeSelectDialog: true
|
showThemeSelectDialog: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleCheckForUpdates()
|
||||||
|
{
|
||||||
|
this.model.showUpdateDialog();
|
||||||
|
}
|
||||||
|
|
||||||
handleMidiMessageSettings() {
|
handleMidiMessageSettings() {
|
||||||
this.setState({
|
this.setState({
|
||||||
showSystemMidiBindingsDialog: true
|
showSystemMidiBindingsDialog: true
|
||||||
@@ -665,7 +671,7 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
|
|||||||
<ButtonBase className={classes.setting} disabled={!isConfigValid} onClick={() => this.handleMidiMessageSettings()} >
|
<ButtonBase className={classes.setting} disabled={!isConfigValid} onClick={() => this.handleMidiMessageSettings()} >
|
||||||
<SelectHoverBackground selected={false} showHover={true} />
|
<SelectHoverBackground selected={false} showHover={true} />
|
||||||
<div style={{ width: "100%" }}>
|
<div style={{ width: "100%" }}>
|
||||||
<Typography className={classes.primaryItem} display="block" variant="body2" noWrap>System MIDI Bindings</Typography>
|
<Typography className={classes.primaryItem} display="block" variant="body2" noWrap>System MIDI bindings</Typography>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
@@ -748,7 +754,7 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
|
|||||||
<SelectHoverBackground selected={false} showHover={true} />
|
<SelectHoverBackground selected={false} showHover={true} />
|
||||||
<div style={{ width: "100%" }}>
|
<div style={{ width: "100%" }}>
|
||||||
<Typography className={classes.primaryItem} display="block" variant="body2" color="textPrimary" noWrap>
|
<Typography className={classes.primaryItem} display="block" variant="body2" color="textPrimary" noWrap>
|
||||||
Color Theme</Typography>
|
Color theme</Typography>
|
||||||
<Typography className={classes.secondaryItem} display="block" variant="caption" color="textSecondary" noWrap>
|
<Typography className={classes.secondaryItem} display="block" variant="caption" color="textSecondary" noWrap>
|
||||||
{ this.model.getTheme() === ColorTheme.Dark ? "Dark" :
|
{ this.model.getTheme() === ColorTheme.Dark ? "Dark" :
|
||||||
(this.model.getTheme() === ColorTheme.Light ? "Light": "System")}
|
(this.model.getTheme() === ColorTheme.Light ? "Light": "System")}
|
||||||
@@ -786,6 +792,18 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
|
|||||||
</div>
|
</div>
|
||||||
</ButtonBase>
|
</ButtonBase>
|
||||||
|
|
||||||
|
<ButtonBase
|
||||||
|
className={classes.setting}
|
||||||
|
onClick={() => { this.handleCheckForUpdates(); }} >
|
||||||
|
<SelectHoverBackground selected={false} showHover={true} />
|
||||||
|
<div style={{ width: "100%" }}>
|
||||||
|
<Typography className={classes.primaryItem} display="block" variant="body2" color="textPrimary" noWrap>
|
||||||
|
Check for updates...</Typography>
|
||||||
|
</div>
|
||||||
|
</ButtonBase>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<ButtonBase className={classes.setting} disabled={disableShutdown}
|
<ButtonBase className={classes.setting} disabled={disableShutdown}
|
||||||
onClick={() => this.handleRestart()} >
|
onClick={() => this.handleRestart()} >
|
||||||
<SelectHoverBackground selected={false} showHover={true} />
|
<SelectHoverBackground selected={false} showHover={true} />
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import DialogEx from './DialogEx';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import { UpdateStatus, UpdateRelease, UpdatePolicyT, intToUpdatePolicyT } from './Updater';
|
||||||
|
import { PiPedalModelFactory, PiPedalModel } from './PiPedalModel';
|
||||||
|
import Select from '@mui/material/Select';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
|
||||||
|
|
||||||
|
const UPDATE_CHECK_DELAY = 86400000; // one day in ms.
|
||||||
|
// const UPDATE_CHECK_DELAY = 30 * 1000; // testing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export interface UpdateDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface UpdateDialogState {
|
||||||
|
updateStatus: UpdateStatus;
|
||||||
|
compactLandscape: boolean;
|
||||||
|
alertDialogOpen: boolean;
|
||||||
|
alertDialogMessage: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class UpdateDialog extends React.Component<UpdateDialogProps, UpdateDialogState> {
|
||||||
|
private model: PiPedalModel;
|
||||||
|
|
||||||
|
constructor(props: UpdateDialogProps) {
|
||||||
|
super(props);
|
||||||
|
this.model = PiPedalModelFactory.getInstance();
|
||||||
|
let updateStatus = this.model.updateStatus.get();
|
||||||
|
this.state = {
|
||||||
|
updateStatus: updateStatus,
|
||||||
|
compactLandscape: false,
|
||||||
|
alertDialogOpen: false,
|
||||||
|
alertDialogMessage: ""
|
||||||
|
};
|
||||||
|
this.onUpdateStatusChanged = this.onUpdateStatusChanged.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdateStatusChanged(newValue: UpdateStatus) {
|
||||||
|
if (!newValue.equals(this.state.updateStatus)) {
|
||||||
|
this.setState({ updateStatus: newValue });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mounted: boolean = false;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.mounted = true;
|
||||||
|
this.model.updateStatus.addOnChangedHandler(this.onUpdateStatusChanged);
|
||||||
|
}
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.model.updateStatus.removeOnChangedHandler(this.onUpdateStatusChanged);
|
||||||
|
this.mounted = false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private handleOK() {
|
||||||
|
this.model.showUpdateDialog(false);
|
||||||
|
}
|
||||||
|
private handleUpdateNow() {
|
||||||
|
this.model.updateNow()
|
||||||
|
.then(() => { }) // all handling is done by model, so we can run ui-less.
|
||||||
|
.catch((e: any) => {
|
||||||
|
this.showAlert(e.toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private handleUpdateLater() {
|
||||||
|
this.model.updateLater(UPDATE_CHECK_DELAY);
|
||||||
|
this.model.showUpdateDialog(false); // allow close if launched from the settings window.
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleClose() {
|
||||||
|
// ideally, we'd like to not close at all, but it screws up the nav backstack if we dont.
|
||||||
|
// so, close, but do so very briefly
|
||||||
|
if (this.canUpgrade()) {
|
||||||
|
this.model.updateLater(UPDATE_CHECK_DELAY); // close and re-open immediately.
|
||||||
|
}
|
||||||
|
this.model.showUpdateDialog(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseAlert() {
|
||||||
|
this.setState({ alertDialogOpen: false, alertDialogMessage: "" })
|
||||||
|
}
|
||||||
|
|
||||||
|
showAlert(message: string) {
|
||||||
|
this.setState({ alertDialogOpen: true, alertDialogMessage: message });
|
||||||
|
}
|
||||||
|
|
||||||
|
onViewReleaseNotes() {
|
||||||
|
this.model.launchExternalUrl("https://rerdavies.github.io/pipedal/ReleaseNotes.html");
|
||||||
|
}
|
||||||
|
|
||||||
|
upToDate(): boolean {
|
||||||
|
let updateStatus = this.state.updateStatus;
|
||||||
|
let updateRelease: UpdateRelease = updateStatus.getActiveRelease();
|
||||||
|
return !updateRelease.updateAvailable
|
||||||
|
}
|
||||||
|
canUpgrade(): boolean {
|
||||||
|
let updateStatus = this.state.updateStatus;
|
||||||
|
if (updateStatus.updatePolicy === UpdatePolicyT.Disable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (updateStatus.errorMessage !== "") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let updateRelease = updateStatus.getActiveRelease();
|
||||||
|
|
||||||
|
return updateStatus.isValid && updateStatus.isOnline && updateRelease.updateAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onPolicySelected(newValue: string | number): void {
|
||||||
|
if (newValue.toString() === "") return;
|
||||||
|
let updatePolicy = intToUpdatePolicyT(newValue as number);
|
||||||
|
this.model.setUpdatePolicy(updatePolicy);
|
||||||
|
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
let updateStatus = this.state.updateStatus;
|
||||||
|
let updateRelease = updateStatus.getActiveRelease();
|
||||||
|
let upToDate = this.upToDate();
|
||||||
|
let canUpgrade = this.canUpgrade();
|
||||||
|
let compact = this.state.compactLandscape;
|
||||||
|
let showUpgradeVersion = !upToDate && updateStatus.isValid && updateRelease.upgradeVersionDisplayName !== "";
|
||||||
|
return (
|
||||||
|
<DialogEx tag="update" open={this.props.open} onClose={() => { this.handleClose(); }}
|
||||||
|
style={{ userSelect: "none" }}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
(!compact) &&
|
||||||
|
(
|
||||||
|
<DialogTitle>
|
||||||
|
<div style={{ display: "flex", flexFlow: "row noWrap", alignItems: "center" }} >
|
||||||
|
<Typography style={{ flexGrow: 1, flexShrink: 1, marginRight: 20 }} noWrap>PiPedal Updates</Typography>
|
||||||
|
<Select style={{ opacity: 0.6 }} variant="standard" value={updateStatus.updatePolicy} onChange={(ev) => { this.onPolicySelected(ev.target.value); }}>
|
||||||
|
<MenuItem value={UpdatePolicyT.ReleaseOnly}>Release only</MenuItem>
|
||||||
|
<MenuItem value={UpdatePolicyT.ReleaseOrBeta}>Release or Beta</MenuItem>
|
||||||
|
<MenuItem value={UpdatePolicyT.Development}>Development</MenuItem>
|
||||||
|
<MenuItem value={UpdatePolicyT.Disable}>Disable</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</DialogTitle>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
(!compact) &&
|
||||||
|
(
|
||||||
|
<Divider />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
{(upToDate) && (
|
||||||
|
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 12 }}>
|
||||||
|
PiPedal is up to date.
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{(canUpgrade) && (
|
||||||
|
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 12 }}>
|
||||||
|
A new version of the PiPedal server is available. Would you like to update now?
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<div style={{ display: "flex", flexFlow: "row noWrap" }}>
|
||||||
|
<div>
|
||||||
|
<Typography noWrap variant="body2" color="textSecondary" >
|
||||||
|
Current version:
|
||||||
|
</Typography>
|
||||||
|
{(showUpgradeVersion) && (
|
||||||
|
<Typography noWrap variant="body2" color="textSecondary" style={{ marginTop: 4 }} >
|
||||||
|
Update version:
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div style={{ flexShrink: 1, flexGrow: 1, marginLeft: 12 }}>
|
||||||
|
<Typography noWrap variant="body2" color="textSecondary" >
|
||||||
|
{updateStatus.currentVersionDisplayName}
|
||||||
|
</Typography>
|
||||||
|
{(showUpgradeVersion) && (
|
||||||
|
<Typography noWrap variant="body2" color="textSecondary" style={{ marginTop: 4 }} >
|
||||||
|
{updateRelease.upgradeVersionDisplayName}
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
(updateStatus.errorMessage.length !== 0) &&
|
||||||
|
(
|
||||||
|
<Typography variant="body2" style={{ marginTop: 12 }}>
|
||||||
|
{
|
||||||
|
updateStatus.errorMessage
|
||||||
|
}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<Button style={{ marginLeft: 16, marginTop: 12 }} onClick={() => { this.onViewReleaseNotes(); }}>View release notes</Button>
|
||||||
|
</DialogContent>
|
||||||
|
<Divider />
|
||||||
|
<DialogActions>
|
||||||
|
{
|
||||||
|
(canUpgrade) ?
|
||||||
|
(
|
||||||
|
<div style={{ display: "flex", flexFlow: "row noWrap", alignItems: "center" }}>
|
||||||
|
<Button variant="dialogSecondary" onClick={() => { this.handleUpdateLater(); }}>Update later</Button>
|
||||||
|
<div style={{ flexGrow: 1, flexShrink: 1 }} />
|
||||||
|
<Button variant="dialogPrimary" onClick={() => { this.handleUpdateNow(); }}>Update now</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
) : (
|
||||||
|
|
||||||
|
<div style={{ display: "flex", flexFlow: "row noWrap", alignItems: "center" }}
|
||||||
|
|
||||||
|
>
|
||||||
|
<div style={{ flexGrow: 1, flexShrink: 1 }} />
|
||||||
|
<Button onClick={() => { this.handleOK(); }} variant="dialogPrimary">OK</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</DialogActions>
|
||||||
|
|
||||||
|
<DialogEx
|
||||||
|
tag="UpdateAlert"
|
||||||
|
open={this.state.alertDialogOpen}
|
||||||
|
onClose={() => { this.handleCloseAlert(); }}
|
||||||
|
aria-describedby="Alert Dialog"
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
<Typography variant="body2" color="secondaryText">
|
||||||
|
{
|
||||||
|
this.state.alertDialogMessage
|
||||||
|
}
|
||||||
|
</Typography>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="dialogPrimary" onClick={() => { this.handleCloseAlert(); }} autoFocus>
|
||||||
|
OK
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</DialogEx>
|
||||||
|
|
||||||
|
</DialogEx >
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
export enum UpdatePolicyT {
|
||||||
|
// must match declaration in Updater.hpp
|
||||||
|
ReleaseOnly = 0,
|
||||||
|
ReleaseOrBeta = 1,
|
||||||
|
Development = 2,
|
||||||
|
Disable = 3,
|
||||||
|
|
||||||
|
};
|
||||||
|
export function intToUpdatePolicyT(intValue: number): UpdatePolicyT {
|
||||||
|
return (Object.values(UpdatePolicyT).find(enumValue => enumValue === intValue)) as UpdatePolicyT;
|
||||||
|
}
|
||||||
|
export class UpdateRelease {
|
||||||
|
deserialize(input: any): UpdateRelease {
|
||||||
|
this.updateAvailable = input.updateAvailable;
|
||||||
|
this.upgradeVersion = input.upgradeVersion;
|
||||||
|
this.upgradeVersionDisplayName = input.upgradeVersionDisplayName;
|
||||||
|
this.assetName = input.assetName;
|
||||||
|
this.updateUrl = input.updateUrl;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
updateAvailable: boolean = false;
|
||||||
|
upgradeVersion: string = ""; //just the version.
|
||||||
|
upgradeVersionDisplayName: string = ""; // display name for the version.
|
||||||
|
assetName: string = ""; // filename only
|
||||||
|
updateUrl: string = ""; // url from which to download the .deb file.
|
||||||
|
gpgSignatureUrl: string = "";
|
||||||
|
|
||||||
|
equals(other: UpdateRelease): boolean {
|
||||||
|
return (this.updateAvailable === other.updateAvailable) &&
|
||||||
|
(this.upgradeVersion === other.upgradeVersion) &&
|
||||||
|
(this.upgradeVersionDisplayName === other.upgradeVersionDisplayName) &&
|
||||||
|
(this.assetName === other.assetName) &&
|
||||||
|
(this.updateUrl === other.updateUrl) &&
|
||||||
|
(this.gpgSignatureUrl === other.gpgSignatureUrl)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export class UpdateStatus {
|
||||||
|
lastUpdateTime: number = 0;
|
||||||
|
isValid: boolean = false;
|
||||||
|
errorMessage: string = "";
|
||||||
|
updatePolicy: UpdatePolicyT = UpdatePolicyT.ReleaseOrBeta;
|
||||||
|
isOnline: boolean = false;
|
||||||
|
currentVersion: string = "";
|
||||||
|
currentVersionDisplayName: string = "";
|
||||||
|
|
||||||
|
releaseOnlyRelease: UpdateRelease = new UpdateRelease();
|
||||||
|
releaseOrBetaRelease: UpdateRelease = new UpdateRelease();
|
||||||
|
devRelease: UpdateRelease = new UpdateRelease();
|
||||||
|
|
||||||
|
deserialize(input: any): UpdateStatus {
|
||||||
|
this.lastUpdateTime = input.lastUpdateTime;
|
||||||
|
this.isValid = input.isValid;
|
||||||
|
this.errorMessage = input.errorMessage;
|
||||||
|
this.isOnline = input.isOnline;
|
||||||
|
this.updatePolicy = intToUpdatePolicyT(input.updatePolicy);
|
||||||
|
this.currentVersion = input.currentVersion;
|
||||||
|
this.currentVersionDisplayName = input.currentVersionDisplayName;
|
||||||
|
this.releaseOnlyRelease = new UpdateRelease().deserialize(input.releaseOnlyRelease);
|
||||||
|
this.releaseOrBetaRelease = new UpdateRelease().deserialize(input.releaseOrBetaRelease);
|
||||||
|
this.devRelease = new UpdateRelease().deserialize(input.devRelease);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
static deserialize_array(input: any): UpdateStatus[] {
|
||||||
|
let result: UpdateStatus[] = [];
|
||||||
|
for (let i = 0; i < input.length; ++i) {
|
||||||
|
result[i] = new UpdateStatus().deserialize(input[i]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
getActiveRelease(): UpdateRelease {
|
||||||
|
switch (this.updatePolicy) {
|
||||||
|
case UpdatePolicyT.Disable: // show available updates in the settings dialog.
|
||||||
|
case UpdatePolicyT.ReleaseOnly:
|
||||||
|
return this.releaseOnlyRelease;
|
||||||
|
case UpdatePolicyT.ReleaseOrBeta:
|
||||||
|
return this.releaseOrBetaRelease;
|
||||||
|
case UpdatePolicyT.Development:
|
||||||
|
return this.devRelease;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid argument.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
equals(other: UpdateStatus): boolean {
|
||||||
|
return (this.isValid === other.isValid)
|
||||||
|
&& (this.lastUpdateTime === other.lastUpdateTime)
|
||||||
|
&& (this.errorMessage === other.errorMessage)
|
||||||
|
&& (this.isOnline === other.isOnline)
|
||||||
|
&& (this.currentVersion === other.currentVersion)
|
||||||
|
&& (this.currentVersionDisplayName === other.currentVersionDisplayName)
|
||||||
|
&& (this.releaseOnlyRelease.equals(other.releaseOnlyRelease))
|
||||||
|
&& (this.releaseOrBetaRelease.equals(other.releaseOrBetaRelease))
|
||||||
|
&& (this.devRelease.equals(other.devRelease))
|
||||||
|
;
|
||||||
|
// other properties are contractually dealt with.
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
+53
-17
@@ -49,7 +49,7 @@ interface ZoomedDialProps extends WithStyles<typeof styles> {
|
|||||||
size: number,
|
size: number,
|
||||||
|
|
||||||
controlInfo: ZoomedControlInfo | undefined,
|
controlInfo: ZoomedControlInfo | undefined,
|
||||||
|
onDoubleTap(): void,
|
||||||
onPreviewValue(value: number): void,
|
onPreviewValue(value: number): void,
|
||||||
onSetValue(value: number): void
|
onSetValue(value: number): void
|
||||||
}
|
}
|
||||||
@@ -81,6 +81,22 @@ const ZoomedDial = withStyles(styles, { withTheme: true })(
|
|||||||
this.onPointerLostCapture = this.onPointerLostCapture.bind(this);
|
this.onPointerLostCapture = this.onPointerLostCapture.bind(this);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
onPointerDoubleTap() {
|
||||||
|
this.props.onDoubleTap();
|
||||||
|
}
|
||||||
|
|
||||||
|
private lastTapMs: number = 0;
|
||||||
|
|
||||||
|
onPointerTap() {
|
||||||
|
let tapTime = Date.now();
|
||||||
|
let dT = tapTime-this.lastTapMs;
|
||||||
|
this.lastTapMs = tapTime;
|
||||||
|
|
||||||
|
if (dT < 500)
|
||||||
|
{
|
||||||
|
this.onPointerDoubleTap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onPointerUp(e: PointerEvent<SVGSVGElement>) {
|
onPointerUp(e: PointerEvent<SVGSVGElement>) {
|
||||||
|
|
||||||
@@ -94,6 +110,15 @@ const ZoomedDial = withStyles(styles, { withTheme: true })(
|
|||||||
|
|
||||||
this.releaseCapture(e);
|
this.releaseCapture(e);
|
||||||
|
|
||||||
|
if (this.isTap) {
|
||||||
|
let ms = Date.now() - this.tapStartMs;
|
||||||
|
if (ms < 200) {
|
||||||
|
this.onPointerTap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
--this.pointersDown;
|
--this.pointersDown;
|
||||||
|
|
||||||
@@ -101,8 +126,7 @@ const ZoomedDial = withStyles(styles, { withTheme: true })(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
releaseCapture(e: PointerEvent<SVGSVGElement>)
|
releaseCapture(e: PointerEvent<SVGSVGElement>) {
|
||||||
{
|
|
||||||
let img = this.imgRef.current;
|
let img = this.imgRef.current;
|
||||||
|
|
||||||
if (img && img.style) {
|
if (img && img.style) {
|
||||||
@@ -229,7 +253,8 @@ const ZoomedDial = withStyles(styles, { withTheme: true })(
|
|||||||
}
|
}
|
||||||
captureElement?: SVGSVGElement;
|
captureElement?: SVGSVGElement;
|
||||||
pointersDown: number = 0;
|
pointersDown: number = 0;
|
||||||
|
private isTap: boolean = false;
|
||||||
|
private tapStartMs: number = 0;
|
||||||
onPointerDown(e: PointerEvent<SVGSVGElement>): void {
|
onPointerDown(e: PointerEvent<SVGSVGElement>): void {
|
||||||
if (!this.mouseDown && this.isValidPointer(e)) {
|
if (!this.mouseDown && this.isValidPointer(e)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -238,6 +263,10 @@ const ZoomedDial = withStyles(styles, { withTheme: true })(
|
|||||||
|
|
||||||
|
|
||||||
this.mouseDown = true;
|
this.mouseDown = true;
|
||||||
|
|
||||||
|
this.isTap = true;
|
||||||
|
this.tapStartMs = Date.now();
|
||||||
|
|
||||||
this.pointersDown = 1;
|
this.pointersDown = 1;
|
||||||
this.pointerId = e.pointerId;
|
this.pointerId = e.pointerId;
|
||||||
this.pointerType = e.pointerType;
|
this.pointerType = e.pointerType;
|
||||||
@@ -276,12 +305,25 @@ const ZoomedDial = withStyles(styles, { withTheme: true })(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clickSlop() {
|
||||||
|
return 3.5; // maybe larger on touch devices.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
onPointerMove(e: PointerEvent<SVGSVGElement>): void {
|
onPointerMove(e: PointerEvent<SVGSVGElement>): void {
|
||||||
if (this.isCapturedPointer(e)) {
|
if (this.isCapturedPointer(e)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.updateAngle(e)
|
this.updateAngle(e);
|
||||||
|
|
||||||
|
|
||||||
|
let x = e.clientX;
|
||||||
|
let y = e.clientY;
|
||||||
|
let dx = x - this.startX;
|
||||||
|
let dy = y - this.startY;
|
||||||
|
let distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
if (distance >= this.clickSlop()) {
|
||||||
|
this.isTap = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,11 +344,9 @@ const ZoomedDial = withStyles(styles, { withTheme: true })(
|
|||||||
while (dAngle < -180) dAngle += 360;
|
while (dAngle < -180) dAngle += 360;
|
||||||
|
|
||||||
let scale = 1.0;
|
let scale = 1.0;
|
||||||
if (this.pointersDown === 2)
|
if (this.pointersDown === 2) {
|
||||||
{
|
|
||||||
scale = FINE_RANGE_SCALE;
|
scale = FINE_RANGE_SCALE;
|
||||||
} else if (this.pointersDown >= 3)
|
} else if (this.pointersDown >= 3) {
|
||||||
{
|
|
||||||
scale = ULTRA_FINE_RANGE_SCALE;
|
scale = ULTRA_FINE_RANGE_SCALE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,21 +358,17 @@ const ZoomedDial = withStyles(styles, { withTheme: true })(
|
|||||||
this.lastPointerAngle = angle;
|
this.lastPointerAngle = angle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
makeRotationTransform(angle: number): string
|
makeRotationTransform(angle: number): string {
|
||||||
{
|
|
||||||
return "rotate(" + angle + "deg)";
|
return "rotate(" + angle + "deg)";
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultRotationTransform(): string
|
getDefaultRotationTransform(): string {
|
||||||
{
|
|
||||||
let v = this.getCurrentValue();
|
let v = this.getCurrentValue();
|
||||||
let a = this.valueToAngle(v);
|
let a = this.valueToAngle(v);
|
||||||
return this.makeRotationTransform(a);
|
return this.makeRotationTransform(a);
|
||||||
}
|
}
|
||||||
previewValue(value: number): void
|
previewValue(value: number): void {
|
||||||
{
|
if (this.imgRef.current) {
|
||||||
if (this.imgRef.current)
|
|
||||||
{
|
|
||||||
let img = this.imgRef.current;
|
let img = this.imgRef.current;
|
||||||
let angle = this.valueToAngle(value);
|
let angle = this.valueToAngle(value);
|
||||||
img.style.transform = this.makeRotationTransform(angle);
|
img.style.transform = this.makeRotationTransform(angle);
|
||||||
|
|||||||
@@ -195,6 +195,14 @@ const ZoomedUiControl = withStyles(styles, { withTheme: true })(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onDoubleTap() {
|
||||||
|
let controlInfo = this.props.controlInfo;
|
||||||
|
if (!controlInfo) return;
|
||||||
|
let instanceId = controlInfo?.instanceId;
|
||||||
|
let uiControl = controlInfo?.uiControl
|
||||||
|
|
||||||
|
this.model.setPedalboardControl(instanceId,uiControl.symbol,uiControl.default_value);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.props.controlInfo) {
|
if (!this.props.controlInfo) {
|
||||||
@@ -241,6 +249,7 @@ const ZoomedUiControl = withStyles(styles, { withTheme: true })(
|
|||||||
{uiControl.isDial() ? (
|
{uiControl.isDial() ? (
|
||||||
|
|
||||||
<ZoomedDial size={200} controlInfo={this.props.controlInfo}
|
<ZoomedDial size={200} controlInfo={this.props.controlInfo}
|
||||||
|
onDoubleTap={()=>{this.onDoubleTap();}}
|
||||||
onPreviewValue={(v) => {
|
onPreviewValue={(v) => {
|
||||||
this.setState({ value: v });
|
this.setState({ value: v });
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1 +1,8 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><path d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M17,11l-1.41-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5 L17,11z"/></g></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" >
|
||||||
|
<g>
|
||||||
|
<rect fill="none" height="24" width="24"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path d="M18,15v3H6v-3H4v3c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-3H18z M17,11l-1.41-1.41L13,12.17V4h-2v8.17L8.41,9.59L7,11l5,5 L17,11z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 333 B After Width: | Height: | Size: 358 B |
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="24px" height="24px" viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||||
|
<path d="M18.993,8.993L18.99,5c0-1.1-0.891-2-1.99-2h-4h-3H6C4.9,3,4.01,3.9,4.01,5L4.007,8.993L4,9v10c0,1.1,0.891,2,1.99,2H6h11
|
||||||
|
h0.01c1.1,0,1.99-0.9,1.99-2V9L18.993,8.993z M17,10v9H6v-9V5h5h1h5V10z"/>
|
||||||
|
<rect x="8" y="7" width="2" height="2"/>
|
||||||
|
<rect x="12" y="7" width="3" height="2"/>
|
||||||
|
<rect x="8" y="11" width="2" height="2"/>
|
||||||
|
<rect x="12" y="11" width="3" height="2"/>
|
||||||
|
<rect x="8" y="15" width="2" height="2"/>
|
||||||
|
<rect x="12" y="15" width="3" height="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 939 B |
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/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 ..
|
||||||
+19
-6
@@ -41,7 +41,7 @@ AdminClient::~AdminClient()
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AdminClient::CanUseShutdownClient()
|
bool AdminClient::CanUseAdminClient()
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -104,7 +104,7 @@ bool AdminClient::SetJackServerConfiguration(const JackServerSettings &jackServe
|
|||||||
|
|
||||||
void AdminClient::SetWifiConfig(const WifiConfigSettings &settings)
|
void AdminClient::SetWifiConfig(const WifiConfigSettings &settings)
|
||||||
{
|
{
|
||||||
if (!CanUseShutdownClient())
|
if (!CanUseAdminClient())
|
||||||
{
|
{
|
||||||
throw PiPedalException("Can't perform this operation when debugging.");
|
throw PiPedalException("Can't perform this operation when debugging.");
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@ void AdminClient::SetWifiConfig(const WifiConfigSettings &settings)
|
|||||||
}
|
}
|
||||||
void AdminClient::SetWifiDirectConfig(const WifiDirectConfigSettings &settings)
|
void AdminClient::SetWifiDirectConfig(const WifiDirectConfigSettings &settings)
|
||||||
{
|
{
|
||||||
if (!CanUseShutdownClient())
|
if (!CanUseAdminClient())
|
||||||
{
|
{
|
||||||
throw PiPedalException("Can't perform this operation when debugging.");
|
throw PiPedalException("Can't perform this operation when debugging.");
|
||||||
}
|
}
|
||||||
@@ -139,7 +139,7 @@ void AdminClient::SetWifiDirectConfig(const WifiDirectConfigSettings &settings)
|
|||||||
|
|
||||||
void AdminClient::SetGovernorSettings(const std::string &settings)
|
void AdminClient::SetGovernorSettings(const std::string &settings)
|
||||||
{
|
{
|
||||||
if (!CanUseShutdownClient())
|
if (!CanUseAdminClient())
|
||||||
{
|
{
|
||||||
throw PiPedalException("Can't use AdminClient when running interactively.");
|
throw PiPedalException("Can't use AdminClient when running interactively.");
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ void AdminClient::SetGovernorSettings(const std::string &settings)
|
|||||||
|
|
||||||
void AdminClient::MonitorGovernor(const std::string &governor)
|
void AdminClient::MonitorGovernor(const std::string &governor)
|
||||||
{
|
{
|
||||||
if (!CanUseShutdownClient())
|
if (!CanUseAdminClient())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -174,7 +174,7 @@ void AdminClient::MonitorGovernor(const std::string &governor)
|
|||||||
}
|
}
|
||||||
void AdminClient::UnmonitorGovernor()
|
void AdminClient::UnmonitorGovernor()
|
||||||
{
|
{
|
||||||
if (!CanUseShutdownClient())
|
if (!CanUseAdminClient())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -183,3 +183,16 @@ void AdminClient::UnmonitorGovernor()
|
|||||||
cmd << '\n';
|
cmd << '\n';
|
||||||
bool ignored = WriteMessage(cmd.str().c_str());
|
bool ignored = WriteMessage(cmd.str().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void AdminClient::InstallUpdate(const std::string&filename)
|
||||||
|
{
|
||||||
|
if (!CanUseAdminClient())
|
||||||
|
{
|
||||||
|
// generally can't do amin operations when running under a debugger.
|
||||||
|
throw std::runtime_error("No admin client.");
|
||||||
|
}
|
||||||
|
std::stringstream cmd;
|
||||||
|
cmd << "InstallUpdate " << filename << '\n';
|
||||||
|
bool ignroed = WriteMessage(cmd.str().c_str());
|
||||||
|
}
|
||||||
|
|||||||
+2
-1
@@ -34,7 +34,7 @@ class AdminClient {
|
|||||||
public:
|
public:
|
||||||
AdminClient();
|
AdminClient();
|
||||||
~AdminClient();
|
~AdminClient();
|
||||||
bool CanUseShutdownClient();
|
bool CanUseAdminClient();
|
||||||
bool RequestShutdown(bool restart);
|
bool RequestShutdown(bool restart);
|
||||||
bool SetJackServerConfiguration(const JackServerSettings & jackServerSettings);
|
bool SetJackServerConfiguration(const JackServerSettings & jackServerSettings);
|
||||||
void SetWifiConfig(const WifiConfigSettings & settings);
|
void SetWifiConfig(const WifiConfigSettings & settings);
|
||||||
@@ -42,6 +42,7 @@ public:
|
|||||||
void SetGovernorSettings(const std::string & governor);
|
void SetGovernorSettings(const std::string & governor);
|
||||||
void MonitorGovernor(const std::string &governor);
|
void MonitorGovernor(const std::string &governor);
|
||||||
void UnmonitorGovernor();
|
void UnmonitorGovernor();
|
||||||
|
void InstallUpdate(const std::string&filename);
|
||||||
private:
|
private:
|
||||||
std::mutex mutex;
|
std::mutex mutex;
|
||||||
UnixSocket socket;
|
UnixSocket socket;
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
// Copyright (c) 2024Robin 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 "AdminInstallUpdate.hpp"
|
||||||
|
#include <fstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include "ss.hpp"
|
||||||
|
#include <vector>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include "Lv2Log.hpp"
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/prctl.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include "UpdateResults.hpp"
|
||||||
|
#include "Updater.cpp"
|
||||||
|
|
||||||
|
using namespace pipedal;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
static fs::path ROOT_INSTALL_DIRECTORY = "/var/pipedal/updates";
|
||||||
|
static fs::path INSTALLER_LOG_FILE_PATH = ROOT_INSTALL_DIRECTORY / "install.log";
|
||||||
|
|
||||||
|
static fs::path UPDATE_DIRECTORY = ROOT_INSTALL_DIRECTORY / "downloads";
|
||||||
|
|
||||||
|
static void removeSignalHandler(int signal)
|
||||||
|
{
|
||||||
|
struct sigaction sa;
|
||||||
|
sa.sa_handler = SIG_DFL;
|
||||||
|
sigemptyset(&sa.sa_mask);
|
||||||
|
sa.sa_flags = 0;
|
||||||
|
sigaction(signal, &sa, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int exec(const std::string &command)
|
||||||
|
{
|
||||||
|
std::string buffer = command;
|
||||||
|
|
||||||
|
|
||||||
|
std::vector<char *> argv;
|
||||||
|
char *p = const_cast<char *>(buffer.c_str());
|
||||||
|
while (*p)
|
||||||
|
{
|
||||||
|
char *start = p;
|
||||||
|
while (*p != ' ' && *p != '\0')
|
||||||
|
{
|
||||||
|
++p;
|
||||||
|
}
|
||||||
|
argv.push_back(start);
|
||||||
|
while (*p == ' ')
|
||||||
|
{
|
||||||
|
*p++ = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
argv.push_back(nullptr);
|
||||||
|
|
||||||
|
pid_t pid;
|
||||||
|
|
||||||
|
if ((pid = fork()) == 0)
|
||||||
|
{
|
||||||
|
execv(argv[0], (char *const *)argv.data());
|
||||||
|
write(1,"!\n",2);
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
if (pid == -1)
|
||||||
|
{
|
||||||
|
write(1,"*",1);
|
||||||
|
perror("execv");
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
int returnValue;
|
||||||
|
waitpid(pid, &returnValue, 0);
|
||||||
|
|
||||||
|
|
||||||
|
int exitStatus = WEXITSTATUS(returnValue);
|
||||||
|
return exitStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateLog(const std::string &message)
|
||||||
|
{
|
||||||
|
write(1,message.c_str(),message.length());
|
||||||
|
write(1,"\n",1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static std::string getVersion(const fs::path path)
|
||||||
|
{
|
||||||
|
std::string fileNameOnly = path.filename();
|
||||||
|
std::vector<std::string> segments = split(path,'_');
|
||||||
|
if (segments.size() != 3) return "Unknown";
|
||||||
|
return SS("v" << segments[1]);
|
||||||
|
}
|
||||||
|
void pipedal::AdminInstallUpdate(const std::filesystem::path path)
|
||||||
|
{
|
||||||
|
|
||||||
|
UpdateResults updateResults;
|
||||||
|
updateResults.updated_ = true;
|
||||||
|
|
||||||
|
updateResults.updateVersion_ = getVersion(path);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// client needs a disconnect/reconnect even if the update doesn't happen.
|
||||||
|
|
||||||
|
exec("/usr/bin/systemctl stop pipedald");
|
||||||
|
|
||||||
|
fs:create_directories(ROOT_INSTALL_DIRECTORY);
|
||||||
|
Lv2Log::info(SS("Installing " << path));
|
||||||
|
|
||||||
|
if (!path.string().starts_with(UPDATE_DIRECTORY.string()))
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Update file path is incorrect.");
|
||||||
|
}
|
||||||
|
if (!fs::exists(path))
|
||||||
|
{
|
||||||
|
throw std::runtime_error("File does not exist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stringstream ssCmd;
|
||||||
|
ssCmd << "/usr/bin/apt-get -q -y install " << path.string();
|
||||||
|
|
||||||
|
std::string cmd = ssCmd.str();
|
||||||
|
|
||||||
|
// direct standard outputs to a log file.
|
||||||
|
close(0);
|
||||||
|
close(1);
|
||||||
|
close(2);
|
||||||
|
|
||||||
|
int fd = open(INSTALLER_LOG_FILE_PATH.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
|
dup2(fd, 1); // to stdout
|
||||||
|
dup2(fd, 2); // to stderr
|
||||||
|
close(fd);
|
||||||
|
fd = -1;
|
||||||
|
|
||||||
|
// disconnect std input from parent's stdin.
|
||||||
|
int fdNull = open("/dev/null", O_RDONLY, 0644);
|
||||||
|
dup2(fdNull, 0);
|
||||||
|
close(fdNull);
|
||||||
|
|
||||||
|
// verify the signature (again), now that we're running as root and could do real damage
|
||||||
|
// if we install somebody else's package.
|
||||||
|
|
||||||
|
Updater::ValidateSignature(path, SS(path.string() << ".asc")); // errors are thrown.
|
||||||
|
|
||||||
|
updateLog("** Installing update");
|
||||||
|
int retcode = exec(cmd);
|
||||||
|
|
||||||
|
if (retcode != EXIT_SUCCESS)
|
||||||
|
{
|
||||||
|
updateResults.updateSuccessful_ = false;
|
||||||
|
updateResults.updateMessage_ = SS("Update failed. See " << INSTALLER_LOG_FILE_PATH.c_str() << " for further details.");
|
||||||
|
} else {
|
||||||
|
updateResults.updateSuccessful_ = true;
|
||||||
|
updateResults.updateMessage_ = SS("Successfully updated to version " << updateResults.updateVersion_ << ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
// in case we didn't actually update for some reason.
|
||||||
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
updateLog(SS("Error: " << e.what()));
|
||||||
|
|
||||||
|
updateResults.updated_ = false;
|
||||||
|
updateResults.updateSuccessful_ = false;
|
||||||
|
updateResults.updateMessage_ = e.what();
|
||||||
|
}
|
||||||
|
updateResults.Save();
|
||||||
|
|
||||||
|
// restart pipedal to get the client to remove the "Updating..." message.
|
||||||
|
exec("/usr/bin/systemctl start pipedald");
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Copyright (c) 2024Robin 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 <filesystem>
|
||||||
|
|
||||||
|
namespace pipedal
|
||||||
|
{
|
||||||
|
void AdminInstallUpdate(const std::filesystem::path path);
|
||||||
|
|
||||||
|
}
|
||||||
+11
-1
@@ -220,6 +220,12 @@ bool setJackConfiguration(JackServerSettings serverSettings)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void InstallUpdate(const std::filesystem::path path)
|
||||||
|
{
|
||||||
|
std::string cmd = SS("/usr/bin/systemd-run --unit=pidal_update /usr/sbin/pipedal_update " <<path.string());
|
||||||
|
int rc = system(cmd.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
class AdminServer
|
class AdminServer
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
@@ -259,7 +265,11 @@ private:
|
|||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (command == "UnmonitorGovernor")
|
if (command == "InstallUpdate")
|
||||||
|
{
|
||||||
|
std::filesystem::path path = args;
|
||||||
|
InstallUpdate(path);
|
||||||
|
} else if (command == "UnmonitorGovernor")
|
||||||
{
|
{
|
||||||
StopGovernorMonitorThread();
|
StopGovernorMonitorThread();
|
||||||
result = 0;
|
result = 0;
|
||||||
|
|||||||
+42
-6
@@ -239,10 +239,10 @@ namespace pipedal
|
|||||||
|
|
||||||
unsigned int periods = 0;
|
unsigned int periods = 0;
|
||||||
|
|
||||||
snd_pcm_hw_params_t *captureHwParams;
|
snd_pcm_hw_params_t *captureHwParams = nullptr;
|
||||||
snd_pcm_sw_params_t *captureSwParams;
|
snd_pcm_sw_params_t *captureSwParams = nullptr;
|
||||||
snd_pcm_hw_params_t *playbackHwParams;
|
snd_pcm_hw_params_t *playbackHwParams = nullptr;
|
||||||
snd_pcm_sw_params_t *playbackSwParams;
|
snd_pcm_sw_params_t *playbackSwParams = nullptr;
|
||||||
|
|
||||||
bool capture_and_playback_not_synced = false;
|
bool capture_and_playback_not_synced = false;
|
||||||
|
|
||||||
@@ -1469,6 +1469,32 @@ namespace pipedal
|
|||||||
Lv2Log::error("ALSA audio thread terminated abnormally.");
|
Lv2Log::error("ALSA audio thread terminated abnormally.");
|
||||||
}
|
}
|
||||||
this->driverHost->OnAudioStopped();
|
this->driverHost->OnAudioStopped();
|
||||||
|
|
||||||
|
// if we terminated abnormally, pump messages until we have been terminated.
|
||||||
|
if (!terminateAudio())
|
||||||
|
{
|
||||||
|
// zero out input buffers.
|
||||||
|
for (size_t i = 0; i < this->captureBuffers.size(); ++i)
|
||||||
|
{
|
||||||
|
float*pBuffer = captureBuffers[i];
|
||||||
|
for (size_t j = 0; j < this->bufferSize; ++j)
|
||||||
|
{
|
||||||
|
pBuffer[j] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
while(!terminateAudio())
|
||||||
|
{
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
// zero out input buffers.
|
||||||
|
this->driverHost->OnProcess(this->bufferSize);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this->driverHost->OnAudioTerminated();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool alsaActive = false;
|
bool alsaActive = false;
|
||||||
@@ -1947,6 +1973,17 @@ namespace pipedal
|
|||||||
std::vector<std::string> &inputAudioPorts,
|
std::vector<std::string> &inputAudioPorts,
|
||||||
std::vector<std::string> &outputAudioPorts)
|
std::vector<std::string> &outputAudioPorts)
|
||||||
{
|
{
|
||||||
|
if (jackServerSettings.IsDummyAudioDevice())
|
||||||
|
{
|
||||||
|
inputAudioPorts.clear();
|
||||||
|
inputAudioPorts.push_back("system::capture_0");
|
||||||
|
inputAudioPorts.push_back("system::capture_1");
|
||||||
|
outputAudioPorts.clear();
|
||||||
|
outputAudioPorts.push_back("system::playback_0");
|
||||||
|
outputAudioPorts.push_back("system::playback_1");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
snd_pcm_t *playbackHandle = nullptr;
|
snd_pcm_t *playbackHandle = nullptr;
|
||||||
snd_pcm_t *captureHandle = nullptr;
|
snd_pcm_t *captureHandle = nullptr;
|
||||||
@@ -1958,7 +1995,7 @@ namespace pipedal
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
int err;
|
int err;
|
||||||
for (int retry = 0; retry < 15; ++retry)
|
for (int retry = 0; retry < 2; ++retry)
|
||||||
{
|
{
|
||||||
err = snd_pcm_open(&playbackHandle, alsaDeviceName.c_str(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
|
err = snd_pcm_open(&playbackHandle, alsaDeviceName.c_str(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
|
||||||
if (err < 0) // field report of a device that is present, but won't immediately open.
|
if (err < 0) // field report of a device that is present, but won't immediately open.
|
||||||
@@ -2044,7 +2081,6 @@ namespace pipedal
|
|||||||
}
|
}
|
||||||
catch (const std::exception &e)
|
catch (const std::exception &e)
|
||||||
{
|
{
|
||||||
Lv2Log::warning(SS("Unable to read ALSA configuration for " << alsaDeviceName << ". " << e.what() << "."));
|
|
||||||
result = false;
|
result = false;
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,6 +208,9 @@ public:
|
|||||||
Oscillator oscillator;
|
Oscillator oscillator;
|
||||||
LatencyMonitor latencyMonitor;
|
LatencyMonitor latencyMonitor;
|
||||||
|
|
||||||
|
virtual void OnAudioTerminated() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
virtual void OnAudioStopped() {
|
virtual void OnAudioStopped() {
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#ifndef JACK_HOST
|
#ifndef JACK_HOST // jack is no longer supported.
|
||||||
#define JACK_HOST 0
|
#define JACK_HOST 0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ namespace pipedal {
|
|||||||
virtual void OnProcess(size_t nFrames) = 0;
|
virtual void OnProcess(size_t nFrames) = 0;
|
||||||
virtual void OnUnderrun() = 0;
|
virtual void OnUnderrun() = 0;
|
||||||
virtual void OnAudioStopped() = 0;
|
virtual void OnAudioStopped() = 0;
|
||||||
|
virtual void OnAudioTerminated() = 0;
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
+99
-75
@@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
#include "JackDriver.hpp"
|
#include "JackDriver.hpp"
|
||||||
#include "AlsaDriver.hpp"
|
#include "AlsaDriver.hpp"
|
||||||
|
#include "DummyAudioDriver.hpp"
|
||||||
#include "AtomConverter.hpp"
|
#include "AtomConverter.hpp"
|
||||||
|
|
||||||
using namespace pipedal;
|
using namespace pipedal;
|
||||||
@@ -49,6 +50,7 @@ using namespace pipedal;
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include "Lv2EventBufferWriter.hpp"
|
#include "Lv2EventBufferWriter.hpp"
|
||||||
#include "InheritPriorityMutex.hpp"
|
#include "InheritPriorityMutex.hpp"
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
#ifdef __linux__
|
#ifdef __linux__
|
||||||
#include <sched.h>
|
#include <sched.h>
|
||||||
@@ -105,13 +107,18 @@ static std::string GetGovernor()
|
|||||||
return pipedal::GetCpuGovernor();
|
return pipedal::GetCpuGovernor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SystemMidiBinding
|
||||||
class SystemMidiBinding {
|
{
|
||||||
private:
|
private:
|
||||||
MidiBinding currentBinding;
|
MidiBinding currentBinding;
|
||||||
bool controlState = false;
|
bool controlState = false;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void SetBinding(const MidiBinding&binding) { currentBinding = binding; controlState = false; }
|
void SetBinding(const MidiBinding &binding)
|
||||||
|
{
|
||||||
|
currentBinding = binding;
|
||||||
|
controlState = false;
|
||||||
|
}
|
||||||
|
|
||||||
bool IsMatch(const MidiEvent &event);
|
bool IsMatch(const MidiEvent &event);
|
||||||
bool IsTriggered(const MidiEvent &event);
|
bool IsTriggered(const MidiEvent &event);
|
||||||
@@ -123,19 +130,25 @@ bool SystemMidiBinding::IsTriggered(const MidiEvent &event)
|
|||||||
{
|
{
|
||||||
case BINDING_TYPE_NOTE:
|
case BINDING_TYPE_NOTE:
|
||||||
{
|
{
|
||||||
if (event.size != 3) return false;
|
if (event.size != 3)
|
||||||
|
return false;
|
||||||
auto command = event.buffer[0] & 0xF0;
|
auto command = event.buffer[0] & 0xF0;
|
||||||
if (command != 0x90) return false; // MIDI note on
|
if (command != 0x90)
|
||||||
if (event.buffer[1] != currentBinding.note()) return false;
|
return false; // MIDI note on
|
||||||
|
if (event.buffer[1] != currentBinding.note())
|
||||||
|
return false;
|
||||||
return event.buffer[2] != 0; // note-on with velicity of zero is a note-off.
|
return event.buffer[2] != 0; // note-on with velicity of zero is a note-off.
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case BINDING_TYPE_CONTROL:
|
case BINDING_TYPE_CONTROL:
|
||||||
{
|
{
|
||||||
if (event.size != 3) return false;
|
if (event.size != 3)
|
||||||
|
return false;
|
||||||
auto command = event.buffer[0] & 0xF0;
|
auto command = event.buffer[0] & 0xF0;
|
||||||
if (command != 0xB0) return false;
|
if (command != 0xB0)
|
||||||
if (event.buffer[1] != currentBinding.control()) return false;
|
return false;
|
||||||
|
if (event.buffer[1] != currentBinding.control())
|
||||||
|
return false;
|
||||||
bool state = event.buffer[2] >= 0x64;
|
bool state = event.buffer[2] >= 0x64;
|
||||||
if (state != this->controlState)
|
if (state != this->controlState)
|
||||||
{
|
{
|
||||||
@@ -156,17 +169,21 @@ bool SystemMidiBinding::IsMatch(const MidiEvent&event)
|
|||||||
{
|
{
|
||||||
case BINDING_TYPE_NOTE:
|
case BINDING_TYPE_NOTE:
|
||||||
{
|
{
|
||||||
if (event.size != 3) return false;
|
if (event.size != 3)
|
||||||
|
return false;
|
||||||
auto command = event.buffer[0] & 0xF0;
|
auto command = event.buffer[0] & 0xF0;
|
||||||
if (command != 0x80 && command != 0x90) return false; // note on/note off.
|
if (command != 0x80 && command != 0x90)
|
||||||
|
return false; // note on/note off.
|
||||||
return event.buffer[1] == currentBinding.note();
|
return event.buffer[1] == currentBinding.note();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case BINDING_TYPE_CONTROL:
|
case BINDING_TYPE_CONTROL:
|
||||||
{
|
{
|
||||||
if (event.size != 3) return false;
|
if (event.size != 3)
|
||||||
|
return false;
|
||||||
auto command = event.buffer[0] & 0xF0;
|
auto command = event.buffer[0] & 0xF0;
|
||||||
if (command != 0xB0) return false;
|
if (command != 0xB0)
|
||||||
|
return false;
|
||||||
return event.buffer[1] == currentBinding.control();
|
return event.buffer[1] == currentBinding.control();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -181,7 +198,6 @@ private:
|
|||||||
IHost *pHost = nullptr;
|
IHost *pHost = nullptr;
|
||||||
LV2_Atom_Forge inputWriterForge;
|
LV2_Atom_Forge inputWriterForge;
|
||||||
|
|
||||||
|
|
||||||
std::mutex atomConverterMutex;
|
std::mutex atomConverterMutex;
|
||||||
AtomConverter atomConverter;
|
AtomConverter atomConverter;
|
||||||
|
|
||||||
@@ -254,7 +270,7 @@ private:
|
|||||||
|
|
||||||
Uris uris;
|
Uris uris;
|
||||||
|
|
||||||
AudioDriver *audioDriver = nullptr;
|
std::unique_ptr<AudioDriver> audioDriver;
|
||||||
|
|
||||||
std::recursive_mutex mutex;
|
std::recursive_mutex mutex;
|
||||||
int64_t overrunGracePeriodSamples = 0;
|
int64_t overrunGracePeriodSamples = 0;
|
||||||
@@ -282,7 +298,9 @@ private:
|
|||||||
SystemMidiBinding prevMidiBinding;
|
SystemMidiBinding prevMidiBinding;
|
||||||
|
|
||||||
JackChannelSelection channelSelection;
|
JackChannelSelection channelSelection;
|
||||||
bool active = false;
|
std::atomic<bool> active = false;
|
||||||
|
std::atomic<bool> audioStopped = false;
|
||||||
|
std::atomic<bool> isDummyAudioDriver = false;
|
||||||
|
|
||||||
std::shared_ptr<Lv2Pedalboard> currentPedalboard;
|
std::shared_ptr<Lv2Pedalboard> currentPedalboard;
|
||||||
std::vector<std::shared_ptr<Lv2Pedalboard>> activePedalboards; // pedalboards that have been sent to the audio queue.
|
std::vector<std::shared_ptr<Lv2Pedalboard>> activePedalboards; // pedalboards that have been sent to the audio queue.
|
||||||
@@ -307,7 +325,6 @@ private:
|
|||||||
return pHost->Lv2UridToString(pAtom->body.otype);
|
return pHost->Lv2UridToString(pAtom->body.otype);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
virtual std::string AtomToJson(const LV2_Atom *pAtom) override
|
virtual std::string AtomToJson(const LV2_Atom *pAtom) override
|
||||||
{
|
{
|
||||||
std::lock_guard lock(atomConverterMutex);
|
std::lock_guard lock(atomConverterMutex);
|
||||||
@@ -350,7 +367,6 @@ private:
|
|||||||
|
|
||||||
StopReaderThread();
|
StopReaderThread();
|
||||||
|
|
||||||
|
|
||||||
// release any pdealboards owned by the process thread.
|
// release any pdealboards owned by the process thread.
|
||||||
this->activePedalboards.resize(0);
|
this->activePedalboards.resize(0);
|
||||||
this->realtimeActivePedalboard = nullptr;
|
this->realtimeActivePedalboard = nullptr;
|
||||||
@@ -375,7 +391,7 @@ private:
|
|||||||
delete[] midiLv2Buffers[i];
|
delete[] midiLv2Buffers[i];
|
||||||
}
|
}
|
||||||
midiLv2Buffers.resize(0);
|
midiLv2Buffers.resize(0);
|
||||||
|
audioDriver = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ZeroBuffer(float *buffer, size_t nframes)
|
void ZeroBuffer(float *buffer, size_t nframes)
|
||||||
@@ -424,7 +440,6 @@ private:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
virtual void SetSystemMidiBindings(const std::vector<MidiBinding> &bindings);
|
virtual void SetSystemMidiBindings(const std::vector<MidiBinding> &bindings);
|
||||||
|
|
||||||
void writeVu()
|
void writeVu()
|
||||||
@@ -654,7 +669,6 @@ private:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void ProcessMidiInput()
|
void ProcessMidiInput()
|
||||||
{
|
{
|
||||||
Lv2EventBufferWriter eventBufferWriter(this->eventBufferUrids);
|
Lv2EventBufferWriter eventBufferWriter(this->eventBufferUrids);
|
||||||
@@ -676,7 +690,6 @@ private:
|
|||||||
Lv2EventBufferWriter::LV2_EvBuf_Iterator iterator = eventBufferWriter.begin();
|
Lv2EventBufferWriter::LV2_EvBuf_Iterator iterator = eventBufferWriter.begin();
|
||||||
MidiEvent event;
|
MidiEvent event;
|
||||||
|
|
||||||
|
|
||||||
// write all deferred midi messages.
|
// write all deferred midi messages.
|
||||||
if (deferredMidiMessageCount != 0 && !midiProgramChangePending)
|
if (deferredMidiMessageCount != 0 && !midiProgramChangePending)
|
||||||
{
|
{
|
||||||
@@ -697,7 +710,6 @@ private:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
for (size_t frame = 0; frame < n; ++frame)
|
for (size_t frame = 0; frame < n; ++frame)
|
||||||
{
|
{
|
||||||
if (audioDriver->GetMidiInputEvent(&event, portBuffer, frame))
|
if (audioDriver->GetMidiInputEvent(&event, portBuffer, frame))
|
||||||
@@ -713,14 +725,16 @@ private:
|
|||||||
else if (isBankChange(event))
|
else if (isBankChange(event))
|
||||||
{
|
{
|
||||||
this->selectedBank = event.buffer[2];
|
this->selectedBank = event.buffer[2];
|
||||||
} else if (this->nextMidiBinding.IsMatch(event))
|
}
|
||||||
|
else if (this->nextMidiBinding.IsMatch(event))
|
||||||
{
|
{
|
||||||
if (nextMidiBinding.IsTriggered(event))
|
if (nextMidiBinding.IsTriggered(event))
|
||||||
{
|
{
|
||||||
midiProgramChangePending = true;
|
midiProgramChangePending = true;
|
||||||
this->realtimeWriter.OnNextMidiProgram(++(this->midiProgramChangeId), 1);
|
this->realtimeWriter.OnNextMidiProgram(++(this->midiProgramChangeId), 1);
|
||||||
}
|
}
|
||||||
} else if (this->prevMidiBinding.IsMatch(event))
|
}
|
||||||
|
else if (this->prevMidiBinding.IsMatch(event))
|
||||||
{
|
{
|
||||||
if (prevMidiBinding.IsTriggered(event))
|
if (prevMidiBinding.IsTriggered(event))
|
||||||
{
|
{
|
||||||
@@ -731,9 +745,7 @@ private:
|
|||||||
else if (midiProgramChangePending)
|
else if (midiProgramChangePending)
|
||||||
{
|
{
|
||||||
// defer the message for processing after the program change has completed.
|
// defer the message for processing after the program change has completed.
|
||||||
if (event.size > 0
|
if (event.size > 0 && event.size < 128 && event.size + 2 + this->deferredMidiMessageCount < DEFERRED_MIDI_BUFFER_SIZE)
|
||||||
&& event.size < 128
|
|
||||||
&& event.size + 2 + this->deferredMidiMessageCount < DEFERRED_MIDI_BUFFER_SIZE)
|
|
||||||
{
|
{
|
||||||
this->deferredMidiMessages[deferredMidiMessageCount++] = midiDeviceIx;
|
this->deferredMidiMessages[deferredMidiMessageCount++] = midiDeviceIx;
|
||||||
this->deferredMidiMessages[deferredMidiMessageCount++] = (uint8_t)event.size;
|
this->deferredMidiMessages[deferredMidiMessageCount++] = (uint8_t)event.size;
|
||||||
@@ -757,18 +769,24 @@ private:
|
|||||||
|
|
||||||
std::mutex audioStoppedMutex;
|
std::mutex audioStoppedMutex;
|
||||||
|
|
||||||
bool IsAudioActive()
|
|
||||||
|
|
||||||
|
bool IsAudioRunning()
|
||||||
{
|
{
|
||||||
std::lock_guard lock{audioStoppedMutex};
|
return this->active && (!this->audioStopped) && !this->isDummyAudioDriver;
|
||||||
return this->active;
|
|
||||||
}
|
}
|
||||||
virtual void OnAudioStopped()
|
virtual void OnAudioStopped() override
|
||||||
|
{
|
||||||
|
this->audioStopped = true;
|
||||||
|
Lv2Log::info("Audio stopped.");
|
||||||
|
}
|
||||||
|
virtual void OnAudioTerminated() override
|
||||||
{
|
{
|
||||||
std::lock_guard lock{audioStoppedMutex};
|
|
||||||
this->active = false;
|
this->active = false;
|
||||||
Lv2Log::info("Audio stopped.");
|
Lv2Log::info("Audio stopped.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
virtual void OnProcess(size_t nframes)
|
virtual void OnProcess(size_t nframes)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -862,6 +880,7 @@ private:
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
AudioHostImpl(IHost *pHost)
|
AudioHostImpl(IHost *pHost)
|
||||||
: inputRingBuffer(RING_BUFFER_SIZE),
|
: inputRingBuffer(RING_BUFFER_SIZE),
|
||||||
@@ -878,22 +897,14 @@ public:
|
|||||||
realtimeAtomBuffer.resize(32 * 1024);
|
realtimeAtomBuffer.resize(32 * 1024);
|
||||||
lv2_atom_forge_init(&inputWriterForge, pHost->GetMapFeature().GetMap());
|
lv2_atom_forge_init(&inputWriterForge, pHost->GetMapFeature().GetMap());
|
||||||
|
|
||||||
#if JACK_HOST
|
|
||||||
audioDriver = CreateJackDriver(this);
|
|
||||||
#endif
|
|
||||||
#if ALSA_HOST
|
|
||||||
audioDriver = CreateAlsaDriver(this);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
virtual ~AudioHostImpl()
|
virtual ~AudioHostImpl()
|
||||||
{
|
{
|
||||||
Close();
|
Close();
|
||||||
CleanRestartThreads(true);
|
CleanRestartThreads(true);
|
||||||
|
audioDriver = nullptr;
|
||||||
delete audioDriver;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
virtual JackConfiguration GetServerConfiguration()
|
virtual JackConfiguration GetServerConfiguration()
|
||||||
{
|
{
|
||||||
JackConfiguration result;
|
JackConfiguration result;
|
||||||
@@ -907,8 +918,6 @@ public:
|
|||||||
return this->sampleRate;
|
return this->sampleRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void OnAudioComplete()
|
void OnAudioComplete()
|
||||||
{
|
{
|
||||||
// there is actually no compelling circumstance in which this should ever happen.
|
// there is actually no compelling circumstance in which this should ever happen.
|
||||||
@@ -942,7 +951,7 @@ public:
|
|||||||
{
|
{
|
||||||
Lv2Log::debug("Service thread priority successfully boosted.");
|
Lv2Log::debug("Service thread priority successfully boosted.");
|
||||||
}
|
}
|
||||||
SetThreadName("aout");
|
SetThreadName("rtsvc");
|
||||||
#else
|
#else
|
||||||
xxx; // TODO!
|
xxx; // TODO!
|
||||||
#endif
|
#endif
|
||||||
@@ -1036,7 +1045,8 @@ public:
|
|||||||
{
|
{
|
||||||
if (pRequest->errorMessage == nullptr)
|
if (pRequest->errorMessage == nullptr)
|
||||||
{
|
{
|
||||||
if (pRequest->GetSize() != 0) {
|
if (pRequest->GetSize() != 0)
|
||||||
|
{
|
||||||
IEffect *pEffect = currentPedalboard->GetEffect(pRequest->instanceId);
|
IEffect *pEffect = currentPedalboard->GetEffect(pRequest->instanceId);
|
||||||
if (pEffect == nullptr)
|
if (pEffect == nullptr)
|
||||||
{
|
{
|
||||||
@@ -1046,7 +1056,9 @@ public:
|
|||||||
{
|
{
|
||||||
pRequest->jsonResponse = AtomToJson((LV2_Atom *)pRequest->GetBuffer());
|
pRequest->jsonResponse = AtomToJson((LV2_Atom *)pRequest->GetBuffer());
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
pRequest->errorMessage = "Plugin did not respond.";
|
pRequest->errorMessage = "Plugin did not respond.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1157,17 +1169,20 @@ public:
|
|||||||
hostReader.read(&body);
|
hostReader.read(&body);
|
||||||
OnAudioComplete();
|
OnAudioComplete();
|
||||||
return;
|
return;
|
||||||
} else if (command == RingBufferCommand::MidiProgramChange)
|
}
|
||||||
|
else if (command == RingBufferCommand::MidiProgramChange)
|
||||||
{
|
{
|
||||||
RealtimeMidiProgramRequest programRequest;
|
RealtimeMidiProgramRequest programRequest;
|
||||||
hostReader.read(&programRequest);
|
hostReader.read(&programRequest);
|
||||||
OnMidiProgramRequest(programRequest);
|
OnMidiProgramRequest(programRequest);
|
||||||
} else if (command == RingBufferCommand::NextMidiProgram)
|
}
|
||||||
|
else if (command == RingBufferCommand::NextMidiProgram)
|
||||||
{
|
{
|
||||||
RealtimeNextMidiProgramRequest request;
|
RealtimeNextMidiProgramRequest request;
|
||||||
hostReader.read(&request);
|
hostReader.read(&request);
|
||||||
pNotifyCallbacks->OnNotifyNextMidiProgram(request);
|
pNotifyCallbacks->OnNotifyNextMidiProgram(request);
|
||||||
} else if (command == RingBufferCommand::Lv2ErrorMessage)
|
}
|
||||||
|
else if (command == RingBufferCommand::Lv2ErrorMessage)
|
||||||
{
|
{
|
||||||
size_t size;
|
size_t size;
|
||||||
int64_t instanceId;
|
int64_t instanceId;
|
||||||
@@ -1245,6 +1260,22 @@ public:
|
|||||||
{
|
{
|
||||||
|
|
||||||
std::lock_guard guard(mutex);
|
std::lock_guard guard(mutex);
|
||||||
|
|
||||||
|
if (isOpen)
|
||||||
|
{
|
||||||
|
throw PiPedalStateException("Already open.");
|
||||||
|
}
|
||||||
|
isOpen = true;
|
||||||
|
|
||||||
|
if (jackServerSettings.IsDummyAudioDevice())
|
||||||
|
{
|
||||||
|
this->isDummyAudioDriver = true;
|
||||||
|
this->audioDriver = std::unique_ptr<AudioDriver>(CreateDummyAudioDriver(this));
|
||||||
|
} else {
|
||||||
|
this->isDummyAudioDriver = false;
|
||||||
|
this->audioDriver = std::unique_ptr<AudioDriver>(CreateAlsaDriver(this));
|
||||||
|
}
|
||||||
|
|
||||||
if (channelSelection.GetInputAudioPorts().size() == 0 || channelSelection.GetOutputAudioPorts().size() == 0)
|
if (channelSelection.GetInputAudioPorts().size() == 0 || channelSelection.GetOutputAudioPorts().size() == 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -1253,11 +1284,6 @@ public:
|
|||||||
this->currentSample = 0;
|
this->currentSample = 0;
|
||||||
this->underruns = 0;
|
this->underruns = 0;
|
||||||
|
|
||||||
if (isOpen)
|
|
||||||
{
|
|
||||||
throw PiPedalStateException("Already open.");
|
|
||||||
}
|
|
||||||
isOpen = true;
|
|
||||||
|
|
||||||
this->inputRingBuffer.reset();
|
this->inputRingBuffer.reset();
|
||||||
this->outputRingBuffer.reset();
|
this->outputRingBuffer.reset();
|
||||||
@@ -1272,7 +1298,6 @@ public:
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
||||||
audioDriver->Open(jackServerSettings, this->channelSelection);
|
audioDriver->Open(jackServerSettings, this->channelSelection);
|
||||||
|
|
||||||
this->sampleRate = audioDriver->GetSampleRate();
|
this->sampleRate = audioDriver->GetSampleRate();
|
||||||
@@ -1287,12 +1312,12 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
active = true;
|
active = true;
|
||||||
|
audioStopped = false;
|
||||||
audioDriver->Activate();
|
audioDriver->Activate();
|
||||||
Lv2Log::info(SS("Audio started. " << audioDriver->GetConfigurationDescription()));
|
Lv2Log::info(SS("Audio started. " << audioDriver->GetConfigurationDescription()));
|
||||||
}
|
}
|
||||||
catch (const std::exception &e)
|
catch (const std::exception &e)
|
||||||
{
|
{
|
||||||
Lv2Log::error(SS("Failed to start audio. " << e.what()));
|
|
||||||
Close();
|
Close();
|
||||||
active = false;
|
active = false;
|
||||||
throw;
|
throw;
|
||||||
@@ -1368,7 +1393,6 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SetControlValue(uint64_t instanceId, const std::string &symbol, float value)
|
void SetControlValue(uint64_t instanceId, const std::string &symbol, float value)
|
||||||
@@ -1387,22 +1411,22 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void SetInputVolume(float value) {
|
virtual void SetInputVolume(float value)
|
||||||
|
{
|
||||||
std::lock_guard guard(mutex);
|
std::lock_guard guard(mutex);
|
||||||
if (active && this->currentPedalboard)
|
if (active && this->currentPedalboard)
|
||||||
{
|
{
|
||||||
hostWriter.SetInputVolume(value);
|
hostWriter.SetInputVolume(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void SetOutputVolume(float value) {
|
virtual void SetOutputVolume(float value)
|
||||||
|
{
|
||||||
std::lock_guard guard(mutex);
|
std::lock_guard guard(mutex);
|
||||||
if (active && this->currentPedalboard)
|
if (active && this->currentPedalboard)
|
||||||
{
|
{
|
||||||
hostWriter.SetOutputVolume(value);
|
hostWriter.SetOutputVolume(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual void SetVuSubscriptions(const std::vector<int64_t> &instanceIds)
|
virtual void SetVuSubscriptions(const std::vector<int64_t> &instanceIds)
|
||||||
@@ -1436,7 +1460,9 @@ public:
|
|||||||
|
|
||||||
vuConfig->vuUpdateWorkingData.push_back(v);
|
vuConfig->vuUpdateWorkingData.push_back(v);
|
||||||
vuConfig->vuUpdateResponseData.push_back(v);
|
vuConfig->vuUpdateResponseData.push_back(v);
|
||||||
} else if (instanceId == Pedalboard::INPUT_VOLUME_ID || instanceId == Pedalboard::OUTPUT_VOLUME_ID) {
|
}
|
||||||
|
else if (instanceId == Pedalboard::INPUT_VOLUME_ID || instanceId == Pedalboard::OUTPUT_VOLUME_ID)
|
||||||
|
{
|
||||||
int index = (int)instanceId;
|
int index = (int)instanceId;
|
||||||
VuUpdate v;
|
VuUpdate v;
|
||||||
vuConfig->enabledIndexes.push_back(index);
|
vuConfig->enabledIndexes.push_back(index);
|
||||||
@@ -1444,12 +1470,13 @@ public:
|
|||||||
if (instanceId == Pedalboard::INPUT_VOLUME_ID)
|
if (instanceId == Pedalboard::INPUT_VOLUME_ID)
|
||||||
{
|
{
|
||||||
v.isStereoInput_ = v.isStereoOutput_ = this->pHost->GetNumberOfInputAudioChannels() > 1;
|
v.isStereoInput_ = v.isStereoOutput_ = this->pHost->GetNumberOfInputAudioChannels() > 1;
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
v.isStereoInput_ = v.isStereoOutput_ = this->pHost->GetNumberOfOutputAudioChannels() > 1;
|
v.isStereoInput_ = v.isStereoOutput_ = this->pHost->GetNumberOfOutputAudioChannels() > 1;
|
||||||
}
|
}
|
||||||
vuConfig->vuUpdateWorkingData.push_back(v);
|
vuConfig->vuUpdateWorkingData.push_back(v);
|
||||||
vuConfig->vuUpdateResponseData.push_back(v);
|
vuConfig->vuUpdateResponseData.push_back(v);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1499,7 +1526,6 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
virtual bool UpdatePluginStates(Pedalboard &pedalboard) override;
|
virtual bool UpdatePluginStates(Pedalboard &pedalboard) override;
|
||||||
virtual bool UpdatePluginState(PedalboardItem &pedalboardItem) override;
|
virtual bool UpdatePluginState(PedalboardItem &pedalboardItem) override;
|
||||||
|
|
||||||
@@ -1641,7 +1667,7 @@ public:
|
|||||||
|
|
||||||
result.temperaturemC_ = GetRaspberryPiTemperature();
|
result.temperaturemC_ = GetRaspberryPiTemperature();
|
||||||
|
|
||||||
result.active_ = IsAudioActive();
|
result.active_ = IsAudioRunning();
|
||||||
result.restarting_ = this->restarting;
|
result.restarting_ = this->restarting;
|
||||||
|
|
||||||
if (this->audioDriver != nullptr)
|
if (this->audioDriver != nullptr)
|
||||||
@@ -1687,14 +1713,14 @@ void AudioHostImpl::SetSystemMidiBindings(const std::vector<MidiBinding>&binding
|
|||||||
if (i->symbol() == "nextProgram")
|
if (i->symbol() == "nextProgram")
|
||||||
{
|
{
|
||||||
this->nextMidiBinding.SetBinding(*i);
|
this->nextMidiBinding.SetBinding(*i);
|
||||||
} else if (i->symbol() == "prevProgram")
|
}
|
||||||
|
else if (i->symbol() == "prevProgram")
|
||||||
{
|
{
|
||||||
this->prevMidiBinding.SetBinding(*i);
|
this->prevMidiBinding.SetBinding(*i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
AudioHost *AudioHost::CreateInstance(IHost *pHost)
|
AudioHost *AudioHost::CreateInstance(IHost *pHost)
|
||||||
{
|
{
|
||||||
return new AudioHostImpl(pHost);
|
return new AudioHostImpl(pHost);
|
||||||
@@ -1715,14 +1741,14 @@ bool AudioHostImpl::UpdatePluginStates(Pedalboard& pedalboard)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
}
|
}
|
||||||
bool AudioHostImpl::UpdatePluginState(PedalboardItem &pedalboardItem)
|
bool AudioHostImpl::UpdatePluginState(PedalboardItem &pedalboardItem)
|
||||||
{
|
{
|
||||||
IEffect *effect = currentPedalboard->GetEffect(pedalboardItem.instanceId());
|
IEffect *effect = currentPedalboard->GetEffect(pedalboardItem.instanceId());
|
||||||
if (effect != nullptr)
|
if (effect != nullptr)
|
||||||
{
|
{
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
Lv2PluginState state;
|
Lv2PluginState state;
|
||||||
if (!effect->GetLv2State(&state))
|
if (!effect->GetLv2State(&state))
|
||||||
{
|
{
|
||||||
@@ -1734,7 +1760,8 @@ bool AudioHostImpl::UpdatePluginState(PedalboardItem &pedalboardItem)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
} catch(const std::exception &e)
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
{
|
{
|
||||||
Lv2Log::warning(SS(pedalboardItem.pluginName() << ": " << e.what()));
|
Lv2Log::warning(SS(pedalboardItem.pluginName() << ": " << e.what()));
|
||||||
return false;
|
return false;
|
||||||
@@ -1743,9 +1770,6 @@ bool AudioHostImpl::UpdatePluginState(PedalboardItem &pedalboardItem)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
JSON_MAP_BEGIN(JackHostStatus)
|
JSON_MAP_BEGIN(JackHostStatus)
|
||||||
JSON_MAP_REFERENCE(JackHostStatus, active)
|
JSON_MAP_REFERENCE(JackHostStatus, active)
|
||||||
JSON_MAP_REFERENCE(JackHostStatus, errorMessage)
|
JSON_MAP_REFERENCE(JackHostStatus, errorMessage)
|
||||||
|
|||||||
+1
-1
@@ -166,7 +166,7 @@ public:
|
|||||||
|
|
||||||
class JackHostStatus {
|
class JackHostStatus {
|
||||||
public:
|
public:
|
||||||
bool active_;
|
bool active_ = false;
|
||||||
std::string errorMessage_;
|
std::string errorMessage_;
|
||||||
bool restarting_;
|
bool restarting_;
|
||||||
uint64_t underruns_;
|
uint64_t underruns_;
|
||||||
|
|||||||
+24
-3
@@ -89,12 +89,17 @@ set(CMAKE_CXX_STANDARD_REQUIRED True)
|
|||||||
|
|
||||||
set (USE_SANITIZE False)
|
set (USE_SANITIZE False)
|
||||||
|
|
||||||
|
|
||||||
if (!ENABLE_VST3)
|
if (!ENABLE_VST3)
|
||||||
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -Wno-psabi")
|
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror -Wno-psabi -DARCHITECTURE=${CPACK_SYSTEM_NAME}")
|
||||||
else()
|
else()
|
||||||
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-multichar -Wno-psabi")
|
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-multichar -Wno-psabi")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
EXECUTE_PROCESS( COMMAND dpkg --print-architecture COMMAND tr -d '\n' OUTPUT_VARIABLE DEBIAN_ARCHITECTURE )
|
||||||
|
message(STATUS DEBIAN_ARCHITECTURE="${DEBIAN_ARCHITECTURE}")
|
||||||
|
|
||||||
|
add_compile_definitions(DEBIAN_ARCHITECTURE="${DEBIAN_ARCHITECTURE}")
|
||||||
|
|
||||||
if (CMAKE_BUILD_TYPE MATCHES Debug)
|
if (CMAKE_BUILD_TYPE MATCHES Debug)
|
||||||
message(STATUS "Debug build")
|
message(STATUS "Debug build")
|
||||||
@@ -132,6 +137,9 @@ else()
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
set (PIPEDAL_SOURCES
|
set (PIPEDAL_SOURCES
|
||||||
|
UpdateResults.cpp UpdateResults.hpp
|
||||||
|
UpdaterSecurity.hpp
|
||||||
|
Updater.cpp Updater.hpp
|
||||||
Lv2PluginChangeMonitor.cpp Lv2PluginChangeMonitor.hpp
|
Lv2PluginChangeMonitor.cpp Lv2PluginChangeMonitor.hpp
|
||||||
WebServerConfig.cpp WebServerConfig.hpp
|
WebServerConfig.cpp WebServerConfig.hpp
|
||||||
Locale.hpp Locale.cpp
|
Locale.hpp Locale.cpp
|
||||||
@@ -214,6 +222,7 @@ set (PIPEDAL_SOURCES
|
|||||||
|
|
||||||
JackDriver.cpp JackDriver.hpp
|
JackDriver.cpp JackDriver.hpp
|
||||||
AlsaDriver.cpp AlsaDriver.hpp
|
AlsaDriver.cpp AlsaDriver.hpp
|
||||||
|
DummyAudioDriver.cpp DummyAudioDriver.hpp
|
||||||
AudioDriver.hpp
|
AudioDriver.hpp
|
||||||
AudioConfig.hpp
|
AudioConfig.hpp
|
||||||
|
|
||||||
@@ -278,6 +287,7 @@ add_executable(pipedaltest testMain.cpp
|
|||||||
|
|
||||||
InvertingMutexTest.cpp
|
InvertingMutexTest.cpp
|
||||||
jsonTest.cpp
|
jsonTest.cpp
|
||||||
|
UpdaterTest.cpp
|
||||||
|
|
||||||
utilTest.cpp
|
utilTest.cpp
|
||||||
|
|
||||||
@@ -563,6 +573,7 @@ add_executable(pipedalconfig
|
|||||||
CommandLineParser.hpp
|
CommandLineParser.hpp
|
||||||
PiPedalException.hpp
|
PiPedalException.hpp
|
||||||
ConfigMain.cpp
|
ConfigMain.cpp
|
||||||
|
PiPedalConfiguration.hpp PiPedalConfiguration.cpp
|
||||||
JackServerSettings.hpp JackServerSettings.cpp
|
JackServerSettings.hpp JackServerSettings.cpp
|
||||||
SystemConfigFile.hpp SystemConfigFile.cpp
|
SystemConfigFile.hpp SystemConfigFile.cpp
|
||||||
WifiChannelSelectors.cpp WifiChannelSelectors.hpp
|
WifiChannelSelectors.cpp WifiChannelSelectors.hpp
|
||||||
@@ -604,12 +615,22 @@ target_include_directories(capturepresets PRIVATE ${PIPEDAL_INCLUDES})
|
|||||||
target_link_libraries(capturepresets ${PIPEDAL_LIBS})
|
target_link_libraries(capturepresets ${PIPEDAL_LIBS})
|
||||||
|
|
||||||
|
|
||||||
|
add_executable(pipedal_update
|
||||||
|
UpdateMain.cpp
|
||||||
|
UpdateResults.cpp UpdateResults.hpp
|
||||||
|
Lv2Log.hpp Lv2Log.cpp
|
||||||
|
Lv2SystemdLogger.cpp Lv2SystemdLogger.hpp
|
||||||
|
UpdateResults.cpp UpdateResults.hpp
|
||||||
|
AdminInstallUpdate.cpp AdminInstallUpdate.hpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(pipedal_update PRIVATE stdc++fs systemd PiPedalCommon )
|
||||||
|
|
||||||
|
|
||||||
add_executable(pipedaladmind AdminMain.cpp CommandLineParser.hpp
|
add_executable(pipedaladmind AdminMain.cpp CommandLineParser.hpp
|
||||||
UnixSocket.cpp UnixSocket.hpp
|
UnixSocket.cpp UnixSocket.hpp
|
||||||
|
|
||||||
SetWifiConfig.cpp SetWifiConfig.hpp
|
SetWifiConfig.cpp SetWifiConfig.hpp
|
||||||
|
|
||||||
JackServerSettings.hpp JackServerSettings.cpp
|
JackServerSettings.hpp JackServerSettings.cpp
|
||||||
Lv2SystemdLogger.hpp Lv2SystemdLogger.cpp
|
Lv2SystemdLogger.hpp Lv2SystemdLogger.cpp
|
||||||
|
|
||||||
@@ -664,7 +685,7 @@ add_custom_target (
|
|||||||
install (TARGETS pipedalconfig pipedal_latency_test DESTINATION ${CMAKE_INSTALL_PREFIX}/bin
|
install (TARGETS pipedalconfig pipedal_latency_test DESTINATION ${CMAKE_INSTALL_PREFIX}/bin
|
||||||
EXPORT pipedalTargets)
|
EXPORT pipedalTargets)
|
||||||
|
|
||||||
install (TARGETS pipedald pipedaladmind DESTINATION ${CMAKE_INSTALL_PREFIX}/sbin
|
install (TARGETS pipedald pipedaladmind pipedal_update DESTINATION ${CMAKE_INSTALL_PREFIX}/sbin
|
||||||
EXPORT pipedalSbinTargets)
|
EXPORT pipedalSbinTargets)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+166
-126
@@ -38,6 +38,7 @@
|
|||||||
#include <random>
|
#include <random>
|
||||||
#include "AudioConfig.hpp"
|
#include "AudioConfig.hpp"
|
||||||
#include "WifiChannelSelectors.hpp"
|
#include "WifiChannelSelectors.hpp"
|
||||||
|
#include "PiPedalConfiguration.hpp"
|
||||||
#include <grp.h>
|
#include <grp.h>
|
||||||
|
|
||||||
#if JACK_HOST
|
#if JACK_HOST
|
||||||
@@ -54,6 +55,7 @@
|
|||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace pipedal;
|
using namespace pipedal;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
#define SERVICE_ACCOUNT_NAME "pipedal_d"
|
#define SERVICE_ACCOUNT_NAME "pipedal_d"
|
||||||
#define SERVICE_GROUP_NAME "pipedal_d"
|
#define SERVICE_GROUP_NAME "pipedal_d"
|
||||||
@@ -80,12 +82,12 @@ using namespace pipedal;
|
|||||||
#define REMOVE_OLD_SERVICE 0 // Grandfathering: whether to remove the old shutdown service (now pipedaladmind)
|
#define REMOVE_OLD_SERVICE 0 // Grandfathering: whether to remove the old shutdown service (now pipedaladmind)
|
||||||
#define OLD_SHUTDOWN_SERVICE "pipedalshutdownd"
|
#define OLD_SHUTDOWN_SERVICE "pipedalshutdownd"
|
||||||
|
|
||||||
std::filesystem::path GetServiceFileName(const std::string &serviceName)
|
fs::path GetServiceFileName(const std::string &serviceName)
|
||||||
{
|
{
|
||||||
return std::filesystem::path(SERVICE_PATH) / (serviceName + ".service");
|
return fs::path(SERVICE_PATH) / (serviceName + ".service");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::filesystem::path findOnPath(const std::string &command)
|
fs::path findOnPath(const std::string &command)
|
||||||
{
|
{
|
||||||
std::string path = getenv("PATH");
|
std::string path = getenv("PATH");
|
||||||
std::vector<std::string> paths;
|
std::vector<std::string> paths;
|
||||||
@@ -98,8 +100,8 @@ std::filesystem::path findOnPath(const std::string &command)
|
|||||||
pos = path.length();
|
pos = path.length();
|
||||||
}
|
}
|
||||||
std::string thisPath = path.substr(t, pos - t);
|
std::string thisPath = path.substr(t, pos - t);
|
||||||
std::filesystem::path path = std::filesystem::path(thisPath) / command;
|
fs::path path = fs::path(thisPath) / command;
|
||||||
if (std::filesystem::exists(path))
|
if (fs::exists(path))
|
||||||
{
|
{
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
@@ -112,11 +114,11 @@ std::filesystem::path findOnPath(const std::string &command)
|
|||||||
|
|
||||||
void EnableService()
|
void EnableService()
|
||||||
{
|
{
|
||||||
if (sysExec(SYSTEMCTL_BIN " enable " PIPEDALD_SERVICE ".service") != EXIT_SUCCESS)
|
if (silentSysExec(SYSTEMCTL_BIN " enable " PIPEDALD_SERVICE ".service") != EXIT_SUCCESS)
|
||||||
{
|
{
|
||||||
cout << "Error: Failed to enable the " PIPEDALD_SERVICE " service.\n";
|
cout << "Error: Failed to enable the " PIPEDALD_SERVICE " service.\n";
|
||||||
}
|
}
|
||||||
if (sysExec(SYSTEMCTL_BIN " enable " ADMIN_SERVICE ".service") != EXIT_SUCCESS)
|
if (silentSysExec(SYSTEMCTL_BIN " enable " ADMIN_SERVICE ".service") != EXIT_SUCCESS)
|
||||||
{
|
{
|
||||||
cout << "Error: Failed to enable the " ADMIN_SERVICE " service.\n";
|
cout << "Error: Failed to enable the " ADMIN_SERVICE " service.\n";
|
||||||
}
|
}
|
||||||
@@ -178,8 +180,10 @@ void StopService(bool excludeShutdownService = false)
|
|||||||
|
|
||||||
void StartService(bool excludeShutdownService = false)
|
void StartService(bool excludeShutdownService = false)
|
||||||
{
|
{
|
||||||
|
if (!UsingNetworkManager())
|
||||||
|
{
|
||||||
silentSysExec("/usr/bin/pulseaudio --kill"); // interferes with Jack audio service startup.
|
silentSysExec("/usr/bin/pulseaudio --kill"); // interferes with Jack audio service startup.
|
||||||
|
}
|
||||||
if (!excludeShutdownService)
|
if (!excludeShutdownService)
|
||||||
{
|
{
|
||||||
if (sysExec(SYSTEMCTL_BIN " start " ADMIN_SERVICE ".service") != EXIT_SUCCESS)
|
if (sysExec(SYSTEMCTL_BIN " start " ADMIN_SERVICE ".service") != EXIT_SUCCESS)
|
||||||
@@ -187,7 +191,7 @@ void StartService(bool excludeShutdownService = false)
|
|||||||
throw std::runtime_error("Failed to start the " ADMIN_SERVICE " service.");
|
throw std::runtime_error("Failed to start the " ADMIN_SERVICE " service.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sysExec(SYSTEMCTL_BIN " start " PIPEDALD_SERVICE ".service") != EXIT_SUCCESS)
|
if (silentSysExec(SYSTEMCTL_BIN " start " PIPEDALD_SERVICE ".service") != EXIT_SUCCESS)
|
||||||
{
|
{
|
||||||
throw std::runtime_error("Failed to start the " PIPEDALD_SERVICE " service.");
|
throw std::runtime_error("Failed to start the " PIPEDALD_SERVICE " service.");
|
||||||
}
|
}
|
||||||
@@ -242,7 +246,7 @@ static void RemoveLine(const std::string &path, const std::string lineToRemove)
|
|||||||
std::vector<std::string> lines;
|
std::vector<std::string> lines;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (std::filesystem::exists(path))
|
if (fs::exists(path))
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
ifstream f(path);
|
ifstream f(path);
|
||||||
@@ -296,10 +300,10 @@ void InstallPamEnv()
|
|||||||
#if INSTALL_JACK_SERVICE
|
#if INSTALL_JACK_SERVICE
|
||||||
std::string newLine = PAM_LINE;
|
std::string newLine = PAM_LINE;
|
||||||
std::vector<std::string> lines;
|
std::vector<std::string> lines;
|
||||||
std::filesystem::path path = "/etc/security/pam_env.conf";
|
fs::path path = "/etc/security/pam_env.conf";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (std::filesystem::exists(path))
|
if (fs::exists(path))
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
ifstream f(path);
|
ifstream f(path);
|
||||||
@@ -353,9 +357,9 @@ void InstallLimits()
|
|||||||
throw std::runtime_error("Failed to create audio service group.");
|
throw std::runtime_error("Failed to create audio service group.");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::filesystem::path limitsConfig = "/etc/security/limits.d/audio.conf";
|
fs::path limitsConfig = "/etc/security/limits.d/audio.conf";
|
||||||
|
|
||||||
if (!std::filesystem::exists(limitsConfig))
|
if (!fs::exists(limitsConfig))
|
||||||
{
|
{
|
||||||
ofstream output(limitsConfig);
|
ofstream output(limitsConfig);
|
||||||
output << "# Realtime priority group used by pipedal audio (and also by jack)"
|
output << "# Realtime priority group used by pipedal audio (and also by jack)"
|
||||||
@@ -370,9 +374,9 @@ void InstallLimits()
|
|||||||
#if JACK_HOST
|
#if JACK_HOST
|
||||||
void MaybeStartJackService()
|
void MaybeStartJackService()
|
||||||
{
|
{
|
||||||
std::filesystem::path drcFile = "/etc/jackdrc";
|
fs::path drcFile = "/etc/jackdrc";
|
||||||
|
|
||||||
if (std::filesystem::exists(drcFile) && std::filesystem::file_size(drcFile) != 0)
|
if (fs::exists(drcFile) && fs::file_size(drcFile) != 0)
|
||||||
{
|
{
|
||||||
sysExec(SYSTEMCTL_BIN " start jack");
|
sysExec(SYSTEMCTL_BIN " start jack");
|
||||||
}
|
}
|
||||||
@@ -464,23 +468,7 @@ void Uninstall()
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
try
|
OnWifiUninstall(true);
|
||||||
{
|
|
||||||
if (IsP2pServiceEnabled())
|
|
||||||
{
|
|
||||||
WifiDirectConfigSettings settings;
|
|
||||||
settings.Load();
|
|
||||||
settings.enable_ = false;
|
|
||||||
settings.Save();
|
|
||||||
SetWifiDirectConfig(settings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (const std::exception e)
|
|
||||||
{
|
|
||||||
std::cout << "Error: Unable to disable the Wiifi P2P service. " << e.what() << std::endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
OnWifiUninstall();
|
|
||||||
|
|
||||||
StopService();
|
StopService();
|
||||||
DisableService();
|
DisableService();
|
||||||
@@ -492,7 +480,7 @@ void Uninstall()
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
std::filesystem::remove("/usr/bin/systemd/system/" OLD_SHUTDOWN_SERVICE ".service");
|
fs::remove("/usr/bin/systemd/system/" OLD_SHUTDOWN_SERVICE ".service");
|
||||||
}
|
}
|
||||||
catch (...)
|
catch (...)
|
||||||
{
|
{
|
||||||
@@ -500,21 +488,21 @@ void Uninstall()
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
std::filesystem::remove("/usr/bin/systemd/system/" ADMIN_SERVICE ".service");
|
fs::remove("/usr/bin/systemd/system/" ADMIN_SERVICE ".service");
|
||||||
}
|
}
|
||||||
catch (...)
|
catch (...)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
std::filesystem::remove("/usr/bin/systemd/system/" PIPEDAL_NM_P2PD_SERVICE ".service");
|
fs::remove("/usr/bin/systemd/system/" PIPEDAL_NM_P2PD_SERVICE ".service");
|
||||||
}
|
}
|
||||||
catch (...)
|
catch (...)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
std::filesystem::remove("/usr/bin/systemd/system/" PIPEDAL_P2PD_SERVICE ".service");
|
fs::remove("/usr/bin/systemd/system/" PIPEDAL_P2PD_SERVICE ".service");
|
||||||
}
|
}
|
||||||
catch (...)
|
catch (...)
|
||||||
{
|
{
|
||||||
@@ -522,7 +510,7 @@ void Uninstall()
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
std::filesystem::remove("/usr/bin/systemd/system/" PIPEDALD_SERVICE ".service");
|
fs::remove("/usr/bin/systemd/system/" PIPEDALD_SERVICE ".service");
|
||||||
}
|
}
|
||||||
catch (...)
|
catch (...)
|
||||||
{
|
{
|
||||||
@@ -530,7 +518,7 @@ void Uninstall()
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
std::filesystem::remove("/usr/bin/systemd/system/" JACK_SERVICE ".service");
|
fs::remove("/usr/bin/systemd/system/" JACK_SERVICE ".service");
|
||||||
}
|
}
|
||||||
catch (...)
|
catch (...)
|
||||||
{
|
{
|
||||||
@@ -594,51 +582,75 @@ static std::string RandomChars(int nchars)
|
|||||||
return s.str();
|
return s.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static std::map<fs::path, fs::perms> sPermissionExceptions({{"/var/pipedal/config/NetworkManagerP2P.json",
|
||||||
|
fs::perms::owner_read | fs::perms::owner_write | fs::perms::group_read | fs::perms::group_write
|
||||||
|
|
||||||
|
}});
|
||||||
|
|
||||||
void SetVarPermissions(
|
void SetVarPermissions(
|
||||||
const std::filesystem::path &path,
|
const fs::path &path,
|
||||||
std::filesystem::perms directoryPermissions,
|
fs::perms directoryPermissions,
|
||||||
std::filesystem::perms filePermissions,
|
fs::perms filePermissions,
|
||||||
uid_t uid, gid_t gid)
|
uid_t uid, gid_t gid)
|
||||||
{
|
{
|
||||||
namespace fs = std::filesystem;
|
|
||||||
using namespace std::filesystem;
|
using namespace std::filesystem;
|
||||||
try {
|
try
|
||||||
if (fs::exists(path)) {
|
{
|
||||||
|
if (fs::exists(path))
|
||||||
|
{
|
||||||
std::ignore = chown(path.c_str(), uid, gid);
|
std::ignore = chown(path.c_str(), uid, gid);
|
||||||
if (fs::is_directory(path)) {
|
if (fs::is_directory(path))
|
||||||
|
{
|
||||||
fs::permissions(path, directoryPermissions, fs::perm_options::replace);
|
fs::permissions(path, directoryPermissions, fs::perm_options::replace);
|
||||||
for (const auto& entry : fs::recursive_directory_iterator(path)) {
|
for (const auto &entry : fs::recursive_directory_iterator(path))
|
||||||
|
{
|
||||||
|
|
||||||
if (chown(entry.path().c_str(), uid, gid) != 0)
|
if (chown(entry.path().c_str(), uid, gid) != 0)
|
||||||
{
|
{
|
||||||
std::cout << "Error: failed to set ownership of file " << entry.path() << std::endl;
|
std::cout << "Error: failed to set ownership of file " << entry.path() << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
if (fs::is_directory(entry.path()))
|
if (fs::is_directory(entry.path()))
|
||||||
{
|
{
|
||||||
fs::permissions(entry.path(), directoryPermissions, fs::perm_options::replace);
|
fs::permissions(entry.path(), directoryPermissions, fs::perm_options::replace);
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
fs::permissions(entry.path(), filePermissions, fs::perm_options::replace);
|
fs::permissions(entry.path(), filePermissions, fs::perm_options::replace);
|
||||||
}
|
}
|
||||||
} catch (const std::exception &e)
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
{
|
{
|
||||||
std::cout << "Error: failed to set permissions on file " << entry.path() << std::endl;
|
std::cout << "Error: failed to set permissions on file " << entry.path() << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (sPermissionExceptions.contains(path))
|
||||||
|
{
|
||||||
|
fs::permissions(path, sPermissionExceptions[path], fs::perm_options::replace);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
fs::permissions(path, filePermissions, fs::perm_options::replace);
|
fs::permissions(path, filePermissions, fs::perm_options::replace);
|
||||||
|
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
std::cout << "Error: Path does not exist: " << path << std::endl;
|
std::cout << "Error: Path does not exist: " << path << std::endl;
|
||||||
}
|
}
|
||||||
} catch (const std::filesystem::filesystem_error& e) {
|
}
|
||||||
|
catch (const fs::filesystem_error &e)
|
||||||
|
{
|
||||||
std::cerr << "Error: " << e.what() << std::endl;
|
std::cerr << "Error: " << e.what() << std::endl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
static void FixPermissions()
|
static void FixPermissions()
|
||||||
{
|
{
|
||||||
@@ -673,8 +685,9 @@ static void FixPermissions()
|
|||||||
fs::perms::owner_read | fs::perms::owner_write |
|
fs::perms::owner_read | fs::perms::owner_write |
|
||||||
fs::perms::group_read | fs::perms::group_write;
|
fs::perms::group_read | fs::perms::group_write;
|
||||||
|
|
||||||
std::filesystem::path wpa_config_path{ "/etc/pipedal/config/wpa_supplicant/wpa_supplicant-pipedal.conf"};
|
fs::path wpa_config_path{"/etc/pipedal/config/wpa_supplicant/wpa_supplicant-pipedal.conf"};
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
fs::permissions(wpa_config_path, wpa_supplicant_perms, fs::perm_options::replace);
|
fs::permissions(wpa_config_path, wpa_supplicant_perms, fs::perm_options::replace);
|
||||||
|
|
||||||
struct group *grp = getgrnam("netdev");
|
struct group *grp = getgrnam("netdev");
|
||||||
@@ -685,7 +698,8 @@ static void FixPermissions()
|
|||||||
cout << "Error: Failed to change ownership of " << wpa_config_path << endl;
|
cout << "Error: Failed to change ownership of " << wpa_config_path << endl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (const std::exception&e)
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
{
|
{
|
||||||
cout << "Error: Failed to set permissions on " << wpa_config_path << ". " << e.what() << endl;
|
cout << "Error: Failed to set permissions on " << wpa_config_path << ". " << e.what() << endl;
|
||||||
}
|
}
|
||||||
@@ -717,24 +731,77 @@ static void PrepareServiceConfigurationFile(uint16_t portNumber)
|
|||||||
|
|
||||||
void CopyWpaSupplicantConfigFile()
|
void CopyWpaSupplicantConfigFile()
|
||||||
{
|
{
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
const char NM_SUPPLICANT_CONFIG_PATH[] = "/etc/pipedal/config/wpa_supplicant/wpa_supplicant-pipedal.conf";
|
const char NM_SUPPLICANT_CONFIG_PATH[] = "/etc/pipedal/config/wpa_supplicant/wpa_supplicant-pipedal.conf";
|
||||||
const char NM_SUPPLICANT_CONFIG_SOURCE_PATH[] = "/etc/pipedal/config/templates/wpa_supplicant-pipedal.conf";
|
const char NM_SUPPLICANT_CONFIG_SOURCE_PATH[] = "/etc/pipedal/config/templates/wpa_supplicant-pipedal.conf";
|
||||||
std::filesystem::path destinationPath = NM_SUPPLICANT_CONFIG_PATH;
|
fs::path destinationPath = NM_SUPPLICANT_CONFIG_PATH;
|
||||||
std::filesystem::path sourcePath = NM_SUPPLICANT_CONFIG_SOURCE_PATH;
|
fs::path sourcePath = NM_SUPPLICANT_CONFIG_SOURCE_PATH;
|
||||||
|
|
||||||
std::filesystem::create_directories(destinationPath.parent_path());
|
fs::create_directories(destinationPath.parent_path());
|
||||||
std::filesystem::copy_file(sourcePath,destinationPath,std::filesystem::copy_options::overwrite_existing);
|
fs::copy_file(sourcePath, destinationPath, fs::copy_options::overwrite_existing);
|
||||||
} catch (const std::exception&e)
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
{
|
{
|
||||||
cout << "ERROR: " << e.what() << endl;
|
cout << "ERROR: " << e.what() << endl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void Install(const std::filesystem::path &programPrefix, const std::string endpointAddress)
|
|
||||||
|
void DeployVarConfig()
|
||||||
|
{
|
||||||
|
fs::path varConfig = "/var/pipedal/config/config.json";
|
||||||
|
if (!fs::exists(varConfig))
|
||||||
|
{
|
||||||
|
auto directory = varConfig.parent_path();
|
||||||
|
fs::create_directories(directory);
|
||||||
|
fs::path templateFile = "/etc/pipedal/config/templates/var_config.json";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fs::copy(templateFile, varConfig);
|
||||||
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
std::cout << "Error: Failed to create " << varConfig << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InstallPgpKey()
|
||||||
|
{
|
||||||
|
fs::path homeDir = "/var/pipedal/config/gpg";
|
||||||
|
fs::path keyPath = "/etc/pipedal/config/updatekey.gpg";
|
||||||
|
|
||||||
|
fs::create_directories(homeDir);
|
||||||
|
std::ignore = chmod(homeDir.c_str(), 0700);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::stringstream s;
|
||||||
|
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::stringstream ss;
|
||||||
|
ss << (CHOWN_BIN " -R " SERVICE_GROUP_NAME ":" SERVICE_GROUP_NAME " ") << homeDir.c_str();
|
||||||
|
std::string cmd = ss.str();
|
||||||
|
sysExec(cmd.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void Install(const fs::path &programPrefix, const std::string endpointAddress)
|
||||||
{
|
{
|
||||||
cout << "Configuring pipedal" << endl;
|
cout << "Configuring pipedal" << endl;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
DeployVarConfig();
|
||||||
|
|
||||||
if (sysExec(GROUPADD_BIN " -f " AUDIO_SERVICE_GROUP_NAME) != EXIT_SUCCESS)
|
if (sysExec(GROUPADD_BIN " -f " AUDIO_SERVICE_GROUP_NAME) != EXIT_SUCCESS)
|
||||||
{
|
{
|
||||||
throw std::runtime_error("Failed to create audio service group.");
|
throw std::runtime_error("Failed to create audio service group.");
|
||||||
@@ -810,8 +877,8 @@ void Install(const std::filesystem::path &programPrefix, const std::string endpo
|
|||||||
|
|
||||||
// create and configure /var directory.
|
// create and configure /var directory.
|
||||||
|
|
||||||
std::filesystem::path varDirectory("/var/pipedal");
|
fs::path varDirectory("/var/pipedal");
|
||||||
std::filesystem::create_directory(varDirectory);
|
fs::create_directory(varDirectory);
|
||||||
|
|
||||||
{
|
{
|
||||||
std::stringstream s;
|
std::stringstream s;
|
||||||
@@ -839,8 +906,8 @@ void Install(const std::filesystem::path &programPrefix, const std::string endpo
|
|||||||
|
|
||||||
if (authBindRequired)
|
if (authBindRequired)
|
||||||
{
|
{
|
||||||
std::filesystem::create_directories("/etc/authbind/byport");
|
fs::create_directories("/etc/authbind/byport");
|
||||||
std::filesystem::path portAuthFile = std::filesystem::path("/etc/authbind/byport") / strPort;
|
fs::path portAuthFile = fs::path("/etc/authbind/byport") / strPort;
|
||||||
|
|
||||||
{
|
{
|
||||||
// create it.
|
// create it.
|
||||||
@@ -928,56 +995,28 @@ void Install(const std::filesystem::path &programPrefix, const std::string endpo
|
|||||||
FixPermissions();
|
FixPermissions();
|
||||||
RestartService(false);
|
RestartService(false);
|
||||||
EnableService();
|
EnableService();
|
||||||
|
|
||||||
|
// Restart WiFi Direct if neccessary.
|
||||||
|
OnWifiReinstall();
|
||||||
|
InstallPgpKey();
|
||||||
}
|
}
|
||||||
catch (const std::exception &e)
|
catch (const std::exception &e)
|
||||||
{
|
{
|
||||||
// don't allow abnormal termination, which leaves the package in a state that's
|
// don't allow abnormal termination, which leaves the package in a state that's
|
||||||
// difficult to uninstall.
|
// difficult to uninstall.
|
||||||
cout << "Error: " << e.what();
|
cout << "Error: " << e.what();
|
||||||
|
cout << " Run 'pipedalconfig --install' again to complete setup of PiPedal." << endl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::string GetCurrentWebServicePort()
|
static std::string GetCurrentWebServicePort()
|
||||||
{
|
{
|
||||||
std::filesystem::path servicePath = GetServiceFileName(PIPEDALD_SERVICE);
|
PiPedalConfiguration config;
|
||||||
try
|
config.Load("/etc/pipedal/config", "");
|
||||||
{
|
|
||||||
if (std::filesystem::exists(servicePath))
|
std::ostringstream ss;
|
||||||
{
|
ss << config.GetSocketServerPort();
|
||||||
{
|
return ss.str();
|
||||||
ifstream f(servicePath);
|
|
||||||
if (!f.is_open())
|
|
||||||
{
|
|
||||||
throw std::runtime_error(SS("Can't open " << servicePath));
|
|
||||||
}
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
std::string line;
|
|
||||||
if (f.eof())
|
|
||||||
break;
|
|
||||||
std::getline(f, line);
|
|
||||||
if (line.starts_with("ExecStart="))
|
|
||||||
{
|
|
||||||
auto startPos = line.find("-port ");
|
|
||||||
if (startPos != std::string::npos)
|
|
||||||
{
|
|
||||||
startPos = startPos + 6;
|
|
||||||
auto endPos = line.find(" -systemd");
|
|
||||||
if (endPos != std::string::npos)
|
|
||||||
{
|
|
||||||
std::string result = line.substr(startPos, endPos - startPos);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (const std::exception & /*ignored*/)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void PrintHelp()
|
static void PrintHelp()
|
||||||
@@ -1071,7 +1110,6 @@ static void PrintHelp()
|
|||||||
<< HangingIndent() << " --list-p2p-channels [<country_code>] \tList valid p2p channels for the current/specified country."
|
<< HangingIndent() << " --list-p2p-channels [<country_code>] \tList valid p2p channels for the current/specified country."
|
||||||
<< "\n\n"
|
<< "\n\n"
|
||||||
|
|
||||||
|
|
||||||
// << HangingIndent() << " --enable-legacy-ap\t <country_code> <ssid> <wep_password> <channel>\tEnable a legacy Wi-Fi access point."
|
// << HangingIndent() << " --enable-legacy-ap\t <country_code> <ssid> <wep_password> <channel>\tEnable a legacy Wi-Fi access point."
|
||||||
// << "\n\n"
|
// << "\n\n"
|
||||||
// << "Enable a legacy Wi-Fi access point. \n\n"
|
// << "Enable a legacy Wi-Fi access point. \n\n"
|
||||||
@@ -1124,15 +1162,19 @@ static void PrintHelp()
|
|||||||
|
|
||||||
static int ListP2PChannels(const std::vector<std::string> &arguments)
|
static int ListP2PChannels(const std::vector<std::string> &arguments)
|
||||||
{
|
{
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
std::string country;
|
std::string country;
|
||||||
if (arguments.size() >= 2)
|
if (arguments.size() >= 2)
|
||||||
{
|
{
|
||||||
throw std::runtime_error("Invalid arguments.");
|
throw std::runtime_error("Invalid arguments.");
|
||||||
} if (arguments.size() == 1)
|
}
|
||||||
|
if (arguments.size() == 1)
|
||||||
{
|
{
|
||||||
country = arguments[0];
|
country = arguments[0];
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// use the currently selected country by default.
|
// use the currently selected country by default.
|
||||||
WifiDirectConfigSettings settings;
|
WifiDirectConfigSettings settings;
|
||||||
settings.Load();
|
settings.Load();
|
||||||
@@ -1147,13 +1189,13 @@ static int ListP2PChannels(const std::vector<std::string>&arguments)
|
|||||||
{
|
{
|
||||||
cout << channelSelector.channelName_ << endl;
|
cout << channelSelector.channelName_ << endl;
|
||||||
}
|
}
|
||||||
} catch (const std::exception &e)
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
{
|
{
|
||||||
cout << "ERROR: " << e.what() << endl;
|
cout << "ERROR: " << e.what() << endl;
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
return EXIT_SUCCESS;
|
return EXIT_SUCCESS;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
@@ -1194,7 +1236,6 @@ int main(int argc, char **argv)
|
|||||||
parser.AddOption("--list-p2p-channels", &list_p2p_channels);
|
parser.AddOption("--list-p2p-channels", &list_p2p_channels);
|
||||||
parser.AddOption("--fix-permissions", &fix_permissions);
|
parser.AddOption("--fix-permissions", &fix_permissions);
|
||||||
|
|
||||||
|
|
||||||
parser.AddOption("--get-current-port", &get_current_port); // private. For debug use only.
|
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("--excludeShutdownService", &excludeShutdownService); // private (unstable) option used by shutdown service.
|
||||||
@@ -1203,10 +1244,7 @@ int main(int argc, char **argv)
|
|||||||
parser.Parse(argc, (const char **)argv);
|
parser.Parse(argc, (const char **)argv);
|
||||||
|
|
||||||
int actionCount =
|
int actionCount =
|
||||||
help + get_current_port + install + uninstall + stop + start + enable + disable
|
help + get_current_port + install + uninstall + stop + start + enable + disable + enable_ap + disable_ap + restart + enable_p2p + disable_p2p + list_p2p_channels + fix_permissions;
|
||||||
+ enable_ap + disable_ap + restart + enable_p2p + disable_p2p
|
|
||||||
+ list_p2p_channels + fix_permissions
|
|
||||||
;
|
|
||||||
if (actionCount > 1)
|
if (actionCount > 1)
|
||||||
{
|
{
|
||||||
throw std::runtime_error("Please provide only one action.");
|
throw std::runtime_error("Please provide only one action.");
|
||||||
@@ -1277,17 +1315,17 @@ int main(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
if (install)
|
if (install)
|
||||||
{
|
{
|
||||||
std::filesystem::path prefix;
|
fs::path prefix;
|
||||||
if (prefixOption.length() != 0)
|
if (prefixOption.length() != 0)
|
||||||
{
|
{
|
||||||
prefix = std::filesystem::path(prefixOption);
|
prefix = fs::path(prefixOption);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
prefix = std::filesystem::path(argv[0]).parent_path().parent_path();
|
prefix = fs::path(argv[0]).parent_path().parent_path();
|
||||||
|
|
||||||
std::filesystem::path pipedalPath = prefix / "sbin" / "pipedald";
|
fs::path pipedalPath = prefix / "sbin" / "pipedald";
|
||||||
if (!std::filesystem::exists(pipedalPath))
|
if (!fs::exists(pipedalPath))
|
||||||
{
|
{
|
||||||
std::stringstream s;
|
std::stringstream s;
|
||||||
s << "Can't find pipedald executable at " << pipedalPath << ". Try again using the -prefix option.";
|
s << "Can't find pipedald executable at " << pipedalPath << ". Try again using the -prefix option.";
|
||||||
@@ -1335,7 +1373,8 @@ int main(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
else if (enable_p2p)
|
else if (enable_p2p)
|
||||||
{
|
{
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
auto argv = parser.Arguments();
|
auto argv = parser.Arguments();
|
||||||
WifiDirectConfigSettings settings;
|
WifiDirectConfigSettings settings;
|
||||||
settings.ParseArguments(argv);
|
settings.ParseArguments(argv);
|
||||||
@@ -1343,7 +1382,8 @@ int main(int argc, char **argv)
|
|||||||
settings.enable_ = true;
|
settings.enable_ = true;
|
||||||
SetWifiDirectConfig(settings);
|
SetWifiDirectConfig(settings);
|
||||||
RestartService(true); // also have to retart web service so that it gets the correct device name.
|
RestartService(true); // also have to retart web service so that it gets the correct device name.
|
||||||
} catch (const std::exception&e)
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
{
|
{
|
||||||
cout << "ERROR: " << e.what() << endl;
|
cout << "ERROR: " << e.what() << endl;
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
|
|||||||
@@ -0,0 +1,564 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
* 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 "pch.h"
|
||||||
|
#include "util.hpp"
|
||||||
|
#include <bit>
|
||||||
|
#include <memory>
|
||||||
|
#include "ss.hpp"
|
||||||
|
#include "DummyAudioDriver.hpp"
|
||||||
|
#include "JackServerSettings.hpp"
|
||||||
|
#include <thread>
|
||||||
|
#include "RtInversionGuard.hpp"
|
||||||
|
#include "PiPedalException.hpp"
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include "CpuUse.hpp"
|
||||||
|
|
||||||
|
#include <alsa/asoundlib.h>
|
||||||
|
|
||||||
|
#include "Lv2Log.hpp"
|
||||||
|
#include <limits>
|
||||||
|
#include "ss.hpp"
|
||||||
|
|
||||||
|
#undef ALSADRIVER_CONFIG_DBG
|
||||||
|
|
||||||
|
#ifdef ALSADRIVER_CONFIG_DBG
|
||||||
|
#include <stdio.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using namespace pipedal;
|
||||||
|
|
||||||
|
namespace pipedal
|
||||||
|
{
|
||||||
|
|
||||||
|
[[noreturn]] static void DummyError(const std::string &message)
|
||||||
|
{
|
||||||
|
throw PiPedalStateException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DummyDriverImpl : public AudioDriver
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
pipedal::CpuUse cpuUse;
|
||||||
|
|
||||||
|
uint32_t sampleRate = 0;
|
||||||
|
|
||||||
|
uint32_t bufferSize;
|
||||||
|
uint32_t numberOfBuffers;
|
||||||
|
|
||||||
|
int playbackChannels = 2;
|
||||||
|
int captureChannels = 2;
|
||||||
|
|
||||||
|
|
||||||
|
uint32_t playbackSampleSize = 0;
|
||||||
|
uint32_t captureSampleSize = 0;
|
||||||
|
uint32_t playbackFrameSize = 0;
|
||||||
|
uint32_t captureFrameSize = 0;
|
||||||
|
|
||||||
|
|
||||||
|
std::vector<float *> activeCaptureBuffers;
|
||||||
|
std::vector<float *> activePlaybackBuffers;
|
||||||
|
|
||||||
|
std::vector<float *> captureBuffers;
|
||||||
|
std::vector<float *> playbackBuffers;
|
||||||
|
|
||||||
|
uint8_t *rawCaptureBuffer = nullptr;
|
||||||
|
uint8_t *rawPlaybackBuffer = nullptr;
|
||||||
|
|
||||||
|
AudioDriverHost *driverHost = nullptr;
|
||||||
|
|
||||||
|
public:
|
||||||
|
DummyDriverImpl(AudioDriverHost *driverHost)
|
||||||
|
: driverHost(driverHost)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
virtual ~DummyDriverImpl()
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void OnShutdown()
|
||||||
|
{
|
||||||
|
Lv2Log::info("Dummy Audio Server has shut down.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual uint32_t GetSampleRate()
|
||||||
|
{
|
||||||
|
return this->sampleRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
JackServerSettings jackServerSettings;
|
||||||
|
|
||||||
|
unsigned int periods = 0;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
std::atomic<bool> terminateAudio_ = false;
|
||||||
|
|
||||||
|
void terminateAudio(bool terminate)
|
||||||
|
{
|
||||||
|
this->terminateAudio_ = terminate;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool terminateAudio()
|
||||||
|
{
|
||||||
|
return this->terminateAudio_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void DummyCleanup()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void AllocateBuffers(std::vector<float *> &buffers, size_t n)
|
||||||
|
{
|
||||||
|
buffers.resize(n);
|
||||||
|
for (size_t i = 0; i < n; ++i)
|
||||||
|
{
|
||||||
|
buffers[i] = new float[this->bufferSize];
|
||||||
|
for (size_t j = 0; j < this->bufferSize; ++j)
|
||||||
|
{
|
||||||
|
buffers[i][j] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JackChannelSelection channelSelection;
|
||||||
|
bool open = false;
|
||||||
|
virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection)
|
||||||
|
{
|
||||||
|
terminateAudio_ = false;
|
||||||
|
if (open)
|
||||||
|
{
|
||||||
|
throw PiPedalStateException("Already open.");
|
||||||
|
}
|
||||||
|
this->jackServerSettings = jackServerSettings;
|
||||||
|
this->channelSelection = channelSelection;
|
||||||
|
this->sampleRate = jackServerSettings.GetSampleRate();
|
||||||
|
|
||||||
|
open = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
OpenMidi(jackServerSettings, channelSelection);
|
||||||
|
OpenAudio(jackServerSettings, channelSelection);
|
||||||
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
virtual std::string GetConfigurationDescription()
|
||||||
|
{
|
||||||
|
std::string result = SS(
|
||||||
|
"DUMMY, "
|
||||||
|
<< "n/a"
|
||||||
|
<< ", " << "Native float"
|
||||||
|
<< ", " << this->sampleRate
|
||||||
|
<< ", " << this->bufferSize << "x" << this->numberOfBuffers
|
||||||
|
<< ", in: " << this->InputBufferCount() << "/" << this->captureChannels
|
||||||
|
<< ", out: " << this->OutputBufferCount() << "/" << this->playbackChannels);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenAudio(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection)
|
||||||
|
{
|
||||||
|
int err;
|
||||||
|
|
||||||
|
this->numberOfBuffers = jackServerSettings.GetNumberOfBuffers();
|
||||||
|
this->bufferSize = jackServerSettings.GetBufferSize();
|
||||||
|
AllocateBuffers(captureBuffers, 2);
|
||||||
|
AllocateBuffers(playbackBuffers, 2);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
std::jthread *audioThread;
|
||||||
|
bool audioRunning;
|
||||||
|
|
||||||
|
bool block = false;
|
||||||
|
|
||||||
|
void AudioThread()
|
||||||
|
{
|
||||||
|
SetThreadName("dummyAudioDriver");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
#if defined(__WIN32)
|
||||||
|
// bump thread prioriy two levels to
|
||||||
|
// ensure that the service thread doesn't
|
||||||
|
// get bogged down by UIwork. Doesn't have to be realtime, but it
|
||||||
|
// MUST run at higher priority than UI threads.
|
||||||
|
xxx; // TO DO.
|
||||||
|
#elif defined(__linux__)
|
||||||
|
int min = sched_get_priority_min(SCHED_RR);
|
||||||
|
int max = sched_get_priority_max(SCHED_RR);
|
||||||
|
|
||||||
|
struct sched_param param;
|
||||||
|
memset(¶m, 0, sizeof(param));
|
||||||
|
param.sched_priority = RT_THREAD_PRIORITY;
|
||||||
|
|
||||||
|
int result = sched_setscheduler(0, SCHED_RR, ¶m);
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
Lv2Log::debug("Service thread priority successfully boosted.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Lv2Log::error(SS("Failed to set ALSA AudioThread priority. (" << strerror(result) << ")"));
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
xxx; // TODO for your platform.
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool ok = true;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (terminateAudio())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t framesRead = this->bufferSize;
|
||||||
|
this->driverHost->OnProcess(framesRead);
|
||||||
|
|
||||||
|
/// no attempt at realtime. Just as long as we run occasionally.
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
Lv2Log::error(e.what());
|
||||||
|
Lv2Log::error("Dummy audio thread terminated abnormally.");
|
||||||
|
}
|
||||||
|
this->driverHost->OnAudioTerminated();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool alsaActive = false;
|
||||||
|
|
||||||
|
static int IndexFromPortName(const std::string &s)
|
||||||
|
{
|
||||||
|
auto pos = s.find_last_of('_');
|
||||||
|
if (pos == std::string::npos)
|
||||||
|
{
|
||||||
|
throw std::invalid_argument("Bad port name.");
|
||||||
|
}
|
||||||
|
const char *p = s.c_str() + (pos + 1);
|
||||||
|
|
||||||
|
int v = atoi(p);
|
||||||
|
if (v < 0)
|
||||||
|
{
|
||||||
|
throw std::invalid_argument("Bad port name.");
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool activated = false;
|
||||||
|
virtual void Activate()
|
||||||
|
{
|
||||||
|
if (activated)
|
||||||
|
{
|
||||||
|
throw PiPedalStateException("Already activated.");
|
||||||
|
}
|
||||||
|
activated = true;
|
||||||
|
|
||||||
|
this->activeCaptureBuffers.resize(channelSelection.GetInputAudioPorts().size());
|
||||||
|
|
||||||
|
playbackBuffers.resize(2);
|
||||||
|
|
||||||
|
int ix = 0;
|
||||||
|
for (auto &x : channelSelection.GetInputAudioPorts())
|
||||||
|
{
|
||||||
|
int sourceIndex = IndexFromPortName(x);
|
||||||
|
if (sourceIndex >= captureBuffers.size())
|
||||||
|
{
|
||||||
|
Lv2Log::error(SS("Invalid audio input port: " << x));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this->activeCaptureBuffers[ix++] = this->captureBuffers[sourceIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this->activePlaybackBuffers.resize(channelSelection.GetOutputAudioPorts().size());
|
||||||
|
|
||||||
|
ix = 0;
|
||||||
|
for (auto &x : channelSelection.GetOutputAudioPorts())
|
||||||
|
{
|
||||||
|
int sourceIndex = IndexFromPortName(x);
|
||||||
|
if (sourceIndex >= playbackBuffers.size())
|
||||||
|
{
|
||||||
|
Lv2Log::error(SS("Invalid audio output port: " << x));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this->activePlaybackBuffers[ix++] = this->playbackBuffers[sourceIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
audioThread = new std::jthread([this]()
|
||||||
|
{ AudioThread(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Deactivate()
|
||||||
|
{
|
||||||
|
if (!activated)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activated = false;
|
||||||
|
terminateAudio(true);
|
||||||
|
if (audioThread)
|
||||||
|
{
|
||||||
|
this->audioThread->join();
|
||||||
|
this->audioThread = 0;
|
||||||
|
}
|
||||||
|
Lv2Log::debug("Audio thread joined.");
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr size_t MIDI_BUFFER_SIZE = 16 * 1024;
|
||||||
|
static constexpr size_t MAX_MIDI_EVENT = 4 * 1024;
|
||||||
|
|
||||||
|
public:
|
||||||
|
class MidiState
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
snd_rawmidi_t *hIn = nullptr;
|
||||||
|
snd_rawmidi_params_t *hInParams = nullptr;
|
||||||
|
std::string deviceName;
|
||||||
|
|
||||||
|
// running status state.
|
||||||
|
uint8_t runningStatus = 0;
|
||||||
|
int dataLength = 0;
|
||||||
|
int dataIndex = 0;
|
||||||
|
size_t statusBytesRemaining = 0;
|
||||||
|
size_t data0;
|
||||||
|
size_t data1;
|
||||||
|
|
||||||
|
size_t eventCount = 0;
|
||||||
|
MidiEvent events[MAX_MIDI_EVENT];
|
||||||
|
size_t bufferCount = 0;
|
||||||
|
uint8_t buffer[MIDI_BUFFER_SIZE];
|
||||||
|
|
||||||
|
uint8_t readBuffer[1024];
|
||||||
|
|
||||||
|
ssize_t sysexStartIndex = -1;
|
||||||
|
|
||||||
|
void checkError(int result, const char *message)
|
||||||
|
{
|
||||||
|
if (result < 0)
|
||||||
|
{
|
||||||
|
throw PiPedalStateException(SS("Unexpected error: " << message << " (" << this->deviceName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
void Open(const AlsaMidiDeviceInfo &device)
|
||||||
|
{
|
||||||
|
bufferCount = 0;
|
||||||
|
eventCount = 0;
|
||||||
|
sysexStartIndex = -1;
|
||||||
|
runningStatus = 0;
|
||||||
|
dataIndex = 0;
|
||||||
|
dataLength = 0;
|
||||||
|
|
||||||
|
this->deviceName = device.description_;
|
||||||
|
|
||||||
|
}
|
||||||
|
void Close()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
size_t GetMidiInputEventCount()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GetMidiInputEvent(MidiEvent *event, size_t nFrame)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MidiPut(uint8_t cc, uint8_t d0, uint8_t d1)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void FillBuffer()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteBuffer(uint8_t *readBuffer, size_t nRead)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<MidiState *> midiStates;
|
||||||
|
|
||||||
|
void OpenMidi(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection)
|
||||||
|
{
|
||||||
|
const auto &devices = channelSelection.GetInputMidiDevices();
|
||||||
|
|
||||||
|
midiStates.resize(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual size_t MidiInputBufferCount() const
|
||||||
|
{
|
||||||
|
return this->midiStates.size();
|
||||||
|
}
|
||||||
|
virtual void *GetMidiInputBuffer(size_t channel, size_t nFrames)
|
||||||
|
{
|
||||||
|
return (void *)midiStates[channel];
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual size_t GetMidiInputEventCount(void *portBuffer)
|
||||||
|
{
|
||||||
|
MidiState *state = (MidiState *)portBuffer;
|
||||||
|
return state->GetMidiInputEventCount();
|
||||||
|
}
|
||||||
|
virtual bool GetMidiInputEvent(MidiEvent *event, void *portBuf, size_t nFrame)
|
||||||
|
{
|
||||||
|
MidiState *state = (MidiState *)portBuf;
|
||||||
|
return state->GetMidiInputEvent(event, nFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void FillMidiBuffers()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual size_t InputBufferCount() const { return activeCaptureBuffers.size(); }
|
||||||
|
virtual float *GetInputBuffer(size_t channel, size_t nFrames)
|
||||||
|
{
|
||||||
|
return activeCaptureBuffers[channel];
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual size_t OutputBufferCount() const { return activePlaybackBuffers.size(); }
|
||||||
|
virtual float *GetOutputBuffer(size_t channel, size_t nFrames)
|
||||||
|
{
|
||||||
|
return activePlaybackBuffers[channel];
|
||||||
|
}
|
||||||
|
|
||||||
|
void FreeBuffers(std::vector<float *> &buffer)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < buffer.size(); ++i)
|
||||||
|
{
|
||||||
|
// delete[] buffer[i];
|
||||||
|
buffer[i] = 0;
|
||||||
|
}
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
void DeleteBuffers()
|
||||||
|
{
|
||||||
|
activeCaptureBuffers.clear();
|
||||||
|
activePlaybackBuffers.clear();
|
||||||
|
FreeBuffers(this->playbackBuffers);
|
||||||
|
FreeBuffers(this->captureBuffers);
|
||||||
|
if (rawCaptureBuffer)
|
||||||
|
{
|
||||||
|
delete[] rawCaptureBuffer;
|
||||||
|
rawCaptureBuffer = nullptr;
|
||||||
|
}
|
||||||
|
if (rawPlaybackBuffer)
|
||||||
|
{
|
||||||
|
delete[] rawPlaybackBuffer;
|
||||||
|
rawPlaybackBuffer = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
virtual void Close()
|
||||||
|
{
|
||||||
|
if (!open)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
open = false;
|
||||||
|
Deactivate();
|
||||||
|
DummyCleanup();
|
||||||
|
DeleteBuffers();
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual float CpuUse()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual float CpuOverhead()
|
||||||
|
{
|
||||||
|
return 0.1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AudioDriver *CreateDummyAudioDriver(AudioDriverHost *driverHost)
|
||||||
|
{
|
||||||
|
return new DummyDriverImpl(driverHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GetDummyChannels(const JackServerSettings &jackServerSettings,
|
||||||
|
std::vector<std::string> &inputAudioPorts,
|
||||||
|
std::vector<std::string> &outputAudioPorts)
|
||||||
|
{
|
||||||
|
|
||||||
|
bool result = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
unsigned int playbackChannels = 2, captureChannels = 2;
|
||||||
|
|
||||||
|
inputAudioPorts.clear();
|
||||||
|
for (unsigned int i = 0; i < captureChannels; ++i)
|
||||||
|
{
|
||||||
|
inputAudioPorts.push_back(SS("system::capture_" << i));
|
||||||
|
}
|
||||||
|
|
||||||
|
outputAudioPorts.clear();
|
||||||
|
for (unsigned int i = 0; i < playbackChannels; ++i)
|
||||||
|
{
|
||||||
|
outputAudioPorts.push_back(SS("system::playback_" << i));
|
||||||
|
}
|
||||||
|
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2024 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
|
||||||
|
* 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 "AudioDriver.hpp"
|
||||||
|
#include "JackServerSettings.hpp"
|
||||||
|
|
||||||
|
namespace pipedal {
|
||||||
|
|
||||||
|
AudioDriver* CreateDummyAudioDriver(AudioDriverHost*driverHost);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -112,7 +112,12 @@ void JackConfiguration::AlsaInitialize(
|
|||||||
this->isValid_ = false;
|
this->isValid_ = false;
|
||||||
this->errorStatus_ = "";
|
this->errorStatus_ = "";
|
||||||
|
|
||||||
|
if (jackServerSettings.IsDummyAudioDevice())
|
||||||
|
{
|
||||||
|
this->inputMidiDevices_.clear();
|
||||||
|
} else {
|
||||||
this->inputMidiDevices_ = GetAlsaMidiInputDevices();
|
this->inputMidiDevices_ = GetAlsaMidiInputDevices();
|
||||||
|
}
|
||||||
if (jackServerSettings.IsValid())
|
if (jackServerSettings.IsValid())
|
||||||
{
|
{
|
||||||
this->blockLength_ = jackServerSettings.GetBufferSize();
|
this->blockLength_ = jackServerSettings.GetBufferSize();
|
||||||
@@ -126,7 +131,7 @@ void JackConfiguration::AlsaInitialize(
|
|||||||
}
|
}
|
||||||
} catch (const std::exception& /*ignore*/)
|
} catch (const std::exception& /*ignore*/)
|
||||||
{
|
{
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ namespace pipedal
|
|||||||
uint32_t GetBufferSize() const { return bufferSize_; }
|
uint32_t GetBufferSize() const { return bufferSize_; }
|
||||||
uint32_t GetNumberOfBuffers() const { return numberOfBuffers_; }
|
uint32_t GetNumberOfBuffers() const { return numberOfBuffers_; }
|
||||||
const std::string &GetAlsaInputDevice() const { return alsaDevice_; }
|
const std::string &GetAlsaInputDevice() const { return alsaDevice_; }
|
||||||
|
void UseDummyAudioDevice() {
|
||||||
|
this->valid_ = true;
|
||||||
|
if (sampleRate_ == 0) sampleRate_ = 48000;
|
||||||
|
this->alsaDevice_ = "__DUMMY_AUDIO__";
|
||||||
|
}
|
||||||
|
bool IsDummyAudioDevice() const {
|
||||||
|
return this->alsaDevice_ == "__DUMMY_AUDIO__";
|
||||||
|
}
|
||||||
|
|
||||||
void ReadJackDaemonConfiguration();
|
void ReadJackDaemonConfiguration();
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <sys/eventfd.h>
|
#include <sys/eventfd.h>
|
||||||
#include "PiPedalModel.hpp"
|
#include "PiPedalModel.hpp"
|
||||||
|
#include "util.hpp"
|
||||||
|
|
||||||
using namespace pipedal;
|
using namespace pipedal;
|
||||||
|
|
||||||
@@ -42,8 +43,11 @@ void Lv2PluginChangeMonitor::Shutdown()
|
|||||||
if (monitorThread)
|
if (monitorThread)
|
||||||
{
|
{
|
||||||
terminateThread = true;
|
terminateThread = true;
|
||||||
|
uint64_t val = 1;
|
||||||
|
write(shutdown_eventfd,(void*)&val, sizeof(val));
|
||||||
monitorThread->join();
|
monitorThread->join();
|
||||||
monitorThread = nullptr;
|
monitorThread = nullptr;
|
||||||
|
close(shutdown_eventfd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +58,7 @@ Lv2PluginChangeMonitor::~Lv2PluginChangeMonitor()
|
|||||||
|
|
||||||
void Lv2PluginChangeMonitor::ThreadProc()
|
void Lv2PluginChangeMonitor::ThreadProc()
|
||||||
{
|
{
|
||||||
|
SetThreadName("Lv2Change");
|
||||||
using clock = std::chrono::steady_clock;
|
using clock = std::chrono::steady_clock;
|
||||||
|
|
||||||
int inotify_fd = inotify_init();
|
int inotify_fd = inotify_init();
|
||||||
|
|||||||
@@ -345,6 +345,10 @@ public:
|
|||||||
|
|
||||||
virtual void OnAudioStopped()
|
virtual void OnAudioStopped()
|
||||||
{
|
{
|
||||||
|
}
|
||||||
|
virtual void OnAudioTerminated()
|
||||||
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
virtual void OnProcess(size_t nFrames)
|
virtual void OnProcess(size_t nFrames)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
// 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
|
// this software and associated documentation files (the "Software"), to deal in
|
||||||
@@ -20,9 +20,76 @@
|
|||||||
#include "pch.h"
|
#include "pch.h"
|
||||||
#include "PiPedalConfiguration.hpp"
|
#include "PiPedalConfiguration.hpp"
|
||||||
#include "json.hpp"
|
#include "json.hpp"
|
||||||
|
#include "ss.hpp"
|
||||||
|
#include "ServiceConfiguration.hpp"
|
||||||
|
|
||||||
using namespace pipedal;
|
using namespace pipedal;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
void PiPedalConfiguration::Load(const std::filesystem::path &path, const std::filesystem::path &webRoot)
|
||||||
|
{
|
||||||
|
docRoot_ = path;
|
||||||
|
webRoot_ = webRoot;
|
||||||
|
|
||||||
|
// load installer defaults.
|
||||||
|
{
|
||||||
|
std::filesystem::path configPath = path / "config.json";
|
||||||
|
std::ifstream f(configPath);
|
||||||
|
if (!f.is_open())
|
||||||
|
{
|
||||||
|
std::stringstream s;
|
||||||
|
s << "Unable to open " << configPath;
|
||||||
|
throw PiPedalStateException(s.str());
|
||||||
|
}
|
||||||
|
json_reader reader(f);
|
||||||
|
reader.read(this);
|
||||||
|
}
|
||||||
|
// web port comes from service.conf
|
||||||
|
ServiceConfiguration serviceConfiguration;
|
||||||
|
|
||||||
|
serviceConfiguration.Load(fs::path(this->local_storage_path_) / "config" / "service.conf");
|
||||||
|
this->socketServerAddress_ = SS("0.0.0.0:" << serviceConfiguration.server_port);
|
||||||
|
|
||||||
|
// load any user-made settings
|
||||||
|
{
|
||||||
|
std::filesystem::path userPath = std::filesystem::path(this->local_storage_path_) / "config" / "config.json";
|
||||||
|
if (std::filesystem::exists(userPath))
|
||||||
|
{
|
||||||
|
std::ifstream f(userPath);
|
||||||
|
json_reader reader(f);
|
||||||
|
reader.read(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t PiPedalConfiguration::GetSocketServerPort() const
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
size_t pos = this->socketServerAddress_.find_last_of(':');
|
||||||
|
if (pos == std::string::npos)
|
||||||
|
{
|
||||||
|
throw std::exception();
|
||||||
|
}
|
||||||
|
std::string strPort = socketServerAddress_.substr(pos + 1);
|
||||||
|
unsigned long port = std::stoul(strPort);
|
||||||
|
if (port == 0)
|
||||||
|
{
|
||||||
|
throw std::exception();
|
||||||
|
}
|
||||||
|
if (port > std::numeric_limits<uint16_t>::max())
|
||||||
|
{
|
||||||
|
throw std::exception();
|
||||||
|
}
|
||||||
|
return (uint16_t)port;
|
||||||
|
}
|
||||||
|
catch (const std::exception &)
|
||||||
|
{
|
||||||
|
std::stringstream s;
|
||||||
|
s << "Invalid port number in '" << this->socketServerAddress_ << "'.";
|
||||||
|
throw PiPedalArgumentException(s.str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
JSON_MAP_BEGIN(PiPedalConfiguration)
|
JSON_MAP_BEGIN(PiPedalConfiguration)
|
||||||
JSON_MAP_REFERENCE(PiPedalConfiguration, lv2_path)
|
JSON_MAP_REFERENCE(PiPedalConfiguration, lv2_path)
|
||||||
@@ -37,4 +104,5 @@ JSON_MAP_REFERENCE(PiPedalConfiguration, maxUploadSize)
|
|||||||
JSON_MAP_REFERENCE(PiPedalConfiguration, accessPointGateway)
|
JSON_MAP_REFERENCE(PiPedalConfiguration, accessPointGateway)
|
||||||
JSON_MAP_REFERENCE(PiPedalConfiguration, accessPointServerAddress)
|
JSON_MAP_REFERENCE(PiPedalConfiguration, accessPointServerAddress)
|
||||||
JSON_MAP_REFERENCE(PiPedalConfiguration, isVst3Enabled)
|
JSON_MAP_REFERENCE(PiPedalConfiguration, isVst3Enabled)
|
||||||
|
JSON_MAP_REFERENCE(PiPedalConfiguration, end)
|
||||||
JSON_MAP_END()
|
JSON_MAP_END()
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ private:
|
|||||||
std::string accessPointGateway_;
|
std::string accessPointGateway_;
|
||||||
std::string accessPointServerAddress_;
|
std::string accessPointServerAddress_;
|
||||||
bool isVst3Enabled_ = true;
|
bool isVst3Enabled_ = true;
|
||||||
|
bool end_ = false; // dummy target for /var/pipedal/config/config.json
|
||||||
|
|
||||||
public:
|
public:
|
||||||
bool IsVst3Enabled() const { return isVst3Enabled_; }
|
bool IsVst3Enabled() const { return isVst3Enabled_; }
|
||||||
@@ -57,26 +58,8 @@ public:
|
|||||||
const std::filesystem::path& GetWebRoot() const {
|
const std::filesystem::path& GetWebRoot() const {
|
||||||
return webRoot_;
|
return webRoot_;
|
||||||
}
|
}
|
||||||
void Load(const std::filesystem::path& path, const std::filesystem::path&webRoot) {
|
void Load(const std::filesystem::path& path, const std::filesystem::path&webRoot);
|
||||||
std::filesystem::path configPath = path / "config.json";
|
|
||||||
if (!std::filesystem::exists(configPath))
|
|
||||||
{
|
|
||||||
throw PiPedalException("File not found.");
|
|
||||||
}
|
|
||||||
std::ifstream f(configPath);
|
|
||||||
if (!f.is_open())
|
|
||||||
{
|
|
||||||
std::stringstream s;
|
|
||||||
s << "Unable to open " << configPath;
|
|
||||||
throw PiPedalStateException(s.str());
|
|
||||||
}
|
|
||||||
json_reader reader(f);
|
|
||||||
reader.read(this);
|
|
||||||
docRoot_ = path;
|
|
||||||
|
|
||||||
webRoot_ = webRoot;
|
|
||||||
|
|
||||||
}
|
|
||||||
const std::filesystem::path &GetDocRoot() const { return docRoot_; }
|
const std::filesystem::path &GetDocRoot() const { return docRoot_; }
|
||||||
const std::string &GetLv2Path() const { return lv2_path_; }
|
const std::string &GetLv2Path() const { return lv2_path_; }
|
||||||
const std::string &GetLocalStoragePath() const { return local_storage_path_; }
|
const std::string &GetLocalStoragePath() const { return local_storage_path_; }
|
||||||
@@ -108,32 +91,8 @@ public:
|
|||||||
return socketServerAddress_.substr(0,pos);
|
return socketServerAddress_.substr(0,pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t GetSocketServerPort() const {
|
uint16_t GetSocketServerPort() const;
|
||||||
try {
|
|
||||||
size_t pos = this->socketServerAddress_.find_last_of(':');
|
|
||||||
if (pos == std::string::npos)
|
|
||||||
{
|
|
||||||
throw std::exception();
|
|
||||||
}
|
|
||||||
std::string strPort = socketServerAddress_.substr(pos+1);
|
|
||||||
unsigned long port = std::stoul(strPort);
|
|
||||||
if (port == 0)
|
|
||||||
{
|
|
||||||
throw std::exception();
|
|
||||||
}
|
|
||||||
if (port > std::numeric_limits<uint16_t>::max())
|
|
||||||
{
|
|
||||||
throw std::exception();
|
|
||||||
}
|
|
||||||
return (uint16_t) port;
|
|
||||||
} catch (const std::exception &)
|
|
||||||
{
|
|
||||||
std::stringstream s;
|
|
||||||
s << "Invalid port number in '" << this->socketServerAddress_ << "'.";
|
|
||||||
throw PiPedalArgumentException(s.str());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
uint32_t GetThreads() const { return threads_; }
|
uint32_t GetThreads() const { return threads_; }
|
||||||
|
|
||||||
DECLARE_JSON_MAP(PiPedalConfiguration);
|
DECLARE_JSON_MAP(PiPedalConfiguration);
|
||||||
|
|||||||
+150
-78
@@ -66,6 +66,7 @@ PiPedalModel::PiPedalModel()
|
|||||||
: pluginHost(),
|
: pluginHost(),
|
||||||
atomConverter(pluginHost.GetMapFeature())
|
atomConverter(pluginHost.GetMapFeature())
|
||||||
{
|
{
|
||||||
|
this->currentUpdateStatus = updater.GetCurrentStatus();
|
||||||
this->pedalboard = Pedalboard::MakeDefault();
|
this->pedalboard = Pedalboard::MakeDefault();
|
||||||
#if JACK_HOST
|
#if JACK_HOST
|
||||||
this->jackServerSettings = this->storage.GetJackServerSettings(); // to get onboarding flag.
|
this->jackServerSettings = this->storage.GetJackServerSettings(); // to get onboarding flag.
|
||||||
@@ -73,6 +74,11 @@ PiPedalModel::PiPedalModel()
|
|||||||
#else
|
#else
|
||||||
this->jackServerSettings = this->storage.GetJackServerSettings();
|
this->jackServerSettings = this->storage.GetJackServerSettings();
|
||||||
#endif
|
#endif
|
||||||
|
updater.SetUpdateListener(
|
||||||
|
[this](const UpdateStatus &updateStatus)
|
||||||
|
{
|
||||||
|
this->OnUpdateStatusChanged(updateStatus);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void PiPedalModel::Close()
|
void PiPedalModel::Close()
|
||||||
@@ -242,64 +248,8 @@ void PiPedalModel::Load()
|
|||||||
scheduler_params.sched_priority = 10;
|
scheduler_params.sched_priority = 10;
|
||||||
memset(&scheduler_params, 0, sizeof(sched_param));
|
memset(&scheduler_params, 0, sizeof(sched_param));
|
||||||
sched_setscheduler(0, SCHED_RR, &scheduler_params);
|
sched_setscheduler(0, SCHED_RR, &scheduler_params);
|
||||||
#if JACK_HOST
|
|
||||||
this->jackConfiguration = this->jackConfiguration.JackInitialize();
|
|
||||||
#else
|
|
||||||
this->jackServerSettings = storage.GetJackServerSettings();
|
|
||||||
if (jackServerSettings.IsValid())
|
|
||||||
{
|
|
||||||
this->jackConfiguration.AlsaInitialize(this->jackServerSettings);
|
|
||||||
if (!jackServerSettings.IsValid())
|
|
||||||
{
|
|
||||||
for (size_t retry = 0; retry < 5; ++retry)
|
|
||||||
{
|
|
||||||
// retry until the device comes online.
|
|
||||||
sleep(3);
|
|
||||||
Lv2Log::info("Retrying AlsaInitialize");
|
|
||||||
jackConfiguration.isValid(true);
|
|
||||||
this->jackConfiguration.AlsaInitialize(this->jackServerSettings);
|
|
||||||
if (jackConfiguration.isValid())
|
|
||||||
{
|
|
||||||
Lv2Log::info("Retry succeeded.");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this->jackConfiguration.AlsaInitialize(this->jackServerSettings);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
if (this->jackConfiguration.isValid())
|
|
||||||
{
|
|
||||||
JackChannelSelection selection = storage.GetJackChannelSelection(jackConfiguration);
|
|
||||||
selection = selection.RemoveInvalidChannels(jackConfiguration);
|
|
||||||
|
|
||||||
this->pluginHost.OnConfigurationChanged(jackConfiguration, selection);
|
RestartAudio();
|
||||||
try
|
|
||||||
{
|
|
||||||
audioHost->Open(this->jackServerSettings, selection);
|
|
||||||
bool loadedSuccessfully = false;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
LoadCurrentPedalboard();
|
|
||||||
loadedSuccessfully = true;
|
|
||||||
}
|
|
||||||
catch (const std::exception &e)
|
|
||||||
{
|
|
||||||
Lv2Log::error("Failed to load initial plugin. %s", e.what());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (std::exception &e)
|
|
||||||
{
|
|
||||||
Lv2Log::error("Failed to start audio device. %s", e.what());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Lv2Log::error("Audio device not configured.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IPiPedalModelSubscriber *PiPedalModel::GetNotificationSubscriber(int64_t clientId)
|
IPiPedalModelSubscriber *PiPedalModel::GetNotificationSubscriber(int64_t clientId)
|
||||||
@@ -386,9 +336,8 @@ void PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId)
|
|||||||
|
|
||||||
item->stateUpdateCount(item->stateUpdateCount() + 1);
|
item->stateUpdateCount(item->stateUpdateCount() + 1);
|
||||||
|
|
||||||
IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()];
|
|
||||||
|
|
||||||
Lv2PluginState newState = item->lv2State();
|
Lv2PluginState newState = item->lv2State();
|
||||||
|
IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()];
|
||||||
for (size_t i = 0; i < subscribers.size(); ++i)
|
for (size_t i = 0; i < subscribers.size(); ++i)
|
||||||
{
|
{
|
||||||
t[i] = this->subscribers[i];
|
t[i] = this->subscribers[i];
|
||||||
@@ -1138,7 +1087,9 @@ JackConfiguration PiPedalModel::GetJackConfiguration()
|
|||||||
return this->jackConfiguration;
|
return this->jackConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PiPedalModel::RestartAudio()
|
void PiPedalModel::RestartAudio(bool useDummyAudioDriver)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
if (this->audioHost->IsOpen())
|
if (this->audioHost->IsOpen())
|
||||||
{
|
{
|
||||||
@@ -1150,17 +1101,19 @@ void PiPedalModel::RestartAudio()
|
|||||||
|
|
||||||
// Still bugs wrt/ restarting the circular buffers for the audio thread.
|
// Still bugs wrt/ restarting the circular buffers for the audio thread.
|
||||||
|
|
||||||
// for the meantime, just rely on the fact that the admin service will restart
|
|
||||||
// the process.
|
|
||||||
//...
|
|
||||||
|
|
||||||
// do a complete reload.
|
// do a complete reload.
|
||||||
|
|
||||||
this->audioHost->SetPedalboard(nullptr);
|
this->audioHost->SetPedalboard(nullptr);
|
||||||
|
|
||||||
this->jackConfiguration.AlsaInitialize(this->jackServerSettings);
|
auto jackServerSettings = this->jackServerSettings;
|
||||||
|
if (useDummyAudioDriver)
|
||||||
|
{
|
||||||
|
jackServerSettings.UseDummyAudioDevice();
|
||||||
|
}
|
||||||
|
|
||||||
if (this->jackConfiguration.isValid())
|
auto jackConfiguration = this->jackConfiguration;
|
||||||
|
jackConfiguration.AlsaInitialize(jackServerSettings);
|
||||||
|
if (jackConfiguration.isValid())
|
||||||
{
|
{
|
||||||
JackChannelSelection selection = storage.GetJackChannelSelection(jackConfiguration);
|
JackChannelSelection selection = storage.GetJackChannelSelection(jackConfiguration);
|
||||||
selection = selection.RemoveInvalidChannels(jackConfiguration);
|
selection = selection.RemoveInvalidChannels(jackConfiguration);
|
||||||
@@ -1169,37 +1122,50 @@ void PiPedalModel::RestartAudio()
|
|||||||
{
|
{
|
||||||
jackConfiguration.setErrorStatus("Error");
|
jackConfiguration.setErrorStatus("Error");
|
||||||
}
|
}
|
||||||
|
if (!useDummyAudioDriver)
|
||||||
|
{
|
||||||
|
this->jackConfiguration = jackConfiguration;
|
||||||
FireJackConfigurationChanged(jackConfiguration);
|
FireJackConfigurationChanged(jackConfiguration);
|
||||||
|
}
|
||||||
|
|
||||||
if (!jackServerSettings.IsValid() || !jackConfiguration.isValid())
|
if (!jackServerSettings.IsValid() || !jackConfiguration.isValid())
|
||||||
{
|
{
|
||||||
Lv2Log::error("Audio configuration not valid.");
|
throw std::runtime_error("Audio configuration not valid.");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
try
|
|
||||||
|
JackChannelSelection channelSelection = this->storage.GetJackChannelSelection(jackConfiguration);
|
||||||
|
if (this->jackConfiguration.isValid())
|
||||||
{
|
{
|
||||||
JackChannelSelection channelSelection = GetJackChannelSelection();
|
channelSelection.RemoveInvalidChannels(this->jackConfiguration);
|
||||||
|
}
|
||||||
if (!channelSelection.isValid())
|
if (!channelSelection.isValid())
|
||||||
{
|
{
|
||||||
Lv2Log::error("Audio configuration not valid.");
|
throw std::runtime_error("Audio configuration not valid.");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
this->audioHost->Open(this->jackServerSettings, channelSelection);
|
this->audioHost->Open(jackServerSettings, channelSelection);
|
||||||
|
|
||||||
this->pluginHost.OnConfigurationChanged(jackConfiguration, channelSelection);
|
this->pluginHost.OnConfigurationChanged(jackConfiguration, channelSelection);
|
||||||
|
|
||||||
std::vector<std::string> errorMessages;
|
|
||||||
|
|
||||||
LoadCurrentPedalboard();
|
LoadCurrentPedalboard();
|
||||||
|
|
||||||
this->UpdateRealtimeVuSubscriptions();
|
this->UpdateRealtimeVuSubscriptions();
|
||||||
UpdateRealtimeMonitorPortSubscriptions();
|
UpdateRealtimeMonitorPortSubscriptions();
|
||||||
}
|
}
|
||||||
catch (const std::exception &e)
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
this->audioHost->Close();
|
||||||
|
if (useDummyAudioDriver)
|
||||||
|
{
|
||||||
|
Lv2Log::error(SS("Failed to start dummy audio driver. " << e.what()));
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
Lv2Log::error(SS("Failed to start audio. " << e.what()));
|
Lv2Log::error(SS("Failed to start audio. " << e.what()));
|
||||||
throw;
|
}
|
||||||
|
if (!useDummyAudioDriver)
|
||||||
|
{
|
||||||
|
RestartAudio(true); // use the dummy audio driver.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1293,7 +1259,6 @@ void PiPedalModel::OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, f
|
|||||||
{
|
{
|
||||||
if (portIndex == -1)
|
if (portIndex == -1)
|
||||||
{
|
{
|
||||||
// bypass! yyy this->value!!!
|
|
||||||
this->pedalboard.SetItemEnabled(instanceId, value != 0);
|
this->pedalboard.SetItemEnabled(instanceId, value != 0);
|
||||||
// take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us)
|
// 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()];
|
IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()];
|
||||||
@@ -2172,3 +2137,110 @@ void PiPedalModel::SetRestartListener(std::function<void(void)> &&listener)
|
|||||||
{
|
{
|
||||||
this->restartListener = std::move(listener);
|
this->restartListener = std::move(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PiPedalModel::OnUpdateStatusChanged(const UpdateStatus &updateStatus)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||||
|
|
||||||
|
if (this->currentUpdateStatus != updateStatus)
|
||||||
|
{
|
||||||
|
this->currentUpdateStatus = updateStatus;
|
||||||
|
FireUpdateStatusChanged(this->currentUpdateStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void PiPedalModel::FireUpdateStatusChanged(const UpdateStatus &updateStatus)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||||
|
|
||||||
|
std::vector<IPiPedalModelSubscriber *> t;
|
||||||
|
t.reserve(subscribers.size());
|
||||||
|
|
||||||
|
for (auto subscriber : subscribers)
|
||||||
|
{
|
||||||
|
t.push_back(subscriber);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto subscriber : t)
|
||||||
|
{
|
||||||
|
subscriber->OnUpdateStatusChanged(updateStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UpdateStatus PiPedalModel::GetUpdateStatus()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||||
|
return updater.GetCurrentStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PiPedalModel::UpdateNow(const std::string &updateUrl)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::recursive_mutex> lock(mutex);
|
||||||
|
std::filesystem::path fileName,signatureName;
|
||||||
|
updater.DownloadUpdate(updateUrl,&fileName,&signatureName);
|
||||||
|
|
||||||
|
adminClient.InstallUpdate(fileName);
|
||||||
|
}
|
||||||
|
void PiPedalModel::ForceUpdateCheck()
|
||||||
|
{
|
||||||
|
updater.ForceUpdateCheck();
|
||||||
|
}
|
||||||
|
void PiPedalModel::SetUpdatePolicy(UpdatePolicyT updatePolicy)
|
||||||
|
{
|
||||||
|
updater.SetUpdatePolicy(updatePolicy);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool HasAlsaDevice(const std::vector<AlsaDeviceInfo> devices, const std::string &deviceId)
|
||||||
|
{
|
||||||
|
for (auto &device : devices)
|
||||||
|
{
|
||||||
|
if (device.id_ == deviceId)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PiPedalModel::WaitForAudioDeviceToComeOnline()
|
||||||
|
{
|
||||||
|
auto serverSettings = this->GetJackServerSettings();
|
||||||
|
// Wait for selected audio device to be initialized.
|
||||||
|
// It may take some time for ALSA to publish all available devices when rebooting.
|
||||||
|
|
||||||
|
if (serverSettings.IsValid())
|
||||||
|
{
|
||||||
|
// wait up to 15 seconds for the midi device to come online.
|
||||||
|
auto devices = GetAlsaDevices();
|
||||||
|
bool found = false;
|
||||||
|
if (HasAlsaDevice(devices, serverSettings.GetAlsaInputDevice()))
|
||||||
|
{
|
||||||
|
Lv2Log::info(SS("Found ALSA device " << serverSettings.GetAlsaInputDevice() << "."));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Lv2Log::info(SS("Waiting for ALSA device " << serverSettings.GetAlsaInputDevice() << "."));
|
||||||
|
for (int i = 0; i < 5; ++i)
|
||||||
|
{
|
||||||
|
sleep(2);
|
||||||
|
devices = GetAlsaDevices();
|
||||||
|
if (HasAlsaDevice(devices, serverSettings.GetAlsaInputDevice()))
|
||||||
|
{
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found)
|
||||||
|
{
|
||||||
|
Lv2Log::info(SS("Found ALSA device " << serverSettings.GetAlsaInputDevice() << "."));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Lv2Log::info(SS("ALSA device " << serverSettings.GetAlsaInputDevice() << " not found."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Lv2Log::info("No ALSA device selected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// pre-cache device info before we let audio services run.
|
||||||
|
GetAlsaDevices();
|
||||||
|
}
|
||||||
|
|||||||
+14
-4
@@ -30,6 +30,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include "Banks.hpp"
|
#include "Banks.hpp"
|
||||||
|
#include "Updater.hpp"
|
||||||
#include "PiPedalConfiguration.hpp"
|
#include "PiPedalConfiguration.hpp"
|
||||||
#include "JackServerSettings.hpp"
|
#include "JackServerSettings.hpp"
|
||||||
#include "WifiConfigSettings.hpp"
|
#include "WifiConfigSettings.hpp"
|
||||||
@@ -56,7 +57,7 @@ namespace pipedal
|
|||||||
virtual void OnControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value) = 0;
|
virtual void OnControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value) = 0;
|
||||||
virtual void OnInputVolumeChanged(float value) = 0;
|
virtual void OnInputVolumeChanged(float value) = 0;
|
||||||
virtual void OnOutputVolumeChanged(float value) = 0;
|
virtual void OnOutputVolumeChanged(float value) = 0;
|
||||||
|
virtual void OnUpdateStatusChanged(const UpdateStatus&updateStatus) = 0;
|
||||||
virtual void OnLv2StateChanged(int64_t pedalItemId,const Lv2PluginState&newState ) = 0;
|
virtual void OnLv2StateChanged(int64_t pedalItemId,const Lv2PluginState&newState ) = 0;
|
||||||
virtual void OnVst3ControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value, const std::string &state) = 0;
|
virtual void OnVst3ControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value, const std::string &state) = 0;
|
||||||
virtual void OnPedalboardChanged(int64_t clientId, const Pedalboard &pedalboard) = 0;
|
virtual void OnPedalboardChanged(int64_t clientId, const Pedalboard &pedalboard) = 0;
|
||||||
@@ -86,6 +87,10 @@ namespace pipedal
|
|||||||
class PiPedalModel : private IAudioHostCallbacks
|
class PiPedalModel : private IAudioHostCallbacks
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
|
|
||||||
|
UpdateStatus currentUpdateStatus;
|
||||||
|
Updater updater;
|
||||||
|
void OnUpdateStatusChanged(const UpdateStatus&updateStatus);
|
||||||
std::function<void(void)> restartListener;
|
std::function<void(void)> restartListener;
|
||||||
|
|
||||||
std::unique_ptr<Lv2PluginChangeMonitor> pluginChangeMonitor;
|
std::unique_ptr<Lv2PluginChangeMonitor> pluginChangeMonitor;
|
||||||
@@ -164,9 +169,8 @@ namespace pipedal
|
|||||||
|
|
||||||
void UpdateRealtimeVuSubscriptions();
|
void UpdateRealtimeVuSubscriptions();
|
||||||
void UpdateRealtimeMonitorPortSubscriptions();
|
void UpdateRealtimeMonitorPortSubscriptions();
|
||||||
void OnVuUpdate(const std::vector<VuUpdate> &updates);
|
|
||||||
|
|
||||||
void RestartAudio();
|
void RestartAudio(bool useDummyAudioDriver = false);
|
||||||
|
|
||||||
std::vector<RealtimePatchPropertyRequest *> outstandingParameterRequests;
|
std::vector<RealtimePatchPropertyRequest *> outstandingParameterRequests;
|
||||||
|
|
||||||
@@ -199,6 +203,11 @@ namespace pipedal
|
|||||||
PiPedalModel();
|
PiPedalModel();
|
||||||
virtual ~PiPedalModel();
|
virtual ~PiPedalModel();
|
||||||
|
|
||||||
|
void WaitForAudioDeviceToComeOnline();
|
||||||
|
|
||||||
|
UpdateStatus GetUpdateStatus();
|
||||||
|
void UpdateNow(const std::string&updateUrl);
|
||||||
|
void FireUpdateStatusChanged(const UpdateStatus&updateStatus);
|
||||||
uint16_t GetWebPort() const { return webPort; }
|
uint16_t GetWebPort() const { return webPort; }
|
||||||
std::filesystem::path GetPluginUploadDirectory() const;
|
std::filesystem::path GetPluginUploadDirectory() const;
|
||||||
void Close();
|
void Close();
|
||||||
@@ -334,7 +343,8 @@ namespace pipedal
|
|||||||
|
|
||||||
std::map<std::string, bool> GetFavorites() const;
|
std::map<std::string, bool> GetFavorites() const;
|
||||||
void SetFavorites(const std::map<std::string, bool> &favorites);
|
void SetFavorites(const std::map<std::string, bool> &favorites);
|
||||||
|
void SetUpdatePolicy(UpdatePolicyT updatePolicy);
|
||||||
|
void ForceUpdateCheck();
|
||||||
std::vector<std::string> GetFileList(const UiFileProperty&fileProperty);
|
std::vector<std::string> GetFileList(const UiFileProperty&fileProperty);
|
||||||
std::vector<FileEntry> GetFileList2(const std::string &relativePath,const UiFileProperty&fileProperty);
|
std::vector<FileEntry> GetFileList2(const std::string &relativePath,const UiFileProperty&fileProperty);
|
||||||
|
|
||||||
|
|||||||
+33
-4
@@ -20,6 +20,7 @@
|
|||||||
#include "pch.h"
|
#include "pch.h"
|
||||||
|
|
||||||
#include "PiPedalSocket.hpp"
|
#include "PiPedalSocket.hpp"
|
||||||
|
#include "Updater.hpp"
|
||||||
#include "json.hpp"
|
#include "json.hpp"
|
||||||
#include "viewstream.hpp"
|
#include "viewstream.hpp"
|
||||||
#include "PiPedalVersion.hpp"
|
#include "PiPedalVersion.hpp"
|
||||||
@@ -1001,7 +1002,19 @@ public:
|
|||||||
pReader->read(&handle);
|
pReader->read(&handle);
|
||||||
this->model.CancelMonitorPatchProperty(this->clientId, handle);
|
this->model.CancelMonitorPatchProperty(this->clientId, handle);
|
||||||
}
|
}
|
||||||
|
else if (message == "getUpdateStatus")
|
||||||
|
{
|
||||||
|
UpdateStatus updateStatus = model.GetUpdateStatus();
|
||||||
|
this->Reply(replyTo,"getUpdateStatus",updateStatus);
|
||||||
|
}
|
||||||
|
else if (message == "updateNow")
|
||||||
|
{
|
||||||
|
std::string updateUrl;
|
||||||
|
pReader->read(&updateUrl);
|
||||||
|
model.UpdateNow(updateUrl);
|
||||||
|
bool result = true;
|
||||||
|
this->Reply(replyTo,"updateNow",result);
|
||||||
|
}
|
||||||
else if (message == "getJackStatus")
|
else if (message == "getJackStatus")
|
||||||
{
|
{
|
||||||
JackHostStatus status = model.GetJackStatus();
|
JackHostStatus status = model.GetJackStatus();
|
||||||
@@ -1054,7 +1067,7 @@ public:
|
|||||||
{
|
{
|
||||||
WifiConfigSettings wifiConfigSettings;
|
WifiConfigSettings wifiConfigSettings;
|
||||||
pReader->read(&wifiConfigSettings);
|
pReader->read(&wifiConfigSettings);
|
||||||
if (!GetAdminClient().CanUseShutdownClient())
|
if (!GetAdminClient().CanUseAdminClient())
|
||||||
{
|
{
|
||||||
throw PiPedalException("Can't change server settings when running interactively.");
|
throw PiPedalException("Can't change server settings when running interactively.");
|
||||||
}
|
}
|
||||||
@@ -1075,7 +1088,7 @@ public:
|
|||||||
{
|
{
|
||||||
WifiDirectConfigSettings wifiDirectConfigSettings;
|
WifiDirectConfigSettings wifiDirectConfigSettings;
|
||||||
pReader->read(&wifiDirectConfigSettings);
|
pReader->read(&wifiDirectConfigSettings);
|
||||||
if (!GetAdminClient().CanUseShutdownClient())
|
if (!GetAdminClient().CanUseAdminClient())
|
||||||
{
|
{
|
||||||
throw PiPedalException("Can't change server settings when running interactively.");
|
throw PiPedalException("Can't change server settings when running interactively.");
|
||||||
}
|
}
|
||||||
@@ -1442,6 +1455,18 @@ public:
|
|||||||
pReader->read(&favorites);
|
pReader->read(&favorites);
|
||||||
this->model.SetFavorites(favorites);
|
this->model.SetFavorites(favorites);
|
||||||
}
|
}
|
||||||
|
else if (message == "setUpdatePolicy")
|
||||||
|
{
|
||||||
|
int iPolicy;
|
||||||
|
pReader->read(&iPolicy);
|
||||||
|
|
||||||
|
this->model.SetUpdatePolicy((UpdatePolicyT)iPolicy);
|
||||||
|
}
|
||||||
|
else if (message == "forceUpdateCheck")
|
||||||
|
{
|
||||||
|
this->model.ForceUpdateCheck();
|
||||||
|
|
||||||
|
}
|
||||||
else if (message == "setSystemMidiBindings")
|
else if (message == "setSystemMidiBindings")
|
||||||
{
|
{
|
||||||
std::vector<MidiBinding> bindings;
|
std::vector<MidiBinding> bindings;
|
||||||
@@ -1581,6 +1606,10 @@ protected:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
virtual void OnUpdateStatusChanged(const UpdateStatus&updateStatus) {
|
||||||
|
Send("onUpdateStatusChanged",updateStatus);
|
||||||
|
}
|
||||||
|
|
||||||
virtual void OnLv2StateChanged(int64_t instanceId, const Lv2PluginState &state)
|
virtual void OnLv2StateChanged(int64_t instanceId, const Lv2PluginState &state)
|
||||||
{
|
{
|
||||||
Lv2StateChangedBody message { (uint64_t)instanceId, state};
|
Lv2StateChangedBody message { (uint64_t)instanceId, state};
|
||||||
@@ -1949,7 +1978,7 @@ std::shared_ptr<ISocketFactory> pipedal::MakePiPedalSocketFactory(PiPedalModel &
|
|||||||
|
|
||||||
void PiPedalSocketHandler::RequestShutdown(bool restart)
|
void PiPedalSocketHandler::RequestShutdown(bool restart)
|
||||||
{
|
{
|
||||||
if (GetAdminClient().CanUseShutdownClient())
|
if (GetAdminClient().CanUseAdminClient())
|
||||||
{
|
{
|
||||||
GetAdminClient().RequestShutdown(restart);
|
GetAdminClient().RequestShutdown(restart);
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-4
@@ -549,13 +549,18 @@ void UninstallP2p()
|
|||||||
restoreP2pDnsmasqConfFile();
|
restoreP2pDnsmasqConfFile();
|
||||||
restoreNetworkManagerP2pConfig();
|
restoreNetworkManagerP2pConfig();
|
||||||
|
|
||||||
|
if (!UsingNetworkManager())
|
||||||
|
{
|
||||||
sysExec(SYSTEMCTL_BIN " restart dhcpcd");
|
sysExec(SYSTEMCTL_BIN " restart dhcpcd");
|
||||||
|
} else {
|
||||||
|
sysExec(SYSTEMCTL_BIN " restart NetworkManager"); // bring up wlan0 wifi.
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
WifiDirectConfigSettings wifiDirectConfigSettings;
|
WifiDirectConfigSettings wifiDirectConfigSettings;
|
||||||
wifiDirectConfigSettings.Load();
|
wifiDirectConfigSettings.Load();
|
||||||
wifiDirectConfigSettings.enable_ = false;
|
wifiDirectConfigSettings.enable_ = false;
|
||||||
wifiDirectConfigSettings.Save();
|
SetWifiDirectConfig(wifiDirectConfigSettings);
|
||||||
sysExec(SYSTEMCTL_BIN " restart NetworkManager"); // bring up wlan0 wifi.
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -675,8 +680,15 @@ void pipedal::SetWifiDirectConfig(const WifiDirectConfigSettings &settings)
|
|||||||
cout << e.what() << endl;
|
cout << e.what() << endl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
void pipedal::OnWifiReinstall() {
|
||||||
void pipedal::OnWifiUninstall()
|
WifiDirectConfigSettings settings;
|
||||||
|
settings.Load();
|
||||||
|
if (settings.enable_)
|
||||||
|
{
|
||||||
|
SetWifiDirectConfig(settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void pipedal::OnWifiUninstall(bool preserveState)
|
||||||
{
|
{
|
||||||
// intaller hook
|
// intaller hook
|
||||||
if (IsApdInstalled())
|
if (IsApdInstalled())
|
||||||
@@ -685,7 +697,15 @@ void pipedal::OnWifiUninstall()
|
|||||||
}
|
}
|
||||||
if (IsP2pInstalled())
|
if (IsP2pInstalled())
|
||||||
{
|
{
|
||||||
|
WifiDirectConfigSettings settings;
|
||||||
|
settings.Load();
|
||||||
UninstallP2p();
|
UninstallP2p();
|
||||||
|
|
||||||
|
// preserve the state for future installs.
|
||||||
|
if (preserveState)
|
||||||
|
{
|
||||||
|
settings.Save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void pipedal::OnWifiInstallComplete()
|
void pipedal::OnWifiInstallComplete()
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ namespace pipedal {
|
|||||||
void SetWifiConfig(const WifiConfigSettings&settings);
|
void SetWifiConfig(const WifiConfigSettings&settings);
|
||||||
void SetWifiDirectConfig(const WifiDirectConfigSettings&settings);
|
void SetWifiDirectConfig(const WifiDirectConfigSettings&settings);
|
||||||
|
|
||||||
void OnWifiUninstall();
|
void OnWifiUninstall(bool preserveState = false);
|
||||||
|
void OnWifiReinstall();
|
||||||
|
|
||||||
void OnWifiInstallComplete();
|
void OnWifiInstallComplete();
|
||||||
|
|
||||||
bool UsingNetworkManager();
|
bool UsingNetworkManager();
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (c) 2024Robin 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 "AdminInstallUpdate.hpp"
|
||||||
|
#include "Lv2Log.hpp"
|
||||||
|
#include "Lv2SystemdLogger.hpp"
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
using namespace pipedal;
|
||||||
|
|
||||||
|
int main(int argc, char**argv)
|
||||||
|
{
|
||||||
|
Lv2Log::set_logger(MakeLv2SystemdLogger());
|
||||||
|
|
||||||
|
if (argc != 2)
|
||||||
|
{
|
||||||
|
Lv2Log::error("Invalid arguments.");
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
std::filesystem::path path = argv[1];
|
||||||
|
AdminInstallUpdate(path);
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// 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 "UpdateResults.hpp"
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include "Lv2Log.hpp"
|
||||||
|
#include "ss.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
using namespace pipedal;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
const fs::path UPDATE_RESULT_PATH{"/var/pipedal/config"};
|
||||||
|
void UpdateResults::Load()
|
||||||
|
{
|
||||||
|
std::ifstream f(UPDATE_RESULT_PATH);
|
||||||
|
if (f.is_open())
|
||||||
|
{
|
||||||
|
json_reader reader(f);
|
||||||
|
reader.read(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void UpdateResults::Save()
|
||||||
|
{
|
||||||
|
std::ofstream f {UPDATE_RESULT_PATH};
|
||||||
|
if (f.is_open())
|
||||||
|
{
|
||||||
|
json_writer writer(f);
|
||||||
|
writer.write(this);
|
||||||
|
} else {
|
||||||
|
Lv2Log::error(SS("Can't write to " << UPDATE_RESULT_PATH));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
JSON_MAP_BEGIN(UpdateResults)
|
||||||
|
JSON_MAP_REFERENCE(UpdateResults, updated)
|
||||||
|
JSON_MAP_REFERENCE(UpdateResults, updateSuccessful)
|
||||||
|
JSON_MAP_REFERENCE(UpdateResults, updateVersion)
|
||||||
|
JSON_MAP_REFERENCE(UpdateResults, updateMessage)
|
||||||
|
JSON_MAP_REFERENCE(UpdateResults, installerLog)
|
||||||
|
JSON_MAP_END()
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// 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 {
|
||||||
|
class UpdateResults {
|
||||||
|
public:
|
||||||
|
bool updated_ = false;
|
||||||
|
bool updateSuccessful_ = false;
|
||||||
|
std::string updateVersion_;
|
||||||
|
std::string updateMessage_;
|
||||||
|
std::string installerLog_;
|
||||||
|
|
||||||
|
void Load();
|
||||||
|
void Save();
|
||||||
|
|
||||||
|
DECLARE_JSON_MAP(UpdateResults);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
+999
@@ -0,0 +1,999 @@
|
|||||||
|
// 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 "Updater.hpp"
|
||||||
|
#include "util.hpp"
|
||||||
|
#include "json.hpp"
|
||||||
|
#include "config.hpp"
|
||||||
|
#include <sys/eventfd.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <chrono>
|
||||||
|
#include <poll.h>
|
||||||
|
#include "Lv2Log.hpp"
|
||||||
|
#include "SysExec.hpp"
|
||||||
|
#include "json_variant.hpp"
|
||||||
|
#include "ss.hpp"
|
||||||
|
#include "Lv2Log.hpp"
|
||||||
|
#include <algorithm>
|
||||||
|
#include "UpdaterSecurity.hpp"
|
||||||
|
#include "SysExec.hpp"
|
||||||
|
|
||||||
|
using namespace pipedal;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
#undef TEST_UPDATE
|
||||||
|
|
||||||
|
#ifndef DEBUG
|
||||||
|
#undef TEST_UPDATE // do NOT leat this leak into a production build!
|
||||||
|
#endif
|
||||||
|
|
||||||
|
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<Updater::clock::duration>(std::chrono::days(1));
|
||||||
|
static std::chrono::system_clock::duration CACHE_DURATION = std::chrono::duration_cast<std::chrono::system_clock::duration>(std::chrono::minutes(30));
|
||||||
|
|
||||||
|
std::mutex cacheMutex;
|
||||||
|
static UpdateStatus GetCachedUpdateStatus()
|
||||||
|
{
|
||||||
|
std::lock_guard lock{cacheMutex};
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (std::filesystem::exists(UPDATE_STATUS_CACHE_FILE))
|
||||||
|
{
|
||||||
|
std::ifstream f{UPDATE_STATUS_CACHE_FILE};
|
||||||
|
if (f.is_open())
|
||||||
|
{
|
||||||
|
json_reader reader(f);
|
||||||
|
UpdateStatus status;
|
||||||
|
reader.read(&status);
|
||||||
|
|
||||||
|
// cached curruent version might come from a different version.
|
||||||
|
status.ResetCurrentVersion();
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
Lv2Log::error(SS("Unable to read cached UpdateStatus. " << e.what()));
|
||||||
|
}
|
||||||
|
return UpdateStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void SetCachedUpdateStatus(UpdateStatus &updateStatus)
|
||||||
|
{
|
||||||
|
std::lock_guard lock{cacheMutex};
|
||||||
|
updateStatus.LastUpdateTime(std::chrono::system_clock::now());
|
||||||
|
try
|
||||||
|
{
|
||||||
|
std::ofstream f{UPDATE_STATUS_CACHE_FILE};
|
||||||
|
json_writer writer{f};
|
||||||
|
writer.write(updateStatus);
|
||||||
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
Lv2Log::error(SS("Unable to write cached UpdateStatus. " << e.what()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Updater::Updater()
|
||||||
|
{
|
||||||
|
cachedUpdateStatus = GetCachedUpdateStatus();
|
||||||
|
this->updatePolicy = cachedUpdateStatus.UpdatePolicy();
|
||||||
|
currentResult = cachedUpdateStatus;
|
||||||
|
|
||||||
|
int fds[2];
|
||||||
|
int rc = pipe(fds);
|
||||||
|
if (rc != 0)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Updater: cant create event pipe.");
|
||||||
|
}
|
||||||
|
this->event_reader = fds[0];
|
||||||
|
this->event_writer = fds[1];
|
||||||
|
|
||||||
|
this->thread = std::make_unique<std::thread>([this]()
|
||||||
|
{ ThreadProc(); });
|
||||||
|
CheckNow();
|
||||||
|
}
|
||||||
|
Updater::~Updater()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
void Updater::Stop()
|
||||||
|
{
|
||||||
|
if (stopped)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stopped = true;
|
||||||
|
|
||||||
|
if (event_writer != -1)
|
||||||
|
{
|
||||||
|
uint64_t value = CLOSE_EVENT;
|
||||||
|
write(this->event_writer, &value, sizeof(uint64_t));
|
||||||
|
}
|
||||||
|
if (thread)
|
||||||
|
{
|
||||||
|
thread->join();
|
||||||
|
thread = nullptr;
|
||||||
|
}
|
||||||
|
if (event_reader != -1)
|
||||||
|
{
|
||||||
|
close(event_reader);
|
||||||
|
}
|
||||||
|
if (event_writer != -1)
|
||||||
|
{
|
||||||
|
close(event_writer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Updater::CheckNow()
|
||||||
|
{
|
||||||
|
uint64_t value = CHECK_NOW_EVENT;
|
||||||
|
write(this->event_writer, &value, sizeof(uint64_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Updater::SetUpdateListener(UpdateListener &&listener)
|
||||||
|
{
|
||||||
|
std::lock_guard lock{mutex};
|
||||||
|
this->listener = listener;
|
||||||
|
if (hasInfo)
|
||||||
|
{
|
||||||
|
listener(currentResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Updater::ThreadProc()
|
||||||
|
{
|
||||||
|
SetThreadName("UpdateMonitor");
|
||||||
|
struct pollfd pfd;
|
||||||
|
pfd.fd = this->event_reader;
|
||||||
|
pfd.events = POLLIN;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int ret = poll(&pfd, 1, std::chrono::duration_cast<std::chrono::milliseconds>(updateRate).count()); // 1000 ms timeout
|
||||||
|
|
||||||
|
if (ret == -1)
|
||||||
|
{
|
||||||
|
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 (value == CHECK_NOW_EVENT)
|
||||||
|
{
|
||||||
|
CheckForUpdate(true);
|
||||||
|
}
|
||||||
|
else if (value == UNCACHED_CHECK_NOW_EVENT)
|
||||||
|
{
|
||||||
|
CheckForUpdate(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
bool draft = true;
|
||||||
|
bool prerelease = true;
|
||||||
|
std::string name;
|
||||||
|
std::string url;
|
||||||
|
std::string version;
|
||||||
|
std::string body;
|
||||||
|
std::vector<GithubAsset> assets;
|
||||||
|
std::string published_at;
|
||||||
|
};
|
||||||
|
|
||||||
|
GithubAsset::GithubAsset(json_variant &v)
|
||||||
|
{
|
||||||
|
auto o = v.as_object();
|
||||||
|
this->name = o->at("name").as_string();
|
||||||
|
this->browser_download_url = o->at("browser_download_url").as_string();
|
||||||
|
this->updated_at = o->at("updated_at").as_string();
|
||||||
|
}
|
||||||
|
GithubRelease::GithubRelease(json_variant &v)
|
||||||
|
{
|
||||||
|
auto o = v.as_object();
|
||||||
|
this->name = o->at("name").as_string();
|
||||||
|
this->draft = o->at("draft").as_bool();
|
||||||
|
this->prerelease = o->at("prerelease").as_bool();
|
||||||
|
this->body = o->at("body").as_string();
|
||||||
|
|
||||||
|
auto assets = o->at("assets").as_array();
|
||||||
|
for (size_t i = 0; i < assets->size(); ++i)
|
||||||
|
{
|
||||||
|
auto &el = assets->at(i);
|
||||||
|
this->assets.push_back(GithubAsset(el));
|
||||||
|
}
|
||||||
|
this->published_at = o->at("published_at").as_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<std::string> split(const std::string &s, char delimiter)
|
||||||
|
{
|
||||||
|
std::vector<std::string> tokens;
|
||||||
|
std::string token;
|
||||||
|
std::istringstream tokenStream(s);
|
||||||
|
while (std::getline(tokenStream, token, delimiter))
|
||||||
|
{
|
||||||
|
tokens.push_back(token);
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
static std::string justTheVersion(const std::string &assetName)
|
||||||
|
{
|
||||||
|
// eg. pipedal_1.2.41_arm64.deb
|
||||||
|
auto t = split(assetName, '_');
|
||||||
|
if (t.size() != 3)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Unable to parse version.");
|
||||||
|
}
|
||||||
|
return t[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
int compareVersions(const std::string &l, const std::string &r)
|
||||||
|
{
|
||||||
|
std::stringstream sl(l);
|
||||||
|
std::stringstream sr(r);
|
||||||
|
|
||||||
|
int majorL = -1, majorR = 1, minorL = -1,
|
||||||
|
minorR = -1, buildL = -1, buildR = -1;
|
||||||
|
sl >> majorL;
|
||||||
|
sr >> majorR;
|
||||||
|
if (majorL != majorR)
|
||||||
|
{
|
||||||
|
return (majorL < majorR) ? -1 : 1;
|
||||||
|
}
|
||||||
|
char discard;
|
||||||
|
sl >> discard >> minorL;
|
||||||
|
sr >> discard >> minorR;
|
||||||
|
if (minorL != minorR)
|
||||||
|
{
|
||||||
|
return minorL < minorR ? -1 : 1;
|
||||||
|
}
|
||||||
|
sl >> discard >> buildL;
|
||||||
|
sr >> discard >> buildR;
|
||||||
|
|
||||||
|
if (buildL != buildR)
|
||||||
|
{
|
||||||
|
return buildL < buildR ? -1 : 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
static std::string normalizeReleaseName(const std::string &releaseName)
|
||||||
|
{
|
||||||
|
// e.g. "PiPedal 1.2.34 Release" -> "PiPedal v1.2.34-Release"
|
||||||
|
if (releaseName.empty())
|
||||||
|
return "";
|
||||||
|
|
||||||
|
std::string result = releaseName;
|
||||||
|
|
||||||
|
auto nPos = result.find(' ');
|
||||||
|
if (nPos == std::string::npos)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
++nPos;
|
||||||
|
if (nPos >= result.length())
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
char c = releaseName[nPos];
|
||||||
|
if (c >= '0' && c <= '9')
|
||||||
|
{
|
||||||
|
result.insert(result.begin() + nPos, 'v');
|
||||||
|
}
|
||||||
|
nPos = result.find(' ', nPos);
|
||||||
|
if (nPos != std::string::npos)
|
||||||
|
{
|
||||||
|
result.at(nPos) = '-';
|
||||||
|
}
|
||||||
|
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(
|
||||||
|
const std::vector<GithubRelease> &githubReleases,
|
||||||
|
const std::string ¤tVersion,
|
||||||
|
const UpdateReleasePredicate &predicate)
|
||||||
|
{
|
||||||
|
for (const auto &githubRelease : githubReleases)
|
||||||
|
{
|
||||||
|
auto *asset = githubRelease.GetDownloadForCurrentArchitecture();
|
||||||
|
if (!asset)
|
||||||
|
continue;
|
||||||
|
auto *pgpKey = githubRelease.GetGpgKeyForAsset(asset->name);
|
||||||
|
if (!pgpKey)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!predicate(githubRelease))
|
||||||
|
continue;
|
||||||
|
UpdateRelease updateRelease;
|
||||||
|
updateRelease.upgradeVersion_ = justTheVersion(asset->name);
|
||||||
|
updateRelease.updateAvailable_ = compareVersions(currentVersion, updateRelease.upgradeVersion_) < 0;
|
||||||
|
updateRelease.upgradeVersionDisplayName_ = normalizeReleaseName(githubRelease.name);
|
||||||
|
updateRelease.assetName_ = asset->name;
|
||||||
|
updateRelease.updateUrl_ = asset->browser_download_url;
|
||||||
|
updateRelease.gpgSignatureUrl_ = pgpKey->browser_download_url;
|
||||||
|
return updateRelease;
|
||||||
|
}
|
||||||
|
return UpdateRelease();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CheckUpdateHttpResponse(std::string errorCode)
|
||||||
|
{
|
||||||
|
if (errorCode.starts_with("%"))
|
||||||
|
{
|
||||||
|
errorCode = errorCode.substr(1);
|
||||||
|
}
|
||||||
|
int code = -999;
|
||||||
|
{
|
||||||
|
std::istringstream ss{errorCode};
|
||||||
|
ss >> code;
|
||||||
|
}
|
||||||
|
if (code == -999)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(SS("Invalid curl response: " << errorCode));
|
||||||
|
}
|
||||||
|
if (code == 200)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (code == 0)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("PiPedal server can't reach the internet.");
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::string message = SS("HTTP error " << code << "");
|
||||||
|
throw std::runtime_error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Updater::CheckForUpdate(bool useCache)
|
||||||
|
{
|
||||||
|
UpdateStatus updateResult;
|
||||||
|
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string responseOption = "-w \"%{response_code}\"";
|
||||||
|
#ifdef WIN32
|
||||||
|
responseOption = "-w \"%%{response_code}\""; // windows shell requires doubling of the %%.
|
||||||
|
#endif
|
||||||
|
|
||||||
|
std::string args = SS("-s -L " << responseOption << " " << 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<GithubRelease> releases;
|
||||||
|
for (size_t i = 0; i < vArray->size(); ++i)
|
||||||
|
{
|
||||||
|
auto &el = vArray->at(0);
|
||||||
|
GithubRelease release{el};
|
||||||
|
if (!release.draft && release.GetDownloadForCurrentArchitecture() != nullptr)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
Lv2Log::error(SS("Failed to fetch update info. " << e.what()));
|
||||||
|
updateResult.errorMessage_ = e.what();
|
||||||
|
updateResult.isValid_ = false;
|
||||||
|
updateResult.isOnline_ = false;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard lock{mutex};
|
||||||
|
updateResult.UpdatePolicy(this->updatePolicy);
|
||||||
|
this->currentResult = updateResult;
|
||||||
|
SetCachedUpdateStatus(this->currentResult);
|
||||||
|
if (listener)
|
||||||
|
{
|
||||||
|
listener(this->currentResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool UpdateRelease::operator==(const UpdateRelease &other) const
|
||||||
|
{
|
||||||
|
return (updateAvailable_ == other.updateAvailable_) &&
|
||||||
|
(upgradeVersion_ == other.upgradeVersion_) &&
|
||||||
|
(upgradeVersionDisplayName_ == other.upgradeVersionDisplayName_) &&
|
||||||
|
(assetName_ == other.assetName_) &&
|
||||||
|
(updateUrl_ == other.updateUrl_);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UpdateStatus::operator==(const UpdateStatus &other) const
|
||||||
|
{
|
||||||
|
return (lastUpdateTime_ == other.lastUpdateTime_) &&
|
||||||
|
(isValid_ == other.isValid_) &&
|
||||||
|
(errorMessage_ == other.errorMessage_) &&
|
||||||
|
(isOnline_ == other.isOnline_) &&
|
||||||
|
(currentVersion_ == other.currentVersion_) &&
|
||||||
|
(currentVersionDisplayName_ == other.currentVersionDisplayName_) &&
|
||||||
|
(updatePolicy_ == other.updatePolicy_) &&
|
||||||
|
(releaseOnlyRelease_ == other.releaseOnlyRelease_) &&
|
||||||
|
(releaseOrBetaRelease_ == other.releaseOrBetaRelease_) &&
|
||||||
|
(devRelease_ == other.devRelease_);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdatePolicyT Updater::GetUpdatePolicy()
|
||||||
|
{
|
||||||
|
std::lock_guard lock{mutex};
|
||||||
|
return updatePolicy;
|
||||||
|
}
|
||||||
|
void Updater::SetUpdatePolicy(UpdatePolicyT updatePolicy)
|
||||||
|
{
|
||||||
|
std::lock_guard lock{mutex};
|
||||||
|
if (updatePolicy == this->updatePolicy)
|
||||||
|
return;
|
||||||
|
this->updatePolicy = updatePolicy;
|
||||||
|
this->currentResult.UpdatePolicy(updatePolicy);
|
||||||
|
SetCachedUpdateStatus(this->currentResult);
|
||||||
|
if (listener)
|
||||||
|
{
|
||||||
|
listener(currentResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void Updater::ForceUpdateCheck()
|
||||||
|
{
|
||||||
|
uint64_t value = UNCACHED_CHECK_NOW_EVENT;
|
||||||
|
write(this->event_writer, &value, sizeof(uint64_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateStatus::UpdateStatus()
|
||||||
|
{
|
||||||
|
currentVersion_ = PROJECT_VER;
|
||||||
|
currentVersionDisplayName_ = PROJECT_DISPLAY_VERSION;
|
||||||
|
|
||||||
|
#ifdef TEST_UPDATE
|
||||||
|
// uncomment this line to test upgrading.
|
||||||
|
currentVersion_ = "1.2.39";
|
||||||
|
currentVersionDisplayName_ = "PiPedal 1.2.39-Debug";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateStatus::ResetCurrentVersion()
|
||||||
|
{
|
||||||
|
currentVersion_ = PROJECT_VER;
|
||||||
|
currentVersionDisplayName_ = PROJECT_DISPLAY_VERSION;
|
||||||
|
|
||||||
|
#ifdef TEST_UPDATE
|
||||||
|
// uncomment this line to test upgrading.
|
||||||
|
currentVersion_ = "1.2.39";
|
||||||
|
currentVersionDisplayName_ = "PiPedal 1.2.39-Debug";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
std::chrono::system_clock::time_point UpdateStatus::LastUpdateTime() const
|
||||||
|
{
|
||||||
|
std::chrono::system_clock::duration duration{this->lastUpdateTime_};
|
||||||
|
std::chrono::system_clock::time_point tp{duration};
|
||||||
|
return tp;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateStatus::LastUpdateTime(const std::chrono::system_clock::time_point &timePoint)
|
||||||
|
{
|
||||||
|
this->lastUpdateTime_ = timePoint.time_since_epoch().count();
|
||||||
|
}
|
||||||
|
|
||||||
|
const GithubAsset *GithubRelease::GetGpgKeyForAsset(const std::string &name) const
|
||||||
|
{
|
||||||
|
std::string targetName = name + ".asc";
|
||||||
|
|
||||||
|
for (auto &asset : assets)
|
||||||
|
{
|
||||||
|
if (asset.name == targetName)
|
||||||
|
{
|
||||||
|
return &asset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
const GithubAsset *GithubRelease::GetDownloadForCurrentArchitecture() const
|
||||||
|
{
|
||||||
|
// deb package names end in {DEBIAN_ARCHITECTURE}.deb
|
||||||
|
// pipedal build gets this value from `dpkg --print-architecture`
|
||||||
|
#ifndef DEBIAN_ARCHITECTURE // deb package names end in {DEBIAN_ARCHITECTURE}.deb
|
||||||
|
#error DEBIAN_ARCHITECTURE not defined
|
||||||
|
#endif
|
||||||
|
std::string downloadEnding = SS("_" << (DEBIAN_ARCHITECTURE) << ".deb");
|
||||||
|
|
||||||
|
for (auto &asset : assets)
|
||||||
|
{
|
||||||
|
if (asset.name.ends_with(downloadEnding))
|
||||||
|
{
|
||||||
|
return &asset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateRelease::UpdateRelease()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Updater::GetSignatureUrl(const std::string &url)
|
||||||
|
{
|
||||||
|
|
||||||
|
// partialy whitelisting, partly avoiding having to parse a URL.
|
||||||
|
if (this->currentResult.releaseOnlyRelease_.UpdateUrl() == url)
|
||||||
|
{
|
||||||
|
return this->currentResult.releaseOnlyRelease_.GpgSignatureUrl();
|
||||||
|
}
|
||||||
|
if (this->currentResult.releaseOrBetaRelease_.UpdateUrl() == url)
|
||||||
|
{
|
||||||
|
return this->currentResult.releaseOrBetaRelease_.GpgSignatureUrl();
|
||||||
|
}
|
||||||
|
if (this->currentResult.devRelease_.UpdateUrl() == url)
|
||||||
|
{
|
||||||
|
return this->currentResult.devRelease_.GpgSignatureUrl();
|
||||||
|
}
|
||||||
|
throw std::runtime_error("Permission denied. No signature URL.");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Updater::GetUpdateFilename(const std::string &url)
|
||||||
|
{
|
||||||
|
|
||||||
|
// partialy whitelisting, partly avoiding having to parse a URL.
|
||||||
|
if (this->currentResult.releaseOnlyRelease_.UpdateUrl() == url)
|
||||||
|
{
|
||||||
|
return this->currentResult.releaseOnlyRelease_.AssetName();
|
||||||
|
}
|
||||||
|
if (this->currentResult.releaseOrBetaRelease_.UpdateUrl() == url)
|
||||||
|
{
|
||||||
|
return this->currentResult.releaseOrBetaRelease_.AssetName();
|
||||||
|
}
|
||||||
|
if (this->currentResult.devRelease_.UpdateUrl() == url)
|
||||||
|
{
|
||||||
|
return this->currentResult.devRelease_.AssetName();
|
||||||
|
}
|
||||||
|
throw std::runtime_error("Permission denied. Invalid url.");
|
||||||
|
}
|
||||||
|
static std::string unCRLF(const std::string &text)
|
||||||
|
{
|
||||||
|
std::ostringstream ss;
|
||||||
|
for (char c : text)
|
||||||
|
{
|
||||||
|
if (c == '\r')
|
||||||
|
continue;
|
||||||
|
if (c == '\n')
|
||||||
|
{
|
||||||
|
ss << '/';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ss << c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void removeOldSiblings(int numberToKeep, const std::filesystem::path &fileToKeep)
|
||||||
|
{
|
||||||
|
|
||||||
|
auto extension = fileToKeep.extension();
|
||||||
|
auto directory = fileToKeep.parent_path();
|
||||||
|
if (directory.empty())
|
||||||
|
return; // superstition.
|
||||||
|
struct RemoveEntry
|
||||||
|
{
|
||||||
|
fs::path path;
|
||||||
|
fs::file_time_type time;
|
||||||
|
};
|
||||||
|
std::vector<RemoveEntry> entries;
|
||||||
|
for (const auto &dirEntry : fs::directory_iterator(directory))
|
||||||
|
{
|
||||||
|
if (dirEntry.is_regular_file())
|
||||||
|
{
|
||||||
|
auto thisPath = dirEntry.path();
|
||||||
|
if (thisPath != fileToKeep)
|
||||||
|
{
|
||||||
|
if (thisPath.extension() == extension)
|
||||||
|
{
|
||||||
|
entries.push_back(
|
||||||
|
RemoveEntry{
|
||||||
|
.path = thisPath,
|
||||||
|
.time = dirEntry.last_write_time()});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::sort(
|
||||||
|
entries.begin(), entries.end(),
|
||||||
|
[](const RemoveEntry &left, const RemoveEntry &right)
|
||||||
|
{
|
||||||
|
return left.time > right.time; // by time descending
|
||||||
|
});
|
||||||
|
for (size_t i = numberToKeep; i < entries.size(); ++i)
|
||||||
|
{
|
||||||
|
fs::remove(entries[i].path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string getFingerprint(const std::string &gpgText)
|
||||||
|
{
|
||||||
|
std::string keyPosition = "using RSA key ";
|
||||||
|
size_t nPos = gpgText.find(keyPosition);
|
||||||
|
if (nPos == std::string::npos)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
nPos += keyPosition.length();
|
||||||
|
while (nPos < gpgText.length() && gpgText[nPos] == ' ')
|
||||||
|
{
|
||||||
|
++nPos;
|
||||||
|
}
|
||||||
|
auto start = nPos;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
char c = gpgText[nPos];
|
||||||
|
if (nPos >= gpgText.length() || c == ' ' || c == '\r' || c == '\n' || c == '\t')
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++nPos;
|
||||||
|
}
|
||||||
|
return gpgText.substr(start, nPos - start);
|
||||||
|
}
|
||||||
|
static bool IsSignatureGood(const std::string&gpgText)
|
||||||
|
{
|
||||||
|
std::string originPosition = "gpg: Good signature from \"";
|
||||||
|
|
||||||
|
size_t nPos = gpgText.find(originPosition);
|
||||||
|
return nPos != std::string::npos;
|
||||||
|
}
|
||||||
|
static std::string getAddress(const std::string &gpgText)
|
||||||
|
{
|
||||||
|
std::string originPosition = "gpg: Good signature from \"";
|
||||||
|
|
||||||
|
size_t nPos = gpgText.find(originPosition);
|
||||||
|
if (nPos == std::string::npos)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
nPos += originPosition.length();
|
||||||
|
auto start = nPos;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (nPos >= gpgText.length() || gpgText[nPos] == '\"' || gpgText[nPos] == '\r' || gpgText[nPos] == '\n')
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++nPos;
|
||||||
|
}
|
||||||
|
return gpgText.substr(start, nPos - start);
|
||||||
|
}
|
||||||
|
void Updater::ValidateSignature(const std::filesystem::path &file, const std::filesystem::path &signatureFile)
|
||||||
|
{
|
||||||
|
// sudo gpg --home /var/pipedal/config/gpg --verify pipedal_1.2.41_arm64.deb.asc pipedal_1.2.41_arm64.deb
|
||||||
|
// gpg: assuming signed data in 'pipedal_1.2.41_arm64.deb'
|
||||||
|
// gpg: Signature made Tue 27 Aug 2024 09:25:06 PM EDT
|
||||||
|
// gpg: using RSA key 381124E2BB4478D225D2313B2AEF3F7BD53EAA59
|
||||||
|
// gpg: Good signature from "Robin Davies <rerdavies@gmail.com>" [ultimate]
|
||||||
|
std::ostringstream ss;
|
||||||
|
ss << "--home " << PGP_UPDATE_KEYRING_PATH
|
||||||
|
<< " --verify "
|
||||||
|
<< signatureFile << " " << file;
|
||||||
|
|
||||||
|
Lv2Log::info(SS("/usr/bin/gpg " << ss.str()));
|
||||||
|
auto gpgOutput = sysExecForOutput("/usr/bin/gpg", ss.str());
|
||||||
|
if (gpgOutput.exitCode != EXIT_SUCCESS)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Update file is not correctly signed.");
|
||||||
|
}
|
||||||
|
const std::string &gpgText = gpgOutput.output;
|
||||||
|
|
||||||
|
if (!IsSignatureGood(gpgText))
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(SS("Update signature has the wrong fingerprint: " << keyId));
|
||||||
|
}
|
||||||
|
std::string origin = getAddress(gpgText);
|
||||||
|
if (origin != UPDATE_GPG_ADDRESS)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(SS("Update signature has an incorrect address." << origin));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool badOutput(const std::filesystem::path &filePath)
|
||||||
|
{
|
||||||
|
if (!fs::is_regular_file(filePath))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return fs::file_size(filePath) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void checkCurlHttpResponse(std::string errorCode)
|
||||||
|
{
|
||||||
|
if (errorCode.starts_with("%"))
|
||||||
|
{
|
||||||
|
errorCode = errorCode.substr(1);
|
||||||
|
}
|
||||||
|
int code = -999;
|
||||||
|
{
|
||||||
|
std::istringstream ss{errorCode};
|
||||||
|
ss >> code;
|
||||||
|
}
|
||||||
|
if (code == -999)
|
||||||
|
{
|
||||||
|
throw std::runtime_error(SS("Download failed. Invalid curl response: " << errorCode));
|
||||||
|
}
|
||||||
|
if (code == 200)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (code == 0)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("PiPedal server can't reach the internet.");
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::string message = SS("Download failed (HTTP error " << code << ")");
|
||||||
|
throw std::runtime_error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void Updater::DownloadUpdate(const std::string &url, std::filesystem::path *file, std::filesystem::path *signatureFile)
|
||||||
|
{
|
||||||
|
std::string filename, signatureUrl;
|
||||||
|
{
|
||||||
|
std::lock_guard lock{mutex};
|
||||||
|
filename = GetUpdateFilename(url);
|
||||||
|
signatureUrl = GetSignatureUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only permit downloading of updates from the github releases for the pipedal project.
|
||||||
|
// I don't think this will get past GetUpdateFileName, but defense in depth. We will
|
||||||
|
// not download from anywhere we don't trust.
|
||||||
|
if (!WhitelistDownloadUrl(url))
|
||||||
|
{
|
||||||
|
throw std::runtime_error(SS("Invalid update url. Downloads from this address are not permitted: " << url));
|
||||||
|
}
|
||||||
|
if (!WhitelistDownloadUrl(signatureUrl))
|
||||||
|
{
|
||||||
|
throw std::runtime_error(SS("Invalid update url. Downloads from this address are not permitted: " << signatureUrl));
|
||||||
|
}
|
||||||
|
auto downloadDirectory = WORKING_DIRECTORY / "downloads";
|
||||||
|
std::filesystem::create_directories(downloadDirectory);
|
||||||
|
|
||||||
|
auto downloadFilePath = downloadDirectory / filename;
|
||||||
|
auto downloadSignaturePath = downloadDirectory / SS(filename << ".asc");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fs::remove(downloadFilePath);
|
||||||
|
fs::remove(downloadSignaturePath);
|
||||||
|
|
||||||
|
const std::string responseOption = "-w \"%{response_code}\"";
|
||||||
|
#ifdef WIN32
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
Lv2Log::error(SS("Update download failed. " << unCRLF(curlOutput.output)));
|
||||||
|
throw std::runtime_error("PiPedal server does not have access to the internet.");
|
||||||
|
}
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
Lv2Log::error(SS("Update download failed. " << unCRLF(curlOutput.output)));
|
||||||
|
throw std::runtime_error("PiPedal server does not have access to the internet.");
|
||||||
|
}
|
||||||
|
checkCurlHttpResponse(curlOutput.output);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
removeOldSiblings(2, downloadFilePath);
|
||||||
|
removeOldSiblings(2, downloadSignaturePath);
|
||||||
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
Lv2Log::error(SS("Can't remove download siblings" << e.what()));
|
||||||
|
// and carry on.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can't only be performed under pipedal_d or root accouts.
|
||||||
|
// This is as far as you can go while debugging.
|
||||||
|
|
||||||
|
// The admin process will actually check the signature again running as root; but this is our last
|
||||||
|
// chance to give a reasonable error message, so do it here as well, because any subsequent errors
|
||||||
|
// can only go to systemd logs.
|
||||||
|
ValidateSignature(downloadFilePath, downloadSignaturePath);
|
||||||
|
*file = downloadFilePath;
|
||||||
|
*signatureFile = downloadSignaturePath;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
std::filesystem::remove(downloadFilePath);
|
||||||
|
std::filesystem::remove(downloadSignaturePath);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
JSON_MAP_BEGIN(UpdateRelease)
|
||||||
|
JSON_MAP_REFERENCE(UpdateRelease, updateAvailable)
|
||||||
|
JSON_MAP_REFERENCE(UpdateRelease, upgradeVersion)
|
||||||
|
JSON_MAP_REFERENCE(UpdateRelease, upgradeVersionDisplayName)
|
||||||
|
JSON_MAP_REFERENCE(UpdateRelease, assetName)
|
||||||
|
JSON_MAP_REFERENCE(UpdateRelease, updateUrl)
|
||||||
|
JSON_MAP_REFERENCE(UpdateRelease, gpgSignatureUrl)
|
||||||
|
JSON_MAP_END();
|
||||||
|
|
||||||
|
JSON_MAP_BEGIN(UpdateStatus)
|
||||||
|
JSON_MAP_REFERENCE(UpdateStatus, lastUpdateTime)
|
||||||
|
JSON_MAP_REFERENCE(UpdateStatus, isValid)
|
||||||
|
JSON_MAP_REFERENCE(UpdateStatus, errorMessage)
|
||||||
|
JSON_MAP_REFERENCE(UpdateStatus, isOnline)
|
||||||
|
JSON_MAP_REFERENCE(UpdateStatus, currentVersion)
|
||||||
|
JSON_MAP_REFERENCE(UpdateStatus, currentVersionDisplayName)
|
||||||
|
JSON_MAP_REFERENCE(UpdateStatus, updatePolicy)
|
||||||
|
JSON_MAP_REFERENCE(UpdateStatus, releaseOnlyRelease)
|
||||||
|
JSON_MAP_REFERENCE(UpdateStatus, releaseOrBetaRelease)
|
||||||
|
JSON_MAP_REFERENCE(UpdateStatus, devRelease)
|
||||||
|
JSON_MAP_END();
|
||||||
+158
@@ -0,0 +1,158 @@
|
|||||||
|
// 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 <string>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <thread>
|
||||||
|
#include <mutex>
|
||||||
|
#include "json.hpp"
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
class GithubRelease;
|
||||||
|
|
||||||
|
namespace pipedal
|
||||||
|
{
|
||||||
|
|
||||||
|
class Updater;
|
||||||
|
|
||||||
|
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 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.
|
||||||
|
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 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();
|
||||||
|
|
||||||
|
static void ValidateSignature(const std::filesystem::path&file, const std::filesystem::path&signatureFile);
|
||||||
|
|
||||||
|
using UpdateListener = std::function<void(const UpdateStatus &upateResult)>;
|
||||||
|
|
||||||
|
void SetUpdateListener(UpdateListener &&listener);
|
||||||
|
void CheckNow();
|
||||||
|
void Stop();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
UpdatePolicyT updatePolicy = UpdatePolicyT::ReleaseOrBeta;
|
||||||
|
using UpdateReleasePredicate = std::function<bool(const GithubRelease &githubRelease)>;
|
||||||
|
|
||||||
|
UpdateRelease getUpdateRelease(
|
||||||
|
const std::vector<GithubRelease> &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<std::thread> thread;
|
||||||
|
std::mutex mutex;
|
||||||
|
|
||||||
|
bool hasInfo = false;
|
||||||
|
UpdateStatus currentResult;
|
||||||
|
static clock::duration updateRate;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
* 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
|
||||||
|
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
|
||||||
|
If you don't, PiPedal will prompt to update your installs with the latest
|
||||||
|
installs published on github/rerdavies/pipedal.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef GITHUB_PROJECT
|
||||||
|
#define GITHUB_PROJECT "rerdavies/pipedal"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
// public key info for the private key you used to sign .deb files.
|
||||||
|
|
||||||
|
#ifndef UPDATE_GPG_ADDRESS
|
||||||
|
#define UPDATE_GPG_ADDRESS "Robin Davies <rerdavies@gmail.com>"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef UPDATE_GPG_FINGERPRINT
|
||||||
|
#define UPDATE_GPG_FINGERPRINT "381124E2BB4478D225D2313B2AEF3F7BD53EAA59"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Configuration for downloading of updates.
|
||||||
|
|
||||||
|
#define GITHUB_DOWNLOAD_PREFIX "https://github.com/" GITHUB_PROJECT "/releases/"
|
||||||
|
|
||||||
|
#define GITHUB_RELEASES_URL "https://api.github.com/repos/" GITHUB_PROJECT "/releases"
|
||||||
|
|
||||||
|
#define PGP_UPDATE_KEYRING_PATH "/var/pipedal/config/gpg"
|
||||||
|
|
||||||
|
inline bool WhitelistDownloadUrl(const std::string &downloadUrl)
|
||||||
|
{
|
||||||
|
return downloadUrl.starts_with(GITHUB_DOWNLOAD_PREFIX);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// 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 "pch.h"
|
||||||
|
#include "catch.hpp"
|
||||||
|
#include "Updater.hpp"
|
||||||
|
#include "json.hpp"
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
using namespace pipedal;
|
||||||
|
|
||||||
|
TEST_CASE( "updater test", "[updater]" ) {
|
||||||
|
int nCalls = 0;
|
||||||
|
cout << "------ upater test ----" << endl;
|
||||||
|
{ Updater updater;
|
||||||
|
|
||||||
|
|
||||||
|
updater.SetUpdateListener(
|
||||||
|
[&nCalls](const UpdateStatus&updateStatus) mutable
|
||||||
|
{
|
||||||
|
|
||||||
|
cout << "updateStatus:" << endl;
|
||||||
|
json_writer writer(cout,false);
|
||||||
|
writer.write(updateStatus);
|
||||||
|
cout << endl;
|
||||||
|
++nCalls;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
REQUIRE(nCalls == 1);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
+30
-1
@@ -158,11 +158,40 @@ TEST_CASE("json smart ptrs", "[json_smart_ptrs][Build][Dev]")
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename U>
|
||||||
|
U & VariantAs(json_variant& v)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Missing specialization.");
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline json_null &VariantAs<json_null>(json_variant& v) { return v.as_null(); }
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline bool &VariantAs<bool>(json_variant& v) { return v.as_bool(); }
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline double &VariantAs<double>(json_variant& v) { return v.as_number(); }
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline std::string &VariantAs<std::string>(json_variant& v) { return v.as_string(); }
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline std::shared_ptr<json_object> &VariantAs<std::shared_ptr<json_object>>(json_variant& v) { return v.as_object(); }
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline std::shared_ptr<json_array> &VariantAs<std::shared_ptr<json_array>>(json_variant& v) { return v.as_array(); }
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline json_variant &VariantAs<json_variant>(json_variant& v) { return v; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
void TestVariantRoundTrip(const T &value)
|
void TestVariantRoundTrip(const T &value)
|
||||||
{
|
{
|
||||||
json_variant variant(value);
|
json_variant variant(value);
|
||||||
T out = variant.as<T>();
|
T &out = VariantAs<T>(variant);
|
||||||
REQUIRE(out == value);
|
REQUIRE(out == value);
|
||||||
|
|
||||||
std::string output;
|
std::string output;
|
||||||
|
|||||||
+3
-63
@@ -54,15 +54,6 @@ using namespace pipedal;
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
bool HasAlsaDevice(const std::vector<AlsaDeviceInfo> devices, const std::string &deviceId)
|
|
||||||
{
|
|
||||||
for (auto &device : devices)
|
|
||||||
{
|
|
||||||
if (device.id_ == deviceId)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
class application_category : public boost::system::error_category
|
class application_category : public boost::system::error_category
|
||||||
{
|
{
|
||||||
@@ -142,9 +133,9 @@ int main(int argc, char *argv[])
|
|||||||
std::cout << "Usage: pipedald <doc_root> [<web_root>] [options...]\n\n"
|
std::cout << "Usage: pipedald <doc_root> [<web_root>] [options...]\n\n"
|
||||||
<< "Options:\n"
|
<< "Options:\n"
|
||||||
<< " -systemd: Log to systemd journals instead of to the console.\n"
|
<< " -systemd: Log to systemd journals instead of to the console.\n"
|
||||||
<< " -port: Port to listen on e.g. 0.0.0.0:80\n"
|
<< " -port: Port to listen on e.g. 80, or 0.0.0.0:80\n"
|
||||||
<< "Example:\n"
|
<< "Example:\n"
|
||||||
<< " pipedald /etc/pipedal/config /etc/pipedal/react -port 0.0.0.0:80 \n"
|
<< " pipedald /etc/pipedal/config /etc/pipedal/react -port 80 \n"
|
||||||
"\n"
|
"\n"
|
||||||
"Description:\n\n"
|
"Description:\n\n"
|
||||||
" Configuration is read from <doc_root>/config.json\n"
|
" Configuration is read from <doc_root>/config.json\n"
|
||||||
@@ -250,58 +241,7 @@ int main(int argc, char *argv[])
|
|||||||
(unsigned long)getpid());
|
(unsigned long)getpid());
|
||||||
}
|
}
|
||||||
|
|
||||||
auto serverSettings = model.GetJackServerSettings();
|
model.WaitForAudioDeviceToComeOnline();
|
||||||
|
|
||||||
{
|
|
||||||
// Wait for selected audio device to be initialized.
|
|
||||||
// It may take some time for ALSA to publish all available devices when rebooting.
|
|
||||||
|
|
||||||
if (serverSettings.IsValid())
|
|
||||||
{
|
|
||||||
// wait up to 15 seconds for the midi device to come online.
|
|
||||||
auto devices = model.GetAlsaDevices();
|
|
||||||
bool found = false;
|
|
||||||
if (HasAlsaDevice(devices, serverSettings.GetAlsaInputDevice()))
|
|
||||||
{
|
|
||||||
Lv2Log::info(SS("Found ALSA device " << serverSettings.GetAlsaInputDevice() << "."));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for (int i = 0; i < 5; ++i)
|
|
||||||
{
|
|
||||||
sleep(3);
|
|
||||||
devices = model.GetAlsaDevices();
|
|
||||||
if (HasAlsaDevice(devices, serverSettings.GetAlsaInputDevice()))
|
|
||||||
{
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (g_SigBreak)
|
|
||||||
exit(1);
|
|
||||||
if (!systemd)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Lv2Log::info(SS("Waiting for ALSA device " << serverSettings.GetAlsaInputDevice() << " to come online..."));
|
|
||||||
}
|
|
||||||
if (found)
|
|
||||||
{
|
|
||||||
Lv2Log::info(SS("Found ALSA device " << serverSettings.GetAlsaInputDevice() << "."));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Lv2Log::info(SS("ALSA device " << serverSettings.GetAlsaInputDevice() << " not found."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Lv2Log::info("No ALSA device selected.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// pre-cache device info before we let audio services run.
|
|
||||||
model.GetAlsaDevices();
|
|
||||||
}
|
|
||||||
|
|
||||||
// only accept signals on the main thread.
|
// only accept signals on the main thread.
|
||||||
int sig;
|
int sig;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Restart=always
|
|||||||
RestartSec=60
|
RestartSec=60
|
||||||
WorkingDirectory=/var/pipedal
|
WorkingDirectory=/var/pipedal
|
||||||
TimeoutStopSec=15
|
TimeoutStopSec=15
|
||||||
|
KillMode=process
|
||||||
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ Restart=always
|
|||||||
TimeoutStartSec=60
|
TimeoutStartSec=60
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
TimeoutStopSec=15
|
TimeoutStopSec=15
|
||||||
|
|
||||||
WorkingDirectory=/var/pipedal
|
WorkingDirectory=/var/pipedal
|
||||||
Environment=JACK_PROMISCUOUS_SERVER=audio
|
Environment=JACK_PROMISCUOUS_SERVER=audio
|
||||||
Environment=JACK_NO_AUDIO_RESERVATION=1
|
Environment=JACK_NO_AUDIO_RESERVATION=1
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
|
||||||
|
// This file overrides default settings, as set by the installer in /etc/pipdal/config.json
|
||||||
|
//
|
||||||
|
// Changes made in this file will be preserved across upgrades.
|
||||||
|
//
|
||||||
|
// To override default settings, uncomment the setting line you want to modify.
|
||||||
|
//
|
||||||
|
// You should reboot after modifying any of these settings.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Paths where PiPedal will look for LV2 Plugins.
|
||||||
|
// One or more directories, seperated by ':'
|
||||||
|
|
||||||
|
// "lv2_path": "/usr/lib/lv2:/usr/local/lib/lv2:/usr/modep/lv2",
|
||||||
|
|
||||||
|
|
||||||
|
// whether to lock process pages into memory. should be true unless running on a very memory constrained system.
|
||||||
|
// Setting to false may cause unpredictable audio dropouts.
|
||||||
|
|
||||||
|
// "mlock": true,
|
||||||
|
|
||||||
|
// Number of threads the web server should use.
|
||||||
|
|
||||||
|
// "threads" : 5,
|
||||||
|
|
||||||
|
|
||||||
|
// Whether to log individual http requests. (highly inadvisable, as requests are logged to the systemd log)
|
||||||
|
|
||||||
|
// "logHttpRequests": false,
|
||||||
|
|
||||||
|
// Log level for the web server
|
||||||
|
// { None=0,Error =1,Warning =2,Info = 3, Debug=4}
|
||||||
|
|
||||||
|
// "logLevel": 3,
|
||||||
|
|
||||||
|
// Maximum filesize to allow when uploading
|
||||||
|
|
||||||
|
// "maxUploadSize": 536870912 // 512MiB
|
||||||
|
|
||||||
|
|
||||||
|
// does nothing. avoids chasing ','s.
|
||||||
|
"end" : true
|
||||||
|
}
|
||||||
+1
-1
@@ -22,7 +22,7 @@
|
|||||||
#include <Uri.hpp>
|
#include <Uri.hpp>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
using namespace piddle;
|
using namespace pipedal;
|
||||||
|
|
||||||
TEST_CASE( "uri test", "[uri]" ) {
|
TEST_CASE( "uri test", "[uri]" ) {
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user