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

software/js-drone: update default instance

Default software_type requests all required instances for the swarm.

See merge request nexedi/slapos!1264
parents e89e56bb 752cf414
Pipeline #26257 failed with stage
......@@ -5,6 +5,7 @@ extends =
../git/buildout.cfg
../jsoncpp/buildout.cfg
../tinyxml2/buildout.cfg
../zlib/buildout.cfg
parts =
mavsdk
......@@ -41,7 +42,7 @@ configure-options =
-DCMAKE_C_FLAGS="${:CMAKE_CFLAGS}"
-DCMAKE_CXX_FLAGS="${:CMAKE_CFLAGS}"
-DCMAKE_INSTALL_PREFIX=@@LOCATION@@
-DCMAKE_INSTALL_RPATH=${:CMAKE_LIBRARY_PATH}
-DCMAKE_INSTALL_RPATH=${:CMAKE_LIBRARY_PATH}:@@LOCATION@@/lib
-DPKG_CONFIG_EXECUTABLE=${pkgconfig:location}/bin/pkg-config
-DSUPERBUILD=OFF
-Bbuild/default
......@@ -53,7 +54,7 @@ environment =
CMAKE_LIBRARY_PATH=${:CMAKE_LIBRARY_PATH}
CMAKE_PROGRAM_PATH=${cmake:location}/bin
PATH=${pkgconfig:location}/bin/:%(PATH)s
LDFLAGS=-L${jsoncpp:location}/lib -Wl,-rpath=${jsoncpp:location}/lib
LDFLAGS=-L${curl:location}/lib -Wl,-rpath=${curl:location}/lib -L${jsoncpp:location}/lib -Wl,-rpath=${jsoncpp:location}/lib -L${tinyxml2:location}/lib -Wl,-rpath=${tinyxml2:location}/lib -L${zlib:location}/lib -Wl,-rpath=${zlib:location}/lib -Wl,-rpath=@@LOCATION@@/lib
CMAKE_CFLAGS=-I${tinyxml2:location}/include
CMAKE_LIBRARY_PATH=${curl:location}/lib:${jsoncpp:location}/lib:${tinyxml2:location}/lib
CMAKE_LIBRARY_PATH=${curl:location}/lib:${jsoncpp:location}/lib:${tinyxml2:location}/lib:${zlib:location}/lib
......@@ -8,10 +8,9 @@ parts = qjs-wrapper
[qjs-wrapper]
recipe = slapos.recipe.cmmi
shared = true
configure-command = true
url = https://lab.nexedi.com/nexedi/qjs-wrapper/-/archive/v1.0/qjs-wrapper-v1.0.tar.gz
md5sum = 0f1393fa15d46b2b551836197af9de46
url = https://lab.nexedi.com/nexedi/qjs-wrapper/-/archive/v1.2/qjs-wrapper-v1.2.tar.gz
md5sum = e335fc2251610c09dde3e74787317d98
environment =
C_INCLUDE_PATH=include:${open62541:location}/include:${open62541:location}/deps:${open62541:location}/src/pubsub:${quickjs:location}/include
CPLUS_INCLUDE_PATH=include:${mavsdk:location}/include:${mavsdk:location}/include/mavsdk
......
......@@ -14,7 +14,7 @@
# not need these here).
[instance-profile]
filename = instance.cfg
md5sum = 41f5acc071609a0c4b5ada295ede6bac
md5sum = 82c476f22e6b55b674640abef959c3c2
[template-fluentd]
filename = instance-fluentd.cfg
......
......@@ -42,7 +42,7 @@ recipe = slapos.recipe.build
slapparameter-dict = $${slap-configuration:configuration}
init =
import re
options['text'] = options['slapparameter-dict'].get('conf_text') or ''
options['text'] = options['slapparameter-dict'].get('conf_text') or ' '
options['port-list'] = re.findall(r'<source>.*port (\d+).*<\/source>', options['text'], re.DOTALL)
[fluentd-agent-conf]
......
......@@ -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 = 2118d7908a909c585e03531147b1d540
[pubsub]
filename = pubsub.js
md5sum = 4a0c63f9e088fa525a3699484d193c4d
md5sum = 1555496ad591a31a845f33488d5c335d
[worker]
filename = worker.js
md5sum = 5ed534e9ca56b9c0ee321b96b5d7c458
md5sum = 3893fb9228603e8c74f803476ec64dec
{% 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
/* global console */
/*jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */
/*global arm, console, exit, open, scriptArgs, setTimeout, start, stop,
stopPubsub, takeOffAndWait, Worker*/
import {
arm,
start,
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) {
(function (arm, console, exit, open, scriptArgs, setTimeout, start, stop,
stopPubsub, takeOffAndWait, Worker) {
"use strict";
const IP = "{{ autopilot_ip }}",
URL = "udp://" + IP + ":7909",
var CONF_PATH = {{ json_module.dumps(configuration) }},
conf_file = open(CONF_PATH, "r"),
configuration = JSON.parse(conf_file.readAsString()),
URL = "udp://" + configuration.autopilotIp + ":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' }};
pubsubWorker,
worker,
user_script = scriptArgs[1],
FPS = 50, // Minimum sampling interval for open62541 monitored items
previous_timestamp,
can_update = false;
conf_file.close();
// Use a Worker to ensure the user script
// does not block the main script
......@@ -23,17 +35,14 @@ import { exit } from "std";
// Create the update loop in the main script
// to prevent it to finish (and so, exit the quickjs process)
var pubsubWorker,
worker = new Worker("{{ worker_script }}"),
user_script = scriptArgs[1],
// Use the same FPS than browser's requestAnimationFrame
FPS = 1000 / 60,
previous_timestamp,
can_update = false;
worker = new Worker("{{ worker_script }}");
function connect() {
console.log("Will connect to", URL);
exitOnFail(start(URL, LOG_FILE, 60), "Failed to connect to " + URL);
function quit(is_a_drone, exit_code) {
stopPubsub();
if (is_a_drone) {
stop();
}
exit(exit_code);
}
function exitOnFail(ret, msg) {
......@@ -43,25 +52,22 @@ import { exit } from "std";
}
}
function quit(is_a_drone, exit_code) {
if (is_a_drone) {
stop();
}
stopPubsub();
exit(exit_code);
function connect() {
console.log("Will connect to", URL);
exitOnFail(start(URL, LOG_FILE, 60), "Failed to connect to " + URL);
}
if (IS_A_DRONE) {
if (configuration.isADrone) {
console.log("Connecting to aupilot\n");
connect();
}
pubsubWorker = new Worker("{{ pubsub_script }}");
pubsubWorker.onmessage = function(e) {
pubsubWorker.onmessage = function (e) {
if (!e.data.publishing) {
pubsubWorker.onmessage = null;
}
}
};
worker.postMessage({type: "initPubsub"});
......@@ -71,7 +77,7 @@ import { exit } from "std";
}
function load() {
if (IS_A_DRONE && SIMULATION) {
if (configuration.isADrone && configuration.isASimulation) {
takeOff();
}
......@@ -88,7 +94,7 @@ import { exit } from "std";
}
function loop() {
let timestamp = Date.now(),
var timestamp = Date.now(),
timeout;
if (can_update) {
if (FPS <= (timestamp - previous_timestamp)) {
......@@ -114,13 +120,13 @@ import { exit } from "std";
}
worker.onmessage = function (e) {
let type = e.data.type;
var type = e.data.type;
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 +138,11 @@ 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));
}(arm, console, exit, open, scriptArgs, setTimeout, start, stop, stopPubsub,
takeOffAndWait, Worker));
import {runPubsub} from "{{ qjs_wrapper }}";
/*jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */
/*global console, open, runPubsub, Worker*/
import {runPubsub} from {{ json_module.dumps(qjs_wrapper) }};
import {Worker} from "os";
import {open} from "std";
const PORT = "4840";
const IPV6 = "{{ ipv6 }}";
(function (console, open, runPubsub, Worker) {
"use strict";
let parent = Worker.parent;
var CONF_PATH = {{ json_module.dumps(configuration) }},
PORT = "4840",
parent = Worker.parent,
conf_file = open(CONF_PATH, "r"),
configuration = JSON.parse(conf_file.readAsString());
conf_file.close();
function handle_msg(e) {
switch(e.data.action) {
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;
default:
console.log("Undefined action from parent: ", e.data.action);
}
}
}
parent.onmessage = handle_msg;
parent.onmessage = handle_msg;
}(console, open, runPubsub, Worker));
\ No newline at end of file
......@@ -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
}
}
}
Tests for js-drone Software Release
##############################################################################
#
# Copyright (c) 2022 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
from setuptools import setup, find_packages
version = '0.0.1.dev0'
name = 'slapos.test.js_drone'
with open("README.md") as f:
long_description = f.read()
setup(name=name,
version=version,
description="Test for SlapOS' js-drone",
long_description=long_description,
long_description_content_type='text/markdown',
maintainer="Nexedi",
maintainer_email="info@nexedi.com",
url="https://lab.nexedi.com/nexedi/slapos",
packages=find_packages(),
install_requires=[
'slapos.core',
'slapos.libnetworkcache',
'erp5.util'
],
zip_safe=True,
test_suite='test',
)
##############################################################################
#
# Copyright (c) 2022 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
import ipaddress
import json
import os
import socket
import struct
import subprocess
import time
from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
MAIN_SCRIPT_NAME = 'main.js'
'''
0. positionArray
0.1 latitude
0.2 longitude
0.3 absolute altitude
0.4 relative altitude
1. speedArray
1.1 yaw angle
1.2 air speed
1.3 climb rate
2. message
'''
MONITORED_ITEM_NB = 3
OPC_UA_PORT = 4840
OPC_UA_NET_IF = 'lo'
MCAST_GRP = 'ff15::1111'
USER_SCRIPT_NAME = 'user.js'
# OPC UA Pub/Sub related constants
VERSION = 1
PUBLISHER_ID_ENABLED = True
GROUP_HEADER_ENABLED = True
PAYLOAD_HEADER_ENABLED = True
EXTENDED_FLAGS1_ENABLED = True
PUBLISHER_ID_TYPE = 1 #UInt16
DATA_SET_CLASS_ID_ENABLED = False
SECURITY_ENABLED = False
NETWORK_MESSAGE_TIMESTAMP_ENABLED = False
PICOSECONDS_ENABLED = False
EXTENDED_FLAGS2_ENABLED = False
PUBLISHER_ID = 0
WRITER_GROUP_ID_ENABLED = True
GROUP_VERSION_ENABLED = False
NETWORK_MESSAGE_NUMBER_ENABLED = False
SEQUENCE_NUMBER_ENABLED = False
WRITER_GROUP_ID = 1
DATASET_PAYLOAD_HEADER_COUNT = 1
DATA_SET_WRITER_ID = (1,)
DATA_SET_MESSAGE_VALIDE = 1
FIELD_ENCODING = 0 #UA_FIELDENCODING_VARIANT
DATA_SET_MESSAGE_SEQUENCE_NR_ENABLED = False
STATUS_ENABLED = False
CONFIG_VERSION_MAJOR_VERSION_ENABLED = True
CONFIG_VERSION_MINOR_VERSION_ENABLED = True
MESSAGE_HEADER_FLAGS2_ENABLED = True
DATA_SET_MESSAGE_TYPE = 0 #UA_DATASETMESSAGE_DATAKEYFRAME
DATA_SET_MESSAGE_TIMESTAMP_ENABLED = True
PICO_SECONDS_INCLUDED = False
UA_DATETIME_SEC = 10000000
UA_DATETIME_UNIX_EPOCH = 11644473600 * UA_DATETIME_SEC
CONFIG_VERSION_MAJOR_VERSION = 1690792766
CONFIG_VERSION_MINOR_VERSION = 1690781976
POSITION_ARRAY_TYPE = 11 #double
POSITION_ARRAY_VALUES = (45.64, 14.25, 686.61, 91.24)
SPEED_ARRAY_TYPE = 10 #float
SPEED_ARRAY_VALUES = (-72.419998, 15.93, -0.015)
STRING_TYPE = 12
TEST_MESSAGE = b'{"content":"{\\"next_checkpoint\\":1}","dest_id":-1}'
setUpModule, SlapOSInstanceTestCase = makeModuleSetUpAndTestCaseClass(
os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', 'software.cfg')))
class JSDroneTestCase(SlapOSInstanceTestCase):
@classmethod
def getInstanceParameterDict(cls):
return {
'_': json.dumps({
'netIf': OPC_UA_NET_IF,
'subscriberGuidList': [cls.slap._computer_id],
})
}
def get_partition(self, instance_type):
software_url = self.getSoftwareURL()
for computer_partition in self.slap.computer.getComputerPartitionList():
partition_url = computer_partition.getSoftwareRelease()._software_release
partition_type = computer_partition.getType()
if partition_url == software_url and partition_type == instance_type:
return computer_partition
raise Exception("JS-drone %s partition not found" % instance_type)
def setUp(self):
super().setUp()
subscriber_partition = self.get_partition('drone')
instance_path = json.loads(
subscriber_partition.getConnectionParameterDict()['_'])['instance-path']
quickjs_bin = os.path.join(instance_path, 'bin', 'qjs')
script_dir = os.path.join(instance_path, 'etc')
self.qjs_process = subprocess.Popen(
[
quickjs_bin,
os.path.join(script_dir, MAIN_SCRIPT_NAME),
os.path.join(script_dir, USER_SCRIPT_NAME),
],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
time.sleep(0.1)
def tearDown(self):
if self.qjs_process.returncode == None:
self.qjs_process.kill()
self.qjs_process.communicate()
super().tearDown()
def ua_networkMessage_encodeHeader(self):
ua_byte1 = int(VERSION)
ua_byte1 |= int(PUBLISHER_ID_ENABLED) << 4
ua_byte1 |= int(GROUP_HEADER_ENABLED) << 5
ua_byte1 |= int(PAYLOAD_HEADER_ENABLED) << 6
ua_byte1 |= int(EXTENDED_FLAGS1_ENABLED) << 7
ua_byte2 = PUBLISHER_ID_TYPE
ua_byte2 |= int(DATA_SET_CLASS_ID_ENABLED) << 3
ua_byte2 |= int(SECURITY_ENABLED) << 4
ua_byte2 |= int(NETWORK_MESSAGE_TIMESTAMP_ENABLED) << 5
ua_byte2 |= int(PICOSECONDS_ENABLED) << 6
ua_byte2 |= int(EXTENDED_FLAGS2_ENABLED) << 7
return struct.pack('BBH', ua_byte1, ua_byte2, PUBLISHER_ID)
def ua_groupHeader_encode(self):
ua_byte = int(WRITER_GROUP_ID_ENABLED)
ua_byte |= int(GROUP_VERSION_ENABLED) << 1
ua_byte |= int(NETWORK_MESSAGE_NUMBER_ENABLED) << 2
ua_byte |= int(SEQUENCE_NUMBER_ENABLED) << 3
return struct.pack('B', ua_byte) + struct.pack('H', WRITER_GROUP_ID)
def ua_payloadHeader_encode(self):
header = struct.pack('B', DATASET_PAYLOAD_HEADER_COUNT)
for data_set_writer_id in DATA_SET_WRITER_ID:
header += struct.pack('H', data_set_writer_id)
return header
def ua_networkMessage_encodeHeaders(self):
headers = self.ua_networkMessage_encodeHeader()
headers += self.ua_groupHeader_encode()
headers += self.ua_payloadHeader_encode()
return headers
def ua_dataSetMessageHeader_encode(self):
ua_byte1 = int(DATA_SET_MESSAGE_VALIDE)
ua_byte1 |= FIELD_ENCODING << 1
ua_byte1 |= int(DATA_SET_CLASS_ID_ENABLED) << 3
ua_byte1 |= int(STATUS_ENABLED) << 4
ua_byte1 |= int(CONFIG_VERSION_MAJOR_VERSION_ENABLED) << 5
ua_byte1 |= int(CONFIG_VERSION_MINOR_VERSION_ENABLED) << 6
ua_byte1 |= int(MESSAGE_HEADER_FLAGS2_ENABLED) << 7
ua_byte2 = DATA_SET_MESSAGE_TYPE
ua_byte2 |= int(DATA_SET_MESSAGE_TIMESTAMP_ENABLED) << 4
ua_byte2 |= int(PICO_SECONDS_INCLUDED) << 5
header = struct.pack('BB', ua_byte1, ua_byte2)
ua_datetime = time.time() * UA_DATETIME_SEC + UA_DATETIME_UNIX_EPOCH
return header + struct.pack('qII', int(ua_datetime), CONFIG_VERSION_MAJOR_VERSION, CONFIG_VERSION_MINOR_VERSION)
def ua_array_encode(self, type_id, struct_type, values):
encoding_mask = type_id
encoding_mask += 1 << 7 # is an array
ua_array = struct.pack('B', encoding_mask)
ua_array += struct.pack('I', len(values))
for value in values:
ua_array += struct.pack(struct_type, value)
return ua_array
def ua_dataSetMessage_encode(self):
data_set_message = self.ua_dataSetMessageHeader_encode()
data_set_message += struct.pack('H', MONITORED_ITEM_NB)
data_set_message += self.ua_array_encode(
POSITION_ARRAY_TYPE,
'd',
POSITION_ARRAY_VALUES,
)
data_set_message += self.ua_array_encode(
SPEED_ARRAY_TYPE,
'f',
SPEED_ARRAY_VALUES,
)
data_set_message += struct.pack('B', STRING_TYPE)
data_set_message += struct.pack('I', len(TEST_MESSAGE))
data_set_message += TEST_MESSAGE
return data_set_message
def send_ua_networkMessage(self):
ua_message = self.ua_networkMessage_encodeHeaders()
ua_message += self.ua_dataSetMessage_encode()
with socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP) as s:
s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
s.sendto(ua_message, ('::1', OPC_UA_PORT))
def test_requested_instances(self):
connection_parameter_dict = json.loads(
self.computer_partition.getConnectionParameterDict()['_'])
self.assertEqual(connection_parameter_dict['drone-id-list'], [])
self.assertEqual(connection_parameter_dict['subscriber-id-list'], [0])
def test_subscriber_instance_parameter_dict(self):
self.assertEqual(
json.loads(self.get_partition('drone').getInstanceParameterDict()['_']),
{
'autopilotIp': '192.168.27.1',
'id': 0,
'isASimulation': False,
'isADrone': False,
'flightScript': 'https://lab.nexedi.com/nexedi/flight-scripts/raw/master/subscribe.js',
'multicastIp': MCAST_GRP,
'numberOfPeers': 1,
'netIf': OPC_UA_NET_IF
}
)
def net_if_name_to_index(self, name):
with open('/sys/class/net/%s/ifindex' % name) as f:
index = int(f.readline())
return index
def test_joined_multicast_grp(self):
expected_string = '%s %s %s 1 00000004 0\n' % (
self.net_if_name_to_index(OPC_UA_NET_IF),
OPC_UA_NET_IF,
ipaddress.IPv6Address(MCAST_GRP).exploded.replace(':', ''),
)
with open('/proc/net/igmp6') as f:
self.assertIn(expected_string, f.readlines())
def test_pubsub_subscription(self):
self.send_ua_networkMessage()
time.sleep(0.1)
outs, _ = self.qjs_process.communicate(b'q\n', timeout=15)
decoded_out = outs.decode()
for line in (
'Subscription 0 | MonitoredItem %s' % MONITORED_ITEM_NB,
'Received position of drone 0: %f° %f° %fm %fm' % POSITION_ARRAY_VALUES,
'Received speed of drone 0: %f° %fm/s %fm/s' % SPEED_ARRAY_VALUES,
'Received message for drone 0: %s' % TEST_MESSAGE.decode(),
):
self.assertIn(line, decoded_out)
/* global console, std */
/*jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */
/*global console, getAltitude, getAltitudeRel, getInitialAltitude, getLatitude,
getLongitude, getYaw, execUserScript, initPubsub, isInManualMode, landed,
loiter, setAirspeed, setAltitude, setManualControlInput, setMessage,
setTargetCoordinates, std, triggerParachute, Drone, Worker*/
import {
Drone,
triggerParachute,
getAirspeed,
getAltitude,
getAltitudeRel,
getClimbRate,
getInitialAltitude,
getLatitude,
getLongitude,
......@@ -17,18 +23,22 @@ 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) {
(function (console, getAltitude, getAltitudeRel, getInitialAltitude,
getLatitude, getLongitude, getYaw, initPubsub, isInManualMode,
landed, loiter, setAirspeed, setAltitude, setManualControlInput,
setMessage, setTargetCoordinates, std, triggerParachute, Drone,
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' }};
let parent = Worker.parent,
var CONF_PATH = {{ json_module.dumps(configuration) }},
conf_file = std.open(CONF_PATH, "r"),
configuration = JSON.parse(conf_file.readAsString()),
parent = Worker.parent,
user_me = {
//for debugging purpose
fdopen: std.fdopen,
......@@ -36,12 +46,12 @@ import { Worker } from "os";
//required to fly
triggerParachute: triggerParachute,
drone_dict: {},
exit: function(exit_code) {
exit: function (exit_code) {
parent.postMessage({type: "exited", exit: exit_code});
parent.onmessage = null;
},
getAltitudeAbs: getAltitude,
getCurrentPosition: function() {
getCurrentPosition: function () {
return {
x: getLatitude(),
y: getLongitude(),
......@@ -50,28 +60,30 @@ import { Worker } from "os";
},
getInitialAltitude: getInitialAltitude,
getYaw: getYaw,
id: {{ id }},
getSpeed: getAirspeed,
getClimbRate: getClimbRate,
id: configuration.id,
landed: landed,
loiter: loiter,
sendMsg: function(msg, id = -1) {
sendMsg: function (msg, id) {
if (id === undefined) { id = -1; }
setMessage(JSON.stringify({ content: msg, dest_id: id }));
},
setAirspeed: setAirspeed,
setAltitude: setAltitude,
setTargetCoordinates: setTargetCoordinates
};
conf_file.close();
function loadUserScript(path) {
let script_content = std.loadFile(path);
var script_content = std.loadFile(path);
if (script_content === null) {
console.log("Failed to load user script " + path);
std.exit(1);
}
try {
std.evalScript(
"function execUserScript(from, me) {" +
script_content +
"};"
"function execUserScript(from, me) {" + script_content + "};"
);
} catch (e) {
console.log("Failed to evaluate user script", e);
......@@ -86,38 +98,32 @@ 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") {
loadUserScript(evt.data.path);
parent.postMessage({type: "loaded"});
} else if (type === "update") {
for (const [id, drone] of Object.entries(user_me.drone_dict)) {
message = drone.message
if (message.length > 0) {
Object.entries(user_me.drone_dict).forEach(function ([id, drone]) {
message = drone.message;
if (user_me.id !== Number(id) && message.length > 0) {
message = JSON.parse(message);
if (user_me.id === id) {
continue;
}
if (user_me.hasOwnProperty("onGetMsg") &&
[-1, user_me.id].includes(message.dest_id)) {
user_me.onGetMsg(message.content);
}
}
}
});
// 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);
......@@ -138,4 +144,7 @@ import { Worker } from "os";
std.exit(1);
}
};
}(console, Worker));
}(console, getAltitude, getAltitudeRel, getInitialAltitude, getLatitude,
getLongitude, getYaw, initPubsub, isInManualMode, landed, loiter, setAirspeed,
setAltitude, setManualControlInput, setMessage, setTargetCoordinates, std,
triggerParachute, Drone, Worker));
......@@ -34,7 +34,7 @@ md5sum = 0eeb24c6aa0760f0d33c4cc2828ddf30
[template-mariadb.cfg]
_update_hash_filename_ = instance-mariadb.cfg.jinja2.in
md5sum = bd5b0e72e8b5ddcd691e7f90a2947e50
md5sum = d1efa1148ef1d4fb3f2869f08ce85fd2
[template-my-cnf]
_update_hash_filename_ = templates/my.cnf.in
......
......@@ -209,7 +209,7 @@ wait-for-files =
[{{ section('odbc-ini') }}]
recipe = slapos.recipe.template
output = ${directory:etc}/odbc.ini
inline = {{ dumps(slapparameter_dict.get('odbc-ini', '')) }}
inline = {{ dumps(slapparameter_dict.get('odbc-ini', ' ')) }}
[{{ section('logrotate-entry-mariadb') }}]
< = logrotate-entry-base
......
......@@ -252,6 +252,11 @@ setup = ${slapos-repository:location}/software/mosquitto/test/
egg = slapos.test.peertube
setup = ${slapos-repository:location}/software/peertube/test/
[slapos.test.js-drone-setup]
<= setup-develop-egg
egg = slapos.test.js_drone
setup = ${slapos-repository:location}/software/js-drone/test/
[slapos.core-repository]
<= git-clone-repository
repository = https://lab.nexedi.com/nexedi/slapos.core.git
......@@ -316,6 +321,7 @@ eggs +=
${slapos.test.html5as-setup:egg}
${slapos.test.htmlvalidatorserver-setup:egg}
${slapos.test.hugo-setup:egg}
${slapos.test.js-drone-setup:egg}
${slapos.test.jscrawler-setup:egg}
${slapos.test.jstestnode-setup:egg}
${slapos.test.jupyter-setup:egg}
......@@ -407,6 +413,7 @@ tests =
html5as-base ${slapos.test.html5as-base-setup:setup}
htmlvalidatorserver ${slapos.test.htmlvalidatorserver-setup:setup}
hugo ${slapos.test.hugo-setup:setup}
js-drone ${slapos.test.js-drone-setup:setup}
jscrawler ${slapos.test.jscrawler-setup:setup}
jstestnode ${slapos.test.jstestnode-setup:setup}
jupyter ${slapos.test.jupyter-setup:setup}
......
......@@ -26,7 +26,7 @@ md5sum = d10b8e35b02b5391cf46bf0c7dbb1196
[template-mariadb]
filename = instance-mariadb.cfg.in
md5sum = cee995829fbd138a8c2c9209d72d01a0
md5sum = 93b2277185e4949a3d17be79d3710d2d
[template-kumofs]
filename = instance-kumofs.cfg.in
......
......@@ -193,7 +193,7 @@ environ =
[{{ section('odbc-ini') }}]
recipe = slapos.recipe.template
output = ${directory:etc}/odbc.ini
inline = {{ dumps(slapparameter_dict.get('odbc-ini', '')) }}
inline = {{ dumps(slapparameter_dict.get('odbc-ini', ' ')) }}
[{{ section('logrotate-entry-mariadb') }}]
< = logrotate-entry-base
......
......@@ -297,7 +297,7 @@ slapos.libnetworkcache = 0.25
slapos.rebootstrap = 4.5
slapos.recipe.build = 0.56
slapos.recipe.cmmi = 0.19
slapos.recipe.template = 5.0
slapos.recipe.template = 5.1
slapos.toolbox = 0.129
smmap = 5.0.0
sniffio = 1.3.0
......
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