Skip to content

Intrepid Simulator API

Introduction

The Intrepid AI Python SDK allows developers to interact with the Intrepid platform (graph and engine), enabling the creation and management of nodes, callbacks, and Quality of Service (QoS) policies.

This documentation provides a guide on how to use the Intrepid AI Python SDK to attach a callback function to an Intrepid execution graph.

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, require('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 Lua 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.

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:

Work in Progress

Network API

We use our own implementation of Centrifuge 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.

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:

// 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()

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:

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()

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 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 to simulator console
  • map - spawn and query map objects
  • object - operations on a single object
  • session - 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:

import { Centrifuge } from 'npm:centrifuge'
async function main() {
let client = new Centrifuge('ws://localhost:9120/connection/websocket', {
data: { use_commit: true },
})
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()