@@ -0,0 +1,178 @@
|
||||
#include <vamp_zero/portal.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <expected>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/asio/awaitable.hpp>
|
||||
#include <boost/json/object.hpp>
|
||||
#include <boost/json/value.hpp>
|
||||
|
||||
#include <vamp_protocol/page_control.hpp>
|
||||
#include <vamp_zero/page_control.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
auto portal_html() -> std::string {
|
||||
return R"html(<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Product Vamp</title></head><body><main><h1>Product Vamp</h1><pre id="status">Loading...</pre><p><button id="prev">Previous</button> <button id="select">Select</button> <button id="next">Next</button></p><script>
|
||||
async function refresh(){const r=await fetch('/api/status');const j=await r.json();document.getElementById('status').textContent=JSON.stringify(j.payload||j,null,2);}
|
||||
document.getElementById('prev').onclick=async()=>{await fetch('/api/remote/previous',{method:'POST'});refresh();};
|
||||
document.getElementById('select').onclick=async()=>{await fetch('/api/remote/select',{method:'POST'});refresh();};
|
||||
document.getElementById('next').onclick=async()=>{await fetch('/api/remote/next',{method:'POST'});refresh();};
|
||||
refresh();
|
||||
</script></main></body></html>)html";
|
||||
}
|
||||
|
||||
auto endpoint_port(std::string const& endpoint) -> std::string {
|
||||
auto separator = endpoint.rfind(':');
|
||||
if (separator == std::string::npos || separator + 1U >= endpoint.size()) {
|
||||
return "8080";
|
||||
}
|
||||
return endpoint.substr(separator + 1U);
|
||||
}
|
||||
|
||||
auto remote_response(vamp_zero::app_state const& state) -> ligo::service_response {
|
||||
return ligo::text_response("text/plain; charset=utf-8", vamp_zero::remote_display_text(state));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
vamp_zero::control_state::control_state(vamp_zero::config config, vamp_zero::app_state state) : config_{std::move(config)}, state_{std::move(state)} {}
|
||||
|
||||
auto vamp_zero::control_state::config() const -> vamp_zero::config {
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
return config_;
|
||||
}
|
||||
|
||||
auto vamp_zero::control_state::pico_connected_locked() const -> std::bool_t {
|
||||
return last_heartbeat_ != std::chrono::steady_clock::time_point{} && std::chrono::steady_clock::now() - last_heartbeat_ < std::chrono::seconds{3};
|
||||
}
|
||||
|
||||
auto vamp_zero::control_state::snapshot_locked() const -> vamp_zero::app_state {
|
||||
auto copy = state_;
|
||||
copy.pico_connected = pico_connected_locked();
|
||||
return copy;
|
||||
}
|
||||
|
||||
auto vamp_zero::control_state::snapshot() const -> vamp_zero::app_state {
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
return snapshot_locked();
|
||||
}
|
||||
|
||||
auto vamp_zero::control_state::apply(vamp_protocol::page_command command) -> vamp_zero::app_state {
|
||||
auto observer = vamp_zero::control_state::observer{};
|
||||
auto updated = vamp_zero::app_state{};
|
||||
{
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
state_ = vamp_zero::apply_app_command(std::move(state_), command);
|
||||
state_.pico_connected = pico_connected_locked();
|
||||
updated = state_;
|
||||
observer = observer_;
|
||||
}
|
||||
if (observer) {
|
||||
observer(updated);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
auto vamp_zero::control_state::heartbeat() -> vamp_zero::app_state {
|
||||
auto observer = vamp_zero::control_state::observer{};
|
||||
auto updated = vamp_zero::app_state{};
|
||||
{
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
last_heartbeat_ = std::chrono::steady_clock::now();
|
||||
++state_.heartbeat_count;
|
||||
auto notify = !state_.pico_connected;
|
||||
state_.pico_connected = true;
|
||||
updated = state_;
|
||||
if (notify) {
|
||||
observer = observer_;
|
||||
}
|
||||
}
|
||||
if (observer) {
|
||||
observer(updated);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
auto vamp_zero::control_state::refresh_connection() -> std::optional<vamp_zero::app_state> {
|
||||
auto observer = vamp_zero::control_state::observer{};
|
||||
auto updated = vamp_zero::app_state{};
|
||||
{
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
auto connected = pico_connected_locked();
|
||||
if (connected == state_.pico_connected) {
|
||||
return std::nullopt;
|
||||
}
|
||||
state_.pico_connected = connected;
|
||||
updated = state_;
|
||||
observer = observer_;
|
||||
}
|
||||
if (observer) {
|
||||
observer(updated);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
auto vamp_zero::control_state::set_observer(vamp_zero::control_state::observer observer) -> void {
|
||||
auto lock = std::scoped_lock{mutex_};
|
||||
observer_ = std::move(observer);
|
||||
}
|
||||
|
||||
auto vamp_zero::portal_status_json(vamp_zero::config const& config, vamp_zero::app_state const& state) -> boost::json::value {
|
||||
auto control = boost::json::object{};
|
||||
control["ap_ssid"] = config.control.ap_ssid;
|
||||
control["zero_ip"] = config.control.zero_ip;
|
||||
control["endpoint"] = "http://" + config.control.zero_ip + ":" + endpoint_port(config.control.endpoint.bind);
|
||||
|
||||
auto root = boost::json::object{};
|
||||
root["device"] = "zero";
|
||||
root["version"] = VAMP_VERSION;
|
||||
root["git_sha"] = VAMP_GIT_SHA;
|
||||
root["control"] = std::move(control);
|
||||
root["state"] = vamp_zero::app_status_json(state);
|
||||
return root;
|
||||
}
|
||||
|
||||
auto vamp_zero::make_router(vamp_zero::control_state& state) -> ligo::service_router {
|
||||
auto router = ligo::service_router{};
|
||||
router.add_response_route("GET", vamp_protocol::root_path, [](ligo::request_context, boost::json::value) -> boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>> {
|
||||
co_return ligo::text_response("text/html; charset=utf-8", portal_html());
|
||||
});
|
||||
router.add_route("GET", vamp_protocol::status_path, [&state](ligo::request_context, boost::json::value) -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>> {
|
||||
co_return vamp_zero::portal_status_json(state.config(), state.snapshot());
|
||||
});
|
||||
router.add_response_route("GET", vamp_protocol::remote_display_path, [&state](ligo::request_context, boost::json::value) -> boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>> {
|
||||
auto updated = state.heartbeat();
|
||||
co_return remote_response(updated);
|
||||
});
|
||||
router.add_response_route("POST", vamp_protocol::remote_heartbeat_path, [&state](ligo::request_context, boost::json::value) -> boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>> {
|
||||
auto updated = state.heartbeat();
|
||||
co_return remote_response(updated);
|
||||
});
|
||||
router.add_response_route("POST", vamp_protocol::remote_next_path, [&state](ligo::request_context, boost::json::value) -> boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>> {
|
||||
auto updated = state.apply(vamp_protocol::page_command::NEXT);
|
||||
co_return remote_response(updated);
|
||||
});
|
||||
router.add_response_route("POST", vamp_protocol::remote_previous_path, [&state](ligo::request_context, boost::json::value) -> boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>> {
|
||||
auto updated = state.apply(vamp_protocol::page_command::PREVIOUS);
|
||||
co_return remote_response(updated);
|
||||
});
|
||||
router.add_response_route("POST", vamp_protocol::remote_select_path, [&state](ligo::request_context, boost::json::value) -> boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>> {
|
||||
auto updated = state.apply(vamp_protocol::page_command::SELECT);
|
||||
co_return remote_response(updated);
|
||||
});
|
||||
router.add_route("POST", vamp_protocol::next_page_path, [&state](ligo::request_context, boost::json::value) -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>> {
|
||||
auto updated = state.apply(vamp_protocol::page_command::NEXT);
|
||||
co_return vamp_zero::portal_status_json(state.config(), updated);
|
||||
});
|
||||
router.add_route("POST", vamp_protocol::previous_page_path, [&state](ligo::request_context, boost::json::value) -> boost::asio::awaitable<std::expected<boost::json::value, modulus::error>> {
|
||||
auto updated = state.apply(vamp_protocol::page_command::PREVIOUS);
|
||||
co_return vamp_zero::portal_status_json(state.config(), updated);
|
||||
});
|
||||
return router;
|
||||
}
|
||||
Reference in New Issue
Block a user