controller_cli: readline, discovery integration, peers/connect commands
- readline replaces fgets — line editing and command history
- Discovery runs at startup (always); discovered peers print inline as they appear
- --host is now optional; without it, starts in discovery-only mode
- New REPL commands:
peers list discovered nodes with index
connect connect to first discovered peer
connect <idx> connect to peer by index
connect <host:port> connect directly
- connect switching closes the old connection before opening the new one
- Commands that require a connection print "not connected" when conn is NULL
- Makefile: add $(DISCOVERY_OBJ) and -lreadline to controller_cli link
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -109,8 +109,8 @@ $(CLI_BUILD)/stream_recv_cli: $(CLI_BUILD)/stream_recv_cli.o $(COMMON_OBJ) $(SER
|
|||||||
$(CLI_BUILD)/reconciler_cli: $(CLI_BUILD)/reconciler_cli.o $(RECONCILER_OBJ)
|
$(CLI_BUILD)/reconciler_cli: $(CLI_BUILD)/reconciler_cli.o $(RECONCILER_OBJ)
|
||||||
$(CC) $(CFLAGS) -o $@ $^
|
$(CC) $(CFLAGS) -o $@ $^
|
||||||
|
|
||||||
$(CLI_BUILD)/controller_cli: $(CLI_BUILD)/controller_cli.o $(COMMON_OBJ) $(SERIAL_OBJ) $(TRANSPORT_OBJ) $(PROTOCOL_OBJ)
|
$(CLI_BUILD)/controller_cli: $(CLI_BUILD)/controller_cli.o $(COMMON_OBJ) $(SERIAL_OBJ) $(TRANSPORT_OBJ) $(DISCOVERY_OBJ) $(PROTOCOL_OBJ)
|
||||||
$(CC) $(CFLAGS) -o $@ $^ -lpthread
|
$(CC) $(CFLAGS) -o $@ $^ -lpthread -lreadline
|
||||||
|
|
||||||
$(CLI_BUILD):
|
$(CLI_BUILD):
|
||||||
mkdir -p $@
|
mkdir -p $@
|
||||||
|
|||||||
@@ -3,11 +3,82 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include <semaphore.h>
|
#include <semaphore.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <readline/readline.h>
|
||||||
|
#include <readline/history.h>
|
||||||
|
|
||||||
#include "transport.h"
|
#include "transport.h"
|
||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
|
#include "discovery.h"
|
||||||
#include "error.h"
|
#include "error.h"
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------
|
||||||
|
* Discovery peer table
|
||||||
|
* ------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
#define MAX_PEERS 16
|
||||||
|
|
||||||
|
struct Peer_Entry {
|
||||||
|
char host[64];
|
||||||
|
uint16_t port;
|
||||||
|
char name[DISCOVERY_MAX_NAME_LEN + 1];
|
||||||
|
};
|
||||||
|
|
||||||
|
static struct Peer_Entry peer_table[MAX_PEERS];
|
||||||
|
static int peer_count = 0;
|
||||||
|
static pthread_mutex_t peer_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
|
||||||
|
static void on_peer_found(const struct Discovery_Peer *peer, void *ud)
|
||||||
|
{
|
||||||
|
(void)ud;
|
||||||
|
pthread_mutex_lock(&peer_mutex);
|
||||||
|
if (peer_count < MAX_PEERS) {
|
||||||
|
struct in_addr a;
|
||||||
|
a.s_addr = peer->addr;
|
||||||
|
inet_ntop(AF_INET, &a, peer_table[peer_count].host,
|
||||||
|
sizeof(peer_table[0].host));
|
||||||
|
peer_table[peer_count].port = peer->tcp_port;
|
||||||
|
strncpy(peer_table[peer_count].name, peer->name, DISCOVERY_MAX_NAME_LEN);
|
||||||
|
peer_table[peer_count].name[DISCOVERY_MAX_NAME_LEN] = '\0';
|
||||||
|
peer_count++;
|
||||||
|
/* Print inline — readline will redraw the prompt */
|
||||||
|
printf("\n[discovered %d] %s %s:%u\n",
|
||||||
|
peer_count - 1,
|
||||||
|
peer_table[peer_count - 1].name,
|
||||||
|
peer_table[peer_count - 1].host,
|
||||||
|
peer->tcp_port);
|
||||||
|
rl_on_new_line();
|
||||||
|
rl_redisplay();
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&peer_mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_peer_lost(const struct Discovery_Peer *peer, void *ud)
|
||||||
|
{
|
||||||
|
(void)ud;
|
||||||
|
struct in_addr a;
|
||||||
|
a.s_addr = peer->addr;
|
||||||
|
char host[64];
|
||||||
|
inet_ntop(AF_INET, &a, host, sizeof(host));
|
||||||
|
|
||||||
|
pthread_mutex_lock(&peer_mutex);
|
||||||
|
for (int i = 0; i < peer_count; i++) {
|
||||||
|
if (strcmp(peer_table[i].host, host) == 0 &&
|
||||||
|
peer_table[i].port == peer->tcp_port) {
|
||||||
|
printf("\n[lost] %s %s:%u\n",
|
||||||
|
peer_table[i].name, host, peer->tcp_port);
|
||||||
|
rl_on_new_line();
|
||||||
|
rl_redisplay();
|
||||||
|
memmove(&peer_table[i], &peer_table[i + 1],
|
||||||
|
(size_t)(peer_count - i - 1) * sizeof(peer_table[0]));
|
||||||
|
peer_count--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&peer_mutex);
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------
|
/* -------------------------------------------------------------------------
|
||||||
* Shared state between REPL and transport read thread
|
* Shared state between REPL and transport read thread
|
||||||
* ------------------------------------------------------------------------- */
|
* ------------------------------------------------------------------------- */
|
||||||
@@ -194,7 +265,9 @@ static void on_frame(struct Transport_Conn *conn,
|
|||||||
static void on_disconnect(struct Transport_Conn *conn, void *userdata)
|
static void on_disconnect(struct Transport_Conn *conn, void *userdata)
|
||||||
{
|
{
|
||||||
(void)conn; (void)userdata;
|
(void)conn; (void)userdata;
|
||||||
printf("disconnected from node\n");
|
printf("\ndisconnected from node\n");
|
||||||
|
rl_on_new_line();
|
||||||
|
rl_redisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------
|
/* -------------------------------------------------------------------------
|
||||||
@@ -341,6 +414,8 @@ static void cmd_stop_display(struct Transport_Conn *conn,
|
|||||||
static void cmd_help(void)
|
static void cmd_help(void)
|
||||||
{
|
{
|
||||||
printf("commands:\n"
|
printf("commands:\n"
|
||||||
|
" peers list discovered nodes\n"
|
||||||
|
" connect [idx|host:port] connect to peer (no arg = first discovered)\n"
|
||||||
" enum-devices\n"
|
" enum-devices\n"
|
||||||
" enum-controls <device_index>\n"
|
" enum-controls <device_index>\n"
|
||||||
" get-control <device_index> <control_id_hex>\n"
|
" get-control <device_index> <control_id_hex>\n"
|
||||||
@@ -361,40 +436,76 @@ static void cmd_help(void)
|
|||||||
static void usage(void)
|
static void usage(void)
|
||||||
{
|
{
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"usage: controller_cli --host HOST [--port PORT]\n"
|
"usage: controller_cli [--host HOST] [--port PORT]\n"
|
||||||
"\n"
|
"\n"
|
||||||
" Interactive controller for a video node.\n"
|
" Interactive controller for a video node.\n"
|
||||||
" --host HOST node hostname or IP (required)\n"
|
" --host HOST connect directly on startup\n"
|
||||||
" --port PORT node TCP port (default 8000)\n");
|
" --port PORT TCP port (default 8000; used with --host)\n"
|
||||||
|
"\n"
|
||||||
|
" Without --host: starts discovery and waits for nodes.\n"
|
||||||
|
" Use 'connect' in the REPL to connect to a discovered node.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Attempt to connect/reconnect; prints result. Returns new conn or NULL. */
|
||||||
|
static struct Transport_Conn *do_connect(struct Ctrl_State *cs,
|
||||||
|
const char *host, uint16_t port,
|
||||||
|
struct Transport_Conn *old_conn)
|
||||||
|
{
|
||||||
|
if (old_conn) { transport_conn_close(old_conn); }
|
||||||
|
struct Transport_Conn *conn;
|
||||||
|
struct App_Error e = transport_connect(&conn, host, port,
|
||||||
|
TRANSPORT_DEFAULT_MAX_PAYLOAD, on_frame, on_disconnect, cs);
|
||||||
|
if (!APP_IS_OK(e)) {
|
||||||
|
app_error_print(&e);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
printf("connected to %s:%u\n", host, port);
|
||||||
|
return conn;
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char **argv)
|
int main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
const char *host = NULL;
|
const char *init_host = NULL;
|
||||||
uint16_t port = 8000;
|
uint16_t init_port = 8000;
|
||||||
|
|
||||||
for (int i = 1; i < argc; i++) {
|
for (int i = 1; i < argc; i++) {
|
||||||
if (strcmp(argv[i], "--host") == 0 && i + 1 < argc) {
|
if (strcmp(argv[i], "--host") == 0 && i + 1 < argc) {
|
||||||
host = argv[++i];
|
init_host = argv[++i];
|
||||||
} else if (strcmp(argv[i], "--port") == 0 && i + 1 < argc) {
|
} else if (strcmp(argv[i], "--port") == 0 && i + 1 < argc) {
|
||||||
port = (uint16_t)atoi(argv[++i]);
|
init_port = (uint16_t)atoi(argv[++i]);
|
||||||
} else {
|
} else {
|
||||||
usage(); return 1;
|
usage(); return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!host) { usage(); return 1; }
|
|
||||||
|
|
||||||
/* Connect */
|
/* Start discovery (always — useful even when --host given, for 'peers') */
|
||||||
|
struct Discovery *disc = NULL;
|
||||||
|
struct Discovery_Config dcfg = {0};
|
||||||
|
dcfg.site_id = 0;
|
||||||
|
dcfg.tcp_port = 0;
|
||||||
|
dcfg.function_flags = DISCOVERY_FLAG_CONTROLLER;
|
||||||
|
dcfg.name = "controller_cli";
|
||||||
|
dcfg.on_peer_found = on_peer_found;
|
||||||
|
dcfg.on_peer_lost = on_peer_lost;
|
||||||
|
if (!APP_IS_OK(discovery_create(&disc, &dcfg)) ||
|
||||||
|
!APP_IS_OK(discovery_start(disc))) {
|
||||||
|
fprintf(stderr, "warning: discovery failed to start\n");
|
||||||
|
disc = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
struct Ctrl_State cs;
|
struct Ctrl_State cs;
|
||||||
memset(&cs, 0, sizeof(cs));
|
memset(&cs, 0, sizeof(cs));
|
||||||
sem_init(&cs.sem, 0, 0);
|
sem_init(&cs.sem, 0, 0);
|
||||||
|
|
||||||
struct Transport_Conn *conn;
|
struct Transport_Conn *conn = NULL;
|
||||||
struct App_Error e = transport_connect(&conn, host, port,
|
|
||||||
TRANSPORT_DEFAULT_MAX_PAYLOAD, on_frame, on_disconnect, &cs);
|
if (init_host) {
|
||||||
if (!APP_IS_OK(e)) { app_error_print(&e); return 1; }
|
conn = do_connect(&cs, init_host, init_port, NULL);
|
||||||
|
if (!conn) { return 1; }
|
||||||
|
} else {
|
||||||
|
printf("listening for nodes — type 'peers' to list, 'connect' to connect\n");
|
||||||
|
}
|
||||||
|
|
||||||
printf("connected to %s:%u\n\n", host, port);
|
|
||||||
cmd_help();
|
cmd_help();
|
||||||
printf("\n");
|
printf("\n");
|
||||||
|
|
||||||
@@ -403,22 +514,17 @@ int main(int argc, char **argv)
|
|||||||
char line[512];
|
char line[512];
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
printf("> ");
|
char *rl_line = readline(conn ? "> " : "(no node) > ");
|
||||||
fflush(stdout);
|
if (!rl_line) { break; }
|
||||||
|
if (*rl_line) { add_history(rl_line); }
|
||||||
if (fgets(line, sizeof(line), stdin) == NULL) { break; }
|
strncpy(line, rl_line, sizeof(line) - 1);
|
||||||
|
line[sizeof(line) - 1] = '\0';
|
||||||
/* Strip trailing newline */
|
free(rl_line);
|
||||||
size_t len = strlen(line);
|
|
||||||
while (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r')) {
|
|
||||||
line[--len] = '\0';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tokenise (up to 12 tokens) */
|
/* Tokenise (up to 12 tokens) */
|
||||||
char *tokens[12];
|
char *tokens[12];
|
||||||
int ntok = 0;
|
int ntok = 0;
|
||||||
char *p = line;
|
char *p = line;
|
||||||
|
|
||||||
while (*p && ntok < 12) {
|
while (*p && ntok < 12) {
|
||||||
while (*p == ' ' || *p == '\t') { p++; }
|
while (*p == ' ' || *p == '\t') { p++; }
|
||||||
if (!*p) { break; }
|
if (!*p) { break; }
|
||||||
@@ -432,8 +538,71 @@ int main(int argc, char **argv)
|
|||||||
|
|
||||||
if (strcmp(cmd, "quit") == 0 || strcmp(cmd, "exit") == 0) {
|
if (strcmp(cmd, "quit") == 0 || strcmp(cmd, "exit") == 0) {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
} else if (strcmp(cmd, "help") == 0) {
|
} else if (strcmp(cmd, "help") == 0) {
|
||||||
cmd_help();
|
cmd_help();
|
||||||
|
|
||||||
|
} else if (strcmp(cmd, "peers") == 0) {
|
||||||
|
pthread_mutex_lock(&peer_mutex);
|
||||||
|
if (peer_count == 0) {
|
||||||
|
printf("no peers discovered yet\n");
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < peer_count; i++) {
|
||||||
|
printf(" [%d] %s %s:%u\n", i,
|
||||||
|
peer_table[i].name,
|
||||||
|
peer_table[i].host,
|
||||||
|
peer_table[i].port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&peer_mutex);
|
||||||
|
|
||||||
|
} else if (strcmp(cmd, "connect") == 0) {
|
||||||
|
char host[64];
|
||||||
|
uint16_t port = 8000;
|
||||||
|
|
||||||
|
if (ntok < 2) {
|
||||||
|
/* No argument — connect to first discovered peer */
|
||||||
|
pthread_mutex_lock(&peer_mutex);
|
||||||
|
int ok = peer_count > 0;
|
||||||
|
if (ok) {
|
||||||
|
strncpy(host, peer_table[0].host, sizeof(host) - 1);
|
||||||
|
host[sizeof(host) - 1] = '\0';
|
||||||
|
port = peer_table[0].port;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&peer_mutex);
|
||||||
|
if (!ok) {
|
||||||
|
printf("no peers discovered yet — try 'peers'\n");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if (strchr(tokens[1], ':')) {
|
||||||
|
/* host:port */
|
||||||
|
char *colon = strchr(tokens[1], ':');
|
||||||
|
size_t hlen = (size_t)(colon - tokens[1]);
|
||||||
|
if (hlen >= sizeof(host)) { hlen = sizeof(host) - 1; }
|
||||||
|
memcpy(host, tokens[1], hlen);
|
||||||
|
host[hlen] = '\0';
|
||||||
|
port = (uint16_t)atoi(colon + 1);
|
||||||
|
} else {
|
||||||
|
/* numeric index into peer table */
|
||||||
|
int idx = atoi(tokens[1]);
|
||||||
|
pthread_mutex_lock(&peer_mutex);
|
||||||
|
int ok = idx >= 0 && idx < peer_count;
|
||||||
|
if (ok) {
|
||||||
|
strncpy(host, peer_table[idx].host, sizeof(host) - 1);
|
||||||
|
host[sizeof(host) - 1] = '\0';
|
||||||
|
port = peer_table[idx].port;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&peer_mutex);
|
||||||
|
if (!ok) {
|
||||||
|
printf("index %d out of range — try 'peers'\n", idx);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conn = do_connect(&cs, host, port, conn);
|
||||||
|
|
||||||
|
} else if (!conn) {
|
||||||
|
printf("not connected — use 'connect' to connect to a node\n");
|
||||||
|
|
||||||
} else if (strcmp(cmd, "enum-devices") == 0) {
|
} else if (strcmp(cmd, "enum-devices") == 0) {
|
||||||
cmd_enum_devices(conn, &cs, &req_id);
|
cmd_enum_devices(conn, &cs, &req_id);
|
||||||
} else if (strcmp(cmd, "enum-controls") == 0) {
|
} else if (strcmp(cmd, "enum-controls") == 0) {
|
||||||
@@ -460,7 +629,8 @@ int main(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
transport_conn_close(conn);
|
if (conn) { transport_conn_close(conn); }
|
||||||
|
if (disc) { discovery_destroy(disc); }
|
||||||
sem_destroy(&cs.sem);
|
sem_destroy(&cs.sem);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user