@@ -0,0 +1,137 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <charconv>
|
||||
#include <string_view>
|
||||
|
||||
#include <vamp_protocol/page_control.hpp>
|
||||
|
||||
|
||||
namespace vamp_pico {
|
||||
|
||||
constexpr auto digit_width = std::size_t{22};
|
||||
constexpr auto max_pages = std::uint32_t{10000};
|
||||
|
||||
inline auto counter_text(std::uint32_t page, std::uint32_t pages) -> std::array<std::char_t, 12> {
|
||||
auto text = std::array<std::char_t, 12>{};
|
||||
if (pages == 0U || pages > max_pages || page == 0U || page > pages) {
|
||||
return text;
|
||||
}
|
||||
auto first = std::to_chars(text.data(), text.data() + text.size() - 1U, page);
|
||||
*first.ptr = '/';
|
||||
std::to_chars(first.ptr + 1U, text.data() + text.size() - 1U, pages);
|
||||
return text;
|
||||
}
|
||||
|
||||
enum class sprite : std::size_t {
|
||||
VAMP,
|
||||
CONNECTING,
|
||||
OFFLINE,
|
||||
BUSY,
|
||||
READY,
|
||||
LOADING,
|
||||
WIFI,
|
||||
ERROR,
|
||||
PAGE,
|
||||
ACOUSTIC,
|
||||
ELECTRIC,
|
||||
VOCAL,
|
||||
UNAVAILABLE,
|
||||
DIGIT_0,
|
||||
DIGIT_1,
|
||||
DIGIT_2,
|
||||
DIGIT_3,
|
||||
DIGIT_4,
|
||||
DIGIT_5,
|
||||
DIGIT_6,
|
||||
DIGIT_7,
|
||||
DIGIT_8,
|
||||
DIGIT_9,
|
||||
SLASH,
|
||||
COUNT,
|
||||
};
|
||||
|
||||
struct coverage_image {
|
||||
std::string_view pixels{};
|
||||
std::size_t width{};
|
||||
std::size_t height{};
|
||||
auto at(std::size_t x, std::size_t y) const -> std::uint8_t {
|
||||
if (x >= width || y >= height || (y * width + x) / 2U >= pixels.size()) {
|
||||
return 0U;
|
||||
}
|
||||
auto index = y * width + x;
|
||||
auto packed = static_cast<std::uint8_t>(pixels[index / 2U]);
|
||||
return (index & 1U) == 0U ? packed >> 4U : packed & 15U;
|
||||
}
|
||||
};
|
||||
|
||||
inline auto static_sprite(std::string_view atlas, sprite value) -> coverage_image {
|
||||
constexpr auto count = static_cast<std::size_t>(sprite::COUNT);
|
||||
auto index = static_cast<std::size_t>(value);
|
||||
if (atlas.size() < 8U + count * 8U || atlas.substr(0U, 4U) != "VPSA" ||
|
||||
static_cast<std::uint8_t>(atlas[4]) != count || atlas[5] != 0 || atlas[6] != 0 || atlas[7] != 0 || index >= count) {
|
||||
return {};
|
||||
}
|
||||
auto word = [&](std::size_t offset) -> std::size_t {
|
||||
return static_cast<std::uint8_t>(atlas[offset]) | (static_cast<std::size_t>(static_cast<std::uint8_t>(atlas[offset + 1U])) << 8U);
|
||||
};
|
||||
auto entry = 8U + index * 8U;
|
||||
auto width = word(entry);
|
||||
auto height = word(entry + 2U);
|
||||
auto offset = word(entry + 4U) | (word(entry + 6U) << 16U);
|
||||
auto size = (width * height + 1U) / 2U;
|
||||
if (width == 0U || width > 176U || height != 28U || offset < 8U + count * 8U || offset > atlas.size() || size > atlas.size() - offset) {
|
||||
return {};
|
||||
}
|
||||
return {atlas.substr(offset, size), width, height};
|
||||
}
|
||||
|
||||
class title_cache {
|
||||
public:
|
||||
auto select(std::string_view identifier) -> std::bool_t {
|
||||
if (identifier == identifier_.data()) {
|
||||
return false;
|
||||
}
|
||||
identifier_.fill(0);
|
||||
if (vamp_protocol::valid_title_id(identifier)) {
|
||||
std::copy(identifier.begin(), identifier.end(), identifier_.begin());
|
||||
}
|
||||
ready_ = false;
|
||||
failed_ = false;
|
||||
return true;
|
||||
}
|
||||
auto accept(std::string_view identifier, std::string_view bytes) -> std::bool_t {
|
||||
if (identifier != identifier_.data() || !vamp_protocol::valid_title_id(identifier) || !vamp_protocol::valid_title_image(bytes)) {
|
||||
return false;
|
||||
}
|
||||
std::copy(bytes.begin() + vamp_protocol::title_header_size, bytes.end(), pixels_.begin());
|
||||
ready_ = true;
|
||||
failed_ = false;
|
||||
return true;
|
||||
}
|
||||
auto fail(std::string_view identifier) -> std::bool_t {
|
||||
if (identifier != identifier_.data() || ready_) {
|
||||
return false;
|
||||
}
|
||||
failed_ = true;
|
||||
return true;
|
||||
}
|
||||
auto ready() const -> std::bool_t {
|
||||
return ready_;
|
||||
}
|
||||
auto failed() const -> std::bool_t {
|
||||
return failed_;
|
||||
}
|
||||
auto image() const -> coverage_image {
|
||||
return ready_ ? coverage_image{{pixels_.data(), pixels_.size()}, vamp_protocol::title_width, vamp_protocol::title_height} : coverage_image{};
|
||||
}
|
||||
private:
|
||||
std::array<std::char_t, vamp_protocol::title_id_size + 1U> identifier_{};
|
||||
std::array<std::char_t, vamp_protocol::title_width * vamp_protocol::title_height / 2U> pixels_{};
|
||||
std::bool_t ready_{};
|
||||
std::bool_t failed_{};
|
||||
};
|
||||
|
||||
}
|
||||
@@ -3,7 +3,8 @@
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
#include <modulus/types.hpp>
|
||||
#include <vamp_pico/artwork.hpp>
|
||||
#include <vamp_pico/http_client.hpp>
|
||||
#include <vamp_style.hpp>
|
||||
|
||||
|
||||
@@ -12,27 +13,32 @@ namespace vamp_pico {
|
||||
constexpr auto display_width = std::int32_t{320};
|
||||
constexpr auto display_height = std::int32_t{240};
|
||||
|
||||
constexpr auto color_black = std::uint16_t{0x0000};
|
||||
constexpr auto color_white = std::uint16_t{0xffff};
|
||||
constexpr auto color_gray = std::uint16_t{0x8410};
|
||||
enum class link_status { CONNECTING,
|
||||
OFFLINE,
|
||||
CONNECTED,
|
||||
ERROR };
|
||||
|
||||
class display {
|
||||
public:
|
||||
auto init() -> void;
|
||||
auto fill(std::uint16_t color) -> void;
|
||||
auto fill_rect(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height, std::uint16_t color) -> void;
|
||||
auto draw_text(std::int32_t x, std::int32_t y, std::char_t const *text, std::uint16_t color, std::int32_t scale = 2) -> void;
|
||||
auto show_boot(std::char_t const *line1, std::char_t const *line2, std::uint16_t accent) -> void;
|
||||
auto show_remote(vamp_style::color status, std::char_t const *title, std::char_t const *part, std::uint32_t page, std::uint32_t pages, std::bool_t active, std::bool_t busy = false) -> void;
|
||||
auto show_remote(link_status link, remote_display_state const &state, std::bool_t busy = false) -> void;
|
||||
auto accept_title(std::string_view identifier, std::string_view image) -> std::bool_t;
|
||||
auto reject_title(std::string_view identifier) -> void;
|
||||
auto title_ready() const -> std::bool_t;
|
||||
// At most four scanlines per call, with an 8 ms minimum write interval.
|
||||
auto poll(std::uint64_t now_ms) -> void;
|
||||
private:
|
||||
std::bool_t remote_valid_{};
|
||||
vamp_style::color last_status_{};
|
||||
std::bool_t last_active_{};
|
||||
std::bool_t last_busy_{};
|
||||
std::array<std::char_t, 96> last_title_{};
|
||||
std::array<std::char_t, 48> last_part_{};
|
||||
std::uint32_t last_page_{};
|
||||
std::uint32_t last_pages_{};
|
||||
auto dirty(std::size_t begin, std::size_t end) -> void;
|
||||
title_cache title_{};
|
||||
std::array<std::bool_t, display_height> dirty_{};
|
||||
link_status link_{link_status::CONNECTING};
|
||||
std::bool_t active_{};
|
||||
std::bool_t busy_{};
|
||||
sprite part_{sprite::COUNT};
|
||||
std::uint32_t page_{};
|
||||
std::uint32_t pages_{};
|
||||
std::uint64_t last_write_ms_{};
|
||||
std::size_t next_row_{};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ struct remote_display_state {
|
||||
std::char_t title[96]{};
|
||||
std::char_t part[48]{};
|
||||
std::char_t mode[24]{};
|
||||
std::array<std::char_t, vamp_protocol::title_id_size + 1U> title_image{};
|
||||
std::uint32_t page{};
|
||||
std::uint32_t pages{};
|
||||
};
|
||||
@@ -56,6 +57,9 @@ public:
|
||||
auto take_result() -> std::optional<http_result>;
|
||||
// Discards the request/result and returns to IDLE. Call before Wi-Fi deinit.
|
||||
auto cancel() -> void;
|
||||
// View remains valid until start/cancel. Copy only after matching the selected ID.
|
||||
auto title_image() const -> std::string_view;
|
||||
auto current_kind() const -> request_kind;
|
||||
private:
|
||||
friend struct http_client_callbacks;
|
||||
tcp_pcb *pcb_{};
|
||||
@@ -66,8 +70,9 @@ private:
|
||||
std::uint64_t timeout_ms_{};
|
||||
std::array<std::char_t, 384> request_buffer_{};
|
||||
std::size_t request_size_{};
|
||||
std::array<std::char_t, 1024> response_buffer_{};
|
||||
std::array<std::char_t, vamp_protocol::title_image_size + 1024U> response_buffer_{};
|
||||
std::size_t response_size_{};
|
||||
std::size_t title_offset_{};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -14,11 +14,13 @@ enum class request_kind {
|
||||
DISPLAY,
|
||||
COMMAND,
|
||||
HEARTBEAT,
|
||||
TITLE,
|
||||
};
|
||||
|
||||
struct remote_request {
|
||||
request_kind kind{request_kind::DISPLAY};
|
||||
vamp_protocol::page_command command{vamp_protocol::page_command::NEXT};
|
||||
std::array<std::char_t, vamp_protocol::title_id_size + 1U> title_id{};
|
||||
};
|
||||
|
||||
enum class enqueue_result {
|
||||
@@ -33,6 +35,8 @@ public:
|
||||
auto next(std::uint64_t now_ms) -> std::optional<remote_request>;
|
||||
auto complete(std::uint64_t now_ms) -> void;
|
||||
auto queued() const -> std::size_t;
|
||||
auto want_title(std::string_view identifier) -> void;
|
||||
auto cancel_title() -> void;
|
||||
private:
|
||||
std::array<vamp_protocol::page_command, command_capacity> commands_{};
|
||||
std::size_t head_{};
|
||||
@@ -40,6 +44,9 @@ private:
|
||||
std::bool_t in_flight_{};
|
||||
std::bool_t completed_{};
|
||||
std::uint64_t completed_ms_{};
|
||||
std::array<std::char_t, vamp_protocol::title_id_size + 1U> title_id_{};
|
||||
std::bool_t title_pending_{};
|
||||
request_kind active_kind_{request_kind::DISPLAY};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <modulus/types.hpp>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
|
||||
namespace vamp_protocol {
|
||||
@@ -12,6 +15,33 @@ constexpr std::char_t remote_previous_path[] = "/api/remote/previous";
|
||||
constexpr std::char_t remote_select_path[] = "/api/remote/select";
|
||||
constexpr std::char_t remote_heartbeat_path[] = "/api/remote/heartbeat";
|
||||
constexpr std::char_t remote_display_path[] = "/api/remote/display";
|
||||
constexpr std::char_t remote_title_path[] = "/api/remote/title";
|
||||
constexpr auto title_width = std::size_t{296};
|
||||
constexpr auto title_height = std::size_t{64};
|
||||
constexpr auto title_header_size = std::size_t{8};
|
||||
constexpr auto title_image_size = title_header_size + title_width * title_height / 2U;
|
||||
constexpr auto title_id_size = std::size_t{64};
|
||||
|
||||
inline auto valid_title_id(std::string_view value) -> std::bool_t {
|
||||
if (value.size() != title_id_size) {
|
||||
return false;
|
||||
}
|
||||
for (auto character : value) {
|
||||
if (!((character >= '0' && character <= '9') || (character >= 'a' && character <= 'f'))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// VPTI, little-endian uint16 dimensions, then row-major coverage, high nibble first.
|
||||
|
||||
inline auto valid_title_image(std::string_view bytes) -> std::bool_t {
|
||||
return bytes.size() == title_image_size && bytes.substr(0U, 4U) == "VPTI" &&
|
||||
static_cast<std::uint8_t>(bytes[4]) == (title_width & 255U) &&
|
||||
static_cast<std::uint8_t>(bytes[5]) == (title_width >> 8U) &&
|
||||
static_cast<std::uint8_t>(bytes[6]) == title_height && bytes[7] == 0;
|
||||
}
|
||||
|
||||
enum class page_command {
|
||||
PREVIOUS,
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
#include <modulus/types.hpp>
|
||||
|
||||
|
||||
namespace vamp_style {
|
||||
|
||||
constexpr auto ui_font_path = std::string_view{"/usr/share/product-vamp/fonts/petaluma/PetalumaScript.otf"};
|
||||
|
||||
struct color {
|
||||
std::uint8_t r{};
|
||||
std::uint8_t g{};
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
#include <modulus/types.hpp>
|
||||
|
||||
|
||||
namespace vamp_style {
|
||||
|
||||
constexpr auto ui_font_path = std::string_view{"/usr/share/product-vamp/fonts/petaluma/PetalumaScript.otf"};
|
||||
constexpr auto ui_font_height = std::int32_t{36};
|
||||
constexpr auto ui_font_max_width = std::int32_t{28};
|
||||
|
||||
struct font_glyph {
|
||||
std::array<std::uint8_t, ui_font_height * ui_font_max_width> alpha{};
|
||||
std::uint8_t width{};
|
||||
std::uint8_t advance{};
|
||||
};
|
||||
|
||||
auto ui_font_glyph(std::char_t value) -> vamp_style::font_glyph;
|
||||
auto ui_font_pair_adjust(std::char_t left, std::char_t right) -> std::int32_t;
|
||||
auto ui_text_width(std::string_view text, std::int32_t scale) -> std::int32_t;
|
||||
|
||||
}
|
||||
@@ -37,5 +37,6 @@ private:
|
||||
|
||||
auto make_router(vamp_zero::control_state &state) -> ligo::service_router;
|
||||
auto portal_status_json(vamp_zero::config const &config, vamp_zero::app_state const &state) -> boost::json::value;
|
||||
auto validated_title_path(std::string const &assets_path, std::string const &identifier) -> std::expected<std::string, modulus::error>;
|
||||
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ struct library_part {
|
||||
std::string source_path{};
|
||||
std::size_t measure_count{};
|
||||
std::size_t total_pages{};
|
||||
std::string title_image{};
|
||||
};
|
||||
|
||||
struct song_group {
|
||||
|
||||
Reference in New Issue
Block a user