Skip to the content
PCBKitNew board
2 layersInsta360-joystick, the printed circuit board in 3D

A lightweight daemon, CLI tool, and (optional) firmware + hardware for controlling an Insta360 Link webcam without the first-party software.

Copper layers
2
Thickness
1.6 mm
Parts
58
Nets
53
Pads
223
Tracks, vias
0, 0
Routed
Not yet
KiCad
KiCad 10.0 (file 20260206)
Created
31 Jan 2026
Updated
9 Jun 2026

Overview

Insta360-joystick is an open-source printed circuit board by jfwoods, from the jfwoods/insta360link-controller repository on GitHub. It is a 2-layer board with 58 parts on 53 nets.

It is built around the ESP32-S3-WROOM-1-N8, AP2112K-3.3 and BQ25185DLHR, with the MAX17048G+T10 and SP0503BAHTG among its other parts. Its parts include 13 resistors, 9 capacitors, 5 ICs, 5 diodes and 5 switches. It belongs with the keyboards and input boards and the ESP32 boards of this library.

Main components

5

README

README.md

Insta360 Link Controller

GitHub Release CI Release

The first-party control software for the Insta360 Link is (no offense) bloated garbage, even though the hardware is pretty solid. So, I took matters into my own hands.

This implementation controls the camera directly via USB/UVC extension units through linkctl-daemon a lightweight daemon — no Insta360 desktop app required. Interactions with the daemon can be driven by either:

  1. A CLI client linkctl
  2. An ESP32-based joystick controller

This repo contains the ESP32 firmware, Mac daemon and CLI source codes, and KiCad PCB design files for a custom handheld controller.

text
[Insta360 Link] ──USB/UVC──> [linkctl-daemon (Mac)]
                                                    ──WiFi/WebSocket──> [ESP32 + Joystick]
                                                    ──CLI Client──────> [linkctl Interface]

Opportunities For Future Work:

  • Windows Support: this is on my todo list
  • Linux Support: this is not on my todo list currently, but PRs are welcome :)
  • Keyframe Implementation: as I've been using this, the ability to have orientation presets has become the one feature I've missed from the first-party software. This is on my todo list for after Windows support

Setup

1. Install the Mac daemon

The daemon controls the Insta360 Link over USB and exposes a WebSocket + TCP interface for clients.

Option A: Download the latest release

Download linkctl-daemon-*-macos-*.tar.gz from the latest release, then:

bash
tar xzf linkctl-daemon-*-macos-*.tar.gz
cd linkctl-daemon
./install.sh

Option B: Build from source

bash
# Install dependencies
brew install libwebsockets cjson openssl@3

# Build
cd host/linkctl-daemon
mkdir -p build && cd build
PKG_CONFIG_PATH="/opt/homebrew/lib/pkgconfig:$PKG_CONFIG_PATH" cmake ..
make

# Run
./linkctl-daemon

The daemon finds the Insta360 Link over USB, starts a WebSocket server on port 9000 and a TCP control socket on port 9001, and advertises itself via mDNS.

2. Install the CLI client (optional)

Option A: Download the latest release

Download linkctl-*-macos-*.tar.gz from the latest release, then:

bash
tar xzf linkctl-*-macos-*.tar.gz
sudo cp linkctl/linkctl /usr/local/bin/

Option B: Build from source

bash
cd host/linkctl
mkdir -p build && cd build
cmake ..
make

# Optional: install system-wide
sudo cmake --install . --prefix /usr/local

Or run directly from the build directory: ./build/linkctl <command>.

3. Flash ESP32 firmware (if using joystick controller)

Requires PlatformIO.

bash
pio run --target upload

4. Configure WiFi on the ESP32 (if using joystick controller)

On first boot, the ESP32 starts a captive portal:

  1. Connect to the Insta360-Controller WiFi network from your phone or laptop
  2. A setup page opens automatically — enter your WiFi network name and password
  3. The ESP32 saves the credentials and reboots onto your network

Alternatively, configure via the serial CLI (115200 baud):

text
wifi MyNetwork MyPassword
connect

5. Use it

CLI: Run linkctl status to verify the daemon is running and the camera is connected, then use linkctl jog for interactive control.

Joystick: Move the joystick to pan and tilt the camera. The ESP32 reads the analog input, converts it to direction and speed, and sends commands to the daemon over WebSocket. The ESP32 discovers the daemon automatically via mDNS — no IP configuration needed.

Dependencies

Daemon (linkctl-daemon)

DependencySourcePurpose
libwebsocketsHomebrewWebSocket server
cJSONHomebrewJSON parsing
OpenSSL 3HomebrewRequired by libwebsockets
IOKitmacOS (built-in)USB device access
CoreFoundationmacOS (built-in)System data structures
dns_sdmacOS (built-in)mDNS advertisement

CLI (linkctl)

No external dependencies — pure POSIX C.

ESP32 Firmware

DependencySourcePurpose
arduinoWebSocketsPlatformIO (links2004/WebSockets@^2.4.1)WebSocket client
ArduinoJsonPlatformIO (bblanchon/ArduinoJson@^7.0.0)JSON encoding/parsing
PlatformIOCLI or VS Code extensionBuild toolchain (Arduino framework)

Daemon Options

text
./linkctl-daemon                  # Default: port 9000, mDNS enabled
./linkctl-daemon --port 8080      # Custom port
./linkctl-daemon --no-mdns        # Disable mDNS advertisement
./linkctl-daemon --test           # Test camera control and exit

CLI Client (linkctl)

linkctl is a lightweight command-line client for controlling the Insta360 Link via the daemon's TCP control socket. It has zero external dependencies (pure POSIX C) and connects to the daemon at localhost:9001.

Commands

CommandDescription
linkctl helpShow usage information
linkctl statusShow camera connection state and current zoom
linkctl centerCenter the gimbal and reset zoom to 1.0x
linkctl zoom <1.0-4.0>Set zoom level (e.g., linkctl zoom 2.5)
linkctl zoomShow the current zoom level
linkctl stopStop all gimbal movement
linkctl jogEnter interactive pan/tilt mode

Jog Mode

linkctl jog enters an interactive mode for real-time camera control using keyboard input.

KeyAction
WTilt up
STilt down
APan left
DPan right
+ / =Increase speed
-Decrease speed
SpaceStop movement
CCenter gimbal
Q / ESCExit 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).

ESP32 Serial CLI

Connect to the ESP32's serial port at 115200 baud. Type help for a full list.

CommandDescription
wifi <ssid> [pass]Set WiFi credentials
connectConnect to WiFi and daemon
disconnectDisconnect
statusShow connection state
stopStop gimbal movement
centerCenter the gimbal
joyTest joystick output (5 sec)
calibrateCalibrate joystick center point
invertx / invertyToggle axis inversion
portalStart captive portal for WiFi setup
webconfigStart web config on current network
host <ip>Override daemon IP (skip mDNS)
port <port>Override daemon port
clearClear all saved config
rebootRestart ESP32

Protocol

The daemon exposes two interfaces:

  • WebSocket on port 9000 — for the ESP32 joystick controller (JSON over WebSocket text frames)
  • TCP on port 9001 — for the linkctl CLI client (newline-terminated text commands)

WebSocket Commands (ESP32 → Daemon)

json
{"cmd": "pan_tilt", "pan_dir": 1, "pan_speed": 15, "tilt_dir": 255, "tilt_speed": 10}
{"cmd": "zoom", "value": 250}
{"cmd": "stop"}
{"cmd": "center"}
{"cmd": "status"}

WebSocket Responses (Daemon → ESP32)

json
{"type": "status", "camera_connected": true, "zoom": 150}
{"type": "error", "message": "camera not found"}

Direction encoding: 1 = positive (right/up), 255 = negative (left/down), 0 = stop. Speed ranges: pan 0-30, tilt 0-20, zoom 100-400.

How It Works

The Insta360 Link exposes pan/tilt/zoom controls as UVC extension unit registers accessible over USB. The linkctl-daemon writes to these registers directly using macOS IOKit, bypassing the Insta360 desktop app entirely. The daemon runs a WebSocket server on port 9000 that accepts JSON commands and translates them into the appropriate UVC control transfers, plus a TCP control socket on port 9001 for the linkctl CLI. The ESP32 reads the joystick, discovers the daemon via mDNS, and streams movement commands over WiFi. For use without an ESP32, the linkctl CLI connects to the daemon's TCP socket and sends commands directly from the terminal.

Testing

The daemon includes an integration test suite that exercises WebSocket communication, camera control, error handling, and multi-client support. Requires the Insta360 Link to be connected.

bash
# Run all tests (camera will move)
python3 host/linkctl-daemon/test_daemon.py

# Skip tests that move the camera
python3 host/linkctl-daemon/test_daemon.py --skip-motion

Hardware

ESP32-WROOM-32 (Breadboard Prototype)

FunctionGPIONotes
Joystick X (pan)34ADC1, input only
Joystick Y (tilt)35ADC1, input only
Joystick button (home)32ADC1, internal pull-up, active low
Zoom potentiometer33ADC1
I2C SDA21Hardware I2C default
I2C SCL22Hardware I2C default
Calibrate button25Internal pull-up, active low
Spare button26Internal pull-up, active low
Status LED2On-board LED

Note: Use ADC1 pins (GPIO 32-39) for analog inputs — ADC2 conflicts with WiFi on the ESP32. GPIO 0/1/3/6-11/12/15 are reserved (boot/flash/UART/strapping).

LED status patterns:

  • Solid — Connected to daemon, camera ready
  • Fast blink — Connected to daemon, waiting for camera
  • Slow blink — WiFi connecting
  • Off — No connection

ESP32-S3-WROOM (PCB)

FunctionGPIONotes
Joystick X (pan)4ADC1
Joystick Y (tilt)5ADC1
Joystick button (home)13Internal pull-up, active low
Zoom potentiometer6ADC1
I2C SDA (OLED + fuel gauge)8S3 default I2C SDA
I2C SCL (OLED + fuel gauge)9S3 default I2C SCL
Calibrate button14Internal pull-up, active low
Spare button15Internal pull-up, active low
Status LED (connectivity)16External LED
Status LED (TBD)17External LED
Status LED (TBD)18External LED
USB D-19Native USB (reserved)
USB D+20Native USB (reserved)

Note: Use ADC1 pins (GPIO 1-10) for analog inputs — ADC2 conflicts with WiFi on the S3. GPIO 19/20 are reserved for native USB.

Project Structure

text
├── platformio.ini                   # ESP32 build config
├── src/
│   ├── main.cpp                     # Setup and main loop
│   ├── config.h                     # Pin definitions, constants
│   ├── joystick.h/cpp               # Analog input, deadzone, calibration
│   ├── wifi_manager.h/cpp           # WiFi connection handling
│   ├── websocket_client.h/cpp       # WebSocket + mDNS discovery
│   ├── commands.h/cpp               # JSON command encoding
│   ├── serial_cli.h/cpp             # Serial CLI
│   ├── status_led.h/cpp             # LED status patterns
│   ├── zoom.h/cpp                   # Zoom potentiometer input
│   └── captive_portal.h/cpp         # AP mode WiFi config portal
├── host/
│   ├── linkctl/
│   │   ├── CMakeLists.txt           # CLI build config
│   │   ├── linkctl.c                # CLI client implementation
│   │   └── README.md                # CLI usage documentation
│   └── linkctl-daemon/
│       ├── CMakeLists.txt           # Daemon build config
│       ├── main.c                   # Entry point and event loop
│       ├── camera.c/h               # IOKit USB camera control
│       ├── server.c/h               # WebSocket server
│       ├── control.c/h              # TCP control socket (for CLI)
│       ├── mdns.c/h                 # mDNS advertisement

GenAI Disclaimer: I'm a hardware guy-- I tolerate writing code, but I don't love it. So while the hardware design is truly my own work product, Claude Code has been used to support firmware/software development. All code is still reviewed by me before release.

Commit Message Disclaimer: I apologize if my commit messages are occasionally less than informative-- this is a weekend project for me, and sometimes I'm a silly goose.

Parts

58
QtyValueFootprintReferences
51 uFCapacitor_SMD:C_0603_1608MetricC2, C4, C5, C6, C7
410 kOhmResistor_SMD:R_0603_1608MetricR7, R8, R12, R13
4MountingHoleMountingHole:MountingHole_2.2mm_M2_Pad_TopBottomH1, H2, H3, H4
20.1 uFCapacitor_SMD:C_0603_1608MetricC1, C9
210 OhmResistor_SMD:R_0603_1608MetricR9, R11
25.1 kOhmResistor_SMD:R_0603_1608MetricR1, R2
265 OhmResistor_SMD:R_0603_1608MetricR6, R10
2REDDiode_SMD:D_1206_3216MetricD2, D4
1/CETestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mmTP7
110 uFCapacitor_SMD:C_0603_1608MetricC3
113 kOhmResistor_SMD:R_0603_1608MetricR5
122 uFCapacitor_SMD:C_0603_1608MetricC8
13 MOhmResistor_SMD:R_0603_1608MetricR3
1300 OhmResistor_SMD:R_0603_1608MetricR4
13V3TestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mmTP3
1AP2112K-3.3Package_TO_SOT_SMD:SOT-23-5U3
1BAT_ALRTTestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mmTP8
1BLUEDiode_SMD:D_1206_3216MetricD3
1BQ25185DLHRINSTA360:WSON10_DLH_TEXU2
1BatteryINSTA360:18650_HolderBT1
1ESP32-S3-WROOM-1RF_Module:ESP32-S3-WROOM-1U5
1GNDTestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mmTP1
1GREENDiode_SMD:D_1206_3216MetricD5
1MAX17048G+T10INSTA360:21-0168E_T822-3_MXMU1
1POT_XConnector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_VerticalJ3
1POT_YConnector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_VerticalJ4
1PinHeader_1x04_P2.54mm_VerticalConnector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_VerticalU4
1SP0503BAHTPackage_TO_SOT_SMD:SOT-143D1
1STAT1TestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mmTP6
1STAT2TestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mmTP9
1SW_AUXINSTA360:ME02150F3502SW2
1SW_BOOTButton_Switch_SMD:SW_Push_SPST_NO_Alps_SKRKSW3
1SW_CALINSTA360:ME02150F3502SW4
1SW_PWRINSTA360:SW_L101011ML04QSW1
1SW_RSTButton_Switch_SMD:SW_Push_SPST_NO_Alps_SKRKSW5
1TestPoint_SmallTestPoint:TestPoint_Bridge_Pitch2.0mm_Drill0.7mmTP10
1UART_RXTestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mmTP12
1UART_TXTestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mmTP11
1USB_C_Receptacle_USB2.0_14PConnector_USB:USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_HorizontalJ1
1VBATTTestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mmTP2
1VBUSTestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mmTP4
1VCCTestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mmTP5
1YAW_BUTTONConnector_PinHeader_1.27mm:PinHeader_1x05_P1.27mm_VerticalJ2

Questions

What parts does the Insta360-joystick use?

58 parts, built around the ESP32-S3-WROOM-1-N8, AP2112K-3.3, BQ25185DLHR and MAX17048G+T10. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.

Where can I download the Insta360-joystick KiCad files?

From the jfwoods/insta360link-controller repository on GitHub, at the commit this page was made from: the board is hardware/insta360-joystick.kicad_pcb and its schematic hardware/insta360-joystick.kicad_sch. PCBKit opens the board in the browser, with nothing to install.

Can I use the Insta360-joystick design in my own project?

Its repository states no licence, so its author keeps all rights: ask jfwoods before reusing the design.

Similar boards

  • 2 layers72 × 109.5 mmTokenizer, the printed circuit board in 3D

    Tokenizer

    nikethLakshmanan/Tokenizer

    59 parts
  • 4 layers70.9 × 75 mmFlagshep Audio Player V1, the printed circuit board in 3D

    Flagshep Audio Player V1

    HYLDAIC/Flagshep-V1_Audio-Player

    A high fidelity semi-portable audio player student project with provided PCB files, 3D files and Code

    77 parts1CERN-OHL-P-2.0
  • 4 layers108.6 × 87.7 mmBlinky badge light, the printed circuit board in 3D

    Blinky badge light

    rowr111/blinky-badge-light

    lighting badge with unique patterns and sound reactivity

    123 parts9MIT
  • 4 layers60 × 60 mm3117 AUSIGN, the printed circuit board in 3D

    3117 AUSIGN

    r4hulrr/ausign

    Real-time Auslan (Australian Sign Language) to English speech translation glove. Built with ESP32-S3, flex sensors, IMU on a custom PCB, and a Bluetooth-connected Python GUI.

    151 parts3MIT
  • 4 layers51 × 77 mmBODAQS A8 board v1 0, the printed circuit board in 3D

    BODAQS A8 board v1 0

    benconnor1972/BODAQS

    Bicycle Open Data Acquisition System

    120 parts9
  • 2 layers55.6 × 57.2 mmYkb skadi pcb, the printed circuit board in 3D

    Ykb skadi pcb

    YarmanKB/ykb-skadi-pcb

    KiCad project for YKB Skadi boards

    74 parts7Apache-2.0
  • 4 layers24 × 42 mmAura a03, the printed circuit board in 3D

    Aura a03

    Avinash1286/pendent

    AURA: open wearable note-taking device development — PCB, printable case, firmware, Next.js/Convex notes, interactive 3D site and launch film.

    61 parts