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:
- Scripting API (embedded Lua scripts)
- 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
local log = require("log")local map = require("map")local session = require("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 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 consoleimport { 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()
# run `pip install centrifuge-python` to install library,# then run simulator, and `python3 test.py` in another console
import asynciofrom 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:
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()
import asynciofrom 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 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 consolemap
- spawn and query map objectsobject
- operations on a single objectsession
- 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:
- you subscribe to
sync
channel - you receive
22_000_000
(that means current simulation time is 22 seconds at 1408th tick) - you respond with
22_555_555
(bit more than 22.5 seconds) - simulator runs until then and stops when simulation time is more or equal to the given number
- 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()