Add protocol module, video-node binary, query/web CLI tools
- Protocol module: framed binary encoding for control requests/responses (ENUM_DEVICES, ENUM_CONTROLS, GET/SET_CONTROL, STREAM_OPEN/CLOSE) - video-node: scans /dev/media* and /dev/video*, serves V4L2 device topology and controls over TCP; uses UDP discovery for peer announce - query_cli: auto-discovers a node, queries devices and controls - protocol_cli: low-level protocol frame decoder for debugging - dev/web: Express 5 ESM web inspector — live SSE discovery picker, REST bridge to video-node, controls UI with sliders/selects/checkboxes - Makefile: sequential module builds before cli/node to fix make -j races - common.mk: add DEPFLAGS (-MMD -MP) for automatic header dependencies - All module Makefiles: split compile/link, generate .d dependency files - discovery: replace 100ms poll loop with pthread_cond_timedwait; respond to all announcements (not just new peers) for instant re-discovery - ENUM_DEVICES response: carry device_caps (V4L2_CAP_*) per video node so clients can distinguish capture nodes from metadata nodes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,11 +7,13 @@ MODULE_BUILD = $(BUILD)/common
|
||||
|
||||
all: $(MODULE_BUILD)/error.o
|
||||
|
||||
$(MODULE_BUILD)/error.o: error.c $(ROOT)/include/error.h | $(MODULE_BUILD)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
$(MODULE_BUILD)/error.o: error.c | $(MODULE_BUILD)
|
||||
$(CC) $(CFLAGS) $(DEPFLAGS) -c -o $@ $<
|
||||
|
||||
$(MODULE_BUILD):
|
||||
mkdir -p $@
|
||||
|
||||
clean:
|
||||
rm -f $(MODULE_BUILD)/error.o
|
||||
rm -f $(MODULE_BUILD)/error.o $(MODULE_BUILD)/error.d
|
||||
|
||||
-include $(MODULE_BUILD)/error.d
|
||||
|
||||
@@ -17,7 +17,15 @@ void app_error_print(struct App_Error *e) {
|
||||
fprintf(stderr, "syscall error: %s\n", strerror(e->detail.syscall.err_no));
|
||||
break;
|
||||
case ERR_INVALID:
|
||||
fprintf(stderr, "invalid argument\n");
|
||||
if (e->detail.invalid.config_line > 0) {
|
||||
fprintf(stderr, "config error at line %d: %s\n",
|
||||
e->detail.invalid.config_line,
|
||||
e->detail.invalid.message ? e->detail.invalid.message : "invalid value");
|
||||
} else if (e->detail.invalid.message) {
|
||||
fprintf(stderr, "invalid: %s\n", e->detail.invalid.message);
|
||||
} else {
|
||||
fprintf(stderr, "invalid argument\n");
|
||||
}
|
||||
break;
|
||||
case ERR_NOT_FOUND:
|
||||
fprintf(stderr, "not found\n");
|
||||
|
||||
@@ -7,11 +7,13 @@ MODULE_BUILD = $(BUILD)/config
|
||||
|
||||
all: $(MODULE_BUILD)/config.o
|
||||
|
||||
$(MODULE_BUILD)/config.o: config.c $(ROOT)/include/config.h | $(MODULE_BUILD)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
$(MODULE_BUILD)/config.o: config.c | $(MODULE_BUILD)
|
||||
$(CC) $(CFLAGS) $(DEPFLAGS) -c -o $@ $<
|
||||
|
||||
$(MODULE_BUILD):
|
||||
mkdir -p $@
|
||||
|
||||
clean:
|
||||
rm -f $(MODULE_BUILD)/config.o
|
||||
rm -f $(MODULE_BUILD)/config.o $(MODULE_BUILD)/config.d
|
||||
|
||||
-include $(MODULE_BUILD)/config.d
|
||||
|
||||
@@ -54,7 +54,10 @@ static void normalise_separators(char *s) {
|
||||
|
||||
/* -- parse typed value ----------------------------------------------------- */
|
||||
|
||||
static uint32_t parse_flags(const char *raw, const struct Config_Flag_Def *defs) {
|
||||
/* returns -1 on unknown token, otherwise 0 and sets *out */
|
||||
static int parse_flags(const char *raw, const struct Config_Flag_Def *defs,
|
||||
uint32_t *out, char *bad_token, size_t bad_token_len)
|
||||
{
|
||||
char buf[MAX_STR];
|
||||
strncpy(buf, raw, MAX_STR - 1);
|
||||
buf[MAX_STR - 1] = '\0';
|
||||
@@ -63,16 +66,24 @@ static uint32_t parse_flags(const char *raw, const struct Config_Flag_Def *defs)
|
||||
uint32_t bits = 0;
|
||||
char *tok = strtok(buf, " \t");
|
||||
while (tok) {
|
||||
int found = 0;
|
||||
for (const struct Config_Flag_Def *fd = defs; fd && fd->token; fd++) {
|
||||
if (strcmp(tok, fd->token) == 0) { bits |= fd->value; break; }
|
||||
if (strcmp(tok, fd->token) == 0) { bits |= fd->value; found = 1; break; }
|
||||
}
|
||||
if (!found) {
|
||||
strncpy(bad_token, tok, bad_token_len - 1);
|
||||
bad_token[bad_token_len - 1] = '\0';
|
||||
return -1;
|
||||
}
|
||||
tok = strtok(NULL, " \t");
|
||||
}
|
||||
return bits;
|
||||
*out = bits;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void set_entry_value(struct Config_Entry *e,
|
||||
const struct Config_Def *def, const char *raw)
|
||||
/* returns APP_OK or an error with config_line set */
|
||||
static struct App_Error set_entry_value(struct Config_Entry *e,
|
||||
const struct Config_Def *def, const char *raw, int cfg_line)
|
||||
{
|
||||
e->type = def->type;
|
||||
switch (def->type) {
|
||||
@@ -80,16 +91,36 @@ static void set_entry_value(struct Config_Entry *e,
|
||||
strncpy(e->val.s, raw, MAX_STR - 1);
|
||||
e->val.s[MAX_STR - 1] = '\0';
|
||||
break;
|
||||
case CONFIG_UINT16:
|
||||
e->val.u16 = (uint16_t)strtoul(raw, NULL, 10);
|
||||
case CONFIG_UINT16: {
|
||||
char *end;
|
||||
unsigned long v = strtoul(raw, &end, 10);
|
||||
if (end == raw || *end != '\0') {
|
||||
return APP_INVALID_ERROR_MSG(cfg_line, "expected integer");
|
||||
}
|
||||
if (v > 0xFFFF) {
|
||||
return APP_INVALID_ERROR_MSG(cfg_line, "value out of range for u16");
|
||||
}
|
||||
e->val.u16 = (uint16_t)v;
|
||||
break;
|
||||
case CONFIG_UINT32:
|
||||
e->val.u32 = (uint32_t)strtoul(raw, NULL, 10);
|
||||
}
|
||||
case CONFIG_UINT32: {
|
||||
char *end;
|
||||
unsigned long v = strtoul(raw, &end, 10);
|
||||
if (end == raw || *end != '\0') {
|
||||
return APP_INVALID_ERROR_MSG(cfg_line, "expected integer");
|
||||
}
|
||||
e->val.u32 = (uint32_t)v;
|
||||
break;
|
||||
case CONFIG_FLAGS:
|
||||
e->val.flags = parse_flags(raw, def->flags);
|
||||
}
|
||||
case CONFIG_FLAGS: {
|
||||
char bad[MAX_STR];
|
||||
if (parse_flags(raw, def->flags, &e->val.flags, bad, sizeof(bad)) != 0) {
|
||||
return APP_INVALID_ERROR_MSG(cfg_line, "unknown flag token");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
/* -- entry table ----------------------------------------------------------- */
|
||||
@@ -130,13 +161,15 @@ static struct Config_Entry *add_entry(struct Config *cfg,
|
||||
|
||||
/* -- fill defaults --------------------------------------------------------- */
|
||||
|
||||
static void fill_defaults(struct Config *cfg) {
|
||||
static struct App_Error fill_defaults(struct Config *cfg) {
|
||||
for (const struct Config_Def *d = cfg->schema; d->section; d++) {
|
||||
if (find_entry(cfg, d->section, d->key)) { continue; }
|
||||
struct Config_Entry *e = add_entry(cfg, d->section, d->key);
|
||||
if (!e) { continue; }
|
||||
set_entry_value(e, d, d->default_val ? d->default_val : "");
|
||||
struct App_Error err = set_entry_value(e, d, d->default_val ? d->default_val : "", 0);
|
||||
if (!APP_IS_OK(err)) { return err; }
|
||||
}
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
/* -- file parser ----------------------------------------------------------- */
|
||||
@@ -147,8 +180,10 @@ static struct App_Error parse_file(struct Config *cfg, const char *path) {
|
||||
|
||||
char line[MAX_LINE];
|
||||
char section[MAX_STR] = "";
|
||||
int lineno = 0;
|
||||
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
lineno++;
|
||||
strip_comment(line);
|
||||
strip(line);
|
||||
if (line[0] == '\0') { continue; }
|
||||
@@ -178,7 +213,9 @@ static struct App_Error parse_file(struct Config *cfg, const char *path) {
|
||||
struct Config_Entry *e = find_entry(cfg, section, key);
|
||||
if (!e) { e = add_entry(cfg, section, key); }
|
||||
if (!e) { continue; }
|
||||
set_entry_value(e, def, val);
|
||||
|
||||
struct App_Error err = set_entry_value(e, def, val, lineno);
|
||||
if (!APP_IS_OK(err)) { fclose(f); return err; }
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
@@ -197,7 +234,9 @@ struct App_Error config_load(struct Config **out, const char *path,
|
||||
struct App_Error err = parse_file(cfg, path);
|
||||
if (!APP_IS_OK(err)) { free(cfg); return err; }
|
||||
|
||||
fill_defaults(cfg);
|
||||
err = fill_defaults(cfg);
|
||||
if (!APP_IS_OK(err)) { free(cfg); return err; }
|
||||
|
||||
*out = cfg;
|
||||
return APP_OK;
|
||||
}
|
||||
@@ -208,7 +247,10 @@ struct App_Error config_defaults(struct Config **out,
|
||||
struct Config *cfg = calloc(1, sizeof(*cfg));
|
||||
if (!cfg) { return APP_SYSCALL_ERROR(); }
|
||||
cfg->schema = schema;
|
||||
fill_defaults(cfg);
|
||||
|
||||
struct App_Error err = fill_defaults(cfg);
|
||||
if (!APP_IS_OK(err)) { free(cfg); return err; }
|
||||
|
||||
*out = cfg;
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
@@ -7,11 +7,13 @@ MODULE_BUILD = $(BUILD)/discovery
|
||||
|
||||
all: $(MODULE_BUILD)/discovery.o
|
||||
|
||||
$(MODULE_BUILD)/discovery.o: discovery.c $(ROOT)/include/discovery.h | $(MODULE_BUILD)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
$(MODULE_BUILD)/discovery.o: discovery.c | $(MODULE_BUILD)
|
||||
$(CC) $(CFLAGS) $(DEPFLAGS) -c -o $@ $<
|
||||
|
||||
$(MODULE_BUILD):
|
||||
mkdir -p $@
|
||||
|
||||
clean:
|
||||
rm -f $(MODULE_BUILD)/discovery.o
|
||||
rm -f $(MODULE_BUILD)/discovery.o $(MODULE_BUILD)/discovery.d
|
||||
|
||||
-include $(MODULE_BUILD)/discovery.d
|
||||
|
||||
@@ -40,7 +40,9 @@ struct Discovery {
|
||||
pthread_t announce_thread;
|
||||
pthread_t receive_thread;
|
||||
atomic_int running;
|
||||
atomic_int early_announce; /* set when a new peer is seen */
|
||||
|
||||
pthread_mutex_t announce_mutex;
|
||||
pthread_cond_t announce_cond; /* signaled to wake announce thread early */
|
||||
|
||||
pthread_mutex_t peers_mutex;
|
||||
struct Peer_Entry peers[DISCOVERY_MAX_PEERS];
|
||||
@@ -131,23 +133,26 @@ static void *announce_thread_fn(void *arg) {
|
||||
|
||||
send_announcement(d);
|
||||
|
||||
pthread_mutex_lock(&d->announce_mutex);
|
||||
while (atomic_load(&d->running)) {
|
||||
/* sleep in 100 ms increments; breaks early if a new peer is detected
|
||||
* or if destroy is called */
|
||||
uint32_t elapsed = 0;
|
||||
while (atomic_load(&d->running) && elapsed < d->config.interval_ms) {
|
||||
if (atomic_load(&d->early_announce)) { break; }
|
||||
struct timespec ts = { .tv_sec = 0, .tv_nsec = 100 * 1000000L };
|
||||
nanosleep(&ts, NULL);
|
||||
elapsed += 100;
|
||||
struct timespec abs;
|
||||
clock_gettime(CLOCK_REALTIME, &abs);
|
||||
uint32_t ms = d->config.interval_ms;
|
||||
abs.tv_sec += ms / 1000u;
|
||||
abs.tv_nsec += (long)(ms % 1000u) * 1000000L;
|
||||
if (abs.tv_nsec >= 1000000000L) {
|
||||
abs.tv_sec++;
|
||||
abs.tv_nsec -= 1000000000L;
|
||||
}
|
||||
/* blocks until signaled (new peer / shutdown) or interval elapses */
|
||||
pthread_cond_timedwait(&d->announce_cond, &d->announce_mutex, &abs);
|
||||
|
||||
if (!atomic_load(&d->running)) { break; }
|
||||
|
||||
atomic_store(&d->early_announce, 0);
|
||||
send_announcement(d);
|
||||
check_timeouts(d);
|
||||
}
|
||||
pthread_mutex_unlock(&d->announce_mutex);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
@@ -228,11 +233,14 @@ static void *receive_thread_fn(void *arg) {
|
||||
|
||||
pthread_mutex_unlock(&d->peers_mutex);
|
||||
|
||||
if (is_new) {
|
||||
atomic_store(&d->early_announce, 1);
|
||||
if (d->config.on_peer_found) {
|
||||
d->config.on_peer_found(&peer_copy, d->config.userdata);
|
||||
}
|
||||
/* respond to every announcement — the sender may be a fresh instance
|
||||
* that doesn't know about us yet even if we already have it in our table */
|
||||
pthread_mutex_lock(&d->announce_mutex);
|
||||
pthread_cond_signal(&d->announce_cond);
|
||||
pthread_mutex_unlock(&d->announce_mutex);
|
||||
|
||||
if (is_new && d->config.on_peer_found) {
|
||||
d->config.on_peer_found(&peer_copy, d->config.userdata);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,7 +270,8 @@ struct App_Error discovery_create(struct Discovery **out,
|
||||
inet_pton(AF_INET, DISCOVERY_MULTICAST_GROUP, &d->mcast_addr.sin_addr);
|
||||
|
||||
atomic_init(&d->running, 0);
|
||||
atomic_init(&d->early_announce, 0);
|
||||
pthread_mutex_init(&d->announce_mutex, NULL);
|
||||
pthread_cond_init(&d->announce_cond, NULL);
|
||||
pthread_mutex_init(&d->peers_mutex, NULL);
|
||||
|
||||
*out = d;
|
||||
@@ -326,9 +335,15 @@ struct App_Error discovery_start(struct Discovery *d) {
|
||||
|
||||
void discovery_destroy(struct Discovery *d) {
|
||||
atomic_store(&d->running, 0);
|
||||
/* wake announce thread so it exits without waiting for the full interval */
|
||||
pthread_mutex_lock(&d->announce_mutex);
|
||||
pthread_cond_signal(&d->announce_cond);
|
||||
pthread_mutex_unlock(&d->announce_mutex);
|
||||
close(d->sock);
|
||||
pthread_join(d->announce_thread, NULL);
|
||||
pthread_join(d->receive_thread, NULL);
|
||||
pthread_cond_destroy(&d->announce_cond);
|
||||
pthread_mutex_destroy(&d->announce_mutex);
|
||||
pthread_mutex_destroy(&d->peers_mutex);
|
||||
free(d);
|
||||
}
|
||||
|
||||
@@ -7,11 +7,13 @@ MODULE_BUILD = $(BUILD)/media_ctrl
|
||||
|
||||
all: $(MODULE_BUILD)/media_ctrl.o
|
||||
|
||||
$(MODULE_BUILD)/media_ctrl.o: media_ctrl.c $(ROOT)/include/media_ctrl.h $(ROOT)/include/error.h | $(MODULE_BUILD)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
$(MODULE_BUILD)/media_ctrl.o: media_ctrl.c | $(MODULE_BUILD)
|
||||
$(CC) $(CFLAGS) $(DEPFLAGS) -c -o $@ $<
|
||||
|
||||
$(MODULE_BUILD):
|
||||
mkdir -p $@
|
||||
|
||||
clean:
|
||||
rm -f $(MODULE_BUILD)/media_ctrl.o
|
||||
rm -f $(MODULE_BUILD)/media_ctrl.o $(MODULE_BUILD)/media_ctrl.d
|
||||
|
||||
-include $(MODULE_BUILD)/media_ctrl.d
|
||||
|
||||
19
src/modules/protocol/Makefile
Normal file
19
src/modules/protocol/Makefile
Normal file
@@ -0,0 +1,19 @@
|
||||
ROOT := $(abspath ../../..)
|
||||
include $(ROOT)/common.mk
|
||||
|
||||
MODULE_BUILD = $(BUILD)/protocol
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(MODULE_BUILD)/protocol.o
|
||||
|
||||
$(MODULE_BUILD)/protocol.o: protocol.c | $(MODULE_BUILD)
|
||||
$(CC) $(CFLAGS) $(DEPFLAGS) -c -o $@ $<
|
||||
|
||||
$(MODULE_BUILD):
|
||||
mkdir -p $@
|
||||
|
||||
clean:
|
||||
rm -f $(MODULE_BUILD)/protocol.o $(MODULE_BUILD)/protocol.d
|
||||
|
||||
-include $(MODULE_BUILD)/protocol.d
|
||||
706
src/modules/protocol/protocol.c
Normal file
706
src/modules/protocol/protocol.c
Normal file
@@ -0,0 +1,706 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "protocol.h"
|
||||
#include "serial.h"
|
||||
|
||||
/* -- growable write buffer ------------------------------------------------- */
|
||||
|
||||
struct Wbuf {
|
||||
uint8_t *data;
|
||||
uint32_t len;
|
||||
uint32_t cap;
|
||||
};
|
||||
|
||||
static struct App_Error wbuf_init(struct Wbuf *b, uint32_t initial) {
|
||||
b->data = malloc(initial);
|
||||
if (!b->data) { return APP_SYSCALL_ERROR(); }
|
||||
b->len = 0;
|
||||
b->cap = initial;
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
static void wbuf_free(struct Wbuf *b) {
|
||||
free(b->data);
|
||||
}
|
||||
|
||||
static struct App_Error wbuf_grow(struct Wbuf *b, uint32_t extra) {
|
||||
if (b->len + extra <= b->cap) { return APP_OK; }
|
||||
uint32_t newcap = b->cap ? b->cap : 64;
|
||||
while (newcap < b->len + extra) { newcap *= 2; }
|
||||
uint8_t *p = realloc(b->data, newcap);
|
||||
if (!p) { return APP_SYSCALL_ERROR(); }
|
||||
b->data = p;
|
||||
b->cap = newcap;
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
static struct App_Error wbuf_u8(struct Wbuf *b, uint8_t v) {
|
||||
struct App_Error e = wbuf_grow(b, 1);
|
||||
if (!APP_IS_OK(e)) { return e; }
|
||||
put_u8(b->data, b->len, v);
|
||||
b->len += 1;
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
static struct App_Error wbuf_u16(struct Wbuf *b, uint16_t v) {
|
||||
struct App_Error e = wbuf_grow(b, 2);
|
||||
if (!APP_IS_OK(e)) { return e; }
|
||||
put_u16(b->data, b->len, v);
|
||||
b->len += 2;
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
static struct App_Error wbuf_u32(struct Wbuf *b, uint32_t v) {
|
||||
struct App_Error e = wbuf_grow(b, 4);
|
||||
if (!APP_IS_OK(e)) { return e; }
|
||||
put_u32(b->data, b->len, v);
|
||||
b->len += 4;
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
static struct App_Error wbuf_i32(struct Wbuf *b, int32_t v) {
|
||||
struct App_Error e = wbuf_grow(b, 4);
|
||||
if (!APP_IS_OK(e)) { return e; }
|
||||
put_i32(b->data, b->len, v);
|
||||
b->len += 4;
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
static struct App_Error wbuf_i64(struct Wbuf *b, int64_t v) {
|
||||
struct App_Error e = wbuf_grow(b, 8);
|
||||
if (!APP_IS_OK(e)) { return e; }
|
||||
put_i64(b->data, b->len, v);
|
||||
b->len += 8;
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
/* Serialise a string as u8 length prefix + bytes (truncates at 255). */
|
||||
static struct App_Error wbuf_str8(struct Wbuf *b, const char *s) {
|
||||
size_t slen = s ? strlen(s) : 0;
|
||||
uint8_t n = (slen > 255u) ? 255u : (uint8_t)slen;
|
||||
struct App_Error e = wbuf_grow(b, (uint32_t)1 + n);
|
||||
if (!APP_IS_OK(e)) { return e; }
|
||||
put_u8(b->data, b->len, n);
|
||||
b->len += 1;
|
||||
memcpy(b->data + b->len, s, n);
|
||||
b->len += n;
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
static struct App_Error wbuf_bytes(struct Wbuf *b,
|
||||
const uint8_t *data, uint32_t len)
|
||||
{
|
||||
struct App_Error e = wbuf_grow(b, len);
|
||||
if (!APP_IS_OK(e)) { return e; }
|
||||
memcpy(b->data + b->len, data, len);
|
||||
b->len += len;
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
/* -- sequential read cursor ------------------------------------------------ */
|
||||
|
||||
struct Cursor {
|
||||
const uint8_t *buf;
|
||||
uint32_t len;
|
||||
uint32_t pos;
|
||||
int ok;
|
||||
};
|
||||
|
||||
static void cur_init(struct Cursor *c, const uint8_t *buf, uint32_t len) {
|
||||
c->buf = buf;
|
||||
c->len = len;
|
||||
c->pos = 0;
|
||||
c->ok = 1;
|
||||
}
|
||||
|
||||
static void cur_need(struct Cursor *c, uint32_t n) {
|
||||
if (c->ok && c->pos + n > c->len) { c->ok = 0; }
|
||||
}
|
||||
|
||||
static uint8_t cur_u8(struct Cursor *c) {
|
||||
cur_need(c, 1);
|
||||
if (!c->ok) { return 0; }
|
||||
uint8_t v = get_u8(c->buf, c->pos);
|
||||
c->pos += 1;
|
||||
return v;
|
||||
}
|
||||
|
||||
static uint16_t cur_u16(struct Cursor *c) {
|
||||
cur_need(c, 2);
|
||||
if (!c->ok) { return 0; }
|
||||
uint16_t v = get_u16(c->buf, c->pos);
|
||||
c->pos += 2;
|
||||
return v;
|
||||
}
|
||||
|
||||
static uint32_t cur_u32(struct Cursor *c) {
|
||||
cur_need(c, 4);
|
||||
if (!c->ok) { return 0; }
|
||||
uint32_t v = get_u32(c->buf, c->pos);
|
||||
c->pos += 4;
|
||||
return v;
|
||||
}
|
||||
|
||||
static int32_t cur_i32(struct Cursor *c) {
|
||||
cur_need(c, 4);
|
||||
if (!c->ok) { return 0; }
|
||||
int32_t v = get_i32(c->buf, c->pos);
|
||||
c->pos += 4;
|
||||
return v;
|
||||
}
|
||||
|
||||
static int64_t cur_i64(struct Cursor *c) {
|
||||
cur_need(c, 8);
|
||||
if (!c->ok) { return 0; }
|
||||
int64_t v = get_i64(c->buf, c->pos);
|
||||
c->pos += 8;
|
||||
return v;
|
||||
}
|
||||
|
||||
/* Read a u8-length-prefixed string; returns pointer into buf and sets *len. */
|
||||
static const char *cur_str8(struct Cursor *c, uint8_t *len_out) {
|
||||
uint8_t n = cur_u8(c);
|
||||
cur_need(c, n);
|
||||
if (!c->ok) { *len_out = 0; return NULL; }
|
||||
const char *p = (const char *)(c->buf + c->pos);
|
||||
c->pos += n;
|
||||
*len_out = n;
|
||||
return p;
|
||||
}
|
||||
|
||||
|
||||
#define CUR_CHECK(c) \
|
||||
do { if (!(c).ok) { return APP_INVALID_ERROR_MSG(0, "payload too short"); } } while (0)
|
||||
|
||||
/* -- write helpers --------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* Build a CONTROL_RESPONSE base (request_id + status) into b, then append
|
||||
* extra, then send — all in one call. Used by the specific response writers.
|
||||
*/
|
||||
static struct App_Error send_response(struct Transport_Conn *conn,
|
||||
uint16_t request_id, uint16_t status,
|
||||
const uint8_t *extra, uint32_t extra_len)
|
||||
{
|
||||
struct Wbuf b;
|
||||
struct App_Error e = wbuf_init(&b, 4 + extra_len);
|
||||
if (!APP_IS_OK(e)) { return e; }
|
||||
|
||||
e = wbuf_u16(&b, request_id); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u16(&b, status); if (!APP_IS_OK(e)) { goto fail; }
|
||||
if (extra && extra_len > 0) {
|
||||
e = wbuf_bytes(&b, extra, extra_len);
|
||||
if (!APP_IS_OK(e)) { goto fail; }
|
||||
}
|
||||
|
||||
e = transport_send_frame(conn, PROTO_MSG_CONTROL_RESPONSE, b.data, b.len);
|
||||
fail:
|
||||
wbuf_free(&b);
|
||||
return e;
|
||||
}
|
||||
|
||||
/* -- write functions ------------------------------------------------------- */
|
||||
|
||||
struct App_Error proto_write_video_frame(struct Transport_Conn *conn,
|
||||
uint16_t stream_id, const uint8_t *data, uint32_t data_len)
|
||||
{
|
||||
uint32_t total = 2u + data_len;
|
||||
uint8_t *buf = malloc(total);
|
||||
if (!buf) { return APP_SYSCALL_ERROR(); }
|
||||
put_u16(buf, 0, stream_id);
|
||||
memcpy(buf + 2, data, data_len);
|
||||
struct App_Error e = transport_send_frame(conn, PROTO_MSG_VIDEO_FRAME, buf, total);
|
||||
free(buf);
|
||||
return e;
|
||||
}
|
||||
|
||||
struct App_Error proto_write_stream_event(struct Transport_Conn *conn,
|
||||
uint16_t stream_id, uint8_t event_code)
|
||||
{
|
||||
uint8_t buf[3];
|
||||
put_u16(buf, 0, stream_id);
|
||||
put_u8 (buf, 2, event_code);
|
||||
return transport_send_frame(conn, PROTO_MSG_STREAM_EVENT, buf, 3);
|
||||
}
|
||||
|
||||
struct App_Error proto_write_stream_open(struct Transport_Conn *conn,
|
||||
uint16_t request_id, uint16_t stream_id,
|
||||
uint16_t format, uint16_t pixel_format, uint16_t origin)
|
||||
{
|
||||
uint8_t buf[12];
|
||||
put_u16(buf, 0, request_id);
|
||||
put_u16(buf, 2, PROTO_CMD_STREAM_OPEN);
|
||||
put_u16(buf, 4, stream_id);
|
||||
put_u16(buf, 6, format);
|
||||
put_u16(buf, 8, pixel_format);
|
||||
put_u16(buf, 10, origin);
|
||||
return transport_send_frame(conn, PROTO_MSG_CONTROL_REQUEST, buf, 12);
|
||||
}
|
||||
|
||||
struct App_Error proto_write_stream_close(struct Transport_Conn *conn,
|
||||
uint16_t request_id, uint16_t stream_id)
|
||||
{
|
||||
uint8_t buf[6];
|
||||
put_u16(buf, 0, request_id);
|
||||
put_u16(buf, 2, PROTO_CMD_STREAM_CLOSE);
|
||||
put_u16(buf, 4, stream_id);
|
||||
return transport_send_frame(conn, PROTO_MSG_CONTROL_REQUEST, buf, 6);
|
||||
}
|
||||
|
||||
struct App_Error proto_write_enum_devices(struct Transport_Conn *conn,
|
||||
uint16_t request_id)
|
||||
{
|
||||
uint8_t buf[4];
|
||||
put_u16(buf, 0, request_id);
|
||||
put_u16(buf, 2, PROTO_CMD_ENUM_DEVICES);
|
||||
return transport_send_frame(conn, PROTO_MSG_CONTROL_REQUEST, buf, 4);
|
||||
}
|
||||
|
||||
struct App_Error proto_write_enum_controls(struct Transport_Conn *conn,
|
||||
uint16_t request_id, uint16_t device_index)
|
||||
{
|
||||
uint8_t buf[6];
|
||||
put_u16(buf, 0, request_id);
|
||||
put_u16(buf, 2, PROTO_CMD_ENUM_CONTROLS);
|
||||
put_u16(buf, 4, device_index);
|
||||
return transport_send_frame(conn, PROTO_MSG_CONTROL_REQUEST, buf, 6);
|
||||
}
|
||||
|
||||
struct App_Error proto_write_get_control(struct Transport_Conn *conn,
|
||||
uint16_t request_id, uint16_t device_index, uint32_t control_id)
|
||||
{
|
||||
uint8_t buf[10];
|
||||
put_u16(buf, 0, request_id);
|
||||
put_u16(buf, 2, PROTO_CMD_GET_CONTROL);
|
||||
put_u16(buf, 4, device_index);
|
||||
put_u32(buf, 6, control_id);
|
||||
return transport_send_frame(conn, PROTO_MSG_CONTROL_REQUEST, buf, 10);
|
||||
}
|
||||
|
||||
struct App_Error proto_write_set_control(struct Transport_Conn *conn,
|
||||
uint16_t request_id, uint16_t device_index,
|
||||
uint32_t control_id, int32_t value)
|
||||
{
|
||||
uint8_t buf[14];
|
||||
put_u16(buf, 0, request_id);
|
||||
put_u16(buf, 2, PROTO_CMD_SET_CONTROL);
|
||||
put_u16(buf, 4, device_index);
|
||||
put_u32(buf, 6, control_id);
|
||||
put_i32(buf, 10, value);
|
||||
return transport_send_frame(conn, PROTO_MSG_CONTROL_REQUEST, buf, 14);
|
||||
}
|
||||
|
||||
struct App_Error proto_write_enum_monitors(struct Transport_Conn *conn,
|
||||
uint16_t request_id)
|
||||
{
|
||||
uint8_t buf[4];
|
||||
put_u16(buf, 0, request_id);
|
||||
put_u16(buf, 2, PROTO_CMD_ENUM_MONITORS);
|
||||
return transport_send_frame(conn, PROTO_MSG_CONTROL_REQUEST, buf, 4);
|
||||
}
|
||||
|
||||
struct App_Error proto_write_control_response(struct Transport_Conn *conn,
|
||||
uint16_t request_id, uint16_t status,
|
||||
const uint8_t *payload, uint32_t payload_len)
|
||||
{
|
||||
return send_response(conn, request_id, status, payload, payload_len);
|
||||
}
|
||||
|
||||
struct App_Error proto_write_get_control_response(struct Transport_Conn *conn,
|
||||
uint16_t request_id, uint16_t status, int32_t value)
|
||||
{
|
||||
uint8_t extra[4];
|
||||
put_i32(extra, 0, value);
|
||||
return send_response(conn, request_id, status, extra, 4);
|
||||
}
|
||||
|
||||
struct App_Error proto_write_enum_devices_response(struct Transport_Conn *conn,
|
||||
uint16_t request_id, uint16_t status,
|
||||
const struct Proto_Media_Device_Info *media_devices, uint16_t media_count,
|
||||
const struct Proto_Standalone_Device_Info *standalone, uint16_t standalone_count)
|
||||
{
|
||||
struct Wbuf b;
|
||||
struct App_Error e = wbuf_init(&b, 128);
|
||||
if (!APP_IS_OK(e)) { return e; }
|
||||
|
||||
e = wbuf_u16(&b, request_id); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u16(&b, status); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u16(&b, media_count); if (!APP_IS_OK(e)) { goto fail; }
|
||||
|
||||
for (uint16_t i = 0; i < media_count; i++) {
|
||||
const struct Proto_Media_Device_Info *m = &media_devices[i];
|
||||
e = wbuf_str8(&b, m->path); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_str8(&b, m->driver); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_str8(&b, m->model); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_str8(&b, m->bus_info); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u8 (&b, m->video_node_count); if (!APP_IS_OK(e)) { goto fail; }
|
||||
|
||||
for (uint8_t j = 0; j < m->video_node_count; j++) {
|
||||
const struct Proto_Video_Node_Info *v = &m->video_nodes[j];
|
||||
e = wbuf_str8(&b, v->path); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_str8(&b, v->entity_name); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u32 (&b, v->entity_type); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u32 (&b, v->entity_flags);if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u32 (&b, v->device_caps); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u8 (&b, v->pad_flags); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u8 (&b, v->is_capture); if (!APP_IS_OK(e)) { goto fail; }
|
||||
}
|
||||
}
|
||||
|
||||
e = wbuf_u16(&b, standalone_count); if (!APP_IS_OK(e)) { goto fail; }
|
||||
for (uint16_t i = 0; i < standalone_count; i++) {
|
||||
e = wbuf_str8(&b, standalone[i].path); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_str8(&b, standalone[i].name); if (!APP_IS_OK(e)) { goto fail; }
|
||||
}
|
||||
|
||||
e = transport_send_frame(conn, PROTO_MSG_CONTROL_RESPONSE, b.data, b.len);
|
||||
fail:
|
||||
wbuf_free(&b);
|
||||
return e;
|
||||
}
|
||||
|
||||
struct App_Error proto_write_enum_controls_response(struct Transport_Conn *conn,
|
||||
uint16_t request_id, uint16_t status,
|
||||
const struct Proto_Control_Info *controls, uint16_t count)
|
||||
{
|
||||
struct Wbuf b;
|
||||
struct App_Error e = wbuf_init(&b, 256);
|
||||
if (!APP_IS_OK(e)) { return e; }
|
||||
|
||||
e = wbuf_u16(&b, request_id); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u16(&b, status); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u16(&b, count); if (!APP_IS_OK(e)) { goto fail; }
|
||||
|
||||
for (uint16_t i = 0; i < count; i++) {
|
||||
const struct Proto_Control_Info *c = &controls[i];
|
||||
e = wbuf_u32(&b, c->id); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u8 (&b, c->type); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u32(&b, c->flags); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_str8(&b, c->name); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_i32(&b, c->min); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_i32(&b, c->max); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_i32(&b, c->step); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_i32(&b, c->default_val); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_i32(&b, c->current_val); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u8 (&b, c->menu_count); if (!APP_IS_OK(e)) { goto fail; }
|
||||
|
||||
for (uint8_t j = 0; j < c->menu_count; j++) {
|
||||
const struct Proto_Menu_Item *m = &c->menu_items[j];
|
||||
e = wbuf_u32(&b, m->index); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_str8(&b, m->name); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_i64(&b, m->int_value); if (!APP_IS_OK(e)) { goto fail; }
|
||||
}
|
||||
}
|
||||
|
||||
e = transport_send_frame(conn, PROTO_MSG_CONTROL_RESPONSE, b.data, b.len);
|
||||
fail:
|
||||
wbuf_free(&b);
|
||||
return e;
|
||||
}
|
||||
|
||||
struct App_Error proto_write_enum_monitors_response(struct Transport_Conn *conn,
|
||||
uint16_t request_id, uint16_t status,
|
||||
const struct Proto_Monitor_Info *monitors, uint16_t count)
|
||||
{
|
||||
struct Wbuf b;
|
||||
struct App_Error e = wbuf_init(&b, 64);
|
||||
if (!APP_IS_OK(e)) { return e; }
|
||||
|
||||
e = wbuf_u16(&b, request_id); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u16(&b, status); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u16(&b, count); if (!APP_IS_OK(e)) { goto fail; }
|
||||
|
||||
for (uint16_t i = 0; i < count; i++) {
|
||||
const struct Proto_Monitor_Info *m = &monitors[i];
|
||||
e = wbuf_i32(&b, m->x); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_i32(&b, m->y); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u32(&b, m->width); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_u32(&b, m->height); if (!APP_IS_OK(e)) { goto fail; }
|
||||
e = wbuf_str8(&b, m->name); if (!APP_IS_OK(e)) { goto fail; }
|
||||
}
|
||||
|
||||
e = transport_send_frame(conn, PROTO_MSG_CONTROL_RESPONSE, b.data, b.len);
|
||||
fail:
|
||||
wbuf_free(&b);
|
||||
return e;
|
||||
}
|
||||
|
||||
/* -- read functions -------------------------------------------------------- */
|
||||
|
||||
struct App_Error proto_read_video_frame(
|
||||
const uint8_t *payload, uint32_t length,
|
||||
struct Proto_Video_Frame *out)
|
||||
{
|
||||
if (length < 2) { return APP_INVALID_ERROR_MSG(0, "VIDEO_FRAME payload too short"); }
|
||||
out->stream_id = get_u16(payload, 0);
|
||||
out->data = payload + 2;
|
||||
out->data_len = length - 2;
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
struct App_Error proto_read_stream_event(
|
||||
const uint8_t *payload, uint32_t length,
|
||||
struct Proto_Stream_Event *out)
|
||||
{
|
||||
if (length < 3) { return APP_INVALID_ERROR_MSG(0, "STREAM_EVENT payload too short"); }
|
||||
out->stream_id = get_u16(payload, 0);
|
||||
out->event_code = get_u8 (payload, 2);
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
struct App_Error proto_read_request_header(
|
||||
const uint8_t *payload, uint32_t length,
|
||||
struct Proto_Request_Header *out)
|
||||
{
|
||||
if (length < 4) { return APP_INVALID_ERROR_MSG(0, "CONTROL_REQUEST payload too short"); }
|
||||
out->request_id = get_u16(payload, 0);
|
||||
out->command = get_u16(payload, 2);
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
struct App_Error proto_read_stream_open(
|
||||
const uint8_t *payload, uint32_t length,
|
||||
struct Proto_Stream_Open *out)
|
||||
{
|
||||
if (length < 12) { return APP_INVALID_ERROR_MSG(0, "STREAM_OPEN payload too short"); }
|
||||
out->request_id = get_u16(payload, 0);
|
||||
out->stream_id = get_u16(payload, 4);
|
||||
out->format = get_u16(payload, 6);
|
||||
out->pixel_format = get_u16(payload, 8);
|
||||
out->origin = get_u16(payload, 10);
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
struct App_Error proto_read_stream_close(
|
||||
const uint8_t *payload, uint32_t length,
|
||||
struct Proto_Stream_Close *out)
|
||||
{
|
||||
if (length < 6) { return APP_INVALID_ERROR_MSG(0, "STREAM_CLOSE payload too short"); }
|
||||
out->request_id = get_u16(payload, 0);
|
||||
out->stream_id = get_u16(payload, 4);
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
struct App_Error proto_read_enum_controls_req(
|
||||
const uint8_t *payload, uint32_t length,
|
||||
struct Proto_Enum_Controls_Req *out)
|
||||
{
|
||||
if (length < 6) { return APP_INVALID_ERROR_MSG(0, "ENUM_CONTROLS request payload too short"); }
|
||||
out->request_id = get_u16(payload, 0);
|
||||
out->device_index = get_u16(payload, 4);
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
struct App_Error proto_read_get_control_req(
|
||||
const uint8_t *payload, uint32_t length,
|
||||
struct Proto_Get_Control_Req *out)
|
||||
{
|
||||
if (length < 10) { return APP_INVALID_ERROR_MSG(0, "GET_CONTROL request payload too short"); }
|
||||
out->request_id = get_u16(payload, 0);
|
||||
out->device_index = get_u16(payload, 4);
|
||||
out->control_id = get_u32(payload, 6);
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
struct App_Error proto_read_set_control_req(
|
||||
const uint8_t *payload, uint32_t length,
|
||||
struct Proto_Set_Control_Req *out)
|
||||
{
|
||||
if (length < 14) { return APP_INVALID_ERROR_MSG(0, "SET_CONTROL request payload too short"); }
|
||||
out->request_id = get_u16(payload, 0);
|
||||
out->device_index = get_u16(payload, 4);
|
||||
out->control_id = get_u32(payload, 6);
|
||||
out->value = get_i32(payload, 10);
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
struct App_Error proto_read_response_header(
|
||||
const uint8_t *payload, uint32_t length,
|
||||
struct Proto_Response_Header *out)
|
||||
{
|
||||
if (length < 4) { return APP_INVALID_ERROR_MSG(0, "CONTROL_RESPONSE payload too short"); }
|
||||
out->request_id = get_u16(payload, 0);
|
||||
out->status = get_u16(payload, 2);
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
struct App_Error proto_read_get_control_response(
|
||||
const uint8_t *payload, uint32_t length,
|
||||
struct Proto_Get_Control_Resp *out)
|
||||
{
|
||||
if (length < 8) { return APP_INVALID_ERROR_MSG(0, "GET_CONTROL response payload too short"); }
|
||||
out->request_id = get_u16(payload, 0);
|
||||
out->status = get_u16(payload, 2);
|
||||
out->value = get_i32(payload, 4);
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
struct App_Error proto_read_enum_devices_response(
|
||||
const uint8_t *payload, uint32_t length,
|
||||
struct Proto_Response_Header *header_out,
|
||||
void (*on_media_device)(
|
||||
const char *path, uint8_t path_len,
|
||||
const char *driver, uint8_t driver_len,
|
||||
const char *model, uint8_t model_len,
|
||||
const char *bus_info, uint8_t bus_info_len,
|
||||
uint8_t video_node_count,
|
||||
void *userdata),
|
||||
void (*on_video_node)(
|
||||
const char *path, uint8_t path_len,
|
||||
const char *entity_name, uint8_t entity_name_len,
|
||||
uint32_t entity_type, uint32_t entity_flags,
|
||||
uint32_t device_caps,
|
||||
uint8_t pad_flags, uint8_t is_capture,
|
||||
void *userdata),
|
||||
void (*on_standalone)(
|
||||
const char *path, uint8_t path_len,
|
||||
const char *name, uint8_t name_len,
|
||||
void *userdata),
|
||||
void *userdata)
|
||||
{
|
||||
struct Cursor c;
|
||||
cur_init(&c, payload, length);
|
||||
|
||||
header_out->request_id = cur_u16(&c);
|
||||
header_out->status = cur_u16(&c);
|
||||
uint16_t media_count = cur_u16(&c);
|
||||
CUR_CHECK(c);
|
||||
|
||||
for (uint16_t i = 0; i < media_count; i++) {
|
||||
uint8_t path_len, driver_len, model_len, bus_info_len;
|
||||
const char *path = cur_str8(&c, &path_len);
|
||||
const char *driver = cur_str8(&c, &driver_len);
|
||||
const char *model = cur_str8(&c, &model_len);
|
||||
const char *bus_info = cur_str8(&c, &bus_info_len);
|
||||
uint8_t vcount = cur_u8(&c);
|
||||
CUR_CHECK(c);
|
||||
|
||||
if (on_media_device) {
|
||||
on_media_device(path, path_len, driver, driver_len,
|
||||
model, model_len, bus_info, bus_info_len,
|
||||
vcount, userdata);
|
||||
}
|
||||
|
||||
for (uint8_t j = 0; j < vcount; j++) {
|
||||
uint8_t vpath_len, ename_len;
|
||||
const char *vpath = cur_str8(&c, &vpath_len);
|
||||
const char *ename = cur_str8(&c, &ename_len);
|
||||
uint32_t etype = cur_u32(&c);
|
||||
uint32_t eflags = cur_u32(&c);
|
||||
uint32_t dcaps = cur_u32(&c);
|
||||
uint8_t pflags = cur_u8(&c);
|
||||
uint8_t iscap = cur_u8(&c);
|
||||
CUR_CHECK(c);
|
||||
|
||||
if (on_video_node) {
|
||||
on_video_node(vpath, vpath_len, ename, ename_len,
|
||||
etype, eflags, dcaps, pflags, iscap, userdata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t standalone_count = cur_u16(&c);
|
||||
CUR_CHECK(c);
|
||||
|
||||
for (uint16_t i = 0; i < standalone_count; i++) {
|
||||
uint8_t path_len, name_len;
|
||||
const char *path = cur_str8(&c, &path_len);
|
||||
const char *name = cur_str8(&c, &name_len);
|
||||
CUR_CHECK(c);
|
||||
if (on_standalone) { on_standalone(path, path_len, name, name_len, userdata); }
|
||||
}
|
||||
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
struct App_Error proto_read_enum_controls_response(
|
||||
const uint8_t *payload, uint32_t length,
|
||||
struct Proto_Response_Header *header_out,
|
||||
void (*on_control)(
|
||||
uint32_t id, uint8_t type, uint32_t flags,
|
||||
const char *name, uint8_t name_len,
|
||||
int32_t min, int32_t max, int32_t step,
|
||||
int32_t default_val, int32_t current_val,
|
||||
uint8_t menu_count, void *userdata),
|
||||
void (*on_menu_item)(
|
||||
uint32_t index,
|
||||
const char *name, uint8_t name_len,
|
||||
int64_t int_value,
|
||||
void *userdata),
|
||||
void *userdata)
|
||||
{
|
||||
struct Cursor c;
|
||||
cur_init(&c, payload, length);
|
||||
|
||||
header_out->request_id = cur_u16(&c);
|
||||
header_out->status = cur_u16(&c);
|
||||
uint16_t count = cur_u16(&c);
|
||||
CUR_CHECK(c);
|
||||
|
||||
for (uint16_t i = 0; i < count; i++) {
|
||||
uint32_t id = cur_u32(&c);
|
||||
uint8_t type = cur_u8 (&c);
|
||||
uint32_t flags = cur_u32(&c);
|
||||
uint8_t name_len;
|
||||
const char *name = cur_str8(&c, &name_len);
|
||||
int32_t min = cur_i32(&c);
|
||||
int32_t max = cur_i32(&c);
|
||||
int32_t step = cur_i32(&c);
|
||||
int32_t def = cur_i32(&c);
|
||||
int32_t cur = cur_i32(&c);
|
||||
uint8_t menu_count = cur_u8(&c);
|
||||
CUR_CHECK(c);
|
||||
|
||||
if (on_control) {
|
||||
on_control(id, type, flags, name, name_len,
|
||||
min, max, step, def, cur, menu_count, userdata);
|
||||
}
|
||||
|
||||
for (uint8_t j = 0; j < menu_count; j++) {
|
||||
uint32_t midx = cur_u32(&c);
|
||||
uint8_t mname_len;
|
||||
const char *mname = cur_str8(&c, &mname_len);
|
||||
int64_t mval = cur_i64(&c);
|
||||
CUR_CHECK(c);
|
||||
if (on_menu_item) {
|
||||
on_menu_item(midx, mname, mname_len, mval, userdata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
struct App_Error proto_read_enum_monitors_response(
|
||||
const uint8_t *payload, uint32_t length,
|
||||
struct Proto_Response_Header *header_out,
|
||||
void (*on_monitor)(
|
||||
int32_t x, int32_t y, uint32_t width, uint32_t height,
|
||||
const char *name, uint8_t name_len,
|
||||
void *userdata),
|
||||
void *userdata)
|
||||
{
|
||||
struct Cursor c;
|
||||
cur_init(&c, payload, length);
|
||||
|
||||
header_out->request_id = cur_u16(&c);
|
||||
header_out->status = cur_u16(&c);
|
||||
uint16_t count = cur_u16(&c);
|
||||
CUR_CHECK(c);
|
||||
|
||||
for (uint16_t i = 0; i < count; i++) {
|
||||
int32_t x = cur_i32(&c);
|
||||
int32_t y = cur_i32(&c);
|
||||
uint32_t width = cur_u32(&c);
|
||||
uint32_t height = cur_u32(&c);
|
||||
uint8_t name_len;
|
||||
const char *name = cur_str8(&c, &name_len);
|
||||
CUR_CHECK(c);
|
||||
if (on_monitor) {
|
||||
on_monitor(x, y, width, height, name, name_len, userdata);
|
||||
}
|
||||
}
|
||||
|
||||
return APP_OK;
|
||||
}
|
||||
@@ -7,11 +7,13 @@ MODULE_BUILD = $(BUILD)/serial
|
||||
|
||||
all: $(MODULE_BUILD)/serial.o
|
||||
|
||||
$(MODULE_BUILD)/serial.o: serial.c $(ROOT)/include/serial.h | $(MODULE_BUILD)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
$(MODULE_BUILD)/serial.o: serial.c | $(MODULE_BUILD)
|
||||
$(CC) $(CFLAGS) $(DEPFLAGS) -c -o $@ $<
|
||||
|
||||
$(MODULE_BUILD):
|
||||
mkdir -p $@
|
||||
|
||||
clean:
|
||||
rm -f $(MODULE_BUILD)/serial.o
|
||||
rm -f $(MODULE_BUILD)/serial.o $(MODULE_BUILD)/serial.d
|
||||
|
||||
-include $(MODULE_BUILD)/serial.d
|
||||
|
||||
@@ -7,11 +7,13 @@ MODULE_BUILD = $(BUILD)/transport
|
||||
|
||||
all: $(MODULE_BUILD)/transport.o
|
||||
|
||||
$(MODULE_BUILD)/transport.o: transport.c $(ROOT)/include/transport.h | $(MODULE_BUILD)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
$(MODULE_BUILD)/transport.o: transport.c | $(MODULE_BUILD)
|
||||
$(CC) $(CFLAGS) $(DEPFLAGS) -c -o $@ $<
|
||||
|
||||
$(MODULE_BUILD):
|
||||
mkdir -p $@
|
||||
|
||||
clean:
|
||||
rm -f $(MODULE_BUILD)/transport.o
|
||||
rm -f $(MODULE_BUILD)/transport.o $(MODULE_BUILD)/transport.d
|
||||
|
||||
-include $(MODULE_BUILD)/transport.d
|
||||
|
||||
@@ -7,11 +7,13 @@ MODULE_BUILD = $(BUILD)/v4l2_ctrl
|
||||
|
||||
all: $(MODULE_BUILD)/v4l2_ctrl.o
|
||||
|
||||
$(MODULE_BUILD)/v4l2_ctrl.o: v4l2_ctrl.c $(ROOT)/include/v4l2_ctrl.h $(ROOT)/include/error.h | $(MODULE_BUILD)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
$(MODULE_BUILD)/v4l2_ctrl.o: v4l2_ctrl.c | $(MODULE_BUILD)
|
||||
$(CC) $(CFLAGS) $(DEPFLAGS) -c -o $@ $<
|
||||
|
||||
$(MODULE_BUILD):
|
||||
mkdir -p $@
|
||||
|
||||
clean:
|
||||
rm -f $(MODULE_BUILD)/v4l2_ctrl.o
|
||||
rm -f $(MODULE_BUILD)/v4l2_ctrl.o $(MODULE_BUILD)/v4l2_ctrl.d
|
||||
|
||||
-include $(MODULE_BUILD)/v4l2_ctrl.d
|
||||
|
||||
42
src/node/Makefile
Normal file
42
src/node/Makefile
Normal file
@@ -0,0 +1,42 @@
|
||||
ROOT := $(abspath ../..)
|
||||
include $(ROOT)/common.mk
|
||||
|
||||
NODE_BUILD = $(BUILD)/node
|
||||
MAIN_OBJ = $(NODE_BUILD)/main.o
|
||||
COMMON_OBJ = $(BUILD)/common/error.o
|
||||
MEDIA_OBJ = $(BUILD)/media_ctrl/media_ctrl.o
|
||||
V4L2_OBJ = $(BUILD)/v4l2_ctrl/v4l2_ctrl.o
|
||||
SERIAL_OBJ = $(BUILD)/serial/serial.o
|
||||
TRANSPORT_OBJ = $(BUILD)/transport/transport.o
|
||||
DISCOVERY_OBJ = $(BUILD)/discovery/discovery.o
|
||||
CONFIG_OBJ = $(BUILD)/config/config.o
|
||||
PROTOCOL_OBJ = $(BUILD)/protocol/protocol.o
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(NODE_BUILD)/video-node
|
||||
|
||||
$(NODE_BUILD)/video-node: $(MAIN_OBJ) \
|
||||
$(COMMON_OBJ) $(MEDIA_OBJ) $(V4L2_OBJ) $(SERIAL_OBJ) \
|
||||
$(TRANSPORT_OBJ) $(DISCOVERY_OBJ) $(CONFIG_OBJ) $(PROTOCOL_OBJ)
|
||||
$(CC) $(CFLAGS) -o $@ $^ -lpthread
|
||||
|
||||
$(MAIN_OBJ): main.c | $(NODE_BUILD)
|
||||
$(CC) $(CFLAGS) $(DEPFLAGS) -c -o $@ $<
|
||||
|
||||
$(COMMON_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/common
|
||||
$(MEDIA_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/media_ctrl
|
||||
$(V4L2_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/v4l2_ctrl
|
||||
$(SERIAL_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/serial
|
||||
$(TRANSPORT_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/transport
|
||||
$(DISCOVERY_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/discovery
|
||||
$(CONFIG_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/config
|
||||
$(PROTOCOL_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/protocol
|
||||
|
||||
$(NODE_BUILD):
|
||||
mkdir -p $@
|
||||
|
||||
clean:
|
||||
rm -f $(NODE_BUILD)/video-node $(MAIN_OBJ) $(NODE_BUILD)/main.d
|
||||
|
||||
-include $(NODE_BUILD)/main.d
|
||||
617
src/node/main.c
Normal file
617
src/node/main.c
Normal file
@@ -0,0 +1,617 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/sysmacros.h>
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "discovery.h"
|
||||
#include "transport.h"
|
||||
#include "protocol.h"
|
||||
#include "media_ctrl.h"
|
||||
#include "v4l2_ctrl.h"
|
||||
#include "error.h"
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Device enumeration
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/* Entity type flag for a V4L2 I/O interface entity */
|
||||
#define MEDIA_ENT_F_IO_V4L 0x10001u
|
||||
|
||||
#define MAX_VIDEO_NODES 32
|
||||
#define MAX_MEDIA_DEVICES 8
|
||||
#define MAX_VNODES_PER_MD 8
|
||||
#define MAX_CONTROLS 256
|
||||
|
||||
/* 5 ("/dev/") + NAME_MAX (255) + 1 (NUL) = 261; round up */
|
||||
#define DEV_PATH_MAX 264
|
||||
|
||||
struct VNode {
|
||||
char path[DEV_PATH_MAX];
|
||||
uint32_t dev_major;
|
||||
uint32_t dev_minor;
|
||||
uint32_t device_caps; /* V4L2_CAP_* from VIDIOC_QUERYCAP cap.device_caps */
|
||||
int claimed;
|
||||
char card[32]; /* VIDIOC_QUERYCAP card name, empty if unavailable */
|
||||
};
|
||||
|
||||
struct MNode {
|
||||
char path[DEV_PATH_MAX];
|
||||
char entity_name[32];
|
||||
uint32_t entity_type;
|
||||
uint32_t entity_flags;
|
||||
uint32_t device_caps;
|
||||
uint8_t is_capture;
|
||||
int vnode_index; /* index into VNode array */
|
||||
};
|
||||
|
||||
struct MediaDev {
|
||||
char path[DEV_PATH_MAX];
|
||||
char driver[16];
|
||||
char model[32];
|
||||
char bus_info[32];
|
||||
struct MNode vnodes[MAX_VNODES_PER_MD];
|
||||
int vnode_count;
|
||||
};
|
||||
|
||||
struct Device_List {
|
||||
struct VNode vnodes[MAX_VIDEO_NODES];
|
||||
int vnode_count;
|
||||
struct MediaDev media[MAX_MEDIA_DEVICES];
|
||||
int media_count;
|
||||
};
|
||||
|
||||
static int scan_video_nodes(struct Device_List *dl) {
|
||||
DIR *d = opendir("/dev");
|
||||
if (!d) { return -1; }
|
||||
|
||||
struct dirent *ent;
|
||||
while ((ent = readdir(d)) != NULL && dl->vnode_count < MAX_VIDEO_NODES) {
|
||||
if (strncmp(ent->d_name, "video", 5) != 0) { continue; }
|
||||
const char *suffix = ent->d_name + 5;
|
||||
int numeric = (*suffix != '\0');
|
||||
for (const char *p = suffix; *p; p++) {
|
||||
if (*p < '0' || *p > '9') { numeric = 0; break; }
|
||||
}
|
||||
if (!numeric) { continue; }
|
||||
|
||||
struct VNode *v = &dl->vnodes[dl->vnode_count];
|
||||
snprintf(v->path, sizeof(v->path), "/dev/%s", ent->d_name);
|
||||
|
||||
struct stat st;
|
||||
if (stat(v->path, &st) != 0 || !S_ISCHR(st.st_mode)) { continue; }
|
||||
v->dev_major = (uint32_t)major(st.st_rdev);
|
||||
v->dev_minor = (uint32_t)minor(st.st_rdev);
|
||||
v->claimed = 0;
|
||||
v->card[0] = '\0';
|
||||
|
||||
/* Try to get card name */
|
||||
int fd = open(v->path, O_RDONLY | O_NONBLOCK);
|
||||
if (fd >= 0) {
|
||||
struct v4l2_capability cap;
|
||||
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == 0) {
|
||||
strncpy(v->card, (const char *)cap.card, sizeof(v->card) - 1);
|
||||
v->card[sizeof(v->card) - 1] = '\0';
|
||||
/* use per-node caps when available, fall back to physical caps */
|
||||
v->device_caps = (cap.capabilities & V4L2_CAP_DEVICE_CAPS)
|
||||
? cap.device_caps : cap.capabilities;
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
|
||||
dl->vnode_count++;
|
||||
}
|
||||
closedir(d);
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct Entity_Cb_State {
|
||||
struct Device_List *dl;
|
||||
struct MediaDev *md;
|
||||
};
|
||||
|
||||
static void entity_callback(const struct Media_Entity *entity, void *userdata) {
|
||||
struct Entity_Cb_State *state = userdata;
|
||||
struct Device_List *dl = state->dl;
|
||||
struct MediaDev *md = state->md;
|
||||
|
||||
if (entity->dev_major == 0) { return; }
|
||||
if (md->vnode_count >= MAX_VNODES_PER_MD) { return; }
|
||||
|
||||
/* Find matching video node by device number */
|
||||
for (int i = 0; i < dl->vnode_count; i++) {
|
||||
if (dl->vnodes[i].dev_major != entity->dev_major) { continue; }
|
||||
if (dl->vnodes[i].dev_minor != entity->dev_minor) { continue; }
|
||||
|
||||
dl->vnodes[i].claimed = 1;
|
||||
|
||||
struct MNode *mn = &md->vnodes[md->vnode_count++];
|
||||
strncpy(mn->path, dl->vnodes[i].path, sizeof(mn->path) - 1);
|
||||
mn->path[sizeof(mn->path) - 1] = '\0';
|
||||
strncpy(mn->entity_name, entity->name, sizeof(mn->entity_name) - 1);
|
||||
mn->entity_name[sizeof(mn->entity_name) - 1] = '\0';
|
||||
mn->entity_type = entity->type;
|
||||
mn->entity_flags = entity->flags;
|
||||
mn->device_caps = dl->vnodes[i].device_caps;
|
||||
mn->is_capture = (mn->device_caps & V4L2_CAP_VIDEO_CAPTURE) ? 1 : 0;
|
||||
mn->vnode_index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void scan_media_devices(struct Device_List *dl) {
|
||||
DIR *d = opendir("/dev");
|
||||
if (!d) { return; }
|
||||
|
||||
struct dirent *ent;
|
||||
while ((ent = readdir(d)) != NULL && dl->media_count < MAX_MEDIA_DEVICES) {
|
||||
if (strncmp(ent->d_name, "media", 5) != 0) { continue; }
|
||||
const char *suffix = ent->d_name + 5;
|
||||
int numeric = (*suffix != '\0');
|
||||
for (const char *p = suffix; *p; p++) {
|
||||
if (*p < '0' || *p > '9') { numeric = 0; break; }
|
||||
}
|
||||
if (!numeric) { continue; }
|
||||
|
||||
struct MediaDev *md = &dl->media[dl->media_count];
|
||||
snprintf(md->path, sizeof(md->path), "/dev/%s", ent->d_name);
|
||||
md->vnode_count = 0;
|
||||
|
||||
struct Media_Ctrl *ctrl;
|
||||
if (!APP_IS_OK(media_ctrl_open(md->path, &ctrl))) { continue; }
|
||||
|
||||
struct Media_Device_Info info;
|
||||
if (APP_IS_OK(media_ctrl_get_info(ctrl, &info))) {
|
||||
strncpy(md->driver, info.driver, sizeof(md->driver) - 1);
|
||||
strncpy(md->model, info.model, sizeof(md->model) - 1);
|
||||
strncpy(md->bus_info, info.bus_info, sizeof(md->bus_info) - 1);
|
||||
md->driver[sizeof(md->driver) - 1] = '\0';
|
||||
md->model[sizeof(md->model) - 1] = '\0';
|
||||
md->bus_info[sizeof(md->bus_info) - 1] = '\0';
|
||||
}
|
||||
|
||||
struct Entity_Cb_State state = { .dl = dl, .md = md };
|
||||
media_ctrl_enum_entities(ctrl, entity_callback, &state);
|
||||
media_ctrl_close(ctrl);
|
||||
|
||||
dl->media_count++;
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
|
||||
static void build_device_list(struct Device_List *dl) {
|
||||
memset(dl, 0, sizeof(*dl));
|
||||
scan_video_nodes(dl);
|
||||
scan_media_devices(dl);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Control enumeration helpers
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
struct Ctrl_Build {
|
||||
struct Proto_Control_Info items[MAX_CONTROLS];
|
||||
char names[MAX_CONTROLS][32];
|
||||
int count;
|
||||
};
|
||||
|
||||
static void ctrl_enum_cb(
|
||||
const struct V4l2_Ctrl_Desc *desc,
|
||||
uint32_t menu_count, const struct V4l2_Menu_Item *menu_items,
|
||||
void *userdata)
|
||||
{
|
||||
(void)menu_count; (void)menu_items;
|
||||
struct Ctrl_Build *b = userdata;
|
||||
if (b->count >= MAX_CONTROLS) { return; }
|
||||
|
||||
int i = b->count++;
|
||||
strncpy(b->names[i], desc->name, 31);
|
||||
b->names[i][31] = '\0';
|
||||
|
||||
b->items[i].id = desc->id;
|
||||
b->items[i].type = (uint8_t)desc->type;
|
||||
b->items[i].flags = desc->flags;
|
||||
b->items[i].name = b->names[i];
|
||||
b->items[i].min = desc->min;
|
||||
b->items[i].max = desc->max;
|
||||
b->items[i].step = desc->step;
|
||||
b->items[i].default_val = desc->default_value;
|
||||
b->items[i].current_val = desc->current_value;
|
||||
b->items[i].menu_count = 0;
|
||||
b->items[i].menu_items = NULL;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Node state
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
struct Node {
|
||||
struct Config *config;
|
||||
struct Transport_Server *server;
|
||||
struct Discovery *discovery;
|
||||
struct Device_List devices;
|
||||
};
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Request handlers
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
static void handle_enum_devices(struct Node *node,
|
||||
struct Transport_Conn *conn, uint16_t request_id)
|
||||
{
|
||||
/* Build Proto_Media_Device_Info array */
|
||||
struct Proto_Video_Node_Info vnodes[MAX_MEDIA_DEVICES * MAX_VNODES_PER_MD];
|
||||
struct Proto_Media_Device_Info mdevs[MAX_MEDIA_DEVICES];
|
||||
int vnode_offset = 0;
|
||||
|
||||
for (int i = 0; i < node->devices.media_count; i++) {
|
||||
struct MediaDev *md = &node->devices.media[i];
|
||||
mdevs[i].path = md->path;
|
||||
mdevs[i].driver = md->driver;
|
||||
mdevs[i].model = md->model;
|
||||
mdevs[i].bus_info = md->bus_info;
|
||||
mdevs[i].video_node_count = (uint8_t)md->vnode_count;
|
||||
mdevs[i].video_nodes = &vnodes[vnode_offset];
|
||||
|
||||
for (int j = 0; j < md->vnode_count; j++) {
|
||||
struct MNode *mn = &md->vnodes[j];
|
||||
vnodes[vnode_offset + j].path = mn->path;
|
||||
vnodes[vnode_offset + j].entity_name = mn->entity_name;
|
||||
vnodes[vnode_offset + j].entity_type = mn->entity_type;
|
||||
vnodes[vnode_offset + j].entity_flags= mn->entity_flags;
|
||||
vnodes[vnode_offset + j].device_caps = mn->device_caps;
|
||||
vnodes[vnode_offset + j].pad_flags = 0;
|
||||
vnodes[vnode_offset + j].is_capture = mn->is_capture;
|
||||
}
|
||||
vnode_offset += md->vnode_count;
|
||||
}
|
||||
|
||||
/* Build standalone list */
|
||||
struct Proto_Standalone_Device_Info standalone[MAX_VIDEO_NODES];
|
||||
int standalone_count = 0;
|
||||
for (int i = 0; i < node->devices.vnode_count; i++) {
|
||||
if (node->devices.vnodes[i].claimed) { continue; }
|
||||
standalone[standalone_count].path = node->devices.vnodes[i].path;
|
||||
standalone[standalone_count].name = node->devices.vnodes[i].card;
|
||||
standalone_count++;
|
||||
}
|
||||
|
||||
struct App_Error e = proto_write_enum_devices_response(conn,
|
||||
request_id, PROTO_STATUS_OK,
|
||||
mdevs, (uint16_t)node->devices.media_count,
|
||||
standalone, (uint16_t)standalone_count);
|
||||
if (!APP_IS_OK(e)) { app_error_print(&e); }
|
||||
}
|
||||
|
||||
static void handle_enum_controls(struct Node *node,
|
||||
struct Transport_Conn *conn,
|
||||
const uint8_t *payload, uint32_t length)
|
||||
{
|
||||
struct Proto_Enum_Controls_Req req;
|
||||
struct App_Error e = proto_read_enum_controls_req(payload, length, &req);
|
||||
if (!APP_IS_OK(e)) {
|
||||
proto_write_control_response(conn, 0,
|
||||
PROTO_STATUS_INVALID_PARAMS, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Resolve device_index to a path across media-owned + standalone nodes */
|
||||
const char *path = NULL;
|
||||
int idx = (int)req.device_index;
|
||||
for (int i = 0; i < node->devices.media_count && path == NULL; i++) {
|
||||
struct MediaDev *md = &node->devices.media[i];
|
||||
if (idx < md->vnode_count) {
|
||||
path = md->vnodes[idx].path;
|
||||
} else {
|
||||
idx -= md->vnode_count;
|
||||
}
|
||||
}
|
||||
if (path == NULL) {
|
||||
/* Check standalone */
|
||||
for (int i = 0; i < node->devices.vnode_count; i++) {
|
||||
if (node->devices.vnodes[i].claimed) { continue; }
|
||||
if (idx == 0) { path = node->devices.vnodes[i].path; break; }
|
||||
idx--;
|
||||
}
|
||||
}
|
||||
|
||||
if (path == NULL) {
|
||||
proto_write_control_response(conn, req.request_id,
|
||||
PROTO_STATUS_NOT_FOUND, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
struct V4l2_Ctrl_Handle *handle;
|
||||
e = v4l2_ctrl_open(path, &handle);
|
||||
if (!APP_IS_OK(e)) {
|
||||
proto_write_control_response(conn, req.request_id,
|
||||
PROTO_STATUS_ERROR, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
struct Ctrl_Build build = { .count = 0 };
|
||||
v4l2_ctrl_enumerate(handle, ctrl_enum_cb, &build);
|
||||
v4l2_ctrl_close(handle);
|
||||
|
||||
e = proto_write_enum_controls_response(conn,
|
||||
req.request_id, PROTO_STATUS_OK,
|
||||
build.items, (uint16_t)build.count);
|
||||
if (!APP_IS_OK(e)) { app_error_print(&e); }
|
||||
}
|
||||
|
||||
static void handle_get_control(struct Node *node,
|
||||
struct Transport_Conn *conn,
|
||||
const uint8_t *payload, uint32_t length)
|
||||
{
|
||||
struct Proto_Get_Control_Req req;
|
||||
struct App_Error e = proto_read_get_control_req(payload, length, &req);
|
||||
if (!APP_IS_OK(e)) {
|
||||
proto_write_control_response(conn, 0,
|
||||
PROTO_STATUS_INVALID_PARAMS, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Same device resolution as enum_controls */
|
||||
const char *path = NULL;
|
||||
int idx = (int)req.device_index;
|
||||
for (int i = 0; i < node->devices.media_count && path == NULL; i++) {
|
||||
struct MediaDev *md = &node->devices.media[i];
|
||||
if (idx < md->vnode_count) {
|
||||
path = md->vnodes[idx].path;
|
||||
} else {
|
||||
idx -= md->vnode_count;
|
||||
}
|
||||
}
|
||||
if (path == NULL) {
|
||||
for (int i = 0; i < node->devices.vnode_count; i++) {
|
||||
if (node->devices.vnodes[i].claimed) { continue; }
|
||||
if (idx == 0) { path = node->devices.vnodes[i].path; break; }
|
||||
idx--;
|
||||
}
|
||||
}
|
||||
|
||||
if (path == NULL) {
|
||||
proto_write_control_response(conn, req.request_id,
|
||||
PROTO_STATUS_NOT_FOUND, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
struct V4l2_Ctrl_Handle *handle;
|
||||
e = v4l2_ctrl_open(path, &handle);
|
||||
if (!APP_IS_OK(e)) {
|
||||
proto_write_control_response(conn, req.request_id,
|
||||
PROTO_STATUS_ERROR, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t value;
|
||||
e = v4l2_ctrl_get(handle, req.control_id, &value);
|
||||
v4l2_ctrl_close(handle);
|
||||
|
||||
if (!APP_IS_OK(e)) {
|
||||
proto_write_control_response(conn, req.request_id,
|
||||
PROTO_STATUS_ERROR, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
e = proto_write_get_control_response(conn,
|
||||
req.request_id, PROTO_STATUS_OK, value);
|
||||
if (!APP_IS_OK(e)) { app_error_print(&e); }
|
||||
}
|
||||
|
||||
static void handle_set_control(struct Node *node,
|
||||
struct Transport_Conn *conn,
|
||||
const uint8_t *payload, uint32_t length)
|
||||
{
|
||||
struct Proto_Set_Control_Req req;
|
||||
struct App_Error e = proto_read_set_control_req(payload, length, &req);
|
||||
if (!APP_IS_OK(e)) {
|
||||
proto_write_control_response(conn, 0,
|
||||
PROTO_STATUS_INVALID_PARAMS, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const char *path = NULL;
|
||||
int idx = (int)req.device_index;
|
||||
for (int i = 0; i < node->devices.media_count && path == NULL; i++) {
|
||||
struct MediaDev *md = &node->devices.media[i];
|
||||
if (idx < md->vnode_count) {
|
||||
path = md->vnodes[idx].path;
|
||||
} else {
|
||||
idx -= md->vnode_count;
|
||||
}
|
||||
}
|
||||
if (path == NULL) {
|
||||
for (int i = 0; i < node->devices.vnode_count; i++) {
|
||||
if (node->devices.vnodes[i].claimed) { continue; }
|
||||
if (idx == 0) { path = node->devices.vnodes[i].path; break; }
|
||||
idx--;
|
||||
}
|
||||
}
|
||||
|
||||
if (path == NULL) {
|
||||
proto_write_control_response(conn, req.request_id,
|
||||
PROTO_STATUS_NOT_FOUND, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
struct V4l2_Ctrl_Handle *handle;
|
||||
e = v4l2_ctrl_open(path, &handle);
|
||||
if (!APP_IS_OK(e)) {
|
||||
proto_write_control_response(conn, req.request_id,
|
||||
PROTO_STATUS_ERROR, NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
e = v4l2_ctrl_set(handle, req.control_id, req.value);
|
||||
v4l2_ctrl_close(handle);
|
||||
|
||||
uint16_t status = APP_IS_OK(e) ? PROTO_STATUS_OK : PROTO_STATUS_ERROR;
|
||||
proto_write_control_response(conn, req.request_id, status, NULL, 0);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Transport callbacks
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
static void on_frame(struct Transport_Conn *conn,
|
||||
struct Transport_Frame *frame, void *userdata)
|
||||
{
|
||||
struct Node *node = userdata;
|
||||
|
||||
if (frame->message_type == PROTO_MSG_CONTROL_REQUEST) {
|
||||
struct Proto_Request_Header hdr;
|
||||
struct App_Error e = proto_read_request_header(
|
||||
frame->payload, frame->payload_length, &hdr);
|
||||
if (!APP_IS_OK(e)) {
|
||||
free(frame->payload);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (hdr.command) {
|
||||
case PROTO_CMD_ENUM_DEVICES:
|
||||
handle_enum_devices(node, conn, hdr.request_id);
|
||||
break;
|
||||
case PROTO_CMD_ENUM_CONTROLS:
|
||||
handle_enum_controls(node, conn,
|
||||
frame->payload, frame->payload_length);
|
||||
break;
|
||||
case PROTO_CMD_GET_CONTROL:
|
||||
handle_get_control(node, conn,
|
||||
frame->payload, frame->payload_length);
|
||||
break;
|
||||
case PROTO_CMD_SET_CONTROL:
|
||||
handle_set_control(node, conn,
|
||||
frame->payload, frame->payload_length);
|
||||
break;
|
||||
default:
|
||||
proto_write_control_response(conn, hdr.request_id,
|
||||
PROTO_STATUS_UNKNOWN_CMD, NULL, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free(frame->payload);
|
||||
}
|
||||
|
||||
static void on_connect(struct Transport_Conn *conn, void *userdata) {
|
||||
(void)conn; (void)userdata;
|
||||
printf("peer connected\n");
|
||||
}
|
||||
|
||||
static void on_disconnect(struct Transport_Conn *conn, void *userdata) {
|
||||
(void)conn; (void)userdata;
|
||||
printf("peer disconnected\n");
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Config schema
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
static const struct Config_Flag_Def function_flag_defs[] = {
|
||||
{ "source", DISCOVERY_FLAG_SOURCE },
|
||||
{ "relay", DISCOVERY_FLAG_RELAY },
|
||||
{ "sink", DISCOVERY_FLAG_SINK },
|
||||
{ "controller", DISCOVERY_FLAG_CONTROLLER },
|
||||
{ NULL, 0 }
|
||||
};
|
||||
|
||||
static const struct Config_Def schema[] = {
|
||||
{ "node", "name", CONFIG_STRING, "unnamed:0", NULL },
|
||||
{ "node", "site_id", CONFIG_UINT16, "0", NULL },
|
||||
{ "node", "tcp_port", CONFIG_UINT16, "8000", NULL },
|
||||
{ "node", "function", CONFIG_FLAGS, "source", function_flag_defs },
|
||||
{ "discovery", "interval_ms", CONFIG_UINT32, "5000", NULL },
|
||||
{ "discovery", "timeout_intervals", CONFIG_UINT32, "3", NULL },
|
||||
{ "transport", "max_connections", CONFIG_UINT32, "16", NULL },
|
||||
{ "transport", "max_payload", CONFIG_UINT32, "16777216", NULL },
|
||||
{ NULL }
|
||||
};
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* Entry point
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
static void usage(void) {
|
||||
fprintf(stderr,
|
||||
"usage: video-node <config-file>\n"
|
||||
" video-node --defaults\n");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 2) { usage(); return 1; }
|
||||
|
||||
struct Node node;
|
||||
memset(&node, 0, sizeof(node));
|
||||
|
||||
/* Load config */
|
||||
struct App_Error e;
|
||||
if (strcmp(argv[1], "--defaults") == 0) {
|
||||
e = config_defaults(&node.config, schema);
|
||||
} else {
|
||||
e = config_load(&node.config, argv[1], schema);
|
||||
}
|
||||
if (!APP_IS_OK(e)) { app_error_print(&e); return 1; }
|
||||
|
||||
uint16_t tcp_port = config_get_u16(node.config, "node", "tcp_port");
|
||||
uint16_t site_id = config_get_u16(node.config, "node", "site_id");
|
||||
uint32_t func = config_get_flags(node.config, "node", "function");
|
||||
const char *name = config_get_str(node.config, "node", "name");
|
||||
uint32_t interval = config_get_u32(node.config, "discovery", "interval_ms");
|
||||
uint32_t timeout_i = config_get_u32(node.config, "discovery", "timeout_intervals");
|
||||
uint32_t max_conn = config_get_u32(node.config, "transport", "max_connections");
|
||||
uint32_t max_pay = config_get_u32(node.config, "transport", "max_payload");
|
||||
|
||||
printf("node: %s port=%u site=%u\n", name, tcp_port, site_id);
|
||||
|
||||
/* Enumerate devices */
|
||||
printf("scanning devices...\n");
|
||||
build_device_list(&node.devices);
|
||||
printf("found %d media device(s), %d video node(s)\n",
|
||||
node.devices.media_count, node.devices.vnode_count);
|
||||
|
||||
/* Start transport server */
|
||||
struct Transport_Server_Config srv_cfg = {
|
||||
.port = tcp_port,
|
||||
.max_connections = (int)max_conn,
|
||||
.max_payload = max_pay,
|
||||
.on_frame = on_frame,
|
||||
.on_connect = on_connect,
|
||||
.on_disconnect = on_disconnect,
|
||||
.userdata = &node,
|
||||
};
|
||||
|
||||
e = transport_server_create(&node.server, &srv_cfg);
|
||||
if (!APP_IS_OK(e)) { app_error_print(&e); return 1; }
|
||||
|
||||
e = transport_server_start(node.server);
|
||||
if (!APP_IS_OK(e)) { app_error_print(&e); return 1; }
|
||||
|
||||
/* Start discovery */
|
||||
struct Discovery_Config disc_cfg = {
|
||||
.site_id = site_id,
|
||||
.tcp_port = tcp_port,
|
||||
.function_flags = (uint16_t)func,
|
||||
.name = name,
|
||||
.interval_ms = interval,
|
||||
.timeout_intervals= timeout_i,
|
||||
.on_peer_found = NULL,
|
||||
.on_peer_lost = NULL,
|
||||
};
|
||||
|
||||
e = discovery_create(&node.discovery, &disc_cfg);
|
||||
if (!APP_IS_OK(e)) { app_error_print(&e); return 1; }
|
||||
|
||||
e = discovery_start(node.discovery);
|
||||
if (!APP_IS_OK(e)) { app_error_print(&e); return 1; }
|
||||
|
||||
printf("ready\n");
|
||||
pause();
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user