@@ -1,42 +1,6 @@
|
||||
#include <vamp_pico/input.hpp>
|
||||
|
||||
|
||||
auto vamp_pico::gesture_state::update(std::bool_t touching, std::uint64_t now_ms) -> std::optional<vamp_protocol::page_command> {
|
||||
constexpr auto hold_ms = std::uint64_t{700};
|
||||
constexpr auto double_tap_ms = std::uint64_t{220};
|
||||
auto command = std::optional<vamp_protocol::page_command>{};
|
||||
if (pending_tap_ && !touching_ && now_ms - last_up_ms_ > double_tap_ms) {
|
||||
pending_tap_ = false;
|
||||
command = vamp_protocol::page_command::NEXT;
|
||||
}
|
||||
if (touching && !touching_) {
|
||||
touching_ = true;
|
||||
hold_sent_ = false;
|
||||
down_ms_ = now_ms;
|
||||
}
|
||||
if (touching_ && !hold_sent_ && now_ms - down_ms_ >= hold_ms) {
|
||||
hold_sent_ = true;
|
||||
pending_tap_ = false;
|
||||
command = vamp_protocol::page_command::SELECT;
|
||||
}
|
||||
if (!touching && touching_) {
|
||||
touching_ = false;
|
||||
if (!hold_sent_) {
|
||||
if (pending_tap_ && now_ms - last_up_ms_ <= double_tap_ms) {
|
||||
pending_tap_ = false;
|
||||
command = vamp_protocol::page_command::PREVIOUS;
|
||||
} else {
|
||||
if (pending_tap_) {
|
||||
command = vamp_protocol::page_command::NEXT;
|
||||
}
|
||||
pending_tap_ = true;
|
||||
last_up_ms_ = now_ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
auto vamp_pico::request_scheduler::enqueue(vamp_protocol::page_command command) -> enqueue_result {
|
||||
if (size_ == commands_.size()) {
|
||||
return enqueue_result::BUSY;
|
||||
|
||||
@@ -64,7 +64,7 @@ auto main() -> int {
|
||||
}
|
||||
auto remote = vamp_pico::remote_display_state{};
|
||||
auto status = link_status::RETRYING;
|
||||
auto gesture = vamp_pico::gesture_state{};
|
||||
auto gesture = vamp_protocol::gesture_state{};
|
||||
auto scheduler = vamp_pico::request_scheduler{};
|
||||
// Keep the owned request buffers off the Pico's small main stack.
|
||||
static auto client = vamp_pico::http_client{};
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#include <vamp_protocol/gesture.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr auto hold_ms = std::uint64_t{700};
|
||||
constexpr auto double_tap_ms = std::uint64_t{220};
|
||||
|
||||
}
|
||||
|
||||
auto vamp_protocol::gesture_state::update(std::bool_t touching, std::uint64_t now_ms) -> std::optional<vamp_protocol::page_command> {
|
||||
auto command = std::optional<vamp_protocol::page_command>{};
|
||||
if (pending_tap_ && !touching_ && now_ms - last_up_ms_ > double_tap_ms) {
|
||||
pending_tap_ = false;
|
||||
command = vamp_protocol::page_command::NEXT;
|
||||
}
|
||||
if (touching && !touching_) {
|
||||
touching_ = true;
|
||||
hold_sent_ = false;
|
||||
down_ms_ = now_ms;
|
||||
}
|
||||
if (touching_ && !hold_sent_ && now_ms - down_ms_ >= hold_ms) {
|
||||
hold_sent_ = true;
|
||||
pending_tap_ = false;
|
||||
command = vamp_protocol::page_command::SELECT;
|
||||
}
|
||||
if (!touching && touching_) {
|
||||
touching_ = false;
|
||||
if (!hold_sent_) {
|
||||
if (pending_tap_ && now_ms - last_up_ms_ <= double_tap_ms) {
|
||||
pending_tap_ = false;
|
||||
command = vamp_protocol::page_command::PREVIOUS;
|
||||
} else {
|
||||
if (pending_tap_) {
|
||||
command = vamp_protocol::page_command::NEXT;
|
||||
}
|
||||
pending_tap_ = true;
|
||||
last_up_ms_ = now_ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
auto vamp_protocol::gesture_state::reset() -> void {
|
||||
*this = {};
|
||||
}
|
||||
|
||||
auto vamp_protocol::gesture_state::pending_deadline() const -> std::optional<std::uint64_t> {
|
||||
if (!pending_tap_ || touching_) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return last_up_ms_ + double_tap_ms + 1U;
|
||||
}
|
||||
+68
-30
@@ -5,7 +5,6 @@
|
||||
#include <cstdlib>
|
||||
#include <expected>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <iostream>
|
||||
@@ -16,6 +15,8 @@
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
@@ -59,6 +60,30 @@ auto bind_port(vamp_zero::config const &config) -> std::uint16_t {
|
||||
return endpoint->port == 0 ? 8080 : endpoint->port;
|
||||
}
|
||||
|
||||
auto validate_runtime_destination(std::filesystem::path const &destination) -> std::expected<void, modulus::error> {
|
||||
auto error = std::error_code{};
|
||||
auto status = std::filesystem::symlink_status(destination, error);
|
||||
if (error && error != std::errc::no_such_file_or_directory) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to inspect runtime file " + destination.string() + ": " + error.message()));
|
||||
}
|
||||
if (status.type() != std::filesystem::file_type::not_found && !std::filesystem::is_regular_file(status)) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "runtime destination must be absent or a regular file: " + destination.string()));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
struct runtime_temporary {
|
||||
std::filesystem::path path;
|
||||
std::int32_t descriptor{-1};
|
||||
~runtime_temporary() {
|
||||
if (descriptor >= 0) {
|
||||
::close(descriptor);
|
||||
}
|
||||
auto ignored = std::error_code{};
|
||||
std::filesystem::remove(path, ignored);
|
||||
}
|
||||
};
|
||||
|
||||
auto write_text_atomic(std::filesystem::path const &destination, std::string const &text) -> std::expected<void, modulus::error> {
|
||||
auto error = std::error_code{};
|
||||
if (!destination.parent_path().empty()) {
|
||||
@@ -67,23 +92,34 @@ auto write_text_atomic(std::filesystem::path const &destination, std::string con
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to create runtime directory: " + error.message()));
|
||||
}
|
||||
}
|
||||
auto temporary = destination;
|
||||
temporary += ".tmp";
|
||||
auto output = std::ofstream{temporary, std::ios::binary | std::ios::trunc};
|
||||
if (!output) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to create runtime file: " + temporary.string()));
|
||||
auto valid = validate_runtime_destination(destination);
|
||||
if (!valid) {
|
||||
return valid;
|
||||
}
|
||||
output << text;
|
||||
output.close();
|
||||
if (!output) {
|
||||
std::filesystem::remove(temporary, error);
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to write runtime file: " + temporary.string()));
|
||||
auto pattern = destination.string() + ".XXXXXX";
|
||||
auto descriptor = ::mkstemp(pattern.data());
|
||||
if (descriptor < 0) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to create runtime file for " + destination.string() + ": " + std::error_code{errno, std::generic_category()}.message()));
|
||||
}
|
||||
std::filesystem::rename(temporary, destination, error);
|
||||
auto temporary = runtime_temporary{pattern, descriptor};
|
||||
for (auto offset = std::size_t{}; offset < text.size();) {
|
||||
auto written = ::write(descriptor, text.data() + offset, text.size() - offset);
|
||||
if (written < 0 && errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
if (written <= 0) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to write runtime file: " + temporary.path.string()));
|
||||
}
|
||||
offset += static_cast<std::size_t>(written);
|
||||
}
|
||||
auto closed = ::close(descriptor);
|
||||
temporary.descriptor = -1;
|
||||
if (closed != 0) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to close runtime file: " + temporary.path.string()));
|
||||
}
|
||||
std::filesystem::rename(temporary.path, destination, error);
|
||||
if (error) {
|
||||
auto message = error.message();
|
||||
std::filesystem::remove(temporary, error);
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to publish runtime file: " + message));
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to publish runtime file " + destination.string() + ": " + error.message()));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -162,9 +198,9 @@ auto validate_score_pages(boost::asio::any_io_executor executor, vamp_zero::data
|
||||
co_return std::unexpected(score.error());
|
||||
}
|
||||
for (auto page = std::size_t{}; page < vamp_zero::score_page_count(*score); ++page) {
|
||||
auto path = vamp_zero::validated_page_path(*score, page);
|
||||
if (!path) {
|
||||
co_return std::unexpected(path.error());
|
||||
auto valid = vamp_zero::validate_score_page(*score, page);
|
||||
if (!valid) {
|
||||
co_return std::unexpected(valid.error());
|
||||
}
|
||||
}
|
||||
co_return std::expected<void, modulus::error>{};
|
||||
@@ -191,6 +227,10 @@ auto validate_catalogue(boost::asio::any_io_executor executor, vamp_zero::databa
|
||||
}
|
||||
|
||||
auto probe_runtime_directory(vamp_zero::config const &config) -> std::expected<void, modulus::error> {
|
||||
auto valid = validate_runtime_destination(config.runtime.heartbeat_path);
|
||||
if (!valid) {
|
||||
return valid;
|
||||
}
|
||||
auto directory = std::filesystem::path{config.runtime.heartbeat_path}.parent_path();
|
||||
if (directory.empty()) {
|
||||
directory = ".";
|
||||
@@ -201,24 +241,22 @@ auto probe_runtime_directory(vamp_zero::config const &config) -> std::expected<v
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to create runtime directory: " + error.message()));
|
||||
}
|
||||
auto pattern = (directory / ".vamp-readiness-XXXXXX").string();
|
||||
if (::mkdtemp(pattern.data()) == nullptr) {
|
||||
auto descriptor = ::mkstemp(pattern.data());
|
||||
if (descriptor < 0) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE,
|
||||
"failed to create runtime probe: " + std::error_code{errno, std::generic_category()}.message()));
|
||||
}
|
||||
|
||||
struct cleanup {
|
||||
std::filesystem::path path;
|
||||
~cleanup() {
|
||||
auto ignored = std::error_code{};
|
||||
std::filesystem::remove_all(path, ignored);
|
||||
}
|
||||
} probe{pattern};
|
||||
|
||||
auto written = write_text_atomic(probe.path / "probe", "ready\n");
|
||||
auto probe = runtime_temporary{pattern, descriptor};
|
||||
auto closed = ::close(descriptor);
|
||||
probe.descriptor = -1;
|
||||
if (closed != 0) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to close runtime probe: " + pattern));
|
||||
}
|
||||
auto written = write_text_atomic(probe.path, "ready\n");
|
||||
if (!written) {
|
||||
return written;
|
||||
}
|
||||
std::filesystem::remove_all(probe.path, error);
|
||||
std::filesystem::remove(probe.path, error);
|
||||
if (error) {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to remove runtime probe: " + error.message()));
|
||||
}
|
||||
|
||||
@@ -203,6 +203,7 @@ CREATE TABLE IF NOT EXISTS practice_exercises (
|
||||
if (ensured) {
|
||||
ensured = co_await execute_schema_statement(connection, "CREATE TRIGGER IF NOT EXISTS invalidate_" + std::string{table} + "_metadata AFTER UPDATE OF source_json ON " + table + " WHEN NEW.source_json != OLD.source_json BEGIN UPDATE " + table + " SET measure_count = NULL, total_pages = NULL, authored_key_signature = NULL WHERE id = NEW.id; END");
|
||||
}
|
||||
index_statements.push_back("CREATE INDEX IF NOT EXISTS idx_" + std::string{table} + "_pending_metadata ON " + table + "(id) WHERE measure_count IS NULL OR total_pages IS NULL OR authored_key_signature IS NULL");
|
||||
}
|
||||
for (auto &statement : index_statements) {
|
||||
if (!ensured) {
|
||||
@@ -255,8 +256,10 @@ auto now_ms() -> std::int64_t {
|
||||
.count();
|
||||
}
|
||||
|
||||
auto backfill_metadata(noto::connection &connection, std::string const &table) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||
auto backfill_metadata(boost::asio::any_io_executor executor, noto::connection &connection, std::string const &table) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||
for (;;) {
|
||||
// Bound immediate coroutine completion chains even for very large catalogues.
|
||||
co_await boost::asio::post(executor, boost::asio::use_awaitable);
|
||||
auto row = co_await connection.async_execute("SELECT id, source_path, source_json FROM " + table + " WHERE measure_count IS NULL OR total_pages IS NULL OR authored_key_signature IS NULL LIMIT 1");
|
||||
if (!row) {
|
||||
co_return std::unexpected(row.error());
|
||||
@@ -283,6 +286,20 @@ auto backfill_metadata(noto::connection &connection, std::string const &table) -
|
||||
}
|
||||
}
|
||||
|
||||
auto restore_imported_metadata(noto::connection &connection, std::string const &table, std::string const &id, vamp_zero::score_document const &document) -> boost::asio::awaitable<std::expected<void, modulus::error>> {
|
||||
auto params = std::vector<noto::sqlite_value>{
|
||||
noto::sqlite_value{static_cast<std::int64_t>(document.measure_count)},
|
||||
noto::sqlite_value{static_cast<std::int64_t>(document.pages.size())},
|
||||
noto::sqlite_value{document.authored_key_signature},
|
||||
noto::sqlite_value{id},
|
||||
};
|
||||
auto updated = co_await connection.async_execute("UPDATE " + table + " SET measure_count = ?1, total_pages = ?2, authored_key_signature = ?3 WHERE id = ?4 AND catalog_managed = 1 AND (measure_count IS NULL OR total_pages IS NULL OR authored_key_signature IS NULL)", std::move(params));
|
||||
if (!updated) {
|
||||
co_return std::unexpected(updated.error());
|
||||
}
|
||||
co_return std::expected<void, modulus::error>{};
|
||||
}
|
||||
|
||||
auto count_value(noto::sqlite_value const &value) -> std::size_t {
|
||||
auto count = std::get_if<std::int64_t>(&value.value);
|
||||
return count == nullptr || *count <= 0 ? 0U : static_cast<std::size_t>(*count);
|
||||
@@ -394,6 +411,7 @@ auto vamp_zero::async_import_catalogs(boost::asio::any_io_executor executor, vam
|
||||
if (!imported) {
|
||||
break;
|
||||
}
|
||||
co_await boost::asio::post(executor, boost::asio::use_awaitable);
|
||||
auto document = vamp_zero::read_score_document(path);
|
||||
if (!document) {
|
||||
imported = std::unexpected(document.error());
|
||||
@@ -445,6 +463,8 @@ WHERE songs.catalog_managed = 1
|
||||
std::move(params));
|
||||
if (!inserted) {
|
||||
imported = std::unexpected(inserted.error());
|
||||
} else {
|
||||
imported = co_await restore_imported_metadata(connection, "songs", part.id, *document);
|
||||
}
|
||||
}
|
||||
auto practice_ids = std::vector<std::string>{};
|
||||
@@ -453,6 +473,7 @@ WHERE songs.catalog_managed = 1
|
||||
if (!imported) {
|
||||
break;
|
||||
}
|
||||
co_await boost::asio::post(executor, boost::asio::use_awaitable);
|
||||
auto document = vamp_zero::read_score_document(path);
|
||||
if (!document) {
|
||||
imported = std::unexpected(document.error());
|
||||
@@ -526,6 +547,10 @@ WHERE practice_exercises.catalog_managed = 1
|
||||
imported = std::unexpected(inserted.error());
|
||||
break;
|
||||
}
|
||||
imported = co_await restore_imported_metadata(connection, "practice_exercises", variant.id, *document);
|
||||
if (!imported) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (imported) {
|
||||
@@ -565,7 +590,7 @@ auto vamp_zero::async_load_performance_library(boost::asio::any_io_executor exec
|
||||
if (!connected) {
|
||||
co_return std::unexpected(connected.error());
|
||||
}
|
||||
auto filled = co_await backfill_metadata(connection, "songs");
|
||||
auto filled = co_await backfill_metadata(executor, connection, "songs");
|
||||
if (!filled) {
|
||||
static_cast<void>(co_await connection.async_close());
|
||||
co_return std::unexpected(filled.error());
|
||||
@@ -621,7 +646,7 @@ auto vamp_zero::async_load_practice_library(boost::asio::any_io_executor executo
|
||||
if (!connected) {
|
||||
co_return std::unexpected(connected.error());
|
||||
}
|
||||
auto filled = co_await backfill_metadata(connection, "practice_exercises");
|
||||
auto filled = co_await backfill_metadata(executor, connection, "practice_exercises");
|
||||
if (!filled) {
|
||||
static_cast<void>(co_await connection.async_close());
|
||||
co_return std::unexpected(filled.error());
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
@@ -96,6 +97,27 @@ auto vamp_zero::score_paths(std::string const &assets_path, vamp_zero::score_cat
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::UNAVAILABLE, "failed to scan score assets: " + error.message()));
|
||||
}
|
||||
std::sort(paths.begin(), paths.end());
|
||||
if (practice) {
|
||||
auto exercises = std::map<std::string, std::filesystem::path>{};
|
||||
auto variants = std::map<std::string, std::filesystem::path>{};
|
||||
for (auto const &path : paths) {
|
||||
auto relative = path.lexically_relative(root);
|
||||
auto exercise_path = relative.parent_path();
|
||||
auto id = std::string{};
|
||||
for (auto const &part : exercise_path) {
|
||||
id += (id.empty() ? "" : "-") + part.string();
|
||||
}
|
||||
auto [exercise, added] = exercises.emplace(id, relative);
|
||||
if (!added && exercise->second.parent_path() != exercise_path) {
|
||||
return std::unexpected(invalid("practice exercise ID collision '" + id + "': " + (root / exercise->second).string() + " and " + path.string()));
|
||||
}
|
||||
auto variant_id = id + "-" + path.stem().string();
|
||||
auto [variant, unique] = variants.emplace(variant_id, relative);
|
||||
if (!unique) {
|
||||
return std::unexpected(invalid("practice variant ID collision '" + variant_id + "': " + (root / variant->second).string() + " and " + path.string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
|
||||
+49
-3
@@ -5,6 +5,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include <openssl/sha.h>
|
||||
#include <raylib.h>
|
||||
|
||||
#include <vamp_zero/errors.hpp>
|
||||
#include <vamp_zero/score.hpp>
|
||||
@@ -46,8 +47,15 @@ auto vamp_zero::score_page_count(vamp_zero::score_document const &score) -> std:
|
||||
return score.pages.size();
|
||||
}
|
||||
|
||||
auto vamp_zero::validated_page_path(vamp_zero::score_document const &score, std::size_t index) -> std::expected<std::string, modulus::error> {
|
||||
auto fail = [&](std::string message) -> std::expected<std::string, modulus::error> {
|
||||
namespace {
|
||||
|
||||
struct page_file {
|
||||
std::string path;
|
||||
std::vector<std::uchar_t> bytes;
|
||||
};
|
||||
|
||||
auto read_page(vamp_zero::score_document const &score, std::size_t index) -> std::expected<page_file, modulus::error> {
|
||||
auto fail = [&](std::string message) -> std::expected<page_file, modulus::error> {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, score.source_path + ": " + std::move(message) + "; regenerate score page assets"));
|
||||
};
|
||||
if (index >= score.pages.size()) {
|
||||
@@ -90,5 +98,43 @@ auto vamp_zero::validated_page_path(vamp_zero::score_document const &score, std:
|
||||
if (hash != score.pages[index].sha256) {
|
||||
return fail("score page checksum mismatch");
|
||||
}
|
||||
return path.string();
|
||||
return page_file{path.string(), std::move(bytes)};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
auto vamp_zero::validated_page_path(vamp_zero::score_document const &score, std::size_t index) -> std::expected<std::string, modulus::error> {
|
||||
auto page = read_page(score, index);
|
||||
if (!page) {
|
||||
return std::unexpected(page.error());
|
||||
}
|
||||
return std::move(page->path);
|
||||
}
|
||||
|
||||
auto vamp_zero::validate_score_page(vamp_zero::score_document const &score, std::size_t index) -> std::expected<void, modulus::error> {
|
||||
auto page = read_page(score, index);
|
||||
if (!page) {
|
||||
return std::unexpected(page.error());
|
||||
}
|
||||
auto fail = [&](std::string message) -> std::expected<void, modulus::error> {
|
||||
return std::unexpected(vamp_zero::make_error(vamp_zero::error_code::INVALID_ARGUMENT, page->path + ": " + message + "; regenerate score page assets"));
|
||||
};
|
||||
auto const &bytes = page->bytes;
|
||||
constexpr auto ihdr = std::array<std::uchar_t, 8>{0, 0, 0, 13, 'I', 'H', 'D', 'R'};
|
||||
if (bytes.size() < 33U || !std::equal(ihdr.begin(), ihdr.end(), bytes.begin() + 8)) {
|
||||
return fail("invalid PNG header");
|
||||
}
|
||||
auto dimension = [&](std::size_t offset) -> std::uint32_t {
|
||||
return (static_cast<std::uint32_t>(bytes[offset]) << 24U) | (static_cast<std::uint32_t>(bytes[offset + 1U]) << 16U) | (static_cast<std::uint32_t>(bytes[offset + 2U]) << 8U) | bytes[offset + 3U];
|
||||
};
|
||||
if (dimension(16U) != 976U || dimension(20U) != 484U) {
|
||||
return fail("invalid score page dimensions");
|
||||
}
|
||||
auto image = LoadImageFromMemory(".png", bytes.data(), static_cast<std::int32_t>(bytes.size()));
|
||||
auto valid = image.data != nullptr && image.width == 976 && image.height == 484;
|
||||
UnloadImage(image);
|
||||
if (!valid) {
|
||||
return fail("failed to decode score page PNG");
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
#include <vamp_zero/touch_input.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
|
||||
#include <vamp_protocol/gesture.hpp>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
auto milliseconds(std::chrono::steady_clock::time_point time) -> std::uint64_t {
|
||||
return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(time.time_since_epoch()).count());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct vamp_zero::touch_input::state : std::enable_shared_from_this<state> {
|
||||
state(boost::asio::any_io_executor executor, std::function<void(vamp_protocol::page_command)> handler)
|
||||
: timer{std::move(executor)}, handler{std::move(handler)} {}
|
||||
auto send(std::optional<vamp_protocol::page_command> command) -> void {
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
boost::asio::post(timer.get_executor(), [weak = weak_from_this(), command = *command] {
|
||||
auto self = weak.lock();
|
||||
if (!self) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
auto lock = std::scoped_lock{self->mutex};
|
||||
if (self->stopped) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
self->handler(command);
|
||||
});
|
||||
}
|
||||
auto schedule() -> void {
|
||||
auto generation = ++timer_generation;
|
||||
timer.cancel();
|
||||
auto deadline = gesture.pending_deadline();
|
||||
if (!deadline) {
|
||||
return;
|
||||
}
|
||||
timer.expires_at(std::chrono::steady_clock::time_point{std::chrono::milliseconds{*deadline}});
|
||||
timer.async_wait([weak = weak_from_this(), generation](boost::system::error_code const &error) {
|
||||
auto self = weak.lock();
|
||||
if (error || !self) {
|
||||
return;
|
||||
}
|
||||
auto lock = std::scoped_lock{self->mutex};
|
||||
if (self->stopped || self->timer_generation != generation) {
|
||||
return;
|
||||
}
|
||||
self->send(self->gesture.update(false, milliseconds(std::chrono::steady_clock::now())));
|
||||
self->schedule();
|
||||
});
|
||||
}
|
||||
boost::asio::steady_timer timer;
|
||||
std::function<void(vamp_protocol::page_command)> handler;
|
||||
std::mutex mutex{};
|
||||
vamp_protocol::gesture_state gesture{};
|
||||
std::optional<std::int32_t> active_touch{};
|
||||
std::uint64_t timer_generation{};
|
||||
std::bool_t stopped{};
|
||||
};
|
||||
|
||||
vamp_zero::touch_input::touch_input(boost::asio::any_io_executor executor, std::function<void(vamp_protocol::page_command)> handler)
|
||||
: state_{std::make_shared<state>(std::move(executor), std::move(handler))} {}
|
||||
|
||||
vamp_zero::touch_input::~touch_input() {
|
||||
stop();
|
||||
}
|
||||
|
||||
auto vamp_zero::touch_input::handle(luma::touch_event const &event) -> std::bool_t {
|
||||
auto lock = std::scoped_lock{state_->mutex};
|
||||
if (state_->stopped || !state_->handler) {
|
||||
return false;
|
||||
}
|
||||
auto consumed = std::bool_t{false};
|
||||
for (auto const &point : event.points) {
|
||||
if (point.phase == luma::touch_phase::DOWN && !state_->active_touch) {
|
||||
state_->active_touch = point.id;
|
||||
} else if (!state_->active_touch || point.id != *state_->active_touch) {
|
||||
continue;
|
||||
}
|
||||
consumed = true;
|
||||
if (point.phase == luma::touch_phase::CANCEL) {
|
||||
state_->active_touch.reset();
|
||||
state_->gesture.reset();
|
||||
} else {
|
||||
auto touching = point.phase != luma::touch_phase::UP;
|
||||
if (!touching) {
|
||||
state_->active_touch.reset();
|
||||
}
|
||||
state_->send(state_->gesture.update(touching, milliseconds(event.time)));
|
||||
}
|
||||
state_->schedule();
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
||||
auto vamp_zero::touch_input::stop() -> void {
|
||||
auto lock = std::scoped_lock{state_->mutex};
|
||||
state_->stopped = true;
|
||||
state_->active_touch.reset();
|
||||
state_->gesture.reset();
|
||||
state_->schedule();
|
||||
}
|
||||
+6
-141
@@ -1,20 +1,16 @@
|
||||
#include <vamp_zero/ui.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/asio/any_io_executor.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
|
||||
#include <luma.hpp>
|
||||
|
||||
@@ -27,27 +23,15 @@
|
||||
#include <vamp_zero/parse.hpp>
|
||||
#include <vamp_zero/score.hpp>
|
||||
#include <vamp_zero/score_loader.hpp>
|
||||
#include <vamp_zero/touch_input.hpp>
|
||||
|
||||
|
||||
struct vamp_zero::ui_runtime : std::enable_shared_from_this<vamp_zero::ui_runtime> {
|
||||
struct vamp_zero::ui_runtime {
|
||||
ui_runtime(boost::asio::any_io_executor executor, std::function<void(vamp_protocol::page_command)> handler)
|
||||
: executor_{std::move(executor)}, tap_timer_{executor_}, command_handler_{std::move(handler)} {}
|
||||
auto handle_touch(luma::touch_event const &event) -> std::bool_t;
|
||||
auto send_touch_command(vamp_protocol::page_command command) -> void;
|
||||
auto arm_pending_tap_locked() -> void;
|
||||
auto cancel_pending_tap_locked() -> void;
|
||||
boost::asio::any_io_executor executor_;
|
||||
boost::asio::steady_timer tap_timer_;
|
||||
std::function<void(vamp_protocol::page_command)> command_handler_{};
|
||||
: touch_{std::move(executor), std::move(handler)} {}
|
||||
vamp_zero::touch_input touch_;
|
||||
std::mutex mutex_{};
|
||||
vamp_zero::ui_state state_{};
|
||||
std::mutex touch_mutex_{};
|
||||
std::optional<std::int32_t> active_touch_id_{};
|
||||
std::chrono::steady_clock::time_point touch_down_time_{};
|
||||
std::chrono::steady_clock::time_point last_tap_time_{};
|
||||
std::bool_t hold_sent_{};
|
||||
std::bool_t pending_tap_{};
|
||||
std::bool_t stopped_{};
|
||||
};
|
||||
|
||||
namespace {
|
||||
@@ -95,9 +79,6 @@ struct score_draw_cache {
|
||||
std::weak_ptr<vamp_zero::score_document const> document{};
|
||||
};
|
||||
|
||||
constexpr auto touch_hold_duration = std::chrono::milliseconds{700};
|
||||
constexpr auto touch_double_tap_duration = std::chrono::milliseconds{220};
|
||||
|
||||
auto bind_port(vamp_zero::config const &config) -> std::uint16_t {
|
||||
auto const &bind = config.control.endpoint.bind;
|
||||
auto colon = bind.rfind(':');
|
||||
@@ -561,7 +542,7 @@ public:
|
||||
}
|
||||
}
|
||||
auto handle_touch(luma::touch_event const &event) -> std::bool_t override {
|
||||
return runtime_->handle_touch(event);
|
||||
return runtime_->touch_.handle(event);
|
||||
}
|
||||
private:
|
||||
std::shared_ptr<vamp_zero::ui_runtime> runtime_;
|
||||
@@ -583,117 +564,6 @@ vamp_zero::zero_ui::~zero_ui() {
|
||||
stop();
|
||||
}
|
||||
|
||||
auto vamp_zero::ui_runtime::send_touch_command(vamp_protocol::page_command command) -> void {
|
||||
if (!command_handler_) {
|
||||
return;
|
||||
}
|
||||
boost::asio::post(executor_, [weak = weak_from_this(), command] {
|
||||
auto self = weak.lock();
|
||||
if (!self) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
auto lock = std::scoped_lock{self->touch_mutex_};
|
||||
if (self->stopped_) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
self->command_handler_(command);
|
||||
});
|
||||
}
|
||||
|
||||
auto vamp_zero::ui_runtime::cancel_pending_tap_locked() -> void {
|
||||
pending_tap_ = false;
|
||||
}
|
||||
|
||||
auto vamp_zero::ui_runtime::arm_pending_tap_locked() -> void {
|
||||
tap_timer_.expires_after(touch_double_tap_duration);
|
||||
tap_timer_.async_wait([weak = weak_from_this()](boost::system::error_code const &error) {
|
||||
auto self = weak.lock();
|
||||
if (error || !self) {
|
||||
return;
|
||||
}
|
||||
auto send_next = std::bool_t{false};
|
||||
{
|
||||
auto lock = std::scoped_lock{self->touch_mutex_};
|
||||
send_next = self->pending_tap_ && !self->stopped_;
|
||||
self->pending_tap_ = false;
|
||||
}
|
||||
if (send_next) {
|
||||
self->send_touch_command(vamp_protocol::page_command::NEXT);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
auto vamp_zero::ui_runtime::handle_touch(luma::touch_event const &event) -> std::bool_t {
|
||||
if (!command_handler_ || event.points.empty()) {
|
||||
return false;
|
||||
}
|
||||
auto consumed = std::bool_t{false};
|
||||
auto commands = std::vector<vamp_protocol::page_command>{};
|
||||
auto arm_tap = std::bool_t{false};
|
||||
{
|
||||
auto lock = std::scoped_lock{touch_mutex_};
|
||||
if (stopped_) {
|
||||
return false;
|
||||
}
|
||||
for (auto const &point : event.points) {
|
||||
if (point.phase == luma::touch_phase::DOWN && !active_touch_id_) {
|
||||
active_touch_id_ = point.id;
|
||||
touch_down_time_ = event.time;
|
||||
hold_sent_ = false;
|
||||
tap_timer_.cancel();
|
||||
consumed = true;
|
||||
continue;
|
||||
}
|
||||
if (!active_touch_id_ || point.id != *active_touch_id_) {
|
||||
continue;
|
||||
}
|
||||
consumed = true;
|
||||
if (point.phase == luma::touch_phase::CANCEL) {
|
||||
active_touch_id_.reset();
|
||||
hold_sent_ = false;
|
||||
cancel_pending_tap_locked();
|
||||
tap_timer_.cancel();
|
||||
continue;
|
||||
}
|
||||
if (point.phase == luma::touch_phase::MOVE) {
|
||||
if (!hold_sent_ && event.time - touch_down_time_ >= touch_hold_duration) {
|
||||
hold_sent_ = true;
|
||||
cancel_pending_tap_locked();
|
||||
tap_timer_.cancel();
|
||||
commands.push_back(vamp_protocol::page_command::SELECT);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (point.phase == luma::touch_phase::UP) {
|
||||
active_touch_id_.reset();
|
||||
if (!hold_sent_ && event.time - touch_down_time_ >= touch_hold_duration) {
|
||||
commands.push_back(vamp_protocol::page_command::SELECT);
|
||||
} else if (!hold_sent_) {
|
||||
if (pending_tap_ && event.time - last_tap_time_ <= touch_double_tap_duration) {
|
||||
cancel_pending_tap_locked();
|
||||
tap_timer_.cancel();
|
||||
commands.push_back(vamp_protocol::page_command::PREVIOUS);
|
||||
} else {
|
||||
pending_tap_ = true;
|
||||
last_tap_time_ = event.time;
|
||||
arm_tap = true;
|
||||
}
|
||||
}
|
||||
hold_sent_ = false;
|
||||
}
|
||||
}
|
||||
if (arm_tap) {
|
||||
arm_pending_tap_locked();
|
||||
}
|
||||
}
|
||||
for (auto command : commands) {
|
||||
send_touch_command(command);
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
||||
auto vamp_zero::zero_ui::run() -> std::expected<void, modulus::error> {
|
||||
if (auto status = application_->set_root(std::make_unique<vamp_root_control>(runtime_, score_loader_, *application_)); !status) {
|
||||
return status;
|
||||
@@ -704,12 +574,7 @@ auto vamp_zero::zero_ui::run() -> std::expected<void, modulus::error> {
|
||||
}
|
||||
|
||||
auto vamp_zero::zero_ui::stop() -> void {
|
||||
{
|
||||
auto lock = std::scoped_lock{runtime_->touch_mutex_};
|
||||
runtime_->stopped_ = true;
|
||||
runtime_->pending_tap_ = false;
|
||||
runtime_->tap_timer_.cancel();
|
||||
}
|
||||
runtime_->touch_.stop();
|
||||
score_loader_->stop();
|
||||
application_->request_stop();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user