- [Overview](#overview)
- [Scripting API](#scripting-api)
- [Network API](#network-api)
  - [RPC calls](#rpc-calls)
  - [Subscriptions](#subscriptions)
- [Coordinate systems](#coordinate-systems)
- [API reference](#api-reference)
- [Synchronization](#synchronization)

## Overview

There are two ways to control the simulation via API:

1. Scripting API (embedded Lua scripts)
2. Network API (WebSocket JSON-RPC)

API is very similar in both cases. As an example, `sim.log.warn('Hello, world!')` call in Lua script is equivalent to `client.rpc('log.warn', 'Hello, world!')` JSON-RPC call.

However use-cases for them might differ. Lua scripting is used to customize drone configuration, create maps and scenarios. Network API is used to control drones with external stack and extract telemetry data. Ultimately, Lua scripting is faster and more convenient, while Network API is a lot more flexible.

## Scripting API

We use [Luau](https://luau.org/) scripting language because it's very lightweight and designed to be embeddable in other software.

*Note: Two other options, Python and JS, were considered, but turned out to be too clunky and hard to work with (Python is not embeddable, requires shipping dll and non-trivial configuration, and among JS interpreters, V8 is too heavy, Boa wasn't complete at the time of writing). There're also couple of small embeddable languages like Rhai and Rune, but they are much less known than Lua. So Lua just turned out to be the best option, once you get your mind around indices starting from 1 that is. Luau was chosen over Lua because of security and gradual type system.*

If you run simulator using `./intrepid-sim --script script.lua`, that script will be executed on startup.

Here is an example of a script that spawns a tree and pauses the simulation:

```lua
local log = sim.log
local map = sim.map
local session = sim.session

log.warn("Hello, world!")
map.spawn({
    mesh = "trees/tree_a.glb",
    position = { x = 1.5, y = 0 },
})
session.pause()
```

## Network API

We use our own implementation of [Centrifuge](https://centrifugal.dev/docs/transports/client_protocol) protocol. It's basically a JSON-RPC protocol over WebSocket.

You can use any Centrifuge client (official clients are available for JS, Go, Dart, Swift, Java and Python), see links [here](https://centrifugal.dev/docs/transports/client_sdk).

### RPC calls

Here is an example of a script that does the same as the Lua script above (spawns a tree, pauses the simulation) using JavaScript:

```js
// run simulator, then run `deno run -A test.js` in another console
import { Centrifuge } from 'npm:centrifuge'

async function main() {
    let client = new Centrifuge('ws://localhost:9120/connection/websocket')
    await client.connect()

    await client.rpc('session.restart')
    await client.rpc('session.pause')

    await client.rpc('log.warn', 'Hello, world!')
    await client.rpc('map.spawn', {
        mesh: 'trees/tree_a.glb',
        position: { x: 1.5, y: 0 },
    })

    await client.disconnect()
}

await main()
```

And here is the same example using Python.

```python
# run `pip install centrifuge-python` to install library,
# then run simulator, and `python3 test.py` in another console

import asyncio
from centrifuge import Client

async def main():
    client = Client('ws://localhost:9120/connection/websocket')
    await client.connect()

    await client.rpc('session.restart', None)
    await client.rpc('session.pause', None)

    await client.rpc('log.warn', 'Hello, world!')
    await client.rpc('map.spawn', {
        'mesh': 'trees/tree_a.glb',
        'position': { 'x': 1.5, 'y': 0 },
    })

    await client.disconnect()

asyncio.run(main())
```

Note that for RPC calls with no arguments, in Python you need to pass `None`, while in JS you don't. All RPC examples in this document, unless told otherwise, use JS syntax for brevity.

### Subscriptions

Using Network API, you can also subscribe to the output of many functions that are used to retrieve data (those functions typically don't take input arguments and don't change world state). If you do, you will receive output of that function when you subscribe, and then once every physics tick.

Here is an example of subscribing to an object position using JavaScript:

```js
import { Centrifuge } from 'npm:centrifuge'

async function main() {
    let client = new Centrifuge('ws://localhost:9120/connection/websocket')
    await client.connect()

    let vehicles = await client.rpc('map.list_vehicles')
    let vehicle = Object.entries(vehicles.data).find(([_, vehicle]) =>
        vehicle.robot_id === 0
    )[0]

    if (!vehicle) {
        console.log('No vehicle found')
        return
    }

    let sub = client.newSubscription(`object_${vehicle}.position`)
    sub.on('publication', msg => {
        console.log('drone position:', msg.data)
    })
    sub.subscribe()
}

await main()
```

And here is the same example using Python.

```python
import asyncio
from centrifuge import Client, SubscriptionEventHandler, PublicationContext

async def main():
    client = Client('ws://localhost:9120/connection/websocket')
    await client.connect()

    response = await client.rpc('map.list_vehicles', None)
    vehicles = response.data
    vehicle = next((v for v in vehicles if vehicles[v]['robot_id'] == 0), None)

    if not vehicle:
        print('No vehicle found')
        return

    class EventHandler(SubscriptionEventHandler):
        async def on_publication(self, ctx: PublicationContext) -> None:
            print("drone position:", ctx.pub.data)

    sub = client.new_subscription(f'object_{vehicle}.position', EventHandler())
    await sub.subscribe()


asyncio.ensure_future(main())
loop = asyncio.get_event_loop()
loop.run_forever()
```

## Coordinate systems

We use ENU (East-North-Up) coordinate system. Vehicles are pointing by default towards the rising sun (X-axis, East). So X vector is pointing forward, Y is left, Z is up.

Rotations and angular velocities are represented by bivectors (YZ, ZX, XY), and you should definitely check out [this video](https://www.youtube.com/watch?v=htYh-Tq7ZBI) to learn what those are. As an example, orientation `YZ=0.1` means that vehicle is rotating in the YZ plane, with positive direction going from Y to Z, by 0.1 radians. That is equivalent to 5.73 degrees roll of the vehicle to the right.

Note: orientation is usually defined by ZYX Euler angles (XY = yaw, ZX = pitch, YZ = roll, applied in that exact order), and then converted to quaternions internally.

Linear units of distance are meters, angular units are radians, time is in seconds. GPS coordinates are in degrees following lat, lon order. Time elapsed since start of simulation is usually given in API as a whole number of microseconds.

## API reference

There are 4 modules in the API:
 - [`log`](log.md) - log to simulator console
 - [`map`](map.md) - spawn and query map objects
 - [`object`](object.md) - operations on a single object
 - [`script`](script.md) - evaluate Lua scripts
 - [`session`](session.md) - control simulation

## Synchronization

When you use Network API, you may want your code to run in sync with the simulator regardless of the simulation speed.

This is achieved by subscribing to `sync` channel. If you're subscribed to it, you will receive a current timestamp (in microseconds), and you have to respond with future timestamp (in microseconds) until which you want simulator to run.

Example:

1. you subscribe to `sync` channel
2. you receive `22_000_000` (that means current simulation time is 22 seconds at 1408th tick)
3. you respond with `22_555_555` (bit more than 22.5 seconds)
4. simulator runs until then and stops when simulation time is more or equal to the given number
5. you receive `22_562_500` (~22.5 seconds at 1444th tick), and simulator is paused until you respond again

You can respond with timestamp you got plus 1 microsecond, this way, you can sync every tick. If you respond with the same time you got, simulation will be paused, and you'll not receive any ticks until you send future timestamp allowing simulation to advance.

Here is a minimal example requiring sync every second:

```js
import { Centrifuge } from 'npm:centrifuge'

async function main() {
    let client = new Centrifuge('ws://localhost:9120/connection/websocket')
    await client.connect()

    let sub = client.newSubscription('sync');
    sub.on('publication', (message) => {
        console.log(message)
        setTimeout(() => {
            sub.publish(message.data + 1_000_000)
        }, 1);
    });
    sub.subscribe();
}

await main()
```

For better reproduceability and to avoid race conditions, it might be a good idea to send controller commands at the same time as you send sync response (so while sim is running, you only collect data, but don't send any commands to it).
