feat: scaffold project framework with engine, node skeleton, and passthrough node
- CMake build system with vcpkg dependency management - libdmf-node: Node interface, port definitions, WebSocket control server, node runner with MXL lifecycle management - libdmf-engine: Graph model (nodes/edges CRUD, serialization), FlowManager (UUID + V210 NMOS flow definitions), ProcessManager (fork/exec node processes), ApiServer (REST API for graph control) - Passthrough node: reads V210 grains from MXL, copies, writes to MXL - Unit tests for Graph and FlowManager (6 cases, 21 assertions) - MXL SDK as git submodule (symlinked from extern/mxl)
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
#include <dmf-engine/api_server.hpp>
|
||||
#include <dmf-engine/graph.hpp>
|
||||
#include <dmf-engine/flow_manager.hpp>
|
||||
#include <dmf-engine/process_manager.hpp>
|
||||
|
||||
#include <libwebsockets.h>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace dmf_engine {
|
||||
|
||||
struct ApiServerImpl {
|
||||
uint16_t port = 0;
|
||||
Graph* graph = nullptr;
|
||||
FlowManager* flow_manager = nullptr;
|
||||
ProcessManager* process_manager = nullptr;
|
||||
struct lws_context* context = nullptr;
|
||||
};
|
||||
|
||||
struct ApiServer::Impl {
|
||||
std::unique_ptr<ApiServerImpl> data;
|
||||
};
|
||||
|
||||
struct HttpRequest {
|
||||
std::string method;
|
||||
std::string path;
|
||||
std::string body;
|
||||
bool body_done = false;
|
||||
};
|
||||
|
||||
static ApiServerImpl* g_impl = nullptr;
|
||||
|
||||
static int callback_http(struct lws* wsi, enum lws_callback_reasons reason,
|
||||
void* user, void* in, size_t len) {
|
||||
auto* req = static_cast<HttpRequest*>(user);
|
||||
|
||||
switch (reason) {
|
||||
case LWS_CALLBACK_HTTP: {
|
||||
new (req) HttpRequest();
|
||||
|
||||
if (lws_hdr_total_length(wsi, WSI_TOKEN_POST_URI) > 0) {
|
||||
req->method = "POST";
|
||||
char buf[256] = {};
|
||||
lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_POST_URI);
|
||||
req->path = buf;
|
||||
} else {
|
||||
req->method = "GET";
|
||||
char buf[256] = {};
|
||||
lws_hdr_copy(wsi, buf, sizeof(buf), WSI_TOKEN_GET_URI);
|
||||
req->path = buf;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LWS_CALLBACK_HTTP_BODY: {
|
||||
req->body.append(static_cast<char*>(in), len);
|
||||
break;
|
||||
}
|
||||
case LWS_CALLBACK_HTTP_BODY_COMPLETION: {
|
||||
if (!g_impl || !g_impl->graph) {
|
||||
lws_return_http_status(wsi, HTTP_STATUS_INTERNAL_SERVER_ERROR, "Server error");
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::string status_str, content_type, response_body;
|
||||
|
||||
auto ok = [&](const nlohmann::json& j) {
|
||||
status_str = "200 OK";
|
||||
response_body = j.dump();
|
||||
};
|
||||
auto created = [&](const nlohmann::json& j) {
|
||||
status_str = "201 Created";
|
||||
response_body = j.dump();
|
||||
};
|
||||
auto error_resp = [&](int code, const std::string& msg) {
|
||||
status_str = std::to_string(code) + " Error";
|
||||
response_body = nlohmann::json({{"error", msg}}).dump();
|
||||
};
|
||||
|
||||
content_type = "application/json";
|
||||
|
||||
try {
|
||||
nlohmann::json req_body = req->body.empty() ? nlohmann::json::object() : nlohmann::json::parse(req->body);
|
||||
|
||||
auto& graph = *g_impl->graph;
|
||||
auto& fm = *g_impl->flow_manager;
|
||||
auto& pm = *g_impl->process_manager;
|
||||
|
||||
if (req->path == "/api/graph" && req->method == "GET") {
|
||||
ok(graph.serialize());
|
||||
} else if (req->path == "/api/graph/nodes" && req->method == "POST") {
|
||||
if (!req_body.contains("type")) {
|
||||
error_resp(400, "Missing 'type' field");
|
||||
} else {
|
||||
auto type = req_body["type"].get<std::string>();
|
||||
auto config = req_body.value("config", nlohmann::json::object());
|
||||
auto id = graph.add_node(type, config);
|
||||
created({{"id", id}});
|
||||
}
|
||||
} else if (req->path.find("/api/graph/nodes/") == 0 && req->method == "DELETE") {
|
||||
auto node_id = req->path.substr(std::string("/api/graph/nodes/").length());
|
||||
if (graph.remove_node(node_id)) {
|
||||
ok({{"deleted", node_id}});
|
||||
} else {
|
||||
error_resp(404, "Node not found: " + node_id);
|
||||
}
|
||||
} else if (req->path == "/api/graph/edges" && req->method == "POST") {
|
||||
if (!req_body.contains("from_node") || !req_body.contains("to_node")) {
|
||||
error_resp(400, "Missing from_node/to_node");
|
||||
} else {
|
||||
auto from_node = req_body["from_node"].get<std::string>();
|
||||
auto from_port = req_body.value("from_port", "video_out");
|
||||
auto to_node = req_body["to_node"].get<std::string>();
|
||||
auto to_port = req_body.value("to_port", "video_in");
|
||||
|
||||
auto flow_id = fm.create_flow_id();
|
||||
auto flow_def = fm.create_v210_flow_def(flow_id, 1920, 1080, 50, 1);
|
||||
|
||||
auto edge_id = graph.add_edge(from_node, from_port, to_node, to_port, flow_id, flow_def);
|
||||
created({{"id", edge_id}, {"flow_id", flow_id}});
|
||||
}
|
||||
} else if (req->path.find("/api/graph/edges/") == 0 && req->method == "DELETE") {
|
||||
auto edge_id = req->path.substr(std::string("/api/graph/edges/").length());
|
||||
if (graph.remove_edge(edge_id)) {
|
||||
ok({{"deleted", edge_id}});
|
||||
} else {
|
||||
error_resp(404, "Edge not found: " + edge_id);
|
||||
}
|
||||
} else if (req->path == "/api/graph/start" && req->method == "POST") {
|
||||
auto nodes = graph.get_nodes();
|
||||
uint16_t port = 9100;
|
||||
for (auto& node : nodes) {
|
||||
pm.start_node(const_cast<GraphNode&>(node), "/dev/shm/mxl", port++);
|
||||
}
|
||||
ok({{"status", "started"}});
|
||||
} else if (req->path == "/api/graph/stop" && req->method == "POST") {
|
||||
auto nodes = graph.get_nodes();
|
||||
for (auto& node : nodes) {
|
||||
pm.stop_node(const_cast<GraphNode&>(node));
|
||||
}
|
||||
ok({{"status", "stopped"}});
|
||||
} else {
|
||||
error_resp(404, "Not found: " + req->method + " " + req->path);
|
||||
}
|
||||
} catch (const nlohmann::json::exception& e) {
|
||||
error_resp(400, std::string("JSON error: ") + e.what());
|
||||
} catch (const std::exception& e) {
|
||||
error_resp(500, e.what());
|
||||
}
|
||||
|
||||
auto headers = "HTTP/1.1 " + status_str + "\r\n"
|
||||
"Content-Type: " + content_type + "\r\n"
|
||||
"Access-Control-Allow-Origin: *\r\n"
|
||||
"Access-Control-Allow-Methods: GET, POST, DELETE, PUT, OPTIONS\r\n"
|
||||
"Access-Control-Allow-Headers: Content-Type\r\n"
|
||||
"Content-Length: " + std::to_string(response_body.size()) + "\r\n"
|
||||
"\r\n";
|
||||
|
||||
std::vector<uint8_t> buf(LWS_PRE + headers.size() + response_body.size());
|
||||
std::memcpy(buf.data() + LWS_PRE, headers.data(), headers.size());
|
||||
std::memcpy(buf.data() + LWS_PRE + headers.size(), response_body.data(), response_body.size());
|
||||
|
||||
lws_write(wsi, buf.data() + LWS_PRE, headers.size() + response_body.size(), LWS_WRITE_HTTP);
|
||||
|
||||
if (lws_http_transaction_completed(wsi)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct lws_protocols protocols[] = {
|
||||
{"http-api", callback_http, sizeof(HttpRequest), 0},
|
||||
{nullptr, nullptr, 0, 0},
|
||||
};
|
||||
|
||||
ApiServer::ApiServer(uint16_t port, Graph& graph, FlowManager& flow_manager, ProcessManager& process_manager)
|
||||
: impl_(std::make_unique<Impl>()) {
|
||||
impl_->data = std::make_unique<ApiServerImpl>();
|
||||
impl_->data->port = port;
|
||||
impl_->data->graph = &graph;
|
||||
impl_->data->flow_manager = &flow_manager;
|
||||
impl_->data->process_manager = &process_manager;
|
||||
|
||||
g_impl = impl_->data.get();
|
||||
|
||||
struct lws_context_creation_info info;
|
||||
std::memset(&info, 0, sizeof(info));
|
||||
info.port = port;
|
||||
info.protocols = protocols;
|
||||
info.gid = -1;
|
||||
info.uid = -1;
|
||||
|
||||
impl_->data->context = lws_create_context(&info);
|
||||
if (!impl_->data->context) {
|
||||
spdlog::error("Failed to create HTTP context on port {}", port);
|
||||
throw std::runtime_error("Failed to create HTTP context");
|
||||
}
|
||||
spdlog::info("API server: listening on port {}", port);
|
||||
}
|
||||
|
||||
ApiServer::~ApiServer() {
|
||||
if (impl_->data && impl_->data->context) {
|
||||
lws_context_destroy(impl_->data->context);
|
||||
}
|
||||
if (g_impl == impl_->data.get()) {
|
||||
g_impl = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void ApiServer::poll(int timeout_ms) {
|
||||
lws_service(impl_->data->context, timeout_ms);
|
||||
}
|
||||
|
||||
} // namespace dmf_engine
|
||||
Reference in New Issue
Block a user