Publish entities to Lattice
This page covers how to create and update different types of known objects, known as entities in Lattice,
with the Lattice SDK. For reference, see PublishEntity
(gRPC | HTTP).
Before you begin
To publish entities, set up your environment.
Publish an entity
Create and publish entity
objects:
-
Add the following code to your app that creates a single entity in Lattice and replace $YOUR_LATTICE_URL and $YOUR_BEARER_TOKEN with your information:
- gRPC SDKs
- HTTP SDKs
To create and update an entity, use the gRPC
PublishEntity
API.- C++
- Go
- Java
- JavaScript
- Python
- Rust
#include <thread>
#include <google/protobuf/timestamp.pb.h>
#include <google/protobuf/util/time_util.h>
#include <grpc/grpc.h>
#include <grpcpp/channel.h>
#include <grpcpp/create_channel.h>
#include <grpcpp/security/credentials.h>
#include <anduril/entitymanager/v1/entity_manager_api.pub.grpc.pb.h>
int main(int argc, char *argv[]) {
GOOGLE_PROTOBUF_VERIFY_VERSION;
auto url = "$YOUR_LATTICE_URL:443";
auto creds = grpc::SslCredentials(grpc::SslCredentialsOptions());
std::shared_ptr<anduril::entitymanager::v1::EntityManagerAPI::Stub> stub =
anduril::entitymanager::v1::EntityManagerAPI::NewStub(grpc::CreateChannel(url, creds));
double radius_degrees = 0.1;
double count = 0.;
google::protobuf::Timestamp start_time = google::protobuf::util::TimeUtil::GetCurrentTime();
while(true) {
grpc::ClientContext ctx;
// Setting custom metadata to be sent to the server
ctx.AddMetadata("authorization", "Bearer $YOUR_BEARER_TOKEN");
double t = count * M_PI / 180.0;
google::protobuf::Timestamp cur_time = google::protobuf::util::TimeUtil::GetCurrentTime();
google::protobuf::Timestamp expiry_time = cur_time;
expiry_time.set_seconds(cur_time.seconds() + 600); // entity expires in 10 minutes
anduril::entitymanager::v1::PublishEntityRequest req;
auto entity = req.mutable_entity();
// define the entity
entity->set_entity_id("C++ Entity (from docs)");
*entity->mutable_aliases()->mutable_name() = "C++ Entity";
*entity->mutable_created_time() = start_time;
*entity->mutable_expiry_time() = expiry_time;
entity->mutable_mil_view()->set_disposition(anduril::ontology::v1::DISPOSITION_FRIENDLY);
entity->mutable_location()->mutable_position()->set_latitude_degrees(33.69447852698943 +
(radius_degrees * std::cos(t)));
entity->mutable_location()->mutable_position()->set_longitude_degrees(-117.9173785693163 +
(radius_degrees * std::sin(t)));
entity->mutable_ontology()->set_template_(anduril::entitymanager::v1::TEMPLATE_ASSET);
entity->mutable_provenance()->set_integration_name("Anduril Docs");
entity->mutable_provenance()->set_data_type("ANDURIL_DOCS");
*entity->mutable_provenance()->mutable_source_update_time() = cur_time;
entity->set_is_live(true);
anduril::entitymanager::v1::PublishEntityResponse res;
grpc::Status status = stub->PublishEntity(&ctx, req, &res);
if (!status.ok()) {
std::cerr << "Error code: " << status.error_code() << std::endl;
std::cerr << "Error message: " << status.error_message() << std::endl;
break;
}
count += .1;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
return 0;
}package main
import (
"context"
"log"
"math"
"time"
entitymanagerv1 "github.com/anduril/lattice-sdk-go/src/anduril/entitymanager/v1"
ontologyv1 "github.com/anduril/lattice-sdk-go/src/anduril/ontology/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/protobuf/types/known/timestamppb"
)
// BearerTokenAuth supplies PerRPCCredentials from a given token.
type BearerTokenAuth struct {
Token string
}
// GetRequestMetadata gets the current request metadata, adding the bearer token.
func (b *BearerTokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (
map[string]string, error) {
return map[string]string{
"authorization": "Bearer " + b.Token,
}, nil
}
// RequireTransportSecurity indicates whether the credentials requires transport security.
func (b *BearerTokenAuth) RequireTransportSecurity() bool {
return true // or false if you are developing/testing without TLS
}
func main() {
ctx := context.Background()
bearerToken := "$YOUR_BEARER_TOKEN"
opts := []grpc.DialOption{
grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")),
grpc.WithPerRPCCredentials(&BearerTokenAuth{Token: bearerToken}),
}
conn, err := grpc.NewClient("$YOUR_LATTICE_URL:443", opts...)
if err != nil {
log.Fatalf("Did not connect: %v", err)
}
defer conn.Close()
em := entitymanagerv1.NewEntityManagerAPIClient(conn)
if err != nil {
log.Fatalf("Error opening publishing stream: %v", err)
}
startTime := time.Now().UTC()
count := 0.
radiusDegrees := .1
for {
t := count * math.Pi / 180
entity := &entitymanagerv1.Entity{
EntityId: "GoLang Entity (from docs)",
Aliases: &entitymanagerv1.Aliases{
Name: "GoLang Entity",
},
CreatedTime: timestamppb.New(startTime),
ExpiryTime: timestamppb.New(time.Now().Add(time.Minute * 10).UTC()),
MilView: &entitymanagerv1.MilView{
Disposition: ontologyv1.Disposition_DISPOSITION_FRIENDLY,
},
Location: &entitymanagerv1.Location{
Position: &entitymanagerv1.Position{
LatitudeDegrees: 33.69447852698943 +
(radiusDegrees * math.Cos(t)),
LongitudeDegrees: -117.9173785693163 +
(radiusDegrees * math.Sin(t)),
},
},
Ontology: &entitymanagerv1.Ontology{
Template: entitymanagerv1.Template_TEMPLATE_ASSET,
},
Provenance: &entitymanagerv1.Provenance{
IntegrationName: "Anduril Docs",
DataType: "ANDURIL_DOCS",
SourceUpdateTime: timestamppb.New(time.Now().UTC()),
},
IsLive: true,
}
// Create a PublishEntityRequest and call PublishEntity
req := &entitymanagerv1.PublishEntityRequest{Entity: entity}
response, err := em.PublishEntity(ctx, req)
if err != nil {
log.Fatalf("Error publishing entity: %v", err)
}
count += .1
time.Sleep(time.Millisecond * 10)
}
}
// MyEntityManagerClient.java
package org.example;
import java.util.concurrent.CountDownLatch;
import com.anduril.entitymanager.v1.Entity;
import com.anduril.entitymanager.v1.EntityManagerAPIGrpc;
import com.anduril.entitymanager.v1.EntityManagerAPIGrpc.EntityManagerAPIStub;
import com.anduril.entitymanager.v1.GetEntityRequest;
import com.anduril.entitymanager.v1.GetEntityResponse;
import com.anduril.entitymanager.v1.PublishEntityRequest;
import com.anduril.entitymanager.v1.PublishEntityResponse;
import io.grpc.ClientInterceptor;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.stub.MetadataUtils;
import io.grpc.stub.StreamObserver;
public class EntityManagerClient {
private EntityManagerAPIStub serviceStub;
public EntityManagerClient(){
ManagedChannel channel = ManagedChannelBuilder.forAddress("$YOUR_LATTICE_URL",
443).useTransportSecurity().build();
Metadata header = new Metadata();
Metadata.Key<String> key = Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);
String bearerToken = "$YOUR_BEARER_TOKEN";
header.put(key, "Bearer " + bearerToken);
ClientInterceptor interceptor = MetadataUtils.newAttachHeadersInterceptor(header);
this.serviceStub = EntityManagerAPIGrpc.newStub(channel).withInterceptors(interceptor);
}
...
// Publishes a given entity
public void publishEntity(Entity entity) {
PublishEntityRequest request = PublishEntityRequest.newBuilder().setEntity(entity).build();
this.serviceStub.publishEntity(request, new StreamObserver<PublishEntityResponse>() {
@Override
public void onNext(PublishEntityResponse value) {
}
@Override
public void onError(Throwable t) {
t.printStackTrace();
}
@Override
public void onCompleted() {
}
});
}
}Then in our main function we can push entities like so:
// main.java
import java.time.Instant;
import com.google.protobuf.util.Timestamps;
import com.anduril.entitymanager.v1.Entity;
import com.anduril.entitymanager.v1.Aliases;
import com.anduril.entitymanager.v1.MilView;
import com.anduril.entitymanager.v1.Location;
import com.anduril.ontology.v1.Disposition;
import com.anduril.entitymanager.v1.Ontology;
import com.anduril.entitymanager.v1.Template;
import com.anduril.entitymanager.v1.Provenance;
import com.anduril.entitymanager.v1.Position;
public static void main(String[] args) {
EntityManagerClient entityManager = new EntityManagerClient();
try {
double count = 0;
double radiusDegrees = .1;
while(true){
double t = Math.toRadians(count);
Instant nowUtc = Instant.now();
final int TEN_MIN_IN_MILLIS = 10 * 60 * 1000;
// define the entity
Entity entity = Entity.newBuilder()
.setEntityId("$ENTITY_ID") // Use a unique GUID when publishing a new entity to Lattice.
.setAliases(Aliases.newBuilder()
.setName("Java Entity").build())
.setCreatedTime(Timestamps.fromMillis((nowUtc.toEpochMilli())))
.setExpiryTime(Timestamps.fromMillis((nowUtc.toEpochMilli() + TEN_MIN_IN_MILLIS)))
.setMilView(MilView.newBuilder()
.setDisposition(Disposition.DISPOSITION_FRIENDLY)
.build())
.setLocation(Location.newBuilder()
.setPosition(Position.newBuilder()
// move entity in a circle
.setLatitudeDegrees(33.69447852698943 + (radiusDegrees * Math.cos(t)))
.setLongitudeDegrees(-117.9173785693163 + (radiusDegrees * Math.sin(t)))
.build())
.build())
.setOntology(Ontology.newBuilder()
.setTemplate(Template.TEMPLATE_ASSET)
.build())
.setProvenance(Provenance.newBuilder()
.setIntegrationName("Anduril Docs")
.setDataType("ANDURIL_DOCS")
.setSourceUpdateTime(Timestamps.fromMillis((nowUtc.toEpochMilli())))
.build())
.setIsLive(true)
.build();
// publish the entity to stream
entityManager.publishEntity(entity);
Thread.sleep(10);
count += .1;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}import { EntityManagerAPI } from "@anduril-industries/lattice-sdk/src/anduril/entitymanager/v1/entity_manager_grpcapi.pub_pb.js";
import { createGrpcTransport } from "@connectrpc/connect-node";
import { createClient } from "@connectrpc/connect";
import { Disposition } from "@anduril-industries/lattice-sdk/src/anduril/ontology/v1/type.pub_pb.js";
import { Template } from "@anduril-industries/lattice-sdk/src/anduril/entitymanager/v1/types.pub_pb.js";
const TEN_MINUTES_IN_MS = 10 * 60 * 1000;
const transport = createGrpcTransport({
// Requests will be made to <baseUrl>/<package>.<service>/method
baseUrl: "$YOUR_LATTICE_URL",
// You have to tell the Node.js http API which HTTP version to use.
httpVersion: "2",
// Interceptors apply to all calls running through this transport.
interceptors: [],
});
function msToProtoTimestamp(ms) {
const seconds = BigInt(Math.floor((ms || 0) / 1000));
const nanos = ((ms || 0) % 1000) * 1000;
return { seconds, nanos };
}
async function main() {
const client = createClient(EntityManagerAPI, transport);
const headers = new Headers();
headers.set("Authorization", "Bearer $YOUR_BEARER_TOKEN");
const startTime = msToProtoTimestamp(Date.now());
const radiusDegrees = 0.01;
let count = 0;
while (true) {
const t = (count * Math.PI) / 180;
// define the entity
const request = ({
entity: {
entityId: "Javascript Entity (from docs)",
aliases: {
name: "Javascript Entity",
},
createdTime: startTime,
expiryTime: msToProtoTimestamp(Date.now() + TEN_MINUTES_IN_MS),
milView: {
disposition: Disposition.FRIENDLY,
},
location: {
position: {
// move entity in a circle
latitudeDegrees: 33.69447852698943 + radiusDegrees * Math.cos(t),
longitudeDegrees: -117.9173785693163 + radiusDegrees * Math.sin(t),
},
},
ontology: {
template: Template.ASSET,
},
provenance: {
integrationName: "Anduril Docs",
dataType: "ANDURIL_DOCS",
sourceUpdateTime: msToProtoTimestamp(Date.now()),
},
isLive: true,
},
});
// Publish Entity
try {
const response = await client.publishEntity(request, { headers });
} catch (error) {
console.error('Error publishing entity:', error);
}
count += 1;
await new Promise((resolve) => setTimeout(resolve, 10));
}
}
void main();from anduril.entitymanager.v1 import EntityManagerApiStub, GetEntityRequest, PublishEntityRequest, Entity, Aliases, MilView, Location, Position, Ontology, Template, Provenance
from anduril.ontology.v1 import Disposition
from grpclib.client import Channel
from datetime import datetime, timezone, timedelta
import asyncio
import math
metadata = {
'authorization': 'Bearer $YOUR_BEARER_TOKEN'
}
async def publish_entity(entity):
channel = Channel(host="$YOUR_LATTICE_URL", port=443, ssl=True)
stub = EntityManagerApiStub(channel)
request = PublishEntityRequest(entity=entity)
response = await stub.publish_entity(request, metadata=metadata)
channel.close()
return response
async def main():
count = 0
radius_degrees = .01
creation_time = datetime.now(timezone.utc)
while (True):
timenow = datetime.now(timezone.utc)
count += .1
t = math.radians(count)
entity = Entity(
entity_id = "Python Entity (from docs)",
created_time=creation_time,
aliases=Aliases(name="Python Entity"),
expiry_time=timenow + timedelta(minutes=10),
mil_view=MilView(disposition=Disposition.FRIENDLY),
location=Location(
position=Position(
# move entity in a circle
latitude_degrees=33.69447852698943 + (radius_degrees * math.cos(t)),
longitude_degrees=-117.9173785693163 + (radius_degrees * math.sin(t)))),
ontology=Ontology(template=Template.ASSET),
provenance=Provenance(
integration_name="Anduril Docs",
data_type="ANDURIL_DOCS",
source_update_time=timenow),
is_live=True)
response = await publish_entity(entity)
await asyncio.sleep(.01)
if __name__ == "__main__":
asyncio.run(main())Before proceeding, add these dependencies to the Cargo.toml file:
anyhow = "1.0"
chrono = { version = "0.4", features = ["serde"] }
pbjson-types = "0.7.0"use lattice_sdk_rust::anduril::{
entitymanager::v1::{entity_manager_api_client::EntityManagerApiClient, Aliases, Entity,
Location, MilView, Ontology, Position, Provenance, PublishEntityRequest,
Template
};
use lattice_sdk_rust::anduril::ontology::v1::Disposition;
use anyhow::Result;
use pbjson_types::Timestamp;
use tokio::{time::interval};
use tonic::{metadata::MetadataValue, transport::{Channel, ClientTlsConfig}, Request};
use chrono::{self, Utc};
fn main() {
let rt = tokio::runtime::Builder::new_multi_thread()
.thread_name("tokio")
.enable_all()
.build()
.unwrap();
let entity_status_loop_task = rt.spawn(async move {
// send entities to stream instance
let _ = entity_status_loop().await;
});
rt.block_on(entity_status_loop_task).unwrap();
}
pub async fn entity_status_loop() -> Result<(), Box<dyn std::error::Error>> {
let token = "$YOUR_BEARER_TOKEN";
let bearer_token = format!("Bearer {}", token);
let header_value: MetadataValue<_> = bearer_token.parse()?;
let tls_config = ClientTlsConfig::new().with_native_roots();
let http_endpoint = format!("$YOUR_LATTICE_URL");
let registration_channel = Channel::from_shared(http_endpoint)?.tls_config(tls_config)?.connect().await?;
let mut em_client = EntityManagerApiClient::with_interceptor(registration_channel, |mut req: Request<()>| {
req.metadata_mut().insert("authorization", header_value.clone());
Ok(req)
});
let mut interval = interval(std::time::Duration::from_millis(10));
let start_time = Utc::now();
let mut count: f64 = 0.0;
let radius_degrees: f64 = 0.1;
loop {
let t = count.to_radians();
interval.tick().await;
// define the entity
let request = PublishEntityRequest { entity: Some(Entity {
entity_id: String::from("Rust Entity (from docs)"),
is_live: true,
location: Some(Location {
position: Some( Position {
latitude_degrees: 33.69447852698943 + (radius_degrees * f64::cos(t)),
longitude_degrees: -117.9173785693163 + (radius_degrees * f64::sin(t)),
..Default::default()
}),
..Default::default()
}),
aliases: Some(Aliases {
name: String::from("Rust Entity"),
..Default::default()
}),
ontology: Some(Ontology {
platform_type: String::from("ECHODYNE ECHOSHIELD"),
template: Template::Asset.into(),
..Default::default()
}),
provenance: Some(Provenance {
integration_name: String::from("Anduril Docs"),
data_type: String::from("ANDURIL_DOCS"),
source_update_time: Some(Timestamp::from(Utc::now())),
..Default::default()
}),
mil_view: Some(MilView {
disposition: Disposition::Friendly.into(),
..Default::default()
}),
created_time: Some(Timestamp::from(start_time)),
expiry_time: Some(Timestamp::from(Utc::now() + chrono::Duration::minutes(10))),
..Default::default()
})};
// Publish Entity
let response = em_client.publish_entity(request.clone()).await;
println!("{:?}", response);
count += 0.1;
}
}This code creates an entity that moves in a circle around Southern California and receives an update every 10 milliseconds.
Use the following curl example to publish an entity using the HTTP
/entities
endpoint. Replace $YOUR_LATTICE_URL and $YOUR_BEARER_TOKEN with your information:curl --location --request PUT "$YOUR_LATTICE_URL/api/v1/entities" \
--header "Content-Type: application/json" \
--header "Authorization: Bearer $YOUR_BEARER_TOKEN" \
--data '{
"entityId": "curl Entity (from docs)",
"aliases": {
"name": "curl entity"
},
"createdTime": "undefined",
"expiryTime": "1970-01-04T00:00:00.000Z",
"milView": {
"disposition": "DISPOSITION_FRIENDLY"
},
"location": {
"position": {
"latitudeDegrees": 33.69447852698943,
"longitudeDegrees": -117.9173785693163
}
},
"ontology": {
"template": "TEMPLATE_ASSET"
},
"provenance": {
"integrationName": "curl publishing example",
"dataType": "ANDURIL_DOCS",
"sourceUpdateTime": "undefined"
},
"isLive": true
}'
- Verify the published entity in the
$YOUR_LATTICE_URL
/c2 page.
Customize entities
Customize entities by modifying the entity
data:
-
Update the following required components of the entity model:
Component in entity
modelDescription entity_id
Unique string identifier. Can be a Globally Unique Identifier (GUID). expiry_time
Expiration time that must be greater than the current time and less than 30 days in the future. The Entities API will reject any entity update with an expiry_time
in the past. When theexpiry_time
has passed, the Entities API will delete the entity from the COP and send a DELETE event.is_live
Boolean that when true
, creates or updates the entity. Iffalse
and the entity is still live, triggers a DELETE event.provenance.integration_name
String that uniquely identifies the integration responsible for publishing the entity. provenance.data_type
String that identifies the source data type for the entity. Multiple integrations can publish entities of the same data type. provenance.source_update_time
Last modification time of the entity's data at the original source, which determines the acceptance of updates to an entity. aliases.name
Human-readable string that represents the name of an entity. For more information on entity component fields, see the
entity
object API reference. -
Set the entity template in the
ontology.template
field.This field indicates which panel in the Lattice UI (track, asset, or geo-entity panel) to display entity details and render icon types. For more conceptual information on tracks, assets, and other entities, see Entity shapes.
-
Depending on your entity shape, complete the following:
- Asset
- Track
- Geo-entity
To create an asset, define the
ontology.template
field with theTEMPLATE_ASSET
value, and add the requiredlocation
andmilView
fields to the entity.Asset
entity
exampleTo use the following asset entity, replace the UNIQUE_ENTITY_ID with a GUID for your object in Lattice and ensure the time for
ExpiryTime
andSourceUpdateTime
is current:- JSON
{
"entityId": "UNIQUE_ENTITY_ID",
"description": "Test asset",
"isLive": true,
"createdTime": "2024-12-07T00:09:42.816877Z",
"expiryTime": "2024-12-17T00:09:42.816878Z",
"location": {
"position": {
"latitudeDegrees": 50.91402185768586,
"longitudeDegrees": 0.79203612077257,
"altitudeHaeMeters": 2994,
"altitudeAglMeters": 2972.8
}
},
"aliases": {
"name": "Anduril Unmanned Surface Vessel"
},
"milView": {
"disposition": "DISPOSITION_FRIENDLY",
"environment": "ENVIRONMENT_SURFACE"
},
"ontology": {
"template": "TEMPLATE_ASSET"
},
"provenance": {
"integrationName": "Anduril",
"dataType": "Radar",
"sourceUpdateTime": "2024-12-07T00:09:42.816878Z"
}
}This sample adds an Entity representing a friendly surface vessel:
To create a track, define the
ontology.template
field with theTEMPLATE_TRACK
value, and add the requiredlocation
andmilView
fields to the entity.Track
entity
example- JSON
{
"entityId": "UNIQUE_ENTITY_ID",
"description": "Test track",
"isLive": true,
"createdTime": "2024-12-07T00:19:46.706195Z",
"expiryTime": "2024-12-07T00:24:46.706196Z",
"location": {
"position": {
"latitudeDegrees": 50.91402185768586,
"longitudeDegrees": 0.79203612077257,
"altitudeHaeMeters": 46.214997987295234
}
},
"aliases": {
"name": "Documentation Example Track"
},
"milView": {
"disposition": "DISPOSITION_FRIENDLY",
"environment": "ENVIRONMENT_AIR"
},
"ontology": {
"template": "TEMPLATE_TRACK"
},
"provenance": {
"integrationName": "Anduril",
"dataType": "Anduril Entity",
"sourceUpdateTime": "2024-12-07T00:19:47.706196Z"
}
}This sample adds an Entity representing a friendly airplane:
Create the following types of geo-entities by defining the
ontology.template
field with theTEMPLATE_GEO
value:Point
entity
exampleTo create a point, you need to also define the
GeoShape
andLocation
components.The following code creates a point representing an aircraft crash location and details about the incident:
- JSON
{
"entityId": "UNIQUE_ENTITY_ID",
"description": "Sample geopoint",
"isLive": true,
"createdTime": "2024-12-07T00:45:15.581592Z",
"expiryTime": "2024-12-07T00:50:15.581598Z",
"geoShape": {
"point": {
"position": {
"latitudeDegrees": 50.91402185768586,
"longitudeDegrees": 0.79203612077257,
"altitudeHaeMeters": 46.214997987295234,
"altitudeAglMeters": 0.5000067609670396
}
}
},
"geoDetails": {
"type": "GEO_TYPE_GENERAL"
},
"aliases": {
"name": "Documentation Sample Point"
},
"ontology": {
"template": "TEMPLATE_GEO"
},
"provenance": {
"integrationName": "Anduril",
"dataType": "Anduril Entity",
"sourceUpdateTime": "2024-12-07T00:45:15.581599Z"
}
}The image below shows this sample point within Lattice:
Polygon
entity
exampleYou can represent an area on the map as a polygon by defining the
polygon GeoShape
. Note the final position must be the same as the first position to close the polygon:- JSON
{
"entityId": "UNIQUE_ENTITY_ID",
"description": "Sample polygon",
"isLive": true,
"createdTime": "2024-12-07T00:55:43.183738Z",
"expiryTime": "2024-12-07T01:00:43.183743Z",
"geoShape": {
"polygon": {
"rings": [
{
"positions": [
{
"position": {
"latitudeDegrees": 49.01611140463143,
"longitudeDegrees": 1.5746513124955297
}
},
{
"position": {
"latitudeDegrees": 49.01924140463143,
"longitudeDegrees": 2.882469645828863
}
},
{
"position": {
"latitudeDegrees": 48.4172380712981,
"longitudeDegrees": 2.9189863124955298
}
},
{
"position": {
"latitudeDegrees": 49.01611140463143,
"longitudeDegrees": 1.5746513124955297
}
}
]
}
]
}
},
"geoDetails": {
"type": "GEO_TYPE_GENERAL"
},
"aliases": {
"name": "Documentation Sample Polygon"
},
"ontology": {
"template": "TEMPLATE_GEO"
},
"provenance": {
"integrationName": "Anduril",
"dataType": "Anduril Entity",
"sourceUpdateTime": "2024-12-07T00:55:43.183743Z"
}
}This can be used to represent polygon around a geographical area:
Ellipse
entity
exampleFor an ellipse-shaped entity around an area, you can create an
ellipse
geometric shape by defining its location and axes lengths:- JSON
{
"entityId": "UNIQUE_ENTITY_ID",
"description": "Sample ellipse",
"isLive": true,
"createdTime": "2024-12-07T01:06:32.834952Z",
"expiryTime": "2024-12-07T01:11:32.834954Z",
"location": {
"position": {
"latitudeDegrees": 51.46,
"longitudeDegrees": -0.16
}
},
"geoShape": {
"ellipse": {
"semiMajorAxisM": 20,
"semiMinorAxisM": 20,
"orientationD": 40
}
},
"geoDetails": {
"type": "GEO_TYPE_CONTROL_AREA"
},
"aliases": {
"name": "Documentation Sample Ellipse"
},
"ontology": {
"template": "TEMPLATE_GEO"
},
"provenance": {
"integrationName": "Anduril",
"dataType": "Anduril Entity",
"sourceUpdateTime": "2024-12-07T01:06:32.834955Z"
}
}This can be used to represent a keep out circle around an exercise area
Line
entity
exampleSeparate areas with the
line
geometric shape by defining at least one location:- JSON
{
"entityId": "UNIQUE_ENTITY_ID",
"description": "Sample line",
"isLive": true,
"createdTime": "2024-12-07T01:08:39.079826Z",
"expiryTime": "2024-12-07T01:13:39.079828Z",
"geoShape": {
"line": {
"positions": [
{
"latitudeDegrees": 47.605,
"longitudeDegrees": -122.329
},
{
"latitudeDegrees": 47.61,
"longitudeDegrees": -122.33
}
]
}
},
"geoDetails": {
"type": "GEO_TYPE_GENERAL"
},
"aliases": {
"name": "Documentation Sample Line"
},
"ontology": {
"template": "TEMPLATE_GEO"
},
"provenance": {
"integrationName": "Anduril",
"dataType": "Anduril Entity",
"sourceUpdateTime": "2024-12-07T01:08:39.079829Z"
}
}This can be used to represent an enemy line
Assign entity icons
For iconography within Lattice, you must provide an entity's specific_type
field and platform_type
field. See the following example iconography:
Platform Type - Car
Specific Type - ""
Platform Type - person
Specific Type - ""
Platform Type - Group1-2
Specific Type - Rotary Wing
Platform Type - Surface_Vessel
Specific Type - ""
Specify motion
The
location
component contains kinematic fields, including position, attitude, and velocity,
that represent the motion of entities over time. Third-party integrations are
responsible for providing all available kinematic field data.
Altitude
The data model contains four altitude references, specified in meters:
altitude_hae_meters
: The entity's height above the World Geodetic System 1984 (WGS84) ellipsoid.altitude_agl_meters
: The entity's height above the terrain. This is typically measured with a radar altimeter or by using a terrain tile set lookup.altitude_asf_meters
: The entity's height above the sea floor.pressure_depth_meters
: The depth of the entity from the surface of the water.
The data model does not currently support Mean Sea Level (MSL) references,
such as the Earth Gravitational Model 1996 (EGM-96) and the Earth Gravitational
Model 2008 (EGM-08). If the only altitude reference available to your
integration is MSL, convert it to Height Above Ellipsoid (HAE) and populate the
altitude_hae_meters
field. There are many open source libraries available (for
example: Go) that allow for this
conversion.
For example, to indicate to Lattice that a plane is flying at an altitude of 2,994 meters above the World Geodetic System 1984 (WGS-84) ellipsoid, you would populate an entity with the following data:
"location": {
"position": {
"latitudeDegrees": 42.2,
"longitudeDegrees": -71.1,
"altitudeHaeMeters": 2994.0,
"altitudeAglMeters": 2972.8
},
}
Attitude
The entity's attitude is represented by a quaternion in the attitude_enu
field. This representation is used to translate from the entity's body frame to
its East-North-Up (ENU) frame. If the attitude_enu
field isn't populated,
Lattice will use the velocity_enu
field to calculate the heading for display
in Lattice UI. If both fields are populated, Lattice will prioritize the value
from attitude_enu
.
For example, if your entity has a yaw value of 40 degrees, you would need to
convert to the attitude_enu
values with the following:
import math
from typing import List
from anduril.entitymanager.v1 import Entity, Location
def yaw_to_quaternion_enu(yaw_degrees: float) -> List[float]:
# Convert degrees to radians
yaw_rad = math.radians(90 - yaw_degrees) # Adjust NED yaw by 90 degrees for ENU
qw = math.cos(yaw_rad * 0.5)
qz = math.sin(yaw_rad * 0.5)
return [qw, 0, 0, qz]
quaternion = yaw_to_quaternion_enu(40)
entity = Entity(
location = Location(
attitude_enu = Quaternion(
w = quaternion[0],
x = quaternion[1],
y = quaternion[2],
z = quaternion[3],
)
)
)
Specify health
Use the health
component to define the health status that the entity reports.
This component should only be used for health status that the entity reports
back to you. For example, health status should be sources through telemetry or
health reports over a radio or network connection. You should not add a health component
to tracks whose telemetry or other health reporting data you do not have access to.
When an asset sends an update to the Entities API, the health.connection_status
field changes to CONNECTION_STATUS_ONLINE
. If there are no updates after one minute, the field value changes to CONNECTION_STATUS_OFFLINE
.
- JSON
"health": {
"healthStatus": "HEALTH_STATUS_WARN",
"components": [
{
"id": "atc-radar-calibration",
"name": "Air Traffic Control Radar Calibration",
"health": "HEALTH_STATUS_WARN",
"messages": [
{
"status": "HEALTH_STATUS_WARN",
"message": "Calibration has not been completed in 30 days"
}
]
}
]
}
ADS-B
To indicate that an Entity has an ADS-B transponder, there are a few components on the Entity that can be set. Specifically:
Transponder Codes The Transponder Code mode should be sent depending on if it is a military or civilian aircraft. For civilian aircraft it's common to to have a 24-bit ICAO address to uniquely identify an aircraft. In order to add a badge to Lattice with the ADS-B identifier, it's necessary to populate the Mode-S.Address field
Aliases.Name
If there is a flight number associated with the aircraft (e.g. UAL 1393), it's expected to set it as the name of the Entity within aliases.name
.
The call sign of the aircraft can also be associated in a structured way as an alternate ID within the aliases.alternate_ids.id
field. This should
have an alt ID type of ALT_ID_TYPE_CALLSIGN
.
The end result of populating these fields on the Entity are that the Lattice UI renders the specific details in the Entity card:
Representing time in the data model
The entity data model contains a few different timestamps. The most important timestamp
is the provenance.source_update_time
field.
This field is required on every update and is the overall time of validity for the entity update.
Lattice will only apply an entity update if the new provenance.source_update_time
is later than the previous one. Otherwise
it will drop the message.
Other components may include a timestamp when the latest time of observation for that particular
component is important to record for general situational awareness. For example, the
tracked_by contains
a last_measurement_timestamp
that records the latest time the upstream source observed a target in a
camera frame, in a radar dwell, or an RF signal receipt.
In any update driven by a change in track data derived from sensor observation, the
tracked_by.last_measurement_timestamp
will be the same as the provenance.source_update_time
. However,
the tracked_by.last_measurement_timestamp
may differ in a few different scenarios. For example:
- If a user overrides the track name, the
provenance.source_update_time
will reflect the time of that change but thetracked_by.last_measurement_timestamp
will still reflect the time of observation. - If the integration extrapolates the target track forward in time (sometimes called a "coast"), it would
report a new
provenance.source_update_time
while retaining the originaltracked_by.last_measurement_timestamp
.