feat: add node HTTP control, engine-to-node communication, connect-input/output API
- Node control server now accepts HTTP POST /cmd for command dispatch (in addition to existing WebSocket control) - Added NodeControlClient: engine sends commands to nodes via HTTP POST - New REST endpoints: POST /api/graph/nodes/:id/connect-input - connect node input to MXL flow POST /api/graph/nodes/:id/connect-output - create new MXL flow + connect output POST /api/graph/nodes/:id/disconnect-port - remove reader/writer POST /api/graph/nodes/:id/command - send raw command to node - Flow IDs now use proper UUID v4 format (MXL requires standard UUIDs) - Flow definitions use NMOS format (urn:x-nmos:format:video) - Engine passes --mxl-domain to node processes - User-provided node IDs (via 'id' field in POST body) - End-to-end verified: testsrc → passthrough → new MXL output flow
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
#include <dmf-engine/node_control_client.hpp>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <cstring>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <sstream>
|
||||
|
||||
namespace dmf_engine {
|
||||
|
||||
void NodeControlClient::register_node(const std::string& node_id, uint16_t port) {
|
||||
node_ports_[node_id] = port;
|
||||
spdlog::info("NodeControlClient: registered node '{}' on port {}", node_id, port);
|
||||
}
|
||||
|
||||
void NodeControlClient::unregister_node(const std::string& node_id) {
|
||||
node_ports_.erase(node_id);
|
||||
}
|
||||
|
||||
uint16_t NodeControlClient::get_port(const std::string& node_id) const {
|
||||
auto it = node_ports_.find(node_id);
|
||||
return it != node_ports_.end() ? it->second : 0;
|
||||
}
|
||||
|
||||
bool NodeControlClient::send_command(uint16_t port, const std::string& json_cmd) {
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0) {
|
||||
spdlog::error("NodeControlClient: socket() failed: {}", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
struct timeval tv;
|
||||
tv.tv_sec = 2;
|
||||
tv.tv_usec = 0;
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
|
||||
|
||||
struct sockaddr_in addr;
|
||||
std::memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
|
||||
|
||||
if (connect(fd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) < 0) {
|
||||
spdlog::error("NodeControlClient: connect to port {} failed: {}", port, strerror(errno));
|
||||
close(fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ostringstream req;
|
||||
req << "POST /cmd HTTP/1.1\r\n"
|
||||
<< "Host: 127.0.0.1:" << port << "\r\n"
|
||||
<< "Content-Type: application/json\r\n"
|
||||
<< "Content-Length: " << json_cmd.size() << "\r\n"
|
||||
<< "Connection: close\r\n"
|
||||
<< "\r\n"
|
||||
<< json_cmd;
|
||||
|
||||
auto request = req.str();
|
||||
auto sent = write(fd, request.data(), request.size());
|
||||
if (sent != static_cast<ssize_t>(request.size())) {
|
||||
spdlog::error("NodeControlClient: write failed on port {}", port);
|
||||
close(fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
char resp_buf[4096] = {};
|
||||
auto n = read(fd, resp_buf, sizeof(resp_buf) - 1);
|
||||
close(fd);
|
||||
|
||||
if (n <= 0) {
|
||||
spdlog::error("NodeControlClient: read failed on port {}", port);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string resp(resp_buf, n);
|
||||
bool ok = resp.find("200 OK") != std::string::npos || resp.find("201 Created") != std::string::npos;
|
||||
if (!ok) {
|
||||
spdlog::warn("NodeControlClient: command failed on port {}: {}", port, resp.substr(0, 100));
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace dmf_engine
|
||||
Reference in New Issue
Block a user