初版网页
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
#include "Pixel_Frame.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
namespace renderive::web {
|
||||
namespace {
|
||||
|
||||
void write_u32_le(char* target, std::uint32_t value) {
|
||||
target[0] = static_cast<char>(value & 0xffU);
|
||||
target[1] = static_cast<char>((value >> 8U) & 0xffU);
|
||||
target[2] = static_cast<char>((value >> 16U) & 0xffU);
|
||||
target[3] = static_cast<char>((value >> 24U) & 0xffU);
|
||||
}
|
||||
|
||||
std::uint8_t flatten(std::uint8_t premultiplied, std::uint8_t alpha,
|
||||
std::uint8_t background) {
|
||||
return static_cast<std::uint8_t>(
|
||||
std::min(255U, static_cast<unsigned>(premultiplied) +
|
||||
(static_cast<unsigned>(background) * (255U - alpha) + 127U) / 255U));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string encode_pixel_frame(Image_View image, Color background) {
|
||||
if (image.empty() || image.format != Pixel_Format::Premultiplied_32 ||
|
||||
image.stride < image.width * static_cast<int>(sizeof(Pixel))) {
|
||||
return {};
|
||||
}
|
||||
const auto width = static_cast<std::size_t>(image.width);
|
||||
const auto height = static_cast<std::size_t>(image.height);
|
||||
if (width > (std::numeric_limits<std::size_t>::max() - pixel_frame_header_size) /
|
||||
(height * sizeof(Pixel))) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::size_t pixel_bytes = width * height * sizeof(Pixel);
|
||||
std::string frame(pixel_frame_header_size + pixel_bytes, '\0');
|
||||
frame[0] = 'R';
|
||||
frame[1] = 'V';
|
||||
frame[2] = 'P';
|
||||
frame[3] = '1';
|
||||
write_u32_le(frame.data() + 4, static_cast<std::uint32_t>(image.width));
|
||||
write_u32_le(frame.data() + 8, static_cast<std::uint32_t>(image.height));
|
||||
write_u32_le(frame.data() + 12,
|
||||
static_cast<std::uint32_t>(image.width * static_cast<int>(sizeof(Pixel))));
|
||||
|
||||
auto* output = reinterpret_cast<std::uint8_t*>(frame.data() + pixel_frame_header_size);
|
||||
for (int y = 0; y < image.height; ++y) {
|
||||
const auto* row = image.data + static_cast<std::ptrdiff_t>(y) * image.stride;
|
||||
for (int x = 0; x < image.width; ++x) {
|
||||
Pixel pixel{};
|
||||
std::memcpy(&pixel, row + static_cast<std::ptrdiff_t>(x) * sizeof(Pixel), sizeof(pixel));
|
||||
const auto alpha = static_cast<std::uint8_t>((pixel >> 24U) & 0xffU);
|
||||
const auto red = static_cast<std::uint8_t>((pixel >> 16U) & 0xffU);
|
||||
const auto green = static_cast<std::uint8_t>((pixel >> 8U) & 0xffU);
|
||||
const auto blue = static_cast<std::uint8_t>(pixel & 0xffU);
|
||||
*output++ = flatten(red, alpha, background.r);
|
||||
*output++ = flatten(green, alpha, background.g);
|
||||
*output++ = flatten(blue, alpha, background.b);
|
||||
*output++ = 255;
|
||||
}
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
} // namespace renderive::web
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "Core2/base/Types.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
namespace renderive::web {
|
||||
|
||||
inline constexpr std::size_t pixel_frame_header_size = 16;
|
||||
|
||||
[[nodiscard]] std::string encode_pixel_frame(Image_View image,
|
||||
Color background = Color::black());
|
||||
|
||||
} // namespace renderive::web
|
||||
@@ -0,0 +1,70 @@
|
||||
#include "Renderive_WebSocket_Controller.h"
|
||||
|
||||
#include "Web_Event_Adapter.h"
|
||||
#include "Web_Plot_Session.h"
|
||||
|
||||
#include <trantor/utils/Logger.h>
|
||||
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
|
||||
namespace renderive::web {
|
||||
|
||||
void Renderive_WebSocket_Controller::handleNewConnection(
|
||||
const drogon::HttpRequestPtr&,
|
||||
const drogon::WebSocketConnectionPtr& connection) {
|
||||
connection->setContext(std::make_shared<Web_Plot_Session>());
|
||||
connection->setPingMessage("renderive", std::chrono::seconds(20));
|
||||
LOG_INFO << "Renderive WebSocket connected: " << connection->peerAddr().toIpPort();
|
||||
}
|
||||
|
||||
void Renderive_WebSocket_Controller::handleNewMessage(
|
||||
const drogon::WebSocketConnectionPtr& connection,
|
||||
std::string&& message,
|
||||
const drogon::WebSocketMessageType& type) {
|
||||
if (type == drogon::WebSocketMessageType::Ping ||
|
||||
type == drogon::WebSocketMessageType::Pong ||
|
||||
type == drogon::WebSocketMessageType::Close) {
|
||||
return;
|
||||
}
|
||||
if (type != drogon::WebSocketMessageType::Text) {
|
||||
connection->shutdown(drogon::CloseCode::kInvalidMessage,
|
||||
"Renderive accepts text events only");
|
||||
return;
|
||||
}
|
||||
if (message.size() > 16 * 1024) {
|
||||
connection->shutdown(drogon::CloseCode::kMessageTooBig,
|
||||
"Renderive event is too large");
|
||||
return;
|
||||
}
|
||||
const auto event = Web_Event_Adapter::decode(message);
|
||||
if (!event) {
|
||||
connection->shutdown(drogon::CloseCode::kWrongMessageContent,
|
||||
"Invalid Renderive event");
|
||||
return;
|
||||
}
|
||||
const auto session = connection->getContext<Web_Plot_Session>();
|
||||
if (!session) {
|
||||
connection->shutdown(drogon::CloseCode::kUnexpectedCondition,
|
||||
"Renderive session is unavailable");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (auto pixels = session->handle(*event)) {
|
||||
connection->send(pixels->data(), pixels->size(),
|
||||
drogon::WebSocketMessageType::Binary);
|
||||
}
|
||||
} catch (const std::exception& error) {
|
||||
LOG_ERROR << "Renderive WebSocket session failed: " << error.what();
|
||||
connection->shutdown(drogon::CloseCode::kUnexpectedCondition,
|
||||
"Renderive rendering failed");
|
||||
}
|
||||
}
|
||||
|
||||
void Renderive_WebSocket_Controller::handleConnectionClosed(
|
||||
const drogon::WebSocketConnectionPtr& connection) {
|
||||
LOG_INFO << "Renderive WebSocket closed: " << connection->peerAddr().toIpPort();
|
||||
connection->clearContext();
|
||||
}
|
||||
|
||||
} // namespace renderive::web
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <drogon/WebSocketController.h>
|
||||
|
||||
namespace renderive::web {
|
||||
|
||||
class Renderive_WebSocket_Controller final
|
||||
: public drogon::WebSocketController<Renderive_WebSocket_Controller, false> {
|
||||
public:
|
||||
void handleNewMessage(const drogon::WebSocketConnectionPtr& connection,
|
||||
std::string&& message,
|
||||
const drogon::WebSocketMessageType& type) override;
|
||||
void handleNewConnection(const drogon::HttpRequestPtr& request,
|
||||
const drogon::WebSocketConnectionPtr& connection) override;
|
||||
void handleConnectionClosed(const drogon::WebSocketConnectionPtr& connection) override;
|
||||
|
||||
WS_PATH_LIST_BEGIN
|
||||
WS_PATH_ADD("/renderive");
|
||||
WS_PATH_LIST_END
|
||||
};
|
||||
|
||||
} // namespace renderive::web
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include "Core2/event/Event.h"
|
||||
|
||||
#include <variant>
|
||||
|
||||
namespace renderive::web {
|
||||
|
||||
struct Frame_Request {};
|
||||
|
||||
struct Viewport_Resize {
|
||||
Size size;
|
||||
};
|
||||
|
||||
enum class Demo_Mode : std::uint8_t { Am, Fm, Usb, Lsb };
|
||||
|
||||
struct Set_Demo_Mode {
|
||||
Demo_Mode mode = Demo_Mode::Fm;
|
||||
};
|
||||
|
||||
struct Set_Center_Frequency {
|
||||
double megahertz = 102.5;
|
||||
};
|
||||
|
||||
struct Set_Bandwidth {
|
||||
double kilohertz = 1200.0;
|
||||
};
|
||||
|
||||
struct Set_Gain {
|
||||
double decibels{};
|
||||
};
|
||||
|
||||
struct Set_Max_Hold {
|
||||
bool enabled{};
|
||||
};
|
||||
|
||||
struct Set_Smoothing {
|
||||
bool enabled{};
|
||||
};
|
||||
|
||||
struct Clear_Selection {};
|
||||
|
||||
using Web_Event = std::variant<Frame_Request,
|
||||
Viewport_Resize,
|
||||
Event,
|
||||
Pointer_Event,
|
||||
Wheel_Event,
|
||||
Key_Event,
|
||||
Set_Demo_Mode,
|
||||
Set_Center_Frequency,
|
||||
Set_Bandwidth,
|
||||
Set_Gain,
|
||||
Set_Max_Hold,
|
||||
Set_Smoothing,
|
||||
Clear_Selection>;
|
||||
|
||||
} // namespace renderive::web
|
||||
@@ -0,0 +1,213 @@
|
||||
#include "Web_Event_Adapter.h"
|
||||
|
||||
#include <json/json.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace renderive::web {
|
||||
namespace {
|
||||
|
||||
std::optional<Json::Value> parse_json(std::string_view message) {
|
||||
Json::CharReaderBuilder builder;
|
||||
builder["collectComments"] = false;
|
||||
std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
|
||||
Json::Value root;
|
||||
std::string errors;
|
||||
if (!reader->parse(message.data(), message.data() + message.size(), &root, &errors) ||
|
||||
!root.isObject()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
std::optional<double> finite_number(const Json::Value& root, const char* name) {
|
||||
const auto& value = root[name];
|
||||
if (!value.isNumeric())
|
||||
return std::nullopt;
|
||||
const double result = value.asDouble();
|
||||
return std::isfinite(result) ? std::optional<double>(result) : std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<bool> boolean(const Json::Value& root, const char* name) {
|
||||
const auto& value = root[name];
|
||||
return value.isBool() ? std::optional<bool>(value.asBool()) : std::nullopt;
|
||||
}
|
||||
|
||||
Keyboard_Modifier modifiers(const Json::Value& root) {
|
||||
const int value = root["modifiers"].isInt() ? root["modifiers"].asInt() : 0;
|
||||
return static_cast<Keyboard_Modifier>(std::clamp(value, 0, 15));
|
||||
}
|
||||
|
||||
Mouse_Button mouse_button(const Json::Value& root) {
|
||||
const std::string value = root["button"].isString() ? root["button"].asString() : "none";
|
||||
if (value == "left")
|
||||
return Mouse_Button::Left;
|
||||
if (value == "right")
|
||||
return Mouse_Button::Right;
|
||||
if (value == "middle")
|
||||
return Mouse_Button::Middle;
|
||||
return Mouse_Button::None;
|
||||
}
|
||||
|
||||
Key key(const Json::Value& root) {
|
||||
const std::string value = root["key"].isString() ? root["key"].asString() : "";
|
||||
if (value == "Escape")
|
||||
return Key::Escape;
|
||||
if (value == "Enter")
|
||||
return Key::Enter;
|
||||
if (value == "Space")
|
||||
return Key::Space;
|
||||
if (value == "Delete")
|
||||
return Key::Delete;
|
||||
if (value == "Backspace")
|
||||
return Key::Backspace;
|
||||
if (value == "ArrowLeft")
|
||||
return Key::Left;
|
||||
if (value == "ArrowRight")
|
||||
return Key::Right;
|
||||
if (value == "ArrowUp")
|
||||
return Key::Up;
|
||||
if (value == "ArrowDown")
|
||||
return Key::Down;
|
||||
return Key::Unknown;
|
||||
}
|
||||
|
||||
std::optional<Web_Event> pointer_event(const Json::Value& root, Event_Type type) {
|
||||
const auto x = finite_number(root, "x");
|
||||
const auto y = finite_number(root, "y");
|
||||
if (!x || !y)
|
||||
return std::nullopt;
|
||||
Pointer_Event event(type);
|
||||
event.position = {*x, *y};
|
||||
event.global_position = event.position;
|
||||
event.button = mouse_button(root);
|
||||
const int buttons = root["buttons"].isInt() ? root["buttons"].asInt() : 0;
|
||||
event.buttons = static_cast<Mouse_Button_Mask>(std::clamp(buttons, 0, 7));
|
||||
event.modifiers = modifiers(root);
|
||||
return event;
|
||||
}
|
||||
|
||||
std::optional<Web_Event> wheel_event(const Json::Value& root) {
|
||||
const auto x = finite_number(root, "x");
|
||||
const auto y = finite_number(root, "y");
|
||||
const auto pixel_x = finite_number(root, "pixelDeltaX");
|
||||
const auto pixel_y = finite_number(root, "pixelDeltaY");
|
||||
const auto angle_x = finite_number(root, "angleDeltaX");
|
||||
const auto angle_y = finite_number(root, "angleDeltaY");
|
||||
if (!x || !y || !pixel_x || !pixel_y || !angle_x || !angle_y)
|
||||
return std::nullopt;
|
||||
Wheel_Event event;
|
||||
event.position = {*x, *y};
|
||||
event.global_position = event.position;
|
||||
event.pixel_delta_x = *pixel_x;
|
||||
event.pixel_delta_y = *pixel_y;
|
||||
event.angle_delta_x = *angle_x;
|
||||
event.angle_delta_y = *angle_y;
|
||||
event.modifiers = modifiers(root);
|
||||
const int buttons = root["buttons"].isInt() ? root["buttons"].asInt() : 0;
|
||||
event.buttons = static_cast<Mouse_Button_Mask>(std::clamp(buttons, 0, 7));
|
||||
return event;
|
||||
}
|
||||
|
||||
std::optional<Web_Event> key_event(const Json::Value& root, Event_Type type) {
|
||||
Key_Event event(type);
|
||||
event.key = key(root);
|
||||
event.native_key = root["nativeKey"].isUInt() ? root["nativeKey"].asUInt() : 0;
|
||||
event.modifiers = modifiers(root);
|
||||
event.auto_repeat = root["repeat"].isBool() && root["repeat"].asBool();
|
||||
return event;
|
||||
}
|
||||
|
||||
std::optional<Web_Event> control_event(const Json::Value& root) {
|
||||
if (!root["control"].isString())
|
||||
return std::nullopt;
|
||||
const std::string control = root["control"].asString();
|
||||
if (control == "mode" && root["value"].isString()) {
|
||||
const std::string value = root["value"].asString();
|
||||
if (value == "AM")
|
||||
return Set_Demo_Mode{Demo_Mode::Am};
|
||||
if (value == "FM")
|
||||
return Set_Demo_Mode{Demo_Mode::Fm};
|
||||
if (value == "USB")
|
||||
return Set_Demo_Mode{Demo_Mode::Usb};
|
||||
if (value == "LSB")
|
||||
return Set_Demo_Mode{Demo_Mode::Lsb};
|
||||
return std::nullopt;
|
||||
}
|
||||
if (control == "center_frequency_mhz") {
|
||||
const auto value = finite_number(root, "value");
|
||||
return value && *value >= 1.0 && *value <= 6000.0
|
||||
? std::optional<Web_Event>(Set_Center_Frequency{*value})
|
||||
: std::nullopt;
|
||||
}
|
||||
if (control == "bandwidth_khz") {
|
||||
const auto value = finite_number(root, "value");
|
||||
return value && *value >= 10.0 && *value <= 50000.0
|
||||
? std::optional<Web_Event>(Set_Bandwidth{*value})
|
||||
: std::nullopt;
|
||||
}
|
||||
if (control == "gain_db") {
|
||||
const auto value = finite_number(root, "value");
|
||||
return value && *value >= -30.0 && *value <= 80.0
|
||||
? std::optional<Web_Event>(Set_Gain{*value})
|
||||
: std::nullopt;
|
||||
}
|
||||
if (control == "max_hold") {
|
||||
const auto value = boolean(root, "value");
|
||||
return value ? std::optional<Web_Event>(Set_Max_Hold{*value}) : std::nullopt;
|
||||
}
|
||||
if (control == "smoothing") {
|
||||
const auto value = boolean(root, "value");
|
||||
return value ? std::optional<Web_Event>(Set_Smoothing{*value}) : std::nullopt;
|
||||
}
|
||||
if (control == "clear_selection")
|
||||
return Clear_Selection{};
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<Web_Event> Web_Event_Adapter::decode(std::string_view message) {
|
||||
const auto parsed = parse_json(message);
|
||||
if (!parsed || (*parsed)["category"].asString() != "event" || !(*parsed)["type"].isString())
|
||||
return std::nullopt;
|
||||
|
||||
const Json::Value& root = *parsed;
|
||||
const std::string type = root["type"].asString();
|
||||
if (type == "frame")
|
||||
return Frame_Request{};
|
||||
if (type == "resize") {
|
||||
const auto width = finite_number(root, "width");
|
||||
const auto height = finite_number(root, "height");
|
||||
if (!width || !height)
|
||||
return std::nullopt;
|
||||
return Viewport_Resize{{std::clamp(static_cast<int>(*width), 240, 1920),
|
||||
std::clamp(static_cast<int>(*height), 180, 1200)}};
|
||||
}
|
||||
if (type == "show")
|
||||
return Event(Event_Type::Show);
|
||||
if (type == "hide")
|
||||
return Event(Event_Type::Hide);
|
||||
if (type == "leave")
|
||||
return Event(Event_Type::Leave);
|
||||
if (type == "pointer_move")
|
||||
return pointer_event(root, Event_Type::Pointer_Move);
|
||||
if (type == "pointer_press")
|
||||
return pointer_event(root, Event_Type::Pointer_Press);
|
||||
if (type == "pointer_release")
|
||||
return pointer_event(root, Event_Type::Pointer_Release);
|
||||
if (type == "wheel")
|
||||
return wheel_event(root);
|
||||
if (type == "key_press")
|
||||
return key_event(root, Event_Type::Key_Press);
|
||||
if (type == "key_release")
|
||||
return key_event(root, Event_Type::Key_Release);
|
||||
if (type == "control")
|
||||
return control_event(root);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace renderive::web
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "Web_Event.h"
|
||||
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
namespace renderive::web {
|
||||
|
||||
class Web_Event_Adapter final {
|
||||
public:
|
||||
[[nodiscard]] static std::optional<Web_Event> decode(std::string_view message);
|
||||
};
|
||||
|
||||
} // namespace renderive::web
|
||||
@@ -0,0 +1,328 @@
|
||||
#include "Web_Plot_Session.h"
|
||||
|
||||
#include "Pixel_Frame.h"
|
||||
#include "Core2/export.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace renderive::web {
|
||||
namespace {
|
||||
|
||||
Color blend(Color first, Color second, double amount) {
|
||||
const auto channel = [amount](std::uint8_t a, std::uint8_t b) {
|
||||
return static_cast<std::uint8_t>(std::clamp(
|
||||
std::lround(a + (b - a) * amount), 0L, 255L));
|
||||
};
|
||||
return {channel(first.r, second.r), channel(first.g, second.g),
|
||||
channel(first.b, second.b), channel(first.a, second.a)};
|
||||
}
|
||||
|
||||
Color_Map radio_color_map() {
|
||||
constexpr Color stops[] = {
|
||||
{3, 7, 18, 255},
|
||||
{16, 52, 105, 255},
|
||||
{23, 163, 184, 255},
|
||||
{238, 210, 91, 255},
|
||||
{239, 68, 68, 255}
|
||||
};
|
||||
std::vector<Pixel> colors;
|
||||
colors.reserve(256);
|
||||
for (int index = 0; index < 256; ++index) {
|
||||
const double position = index / 255.0 * (std::size(stops) - 1);
|
||||
const auto stop = static_cast<std::size_t>(std::floor(position));
|
||||
const auto next = std::min(stop + 1, std::size(stops) - 1);
|
||||
colors.push_back(premultiply(blend(stops[stop], stops[next], position - stop)));
|
||||
}
|
||||
return Color_Map(std::move(colors));
|
||||
}
|
||||
|
||||
Time_Of_Day current_time_of_day() {
|
||||
constexpr std::int64_t day_ms = 24LL * 60LL * 60LL * 1000LL;
|
||||
const auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
return {now % day_ms};
|
||||
}
|
||||
|
||||
double gaussian(double x, double center, double width) {
|
||||
const double normalized = (x - center) / width;
|
||||
return std::exp(-0.5 * normalized * normalized);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct Web_Plot_Session::Impl {
|
||||
Plot_Core plot;
|
||||
std::shared_ptr<Frequency_Axis> spectrum_frequency_axis;
|
||||
std::shared_ptr<Axis> spectrum_power_axis;
|
||||
std::shared_ptr<Frequency_Axis> waterfall_frequency_axis;
|
||||
std::shared_ptr<Time_Axis> waterfall_time_axis;
|
||||
std::shared_ptr<Spectrum> spectrum;
|
||||
std::shared_ptr<Waterfall> waterfall;
|
||||
std::shared_ptr<Selection_Rectangle_Overlay> selection;
|
||||
Demo_Mode mode = Demo_Mode::Fm;
|
||||
double gain_db = 8.0;
|
||||
std::uint64_t frame_index{};
|
||||
std::mutex mutex;
|
||||
|
||||
Impl() {
|
||||
plot.init();
|
||||
plot.set_background_color({3, 7, 18, 255});
|
||||
plot.set_max_render_fps(30.0);
|
||||
plot.set_viewport_size({960, 600});
|
||||
|
||||
const auto root = plot.root_renderable();
|
||||
const auto data = plot.create_renderable_node(root, "Web_Data");
|
||||
const auto axes = plot.create_renderable_node(root, "Web_Axes");
|
||||
const auto overlay = plot.create_renderable_node(root, "Web_Overlay");
|
||||
axes->set_cache_mode(Renderable_Cache_Mode::Local_Pixel);
|
||||
|
||||
constexpr Color axis_color{93, 116, 151, 255};
|
||||
const Range initial_frequency{101'300'000.0, 103'700'000.0};
|
||||
spectrum_frequency_axis =
|
||||
Frequency_Axis::Builder(axes, Orientation::Horizontal)
|
||||
.set_coord_range(initial_frequency)
|
||||
.set_label_precision(2)
|
||||
.set_tick_length(8)
|
||||
.set_sub_tick_length(4)
|
||||
.set_color(axis_color)
|
||||
.set_use_wheel(true)
|
||||
.set_use_drag(true)
|
||||
.build();
|
||||
spectrum_power_axis =
|
||||
Axis::Builder(axes, Orientation::Vertical)
|
||||
.set_coord_range({-20.0, -120.0})
|
||||
.set_label_precision(0)
|
||||
.set_tick_length(-8)
|
||||
.set_sub_tick_length(-4)
|
||||
.set_color(axis_color)
|
||||
.set_unit_text("dBm")
|
||||
.build();
|
||||
waterfall_frequency_axis =
|
||||
Frequency_Axis::Builder(axes, Orientation::Horizontal)
|
||||
.set_coord_range(initial_frequency)
|
||||
.set_label_precision(2)
|
||||
.set_tick_length(8)
|
||||
.set_sub_tick_length(4)
|
||||
.set_color(axis_color)
|
||||
.set_use_wheel(true)
|
||||
.set_use_drag(true)
|
||||
.build();
|
||||
waterfall_time_axis =
|
||||
Time_Axis::Builder(axes, Orientation::Vertical)
|
||||
.set_visible_time_point_count(72)
|
||||
.set_tick_label_spacing_px(34)
|
||||
.set_time_format("mm:ss")
|
||||
.set_tick_length(-8)
|
||||
.set_sub_tick_length(-4)
|
||||
.set_color(axis_color)
|
||||
.build();
|
||||
|
||||
spectrum = Spectrum::Builder(data, spectrum_frequency_axis, spectrum_power_axis)
|
||||
.set_frequency_range(initial_frequency)
|
||||
.set_frequency_point_size(768)
|
||||
.set_center_frequency(102'500'000.0)
|
||||
.set_sweep_frequency_range({102'200'000.0, 102'800'000.0})
|
||||
.set_max_marker_visible(true)
|
||||
.set_sweep_region_visible(true)
|
||||
.set_interpolation_mode(Line_Interpolation_Mode::Cubic_Value)
|
||||
.build();
|
||||
spectrum->set_current_pen({Color{57, 224, 177, 255}, 2.0, Line_Style::Solid,
|
||||
Line_Cap::Round, Line_Join::Round});
|
||||
spectrum->set_current_brush({Color{31, 174, 145, 32}, Brush_Style::Solid});
|
||||
spectrum->set_max_pen({Color{250, 204, 21, 210}, 1.0});
|
||||
spectrum->set_middle_frequency_pen({Color{56, 189, 248, 220}, 1.0,
|
||||
Line_Style::Dash});
|
||||
|
||||
waterfall = Waterfall::Builder(data, waterfall_frequency_axis, waterfall_time_axis)
|
||||
.set_frequency_range(initial_frequency)
|
||||
.set_power_range({-120.0, -20.0})
|
||||
.set_frequency_bin_count(768)
|
||||
.set_interpolation_mode(Image_Interpolation_Mode::Bilinear)
|
||||
.set_color_map(radio_color_map())
|
||||
.build();
|
||||
selection = Selection_Rectangle_Overlay::Builder(
|
||||
overlay, spectrum_frequency_axis, spectrum_power_axis)
|
||||
.set_rect_brush({Color{56, 189, 248, 36}, Brush_Style::Solid})
|
||||
.set_border_pen({Color{125, 211, 252, 230}, 1.0, Line_Style::Dash})
|
||||
.build();
|
||||
|
||||
apply_layout(plot.viewport_size());
|
||||
plot.activate_view();
|
||||
update_model();
|
||||
(void)plot.render_frame(true);
|
||||
}
|
||||
|
||||
~Impl() { plot.deactivate_view(); }
|
||||
|
||||
void apply_layout(Size viewport) {
|
||||
const int left = viewport.width < 620 ? 54 : 72;
|
||||
const int right = viewport.width < 620 ? 44 : 70;
|
||||
const int top = 16;
|
||||
const int bottom = viewport.height < 480 ? 24 : 32;
|
||||
const int gap = viewport.height < 480 ? 36 : 48;
|
||||
const int content_width = std::max(1, viewport.width - left - right);
|
||||
const int available_height = std::max(2, viewport.height - top - bottom - gap);
|
||||
const int spectrum_height = std::max(1, available_height * 43 / 100);
|
||||
const int waterfall_y = top + spectrum_height + gap;
|
||||
const int waterfall_height = std::max(1, viewport.height - waterfall_y - bottom);
|
||||
|
||||
spectrum_frequency_axis->set_x(left);
|
||||
spectrum_frequency_axis->set_y(top + spectrum_height);
|
||||
spectrum_frequency_axis->set_pixel_length(static_cast<std::size_t>(content_width));
|
||||
spectrum_power_axis->set_x(left);
|
||||
spectrum_power_axis->set_y(top);
|
||||
spectrum_power_axis->set_pixel_length(static_cast<std::size_t>(spectrum_height));
|
||||
|
||||
waterfall_frequency_axis->set_x(left);
|
||||
waterfall_frequency_axis->set_y(waterfall_y + waterfall_height);
|
||||
waterfall_frequency_axis->set_pixel_length(static_cast<std::size_t>(content_width));
|
||||
waterfall_time_axis->set_x(left);
|
||||
waterfall_time_axis->set_y(waterfall_y);
|
||||
waterfall_time_axis->set_pixel_length(static_cast<std::size_t>(waterfall_height));
|
||||
}
|
||||
|
||||
void update_model() {
|
||||
constexpr int sample_count = 768;
|
||||
const double time = static_cast<double>(frame_index) * 0.075;
|
||||
std::vector<double> samples(sample_count);
|
||||
for (int index = 0; index < sample_count; ++index) {
|
||||
const double x = static_cast<double>(index) / (sample_count - 1);
|
||||
const double noise = std::sin(index * 12.9898 + frame_index * 0.371) *
|
||||
std::sin(index * 0.137 + frame_index * 0.071);
|
||||
double signal{};
|
||||
switch (mode) {
|
||||
case Demo_Mode::Am:
|
||||
signal = 66.0 * gaussian(x, 0.5, 0.008) +
|
||||
39.0 * gaussian(x, 0.42, 0.018) +
|
||||
39.0 * gaussian(x, 0.58, 0.018);
|
||||
break;
|
||||
case Demo_Mode::Fm: {
|
||||
const double moving = 0.5 + std::sin(time) * 0.035;
|
||||
signal = 58.0 * gaussian(x, moving, 0.055) +
|
||||
25.0 * gaussian(x, moving - 0.11, 0.023) +
|
||||
25.0 * gaussian(x, moving + 0.11, 0.023);
|
||||
break;
|
||||
}
|
||||
case Demo_Mode::Usb:
|
||||
signal = 61.0 * gaussian(x, 0.57, 0.045) +
|
||||
28.0 * gaussian(x, 0.68, 0.025);
|
||||
break;
|
||||
case Demo_Mode::Lsb:
|
||||
signal = 61.0 * gaussian(x, 0.43, 0.045) +
|
||||
28.0 * gaussian(x, 0.32, 0.025);
|
||||
break;
|
||||
}
|
||||
samples[index] = std::clamp(-110.0 + noise * 4.5 + signal + gain_db, -120.0, -20.0);
|
||||
}
|
||||
spectrum->update_samples(samples);
|
||||
if ((frame_index & 1U) == 0U)
|
||||
waterfall->append_row(current_time_of_day(), samples);
|
||||
++frame_index;
|
||||
}
|
||||
|
||||
void set_center_frequency(double megahertz) {
|
||||
const double center = megahertz * 1'000'000.0;
|
||||
const double bandwidth = spectrum_frequency_axis->coord_range().size();
|
||||
const Range range{center - bandwidth * 0.5, center + bandwidth * 0.5};
|
||||
spectrum_frequency_axis->set_coord_range(range);
|
||||
waterfall_frequency_axis->set_coord_range(range);
|
||||
spectrum->set_frequency_range(range);
|
||||
spectrum->set_center_frequency(center);
|
||||
spectrum->set_sweep_frequency_range({center - bandwidth * 0.125,
|
||||
center + bandwidth * 0.125});
|
||||
waterfall->set_frequency_range(range);
|
||||
}
|
||||
|
||||
void set_bandwidth(double kilohertz) {
|
||||
const double center = spectrum->center_frequency();
|
||||
const double bandwidth = kilohertz * 1000.0;
|
||||
const Range range{center - bandwidth * 0.5, center + bandwidth * 0.5};
|
||||
spectrum_frequency_axis->set_coord_range(range);
|
||||
waterfall_frequency_axis->set_coord_range(range);
|
||||
spectrum->set_frequency_range(range);
|
||||
spectrum->set_sweep_frequency_range({center - bandwidth * 0.125,
|
||||
center + bandwidth * 0.125});
|
||||
waterfall->set_frequency_range(range);
|
||||
}
|
||||
|
||||
std::optional<std::string> render_pixels() {
|
||||
if (!plot.view_active())
|
||||
return std::nullopt;
|
||||
update_model();
|
||||
if (!plot.render_frame(true))
|
||||
return std::nullopt;
|
||||
std::string pixels;
|
||||
const Color background = plot.background_color();
|
||||
plot.with_frame([&pixels, background](Image_View image) {
|
||||
pixels = encode_pixel_frame(image, background);
|
||||
});
|
||||
return pixels.empty() ? std::nullopt : std::optional<std::string>(std::move(pixels));
|
||||
}
|
||||
|
||||
std::optional<std::string> handle(const Web_Event& event) {
|
||||
std::lock_guard lock(mutex);
|
||||
return std::visit(
|
||||
[this](const auto& value) -> std::optional<std::string> {
|
||||
using T = std::decay_t<decltype(value)>;
|
||||
if constexpr (std::is_same_v<T, Frame_Request>) {
|
||||
return render_pixels();
|
||||
} else if constexpr (std::is_same_v<T, Viewport_Resize>) {
|
||||
const Size previous = plot.viewport_size();
|
||||
plot.set_viewport_size(value.size);
|
||||
apply_layout(value.size);
|
||||
Resize_Event resized;
|
||||
resized.old_size = previous;
|
||||
resized.new_size = value.size;
|
||||
plot.dispatch_event(resized);
|
||||
} else if constexpr (std::is_same_v<T, Event>) {
|
||||
if (value.type == Event_Type::Show)
|
||||
plot.activate_view();
|
||||
else if (value.type == Event_Type::Hide)
|
||||
plot.deactivate_view();
|
||||
plot.dispatch_event(value);
|
||||
} else if constexpr (std::is_base_of_v<Event, T>) {
|
||||
plot.dispatch_event(value);
|
||||
} else if constexpr (std::is_same_v<T, Set_Demo_Mode>) {
|
||||
mode = value.mode;
|
||||
plot.notify_model_dirty();
|
||||
} else if constexpr (std::is_same_v<T, Set_Center_Frequency>) {
|
||||
set_center_frequency(value.megahertz);
|
||||
} else if constexpr (std::is_same_v<T, Set_Bandwidth>) {
|
||||
set_bandwidth(value.kilohertz);
|
||||
} else if constexpr (std::is_same_v<T, Set_Gain>) {
|
||||
gain_db = value.decibels;
|
||||
plot.notify_model_dirty();
|
||||
} else if constexpr (std::is_same_v<T, Set_Max_Hold>) {
|
||||
spectrum->set_max_hold_visible(value.enabled);
|
||||
} else if constexpr (std::is_same_v<T, Set_Smoothing>) {
|
||||
spectrum->set_interpolation_mode(
|
||||
value.enabled ? Line_Interpolation_Mode::Cubic_Value
|
||||
: Line_Interpolation_Mode::Nearest_Sample);
|
||||
waterfall->set_interpolation_mode(
|
||||
value.enabled ? Image_Interpolation_Mode::Bilinear
|
||||
: Image_Interpolation_Mode::Nearest);
|
||||
} else if constexpr (std::is_same_v<T, Clear_Selection>) {
|
||||
selection->clear_selected_regions();
|
||||
}
|
||||
return std::nullopt;
|
||||
},
|
||||
event);
|
||||
}
|
||||
};
|
||||
|
||||
Web_Plot_Session::Web_Plot_Session() : impl_(std::make_unique<Impl>()) {}
|
||||
Web_Plot_Session::~Web_Plot_Session() = default;
|
||||
|
||||
std::optional<std::string> Web_Plot_Session::handle(const Web_Event& event) {
|
||||
return impl_->handle(event);
|
||||
}
|
||||
|
||||
} // namespace renderive::web
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "Web_Event.h"
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace renderive::web {
|
||||
|
||||
class Web_Plot_Session final {
|
||||
public:
|
||||
Web_Plot_Session();
|
||||
~Web_Plot_Session();
|
||||
Web_Plot_Session(const Web_Plot_Session&) = delete;
|
||||
Web_Plot_Session& operator=(const Web_Plot_Session&) = delete;
|
||||
|
||||
[[nodiscard]] std::optional<std::string> handle(const Web_Event& event);
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace renderive::web
|
||||
@@ -0,0 +1,29 @@
|
||||
#include "Web_Server.h"
|
||||
|
||||
#include "Renderive_WebSocket_Controller.h"
|
||||
|
||||
#include <drogon/drogon.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
|
||||
namespace renderive::web {
|
||||
|
||||
int run_web_server(std::uint16_t port) {
|
||||
const auto controller = std::make_shared<Renderive_WebSocket_Controller>();
|
||||
const auto hardware_threads = std::max(2U, std::thread::hardware_concurrency());
|
||||
std::cout << "Renderive WebSocket backend: ws://127.0.0.1:" << port
|
||||
<< "/renderive\n"
|
||||
<< "Open webapp/index.html directly; no HTTP application endpoint is used.\n";
|
||||
drogon::app()
|
||||
.registerController(controller)
|
||||
.addListener("127.0.0.1", port)
|
||||
.setThreadNum(std::min(8U, hardware_threads))
|
||||
.setIdleConnectionTimeout(90)
|
||||
.run();
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace renderive::web
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace renderive::web {
|
||||
|
||||
int run_web_server(std::uint16_t port);
|
||||
|
||||
} // namespace renderive::web
|
||||
@@ -0,0 +1,29 @@
|
||||
#include "Web_Server.h"
|
||||
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <string_view>
|
||||
|
||||
namespace {
|
||||
|
||||
std::uint16_t parse_port(int argc, char** argv) {
|
||||
constexpr std::uint16_t default_port = 8848;
|
||||
if (argc != 3 || std::string_view(argv[1]) != "--port")
|
||||
return default_port;
|
||||
unsigned value{};
|
||||
const std::string_view text(argv[2]);
|
||||
const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), value);
|
||||
if (error != std::errc{} || end != text.data() + text.size() || value == 0 || value > 65535) {
|
||||
std::cerr << "Invalid port: " << text << '\n';
|
||||
return 0;
|
||||
}
|
||||
return static_cast<std::uint16_t>(value);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const std::uint16_t port = parse_port(argc, argv);
|
||||
return port == 0 ? 2 : renderive::web::run_web_server(port);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
#include "web_server/Pixel_Frame.h"
|
||||
#include "web_server/Web_Event_Adapter.h"
|
||||
#include "web_server/Web_Plot_Session.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <variant>
|
||||
|
||||
namespace renderive::web {
|
||||
namespace {
|
||||
|
||||
std::uint32_t read_u32_le(const std::string& value, std::size_t offset) {
|
||||
return static_cast<std::uint8_t>(value[offset]) |
|
||||
(static_cast<std::uint32_t>(static_cast<std::uint8_t>(value[offset + 1])) << 8U) |
|
||||
(static_cast<std::uint32_t>(static_cast<std::uint8_t>(value[offset + 2])) << 16U) |
|
||||
(static_cast<std::uint32_t>(static_cast<std::uint8_t>(value[offset + 3])) << 24U);
|
||||
}
|
||||
|
||||
TEST(RenderiveWebBridge, DecodesOnlyEventMessages) {
|
||||
const auto resize = Web_Event_Adapter::decode(
|
||||
R"({"category":"event","type":"resize","width":800,"height":450})");
|
||||
ASSERT_TRUE(resize.has_value());
|
||||
ASSERT_TRUE(std::holds_alternative<Viewport_Resize>(*resize));
|
||||
EXPECT_EQ(std::get<Viewport_Resize>(*resize).size, (Size{800, 450}));
|
||||
|
||||
const auto pointer = Web_Event_Adapter::decode(
|
||||
R"({"category":"event","type":"pointer_press","x":22.5,"y":18,"button":"left","buttons":1,"modifiers":3})");
|
||||
ASSERT_TRUE(pointer.has_value());
|
||||
ASSERT_TRUE(std::holds_alternative<Pointer_Event>(*pointer));
|
||||
EXPECT_EQ(std::get<Pointer_Event>(*pointer).button, Mouse_Button::Left);
|
||||
EXPECT_DOUBLE_EQ(std::get<Pointer_Event>(*pointer).position.x, 22.5);
|
||||
|
||||
EXPECT_FALSE(Web_Event_Adapter::decode(R"({"category":"command","type":"frame"})"));
|
||||
EXPECT_FALSE(Web_Event_Adapter::decode("not-json"));
|
||||
}
|
||||
|
||||
TEST(RenderiveWebBridge, EncodesRgbaPixelFrame) {
|
||||
const Pixel pixels[] = {pack_rgba(255, 0, 0), pack_rgba(0, 255, 0)};
|
||||
const Image_View image{reinterpret_cast<const std::byte*>(pixels), 2, 1,
|
||||
static_cast<int>(sizeof(pixels)), Pixel_Format::Premultiplied_32};
|
||||
const std::string frame = encode_pixel_frame(image);
|
||||
ASSERT_EQ(frame.size(), pixel_frame_header_size + 8);
|
||||
EXPECT_EQ(frame.substr(0, 4), "RVP1");
|
||||
EXPECT_EQ(read_u32_le(frame, 4), 2U);
|
||||
EXPECT_EQ(read_u32_le(frame, 8), 1U);
|
||||
EXPECT_EQ(read_u32_le(frame, 12), 8U);
|
||||
const auto* rgba = reinterpret_cast<const std::uint8_t*>(frame.data() + pixel_frame_header_size);
|
||||
EXPECT_EQ(rgba[0], 255);
|
||||
EXPECT_EQ(rgba[1], 0);
|
||||
EXPECT_EQ(rgba[2], 0);
|
||||
EXPECT_EQ(rgba[3], 255);
|
||||
EXPECT_EQ(rgba[4], 0);
|
||||
EXPECT_EQ(rgba[5], 255);
|
||||
}
|
||||
|
||||
TEST(RenderiveWebBridge, RendersPixelsOnlyForFrameEvent) {
|
||||
Web_Plot_Session session;
|
||||
EXPECT_FALSE(session.handle(Viewport_Resize{{640, 360}}).has_value());
|
||||
EXPECT_FALSE(session.handle(Set_Demo_Mode{Demo_Mode::Usb}).has_value());
|
||||
const auto frame = session.handle(Frame_Request{});
|
||||
ASSERT_TRUE(frame.has_value());
|
||||
ASSERT_GE(frame->size(), pixel_frame_header_size);
|
||||
EXPECT_EQ(frame->substr(0, 4), "RVP1");
|
||||
EXPECT_EQ(read_u32_le(*frame, 4), 640U);
|
||||
EXPECT_EQ(read_u32_le(*frame, 8), 360U);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace renderive::web
|
||||
Reference in New Issue
Block a user