Three input methods for the gimbal, all sharing the widget's velocity model: hold to move, release to stop. Keyboard in the applet. WASD and the arrow keys feed the same tick timer the pointer does, through a new vectorFromKeys helper. Opposing keys cancel and a diagonal is normalised to length 1, so two keys do not move faster than one. Auto-repeat is discarded, because it arrives as a press/release cycle and would make the motion stutter. The held keys are cleared when the popup loses focus: otherwise the matching release never arrives and the camera keeps panning on its own. The buttons and zoom slider are set to Qt.NoFocus so the arrow keys still reach the pad after one of them is clicked. Gamepad support, as a separate process. Qt 6 removed QtGamepad and ships no replacement, so the applet cannot read a controller itself. bin/linkctl-gamepad reads /dev/input/js* directly using only the standard library: the event struct is eight bytes and needs no third-party package. Defaults target a DualSense on hid_playstation, but axis and button numbering comes from the driver rather than the controller and varies between kernels and connection modes, so --monitor prints live events for mapping and every number is overridable. Opening a joystick replays the current state of every button with JS_EVENT_INIT set; those are filtered, or a held button would fire its mapped action at startup. --wait reconnects a wireless pad that sleeps. Directional global shortcuts. Meta+Alt plus the arrow keys nudge the gimbal. These are discrete steps by necessity: kglobalaccel fires once per keypress and exposes no press-and-hold, so this cannot be smooth motion and the README says so and points at the widget or a gamepad instead. NUDGE sets the degrees. Tested: 145 checks across five suites, all passing, shellcheck clean, QML free of syntax errors. 13 new node checks cover the key vector, including the cancellation and diagonal-normalisation cases. 36 new python checks cover event decoding, the INIT flag, deadzone rescaling and command construction. A new FIFO-driven suite stands in for /dev/input/js* and plays scripted events through the real select/read/decode/tick path, confirming diagonals emit both axes, a centred stick goes quiet, a deadzone deflection moves nothing, a real button press fires, the initial-state burst does not, and disconnect is handled. Not verified: no physical controller, camera or Plasma session was available, so the synthetic events cannot confirm a real DualSense reports the assumed axis and button numbers.
Insta360 Link 2 integration for OBS and KDE
Hotkey and OBS integration on top of linkctl,
so AI tracking and gimbal presets are a keypress rather than a terminal command.
Several pieces, usable independently:
| Path | What it gives you |
|---|---|
bin/linkctl-macro |
A wrapper that makes linkctl safe to bind to a hotkey: finds the binary in a desktop session's sparse PATH, passes --force, and reports the result as a desktop notification. |
kde/install-shortcuts.sh |
Registers KDE Plasma global shortcuts, so the keys work from anywhere including under Wayland. |
obs/obs-linkctl.lua |
OBS hotkeys, buttons, and scene automation: a scene switch can set tracking or load a preset on its own. |
plasmoid/ |
A Plasma 6 widget with a virtual joystick, zoom slider, and Home/Center/Tracking buttons. Pointer or WASD/arrow keys. |
bin/linkctl-gamepad |
Drives the gimbal from a game controller (DualSense and anything else the kernel exposes as a joystick). |
Prerequisites
linkctl installed and working (linkctl status reports your camera), and a
saved home preset:
linkctl preset save home # point the camera where you want "home" first
Install
./kde/install-shortcuts.sh --dry-run # review, changes nothing
./kde/install-shortcuts.sh
Default bindings, editable in the BINDINGS table at the top of that script:
| Key | Action |
|---|---|
Meta+Alt+T |
toggle AI tracking |
Meta+Alt+1 |
tracking on |
Meta+Alt+0 |
tracking off |
Meta+Alt+H |
preset load home |
Meta+Alt+C |
recenter gimbal |
Meta+Alt+←/→ |
pan 5 degrees |
Meta+Alt+↑/↓ |
tilt 5 degrees |
The directional ones are discrete nudges: kglobalaccel fires once per
keypress and offers no press-and-hold, so holding the key will not pan
smoothly. Use the widget or a gamepad for continuous motion. NUDGE=10 ./kde/install-shortcuts.sh changes the step.
Check them afterwards with ./kde/install-shortcuts.sh --list, which shows the
registered key, the default from the desktop entry, and flags any value Plasma
cannot parse. Or look in System Settings under Keyboard, Shortcuts, searching
for "Insta360". Reverse the whole thing with --uninstall.
The entries are visible in the launcher by default, because Plasma skips
NoDisplay services when building the Shortcuts list and they would otherwise
not appear there at all. HIDE_FROM_MENU=1 ./kde/install-shortcuts.sh hides
them at that cost.
Joystick widget
./plasmoid/install.sh
Then right-click a panel or the desktop, Add Widgets, and search for
"Insta360". Pass --restart when reinstalling over a running copy, since
Plasma otherwise keeps the old one loaded.
The joystick is a velocity control: how far you push sets how fast the camera
moves, and it stops when you let go. Degrees per step, step interval, deadzone
and axis inversion are all in the widget's settings, along with the linkctl
path and an optional device.
WASD and the arrow keys work whenever the popup has focus, with the same velocity behaviour: hold to move, release to stop. Opposing keys cancel, and a diagonal moves at the same speed as a straight push rather than faster. The buttons and zoom slider are set to take no focus, so the arrow keys keep reaching the pad after you click one. If focus leaves the popup mid-press the held keys are cleared, so the camera cannot be left panning on its own.
It drives the gimbal with relative moves rather than absolute ones, because
linkctl status reports no pan or tilt at all while the camera is idle, so
there is no trustworthy position to aim from. Relative steps work either way
and cannot drift out of sync, and linkctl clamps each one to the camera's own
range, so holding the stick against a limit is harmless.
Game controller
bin/linkctl-gamepad --list # which joystick devices exist
bin/linkctl-gamepad --monitor # print events as you move sticks and press buttons
bin/linkctl-gamepad # run it
Same velocity behaviour as the widget: push the left stick to move, release to
stop. Defaults are a DualSense on the in-tree hid_playstation driver, left
stick on axes 0 and 1, with buttons 0, 1 and 2 bound to recenter, home preset
and tracking toggle.
Confirm your mapping with --monitor before trusting the defaults. Axis
and button numbering comes from the driver, not the controller, so it varies
between kernels and connection modes (a DualSense often enumerates differently
over Bluetooth than over USB). --monitor prints the number for whatever you
touch, and everything is overridable:
bin/linkctl-gamepad --pan-axis 0 --tilt-axis 1 --step 8 --deadzone 0.2 --button 3="tracking toggle" --button 9="preset load home"
It reads /dev/input/js* directly, using only the Python standard library.
Qt 6 removed QtGamepad with no replacement, so the widget cannot do this
itself and this runs as a separate process. You need to be in the input
group to open the device.
--wait makes it tolerate the controller being absent and reconnect when it
returns, which is what you want for a wireless pad that sleeps. There is a
user service in contrib/linkctl-gamepad.service for running it that way;
edit the ExecStart path before installing it.
OBS
Tools, Scripts, +, then pick obs/obs-linkctl.lua.
Set the linkctl binary path if it is not on OBS's PATH, then bind the
hotkeys under Settings, Hotkeys (search "Insta360").
Scene automation. Press Insert current scene names to prefill the mapping box, then give the scenes you care about an action:
Talking Head = tracking on
Overhead = preset load overhead
Slides = tracking off
The left side is the scene name, the right side is any linkctl arguments.
Blank actions and # comments are ignored.
Startup action runs when OBS finishes loading, and defaults to
preset load home. This is the setting worth keeping: the Link parks its
gimbal facing down when idle, so launching OBS puts it back on its own.
Under Wayland, OBS's own hotkeys only fire while OBS has focus. That is an OBS limitation, and the reason the KDE shortcuts exist; install both and use the KDE ones for anything you need mid-stream.
Configuration
Optional, at ~/.config/linkctl-macro.conf (see linkctl-macro.conf.example):
LINKCTL=/usr/local/bin/linkctl # if not on PATH
LINKCTL_DEVICE=/dev/video0 # pin the camera if you have more than one
LINKCTL_FORCE=1 # 0 respects linkctl's idle-camera guard
LINKCTL_NOTIFY=always # always | error | never
--force is on by default because a deliberate keypress means "do it now",
including when nothing currently holds the camera open. That is exactly the
case where the gimbal is parked facing down.
Testing without a camera
lua tests/test_obs_linkctl.lua # pure helpers: quoting, parsing, commands
lua tests/test_obs_lifecycle.lua # the OBS script against a mock OBS API
node tests/test_joystick_logic.js # widget command building, parsing, key vectors
python3 tests/test_gamepad.py # gamepad event decoding and deadzone maths
tests/test_gamepad_loop.sh # the gamepad read loop, fed through a FIFO
bin/linkctl-macro --print tracking on # show the command without running it
bin/linkctl-gamepad --dry-run # print moves instead of sending them
./kde/install-shortcuts.sh --dry-run # show the KDE changes, apply nothing
./kde/install-shortcuts.sh --list # diagnose what is actually registered
shellcheck bin/linkctl-macro kde/install-shortcuts.sh plasmoid/install.sh
The lifecycle suite drives the script the way OBS does, so it catches broken wiring (hotkeys not firing, scene actions not matching, the preset name being captured too early) without needing OBS or a camera.
test_gamepad_loop.sh substitutes a FIFO for /dev/input/js* and plays
scripted events through it, so the select/read/decode/tick path is covered
without a controller attached. It is also the test that pins down the
initial-state burst: opening a joystick replays every button as currently
pressed, and treating that as real input would fire mapped actions at startup.
Troubleshooting
Shortcut does nothing, and System Settings says "No default shortcuts".
Run ./kde/install-shortcuts.sh --list. A _launch value containing commas
is the Plasma 5 format: Plasma 6 passes that value straight to
QKeySequence::fromString, so Meta+Alt+T,none,Some Name parses to nothing
and binds no key. Re-running the installer rewrites it as a bare key sequence.
Versions of this repo before that fix wrote the Plasma 5 form.
Shortcut still does nothing. Run the action directly:
bin/linkctl-macro tracking on. A failure notification names the cause. If
that works but the key does not, check for a conflict in System Settings, and
confirm --list shows the entry as present.
Gamepad does nothing. Check bin/linkctl-gamepad --list sees it. If the
device is listed but nothing moves, run --monitor and push the stick: no
events means a permissions or driver problem, events with unexpected axis
numbers mean you need --pan-axis/--tilt-axis. "permission denied" means
adding yourself to the input group and logging back in.
Arrow keys change zoom instead of moving the camera. That would mean the
slider has keyboard focus. The controls are set to Qt.NoFocus to prevent it;
if you see it, say so, because it means the focus policy is not taking effect
on your Plasma build.
tracking fails with exit 12. AI tracking is experimental in linkctl and
goes through the camera's vendor extension unit. Pan, tilt, zoom and presets use
standard UVC and are unaffected.
Permission denied (exit 6). Add yourself to the video group, or install
the udev rule from linkctl's contrib/99-insta360-link.rules.
Nothing moves at all. The Link 2 rejects single-axis pan/tilt writes and
needs both axes set in one VIDIOC_S_EXT_CTRLS call. linkctl does this
correctly; raw v4l2-ctl needs --set-ctrl pan_absolute=0,tilt_absolute=0
as one argument.
Status
The helpers, the shell scripts and the gamepad read loop are tested offline,
and the KDE installer is dry-runnable and reversible. Use --dry-run first.
Not verified against real hardware at the time of writing: no live camera, no
running Plasma session, and no physical game controller. The gamepad bridge was
tested by feeding synthetic kernel events through a FIFO, which exercises every
line of the loop but cannot confirm that a DualSense reports the axis and
button numbers assumed by the defaults. Treat --monitor as a required first
step rather than a debugging aid.