DEV Community

Cover image for DJI FlightHub Sync: Media File Direct Transfer to AWS
Jacek Kościesza for AWS Community Builders

Posted on • Updated on

DJI FlightHub Sync: Media File Direct Transfer to AWS

There are many drone management platforms and DJI has its own called FlightHub 2. It's quite a powerful software, where you can do a route planning, mission management, watch livestream from a drone or do some basic media files management.

Drones have many use cases, so sometimes there is a need to customize your workflow - e.g. use your own AI/ML models for object detection or share media files captured by a drone to with a 3rd party software.

To achieve this - you have to somehow integrate your software with the DJI ecosystem. There are a few possibilities how to do this.

DJI Cloud API

There is DJI Cloud API, which is a powerful framework that allows developers to interact with DJI drones and their associated data through common standard protocols such as MQTT, HTTPS, and WebSocket.

It gives you a great flexibility, but in some cases it would be too much work. The problem is that you can either use FlightHub 2 or your Cloud API based solution. Using both at the same time is not supported, see Can the third-party platform developed by Cloud API and Flighthub 2 be used at the same time support topic.

This means that you can't for example process media files in our own software, but do the rest e.g. route planning, mission management in FlightHub 2. You would have to also implement those advanced features in your app.

Fortunately, DJI has created a solution for such use cases - FlightHub Sync.

FlightHub Sync

FlightHub Sync is a feature within DJI FlightHub 2 that facilitates communication and data exchange between FlightHub 2 and third-party platforms. This includes APIs for

  • Media File Direct Transfer
  • Telemetry Data Transfer
  • Stream Forwarding

We will focus on media files (photos, videos) transfer.

To configure FlightHub Sync, you must have "Super Admin" or "Organization Admin" role. Go to My Organization and click "Organization Settings" action (icon button with a "cog").

"Organization Settings" action (icon button with a "cog")

You will see FlightHub Sync (Beta) configuration in the upper-right corner.

FlightHub Sync configuration

Click "Details >" link and you will see FlightHub Sync configuration divided into sections.

FlightHub Sync configuration details

We are interested in "Basic Information" and "Media File Direct Transfer" settings. Let's explore what information we will have to provide.

Basic Information

Basic Information

We have to configure a few things

  • Organization Key
  • Third-Party Cloud Name
  • Webhook URL

Format will be somethings like this:

{
  "Organization Key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "Third-Party Cloud Name": "My app",
  "Webhook URL": "https://xxxxxxxxxx.execute-api.eu-west-1.amazonaws.com/prod/dji/flighthub2/notify"
}
Enter fullscreen mode Exit fullscreen mode

We can find more info about this in Configure Information on FlightHub Sync.

Organization Key

"Organization Key" is just a very long (64 hexadecimal digits) unique ID for the organization, which is generated by FlightHub 2.

It's used for example in FlightHub Sync APIs. When you call an API endpoint e.g. Get Task Details - you have to provide it as a header parameter.

FlightHub Sync API header params

Third-Party Cloud Name

It's a name of the third-party cloud platform, so an arbitrary name of your application. Let's use something like "My app".

Webhook URL

It's a unique URL for receiving notifications from FlightHub 2, so the endpoint which we have to provision.

When media files are uploaded - FlightHub 2 will send a POST request to notify us about synced files. Body of the callback will be similar to this:

{
  "notify_type": "way_line_file_upload_complete",
  "org_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "org_name": "My Organization",
  "prj_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "prj_name": "My Project",
  "sn": "XXXXXXXXXXXXXX",
  "task_info": {
    "task_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "task_type": "way_line",
    "tags": []
  },
  "files": [
    {
      "id": 123456,
      "uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "file_type": 10,
      "sub_file_type": 0,
      "name": "DJI_20240329091034_0001",
      "key": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/DJI_20240329091034_0001.jpeg"
    },
    {
      "id": 123457,
      "uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "file_type": 10,
      "sub_file_type": 0,
      "name": "DJI_20240329091112_0002",
      "key": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/DJI_20240329091112_0002.jpeg"
    }
  ],
  "folder_info": {
    "expected_file_count": 2,
    "uploaded_file_count": 2,
    "folder_id": 123458
  }
}
Enter fullscreen mode Exit fullscreen mode

Media File Direct Transfer

Media File Direct Transfer

Storage Location

We have to configure only one thing

  • Storage Location

It's a JSON string, like this:

{
  "access_key_id": "XXXXXXXXXXXXXXXXXXXX",
  "access_key_secret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "region": "eu-west-1",
  "bucket": "flighthub2-xxxxxxxxxxxx",
  "arn": "arn:aws:iam::xxxxxxxxxxxx:role/flighthub2",
  "role_session_name": "flighthub2",
  "provider": "aws" 
}
Enter fullscreen mode Exit fullscreen mode

We can find more info about this configuration in Configure Media File Direct Transfer and FlightHub Sync Documentation.

Storage Location

Storage location is a configuration of OSS (Object Storage Service).

According to FlightHub Sync Documentation - currently supported OSS solutions are

Project

To make it work, we will have to also enable Media File Direct Transfer on the FlightHub 2 project level.

FlightHub 2 project configuration

Important thing to note is that:

When enabled, data collected by dock will only be uploaded to My app. DJI FlightHub 2 will not receive any data

Infrastructure

It's now clear that we have to build a cloud based infrastructure to integrate our solution with FlightHub Sync.

Two high level building blocks will be

  • OSS (Object Storage Service)
  • API (Application Programming Interface)

Let's build our solution using AWS. We will use AWS CDK (Cloud Development Kit) and TypeScript to define our cloud application resources.

AWS

OSS (Object Storage Service)

Our OSS solution will include two things: S3 bucket where media files will be uploaded and access configuration using IAM.

S3 (Simple Storage Service)

Let's start with defining flighthub2-xxxxxxxxxxxx bucket, where media files will be uploaded by Media File Direct Transfer feature of FlightHub Sync. Bucket name must be globally unique, so we will add account number as a postfix.

s3.cdk.ts

import { Construct } from "constructs";
import * as cdk from "aws-cdk-lib";
import * as s3 from "aws-cdk-lib/aws-s3";

export class S3CdkConstruct extends Construct {
  public bucket: s3.Bucket;

  constructor(scope: Construct, id: string) {
    super(scope, id);

    this.bucket = new s3.Bucket(this, "bucket", {
      bucketName: `flighthub2-${cdk.Stack.of(this).account}`,
    });

    new cdk.CfnOutput(this, "bucket", {
      value: this.bucket.bucketName,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode
IAM (Identity and Access Management)

Now let's define flighthub2 user and role. User will not have access to anything, but using AWS STS (Security Token Service) he will be able to assume flighthub2 role and get temporary security credentials with access to the Amazon S3 bucket.

iam.cdk.ts

import { Construct } from "constructs";
import * as cdk from "aws-cdk-lib";
import * as iam from "aws-cdk-lib/aws-iam";

export class IamCdkConstruct extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    const user = new iam.User(this, "User", {
      userName: "flighthub2",
    });

    const accessKey = new iam.CfnAccessKey(this, "CfnAccessKey", {
      userName: user.userName,
    });

    new cdk.CfnOutput(this, "access_key_id", { value: accessKey.ref });
    new cdk.CfnOutput(this, "access_key_secret", {
      value: accessKey.attrSecretAccessKey,
    });

    const role = new iam.Role(this, "role", {
      assumedBy: new iam.ArnPrincipal(user.userArn),
      roleName: "flighthub2",
      managedPolicies: [
        // TODO: restrict it to flighthub2-xxxxxxxxxxxx bucket and s3:PutObject action
        iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonS3FullAccess")
      ],
    });

    new cdk.CfnOutput(this, "arn", { value: role.roleArn });
  }
}

Enter fullscreen mode Exit fullscreen mode

Keep in mind that we attached AmazonS3FullAccess policy to the flighthub2 role. For the production ready solution we should limit access to the flighthub2 bucket we created and allow only uploading files, so s3:PutObject action. This is example of "Principle of least privilege".

API (Application Programming Interface)

API will consist of a single endpoint, which will get notification about uploaded files. We will build it using API Gateway and a Lambda function.

API Gateway

Let's start with defining REST API using API Gateway

gw.cdk.ts

import * as cdk from "aws-cdk-lib";
import * as gw from "aws-cdk-lib/aws-apigateway";
import { Construct } from "constructs";

export class ApiGatewayCdkConstruct extends Construct {
  public api: gw.RestApi;

  constructor(scope: Construct, id: string) {
    super(scope, id);

    this.api = new gw.RestApi(this, "my-app");
  }
}
Enter fullscreen mode Exit fullscreen mode

Next we will create our endpoint by defining resources and method for our webhook and integrating a lambda function with it.

api.cdk.ts

import * as gw from "aws-cdk-lib/aws-apigateway";
import * as path from "path";
import { Construct } from "constructs";

import { ApiGatewayCdkConstruct } from "../../../gw.cdk";
import { LambdaCdkConstruct } from "../../../lambda.cdk";

interface Props {
  apigateway: ApiGatewayCdkConstruct;
}

export class ApiCdkConstruct extends Construct {
  constructor(scope: Construct, id: string, { apigateway: { api } }: Props) {
    super(scope, id);

    const dji = api.root.addResource("dji", {
      // TODO: restrict CORS
      defaultCorsPreflightOptions: {
        allowHeaders: gw.Cors.DEFAULT_HEADERS,
        allowMethods: gw.Cors.ALL_METHODS,
        allowOrigins: gw.Cors.ALL_ORIGINS,
      },
    });
    const flighthub2 = dji.addResource("flighthub2");
    const notify = flighthub2.addResource("notify");

    const notifyLambda = new LambdaCdkConstruct(this, "Notify", {
      name: "dji-flighthub2-notify",
      description: "Notification from DJI FlightHub 2",
      entry: path.join(__dirname, "./functions/notify.lambda.ts"),
    });

    notify.addMethod("POST", new gw.LambdaIntegration(notifyLambda.function));
  }
}

Enter fullscreen mode Exit fullscreen mode

Keep in mind that CORS configuration is very permissive. For the production ready solution we should restrict it.

Lambda

Lambda function will be a core of our workflow. It will get a notification about uploaded media files. What we will do next highly depends on our use case. We can for example process uploaded files (generate thumbnail, detect objects), notify other parts of our app about synced files e.g. using Amazon EventBridge etc.

There are also some requirements about response, which we have to send to FlightHub Sync. According to the FlightHub Sync Documentation we have to respond with HTTP status code 200 and return { code: 0 }.

notify.lambda.ts

import { APIGatewayProxyHandler } from "aws-lambda";

import { Notification } from "../notification";

export const handler: APIGatewayProxyHandler = async (event) => {
  console.log(JSON.stringify(event));

  let notification: Notification;
  try {
    notification = JSON.parse(event.body) as Notification;
  } catch (error) {
    console.error(error);
    return {
      statusCode: 400,
      body: JSON.stringify({ code: 1, message: "JSON parse error" }),
    };
  }

  switch (notification.notify_type) {
    case "way_line_file_upload_complete":
      // TODO: your workflow
      break;
    default:
      console.log("Unknown notification type", notification.notify_type);
      break;
  }

  return {
    statusCode: 200,
    body: JSON.stringify({ code: 0 }),
  };
};

Enter fullscreen mode Exit fullscreen mode

notification.ts

export interface Notification {
  notify_type: "way_line_file_upload_complete" | string;
  org_id: string;
  org_name: string;
  prj_id: string;
  prj_name: string;
  sn: string;
  task_info: TaskInfo;
  files: File[];
  folder_info: FolderInfo;
}

export interface TaskInfo {
  task_id: string;
  task_type: "way_line" | string;
  tags: string[];
}

export interface File {
  id: number;
  uuid: string;
  file_type: number;
  sub_file_type: number;
  name: string;
  key: string;
}

export interface FolderInfo {
  expected_file_count: number;
  uploaded_file_count: number;
  folder_id: number;
}
Enter fullscreen mode Exit fullscreen mode

Testing

After deploying our application to the AWS cloud. It's time for some testing.

Before we do a final end-to-end test with a real drone and FlightHub 2, we can write an integration test, which will mimic what FlightHub Sync is doing.

Integration Test

Let's create a simple Node.js app which will upload a file to our S3 bucket and call our webhook.

async function mediaFileDirectTransfer(): Promise<void> {
  try {
    const credentials = await getCredentialsFromAssumedRole();
    await uploadToS3(credentials);
    await sendNotification();
  } catch (error) {
    logger.error(error);
  }
}
Enter fullscreen mode Exit fullscreen mode

We are doing three things here

  • getting credentials from the assumed role using STS
  • uploading test file to S3 using those credentials
  • sending notification to our API endpoint

When we run the app, we should see 3 things:

  • file should be uploaded to our S3 bucket
  • call to our API endpoint should return HTTP status code 200 and return { code: 0 }
  • we should see logs with our notification in Amazon CloudWatch

Let's see in details how the above functions are implemented.

Get credentials

async function getCredentialsFromAssumedRole(): Promise<Credentials> {
  const sts = new STSClient({
    region: process.env.REGION,
    credentials: {
      accessKeyId: process.env.AWS_ACCESS_KEY_ID,
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
    },
  });

  const { Credentials } = await sts.send(
    new AssumeRoleCommand({
      RoleArn: process.env.ARN,
      RoleSessionName: process.env.ROLE_SESSION_NAME
    })
  );
  logger.info(Credentials);

  return Credentials;
}
Enter fullscreen mode Exit fullscreen mode

Upload file to S3

async function uploadToS3(credentials: Credentials): Promise<void> {
  const s3 = new S3Client({
    region: process.env.REGION,
    credentials: {
      accessKeyId: credentials.AccessKeyId,
      secretAccessKey: credentials.SecretAccessKey,
      sessionToken: credentials.SessionToken,
    },
  });

  const response = await s3.send(
    new PutObjectCommand({
      Bucket: process.env.BUCKET,
      Key: "fh2.txt",
      Body: `Hello from FlighHub 2 ${new Date().toISOString()}`,
    })
  );

  logger.info(response);
}
Enter fullscreen mode Exit fullscreen mode

Send notification

async function sendNotification(): Promise<void> {
  const response = await axios.post(process.env.API_URL, notification);
  logger.info(response.data);
}
Enter fullscreen mode Exit fullscreen mode

notification is just a JSON file like this:

notification.json

{
  "notify_type": "way_line_file_upload_complete",
  "org_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "org_name": "My Organization",
  "prj_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "prj_name": "My Project",
  "sn": "XXXXXXXXXXXXXX",
  "task_info": {
    "task_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "task_type": "way_line",
    "tags": []
  },
  "files": [
    {
      "id": 123456,
      "uuid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "file_type": 10,
      "sub_file_type": 0,
      "name": "fh2",
      "key": "fh2.txt"
    }
  ],
  "folder_info": {
    "expected_file_count": 1,
    "uploaded_file_count": 1,
    "folder_id": 123456
  }
}
Enter fullscreen mode Exit fullscreen mode

Environment variables

Our .env file (with environment variables) will have a structure very similar to the FlightHub Sync configuration. We can take values from the AWS CDK/CloudFormation outputs after we deployment our solution using cdk deploy.

.env

AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXXXXXX
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
REGION=eu-west-1
BUCKET=flighthub2-xxxxxxxxxxxx
ARN=arn:aws:iam::xxxxxxxxxxxx:role/flighthub2
ROLE_SESSION_NAME=flighthub2
PROVIDER=aws
API_URL=https://xxxxxxxxxx.execute-api.eu-west-1.amazonaws.com/prod/dji/flighthub2/notify
Enter fullscreen mode Exit fullscreen mode

Conclusion

Although FlightHub Sync is still in beta, it has some bugs and I definitely have a wishlist of improvements (I sent feedback to the DJI) - it works and opens a lot of new possibilities for a custom drone related workflows.

Top comments (0)