Shared Catalog
ci / checks (push) Failing after 1h5m14s

This commit is contained in:
khodowany
2026-09-17 01:25:26 -05:00
parent 5941a4cd49
commit fa385205e2
23 changed files with 742 additions and 242 deletions
-36
View File
@@ -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;
+1 -1
View File
@@ -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{};
+54
View File
@@ -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
View File
@@ -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()));
}
+28 -3
View File
@@ -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());
+22
View File
@@ -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
View File
@@ -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 {};
}
+114
View File
@@ -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
View File
@@ -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();
}