├── .gitignore
├── .gitmodules
├── README.md
├── babel.config.js
├── dist
├── bundle.css
├── fonts
│ └── 629a55a7e793da068dc580d184cc0e31.ttf
└── index.html
├── docker
├── docker-compose.yml
├── envoy
│ ├── Dockerfile
│ └── envoy.yaml
└── mavsdk_server
│ └── Dockerfile
├── generator.sh
├── package-lock.json
├── package.json
├── postcss.config.js
├── src
├── OpenSans-Regular.ttf
├── index.js
├── mavsdk
│ ├── action
│ │ └── action.js
│ ├── drone.js
│ ├── plugin.js
│ └── telemetry
│ │ └── telemetry.js
├── sdk_logo_full.png
└── styles.scss
├── test-client.js
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *_pb.js
3 | lib/
4 | package-lock.json
5 | dist/bundle.js
6 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "DronecodeSDK-Proto"]
2 | path = DronecodeSDK-Proto
3 | url = git@github.com:Dronecode/DronecodeSDK-Proto.git
4 | [submodule "proto"]
5 | path = proto
6 | url = git@github.com:Dronecode/DronecodeSDK-Proto.git
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MAVSDK-JavaScript
2 |
3 | JS wrapper for [mavlink/MAVSDK](https://github.com/mavlink/MAVSDK) using [grpc-web](https://github.com/grpc/grpc-web) to generate a static http client, communicating through the Envoy proxy.
4 |
5 | __NOTE: this is still a proof of concept, don't try to use it in production!__
6 |
7 | ## Contributing
8 |
9 | The next steps that are required are:
10 |
11 | 1. Write a nice API for some selected features (e.g. action.arm, action.takeoff, and telemetry.position) using RxJS.
12 | 2. (Optional) Make a small UI using those features (e.g. a "takeoff" button, and show the position values somewhere).
13 | 3. Write templates based on 1) to auto-generate the full API from our [proto](./proto/protos) definitions.
14 | 4. Deploy the package.
15 |
16 | ## Getting started
17 |
18 | ### Prerequisites
19 |
20 | This project is about providing a package to write frontend JS code, but this code needs to communicate to the drone through a backend. Two components are required:
21 |
22 | * __Envoy proxy:__ it converts the websocket messages to/from the frontend into gRPC messages sent to `mavsdk_server`.
23 | * __mavsdk_server:__ the MAVSDK gRPC server that handles the MAVLink communication with the drone.
24 | * (optional) __Simulator (SITL)__: which is more convenient and safer than testing on a real drone.
25 |
26 | To help getting started, we built those components as docker micro-services. Given that you have docker and docker-compose installed, simply run the following command to start those components:
27 |
28 | ```sh
29 | $ cd docker
30 | $ docker-compose up
31 | ```
32 |
33 | Once the docker containers are running, you should be able to start QGroundControl, and it should connect to the simulator (headless gazebo in this case).
34 |
35 | ### Install the SDK
36 |
37 | Install the npm dependencies:
38 |
39 | ```sh
40 | $ npm install
41 | ```
42 |
43 | Generate the gRPC/protobut static files (those are the ones that will be used by the final API):
44 |
45 | ```
46 | $ ./generator.sh
47 | ```
48 |
49 | Finally, run the development server:
50 |
51 | ```
52 | $ npm run build
53 | $ npm start
54 | ```
55 |
56 | It will say something like: _Project is running at http://localhost:8080/_. If docker-compose started correctly, opening this webpage should make the drone arm and takeoff. That would be visible from QGroundControl and from the docker-compose logs:
57 |
58 | ```
59 | mavsdk_server_1 | [09:23:33|Debug] MAVLink: info: ARMED by Arm/Disarm component command (system_impl.cpp:306)
60 | mavsdk_server_1 | [09:23:33|Debug] MAVLink: info: Using minimum takeoff altitude: 2.50 m (system_impl.cpp:306)
61 | mavsdk_server_1 | [09:23:34|Debug] MAVLink: info: Takeoff detected (system_impl.cpp:306)
62 | ```
63 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | const presets = [
2 | [
3 | "@babel/preset-env",
4 | {
5 | useBuiltIns: "usage",
6 | },
7 | ],
8 | ];
9 |
10 | module.exports = { presets };
--------------------------------------------------------------------------------
/dist/bundle.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: OpenSans;
3 | src: url(fonts/629a55a7e793da068dc580d184cc0e31.ttf);
4 | }
5 | body {
6 | min-height: 100vh;
7 | background-position: center;
8 | background-repeat: no-repeat;
9 | }
10 |
11 | /*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9zcmMvc3R5bGVzLnNjc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLEMiLCJmaWxlIjoiYnVuZGxlLmNzcyIsInNvdXJjZXNDb250ZW50IjpbIkBmb250LWZhY2Uge1xuICBmb250LWZhbWlseTogT3BlblNhbnM7XG4gIHNyYzogdXJsKGZvbnRzLzYyOWE1NWE3ZTc5M2RhMDY4ZGM1ODBkMTg0Y2MwZTMxLnR0Zik7XG59XG5ib2R5IHtcbiAgbWluLWhlaWdodDogMTAwdmg7XG4gIGJhY2tncm91bmQtcG9zaXRpb246IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1yZXBlYXQ6IG5vLXJlcGVhdDtcbn0iXSwic291cmNlUm9vdCI6IiJ9*/
--------------------------------------------------------------------------------
/dist/fonts/629a55a7e793da068dc580d184cc0e31.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mavlink/MAVSDK-JavaScript/a79b12aebf2ba62264b8a68ffab9992964da9f1d/dist/fonts/629a55a7e793da068dc580d184cc0e31.ttf
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | MAVSDK-Javascript
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/docker/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | envoy:
4 | build: ./envoy
5 | ports:
6 | - "9901:9901"
7 | - "10000:10000"
8 | links:
9 | - mavsdk_server
10 | mavsdk_server:
11 | build: ./mavsdk_server
12 | ports:
13 | - "50051:50051"
14 | - "14540:14540/udp"
15 | gazebo_sitl_headless:
16 | image: jonasvautherin/px4-gazebo-headless:1.11.0
17 | environment:
18 | - NO_PXH=1
19 | links:
20 | - mavsdk_server
21 |
--------------------------------------------------------------------------------
/docker/envoy/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM envoyproxy/envoy:v1.17.1
2 |
3 | COPY envoy.yaml /etc/envoy/envoy.yaml
4 |
--------------------------------------------------------------------------------
/docker/envoy/envoy.yaml:
--------------------------------------------------------------------------------
1 | admin:
2 | access_log_path: /tmp/admin_access.log
3 | address:
4 | socket_address: { address: 0.0.0.0, port_value: 9901 }
5 |
6 | static_resources:
7 | listeners:
8 | - name: listener_0
9 | address:
10 | socket_address: { address: 0.0.0.0, port_value: 10000 }
11 | filter_chains:
12 | - filters:
13 | - name: envoy.filters.network.http_connection_manager
14 | typed_config:
15 | "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
16 | codec_type: auto
17 | stat_prefix: ingress_http
18 | route_config:
19 | name: local_route
20 | virtual_hosts:
21 | - name: local_service
22 | domains: ["*"]
23 | routes:
24 | - match: { prefix: "/" }
25 | route:
26 | cluster: greeter_service
27 | timeout: 0s # otherwise grpc streams get stopped
28 | max_stream_duration:
29 | grpc_timeout_header_max: 0s
30 | cors:
31 | allow_origin_string_match:
32 | - prefix: "*"
33 | allow_methods: GET, PUT, DELETE, POST, OPTIONS
34 | allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
35 | max_age: "1728000"
36 | expose_headers: custom-header-1,grpc-status,grpc-message
37 | http_filters:
38 | - name: envoy.filters.http.grpc_web
39 | - name: envoy.filters.http.cors
40 | - name: envoy.filters.http.router
41 | clusters:
42 | - name: greeter_service
43 | connect_timeout: 0.25s
44 | type: logical_dns
45 | http2_protocol_options: {}
46 | lb_policy: round_robin
47 | # win/mac hosts: Use address: host.docker.internal instead of address: localhost in the line below
48 | load_assignment:
49 | cluster_name: mavsdk_server
50 | endpoints:
51 | - lb_endpoints:
52 | - endpoint:
53 | address:
54 | socket_address:
55 | address: mavsdk_server
56 | port_value: 50051
57 |
--------------------------------------------------------------------------------
/docker/mavsdk_server/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ubuntu:18.04
2 |
3 | RUN apt-get update && \
4 | apt-get install -y curl
5 |
6 | RUN curl -L https://github.com/mavlink/MAVSDK/releases/download/v0.37.0/mavsdk_server_manylinux2010-x64 -o /root/mavsdk_server
7 |
8 | RUN chmod +x /root/mavsdk_server
9 |
10 | ENTRYPOINT /root/mavsdk_server -p 50051
11 |
--------------------------------------------------------------------------------
/generator.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | WORK_DIR="./src"
6 | PROTO_DIR="proto/protos"
7 | SDK_DIR="${WORK_DIR}/mavsdk"
8 | JS_IMPORT_STYLE="commonjs"
9 | PROTOS=`find ${PROTO_DIR} -name "*.proto" -type f`
10 | PATH=$PATH:./node_modules/.bin/
11 |
12 | command -v protoc-gen-grpc-web >/dev/null 2>&1 || { echo "ERROR: 'protoc-gen-grpc-web' is required (find it here: https://github.com/grpc/grpc-web/releases)!"; echo "You can also install the package globally with 'npm install -g protoc-gen-grpc-web'" exit 1; }
13 |
14 | function generateForNode {
15 | echo " [+] Working on: ${PROTOS}"
16 |
17 | grpc_tools_node_protoc \
18 | --js_out=import_style=commonjs,binary:$SDK_DIR \
19 | --grpc_out=$SDK_DIR \
20 | --plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin` \
21 | -I$PROTO_DIR \
22 | $PROTOS
23 | }
24 |
25 | function generateForWeb {
26 | for PROTO_FILE in $PROTOS; do
27 | MODULE_NAME=`echo $(basename -- ${PROTO_FILE}) | cut -f 1 -d '.'`
28 | echo " [+] Working on: ${MODULE_NAME}"
29 |
30 | protoc \
31 | -I$PROTO_DIR \
32 | --js_out=import_style=$JS_IMPORT_STYLE,binary:$SDK_DIR \
33 | --grpc-web_out=import_style=$JS_IMPORT_STYLE,mode=grpcwebtext:$SDK_DIR \
34 | $PROTO_FILE
35 | done
36 |
37 | # We need to add eslint-disable, otherwise create-react-app doesn't work with
38 | # this, see:
39 | # https://github.com/improbable-eng/grpc-web/issues/96#issuecomment-347871452
40 | for f in "${SDK_DIR}"/*/*_pb.js
41 | do
42 | echo '/* eslint-disable */' | cat - "${f}" > temp && mv temp "${f}"
43 | done
44 |
45 | for f in "${SDK_DIR}"/*_pb.js
46 | do
47 | echo '/* eslint-disable */' | cat - "${f}" > temp && mv temp "${f}"
48 | done
49 | }
50 |
51 | echo "[+] Generating plugins for grpc-web "
52 | generateForWeb
53 | echo "[+] Done"
54 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mavsdk",
3 | "version": "0.0.1",
4 | "description": "MAVSDK-JavaScript, a JS wrapper for MAVSDK using grpc-web to generate a static http client, communicating through the Envoy proxy",
5 | "private": true,
6 | "main": "src/index.js",
7 | "dependencies": {
8 | "@babel/runtime": "^7.13.9",
9 | "@grpc/grpc-js": "^1.2.10",
10 | "@grpc/proto-loader": "^0.5.6",
11 | "google-protobuf": "^3.15.5",
12 | "grpc": "^1.22.0",
13 | "grpc-web": "^1.0.5",
14 | "lodash": "^4.17.19",
15 | "npm-check-updates": "^3.1.20",
16 | "protoc-gen-grpc-web": "^1.0.5"
17 | },
18 | "devDependencies": {
19 | "@babel/cli": "^7.8.4",
20 | "@babel/core": "^7.9.0",
21 | "@babel/plugin-transform-runtime": "^7.9.0",
22 | "@babel/preset-env": "^7.9.0",
23 | "autoprefixer": "^9.4.10",
24 | "babel-loader": "^8.1.0",
25 | "css-loader": "^3.4.2",
26 | "cssnano": "^4.1.10",
27 | "file-loader": "^3.0.1",
28 | "grpc-tools": "^1.8.1",
29 | "mini-css-extract-plugin": "^0.5.0",
30 | "postcss-loader": "^3.0.0",
31 | "sass": "^1.17.2",
32 | "sass-loader": "^7.1.0",
33 | "style-loader": "^1.1.3",
34 | "webpack": "^4.42.1",
35 | "webpack-cli": "^3.3.11",
36 | "webpack-dev-server": "^3.10.3"
37 | },
38 | "scripts": {
39 | "test": "echo \"Error: no test specified\" && exit 1",
40 | "build": "webpack --config webpack.config.js",
41 | "build-production": "cross-env NODE_ENV=production webpack --config webpack.config.js",
42 | "start": "webpack-dev-server --open"
43 | },
44 | "author": "@mrpollo",
45 | "license": "BSD-3-Clause",
46 | "repository": "https://github.com/mrpollo/sdk-node",
47 | "browserslist": "> 1%"
48 | }
49 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | if(process.env.NODE_ENV === 'production') {
2 | module.exports = {
3 | plugins: [
4 | require('autoprefixer'),
5 | require('cssnano')
6 | ]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/OpenSans-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mavlink/MAVSDK-JavaScript/a79b12aebf2ba62264b8a68ffab9992964da9f1d/src/OpenSans-Regular.ttf
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import Drone from './mavsdk/drone';
3 | import './styles.scss';
4 |
5 | console.log("MAVSDK-Javascript")
6 |
7 | const drone = new Drone('http://127.0.0.1', 10000, false);
8 |
9 | var action;
10 | var telemetry;
11 |
12 | async function getAction() {
13 | if (!action) {
14 | return drone.connect().then((d) => {
15 | return d.action.then((a) => {
16 | action = a;
17 | return action;
18 | })
19 | })
20 | } else {
21 | return action;
22 | }
23 | }
24 |
25 | async function getTelemetry() {
26 | if (!telemetry) {
27 | return drone.connect().then((d) => {
28 | return d.telemetry.then((t) => {
29 | telemetry = t;
30 | return telemetry;
31 | })
32 | })
33 | } else {
34 | return telemetry;
35 | }
36 | }
37 |
38 | function armDrone() {
39 | getAction().then((a) => {
40 | a.arm().then(() => {
41 | console.log('Arming');
42 | }).catch((e) => {
43 | console.log(e);
44 | })
45 | })
46 | }
47 |
48 | function disarmDrone() {
49 | getAction().then((a) => {
50 | a.disarm().then(() => {
51 | console.log('Disarming');
52 | }).catch((e) => {
53 | console.log(e);
54 | })
55 | })
56 | }
57 |
58 | function GetMaximumSpeed() {
59 | getAction().then((a) => {
60 | a.getMaximumSpeed().then((response) => {
61 | console.log(response);
62 | }).catch((e) => {
63 | console.log(e);
64 | })
65 | })
66 | }
67 |
68 | function takeoffDrone() {
69 | getAction().then((a) => {
70 | a.takeoff().then(() => {
71 | console.log('Taking off');
72 | }).catch((e) => {
73 | console.log(e);
74 | })
75 | })
76 | }
77 |
78 | function landDrone() {
79 | getAction().then((a) => {
80 | a.land().then(() => {
81 | console.log('Landing Drone');
82 | }).catch((e) => {
83 | console.log(e);
84 | })
85 | })
86 | }
87 |
88 | function subscribePosition() {
89 | getTelemetry().then((t) => {
90 | t.setRatePosition().then(() => {
91 | t.subscribePosition((position) => {
92 | document.getElementById('position_output').innerHTML =
93 | "Got position: "
94 | + position.getLatitudeDeg().toFixed(6)
95 | + ", "
96 | + position.getLongitudeDeg().toFixed(6)
97 | + ", altitude: "
98 | + position.getRelativeAltitudeM().toFixed(1);
99 | });
100 | }).catch((e) => {
101 | console.log(e);
102 | });
103 | });
104 | }
105 |
106 | function component() {
107 | const element = document.createElement('div');
108 | const innerElement = document.createElement('div');
109 | const armBtn = document.createElement('button');
110 | const disarmBtn = document.createElement('button');
111 | const getMaxSpeedBtn = document.createElement('button');
112 | const landBtn = document.createElement('button');
113 | const takeoffBtn = document.createElement('button');
114 | const positionBtn = document.createElement('button');
115 | var positionOutput = document.createElement('div');
116 | positionOutput.setAttribute('id', 'position_output');
117 |
118 | // Lodash, currently included via a script, is required for this line to work
119 | element.innerHTML = _.join(['Hello', 'MAVSDK'], ' ');
120 |
121 | element.appendChild(innerElement);
122 |
123 | armBtn.innerHTML = "Arm Drone";
124 | armBtn.onclick = armDrone;
125 |
126 | disarmBtn.innerHTML = "Disarm Drone";
127 | disarmBtn.onclick = disarmDrone;
128 |
129 | getMaxSpeedBtn.innerHTML = "Get Maximum Speed";
130 | getMaxSpeedBtn.onclick = GetMaximumSpeed;
131 |
132 | landBtn.innerHTML = "Land Drone";
133 | landBtn.onclick = landDrone;
134 |
135 | takeoffBtn.innerHTML = "Takeoff Drone";
136 | takeoffBtn.onclick = takeoffDrone;
137 |
138 | positionBtn.innerHTML = "Subscribe to position";
139 | positionBtn.onclick = subscribePosition;
140 |
141 | innerElement.appendChild(armBtn);
142 | innerElement.appendChild(disarmBtn);
143 | innerElement.appendChild(getMaxSpeedBtn);
144 | innerElement.appendChild(takeoffBtn);
145 | innerElement.appendChild(landBtn);
146 | innerElement.appendChild(positionBtn);
147 | innerElement.appendChild(positionOutput);
148 |
149 | return element;
150 | }
151 |
152 | document.body.appendChild(component());
153 |
--------------------------------------------------------------------------------
/src/mavsdk/action/action.js:
--------------------------------------------------------------------------------
1 | const {
2 | ActionResult,
3 | ArmRequest,
4 | ArmResponse,
5 | DisarmRequest,
6 | DisarmResponse,
7 | GetMaximumSpeedRequest,
8 | GetMaximumSpeedResponse,
9 | GetReturnToLaunchAltitudeRequest,
10 | GetReturnToLaunchAltitudeResponse,
11 | GetTakeoffAltitudeRequest,
12 | GetTakeoffAltitudeResponse,
13 | KillRequest,
14 | KillResponse,
15 | LandRequest,
16 | LandResponse,
17 | RebootRequest,
18 | RebootResponse,
19 | ReturnToLaunchRequest,
20 | ReturnToLaunchResponse,
21 | SetMaximumSpeedRequest,
22 | SetMaximumSpeedResponse,
23 | SetReturnToLaunchAltitudeRequest,
24 | SetReturnToLaunchAltitudeResponse,
25 | SetTakeoffAltitudeRequest,
26 | SetTakeoffAltitudeResponse,
27 | TakeoffRequest,
28 | TakeoffResponse,
29 | TransitionToFixedWingRequest,
30 | TransitionToFixedWingResponse,
31 | TransitionToMulticopterRequest,
32 | TransitionToMulticopterResponse
33 | } = require('./action_pb');
34 | const { ActionServicePromiseClient } = require('./action_grpc_web_pb');
35 |
36 | class Action {
37 | constructor(path) {
38 | this.path = path;
39 | this.ready = false;
40 |
41 | this.plugin = new ActionServicePromiseClient(path);
42 | return new Promise((resolve, reject) => {
43 | resolve(this);
44 | });
45 | }
46 |
47 | arm() {
48 | const request = new ArmRequest();
49 | return this.plugin.arm(request);
50 | }
51 |
52 | disarm() {
53 | const request = new DisarmRequest();
54 | return this.plugin.disarm(request);
55 | }
56 |
57 | getMaximumSpeed() {
58 | const request = new GetMaximumSpeedRequest();
59 | return this.plugin.getMaximumSpeed(request);
60 | }
61 |
62 | getReturnToLaunchAltitude() {
63 | const request = new GetReturnToLaunchAltitudeRequest();
64 | return this.plugin.getReturnToLaunchAltitude(request);
65 | }
66 |
67 | getTakeoffAltitude() {
68 | const request = new GetTakeoffAltitudeRequest();
69 | return this.plugin.getTakeoffAltitude(request);
70 | }
71 |
72 | kill() {
73 | const request = new KillRequest();
74 | return this.plugin.kill(request);
75 | }
76 |
77 | land() {
78 | const request = new LandRequest();
79 | return this.plugin.land(request);
80 | }
81 |
82 | reboot() {
83 | const request = new RebootRequest();
84 | return this.plugin.reboot(request);
85 | }
86 |
87 | returnToLaunch() {
88 | const request = new ReturnToLaunchRequest();
89 | return this.plugin.returnToLaunch(request);
90 | }
91 |
92 | setMaximumSpeed() {
93 | const request = new SetMaximumSpeedRequest();
94 | return this.plugin.setMaximumSpeed(request);
95 | }
96 |
97 | setReturnToLaunchAltitude() {
98 | const request = new SetReturnToLaunchAltitudeRequest();
99 | return this.plugin.setReturnToLaunchAltitude(request);
100 | }
101 |
102 | setTakeoffAltitude() {
103 | const request = new SetTakeoffAltitudeRequest();
104 | return this.plugin.setTakeoffAltitude(request);
105 | }
106 |
107 | takeoff() {
108 | const request = new TakeoffRequest();
109 | return this.plugin.takeoff(request);
110 | }
111 |
112 | transitionToFixedWing() {
113 | const request = new TransitionToFixedWingRequest();
114 | return this.plugin.transitionToFixedWing(request);
115 | }
116 |
117 | transitionToMulticopter() {
118 | const request = new TransitionToMulticopterRequest();
119 | return this.plugin.transitionToMulticopter(request);
120 | }
121 |
122 | }
123 |
124 |
125 | export default Action;
126 |
--------------------------------------------------------------------------------
/src/mavsdk/drone.js:
--------------------------------------------------------------------------------
1 | import Action from './action/action';
2 | import Telemetry from './telemetry/telemetry';
3 |
4 | class Drone {
5 | constructor(host, port, autoconnect) {
6 | this._host = host;
7 | this._port = port;
8 | this._ssl = false;
9 | this._ready = false;
10 | if (autoconnect) {
11 | return this.connect();
12 | }
13 | }
14 |
15 | async connect() {
16 | if (this._ssl) {
17 | throw 'SSL not yet implemented'
18 | }
19 |
20 | if (this._ready) {
21 | return this;
22 | }
23 |
24 | this._ready = true;
25 |
26 | const plugin_options = {
27 | keepCase: true,
28 | longs: String,
29 | enums: String,
30 | defaults: true,
31 | oneofs: true,
32 | includeDirs: ['proto/protos'],
33 | };
34 |
35 | const plugins = [
36 | { name: 'action', handler: Action },
37 | { name: 'telemetry', handler: Telemetry },
38 | ];
39 |
40 | const pluginsMap = plugins.map((pluginObject) => {
41 | return new Promise((resolve, reject) => {
42 | const plugin = new pluginObject.handler(this.getConnectionPath());
43 | this.registerPlugin(pluginObject.name, plugin);
44 | resolve(plugin);
45 | });
46 | });
47 |
48 | return Promise.all(pluginsMap).then((values) => {
49 | return this;
50 | });
51 | }
52 |
53 | getConnectionPath(pluginName) {
54 | return this._host + ":" + this._port;
55 | }
56 |
57 | registerPlugin(pluginName, plugin) {
58 | Object.defineProperty(this,
59 | pluginName, {
60 | get: () => {
61 | return plugin;
62 | }
63 | },
64 | );
65 | }
66 | }
67 |
68 | export default Drone;
69 |
--------------------------------------------------------------------------------
/src/mavsdk/plugin.js:
--------------------------------------------------------------------------------
1 | // const loader = require('@grpc/proto-loader');
2 | // const grpc = require('@grpc/grpc-js');
3 |
4 | const pluginOptions = {
5 | keepCase: true,
6 | longs: String,
7 | enums: String,
8 | defaults: true,
9 | oneofs: true,
10 | includeDirs: ['proto/protos'],
11 | };
12 |
13 | class Plugin {
14 | constructor(path, name) {
15 | this.path = path;
16 | this.name = name;
17 | // const packageDefinition = loader.loadSync(this.getProtoPath(), pluginOptions);
18 | // this.mavsdk = grpc.loadPackageDefinition(packageDefinition).mavsdk;
19 | // this.basePlugin = this.mavsdk.rpc[this.name];
20 | // this.connectService();
21 |
22 | return this;
23 | }
24 |
25 | connectService() {
26 | // const serviceClient = this.getPluginService();
27 | // this.service = new serviceClient(this.path, grpc.credentials.createInsecure())
28 | }
29 |
30 | getPluginService() {
31 | // return this.basePlugin[this.getPluginServiceName()];
32 | }
33 |
34 | getProtoPath() {
35 | // return `proto/protos/${this.name}/${this.name}.proto`;
36 | }
37 |
38 | getPluginServiceName() {
39 | // return this.name.charAt(0).toUpperCase() + this.name.slice(1) + 'Service';
40 | }
41 | }
42 |
43 | module.exports = Plugin;
44 |
--------------------------------------------------------------------------------
/src/mavsdk/telemetry/telemetry.js:
--------------------------------------------------------------------------------
1 | const {
2 | SubscribePositionRequest,
3 | SubscribeAttitudeEulerRequest,
4 | SetRatePositionRequest,
5 | SetRateAttitudeRequest,
6 | } = require('./telemetry_pb');
7 | const { TelemetryServicePromiseClient } = require('./telemetry_grpc_web_pb');
8 |
9 | class Telemetry {
10 | constructor(path) {
11 | this.path = path;
12 | this.ready = false;
13 |
14 | this.plugin = new TelemetryServicePromiseClient(path);
15 | return new Promise((resolve, reject) => {
16 | resolve(this);
17 | });
18 | }
19 |
20 | setRatePosition() {
21 | const request = new SetRatePositionRequest();
22 | // TODO: hardcoded for now
23 | request.setRateHz(5.0);
24 | return this.plugin.setRatePosition(request);
25 | }
26 |
27 | setRateAttitude() {
28 | const request = new SetRateAttitudeRequest();
29 | // TODO: hardcoded for now
30 | request.setRateHz(5.0);
31 | return this.plugin.setRateAttitude(request);
32 | }
33 |
34 | subscribePosition(callback) {
35 | const request = new SubscribePositionRequest();
36 | var stream = this.plugin.subscribePosition(request, {});
37 | stream.on('data', (response) => {
38 | callback(response.getPosition());
39 | });
40 | }
41 |
42 | subscribeAttitude(callback) {
43 | const request = new SubscribeAttitudeEulerRequest();
44 | var stream = this.plugin.subscribeAttitudeEuler(request, {});
45 | stream.on('data', (response) => {
46 | callback(response.getAttitudeEuler());
47 | });
48 | }
49 | }
50 |
51 | export default Telemetry;
52 |
--------------------------------------------------------------------------------
/src/sdk_logo_full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mavlink/MAVSDK-JavaScript/a79b12aebf2ba62264b8a68ffab9992964da9f1d/src/sdk_logo_full.png
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: OpenSans;
3 | src: url('./OpenSans-Regular.ttf');
4 | }
5 |
6 | body {
7 | min-height: 100vh;
8 | background-position: center;
9 | background-repeat: no-repeat;
10 | }
11 |
--------------------------------------------------------------------------------
/test-client.js:
--------------------------------------------------------------------------------
1 | const grpc = require('grpc');
2 | const telemetryMessages = require('./dronecode_sdk/telemetry/telemetry_pb');
3 | const telemetryServices = require('./dronecode_sdk/telemetry/telemetry_grpc_pb');
4 |
5 | const actionMessages = require('./dronecode_sdk/action/action_pb');
6 | const actionServices = require('./dronecode_sdk/action/action_grpc_pb');
7 |
8 | console.log("[+] started");
9 |
10 | const telemetryServiceClient = new telemetryServices.TelemetryServiceClient('localhost:50051', grpc.credentials.createInsecure());
11 | const actionServiceClient = new actionServices.ActionServiceClient('localhost:50051', grpc.credentials.createInsecure());
12 |
13 | const armSubscribeRequest = new telemetryMessages.SubscribeArmedRequest();
14 | const posRequest = new telemetryMessages.SubscribePositionRequest();
15 |
16 | const timeout = (new Date().getTime()) + (5 * 1000); // 5 seconds in ms
17 | telemetryServiceClient.waitForReady(timeout, (error, data) => {
18 | const stateId = telemetryServiceClient.getChannel().getConnectivityState();
19 | const stateStr = Object.keys(grpc.connectivityState).find(key => grpc.connectivityState[key] === stateId)
20 | console.log(`Telemetry Service State: ${stateStr}`);
21 | if (error) {
22 | console.log('tele error');
23 | if (stateId !== grpc.connectivityState.READY) {
24 | throw error;
25 | }
26 | }
27 |
28 | telemetryServiceClient.subscribeArmed(armSubscribeRequest, (error, response) => {
29 | if (error){
30 | console.log('[-] error');
31 | console.log(error);
32 | }
33 | console.log('[+] armed?');
34 | console.log(response);
35 | });
36 |
37 | telemetryServiceClient.subscribePosition(posRequest, (error, response) => {
38 | if (error){
39 | throw error;
40 | }
41 | console.log('[+] subscribePosition?');
42 | console.log(response);
43 | });
44 | });
45 |
46 |
47 | const armRequest = new actionMessages.ArmRequest();
48 | const takeoffRequest = new actionMessages.TakeoffRequest();
49 |
50 | actionServiceClient.arm(armRequest, (error, response) => {
51 | if (error) {
52 | throw error;
53 | }
54 | const armResult = response.getActionResult().getResult();
55 | const armResponse = response.getActionResult().getResultStr();
56 | console.log(`[+] arm: ${armResponse}`);
57 | if (armResult === 1) {
58 | actionServiceClient.takeoff(takeoffRequest, (err, res) => {
59 | if (err) {
60 | throw err;
61 | }
62 | const takeoffResult = res.getActionResult().getResult();
63 | const takeoffResponse = res.getActionResult().getResultStr();
64 | console.log(`[+] takeoff: ${takeoffResponse}`);
65 | if (takeoffResult === 1) {
66 | console.log('[+] --- end ---');
67 | }
68 | });
69 | }
70 | });
71 |
72 | console.log("[+] ended");
73 |
74 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
3 |
4 | module.exports = {
5 | entry: path.resolve(__dirname, 'src/index.js'),
6 | output: {
7 | path: path.resolve(__dirname, 'dist'),
8 | filename: 'bundle.js'
9 | },
10 | mode: 'development',
11 | module: {
12 | rules: [{
13 | test: /\.m?js$/,
14 | exclude: /(node_modules|bower_components)/,
15 | use: {
16 | loader: 'babel-loader',
17 | options: {
18 | presets: ['@babel/preset-env'],
19 | plugins: ['@babel/plugin-transform-runtime']
20 | // cacheDirectory: true
21 | }
22 | }
23 | },
24 | {
25 | test: /\.(sa|sc|c)ss$/,
26 | use: [{
27 | loader: MiniCssExtractPlugin.loader
28 | },
29 | {
30 | loader: "css-loader"
31 | },
32 | {
33 | loader: "postcss-loader"
34 | },
35 | {
36 | loader: "sass-loader",
37 | options: {
38 | implementation: require("sass")
39 | }
40 | }
41 | ]
42 | },
43 | {
44 | test: /\.(png|jpe?g|gif|svg)$/,
45 | use: [{
46 | loader: "file-loader",
47 | options: {
48 | outputPath: 'images'
49 | }
50 | }]
51 | },
52 | {
53 | test: /\.(woff|woff2|ttf|otf|eot)$/,
54 | use: [{
55 | loader: "file-loader",
56 | options: {
57 | outputPath: 'fonts'
58 | }
59 | }]
60 | }
61 | ]
62 | },
63 | devtool: 'inline-source-map',
64 | devServer: {
65 | contentBase: path.resolve(__dirname, 'dist')
66 | },
67 | plugins: [
68 | new MiniCssExtractPlugin({
69 | filename: "bundle.css"
70 | })
71 | ]
72 | };
73 |
--------------------------------------------------------------------------------