├── .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 | --------------------------------------------------------------------------------