-
Notifications
You must be signed in to change notification settings - Fork 0
Adding CLI client for MacOS #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2572240
a3a2fa7
ce3ae1c
1e01b45
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ add_executable(linkctl-daemon | |
| main.c | ||
| camera.c | ||
| server.c | ||
| control.c | ||
| mdns.c | ||
| ) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| #include "control.h" | ||
| #include "camera.h" | ||
|
|
||
| #include <stdio.h> | ||
| #include <stdlib.h> | ||
| #include <string.h> | ||
| #include <unistd.h> | ||
| #include <fcntl.h> | ||
| #include <errno.h> | ||
| #include <pthread.h> | ||
| #include <sys/socket.h> | ||
| #include <netinet/in.h> | ||
|
|
||
| /* 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), | ||
| }; | ||
|
jfwoods marked this conversation as resolved.
|
||
|
|
||
| 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; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| #ifndef CONTROL_H | ||
| #define CONTROL_H | ||
|
|
||
| #include <stdint.h> | ||
|
|
||
| // 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| #include "camera.h" | ||
| #include "server.h" | ||
| #include "control.h" | ||
| #include "mdns.h" | ||
|
|
||
| #include <stdio.h> | ||
|
|
@@ -8,11 +9,13 @@ | |
| #include <string.h> | ||
| #include <unistd.h> | ||
| #include <stdbool.h> | ||
| #include <stdatomic.h> | ||
| #include <time.h> | ||
| #include <pthread.h> | ||
|
|
||
| #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; | ||
|
jfwoods marked this conversation as resolved.
|
||
| } | ||
|
|
||
| int main(int argc, char **argv) { | ||
| if (parse_args(argc, argv) != 0) { | ||
| return 1; | ||
|
|
@@ -143,16 +155,27 @@ 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); | ||
|
|
||
|
jfwoods marked this conversation as resolved.
Comment on lines
155
to
+166
|
||
| 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(); | ||
|
jfwoods marked this conversation as resolved.
|
||
| } | ||
|
|
||
| printf("\n[main] Shutting down...\n"); | ||
|
|
||
| pthread_join(ctrl_thread, NULL); | ||
| control_destroy(); | ||
| if (use_mdns) mdns_unregister(); | ||
| camera_stop(); | ||
| server_destroy(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <command>`. | ||
|
|
||
| ## Usage | ||
|
|
||
| ``` | ||
| linkctl <command> [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. |
Uh oh!
There was an error while loading. Please reload this page.