

Insta360-joystick
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
5ESP32-S3-WROOM-1-N8
SMD,25.5x18mm-40℃~+85℃ -98.2dBm 2.4GHz 20.5dBm 355mA 3V~3.6V 95mA Bluetooth5.0 ESP32-S3 Chip On-board PCB Antenna UART、SPI、I2C、I2S、USB SMD,25.5x18mm WiFi Modules ROHSEspressif SystemsWiFi ModulesAP2112K-3.3
SOT253.3V 600mA CMOS Low Dropout Linear Regulator with EnableDiodes IncorporatedUncategorisedBQ25185DLHR
WSON-10-EP(2.2x2)-40℃~+125℃ 1 1A 2.2V 3V~18V 4.4V 4uA Charging IC Lithium Battery、LiFePO4 Battery Support WSON-10-EP(2.2x2) Battery Management ROHSTexas InstrumentsBattery ManagementMAX17048G+T10
DFN-8-EP(2x2)-40℃~+85℃ 1 2.5V~4.5V 3uA I2C Lithium Battery Not supported Not supported DFN-8-EP(2x2) Battery Management ROHSMaxim Integrated (Analog Devices)Battery ManagementSP0503BAHTG
SOT-143-55℃~+125℃ 10A@8/20us 14V 150W@8/20us 30pF、60pF 5.5V 6V 80nA ESD IEC 61000-4-2、IEC 61000-4-5、IEC 61000-4-4 Three channels Unidirectional SOT-143 ESD and Surge Protection (TVS/ESD) ROHSTECH PUBLICESD And Surge Protection (TVS/ESD)
README
README.mdInsta360 Link Controller
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:
- A CLI client
linkctl - 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.
[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:
tar xzf linkctl-daemon-*-macos-*.tar.gz
cd linkctl-daemon
./install.shOption B: Build from source
# 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-daemonThe 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:
tar xzf linkctl-*-macos-*.tar.gz
sudo cp linkctl/linkctl /usr/local/bin/Option B: Build from source
cd host/linkctl
mkdir -p build && cd build
cmake ..
make
# Optional: install system-wide
sudo cmake --install . --prefix /usr/localOr run directly from the build directory: ./build/linkctl <command>.
3. Flash ESP32 firmware (if using joystick controller)
Requires PlatformIO.
pio run --target upload4. Configure WiFi on the ESP32 (if using joystick controller)
On first boot, the ESP32 starts a captive portal:
- Connect to the Insta360-Controller WiFi network from your phone or laptop
- A setup page opens automatically — enter your WiFi network name and password
- The ESP32 saves the credentials and reboots onto your network
Alternatively, configure via the serial CLI (115200 baud):
wifi MyNetwork MyPassword
connect5. 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)
| Dependency | Source | Purpose |
|---|---|---|
| libwebsockets | Homebrew | WebSocket server |
| cJSON | Homebrew | JSON parsing |
| OpenSSL 3 | Homebrew | Required by libwebsockets |
| IOKit | macOS (built-in) | USB device access |
| CoreFoundation | macOS (built-in) | System data structures |
| dns_sd | macOS (built-in) | mDNS advertisement |
CLI (linkctl)
No external dependencies — pure POSIX C.
ESP32 Firmware
| Dependency | Source | Purpose |
|---|---|---|
| arduinoWebSockets | PlatformIO (links2004/WebSockets@^2.4.1) | WebSocket client |
| ArduinoJson | PlatformIO (bblanchon/ArduinoJson@^7.0.0) | JSON encoding/parsing |
| PlatformIO | CLI or VS Code extension | Build toolchain (Arduino framework) |
Daemon Options
./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 exitCLI 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
| Command | Description |
|---|---|
linkctl help | Show usage information |
linkctl status | Show camera connection state and current zoom |
linkctl center | Center the gimbal and reset zoom to 1.0x |
linkctl zoom <1.0-4.0> | Set zoom level (e.g., linkctl zoom 2.5) |
linkctl zoom | Show the current zoom level |
linkctl stop | Stop all gimbal movement |
linkctl jog | Enter interactive pan/tilt mode |
Jog Mode
linkctl jog enters an interactive mode for real-time camera control using keyboard input.
| Key | Action |
|---|---|
W | Tilt up |
S | Tilt down |
A | Pan left |
D | Pan right |
+ / = | Increase speed |
- | Decrease speed |
| Space | Stop movement |
C | Center gimbal |
Q / ESC | Exit 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.
| Command | Description |
|---|---|
wifi <ssid> [pass] | Set WiFi credentials |
connect | Connect to WiFi and daemon |
disconnect | Disconnect |
status | Show connection state |
stop | Stop gimbal movement |
center | Center the gimbal |
joy | Test joystick output (5 sec) |
calibrate | Calibrate joystick center point |
invertx / inverty | Toggle axis inversion |
portal | Start captive portal for WiFi setup |
webconfig | Start web config on current network |
host <ip> | Override daemon IP (skip mDNS) |
port <port> | Override daemon port |
clear | Clear all saved config |
reboot | Restart 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
linkctlCLI client (newline-terminated text commands)
WebSocket Commands (ESP32 → Daemon)
{"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)
{"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.
# 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-motionHardware
ESP32-WROOM-32 (Breadboard Prototype)
| Function | GPIO | Notes |
|---|---|---|
| Joystick X (pan) | 34 | ADC1, input only |
| Joystick Y (tilt) | 35 | ADC1, input only |
| Joystick button (home) | 32 | ADC1, internal pull-up, active low |
| Zoom potentiometer | 33 | ADC1 |
| I2C SDA | 21 | Hardware I2C default |
| I2C SCL | 22 | Hardware I2C default |
| Calibrate button | 25 | Internal pull-up, active low |
| Spare button | 26 | Internal pull-up, active low |
| Status LED | 2 | On-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)
| Function | GPIO | Notes |
|---|---|---|
| Joystick X (pan) | 4 | ADC1 |
| Joystick Y (tilt) | 5 | ADC1 |
| Joystick button (home) | 13 | Internal pull-up, active low |
| Zoom potentiometer | 6 | ADC1 |
| I2C SDA (OLED + fuel gauge) | 8 | S3 default I2C SDA |
| I2C SCL (OLED + fuel gauge) | 9 | S3 default I2C SCL |
| Calibrate button | 14 | Internal pull-up, active low |
| Spare button | 15 | Internal pull-up, active low |
| Status LED (connectivity) | 16 | External LED |
| Status LED (TBD) | 17 | External LED |
| Status LED (TBD) | 18 | External LED |
| USB D- | 19 | Native USB (reserved) |
| USB D+ | 20 | Native 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
├── 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 advertisementGenAI 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| Qty | Value | Footprint | References |
|---|---|---|---|
| 5 | 1 uF | Capacitor_SMD:C_0603_1608Metric | C2, C4, C5, C6, C7 |
| 4 | 10 kOhm | Resistor_SMD:R_0603_1608Metric | R7, R8, R12, R13 |
| 4 | MountingHole | MountingHole:MountingHole_2.2mm_M2_Pad_TopBottom | H1, H2, H3, H4 |
| 2 | 0.1 uF | Capacitor_SMD:C_0603_1608Metric | C1, C9 |
| 2 | 10 Ohm | Resistor_SMD:R_0603_1608Metric | R9, R11 |
| 2 | 5.1 kOhm | Resistor_SMD:R_0603_1608Metric | R1, R2 |
| 2 | 65 Ohm | Resistor_SMD:R_0603_1608Metric | R6, R10 |
| 2 | RED | Diode_SMD:D_1206_3216Metric | D2, D4 |
| 1 | /CE | TestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mm | TP7 |
| 1 | 10 uF | Capacitor_SMD:C_0603_1608Metric | C3 |
| 1 | 13 kOhm | Resistor_SMD:R_0603_1608Metric | R5 |
| 1 | 22 uF | Capacitor_SMD:C_0603_1608Metric | C8 |
| 1 | 3 MOhm | Resistor_SMD:R_0603_1608Metric | R3 |
| 1 | 300 Ohm | Resistor_SMD:R_0603_1608Metric | R4 |
| 1 | 3V3 | TestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mm | TP3 |
| 1 | AP2112K-3.3 | Package_TO_SOT_SMD:SOT-23-5 | U3 |
| 1 | BAT_ALRT | TestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mm | TP8 |
| 1 | BLUE | Diode_SMD:D_1206_3216Metric | D3 |
| 1 | BQ25185DLHR | INSTA360:WSON10_DLH_TEX | U2 |
| 1 | Battery | INSTA360:18650_Holder | BT1 |
| 1 | ESP32-S3-WROOM-1 | RF_Module:ESP32-S3-WROOM-1 | U5 |
| 1 | GND | TestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mm | TP1 |
| 1 | GREEN | Diode_SMD:D_1206_3216Metric | D5 |
| 1 | MAX17048G+T10 | INSTA360:21-0168E_T822-3_MXM | U1 |
| 1 | POT_X | Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical | J3 |
| 1 | POT_Y | Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical | J4 |
| 1 | PinHeader_1x04_P2.54mm_Vertical | Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical | U4 |
| 1 | SP0503BAHT | Package_TO_SOT_SMD:SOT-143 | D1 |
| 1 | STAT1 | TestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mm | TP6 |
| 1 | STAT2 | TestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mm | TP9 |
| 1 | SW_AUX | INSTA360:ME02150F3502 | SW2 |
| 1 | SW_BOOT | Button_Switch_SMD:SW_Push_SPST_NO_Alps_SKRK | SW3 |
| 1 | SW_CAL | INSTA360:ME02150F3502 | SW4 |
| 1 | SW_PWR | INSTA360:SW_L101011ML04Q | SW1 |
| 1 | SW_RST | Button_Switch_SMD:SW_Push_SPST_NO_Alps_SKRK | SW5 |
| 1 | TestPoint_Small | TestPoint:TestPoint_Bridge_Pitch2.0mm_Drill0.7mm | TP10 |
| 1 | UART_RX | TestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mm | TP12 |
| 1 | UART_TX | TestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mm | TP11 |
| 1 | USB_C_Receptacle_USB2.0_14P | Connector_USB:USB_C_Receptacle_GCT_USB4105-xx-A_16P_TopMnt_Horizontal | J1 |
| 1 | VBATT | TestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mm | TP2 |
| 1 | VBUS | TestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mm | TP4 |
| 1 | VCC | TestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mm | TP5 |
| 1 | YAW_BUTTON | Connector_PinHeader_1.27mm:PinHeader_1x05_P1.27mm_Vertical | J2 |
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 mm

- 4 layers70.9 × 75 mm

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 mm

Blinky badge light
rowr111/blinky-badge-light
lighting badge with unique patterns and sound reactivity
123 parts9MIT - 4 layers60 × 60 mm

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 layers93 × 40 mm

- 4 layers51 × 77 mm

- 2 layers55.6 × 57.2 mm

- 4 layers24 × 42 mm

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