# LLUAV Developer SDK

A multi-language developer SDK for connecting to the **LLUAV** drone firmware via its built-in WebSocket server.

📚 **Documentation**
- [Quick Start](#python-sdk) (below)
- [Protocol Specification](docs/PROTOCOL.md) — Wire format, command reference, error codes
- [Architecture](docs/ARCHITECTURE.md) — Queue, reconnect, heartbeat, periodic tasks internals
- [API Reference](docs/API_REFERENCE.md) — Complete method list for all languages
- [Troubleshooting](docs/TROUBLESHOOTING.md) — Common issues and tuning guide
- [Changelog](docs/CHANGELOG.md) — Version history

## Protocol Overview

The drone firmware exposes a WebSocket server (default port `53001`). Clients send **plain text commands** and receive **JSON responses**.

- **Request format**: `cmd [args]`  
  Example: `takeoff`, `set_sp_ned 1.0:2.0:-3.0:0.5`, `mission 1001:/path/to/plan.json`
- **Response format**:
  ```json
  {
    "ret": 0,
    "cmd": 52,
    "ts": 1234567890,
    ...
  }
  ```
  - `ret == 0`: success
  - `ret < 0`: error, `msg` field contains the error description
- **Authentication**: If password protection is enabled, send `login <password>` first.

## SDK Features

All three language implementations share the same robust core features:

| Feature | Description |
|---------|-------------|
| **Auto Login** | Automatically sends `login` after establishing the connection if a password is configured. |
| **Auto Reconnect** | Detects connection loss and reconnects with **exponential backoff** (configurable initial delay, max delay, max retries). Re-authenticates after reconnect. |
| **Connection Keep-Alive** | Configurable heartbeat that sends lightweight commands when the queue is idle, keeping NAT/firewall sessions alive. |
| **Command Queue** | Strict **FIFO queue** guarantees one command in flight at a time (request/response serialization). New commands are queued locally until the previous one completes. |
| **Timeout & Retry** | Each command has its own deadline. On timeout or send failure, the command is retried up to `max_retries` times before failing. |
| **Periodic Tasks** | Register named tasks that are automatically enqueued at a fixed interval (e.g. poll `state` every 2 seconds). |

## Project Structure

```
sdk/
├── README.md                         # This file
├── python/                           # Python SDK (sync + async)
├── cpp/                              # C++ SDK (websocketpp + nlohmann/json)
└── js/                               # JavaScript/Node.js SDK (ws / native WebSocket)
```

---

## Python SDK

### Installation

```bash
cd sdk/python
pip install -e .
# Or with async support:
pip install -e ".[async]"
```

Dependencies:
- `websocket-client` (required, sync)
- `websockets` (optional, async)

### Quick Start

```python
from lluav_sdk import LLUAVClient

client = LLUAVClient(
    "192.168.1.100",
    password="your_password",
    auto_reconnect=True,
    heartbeat_interval=5.0,
)
client.connect()

# Register a periodic task: poll state every 2s
client.add_periodic_task("status", 2.0, "state")

# Commands are queued and executed one-by-one
state = client.get_state()
client.takeoff()
client.set_sp_ned(1.0, 2.0, -3.0, 0.5)
client.rtl()

client.remove_periodic_task("status")
client.disconnect()
```

### Non-blocking Commands

`send_cmd` returns a `concurrent.futures.Future` when `block=False`:

```python
future = client.send_cmd("state", block=False)
# ... do other work ...
result = future.result(timeout=10)
```

### Async Quick Start

```python
import asyncio
from lluav_sdk import LLUAVAsyncClient

async def main():
    client = LLUAVAsyncClient("192.168.1.100", password="your_password")
    await client.connect()
    await client.add_periodic_task("sysinfo", 3.0, "sysinfo")
    state, ver = await asyncio.gather(client.get_state(), client.get_version())
    await client.disconnect()

asyncio.run(main())
```

### Demos

```bash
# Real-time state monitor with periodic polling
python demos/demo_state_monitor.py --host 192.168.1.100 --password your_password

# Automated flight mission (preflight, takeoff, setpoint, RTL)
python demos/demo_flight_mission.py --host 192.168.1.100 --password your_password

# Camera & map management
python demos/demo_camera_map.py --host 192.168.1.100 --password your_password
```

---

## C++ SDK

### Dependencies

- [websocketpp](https://github.com/zaphoyd/websocketpp) (header-only)
- [nlohmann/json](https://github.com/nlohmann/json) (header-only)
- CMake >= 3.14
- C++14 compiler

Ubuntu/Debian installation:
```bash
sudo apt install libwebsocketpp-dev nlohmann-json3-dev cmake g++
```

### Build

```bash
cd sdk/cpp
mkdir build && cd build
cmake ..
make -j$(nproc)
```

### Quick Start

```cpp
#include "lluav_sdk/client.hpp"
#include <iostream>
#include <thread>

int main() {
    // Default: auto_reconnect=true, heartbeat=5s, timeout=10s
    lluav::LLUAVClient client("192.168.1.100", 53001, "your_password");
    client.connect();

    // Poll lidar pose every 3 seconds
    client.add_periodic_task("pose", 3, "lidarpose");

    auto state = client.get_state();
    std::cout << state.dump(2) << std::endl;

    client.takeoff();
    client.rtl();

    client.remove_periodic_task("pose");
    client.disconnect();
    return 0;
}
```

### Demos

Build with `-DLLUAV_SDK_BUILD_DEMOS=ON` (default ON), then run:

```bash
./demo_state_monitor   192.168.1.100 53001 your_password
./demo_flight_mission  192.168.1.100 53001 your_password
./demo_camera_map      192.168.1.100 53001 your_password [map_name]
```

---

## JavaScript / Node.js SDK

### Installation

```bash
cd sdk/js
npm install
```

### Quick Start (Node.js)

```javascript
const { LLUAVClient } = require('./src/client');

async function main() {
    const client = new LLUAVClient('192.168.1.100', 53001, 'your_password', 10000, {
        autoReconnect: true,
        heartbeatInterval: 5000,
    });
    await client.connect();

    // Poll state every 2 seconds
    client.addPeriodicTask('status', 2, 'state');

    const state = await client.getState();
    console.log(JSON.stringify(state, null, 2));

    await client.takeoff();
    await client.rtl();

    client.removePeriodicTask('status');
    await client.disconnect();
}

main().catch(console.error);
```

### Demos

```bash
# Real-time state monitor
node demos/demo_state_monitor.js 192.168.1.100 53001 your_password

# Automated flight mission
node demos/demo_flight_mission.js 192.168.1.100 53001 your_password

# Camera & map management
node demos/demo_camera_map.js 192.168.1.100 53001 your_password [map_name]
```

### Browser Usage

The SDK also works in modern browsers using the native `WebSocket` API. Bundle `src/client.js` with your preferred bundler (Webpack, Rollup, Vite, etc.) or load it as an ES module.

See `js/examples/browser_example.html` for a reference.

---

## Constructor Options

| Option | Python | C++ | JS | Default | Description |
|--------|--------|-----|-----|---------|-------------|
| `host` | ✓ | ✓ | ✓ | `localhost` | Drone IP/hostname |
| `port` | ✓ | ✓ | ✓ | `53001` | WebSocket port |
| `password` | ✓ | ✓ | ✓ | `null/""` | Login password |
| `timeout` | ✓ | ✓ | ✓ | `10s` | Default command timeout |
| `auto_reconnect` | ✓ | ✓ | ✓ | `true` | Enable auto-reconnect |
| `reconnect_interval` | ✓ | ✓ | ✓ | `1s` | Initial reconnect delay |
| `max_reconnect_interval` | ✓ | ✓ | ✓ | `30s` | Max reconnect delay (backoff cap) |
| `max_retries` | ✓ | ✓ | ✓ | `3` | Max retries per command |
| `heartbeat_interval` | ✓ | ✓ | ✓ | `5s` | Heartbeat interval (`0` to disable) |
| `heartbeat_cmd` | ✓ | ✓ | ✓ | `state` | Command used for heartbeat |

---

## Periodic Task API

All SDKs provide `add_periodic_task` / `remove_periodic_task`:

**Python**
```python
client.add_periodic_task("poll_state", 2.0, "state")
client.remove_periodic_task("poll_state")
```

**C++**
```cpp
client.add_periodic_task("poll_state", 2, "state");
client.remove_periodic_task("poll_state");
```

**JavaScript**
```javascript
client.addPeriodicTask('pollState', 2, 'state');
client.removePeriodicTask('pollState');
```

Periodic tasks are enqueued like normal commands, so they respect the strict request/response order and never interrupt an in-flight user command.

---

## Supported Commands

All SDKs expose a consistent high-level API covering the following categories:

| Category | Methods |
|----------|---------|
| **Authentication** | `login`, `adminLock`/`admin_unlock`, `adminUnlock`/`admin_unlock` |
| **State & Info** | `get_state`/`getState`, `get_param`/`getParam`, `get_version`/`getVersion`, `get_sysinfo`/`getSysinfo`, `get_disk_usage`/`getDiskUsage`, `get_statics`/`getStatics`, `get_machine_id`/`getMachineId`, `get_hud1`/`getHud1`, `get_lidar_pose`/`getLidarPose`, `get_apron_state`/`getApronState`, `get_mavlink_msgs`/`getMavlinkMsgs`, `get_submap_info`/`getSubmapInfo` |
| **Flight Control** | `takeoff`, `land`, `rtl`, `mission_rtl`/`missionRtl`, `tether_land`/`tetherLand`, `set_sp_ned`/`setSpNed`, `relocate_pos`/`relocatePos`, `tunnel_fly`/`tunnelFly`, `disarm`, `sleep`, `wakeup`, `auto_sleep_enable`/`autoSleepEnable`, `zyhk` |
| **Mission Management** | `mission`, `start_plan`/`startPlan`, `unlock_plan`/`unlockPlan`, `mission_wp_list`/`missionWpList`, `mission_wp_exec`/`missionWpExec`, `ai1_mission`/`ai1Mission`, `obs_mission`/`obsMission`, `dump_mission_result`/`dumpMissionResult`, `dump_mission_list`/`dumpMissionList`, `abort_report`/`abortReport`, `set_taskname`/`setTaskname` |
| **Map Management** | `save_map`/`saveMap`, `set_map_locate`/`setMapLocate`, `set_map_path`/`setMapPath`, `list_maps`/`listMaps`, `rm_map`/`rmMap` |
| **Camera & Recording** | `capture`, `snapshot`, `record_toggle`/`recordToggle`, `savebag` |
| **SIYI Camera** | `siyi_reboot`/`siyiReboot`, `siyi_format`/`siyiFormat`, `siyi_center`/`siyiCenter`, `siyi_init`/`siyiInit`, `siyi_downvideo`/`siyiDownvideo`, `siyi_sync`/`siyiSync` |
| **LED Control** | `led_on`/`ledOn`, `led_off`/`ledOff`, `led_enable`/`ledEnable`, `led_disable`/`ledDisable`, `led_pwm`/`ledPwm`, `led_blink`/`ledBlink` |
| **Parameter Management** | `set_param`/`setParam`, `get_param_value`/`getParamValue`, `reset_params`/`resetParams`, `save_params`/`saveParams`, `change_password`/`changePassword` |
| **Apron** | `set_apron_vol`/`setApronVol`, `set_apron_amp`/`setApronAmp` |
| **System** | `reboot_node`/`rebootNode`, `reboot_ap`/`rebootAp`, `reboot_system`/`rebootSystem`, `poweroff_system`/`poweroffSystem`, `prepare_poweroff`/`preparePoweroff`, `reboot_dlo`/`rebootDlo`, `restart_dlo`/`restartDlo`, `kill_livox`/`killLivox` |
| **Lidar & Perception** | `lidar_onoff`/`lidarOnoff`, `lidar_warn`/`lidarWarn`, `utg_get`/`utgGet`, `utg_set`/`utgSet` |
| **Other** | `grabber_ctrl`/`grabberCtrl`, `ara8_center`/`ara8Center`, `ara8_pitch`/`ara8Pitch`, `cv_test`/`cvTest`, `preflight_check`/`preflightCheck`, `preflight_enable`/`preflightEnable`, `clear_bags`/`clearBags`, `clear_images`/`clearImages`, `list_imagedir`/`listImagedir`, `rm_imagedir`/`rmImagedir`, `test_wp`/`testWp`, `test_wp_list`/`testWpList`, `show_msg`/`showMsg`, `save_msg`/`saveMsg`, `bms_setpower`/`bmsSetpower`, `bms_setcharge`/`bmsSetcharge`, `show_setpoint`/`showSetpoint`, `show_ap_guid2`/`showApGuid2`, `help` |

> **Naming convention**: Python uses `snake_case`; C++ uses `snake_case`; JavaScript uses `camelCase`.

---

## Error Handling

All SDKs throw/raise on connection failures and command errors.

- **Connection errors**: WebSocket cannot be established or was lost (and reconnect failed).
- **Command errors**: Drone responded with `ret != 0`, or the command timed out / exceeded max retries. The error object includes the `ret` code and the full JSON response.

---

## License

MIT
