Commit a14f1408 authored by Léo-Paul Géneau's avatar Léo-Paul Géneau 👾

software/js-drone: add software-type drone

Now the default software-type request a list of drone software-type
parent 396094ed
......@@ -2,26 +2,29 @@
## Presentation ##
* Deploy `user.js` script on a drone to fly it
* Deploy `user.js` flight script on a drone swarm
* Compile all required libraries to run flight scripts
* Compile all required libraries to run the flight script
## Parameters ##
* autopilot-ip: IPv4 address to identify the autpilot from the companion board
* autopilot-ip: IPv4 address to identify the autopilot from the companion board
* id: User chosen ID for the drone (must be unique in a swarm, will be used as an identifier in multicast communications)
* drone-guid-list: List of computer id on which flight script must be deployed
* is-a-simulation: Must be set to 'true' to automatically take off during simulation
* multicast-ipv6: IPv6 of the multicast group of the swarm
* multicast-ip: IPv6 of the multicast group of the swarm
* net-if: Network interface used for multicast traffic
* drone-id-list: List of the drone IDs of the swarm (recommended to add the current drone ID)
* flight-script: URL of user's script to execute to fly drone swarm
* flight-script: User script to execute to fly drone swarm
* subscriber-guid-list: List of computer id on which subscription script must be deployed
## How it works ##
Run `quickjs binary location` `scripts location`/main.js `scripts location`/user.js
For each computer listed in `drone-guid-list` and `subscriber-guid-list` a drone SR will be instanciated.
Each instance will return a `instance-path`. Under this path one will find `quickjs binary` in `bin` folder
and `scripts` in `etc` folder.
Run `quickjs binary location` `scripts location`/main.js `scripts location`/user.js .
......@@ -14,16 +14,24 @@
# not need these here).
[instance-profile]
filename = instance.cfg
md5sum = 7d4969239eb9d46bb44d57fc32b68c44
md5sum = 360b58007c25727b7bd8a9154d5cafd4
[instance-default]
filename = instance-default.cfg
md5sum = b26633b118cddd7c7b8dfd61b360999c
[instance-drone]
filename = instance-drone.cfg
md5sum = 1ff50063f5a54712a0bc0ff38fa74630
[main]
filename = main.js
md5sum = 4b1b27ea3e06b8d40cbc33f0ec617601
md5sum = c381d2a6c4008a9ef69dd176d3a06028
[pubsub]
filename = pubsub.js
md5sum = 4a0c63f9e088fa525a3699484d193c4d
md5sum = c732be66f8ec97bd16cd34d06a0c0a0b
[worker]
filename = worker.js
md5sum = 5ed534e9ca56b9c0ee321b96b5d7c458
md5sum = d919ce35d42561bc38a06273781cb702
{% set autopilot_ip = slapparameter_dict.get('autopilotIp', '192.168.27.1') -%}
{% set flight_script = slapparameter_dict.get('flightScript', 'https://lab.nexedi.com/nexedi/flight-scripts/raw/master/default.js') -%}
{% set is_a_simulation = slapparameter_dict.get('isASimulation', False) -%}
{% set multicast_ip = slapparameter_dict.get('multicastIp', 'ff15::1111') -%}
{% set net_if = slapparameter_dict.get('netIf', 'eth0') -%}
{% set drone_guid_list = slapparameter_dict.get('droneGuidList', []) -%}
{% set subscriber_guid_list = slapparameter_dict.get('subscriberGuidList', []) -%}
{% set guid_list = drone_guid_list + subscriber_guid_list -%}
{% set nb_peer = len(guid_list) -%}
{% set drone_id_list = [] -%}
{% set subscriber_id_list = [] -%}
{% set part_list = ['publish-connection-information'] -%}
{% for id, guid in enumerate(guid_list) -%}
{% set request_drone_section_title = 'request-drone' ~ id -%}
{% do part_list.append(request_drone_section_title) %}
[{{ request_drone_section_title }}]
<= slap-connection
recipe = slapos.cookbook:request.serialised
name = Drone{{ id }}
software-url = $${:software-release-url}
software-type = drone
return = instance-path
sla-computer_guid = {{ guid }}
config-autopilotIp = {{ autopilot_ip }}
config-numberOfPeers = {{ dumps(nb_peer) }}
config-id = {{ dumps(id) }}
config-isASimulation = {{ dumps(is_a_simulation) }}
{% if guid in drone_guid_list -%}
{% do drone_id_list.append(id) %}
config-isADrone = {{ dumps(True) }}
config-flightScript = {{ flight_script }}
{% else -%}
{% do subscriber_id_list.append(id) %}
config-isADrone = {{ dumps(False) }}
config-flightScript = https://lab.nexedi.com/nexedi/flight-scripts/raw/master/subscribe.js
{% endif -%}
config-multicastIp = {{ multicast_ip }}
config-netIf = {{ net_if }}
{% endfor %}
[publish-connection-information]
recipe = slapos.cookbook:publish.serialised
drone-id-list = {{ dumps(drone_id_list) }}
subscriber-id-list = {{ dumps(subscriber_id_list) }}
[buildout]
parts =
{%- for part in part_list %}
{{ part }}
{%- endfor -%}
{
"$schema": "http://json-schema.org/draft-06/schema",
"type": "object",
"description": "Parameters to instantiate JS drone",
"additionalProperties": false,
"properties": {
"autopilotIp": {
"title": "IP address of the drone's autopilot",
"description": "IP used to create a connection with the autopilot.",
"type": "string"
},
"numberOfPeers": {
"title": "Number of Peers",
"description": "Number of drones and subscribers in the swarm",
"type": "integer"
},
"id": {
"title": "drone ID",
"description": "Drone unique identifier",
"type": "integer"
},
"isADrone": {
"title": "Set the requested instance as a drone",
"description": "The option used to determine if the instance is a drone. This affects the context of the user script (e.g. if it should be linked to an autopilot or publish its GPS coordinates)",
"type": "boolean"
},
"isASimulation": {
"title": "Set the flight as a simulation",
"description": "The option used to determine if the flight is real or if it is a simulation. This affects the context of the flight (e.g. if the take off is manual or automatic).",
"type": "boolean"
},
"multicastIp": {
"title": "IP of the multicast group",
"description": "IP address used to communicate with the other drones.",
"type": "string"
},
"netIf": {
"title": "Network interface",
"description": "Interface used for multicast traffic.",
"type": "string"
},
"flightScript": {
"title": "Script's URL of the flight",
"description": "URL of the script which will be executed for the flight. This URL must be publicly accesible so that all drones can fetch the script.",
"type": "string"
}
}
}
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Values returned by drone instantiation",
"additionalProperties": false,
"properties": {
"instance-path": {
"description": "Path of the directory where the quickjs binary and the flight scripts are located",
"type": "string"
}
},
"type": "object"
}
[buildout]
parts =
main
symlink-quickjs-binary
publish-connection-information
[directory]
recipe = slapos.cookbook:mkdirectory
home = $${buildout:directory}
bin = $${:home}/bin
etc = $${:home}/etc
var = $${:home}/var
log = $${:var}/log
[js-dynamic-template]
recipe = slapos.recipe.template:jinja2
rendered = $${directory:etc}/$${:_buildout_section_name_}.js
template = ${buildout:directory}/$${:_buildout_section_name_}.js
extra-context =
context =
import json_module json
raw qjs_wrapper ${qjs-wrapper:location}/lib/libqjswrapper.so
raw configuration {{ configuration }}
$${:extra-context}
[main]
<= js-dynamic-template
extra-context =
key log_dir directory:log
key pubsub_script pubsub:rendered
key worker_script worker:rendered
[pubsub]
<= js-dynamic-template
[worker]
<= js-dynamic-template
[symlink-quickjs-binary]
recipe = slapos.recipe.build
binary-path = ${quickjs:location}/bin/qjs
target = $${directory:bin}/qjs
init =
import os
if not os.path.exists(options['target']):
os.symlink(options['binary-path'], options['target'])
[publish-connection-information]
recipe = slapos.cookbook:publish.serialised
instance-path = $${directory:home}
......@@ -4,47 +4,47 @@
"description": "Parameters to instantiate JS drone",
"additionalProperties": false,
"properties": {
"autopilot-ip": {
"autopilotIp": {
"title": "IP address of the drone's autopilot",
"description": "IP used to create a connection with the autopilot.",
"type": "string",
"default": "192.168.27.1"
},
"id": {
"title": "Drone ID",
"description": "Unique identifier of the drone.",
"type": "integer",
"default": 1
"droneGuidList": {
"title": "List of drones computer ID",
"description": "List of computer ID of drones in the swarm",
"type": "array",
"default": []
},
"is-a-simulation": {
"isASimulation": {
"title": "Set the flight as a simulation",
"description": "The option used to determine if the flight is real or if it is a simulation. This affects the context of the flight (e.g. if the take off is manual or automatic).",
"type": "boolean",
"default": false
},
"multicast-ipv6": {
"multicastIpv6": {
"title": "IP of the multicast group",
"description": "IP address used to communicate with the other drones.",
"type": "string",
"default": "ff15::1111"
},
"net-if": {
"netIf": {
"title": "Network interface",
"description": "Interface used for multicast traffic.",
"type": "string",
"default": "eth0"
},
"drone-id-list": {
"title": "List of drones IDs",
"description": "List of identifiers of drones.",
"flightScript": {
"title": "Script's URL of the flight",
"description": "URL of the script which will be executed for the flight. This URL must be publicly accesible so that the drone can fetch the script.",
"type": "string",
"default": "https://lab.nexedi.com/nexedi/flight-scripts/raw/master/default.js"
},
"subscriberGuidList": {
"title": "List of subscribers computer ID",
"description": "List of computer ID of swarms subscribers",
"type": "array",
"default": []
},
"flight-script": {
"title": "Script of the flight",
"description": "Script which will be executed for the flight",
"type": "string",
"textarea": true
}
}
}
......@@ -2,6 +2,15 @@
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Values returned by drone swarm (default) instantiation",
"additionalProperties": false,
"properties": {},
"properties": {
"drone-id-list": {
"description": "List of drones IDs",
"type": "array"
},
"subscriber-id-list": {
"description": "List of subscribers IDs",
"type": "array"
}
},
"type": "object"
}
[buildout]
parts =
main
user
switch-softwaretype
eggs-directory = ${buildout:eggs-directory}
develop-eggs-directory = ${buildout:develop-eggs-directory}
offline = true
[directory]
recipe = slapos.cookbook:mkdirectory
home = $${buildout:directory}
etc = $${:home}/etc
var = $${:home}/var
log = $${:var}/log
[switch-softwaretype]
recipe = slapos.cookbook:switch-softwaretype
default = instance-default:output
drone = instance-drone:output
RootSoftwareInstance = $${:default}
[slap-configuration]
recipe = slapos.cookbook:slapconfiguration
recipe = slapos.cookbook:slapconfiguration.serialised
computer = $${slap_connection:computer_id}
partition = $${slap_connection:partition_id}
url = $${slap_connection:server_url}
key = $${slap_connection:key_file}
cert = $${slap_connection:cert_file}
[drone]
recipe = slapos.recipe.build
slapparameter-dict = $${slap-configuration:configuration}
init =
options['autopilot-ip'] = options['slapparameter-dict'].get('autopilot-ip', '192.168.27.1')
options['id'] = options['slapparameter-dict'].get('id', 1)
options['is-a-simulation'] = options['slapparameter-dict'].get('is-a-simulation', False)
options['multicast-ipv6'] = options['slapparameter-dict'].get('multicast-ip', 'ff15::1111')
options['net-if'] = options['slapparameter-dict'].get('net-if', 'eth0')
options['drone-id-list'] = options['slapparameter-dict'].get('drone-id-list', [])
options['is-a-drone'] = 'flight-script' in options['slapparameter-dict']
subscription_script = '''
me.onStart = function() {
me.f = me.fdopen(me.in, "r");
console.log("Use q to quit");
};
[dynamic-template-base]
recipe = slapos.recipe.template:jinja2
url = ${buildout:directory}/$${:_buildout_section_name_}.cfg
output = $${buildout:directory}/$${:_buildout_section_name_}
me.onUpdate= function(timestamp) {
while(me.f.getline() != "q") {
continue;
}
try {
me.f.close();;
} catch (error) {
console.error(error);
}
me.exit(0);
};
'''
[instance-default]
<= dynamic-template-base
extensions = jinja2.ext.do
context =
key slapparameter_dict slap-configuration:configuration
options['flight-script'] = options['slapparameter-dict'].get('flight-script', subscription_script)
[instance-drone]
<= dynamic-template-base
context =
key configuration drone-configuration:output
key user-script user:destination
[js-dynamic-template]
[drone-configuration]
recipe = slapos.recipe.template:jinja2
rendered = $${directory:etc}/$${:_buildout_section_name_}.js
template = ${buildout:directory}/$${:_buildout_section_name_}.js
extra-context =
output = $${directory:etc}/configuration.json
context =
raw qjs_wrapper ${qjs-wrapper:location}/lib/libqjswrapper.so
$${:extra-context}
[main]
<= js-dynamic-template
extra-context =
key autopilot_ip drone:autopilot-ip
key id drone:id
key is_a_simulation drone:is-a-simulation
key is_a_drone drone:is-a-drone
key log_dir directory:log
key pubsub_script pubsub:rendered
key worker_script worker:rendered
[pubsub]
<= js-dynamic-template
extra-context =
key ipv6 drone:multicast-ipv6
key net_if drone:net-if
import json_module json
key slapparameter_dict slap-configuration:configuration
inline = {{ json_module.dumps(slapparameter_dict) }}
[user]
recipe = slapos.recipe.template:jinja2
output = $${directory:etc}/user.js
context =
key script drone:flight-script
inline = {{ script }}
recipe = slapos.recipe.build:download
url = $${slap-configuration:configuration.flightScript}
destination = $${directory:etc}/user.js
offline = false
[worker]
<= js-dynamic-template
extra-context =
key drone_id_list drone:drone-id-list
key id drone:id
key is_a_drone drone:is-a-drone
[directory]
recipe = slapos.cookbook:mkdirectory
home = $${buildout:directory}
etc = $${:home}/etc
......@@ -5,17 +5,20 @@ import {
stop,
stopPubsub,
takeOffAndWait
} from "{{ qjs_wrapper }}";
} from {{ json_module.dumps(qjs_wrapper) }};
import { setTimeout, Worker } from "os";
import { exit } from "std";
import { open, exit } from "std";
(function (console, setTimeout, Worker) {
"use strict";
const IP = "{{ autopilot_ip }}",
URL = "udp://" + IP + ":7909",
LOG_FILE = "{{ log_dir }}/mavsdk-log",
IS_A_DRONE = {{ 'true' if is_a_drone else 'false' }},
SIMULATION = {{ 'true' if is_a_simulation else 'false' }};
const CONF_PATH = {{ json_module.dumps(configuration) }};
var conf_file = open(CONF_PATH, "r");
const configuration = JSON.parse(conf_file.readAsString());
conf_file.close();
const URL = "udp://" + configuration.autopilotIp + ":7909",
LOG_FILE = "{{ log_dir }}/mavsdk-log";
// Use a Worker to ensure the user script
// does not block the main script
......@@ -51,7 +54,7 @@ import { exit } from "std";
exit(exit_code);
}
if (IS_A_DRONE) {
if (configuration.isADrone) {
console.log("Connecting to aupilot\n");
connect();
}
......@@ -71,7 +74,7 @@ import { exit } from "std";
}
function load() {
if (IS_A_DRONE && SIMULATION) {
if (configuration.isADrone && configuration.isASimulation) {
takeOff();
}
......@@ -118,9 +121,9 @@ import { exit } from "std";
if (type === 'initialized') {
pubsubWorker.postMessage({
action: "run",
id: {{ id }},
id: configuration.id,
interval: FPS,
publish: IS_A_DRONE
publish: configuration.isADrone
});
load();
} else if (type === 'loaded') {
......@@ -132,10 +135,10 @@ import { exit } from "std";
can_update = true;
} else if (type === 'exited') {
worker.onmessage = null;
quit(IS_A_DRONE, e.data.exit);
quit(configuration.isADrone, e.data.exit);
} else {
console.log('Unsupported message type', type);
quit(IS_A_DRONE, 1);
quit(configuration.isADrone, 1);
}
};
}(console, setTimeout, Worker));
import {runPubsub} from "{{ qjs_wrapper }}";
import {runPubsub} from {{ json_module.dumps(qjs_wrapper) }};
import {Worker} from "os";
import {open} from "std";
const PORT = "4840";
const IPV6 = "{{ ipv6 }}";
const CONF_PATH = {{ json_module.dumps(configuration) }},
PORT = "4840";
let parent = Worker.parent;
var conf_file = open(CONF_PATH, "r");
const configuration = JSON.parse(conf_file.readAsString());
conf_file.close();
function handle_msg(e) {
switch(e.data.action) {
case "run":
runPubsub(IPV6, PORT, "{{ net_if }}", e.data.id, e.data.interval, e.data.publish);
runPubsub(configuration.multicastIp, PORT, configuration.netIf, e.data.id, e.data.interval, e.data.publish);
parent.postMessage({running: false});
parent.onmessage = null;
break;
......
......@@ -6,21 +6,34 @@ extends =
parts =
instance-profile
instance-default
instance-drone
main
pubsub
worker
slapos-cookbook
[download-file-base]
recipe = slapos.recipe.build:download
url = ${:_profile_base_location_}/${:filename}
destination = ${buildout:directory}/${:filename}
[instance-profile]
recipe = slapos.recipe.template
url = ${:_profile_base_location_}/${:filename}
output = ${buildout:directory}/template.cfg
[jinja-template-base]
recipe = slapos.recipe.template
url = ${:_profile_base_location_}/${:_buildout_section_name_}.cfg
output = ${buildout:directory}/${:_buildout_section_name_}.cfg
[instance-default]
<= jinja-template-base
[instance-drone]
<= jinja-template-base
[download-file-base]
recipe = slapos.recipe.build:download
url = ${:_profile_base_location_}/${:filename}
destination = ${buildout:directory}/${:filename}
[main]
<= download-file-base
......
{
"name": "JS Drone",
"description": "JS Drone",
"serialisation": "xml",
"serialisation": "json-in-xml",
"software-type": {
"default": {
"title": "Default",
"software-type": "default",
"description": "Default",
"description": "Drone Swarm",
"request": "instance-input-schema.json",
"response": "instance-output-schema.json",
"index": 0
},
"drone": {
"title": "Drone",
"software-type": "drone",
"description": "Drone Instance",
"request": "instance-drone-input-schema.json",
"response": "instance-drone-output-schema.json",
"index": 1
}
}
}
......@@ -17,16 +17,19 @@ import {
setManualControlInput,
setMessage,
setTargetCoordinates
} from "{{ qjs_wrapper }}";
} from {{ json_module.dumps(qjs_wrapper) }};
import * as std from "std";
import { Worker } from "os";
(function (console, Worker) {
// Every script is evaluated per drone
"use strict";
const drone_dict = {},
drone_id_list = [{{ drone_id_list }}],
IS_A_DRONE = {{ 'true' if is_a_drone else 'false' }};
const CONF_PATH = {{ json_module.dumps(configuration) }},
drone_dict = {};
var conf_file = std.open(CONF_PATH, "r");
const configuration = JSON.parse(conf_file.readAsString());
conf_file.close();
let parent = Worker.parent,
user_me = {
......@@ -50,7 +53,7 @@ import { Worker } from "os";
},
getInitialAltitude: getInitialAltitude,
getYaw: getYaw,
id: {{ id }},
id: configuration.id,
landed: landed,
loiter: loiter,
sendMsg: function(msg, id = -1) {
......@@ -86,16 +89,13 @@ import { Worker } from "os";
}
function handleMainMessage(evt) {
let type = evt.data.type,
message,
drone_id;
var type = evt.data.type, message, drone_id;
if (type === "initPubsub") {
initPubsub(drone_id_list.length);
for (let i = 0; i < drone_id_list.length; i++) {
drone_id = drone_id_list[i];
initPubsub(configuration.numberOfPeers);
for (drone_id = 0; drone_id < configuration.numberOfPeers; drone_id++) {
user_me.drone_dict[drone_id] = new Drone(drone_id);
user_me.drone_dict[drone_id].init(i);
user_me.drone_dict[drone_id].init(drone_id);
}
parent.postMessage({type: "initialized"});
} else if (type === "load") {
......@@ -117,7 +117,7 @@ import { Worker } from "os";
}
// Call the drone onStart function
if (user_me.hasOwnProperty("onUpdate")) {
if (IS_A_DRONE && isInManualMode()) {
if (configuration.isADrone && isInManualMode()) {
setManualControlInput();
}
user_me.onUpdate(evt.data.timestamp);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment