Something went wrong. Try again.
sppoky
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636/* uinjectd.c — Persistent injection + event daemon for glossa * * Runs ON the reMarkable. Listens on a TCP socket; the host connects * once and keeps the connection open for the whole session. This kills * the per-call SSH handshake for injection AND the 500ms idle polling. * * Two directions over one socket (newline-delimited JSON): * * Host -> daemon (commands): * {"cmd":"draw","file":"/tmp/glossa_strokes.json","speed":3} * {"cmd":"erase","file":"/tmp/glossa_thinking_live.json","speed":20} * {"cmd":"ping"} * * Daemon -> host (responses + pushed events): * {"resp":"draw","ok":true,"strokes":40} * {"resp":"erase","ok":true,"points":36} * {"resp":"ping","ok":true} * {"event":"idle","ts":172...} <- pushed, no request * * The daemon watches /tmp/glossa_idle (written by the xovi extension) * and pushes an "idle" event whenever it changes. The host no longer * polls. * * Usage: uinjectd [-v] [-p PORT] (default port 9999) */#include <stdio.h>#include <stdlib.h>#include <string.h>#include <fcntl.h>#include <unistd.h>#include <linux/input.h>#include <sys/stat.h>#include <sys/socket.h>#include <netinet/in.h>#include <netinet/tcp.h>#include <arpa/inet.h>#include <pthread.h>#include <errno.h>#include <signal.h>
#define DEV_PATH "/dev/input/event1"#define TOUCH_PATH "/dev/input/event2"#define IDLE_PATH "/tmp/glossa_idle"#define DEFAULT_PORT 9999#define MAX_CMD 4096
static int g_verbose = 0;static int g_dev_fd = -1;static int g_client_fd = -1;static pthread_mutex_t g_inject_lock = PTHREAD_MUTEX_INITIALIZER;static pthread_mutex_t g_write_lock = PTHREAD_MUTEX_INITIALIZER;
#define vlog(...) do { if (g_verbose) fprintf(stderr, "[uinjectd] " __VA_ARGS__); } while (0)
/* ─── evdev helpers ──────────────────────────────────────────────── */
static void emit_event(int fd, int type, int code, int value) { struct input_event ev; memset(&ev, 0, sizeof(ev)); ev.type = type; ev.code = code; ev.value = value; write(fd, &ev, sizeof(ev));}
static void emit_syn(int fd) { emit_event(fd, EV_SYN, SYN_REPORT, 0);}
static void rm_to_wacom(float rm_x, float rm_y, int *wx, int *wy) { float screen_x = rm_x + 702.0f; float screen_y = rm_y; *wx = (int)((1872.0f - screen_y) * 20966.0f / 1872.0f); *wy = (int)(screen_x * 15725.0f / 1404.0f);}
static int rm_pressure_to_wacom(int rm_pressure) { return rm_pressure * 4095 / 255;}
/* ─── minimal JSON parsing (flat objects only) ───────────────────── */
static const char *json_str(const char *json, const char *key, int *len) { char pattern[64]; snprintf(pattern, sizeof(pattern), "\"%s\"", key); const char *p = strstr(json, pattern); if (!p) return NULL; p += strlen(pattern); while (*p == ' ' || *p == ':') p++; if (*p != '"') return NULL; p++; const char *start = p; while (*p && *p != '"') p++; *len = (int)(p - start); return start;}
static int json_int(const char *json, const char *key, int def) { char pattern[64]; snprintf(pattern, sizeof(pattern), "\"%s\"", key); const char *p = strstr(json, pattern); if (!p) return def; p += strlen(pattern); while (*p == ' ' || *p == ':') p++; return atoi(p);}
/* ─── socket write (thread-safe, newline-framed) ─────────────────── */
static void send_line(const char *msg) { pthread_mutex_lock(&g_write_lock); int fd = g_client_fd; if (fd >= 0) { write(fd, msg, strlen(msg)); write(fd, "\n", 1); } pthread_mutex_unlock(&g_write_lock);}
/* ─── file loading ───────────────────────────────────────────────── */
static int load_file(const char *path, char **out, long *out_size) { FILE *fp = fopen(path, "r"); if (!fp) return -1; fseek(fp, 0, SEEK_END); long sz = ftell(fp); fseek(fp, 0, SEEK_SET); char *buf = malloc(sz + 1); if (!buf) { fclose(fp); return -1; } fread(buf, 1, sz, fp); buf[sz] = '\0'; fclose(fp); *out = buf; *out_size = sz; return 0;}
/* ─── injection ──────────────────────────────────────────────────── */
static int do_draw(int fd, const char *json, int delay_us) { char *p = (char *)json; int stroke_count = 0;
while ((p = strstr(p, "\"points\"")) != NULL) { p = strchr(p, '['); if (!p) break; p++;
/* Peek first point for hover frame */ float hx, hy; int hs, hw, hd, hp; if (sscanf(p, "[%f,%f,%d,%d,%d,%d]", &hx, &hy, &hs, &hw, &hd, &hp) == 6) { int hwx, hwy; rm_to_wacom(hx, hy, &hwx, &hwy); emit_event(fd, EV_KEY, BTN_TOOL_PEN, 1); emit_event(fd, EV_ABS, ABS_X, hwx); emit_event(fd, EV_ABS, ABS_Y, hwy); emit_event(fd, EV_ABS, ABS_DISTANCE, 60); emit_syn(fd); usleep(2000); } else { emit_event(fd, EV_KEY, BTN_TOOL_PEN, 1); emit_event(fd, EV_ABS, ABS_DISTANCE, 60); emit_syn(fd); usleep(2000); } emit_event(fd, EV_KEY, BTN_TOUCH, 1);
int point_count = 0; while (*p && *p != ']') { float x, y; int speed, width, direction, pressure; if (sscanf(p, "[%f,%f,%d,%d,%d,%d]", &x, &y, &speed, &width, &direction, &pressure) == 6) { int wx, wy; rm_to_wacom(x, y, &wx, &wy); int wp = rm_pressure_to_wacom(pressure);
emit_event(fd, EV_ABS, ABS_X, wx); emit_event(fd, EV_ABS, ABS_Y, wy); emit_event(fd, EV_ABS, ABS_PRESSURE, wp); emit_event(fd, EV_ABS, ABS_DISTANCE, 0); emit_syn(fd); point_count++; usleep(delay_us); } p = strchr(p, ']'); if (p) p++; while (*p && (*p == ',' || *p == ' ' || *p == '\n' || *p == '\r')) p++; }
emit_event(fd, EV_ABS, ABS_PRESSURE, 0); emit_event(fd, EV_ABS, ABS_DISTANCE, 86); emit_syn(fd); emit_event(fd, EV_KEY, BTN_TOUCH, 0); emit_syn(fd); usleep(8000); /* Fully leave proximity so the device commits this stroke and won't * connect it to the next one. Max out distance, drop the tool, and * pause long enough for the stroke tracker to finalize before the * next stroke's hover-down begins. */ emit_event(fd, EV_ABS, ABS_DISTANCE, 255); emit_event(fd, EV_KEY, BTN_TOOL_PEN, 0); emit_syn(fd);
stroke_count++; vlog("Stroke %d: %d points\n", stroke_count, point_count); usleep(60000); } return stroke_count;}
static int do_erase(int fd, const char *json, int delay_us) { static const float offsets[][2] = { {0, 0}, {-6, 0}, {6, 0}, }; int num_passes = sizeof(offsets) / sizeof(offsets[0]); int total_emitted = 0;
for (int pass = 0; pass < num_passes; pass++) { float ox = offsets[pass][0]; float oy = offsets[pass][1]; char *p = (char *)json; int pass_emitted = 0;
while ((p = strstr(p, "\"points\"")) != NULL) { p = strchr(p, '['); if (!p) break; p++;
/* Peek first point for hover frame */ float hx, hy; int hs, hw, hd, hp; char *saved_p = p; if (sscanf(p, "[%f,%f,%d,%d,%d,%d]", &hx, &hy, &hs, &hw, &hd, &hp) == 6) { int hwx, hwy; rm_to_wacom(hx + ox, hy + oy, &hwx, &hwy); emit_event(fd, EV_KEY, BTN_TOOL_RUBBER, 1); emit_event(fd, EV_ABS, ABS_X, hwx); emit_event(fd, EV_ABS, ABS_Y, hwy); emit_event(fd, EV_ABS, ABS_DISTANCE, 60); emit_syn(fd); usleep(2000); } else { emit_event(fd, EV_KEY, BTN_TOOL_RUBBER, 1); emit_event(fd, EV_ABS, ABS_DISTANCE, 60); emit_syn(fd); usleep(2000); } p = saved_p; emit_event(fd, EV_KEY, BTN_TOUCH, 1);
int bracket_depth = 1; float last_x = 1e9f, last_y = 1e9f; const float min_dist_sq = 15.0f * 15.0f;
while (*p && bracket_depth > 0) { if (*p == '[') { float x, y; int speed, width, direction, pressure; if (sscanf(p, "[%f,%f,%d,%d,%d,%d]", &x, &y, &speed, &width, &direction, &pressure) == 6) { float dx = (x + ox) - last_x; float dy = (y + oy) - last_y; if (dx*dx + dy*dy >= min_dist_sq || pass_emitted == 0) { int wx, wy; rm_to_wacom(x + ox, y + oy, &wx, &wy);
emit_event(fd, EV_ABS, ABS_X, wx); emit_event(fd, EV_ABS, ABS_Y, wy); emit_event(fd, EV_ABS, ABS_PRESSURE, 4095); emit_event(fd, EV_ABS, ABS_DISTANCE, 0); emit_syn(fd);
last_x = x + ox; last_y = y + oy; pass_emitted++; total_emitted++; usleep(delay_us); } } char *close = strchr(p + 1, ']'); if (close) p = close + 1; else break; } else if (*p == ']') { bracket_depth--; if (bracket_depth <= 0) break; p++; } else { p++; } }
emit_event(fd, EV_ABS, ABS_PRESSURE, 0); emit_event(fd, EV_ABS, ABS_DISTANCE, 86); emit_syn(fd); emit_event(fd, EV_KEY, BTN_TOUCH, 0); emit_event(fd, EV_KEY, BTN_TOOL_RUBBER, 0); emit_syn(fd); usleep(10000); } vlog("Erase pass %d/%d: %d points\n", pass+1, num_passes, pass_emitted); } return total_emitted;}
/* ─── page swipe (touchscreen MT-B) ─────────────────────────────── */
static int do_swipe_page(void) { int fd = open(TOUCH_PATH, O_WRONLY); if (fd < 0) { vlog("swipe: open %s failed\n", TOUCH_PATH); return -1; }
/* Right-to-left swipe across bottom third of screen (next page). * Touch coords match display: X 0-1403, Y 0-1871. * Swipe from (1200, 1400) to (200, 1400) in ~20 steps. * Protocol matches real pt_mt device: no ABS_MT_SLOT in frames, * tracking_id only on first/last, touch_major after a few frames. */ const int x0 = 1200, x1 = 200, y = 1400; const int steps = 20; const int step_delay = 8000; /* 8ms per step = ~160ms total */
/* First frame: tracking_id + position + pressure */ emit_event(fd, EV_ABS, ABS_MT_TRACKING_ID, 9999); emit_event(fd, EV_ABS, ABS_MT_POSITION_X, x0); emit_event(fd, EV_ABS, ABS_MT_POSITION_Y, y); emit_event(fd, EV_ABS, ABS_MT_PRESSURE, 100); emit_syn(fd); usleep(step_delay);
for (int i = 1; i <= steps; i++) { int cx = x0 + (x1 - x0) * i / steps; emit_event(fd, EV_ABS, ABS_MT_POSITION_X, cx); emit_event(fd, EV_ABS, ABS_MT_PRESSURE, 100); if (i == 3) { /* Add touch_major after a few frames like real hardware */ emit_event(fd, EV_ABS, ABS_MT_TOUCH_MAJOR, 17); } emit_syn(fd); usleep(step_delay); }
/* Release: tracking_id = -1 */ emit_event(fd, EV_ABS, ABS_MT_TRACKING_ID, -1); emit_syn(fd);
close(fd); vlog("Swipe page done\n"); return 0;}
/* ─── command handling ───────────────────────────────────────────── */
static void handle_command(const char *line) { int cmd_len; const char *cmd = json_str(line, "cmd", &cmd_len); if (!cmd) { send_line("{\"resp\":\"?\",\"ok\":false,\"error\":\"missing cmd\"}"); return; }
if (strncmp(cmd, "ping", 4) == 0) { send_line("{\"resp\":\"ping\",\"ok\":true}"); return; }
if (strncmp(cmd, "swipe", 5) == 0) { pthread_mutex_lock(&g_inject_lock); int ret = do_swipe_page(); pthread_mutex_unlock(&g_inject_lock); if (ret == 0) send_line("{\"resp\":\"swipe\",\"ok\":true}"); else send_line("{\"resp\":\"swipe\",\"ok\":false,\"error\":\"touch open failed\"}"); return; }
/* Capture: trigger screenshot, wait for FB dump, stream raw bytes */ if (strncmp(cmd, "capture", 7) == 0) { /* Trigger the xovi watch thread to dump the framebuffer */ unlink("/tmp/glossa_fb.raw"); int tf = open("/tmp/glossa_screenshot", O_CREAT|O_WRONLY, 0644); if (tf >= 0) close(tf);
/* Wait for the dump (watch thread checks every ~1s worst case) */ const long expected_size = 1404L * 1872 * 4; char *fb = NULL; long fb_size = 0; for (int i = 0; i < 20; i++) { usleep(100000); /* 100ms polls */ FILE *fp = fopen("/tmp/glossa_fb.raw", "r"); if (!fp) continue; fseek(fp, 0, SEEK_END); fb_size = ftell(fp); if (fb_size >= expected_size) { fseek(fp, 0, SEEK_SET); fb = malloc(fb_size); if (fb) fread(fb, 1, fb_size, fp); fclose(fp); break; } fclose(fp); }
if (!fb || fb_size < expected_size) { send_line("{\"resp\":\"capture\",\"ok\":false,\"error\":\"timeout\"}"); free(fb); return; }
/* Send JSON header with size, then raw bytes */ char hdr[128]; snprintf(hdr, sizeof(hdr), "{\"resp\":\"capture\",\"ok\":true,\"size\":%ld}", fb_size); pthread_mutex_lock(&g_write_lock); int cfd = g_client_fd; if (cfd >= 0) { write(cfd, hdr, strlen(hdr)); write(cfd, "\n", 1); /* Stream raw bytes in chunks */ long sent = 0; while (sent < fb_size) { long chunk = fb_size - sent; if (chunk > 65536) chunk = 65536; ssize_t w = write(cfd, fb + sent, chunk); if (w <= 0) break; sent += w; } } pthread_mutex_unlock(&g_write_lock); free(fb); vlog("Captured and streamed %ld bytes\n", fb_size); return; }
int file_len; const char *file_val = json_str(line, "file", &file_len); if (!file_val) { send_line("{\"resp\":\"?\",\"ok\":false,\"error\":\"missing file\"}"); return; } char filepath[512]; if (file_len >= (int)sizeof(filepath)) file_len = sizeof(filepath) - 1; memcpy(filepath, file_val, file_len); filepath[file_len] = '\0';
int speed_ms = json_int(line, "speed", 5); int delay_us = speed_ms * 1000;
char *json = NULL; long fsize = 0; if (load_file(filepath, &json, &fsize) != 0) { char err[256]; snprintf(err, sizeof(err), "{\"resp\":\"%.*s\",\"ok\":false,\"error\":\"load: %s\"}", cmd_len, cmd, strerror(errno)); send_line(err); return; }
vlog("Loaded %ld bytes from %s (speed=%dms)\n", fsize, filepath, speed_ms);
char resp[128]; /* Serialize all pen access: only one stroke job touches the * digitizer at a time, so concurrent draw/erase can't interleave. */ pthread_mutex_lock(&g_inject_lock); if (strncmp(cmd, "draw", 4) == 0) { int strokes = do_draw(g_dev_fd, json, delay_us); snprintf(resp, sizeof(resp), "{\"resp\":\"draw\",\"ok\":true,\"strokes\":%d}", strokes); } else if (strncmp(cmd, "erase", 5) == 0) { int points = do_erase(g_dev_fd, json, delay_us); snprintf(resp, sizeof(resp), "{\"resp\":\"erase\",\"ok\":true,\"points\":%d}", points); } else { snprintf(resp, sizeof(resp), "{\"resp\":\"?\",\"ok\":false,\"error\":\"unknown cmd\"}"); } pthread_mutex_unlock(&g_inject_lock);
free(json); send_line(resp);}
/* ─── idle-event watcher thread ──────────────────────────────────── */
static void *idle_watch_thread(void *arg) { (void)arg; long long last_ts = -1;
while (g_client_fd >= 0) { FILE *fp = fopen(IDLE_PATH, "r"); if (fp) { long long ts = 0; if (fscanf(fp, "%lld", &ts) == 1 && ts != last_ts) { last_ts = ts; char evt[64]; snprintf(evt, sizeof(evt), "{\"event\":\"idle\",\"ts\":%lld}", ts); send_line(evt); vlog("Pushed idle event ts=%lld\n", ts); } fclose(fp); } usleep(200000); /* poll the local file every 200ms (no network) */ } return NULL;}
/* ─── client session ─────────────────────────────────────────────── */
static void serve_client(int client_fd) { g_client_fd = client_fd;
/* Disable Nagle so small JSON commands go out immediately */ int one = 1; setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
/* TCP keepalive: detect dead clients even if they didn't close cleanly. * After 30s idle, probe every 10s, give up after 3 missed probes. */ setsockopt(client_fd, SOL_SOCKET, SO_KEEPALIVE, &one, sizeof(one));#ifdef TCP_KEEPIDLE int keepidle = 30; setsockopt(client_fd, IPPROTO_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle));#endif#ifdef TCP_KEEPINTVL int keepintvl = 10; setsockopt(client_fd, IPPROTO_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(keepintvl));#endif#ifdef TCP_KEEPCNT int keepcnt = 3; setsockopt(client_fd, IPPROTO_TCP, TCP_KEEPCNT, &keepcnt, sizeof(keepcnt));#endif
/* Read timeout: if no data for 15s, assume client is dead. * Python sends pings every 5s, so 15s gives 3 missed pings. */ struct timeval tv; tv.tv_sec = 15; tv.tv_usec = 0; setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
/* Spawn idle watcher for this session */ pthread_t watcher; pthread_create(&watcher, NULL, idle_watch_thread, NULL); pthread_detach(watcher);
send_line("{\"event\":\"ready\"}"); vlog("Client connected\n");
char buf[MAX_CMD]; char line[MAX_CMD]; int line_len = 0;
while (1) { ssize_t n = read(client_fd, buf, sizeof(buf)); if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { vlog("Read timeout, client assumed dead\n"); break; } if (errno == EINTR) continue; break; } if (n == 0) break; /* client disconnected cleanly */
/* Accumulate into lines (newline-delimited framing) */ for (ssize_t i = 0; i < n; i++) { if (buf[i] == '\n') { line[line_len] = '\0'; if (line_len > 0) handle_command(line); line_len = 0; } else if (line_len < (int)sizeof(line) - 1) { line[line_len++] = buf[i]; } } }
g_client_fd = -1; close(client_fd); vlog("Client disconnected\n");}
/* ─── main ───────────────────────────────────────────────────────── */
int main(int argc, char **argv) { int port = DEFAULT_PORT; for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) g_verbose = 1; else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) port = atoi(argv[++i]); }
signal(SIGPIPE, SIG_IGN);
g_dev_fd = open(DEV_PATH, O_WRONLY); if (g_dev_fd < 0) { perror("open " DEV_PATH); return 1; }
int srv = socket(AF_INET, SOCK_STREAM, 0); if (srv < 0) { perror("socket"); return 1; }
int one = 1; setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(port);
if (bind(srv, (struct sockaddr *)&addr, sizeof(addr)) < 0) { perror("bind"); return 1; } if (listen(srv, 1) < 0) { perror("listen"); return 1; }
fprintf(stderr, "[uinjectd] Listening on :%d (dev=%s%s)\n", port, DEV_PATH, g_verbose ? " verbose" : "");
/* Accept one client at a time; reconnect loop for robustness */ while (1) { struct sockaddr_in cli; socklen_t clen = sizeof(cli); int client_fd = accept(srv, (struct sockaddr *)&cli, &clen); if (client_fd < 0) { if (errno == EINTR) continue; perror("accept"); break; } serve_client(client_fd); }
close(srv); close(g_dev_fd); return 0;}