The joystick, the keyboard and every movement command did nothing. The cause
was in the journal all along:
main.qml: ReferenceError: stick is not defined
`stick` is the id of the Joystick inside the `fullRepresentation` Component.
Ids declared inside a Component are not visible from the enclosing scope, so
the inNx, inNy and inEngaged properties declared on the root object could not
resolve it. Those bindings threw, inEngaged stayed false, the tick timer never
ran, and no input of any kind produced a command. Everything else kept
working, which is why status, zoom and binary resolution all looked healthy in
the log while dragging did nothing.
The three properties now live inside the fullRepresentation, where both
`stick` and the root object are in scope.
Also fixed from the same log: the configuration page logged "does not have a
property called cfg_<name>Default" for all eight keys plus `title`, because
Plasma assigns those when it instantiates a config page and they were never
declared. They are declared now, so the Defaults button has something to
compare against.
Startup probed for the binary twice, because Component.onCompleted and
onConfiguredBinChanged both fired while the component was still being set up,
duplicating every follow-up query. The second path is now guarded.
The diagnostics readout is one label built by one guarded function instead of
a Repeater over an array of expressions. In that earlier shape a single
throwing sub-expression discards the whole model and the panel renders empty,
which is precisely the failure mode it is supposed to diagnose, and is what it
did.
The real repair is to the verification, in tests/lint_qml.py. qmllint had
flagged `stick`, and a filter discarded it. Without the KDE QML modules
present qmllint cannot resolve PlasmoidItem, so nothing inside it has a known
scope and all 87 identifiers in a clean file are reported as unqualified
access. Filtering that category wholesale was the only way to get a readable
result, and it threw away the one finding that mattered.
tests/qmlstubs now holds minimal stand-ins for org.kde.plasma.plasmoid,
org.kde.plasma.components, org.kde.plasma.plasma5support and
org.kde.kirigami, so the types resolve and the scope analysis is meaningful.
With the stubs in place the only remaining unqualified accesses are `root`,
`sh` and `modelData`, each allowlisted with a reason, so anything else is a
genuine finding. Confirmed by reintroducing the bug: the linter fails and
names the exact lines and identifier.
Tested: 169 checks across seven suites plus the new linter, all passing.
shellcheck clean.
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".
Reinstalling needs --restart. Plasma loads an applet's QML once and keeps
it. Pulling new code and restarting the desktop is not enough on its own,
because the installed copy is only replaced by install.sh. After updating
this repo, run ./plasmoid/install.sh --restart, or new behaviour will appear
to be missing while the old code carries on running.
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
tests/test_resolver.sh # binary resolution, run in a real shell
python3 tests/lint_qml.py # QML scope errors, with KDE types resolved
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.
tests/lint_qml.py is worth understanding before trusting a clean QML run.
Plain qmllint cannot resolve the KDE QML modules, so it reports every
identifier inside PlasmoidItem as "Unqualified access" and buries real
findings in roughly ninety lines of noise. tests/qmlstubs/ holds minimal
stand-ins for those modules so the types resolve and the scope analysis means
something; with them in place the only legitimate unqualified accesses are a
short allowlist, so anything else is a real finding. This exists because
filtering that noise wholesale once hid a ReferenceError: stick is not defined that disabled all camera movement.
If you add a configuration key to contents/config/main.xml, add it to
tests/qmlstubs/org/kde/plasma/plasmoid/Configuration.qml too, or the linter
will not catch a typo in the key name.
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.
The widget cannot control the camera, but linkctl works in a terminal.
Almost always PATH. Plasma runs applet commands through
KProcess::setShellCommand, which is /bin/sh -c inheriting plasmashell's
environment, and that environment never sources ~/.bashrc or a login
profile. An install that puts linkctl on PATH from a shell rc file, which
includes mise and asdf shims and ~/.local/bin on some distributions, is
therefore invisible to the widget. The widget now searches those locations
itself at startup and says so in its status line if it finds nothing; the
settings page accepts a full path to override it. Compare what the session
sees with what your shell sees:
systemctl --user show-environment | grep '^PATH'
command -v linkctl
The widget worked, then stopped after a restart or a replug. The Link's
/dev/videoN number is not stable. If the Video device field in the widget's
settings is set, clear it and let auto-detection run: a pinned node that has
since moved makes every command fail.
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.