diff --git a/.gitignore b/.gitignore index f10aabe..ac3a1e3 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ fp-info-cache hardware/.history/ +# CMake build directories +host/*/build/ # Component library noise (import guides, non-KiCAD EDA tool files) hardware/Components/**/ImportGuides.html hardware/Components/**/ImportGuide.html diff --git a/host/linkctl-daemon/CMakeLists.txt b/host/linkctl-daemon/CMakeLists.txt index 45c2a30..1aac07a 100644 --- a/host/linkctl-daemon/CMakeLists.txt +++ b/host/linkctl-daemon/CMakeLists.txt @@ -24,6 +24,7 @@ add_executable(linkctl-daemon main.c camera.c server.c + control.c mdns.c ) diff --git a/host/linkctl-daemon/control.c b/host/linkctl-daemon/control.c new file mode 100644 index 0000000..bd89508 --- /dev/null +++ b/host/linkctl-daemon/control.c @@ -0,0 +1,182 @@ +#include "control.h" +#include "camera.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Serializes camera access between control thread and WebSocket thread */ +static pthread_mutex_t camera_mutex = PTHREAD_MUTEX_INITIALIZER; + +#define LINE_BUF_SIZE 256 + +static int listen_fd = -1; +static int client_fd = -1; +static char line_buf[LINE_BUF_SIZE]; +static int line_pos = 0; + +static void set_nonblock(int fd) { + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); +} + +static void send_response(const char *resp) { + if (client_fd < 0) return; + write(client_fd, resp, strlen(resp)); +} + +static void dispatch_command(const char *line) { + int rc = 0; + + if (strncmp(line, "pan_tilt ", 9) == 0) { + int pd, ps, td, ts; + if (sscanf(line + 9, "%d %d %d %d", &pd, &ps, &td, &ts) == 4) { + pthread_mutex_lock(&camera_mutex); + if (ps == 0 && ts == 0) { + rc = camera_stop(); + } else { + rc = camera_pan_tilt((uint8_t)pd, (uint8_t)ps, + (uint8_t)td, (uint8_t)ts); + } + pthread_mutex_unlock(&camera_mutex); + } else { + send_response("error:bad pan_tilt args\n"); + return; + } + } else if (strcmp(line, "stop") == 0) { + pthread_mutex_lock(&camera_mutex); + rc = camera_stop(); + pthread_mutex_unlock(&camera_mutex); + } else if (strcmp(line, "center") == 0) { + pthread_mutex_lock(&camera_mutex); + rc = camera_center(); + pthread_mutex_unlock(&camera_mutex); + } else if (strncmp(line, "zoom ", 5) == 0) { + int val; + if (sscanf(line + 5, "%d", &val) == 1) { + if (val < ZOOM_MIN || val > ZOOM_MAX) { + send_response("error:zoom out of range (100-400)\n"); + return; + } + pthread_mutex_lock(&camera_mutex); + rc = camera_zoom((uint16_t)val); + pthread_mutex_unlock(&camera_mutex); + } else { + send_response("error:bad zoom value\n"); + return; + } + } else if (strcmp(line, "status") == 0) { + pthread_mutex_lock(&camera_mutex); + camera_status_t st = camera_get_status(); + pthread_mutex_unlock(&camera_mutex); + char resp[64]; + snprintf(resp, sizeof(resp), "status:%d %u\n", st.connected, st.zoom); + send_response(resp); + return; + } else { + send_response("error:unknown command\n"); + return; + } + + send_response(rc == 0 ? "ok\n" : "error:camera not connected\n"); +} + +int control_init(uint16_t port) { + listen_fd = socket(AF_INET, SOCK_STREAM, 0); + if (listen_fd < 0) { + perror("[control] socket"); + return -1; + } + + int opt = 1; + setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + struct sockaddr_in addr = { + .sin_family = AF_INET, + .sin_port = htons(port), + .sin_addr.s_addr = htonl(INADDR_LOOPBACK), + }; + + if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + perror("[control] bind"); + close(listen_fd); + listen_fd = -1; + return -1; + } + + if (listen(listen_fd, 1) < 0) { + perror("[control] listen"); + close(listen_fd); + listen_fd = -1; + return -1; + } + + set_nonblock(listen_fd); + + printf("[control] TCP control socket listening on port %u\n", port); + return 0; +} + +void control_service(void) { + if (listen_fd < 0) return; + + /* Accept new connection (non-blocking) */ + if (client_fd < 0) { + client_fd = accept(listen_fd, NULL, NULL); + if (client_fd >= 0) { + set_nonblock(client_fd); + line_pos = 0; + } + } + + if (client_fd < 0) return; + + /* Read available data into line buffer */ + char buf[128]; + ssize_t n = read(client_fd, buf, sizeof(buf)); + + if (n == 0) { + /* Client disconnected */ + close(client_fd); + client_fd = -1; + line_pos = 0; + return; + } + + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) return; + /* Read error */ + close(client_fd); + client_fd = -1; + line_pos = 0; + return; + } + + for (ssize_t i = 0; i < n; i++) { + if (buf[i] == '\n' || buf[i] == '\r') { + if (line_pos > 0) { + line_buf[line_pos] = '\0'; + dispatch_command(line_buf); + line_pos = 0; + } + } else if (line_pos < LINE_BUF_SIZE - 1) { + line_buf[line_pos++] = buf[i]; + } + } +} + +void control_destroy(void) { + if (client_fd >= 0) { + close(client_fd); + client_fd = -1; + } + if (listen_fd >= 0) { + close(listen_fd); + listen_fd = -1; + } +} diff --git a/host/linkctl-daemon/control.h b/host/linkctl-daemon/control.h new file mode 100644 index 0000000..43d2218 --- /dev/null +++ b/host/linkctl-daemon/control.h @@ -0,0 +1,18 @@ +#ifndef CONTROL_H +#define CONTROL_H + +#include + +// Default TCP control port (separate from WebSocket on 9000) +#define DEFAULT_CONTROL_PORT 9001 + +// Initialize the TCP control socket. Returns 0 on success. +int control_init(uint16_t port); + +// Non-blocking: accept new connections, read commands, dispatch. +void control_service(void); + +// Shut down the control socket. +void control_destroy(void); + +#endif diff --git a/host/linkctl-daemon/main.c b/host/linkctl-daemon/main.c index b26afe0..c1b729c 100644 --- a/host/linkctl-daemon/main.c +++ b/host/linkctl-daemon/main.c @@ -1,5 +1,6 @@ #include "camera.h" #include "server.h" +#include "control.h" #include "mdns.h" #include @@ -8,11 +9,13 @@ #include #include #include +#include #include +#include #define CAMERA_POLL_INTERVAL_SEC 5 -static volatile bool running = true; +static atomic_bool running = true; static bool use_mdns = true; static uint16_t port = DEFAULT_PORT; static bool test_mode = false; @@ -103,6 +106,15 @@ static void poll_camera(void) { } } +static void *control_thread(void *arg) { + (void)arg; + while (running) { + control_service(); + usleep(5000); // 5ms = 200Hz + } + return NULL; +} + int main(int argc, char **argv) { if (parse_args(argc, argv) != 0) { return 1; @@ -143,9 +155,18 @@ int main(int argc, char **argv) { } } + // Start TCP control socket for CLI (runs in its own thread) + if (control_init(DEFAULT_CONTROL_PORT) != 0) { + fprintf(stderr, "[main] Control socket failed (continuing without)\n"); + } + + // Control socket thread — decoupled from lws event loop + pthread_t ctrl_thread; + pthread_create(&ctrl_thread, NULL, control_thread, NULL); + printf("[main] linkctl-daemon running on port %u\n", port); - // Main event loop + // Main event loop (WebSocket + camera only) while (running) { server_service(50); // 50ms poll poll_camera(); @@ -153,6 +174,8 @@ int main(int argc, char **argv) { printf("\n[main] Shutting down...\n"); + pthread_join(ctrl_thread, NULL); + control_destroy(); if (use_mdns) mdns_unregister(); camera_stop(); server_destroy(); diff --git a/host/linkctl-daemon/server.c b/host/linkctl-daemon/server.c index 07be81f..719185d 100644 --- a/host/linkctl-daemon/server.c +++ b/host/linkctl-daemon/server.c @@ -220,6 +220,8 @@ static void handle_command(struct lws *wsi, const char *json, size_t len) { rc = camera_zoom((uint16_t)val->valueint); } else { send_json(wsi, "{\"type\":\"error\",\"message\":\"missing zoom value\"}"); + cJSON_Delete(root); + return; } } else if (strcmp(cmd_str, "stop") == 0) { rc = camera_stop(); @@ -237,12 +239,18 @@ static void handle_command(struct lws *wsi, const char *json, size_t len) { free(resp_json); } cJSON_Delete(resp); + cJSON_Delete(root); + return; } else { send_json(wsi, "{\"type\":\"error\",\"message\":\"unknown command\"}"); + cJSON_Delete(root); + return; } if (rc != 0) { send_json(wsi, "{\"type\":\"error\",\"message\":\"camera not connected\"}"); + } else { + send_json(wsi, "{\"type\":\"ack\"}"); } cJSON_Delete(root); diff --git a/host/linkctl/CMakeLists.txt b/host/linkctl/CMakeLists.txt new file mode 100644 index 0000000..8c6077b --- /dev/null +++ b/host/linkctl/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.14) +project(linkctl C) + +set(CMAKE_C_STANDARD 11) + +add_executable(linkctl linkctl.c) + +install(TARGETS linkctl DESTINATION bin) diff --git a/host/linkctl/README.md b/host/linkctl/README.md new file mode 100644 index 0000000..32a2c18 --- /dev/null +++ b/host/linkctl/README.md @@ -0,0 +1,71 @@ +# linkctl + +Command-line client for controlling the Insta360 Link camera via the `linkctl-daemon`. + +## Prerequisites + +- `linkctl-daemon` must be running (see `host/linkctl-daemon/`) +- No external dependencies — pure POSIX C + +## Build + +```bash +cd host/linkctl +mkdir -p build && cd build +cmake .. +make +``` + +## Install (optional) + +Install to `/usr/local/bin` so `linkctl` is available system-wide: + +```bash +sudo cmake --install . --prefix /usr/local +``` + +Or run directly from the build directory: `./build/linkctl `. + +## Usage + +``` +linkctl [args] +``` + +### Commands + +| Command | Description | +|---------|-------------| +| `linkctl help` | Show usage information | +| `linkctl status` | Show camera connection state and current zoom | +| `linkctl center` | Center the gimbal and reset zoom to 1.0x | +| `linkctl zoom <1.0-4.0>` | Set zoom level (e.g., `linkctl zoom 2.5`) | +| `linkctl zoom` | Show the current zoom level | +| `linkctl stop` | Stop all gimbal movement | +| `linkctl jog` | Enter interactive pan/tilt mode | + +### Jog Mode + +`linkctl jog` enters an interactive mode for real-time camera control using keyboard input. + +**Controls:** + +| Key | Action | +|-----|--------| +| `W` | Tilt up | +| `S` | Tilt down | +| `A` | Pan left | +| `D` | Pan right | +| `+` / `=` | Increase speed | +| `-` | Decrease speed | +| Space | Stop movement | +| `C` | Center gimbal | +| `Q` / ESC | Exit jog mode | + +Movement continues while a key is held (via terminal key repeat) and automatically stops ~400ms after release. + +Default speeds: pan=15, tilt=10. Speed range: 3–30 (pan), 3–20 (tilt). + +## How It Works + +`linkctl` connects to the daemon's TCP control socket at `localhost:9001` using a simple line-based text protocol. For one-shot commands it connects, sends, prints the result, and exits. Jog mode maintains the connection and uses `poll()` on both stdin and the socket for responsive keyboard-driven camera control. diff --git a/host/linkctl/linkctl.c b/host/linkctl/linkctl.c new file mode 100644 index 0000000..b3f0c6a --- /dev/null +++ b/host/linkctl/linkctl.c @@ -0,0 +1,391 @@ +/* + * linkctl — CLI client for linkctl-daemon + * + * Connects to the daemon's TCP control socket and sends camera commands. + * Usage: linkctl [args] + * + * Zero external dependencies — pure POSIX. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* --- Constants --- */ + +#define DEFAULT_HOST "127.0.0.1" +#define DEFAULT_PORT 9001 + +#define DIR_POSITIVE 1 +#define DIR_NEGATIVE 255 +#define DIR_STOP 0 + +#define PAN_MAX_SPEED 30 +#define TILT_MAX_SPEED 20 +#define ZOOM_MIN 100 +#define ZOOM_MAX 400 + +#define JOG_DEFAULT_PAN_SPEED 15 +#define JOG_DEFAULT_TILT_SPEED 10 +#define JOG_SPEED_STEP 3 +#define JOG_MIN_SPEED 3 +#define JOG_STOP_TIMEOUT_US 400000 /* 400ms — matches typical macOS key repeat delay */ + +/* --- State --- */ + +static volatile bool done = false; +static struct termios orig_termios; +static bool termios_saved = false; +static int orig_stdin_flags = -1; + +/* --- Terminal helpers --- */ + +static void restore_terminal(void) { + if (termios_saved) + tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios); + if (orig_stdin_flags >= 0) + fcntl(STDIN_FILENO, F_SETFL, orig_stdin_flags); +} + +static void enter_raw_mode(void) { + tcgetattr(STDIN_FILENO, &orig_termios); + termios_saved = true; + + orig_stdin_flags = fcntl(STDIN_FILENO, F_GETFL); + fcntl(STDIN_FILENO, F_SETFL, orig_stdin_flags | O_NONBLOCK); + + struct termios raw = orig_termios; + raw.c_lflag &= ~(ICANON | ECHO); + raw.c_cc[VMIN] = 0; + raw.c_cc[VTIME] = 0; + tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); +} + +static void handle_signal(int sig) { + (void)sig; + done = true; +} + +/* --- TCP helpers --- */ + +static int tcp_connect(const char *host, uint16_t port) { + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) return -1; + + struct sockaddr_in addr = { + .sin_family = AF_INET, + .sin_port = htons(port), + }; + inet_pton(AF_INET, host, &addr.sin_addr); + + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + close(fd); + return -1; + } + return fd; +} + +/* Send a command and read the response line. Returns response in buf. */ +static int send_cmd(int sock, const char *cmd, char *resp, size_t resp_size) { + if (write(sock, cmd, strlen(cmd)) < 0) return -1; + + ssize_t n = read(sock, resp, resp_size - 1); + if (n <= 0) return -1; + resp[n] = '\0'; + + /* Strip trailing newline */ + while (n > 0 && (resp[n-1] == '\n' || resp[n-1] == '\r')) + resp[--n] = '\0'; + + return 0; +} + +/* --- Usage --- */ + +static void print_usage(void) { + printf("Usage: linkctl [args]\n\n"); + printf("Commands:\n"); + printf(" help Show this help\n"); + printf(" status Show camera connection and zoom level\n"); + printf(" center Center gimbal and reset zoom to 1.0x\n"); + printf(" zoom <1.0-4.0> Set zoom level\n"); + printf(" zoom Show current zoom level\n"); + printf(" stop Stop all gimbal movement\n"); + printf(" jog Interactive WASD pan/tilt mode\n"); + printf("\nThe linkctl-daemon must be running for commands to work.\n"); +} + +/* --- One-shot commands --- */ + +static int cmd_status(int sock) { + char resp[128]; + if (send_cmd(sock, "status\n", resp, sizeof(resp)) < 0) return 1; + + int connected; + unsigned zoom; + if (sscanf(resp, "status:%d %u", &connected, &zoom) == 2) { + printf("Camera: %s\n", connected ? "connected" : "not connected"); + if (connected && zoom > 0) + printf("Zoom: %.1fx (%u)\n", zoom / 100.0, zoom); + } else { + fprintf(stderr, "%s\n", resp); + return 1; + } + return 0; +} + +static int cmd_center(int sock) { + char resp[128]; + /* Center + stop + zoom reset */ + if (send_cmd(sock, "center\n", resp, sizeof(resp)) < 0) return 1; + if (strncmp(resp, "ok", 2) != 0) { fprintf(stderr, "%s\n", resp); return 1; } + + if (send_cmd(sock, "stop\n", resp, sizeof(resp)) < 0) return 1; + if (strncmp(resp, "ok", 2) != 0) { fprintf(stderr, "%s\n", resp); return 1; } + + char cmd[32]; + snprintf(cmd, sizeof(cmd), "zoom %d\n", ZOOM_MIN); + if (send_cmd(sock, cmd, resp, sizeof(resp)) < 0) return 1; + if (strncmp(resp, "ok", 2) != 0) { fprintf(stderr, "%s\n", resp); return 1; } + + printf("Centered, zoom reset to 1.0x\n"); + return 0; +} + +static int cmd_zoom_set(int sock, float value) { + uint16_t raw = (uint16_t)(value * 100.0f + 0.5f); + if (raw < ZOOM_MIN) raw = ZOOM_MIN; + if (raw > ZOOM_MAX) raw = ZOOM_MAX; + + char cmd[32], resp[128]; + snprintf(cmd, sizeof(cmd), "zoom %u\n", raw); + if (send_cmd(sock, cmd, resp, sizeof(resp)) < 0) return 1; + if (strncmp(resp, "ok", 2) != 0) { fprintf(stderr, "%s\n", resp); return 1; } + + printf("Zoom set to %.1fx\n", value); + return 0; +} + +static int cmd_zoom_query(int sock) { + char resp[128]; + if (send_cmd(sock, "status\n", resp, sizeof(resp)) < 0) return 1; + + int connected; + unsigned zoom; + if (sscanf(resp, "status:%d %u", &connected, &zoom) == 2 && connected) { + printf("Zoom: %.1fx (%u)\n", zoom / 100.0, zoom); + } else { + printf("Camera not connected\n"); + return 1; + } + return 0; +} + +static int cmd_stop(int sock) { + char resp[128]; + if (send_cmd(sock, "stop\n", resp, sizeof(resp)) < 0) return 1; + if (strncmp(resp, "ok", 2) != 0) { fprintf(stderr, "%s\n", resp); return 1; } + printf("Stopped\n"); + return 0; +} + +/* --- Jog mode --- */ + +static int cmd_jog(int sock) { + if (!isatty(STDIN_FILENO)) { + fprintf(stderr, "Jog mode requires a terminal\n"); + return 1; + } + + printf("\n=== Jog Mode ===\n"); + printf(" WASD Pan/tilt +/- Speed\n"); + printf(" Space Stop c Center\n"); + printf(" q/ESC Exit\n\n"); + + uint8_t pan_speed = JOG_DEFAULT_PAN_SPEED; + uint8_t tilt_speed = JOG_DEFAULT_TILT_SPEED; + printf("Speed: pan=%u tilt=%u\n", pan_speed, tilt_speed); + + enter_raw_mode(); + + bool moving = false; + struct timeval last_input; + gettimeofday(&last_input, NULL); + + struct pollfd fds[2] = { + { .fd = STDIN_FILENO, .events = POLLIN }, + { .fd = sock, .events = POLLIN }, + }; + + while (!done) { + poll(fds, 2, 50); + + /* Read keyboard input */ + if (fds[0].revents & POLLIN) { + char c; + if (read(STDIN_FILENO, &c, 1) == 1) { + gettimeofday(&last_input, NULL); + c = tolower((unsigned char)c); + + char cmd[128]; + cmd[0] = '\0'; + + switch (c) { + case 'w': + snprintf(cmd, sizeof(cmd), "pan_tilt %d %d %d %d\n", + DIR_STOP, 0, DIR_NEGATIVE, tilt_speed); + moving = true; + break; + case 's': + snprintf(cmd, sizeof(cmd), "pan_tilt %d %d %d %d\n", + DIR_STOP, 0, DIR_POSITIVE, tilt_speed); + moving = true; + break; + case 'a': + snprintf(cmd, sizeof(cmd), "pan_tilt %d %d %d %d\n", + DIR_POSITIVE, pan_speed, DIR_STOP, 0); + moving = true; + break; + case 'd': + snprintf(cmd, sizeof(cmd), "pan_tilt %d %d %d %d\n", + DIR_NEGATIVE, pan_speed, DIR_STOP, 0); + moving = true; + break; + case '+': case '=': + if (pan_speed + JOG_SPEED_STEP <= PAN_MAX_SPEED) + pan_speed += JOG_SPEED_STEP; + else + pan_speed = PAN_MAX_SPEED; + if (tilt_speed + JOG_SPEED_STEP <= TILT_MAX_SPEED) + tilt_speed += JOG_SPEED_STEP; + else + tilt_speed = TILT_MAX_SPEED; + printf("\rSpeed: pan=%u tilt=%u \r", pan_speed, tilt_speed); + fflush(stdout); + break; + case '-': + if (pan_speed > JOG_MIN_SPEED + JOG_SPEED_STEP) + pan_speed -= JOG_SPEED_STEP; + else + pan_speed = JOG_MIN_SPEED; + if (tilt_speed > JOG_MIN_SPEED + JOG_SPEED_STEP) + tilt_speed -= JOG_SPEED_STEP; + else + tilt_speed = JOG_MIN_SPEED; + printf("\rSpeed: pan=%u tilt=%u \r", pan_speed, tilt_speed); + fflush(stdout); + break; + case ' ': + snprintf(cmd, sizeof(cmd), "stop\n"); + moving = false; + break; + case 'c': + snprintf(cmd, sizeof(cmd), "center\n"); + moving = false; + break; + case 'q': + case 0x1B: /* ESC */ + case 0x03: /* Ctrl-C */ + write(sock, "stop\n", 5); + done = true; + break; + } + + if (cmd[0]) + write(sock, cmd, strlen(cmd)); + } + } + + /* Drain incoming responses (don't need them during jog) */ + if (fds[1].revents & POLLIN) { + char discard[256]; + read(sock, discard, sizeof(discard)); + } + + /* Auto-stop after no input */ + if (moving) { + struct timeval now; + gettimeofday(&now, NULL); + long elapsed_us = (now.tv_sec - last_input.tv_sec) * 1000000L + + (now.tv_usec - last_input.tv_usec); + if (elapsed_us > JOG_STOP_TIMEOUT_US) { + write(sock, "stop\n", 5); + moving = false; + } + } + } + + restore_terminal(); + printf("\nExited jog mode.\n"); + return 0; +} + +/* --- Main --- */ + +int main(int argc, char **argv) { + if (argc < 2) { + print_usage(); + return 1; + } + + const char *subcmd = argv[1]; + + if (strcmp(subcmd, "help") == 0 || strcmp(subcmd, "--help") == 0 || + strcmp(subcmd, "-h") == 0) { + print_usage(); + return 0; + } + + signal(SIGINT, handle_signal); + signal(SIGTERM, handle_signal); + + /* Connect to daemon */ + int sock = tcp_connect(DEFAULT_HOST, DEFAULT_PORT); + if (sock < 0) { + fprintf(stderr, "Error: daemon not active (is linkctl-daemon running?)\n"); + return 1; + } + + int rc = 0; + + if (strcmp(subcmd, "status") == 0) { + rc = cmd_status(sock); + } else if (strcmp(subcmd, "center") == 0) { + rc = cmd_center(sock); + } else if (strcmp(subcmd, "zoom") == 0) { + if (argc >= 3) { + char *end; + float val = strtof(argv[2], &end); + if (*end != '\0' || val < 1.0f || val > 4.0f) { + fprintf(stderr, "Error: zoom must be a number between 1.0 and 4.0\n"); + rc = 1; + } else { + rc = cmd_zoom_set(sock, val); + } + } else { + rc = cmd_zoom_query(sock); + } + } else if (strcmp(subcmd, "stop") == 0) { + rc = cmd_stop(sock); + } else if (strcmp(subcmd, "jog") == 0) { + rc = cmd_jog(sock); + } else { + fprintf(stderr, "Unknown command: %s\nRun 'linkctl help' for usage.\n", subcmd); + rc = 1; + } + + close(sock); + return rc; +}