/* * Pure helpers for the joystick applet. * * Kept free of QML types so they can be unit-tested with node; see * tests/test_joystick_logic.js. `.pragma library` means this file holds no * per-instance state and cannot touch the QML context, which is exactly the * constraint that keeps it testable. */ .pragma library // Quote a value for /bin/sh. Config values (binary path, device, preset name) // all reach a shell, so this is a boundary that has to hold. function shq(s) { return "'" + String(s).replace(/'/g, "'\\''") + "'"; } // The invocation prefix shared by every command the applet runs. function baseCmd(linkctl, device) { var c = shq(linkctl || "linkctl") + " --force --json"; if (device && device !== "") { c += " --device " + shq(device); } return c; } /* * Build the shell command for one joystick tick. * * nx/ny are the normalised stick position, ny positive upwards. Returns "" * when the deflection on both axes is too small to be worth a command, so the * caller can skip the tick entirely. * * Each axis needs its own linkctl call because the relative commands are * single-axis, so a diagonal push produces two, chained with ";". */ function tickCommand(base, nx, ny, stepMax, invertPan, invertTilt) { var x = invertPan ? -nx : nx; var y = invertTilt ? -ny : ny; var dx = Math.abs(x) * stepMax; var dy = Math.abs(y) * stepMax; var parts = []; // Per-axis threshold, so a mostly-horizontal push does not also creep // vertically. Below 0.5 degrees the camera's 0.1 degree resolution makes // the step pointless anyway. if (dx >= 0.5) { parts.push(base + (x > 0 ? " right " : " left ") + dx.toFixed(1)); } if (dy >= 0.5) { parts.push(base + (y > 0 ? " up " : " down ") + dy.toFixed(1)); } return parts.join(" ; "); } /* * Turn a set of held direction keys into a stick vector. * * `held` has boolean left/right/up/down members. Opposing keys cancel, and a * diagonal is normalised to length 1 so holding two keys does not move the * camera faster than holding one. * * Key codes are deliberately not mapped here: this file is a `.pragma * library` and so cannot reach the Qt namespace for Qt.Key_*. The QML side * resolves a key code to a direction name and passes the result in. */ function vectorFromKeys(held) { var x = 0; var y = 0; if (held.left) { x -= 1; } if (held.right) { x += 1; } if (held.up) { y += 1; } if (held.down) { y -= 1; } var len = Math.sqrt(x * x + y * y); if (len > 1) { x /= len; y /= len; } return { nx: x, ny: y }; } /* * Merge every JSON object found in linkctl's stdout into a flat update. * * A diagonal tick runs two linkctl calls, and each reports only the axis it * changed, so the results have to be merged rather than replaced. Only keys * actually present with the right type are returned, which keeps a null from * an unrelated field from clobbering a known value. */ function absorb(stdout) { var update = {}; var lines = String(stdout).split("\n"); for (var i = 0; i < lines.length; i++) { var t = lines[i].trim(); if (t.length === 0 || t.charAt(0) !== "{") { continue; } var o; try { o = JSON.parse(t); } catch (e) { continue; } if (typeof o.pan === "number") { update.pan = o.pan; } if (typeof o.tilt === "number") { update.tilt = o.tilt; } if (typeof o.zoom === "number") { update.zoom = o.zoom; } if (typeof o.tracking === "boolean") { update.tracking = o.tracking; } if (typeof o.state === "string") { update.state = o.state; } } return update; } // Pull a human-readable reason out of a failed linkctl run. function errorMessage(stdout, stderr, exitCode) { var err = String(stderr || "").trim(); try { var o = JSON.parse(String(stdout || "").trim()); if (o && o.error) { return String(o.error); } } catch (e) { // not JSON, fall through to stderr } if (err !== "") { return err; } return "linkctl exited " + exitCode; }