Commit 51fbcbb8 authored by Léo-Paul Géneau's avatar Léo-Paul Géneau :space_invader:

component/qjs-wrapper: v↑ qjs-wrapper (2.0 -> 2.1)

* Separate root and subscriber from drone SRs to avoid compilation of unnecessary dependencies
* display logs in debug mode
parent 50000c88
...@@ -9,7 +9,7 @@ parts = qjs-wrapper ...@@ -9,7 +9,7 @@ parts = qjs-wrapper
[qjs-wrapper-source] [qjs-wrapper-source]
recipe = slapos.recipe.build:gitclone recipe = slapos.recipe.build:gitclone
repository = https://lab.nexedi.com/nexedi/qjs-wrapper.git repository = https://lab.nexedi.com/nexedi/qjs-wrapper.git
revision = v2.0 revision = v2.1
git-executable = ${git:location}/bin/git git-executable = ${git:location}/bin/git
[qjs-wrapper] [qjs-wrapper]
......
...@@ -10,10 +10,12 @@ ...@@ -10,10 +10,12 @@
## Parameters ## ## Parameters ##
* autopilotType: Select which autopilot wrapper should be used
* autopilotIp: IPv4 address to identify the autopilot from the companion board * autopilotIp: IPv4 address to identify the autopilot from the companion board
* droneGuidList: List of computer id on which flight script must be deployed * droneGuidList: List of computer id on which flight script must be deployed
* droneNetIf: Drone network interface used for multicast traffic * droneNetIf: Drone network interface used for multicast traffic
* isASimulation: Must be set to 'true' to automatically take off during simulation * isASimulation: Must be set to 'true' to automatically take off during simulation
* debug: Must be set to 'true' to send drone logs through OPC-UA
* multicastIp: IPv6 of the multicast group of the swarm * multicastIp: IPv6 of the multicast group of the swarm
* flightScript: URL of user's script to execute to fly drone swarm * flightScript: URL of user's script to execute to fly drone swarm
* subscriberGuidList: List of computer id on which a GUI must be deployed * subscriberGuidList: List of computer id on which a GUI must be deployed
......
...@@ -14,32 +14,36 @@ ...@@ -14,32 +14,36 @@
# not need these here). # not need these here).
[index-html] [index-html]
_update_hash_filename_ = web-gui/index.html.jinja2 _update_hash_filename_ = web-gui/index.html.jinja2
md5sum = 8c18ed73d4b8212106f226d95c0ae3f3 md5sum = 1644d25ea48e35f4b50fbc31e899a74a
[instance-peer-base]
filename = instance-peer-base.cfg.in
md5sum = 01425a1c77e79788e1948398b9136724
[instance-profile] [instance-profile]
filename = instance.cfg.in filename = instance.cfg.in
md5sum = e0ea95530bf84edfc37eea994cc9ee86 md5sum = 9c99b9b11e886eee41210e83be239a78
[instance-default] [instance-root]
filename = instance-default.cfg.jinja2 filename = instance-root.cfg.jinja2
md5sum = 2572f4c8e0f9bda92a5db1909c81b932 md5sum = 626a93a93c89e0fdcab5c25eec1343da
[instance-peer] [instance-subscriber]
filename = instance-peer.cfg.jinja2.in filename = instance-subscriber.cfg.in
md5sum = 328ba59583e7b47ab9e1a184ac8a201f md5sum = 8559dc8c95e9232060be6db3e0af4379
[main] [main]
_update_hash_filename_ = drone-scripts/main.js.jinja2 _update_hash_filename_ = drone-scripts/main.js.jinja2
md5sum = d530a2f97e3796e6b2f95270252c3b70 md5sum = cfdb011b995e976b750fc2c707fbec78
[pubsub] [pubsub]
_update_hash_filename_ = drone-scripts/pubsub.js.jinja2 _update_hash_filename_ = drone-scripts/pubsub.js.jinja2
md5sum = 1555496ad591a31a845f33488d5c335d md5sum = 34a02101a607e60f4e422375beaf7fc2
[script-js] [script-js]
_update_hash_filename_ = web-gui/script.js.jinja2 _update_hash_filename_ = web-gui/script.js.jinja2
md5sum = 9a5e44ae134697abb5b257ef9cb5b22a md5sum = efd986b3685e50f73c17c9352804bae0
[worker] [worker]
_update_hash_filename_ = drone-scripts/worker.js.jinja2 _update_hash_filename_ = drone-scripts/worker.js.jinja2
md5sum = 39c25f9083151a18318ed8ee4ad4fdbe md5sum = 5fc7f9738d8230aeea9a9d25ea30f3f0
/*jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */ /*jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */
{% if isADrone -%}
/*global arm, console, close, dup2, exit, open, scriptArgs, setTimeout, start, /*global arm, console, close, dup2, exit, open, scriptArgs, setTimeout, start,
stop, stopPubsub, takeOffAndWait, Worker, SIGINT, SIGTERM*/ stop, stopPubsub, takeOffAndWait, Worker, SIGINT, SIGTERM*/
{% else -%}
/*global console, close, dup2, exit, open, scriptArgs, setTimeout, stopPubsub,
Worker, SIGINT, SIGTERM*/
{% endif -%}
import { import {
{% if isADrone -%}
arm, arm,
start, start,
stop, stop,
{% endif -%}
stopPubsub, stopPubsub,
{% if isADrone -%}
takeOffAndWait takeOffAndWait
} from {{ json_module.dumps(qjs_wrapper) }}; {% endif -%}
} from "{{ qjs_wrapper }}";
import { import {
Worker, Worker,
SIGINT, SIGINT,
...@@ -18,18 +27,25 @@ import { ...@@ -18,18 +27,25 @@ import {
} from "os"; } from "os";
import { err, exit, open, out } from "std"; import { err, exit, open, out } from "std";
{% if isADrone -%}
(function (arm, console, dup2, err, exit, open, out, scriptArgs, setTimeout, (function (arm, console, dup2, err, exit, open, out, scriptArgs, setTimeout,
start, stop, stopPubsub, takeOffAndWait, Worker, SIGINT, SIGTERM) { start, stop, stopPubsub, takeOffAndWait, Worker, SIGINT, SIGTERM) {
{% else -%}
(function (console, dup2, err, exit, open, out, scriptArgs, setTimeout,
stopPubsub, Worker, SIGINT, SIGTERM) {
{% endif -%}
"use strict"; "use strict";
var CONF_PATH = {{ json_module.dumps(configuration) }}, var CONF_PATH = "{{ configuration }}",
conf_file = open(CONF_PATH, "r"), conf_file = open(CONF_PATH, "r"),
configuration = JSON.parse(conf_file.readAsString()), configuration = JSON.parse(conf_file.readAsString()),
AUTOPILOT_CONNECTION_TIMEOUT = 5, AUTOPILOT_CONNECTION_TIMEOUT = 5,
MAVSDK_LOG_FILE_PATH = MAVSDK_LOG_FILE_PATH =
"{{ log_dir }}/mavsdk_" + new Date().toISOString() + ".log", "{{ log_dir }}/mavsdk_" + new Date().toISOString() + ".log",
LOG_FILE = QUICKJS_LOG_FILE_PATH =
open("{{ log_dir }}/quickjs_" + new Date().toISOString() + ".log", "w"), "{{ log_dir }}/quickjs_" + new Date().toISOString() + ".log",
QUICKJS_LOG_FILE =
open(QUICKJS_LOG_FILE_PATH, "w"),
pubsubWorker, pubsubWorker,
worker, worker,
user_script = scriptArgs[1], user_script = scriptArgs[1],
...@@ -40,8 +56,8 @@ import { err, exit, open, out } from "std"; ...@@ -40,8 +56,8 @@ import { err, exit, open, out } from "std";
conf_file.close(); conf_file.close();
// redirect stdout and stderr // redirect stdout and stderr
dup2(LOG_FILE.fileno(), out.fileno()); dup2(QUICKJS_LOG_FILE.fileno(), out.fileno());
dup2(LOG_FILE.fileno(), err.fileno()); dup2(QUICKJS_LOG_FILE.fileno(), err.fileno());
// Use a Worker to ensure the user script // Use a Worker to ensure the user script
// does not block the main script // does not block the main script
...@@ -51,13 +67,13 @@ import { err, exit, open, out } from "std"; ...@@ -51,13 +67,13 @@ import { err, exit, open, out } from "std";
// to prevent it to finish (and so, exit the quickjs process) // to prevent it to finish (and so, exit the quickjs process)
worker = new Worker("{{ worker_script }}"); worker = new Worker("{{ worker_script }}");
function quit(is_a_drone, exit_code) { function quit(exit_code) {
worker.onmessage = null; worker.onmessage = null;
stopPubsub(); stopPubsub();
if (is_a_drone) { {% if isADrone -%}
stop(); stop();
} {% endif -%}
LOG_FILE.close(); QUICKJS_LOG_FILE.close();
exit(exit_code); exit(exit_code);
} }
...@@ -78,6 +94,7 @@ import { err, exit, open, out } from "std"; ...@@ -78,6 +94,7 @@ import { err, exit, open, out } from "std";
} }
} }
{% if isADrone -%}
function connect() { function connect() {
var address = configuration.autopilotIp + ":" + configuration.autopilotPort; var address = configuration.autopilotIp + ":" + configuration.autopilotPort;
console.log("Will connect to", address); console.log("Will connect to", address);
...@@ -86,16 +103,17 @@ import { err, exit, open, out } from "std"; ...@@ -86,16 +103,17 @@ import { err, exit, open, out } from "std";
configuration.autopilotIp, configuration.autopilotIp,
configuration.autopilotPort, configuration.autopilotPort,
MAVSDK_LOG_FILE_PATH, MAVSDK_LOG_FILE_PATH,
AUTOPILOT_CONNECTION_TIMEOUT QUICKJS_LOG_FILE_PATH,
AUTOPILOT_CONNECTION_TIMEOUT,
configuration.debug
), ),
"Failed to connect to " + address "Failed to connect to " + address
); );
} }
if (configuration.isADrone) { console.log("Connecting to aupilot\n");
console.log("Connecting to aupilot\n"); connect();
connect(); {% endif -%}
}
pubsubWorker = new Worker("{{ pubsub_script }}"); pubsubWorker = new Worker("{{ pubsub_script }}");
pubsubWorker.onmessage = function (e) { pubsubWorker.onmessage = function (e) {
...@@ -106,15 +124,19 @@ import { err, exit, open, out } from "std"; ...@@ -106,15 +124,19 @@ import { err, exit, open, out } from "std";
worker.postMessage({type: "initPubsub"}); worker.postMessage({type: "initPubsub"});
{% if isADrone -%}
function takeOff() { function takeOff() {
exitOnFail(arm(), "Failed to arm"); exitOnFail(arm(), "Failed to arm");
takeOffAndWait(); takeOffAndWait();
} }
{% endif -%}
function load() { function load() {
if (configuration.isADrone && configuration.isASimulation) { {% if isADrone -%}
if (configuration.isASimulation) {
takeOff(); takeOff();
} }
{% endif -%}
// First argument must provide the user script path // First argument must provide the user script path
if (user_script === undefined) { if (user_script === undefined) {
...@@ -175,11 +197,16 @@ import { err, exit, open, out } from "std"; ...@@ -175,11 +197,16 @@ import { err, exit, open, out } from "std";
can_update = true; can_update = true;
} else if (type === 'exited') { } else if (type === 'exited') {
worker.onmessage = null; worker.onmessage = null;
quit(configuration.isADrone, e.data.exit); quit(e.data.exit);
} else { } else {
console.log('Unsupported message type', type); console.log('Unsupported message type', type);
exitWorker(1); exitWorker(1);
} }
}; };
{% if isADrone -%}
}(arm, console, dup2, err, exit, open, out, scriptArgs, setTimeout, start, stop, }(arm, console, dup2, err, exit, open, out, scriptArgs, setTimeout, start, stop,
stopPubsub, takeOffAndWait, Worker, SIGINT, SIGTERM)); stopPubsub, takeOffAndWait, Worker, SIGINT, SIGTERM));
{% else -%}
}(console, dup2, err, exit, open, out, scriptArgs, setTimeout, stopPubsub,
Worker, SIGINT, SIGTERM));
{% endif -%}
/*jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */ /*jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */
/*global console, open, runPubsub, Worker*/ /*global console, open, runPubsub, Worker*/
import {runPubsub} from {{ json_module.dumps(qjs_wrapper) }}; import {runPubsub} from "{{ qjs_wrapper }}";
import {Worker} from "os"; import {Worker} from "os";
import {open} from "std"; import {open} from "std";
(function (console, open, runPubsub, Worker) { (function (console, open, runPubsub, Worker) {
"use strict"; "use strict";
var CONF_PATH = {{ json_module.dumps(configuration) }}, var CONF_PATH = "{{ configuration }}",
PORT = "4840", PORT = "4840",
parent = Worker.parent, parent = Worker.parent,
conf_file = open(CONF_PATH, "r"), conf_file = open(CONF_PATH, "r"),
......
...@@ -5,23 +5,28 @@ ...@@ -5,23 +5,28 @@
updateLogAndProjection, Drone, Worker*/ updateLogAndProjection, Drone, Worker*/
import { import {
Drone, Drone,
{% if isADrone -%}
triggerParachute, triggerParachute,
getAirspeed, getAirspeed,
getAltitude, getAltitude,
getClimbRate, getClimbRate,
getInitialAltitude, getInitialAltitude,
gpsIsOk, gpsIsOk,
getLog,
getPosition, getPosition,
getYaw, getYaw,
{% endif -%}
initPubsub, initPubsub,
{% if isADrone -%}
isLanding, isLanding,
loiter, loiter,
setAirSpeed, setAirSpeed,
{% endif -%}
setMessage, setMessage,
{% if isADrone -%}
setTargetCoordinates, setTargetCoordinates,
updateLogAndProjection updateLogAndProjection
} from {{ json_module.dumps(qjs_wrapper) }}; {% endif -%}
} from "{{ qjs_wrapper }}";
import { import {
SIGTERM, SIGTERM,
WNOHANG, WNOHANG,
...@@ -35,15 +40,21 @@ import { ...@@ -35,15 +40,21 @@ import {
} from "os"; } from "os";
import { evalScript, fdopen, loadFile, open } from "std"; import { evalScript, fdopen, loadFile, open } from "std";
{% if isADrone -%}
(function (Drone, SIGTERM, WNOHANG, Worker, close, console, evalScript, exec, (function (Drone, SIGTERM, WNOHANG, Worker, close, console, evalScript, exec,
fdopen, getAltitude, getInitialAltitude, gpsIsOk, getPosition, fdopen, getAltitude, getInitialAltitude, gpsIsOk, getPosition,
getLog, getYaw, initPubsub, isLanding, kill, loadFile, loiter, open, getYaw, initPubsub, isLanding, kill, loadFile, loiter, open,
pipe, setAirSpeed, setMessage, setReadHandler, setTargetCoordinates, pipe, setAirSpeed, setMessage, setReadHandler, setTargetCoordinates,
triggerParachute, updateLogAndProjection, waitpid) { triggerParachute, updateLogAndProjection, waitpid) {
{% else -%}
(function (Drone, SIGTERM, WNOHANG, Worker, close, console, evalScript, exec,
fdopen, initPubsub, kill, loadFile, open, pipe, setMessage,
setReadHandler, waitpid) {
{% endif -%}
// Every script is evaluated per drone // Every script is evaluated per drone
"use strict"; "use strict";
var CONF_PATH = {{ json_module.dumps(configuration) }}, var CONF_PATH = "{{ configuration }}",
conf_file = open(CONF_PATH, "r"), conf_file = open(CONF_PATH, "r"),
configuration = JSON.parse(conf_file.readAsString()), configuration = JSON.parse(conf_file.readAsString()),
clientId, clientId,
...@@ -58,9 +69,12 @@ import { evalScript, fdopen, loadFile, open } from "std"; ...@@ -58,9 +69,12 @@ import { evalScript, fdopen, loadFile, open } from "std";
peer_dict = {}, peer_dict = {},
user_me = { user_me = {
//required to fly //required to fly
{% if isADrone -%}
triggerParachute: triggerParachute, triggerParachute: triggerParachute,
{% endif -%}
exit: exitWorker, exit: exitWorker,
getDroneDict: function () { return drone_dict; }, getDroneDict: function () { return drone_dict; },
{% if isADrone -%}
getAltitudeAbs: getAltitude, getAltitudeAbs: getAltitude,
getCurrentPosition: getPosition, getCurrentPosition: getPosition,
getInitialAltitude: getInitialAltitude, getInitialAltitude: getInitialAltitude,
...@@ -68,9 +82,14 @@ import { evalScript, fdopen, loadFile, open } from "std"; ...@@ -68,9 +82,14 @@ import { evalScript, fdopen, loadFile, open } from "std";
getYaw: getYaw, getYaw: getYaw,
getSpeed: getAirspeed, getSpeed: getAirspeed,
getClimbRate: getClimbRate, getClimbRate: getClimbRate,
{% endif -%}
id: configuration.id, id: configuration.id,
{% if isADrone -%}
isLanding: isLanding, isLanding: isLanding,
loiter: loiter, loiter: loiter,
setAirSpeed: setAirSpeed,
setTargetCoordinates: setTargetCoordinates,
{% endif -%}
sendMsg: function (msg, id) { sendMsg: function (msg, id) {
if (id === undefined) { id = -1; } if (id === undefined) { id = -1; }
setMessage(JSON.stringify({ setMessage(JSON.stringify({
...@@ -78,9 +97,7 @@ import { evalScript, fdopen, loadFile, open } from "std"; ...@@ -78,9 +97,7 @@ import { evalScript, fdopen, loadFile, open } from "std";
timestamp: Date.now(), timestamp: Date.now(),
dest_id: id dest_id: id
})); }));
}, }
setAirSpeed: setAirSpeed,
setTargetCoordinates: setTargetCoordinates
}; };
conf_file.close(); conf_file.close();
...@@ -140,7 +157,7 @@ import { evalScript, fdopen, loadFile, open } from "std"; ...@@ -140,7 +157,7 @@ import { evalScript, fdopen, loadFile, open } from "std";
], { ], {
block: false, block: false,
usePath: false, usePath: false,
file: {{ json_module.dumps(gwsocket_bin) }}, file: "{{ gwsocket_bin }}",
stdin: gwsocket_w_pipe[0], stdin: gwsocket_w_pipe[0],
stdout: gwsocket_r_pipe[1] stdout: gwsocket_r_pipe[1]
}); });
...@@ -233,21 +250,15 @@ import { evalScript, fdopen, loadFile, open } from "std"; ...@@ -233,21 +250,15 @@ import { evalScript, fdopen, loadFile, open } from "std";
} }
}); });
if (clientId !== undefined) {
log = getLog();
while (log.length > 0) {
user_me.writeWebsocketMessage(JSON.stringify({log: log}));
log = getLog();
}
}
// Call the drone onUpdate function // Call the drone onUpdate function
if (user_me.hasOwnProperty("onUpdate")) { if (user_me.hasOwnProperty("onUpdate")) {
user_me.onUpdate(evt.data.timestamp); user_me.onUpdate(evt.data.timestamp);
} }
if (evt.data.timestamp - last_log_timestamp >= 1000) { if (evt.data.timestamp - last_log_timestamp >= 1000) {
{% if isADrone -%}
updateLogAndProjection(); updateLogAndProjection();
{% endif -%}
last_log_timestamp = evt.data.timestamp; last_log_timestamp = evt.data.timestamp;
} }
...@@ -273,8 +284,14 @@ import { evalScript, fdopen, loadFile, open } from "std"; ...@@ -273,8 +284,14 @@ import { evalScript, fdopen, loadFile, open } from "std";
exitWorker(1); exitWorker(1);
} }
}; };
{% if isADrone -%}
}(Drone, SIGTERM, WNOHANG, Worker, close, console, evalScript, exec, }(Drone, SIGTERM, WNOHANG, Worker, close, console, evalScript, exec,
fdopen, getAltitude, getInitialAltitude, gpsIsOk, getPosition, getLog, getYaw, fdopen, getAltitude, getInitialAltitude, gpsIsOk, getPosition, getYaw,
initPubsub, isLanding, kill, loadFile, loiter, open, pipe, setAirSpeed, initPubsub, isLanding, kill, loadFile, loiter, open, pipe, setAirSpeed,
setMessage, setReadHandler, setTargetCoordinates, triggerParachute, setMessage, setReadHandler, setTargetCoordinates, triggerParachute,
updateLogAndProjection, waitpid)); updateLogAndProjection, waitpid));
{% else -%}
}(Drone, SIGTERM, WNOHANG, Worker, close, console, evalScript, exec,
fdopen, initPubsub, kill, loadFile, open, pipe, setMessage, setReadHandler,
waitpid));
{% endif -%}
...@@ -4,6 +4,16 @@ ...@@ -4,6 +4,16 @@
"description": "Parameters to instantiate JS drone", "description": "Parameters to instantiate JS drone",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"autopilotType": {
"title": "Type of the drone's autopilot",
"description": "Model of the autopilot used in the drones.",
"type": "string",
"default": "c-astral",
"enum": [
"c-astral",
"sqdr"
]
},
"autopilotIp": { "autopilotIp": {
"title": "IP address of the drone's autopilot", "title": "IP address of the drone's autopilot",
"description": "IP used to create a connection with the autopilot.", "description": "IP used to create a connection with the autopilot.",
...@@ -34,6 +44,12 @@ ...@@ -34,6 +44,12 @@
"type": "boolean", "type": "boolean",
"default": false "default": false
}, },
"debug": {
"title": "Set debug mode",
"description": "When debug mode is enabled, drone are publishing the script logs through OPC-UA.",
"type": "boolean",
"default": false
},
"multicastIpv6": { "multicastIpv6": {
"title": "IP of the multicast group", "title": "IP of the multicast group",
"description": "IP address used to communicate with the other drones.", "description": "IP address used to communicate with the other drones.",
......
...@@ -3,6 +3,18 @@ parts = ...@@ -3,6 +3,18 @@ parts =
qjs-launcher qjs-launcher
publish-connection-information publish-connection-information
eggs-directory = ${buildout:eggs-directory}
develop-eggs-directory = ${buildout:develop-eggs-directory}
offline = true
[slap-configuration]
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}
[directory] [directory]
recipe = slapos.cookbook:mkdirectory recipe = slapos.cookbook:mkdirectory
...@@ -16,15 +28,26 @@ log = $${:var}/log ...@@ -16,15 +28,26 @@ log = $${:var}/log
public = $${:srv}/public public = $${:srv}/public
service = $${:etc}/service service = $${:etc}/service
[peer-configuration]
recipe = slapos.recipe.template:jinja2
output = $${directory:etc}/configuration.json
extensions = jinja2.ext.do
extra-context =
context =
import json_module json
key parameter_dict slap-configuration:configuration
$${:extra-context}
inline =
{{ json_module.dumps(parameter_dict) }}
[js-dynamic-template] [js-dynamic-template]
recipe = slapos.recipe.template:jinja2 recipe = slapos.recipe.template:jinja2
rendered = $${directory:etc}/$${:_buildout_section_name_}.js rendered = $${directory:etc}/$${:_buildout_section_name_}.js
extra-context = extra-context =
context = context =
import json_module json key configuration peer-configuration:output
raw gwsocket_bin ${gwsocket:location}/bin/gwsocket key isADrone slap-configuration:configuration.isADrone
raw qjs_wrapper ${qjs-wrapper:location}/lib/libqjswrapper.so raw qjs_wrapper ${qjs-wrapper:location}/lib/libqjswrapper.so
raw configuration {{ configuration }}
$${:extra-context} $${:extra-context}
[main] [main]
...@@ -42,10 +65,13 @@ template = ${pubsub:target} ...@@ -42,10 +65,13 @@ template = ${pubsub:target}
[worker] [worker]
<= js-dynamic-template <= js-dynamic-template
template = ${worker:target} template = ${worker:target}
gwsocket_bin =
extra-context =
key gwsocket_bin :gwsocket_bin
[user] [user]
recipe = slapos.recipe.build:download recipe = slapos.recipe.build:download
url = {{ parameter_dict['flightScript'] }} url = $${slap-configuration:configuration.flightScript}
destination = $${directory:etc}/user.js destination = $${directory:etc}/user.js
offline = false offline = false
...@@ -54,41 +80,6 @@ recipe = slapos.cookbook:wrapper ...@@ -54,41 +80,6 @@ recipe = slapos.cookbook:wrapper
wrapper-path = $${directory:service}/qjs-launcher wrapper-path = $${directory:service}/qjs-launcher
command-line = ${quickjs:location}/bin/qjs $${main:rendered} $${user:target} command-line = ${quickjs:location}/bin/qjs $${main:rendered} $${user:target}
[script-js]
recipe = slapos.recipe.template:jinja2
template = ${script-js:target}
rendered = $${directory:public}/script.js
websocket-url = [{{ ipv6 }}]:{{ websocket_port }}
context =
raw websocket_url $${:websocket-url}
[index-html]
recipe = slapos.recipe.template:jinja2
template = ${index-html:target}
rendered = $${directory:public}/index.html
context =
raw nb_drones {{ parameter_dict['numberOfDrones'] }}
[httpd-port]
recipe = slapos.cookbook:free_port
minimum = 8080
maximum = 8090
ip = {{ ipv6 }}
[httpd]
recipe = slapos.cookbook:simplehttpserver
host = {{ ipv6 }}
port = $${httpd-port:port}
base-path = $${directory:public}
wrapper = $${directory:service}/http-server
log-file = $${directory:log}/httpd.log
use-hash-url = false
depends = $${index-html:rendered}
[publish-connection-information] [publish-connection-information]
recipe = slapos.cookbook:publish.serialised recipe = slapos.cookbook:publish.serialised
instance-path = $${directory:home} instance-path = $${directory:home}
{% if not parameter_dict['isADrone'] -%}
httpd-url = [$${httpd:host}]:$${httpd:port}
websocket-url = ws://$${script-js:websocket-url}
{% endif -%}
{
"$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"
},
"autopilotPort": {
"title": "Port of the drone's autopilot",
"description": "Port on which autopilot service is running.",
"type": "integer"
},
"numberOfDrones": {
"title": "Number of drones",
"description": "Number of drones in the swarm",
"type": "integer"
},
"numberOfSubscribers": {
"title": "Number of subscribers",
"description": "Number of subscribers of the swarm (entities able to listen/send OPC-UA messages from/to 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"
}
...@@ -11,12 +11,13 @@ ...@@ -11,12 +11,13 @@
<= slap-connection <= slap-connection
recipe = slapos.cookbook:request.serialised recipe = slapos.cookbook:request.serialised
{% if id < len(parameter_dict['droneGuidList']) -%} {% if id < len(parameter_dict['droneGuidList']) -%}
{% set sr_name = parameter_dict['autopilotType'] -%}
name = Drone{{ id }}_{{ guid }} name = Drone{{ id }}_{{ guid }}
{% else -%} {% else -%}
{% set sr_name = 'subscriber' -%}
name = Subscriber{{ len(parameter_dict['droneGuidList']) - id }}_{{ guid }} name = Subscriber{{ len(parameter_dict['droneGuidList']) - id }}_{{ guid }}
{% endif -%} {% endif -%}
software-url = ${:software-release-url} software-url = {{ '/'.join(software_url.split('/')[:-1]) + '/software-%s.cfg' % sr_name }}
software-type = peer
return = instance-path return = instance-path
sla-computer_guid = {{ guid }} sla-computer_guid = {{ guid }}
config-autopilotIp = {{ parameter_dict['autopilotIp'] }} config-autopilotIp = {{ parameter_dict['autopilotIp'] }}
...@@ -25,6 +26,7 @@ config-numberOfDrones = {{ dumps(len(parameter_dict['droneGuidList'])) }} ...@@ -25,6 +26,7 @@ config-numberOfDrones = {{ dumps(len(parameter_dict['droneGuidList'])) }}
config-numberOfSubscribers = {{ dumps(len(parameter_dict['subscriberGuidList'])) }} config-numberOfSubscribers = {{ dumps(len(parameter_dict['subscriberGuidList'])) }}
config-id = {{ dumps(id) }} config-id = {{ dumps(id) }}
config-isASimulation = {{ dumps(parameter_dict['isASimulation']) }} config-isASimulation = {{ dumps(parameter_dict['isASimulation']) }}
config-debug = {{ dumps(parameter_dict['debug']) }}
{% if id < len(parameter_dict['droneGuidList']) -%} {% if id < len(parameter_dict['droneGuidList']) -%}
{% do drone_id_list.append(id) %} {% do drone_id_list.append(id) %}
config-isADrone = {{ dumps(True) }} config-isADrone = {{ dumps(True) }}
......
[buildout]
extends =
${instance-peer-base:output}
[worker]
gwsocket_bin = ${gwsocket:location}/bin/gwsocket
[gwsocket-port]
recipe = slapos.cookbook:free_port
minimum = 6789
maximum = 6799
ip = $${slap-configuration:ipv6-random}
[peer-configuration]
extra-context =
key websocket_ip gwsocket-port:ip
key websocket_port gwsocket-port:port
inline =
{% do parameter_dict.__setitem__('websocketIp', websocket_ip) -%}
{% do parameter_dict.__setitem__('websocketPort', websocket_port) -%}
{{ json_module.dumps(parameter_dict) }}
[script-js]
recipe = slapos.recipe.template:jinja2
template = ${script-js:target}
rendered = $${directory:public}/script.js
websocket-url = [$${gwsocket-port:ip}]:$${gwsocket-port:port}
context =
key debug slap-configuration:configuration.debug
key websocket_url :websocket-url
[index-html]
recipe = slapos.recipe.template:jinja2
template = ${index-html:target}
rendered = $${directory:public}/index.html
context =
key debug slap-configuration:configuration.debug
key nb_drones slap-configuration:configuration.numberOfDrones
[httpd-port]
recipe = slapos.cookbook:free_port
minimum = 8080
maximum = 8090
ip = $${slap-configuration:ipv6-random}
[httpd]
recipe = slapos.cookbook:simplehttpserver
host = $${slap-configuration:ipv6-random}
port = $${httpd-port:port}
base-path = $${directory:public}
wrapper = $${directory:service}/http-server
log-file = $${directory:log}/httpd.log
use-hash-url = false
depends = $${index-html:rendered}
[publish-connection-information]
httpd-url = [$${httpd:host}]:$${httpd:port}
websocket-url = ws://$${script-js:websocket-url}
...@@ -8,9 +8,8 @@ offline = true ...@@ -8,9 +8,8 @@ offline = true
[switch-softwaretype] [switch-softwaretype]
recipe = slapos.cookbook:switch-softwaretype recipe = slapos.cookbook:switch-softwaretype
default = instance-default:output
peer = instance-peer:output
RootSoftwareInstance = $${:default} RootSoftwareInstance = $${:default}
default = instance-root:output
[slap-configuration] [slap-configuration]
recipe = slapos.cookbook:slapconfiguration.serialised recipe = slapos.cookbook:slapconfiguration.serialised
...@@ -19,19 +18,23 @@ partition = $${slap_connection:partition_id} ...@@ -19,19 +18,23 @@ partition = $${slap_connection:partition_id}
url = $${slap_connection:server_url} url = $${slap_connection:server_url}
key = $${slap_connection:key_file} key = $${slap_connection:key_file}
cert = $${slap_connection:cert_file} cert = $${slap_connection:cert_file}
software = $${slap-connection:software-release-url}
[dynamic-template-base] [instance-root]
recipe = slapos.recipe.template:jinja2 recipe = slapos.recipe.template:jinja2
url = ${instance-root:target}
output = $${buildout:directory}/$${:_buildout_section_name_}.cfg output = $${buildout:directory}/$${:_buildout_section_name_}.cfg
extra-context = extensions = jinja2.ext.do
context = context =
jsonkey default_parameter_dict :default-parameters jsonkey default_parameter_dict :default-parameters
key parameter_dict slap-configuration:configuration key parameter_dict slap-configuration:configuration
$${:extra-context} key software_url slap-configuration:software
default-parameters = default-parameters =
{ {
"autopilotType": "c-astral",
"autopilotIp": "192.168.27.1", "autopilotIp": "192.168.27.1",
"autopilotPort": 7909, "autopilotPort": 7909,
"debug": false,
"droneNetIf": "eth0", "droneNetIf": "eth0",
"flightScript": "https://lab.nexedi.com/nexedi/flight-scripts/-/raw/v2.0/default.js", "flightScript": "https://lab.nexedi.com/nexedi/flight-scripts/-/raw/v2.0/default.js",
"isASimulation": false, "isASimulation": false,
...@@ -40,41 +43,3 @@ default-parameters = ...@@ -40,41 +43,3 @@ default-parameters =
"subscriberGuidList":[], "subscriberGuidList":[],
"subscriberNetIf": "eth0" "subscriberNetIf": "eth0"
} }
[instance-default]
<= dynamic-template-base
url = ${instance-default:target}
extensions = jinja2.ext.do
[directory]
recipe = slapos.cookbook:mkdirectory
home = $${buildout:directory}
etc = $${:home}/etc
[gwsocket-port]
recipe = slapos.cookbook:free_port
minimum = 6789
maximum = 6799
ip = $${slap-configuration:ipv6-random}
[peer-configuration]
recipe = slapos.recipe.template:jinja2
output = $${directory:etc}/configuration.json
extensions = jinja2.ext.do
context =
import json_module json
key websocket_ip gwsocket-port:ip
key websocket_port gwsocket-port:port
key parameter_dict slap-configuration:configuration
inline =
{% do parameter_dict.__setitem__('websocketIp', websocket_ip) -%}
{% do parameter_dict.__setitem__('websocketPort', websocket_port) -%}
{{ json_module.dumps(parameter_dict) }}
[instance-peer]
<= dynamic-template-base
url = ${instance-peer:output}
extra-context =
key configuration peer-configuration:output
key ipv6 slap-configuration:ipv6-random
key websocket_port gwsocket-port:port
[buildout] [buildout]
extends = extends =
../../component/mavsdk/buildout.cfg ../../component/mavsdk/buildout.cfg
software-base.cfg software-peer-base.cfg
[c-astral-xml-definition] [c-astral-xml-definition]
...@@ -17,11 +17,12 @@ pre-configure += ...@@ -17,11 +17,12 @@ pre-configure +=
[c-astral-wrapper] [c-astral-wrapper]
recipe = slapos.recipe.cmmi recipe = slapos.recipe.cmmi
configure-command = true configure-command = true
url = https://lab.nexedi.com/nexedi/c-astral-wrapper/-/archive/v2.0/c-astral-wrapper-v2.0.tar.gz url = https://lab.nexedi.com/nexedi/c-astral-wrapper/-/archive/v2.1/c-astral-wrapper-v2.1.tar.gz
md5sum = ee2d05d225a57d17318282ff595fd498 md5sum = cca66724e1b7a61c1b9559fde95c420b
environment = environment =
CPLUS_INCLUDE_PATH=${qjs-wrapper-source:location}/include:${mavsdk:location}/include:${mavsdk:location}/include/mavsdk CPLUS_INCLUDE_PATH=${qjs-wrapper-source:location}/include:${mavsdk:location}/include:${mavsdk:location}/include/mavsdk
LDFLAGS=-L${mavsdk:location}/lib -Wl,-rpath=${mavsdk:location}/lib LDFLAGS=-L${mavsdk:location}/lib -Wl,-rpath=${mavsdk:location}/lib
[qjs-wrapper] [qjs-wrapper]
autopilot-wrapper = ${c-astral-wrapper:location} autopilot-wrapper = ${c-astral-wrapper:location}
make-options = WITH_AUTOPILOT=y
...@@ -3,43 +3,25 @@ extends = ...@@ -3,43 +3,25 @@ extends =
buildout.hash.cfg buildout.hash.cfg
../../stack/slapos.cfg ../../stack/slapos.cfg
../../component/qjs-wrapper/buildout.cfg ../../component/qjs-wrapper/buildout.cfg
../../component/gwsocket/buildout.cfg
parts = parts =
instance-profile instance-peer-base
slapos-cookbook slapos-cookbook
[instance-default] [instance-peer-base]
recipe = slapos.recipe.build:download
url = ${:_profile_base_location_}/${:filename}
[template-base]
recipe = slapos.recipe.template recipe = slapos.recipe.template
url = ${:_profile_base_location_}/${:filename} url = ${:_profile_base_location_}/${:filename}
[instance-peer]
<= template-base
output = ${buildout:directory}/${:_buildout_section_name_}
[instance-profile]
<= template-base
output = ${buildout:directory}/template.cfg output = ${buildout:directory}/template.cfg
[download] [download]
recipe = slapos.recipe.build:download recipe = slapos.recipe.build:download
url = ${:_profile_base_location_}/${:_update_hash_filename_} url = ${:_profile_base_location_}/${:_update_hash_filename_}
[index-html]
<= download
[main] [main]
<= download <= download
[pubsub] [pubsub]
<= download <= download
[script-js]
<= download
[worker] [worker]
<= download <= download
[buildout]
extends =
buildout.hash.cfg
../../stack/slapos.cfg
parts =
instance-profile
slapos-cookbook
[instance-root]
recipe = slapos.recipe.build:download
url = ${:_profile_base_location_}/${:filename}
[instance-profile]
recipe = slapos.recipe.template
url = ${:_profile_base_location_}/${:filename}
output = ${buildout:directory}/template.cfg
{ {
"name": "JS Drone C-Astral", "name": "JS Drone Root",
"description": "JS Drone with C-Astral's autopilot", "description": "Root instance requesting drones and subscribers",
"serialisation": "json-in-xml", "serialisation": "json-in-xml",
"software-type": { "software-type": {
"default": { "default": {
...@@ -10,14 +10,6 @@ ...@@ -10,14 +10,6 @@
"request": "instance-input-schema.json", "request": "instance-input-schema.json",
"response": "instance-output-schema.json", "response": "instance-output-schema.json",
"index": 0 "index": 0
},
"drone": {
"title": "Peer",
"software-type": "peer",
"description": "Peer Instance",
"request": "instance-peer-input-schema.json",
"response": "instance-peer-output-schema.json",
"index": 1
} }
} }
} }
[buildout] [buildout]
extends = extends =
software-base.cfg software-peer-base.cfg
[sqdr-source] [sqdr-source]
recipe = slapos.recipe.build:gitclone recipe = slapos.recipe.build:gitclone
repository = https://lab.nexedi.com/slaposdrone/squadrone.git repository = https://lab.nexedi.com/slaposdrone/squadrone.git
revision = v2.0 revision = v2.1
git-executable = ${git:location}/bin/git git-executable = ${git:location}/bin/git
[sqdr-wrapper] [sqdr-wrapper]
...@@ -18,3 +18,4 @@ environment = ...@@ -18,3 +18,4 @@ environment =
[qjs-wrapper] [qjs-wrapper]
autopilot-wrapper = ${sqdr-wrapper:location} autopilot-wrapper = ${sqdr-wrapper:location}
make-options = WITH_AUTOPILOT=y
{
"name": "JS Drone Squadrone",
"description": "JS Drone with Squadrone's autopilot",
"serialisation": "json-in-xml",
"software-type": {
"default": {
"title": "Default",
"software-type": "default",
"description": "Drone Swarm",
"request": "instance-input-schema.json",
"response": "instance-output-schema.json",
"index": 0
},
"drone": {
"title": "Peer",
"software-type": "peer",
"description": "Peer Instance",
"request": "instance-peer-input-schema.json",
"response": "instance-peer-output-schema.json",
"index": 1
}
}
}
[buildout]
extends =
../../component/gwsocket/buildout.cfg
software-peer-base.cfg
parts +=
instance-subscriber
[instance-subscriber]
recipe = slapos.recipe.template
url = ${:_profile_base_location_}/${:filename}
output = ${buildout:directory}/template.cfg
[instance-peer-base]
output = ${buildout:directory}/template-base.cfg
[index-html]
<= download
[script-js]
<= download
...@@ -35,7 +35,7 @@ import time ...@@ -35,7 +35,7 @@ import time
from contextlib import closing from contextlib import closing
from websocket import create_connection from websocket import create_connection
from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass from slapos.testing.testcase import installSoftwareUrlList, makeModuleSetUpAndTestCaseClass
''' '''
0. positionArray 0. positionArray
...@@ -48,8 +48,9 @@ from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass ...@@ -48,8 +48,9 @@ from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
1.2 air speed 1.2 air speed
1.3 climb rate 1.3 climb rate
2. message 2. message
3. log
''' '''
MONITORED_ITEM_NB = 3 MONITORED_ITEM_NB = 4
OPC_UA_PORT = 4840 OPC_UA_PORT = 4840
OPC_UA_NET_IF = 'lo' OPC_UA_NET_IF = 'lo'
MCAST_GRP = 'ff15::1111' MCAST_GRP = 'ff15::1111'
...@@ -109,10 +110,27 @@ SPEED_ARRAY_VALUES = (-72.419998, 15.93, -0.015) ...@@ -109,10 +110,27 @@ SPEED_ARRAY_VALUES = (-72.419998, 15.93, -0.015)
STRING_TYPE = 12 STRING_TYPE = 12
MESSAGE_CONTENT = b'{\\"next_checkpoint\\":1}' MESSAGE_CONTENT = b'{\\"next_checkpoint\\":1}'
TEST_MESSAGE = b'{"content":"' + MESSAGE_CONTENT + b'","dest_id":-1}' TEST_MESSAGE = b'{"content":"' + MESSAGE_CONTENT + b'","dest_id":-1}'
LOG = b''
setUpModule, SlapOSInstanceTestCase = makeModuleSetUpAndTestCaseClass( root_software_release_url = os.path.abspath(
os.path.abspath( os.path.join(os.path.dirname(__file__), '..', 'software-root.cfg'))
os.path.join(os.path.dirname(__file__), '..', 'software-c-astral.cfg'))) subscriber_software_release_url = os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', 'software-subscriber.cfg'))
c_astral_software_release_url = os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', 'software-c-astral.cfg'))
_, SlapOSInstanceTestCase = makeModuleSetUpAndTestCaseClass(
os.path.abspath(root_software_release_url))
def setUpModule():
installSoftwareUrlList(
SlapOSInstanceTestCase,
[root_software_release_url, subscriber_software_release_url,
c_astral_software_release_url],
debug=bool(int(os.environ.get('SLAPOS_TEST_DEBUG', 0))),
)
class SubscriberTestCase(SlapOSInstanceTestCase): class SubscriberTestCase(SlapOSInstanceTestCase):
...@@ -203,6 +221,12 @@ class SubscriberTestCase(SlapOSInstanceTestCase): ...@@ -203,6 +221,12 @@ class SubscriberTestCase(SlapOSInstanceTestCase):
ua_array += struct.pack(struct_type, value) ua_array += struct.pack(struct_type, value)
return ua_array return ua_array
def ua_string_encode(self, string):
ua_string = struct.pack('B', STRING_TYPE)
ua_string += struct.pack('I', len(string))
ua_string += string
return ua_string
def ua_dataSetMessage_encode(self): def ua_dataSetMessage_encode(self):
data_set_message = self.ua_dataSetMessageHeader_encode() data_set_message = self.ua_dataSetMessageHeader_encode()
data_set_message += struct.pack('H', MONITORED_ITEM_NB) data_set_message += struct.pack('H', MONITORED_ITEM_NB)
...@@ -216,9 +240,8 @@ class SubscriberTestCase(SlapOSInstanceTestCase): ...@@ -216,9 +240,8 @@ class SubscriberTestCase(SlapOSInstanceTestCase):
'f', 'f',
SPEED_ARRAY_VALUES, SPEED_ARRAY_VALUES,
) )
data_set_message += struct.pack('B', STRING_TYPE) data_set_message += self.ua_string_encode(TEST_MESSAGE)
data_set_message += struct.pack('I', len(TEST_MESSAGE)) data_set_message += self.ua_string_encode(LOG)
data_set_message += TEST_MESSAGE
return data_set_message return data_set_message
def send_ua_networkMessage(self): def send_ua_networkMessage(self):
...@@ -240,7 +263,6 @@ class SubscriberTestCase(SlapOSInstanceTestCase): ...@@ -240,7 +263,6 @@ class SubscriberTestCase(SlapOSInstanceTestCase):
for expected_process_name in expected_process_name_list: for expected_process_name in expected_process_name_list:
self.assertIn(expected_process_name, process_names) self.assertIn(expected_process_name, process_names)
def test_requested_instances(self): def test_requested_instances(self):
connection_parameter_dict = json.loads( connection_parameter_dict = json.loads(
self.computer_partition.getConnectionParameterDict()['_']) self.computer_partition.getConnectionParameterDict()['_'])
...@@ -256,6 +278,7 @@ class SubscriberTestCase(SlapOSInstanceTestCase): ...@@ -256,6 +278,7 @@ class SubscriberTestCase(SlapOSInstanceTestCase):
'numberOfDrones': 1, 'numberOfDrones': 1,
'numberOfSubscribers': 1, 'numberOfSubscribers': 1,
'id': 1, 'id': 1,
'debug': False,
'isASimulation': False, 'isASimulation': False,
'isADrone': False, 'isADrone': False,
'flightScript': 'https://lab.nexedi.com/nexedi/flight-scripts/-/raw/v2.0/subscribe.js', 'flightScript': 'https://lab.nexedi.com/nexedi/flight-scripts/-/raw/v2.0/subscribe.js',
...@@ -285,48 +308,26 @@ class SubscriberTestCase(SlapOSInstanceTestCase): ...@@ -285,48 +308,26 @@ class SubscriberTestCase(SlapOSInstanceTestCase):
b'Unknown instruction %s' % conn.sock.getsockname()[0].encode(), b'Unknown instruction %s' % conn.sock.getsockname()[0].encode(),
conn.recv_frame().data, conn.recv_frame().data,
) )
self.assertIn(
b'\\u001b[32minfo/userland\\u001b[0m\\tfieldsSize 3\\n"}',
ws.recv_frame().data,
)
self.assertIn(
b'\\u001b[32minfo/client\\u001b[0m\\tReceived position of drone 0: %.6f ? %.6f ? %.2f m %.2f m\\n"}' % (0, 0 , 0, 0),
ws.recv_frame().data,
)
self.assertIn(
b'\\u001b[32minfo/client\\u001b[0m\\tReceived speed of drone 0: %.2f ? %.2f m/s %.2f m/s\\n"}' % (0, 0 , 0),
ws.recv_frame().data,
)
self.assertIn(
b'\\u001b[32minfo/userland\\u001b[0m\\tfieldsSize 1\\n"}',
ws.recv_frame().data,
)
self.assertEqual( self.assertEqual(
conn.recv_frame().data, conn.recv_frame().data,
b''.join(( b''.join((
b'{"drone_dict":{"0":{"latitude":', b'{"drone_dict":{"0":{"latitude":',
b'"%.6f","longitude":"%.6f","altitude":"%.2f",' % (0, 0, 0), b'"%.6f","longitude":"%.6f","altitude":"%.2f",' % (0, 0, 0),
b'"yaw":"%.2f","speed":"%.2f","climbRate":"%.2f",' % (0, 0, 0), b'"yaw":"%.2f","speed":"%.2f","climbRate":"%.2f",' % (0, 0, 0),
b'"timestamp":%d}}}' % 0, b'"timestamp":%d,' % 0,
b'"log":""}}}',
)), )),
) )
self.send_ua_networkMessage() self.send_ua_networkMessage()
time.sleep(0.1) time.sleep(0.1)
self.assertEqual(conn.recv_frame().data, MESSAGE_CONTENT.replace(b'\\', b'')) self.assertEqual(conn.recv_frame().data, MESSAGE_CONTENT.replace(b'\\', b''))
self.assertIn(
b'\\u001b[32minfo/client\\u001b[0m\\tReceived position of drone 0: %.6f ? %.6f ? %.2f m %.2f m\\n"}' % POSITION_ARRAY_OUTPUT_VALUES,
conn.recv_frame().data,
)
self.assertIn(
b'\\u001b[32minfo/client\\u001b[0m\\tReceived speed of drone 0: %.2f ? %.2f m/s %.2f m/s\\n"}' % SPEED_ARRAY_VALUES,
conn.recv_frame().data,
)
self.assertEqual( self.assertEqual(
conn.recv_frame().data, conn.recv_frame().data,
b''.join(( b''.join((
b'{"drone_dict":{"0":{"latitude":', b'{"drone_dict":{"0":{"latitude":',
b'"%.6f","longitude":"%.6f","altitude":"%.2f",' % POSITION_ARRAY_OUTPUT_VALUES[:-1], b'"%.6f","longitude":"%.6f","altitude":"%.2f",' % POSITION_ARRAY_OUTPUT_VALUES[:-1],
b'"yaw":"%.2f","speed":"%.2f","climbRate":"%.2f",' % SPEED_ARRAY_VALUES, b'"yaw":"%.2f","speed":"%.2f","climbRate":"%.2f",' % SPEED_ARRAY_VALUES,
b'"timestamp":%d}}}' % POSITION_ARRAY_INPUT_VALUES[-1], b'"timestamp":%d,' % POSITION_ARRAY_INPUT_VALUES[-1],
b'"log":""}}}',
)), )),
) )
...@@ -26,10 +26,14 @@ ...@@ -26,10 +26,14 @@
min-width: 1028px; min-width: 1028px;
height: max-content; height: max-content;
} }
textarea {
resize: none;
}
th, td{ th, td{
padding: 1%; padding: 1%;
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
white-space: nowrap;
} }
.blue-text {color: blue} .blue-text {color: blue}
.connected {color: green} .connected {color: green}
...@@ -52,27 +56,27 @@ ...@@ -52,27 +56,27 @@
.white-text {color: white} .white-text {color: white}
.yellow-text {color: yellow} .yellow-text {color: yellow}
#drones-status {height: 50vh} #drones-status {height: 50vh}
#prompt {
background-color: rgb(18, 19, 20);
max-width: 1028px;
height: 20vh;
margin: auto;
}
#prompt-div {
height: max-content;
}
</style> </style>
</head> </head>
<body> <body>
<div class="container">
<label for="web-socket-status">web socket status:</label>
<output class="disconnected" id="web-socket-status">Disconnected</output>
</div>
<div id="prompt-div"> {% if debug -%}
<div class="container"> <div class="container">
<label for="web-socket-status">web socket status:</label> <table>
<output class="disconnected" id="web-socket-status">Disconnected</output> {% for i in range(int(nb_drones)) -%}
</div> <tr>
<pre id="prompt"></pre> <th>Drone {{ i }} logs</th>
<td><textarea id="log_{{ i }}" rows="4" cols="100" readonly></textarea><td>
</tr>
{% endfor %}
</table>
</div> </div>
{% endif -%}
<div class="container" id="drones-status"> <div class="container" id="drones-status">
<table> <table>
......
...@@ -12,10 +12,8 @@ ...@@ -12,10 +12,8 @@
FLIGHT_STATUS_BASE_ID = "flight_state_", FLIGHT_STATUS_BASE_ID = "flight_state_",
GREEN_BTN_CLASS_NAME = "green-button", GREEN_BTN_CLASS_NAME = "green-button",
LATITUDE_BASE_ID = "latitude_", LATITUDE_BASE_ID = "latitude_",
LOG_BASE_ID = "log_",
LONGITUDE_BASE_ID = "longitude_", LONGITUDE_BASE_ID = "longitude_",
PROMPT_COLOR_RE = /\u001b.{2,3}m/g,
PROMPT_ID = "prompt",
PROMPT_MAX_MSG,
QUIT_BTN_ID = "quit-btn", QUIT_BTN_ID = "quit-btn",
RED_BTN_CLASS_NAME = "red-button", RED_BTN_CLASS_NAME = "red-button",
SWITCH_BTN_ID = "switch-btn", SWITCH_BTN_ID = "switch-btn",
...@@ -66,80 +64,35 @@ ...@@ -66,80 +64,35 @@
date, date,
flight_state_cell, flight_state_cell,
i, i,
message = JSON.parse(event.data), message,
prompt,
new_div, new_div,
new_span, new_span,
text_array; text_array;
if (message.hasOwnProperty("drone_dict")) { try {
Object.entries(message.drone_dict).forEach(function ([id, drone]) { message = JSON.parse(event.data);
document.getElementById(LATITUDE_BASE_ID + id).innerHTML = drone["latitude"]; if (message.hasOwnProperty("drone_dict")) {
document.getElementById(LONGITUDE_BASE_ID + id).innerHTML = drone["longitude"]; Object.entries(message.drone_dict).forEach(function ([id, drone]) {
document.getElementById(ALTITUDE_BASE_ID + id).innerHTML = drone["altitude"]; document.getElementById(LATITUDE_BASE_ID + id).innerHTML = drone["latitude"];
document.getElementById(YAW_BASE_ID + id).innerHTML = drone["yaw"]; document.getElementById(LONGITUDE_BASE_ID + id).innerHTML = drone["longitude"];
document.getElementById(SPEED_BASE_ID + id).innerHTML = drone["speed"]; document.getElementById(ALTITUDE_BASE_ID + id).innerHTML = drone["altitude"];
document.getElementById(CLIMB_RATE_BASE_ID + id).innerHTML = drone["climbRate"]; document.getElementById(YAW_BASE_ID + id).innerHTML = drone["yaw"];
document.getElementById(TIMESTAMP_BASE_ID + id).innerHTML = new Date(drone["timestamp"]).toLocaleTimeString('fr-FR'); document.getElementById(SPEED_BASE_ID + id).innerHTML = drone["speed"];
}); document.getElementById(CLIMB_RATE_BASE_ID + id).innerHTML = drone["climbRate"];
} else if (message.hasOwnProperty("state") && message.hasOwnProperty("id")) { document.getElementById(TIMESTAMP_BASE_ID + id).innerHTML = new Date(drone["timestamp"]).toLocaleTimeString('fr-FR');
flight_state_cell = document.getElementById(FLIGHT_STATUS_BASE_ID + message['id']); {% if debug -%}
flight_state_cell.innerHTML = message['state']; document.getElementById(LOG_BASE_ID + id).value += drone["log"];
updateConnexionClass(flight_state_cell, message['inAir']); {% endif -%}
} else if(message.hasOwnProperty("log")) { });
prompt = document.getElementById(PROMPT_ID); } else if (message.hasOwnProperty("state") && message.hasOwnProperty("id")) {
if (PROMPT_MAX_MSG === undefined && prompt.children.length > 0) { flight_state_cell = document.getElementById(FLIGHT_STATUS_BASE_ID + message['id']);
PROMPT_MAX_MSG = Math.trunc( flight_state_cell.innerHTML = message['state'];
prompt.offsetHeight / prompt.children[0].offsetHeight updateConnexionClass(flight_state_cell, message['inAir']);
); } else {
console.info(message);
} }
} catch (error) {
new_div = document.createElement("div"); console.error(error, event.data);
text_array = message['log'].split(PROMPT_COLOR_RE);
color_array = message['log'].match(PROMPT_COLOR_RE);
for (i = 0; i < text_array.length; i++) {
new_span = document.createElement("span");
new_span.appendChild(document.createTextNode(text_array[i]));
if (i > 0 && i < color_array.length + 1) {
switch (color_array[i - 1]) {
case "\u001b[31m":
new_span.classList.add('red-text');
break;
case "\u001b[32m":
new_span.classList.add('green-text');
break;
case "\u001b[33m":
new_span.classList.add('yellow-text');
break;
case "\u001b[34m":
new_span.classList.add('blue-text');
break;
case "\u001b[35m":
new_span.classList.add('magenta-text');
break;
case "\u001b[36m":
new_span.classList.add('cyan-text');
break;
default:
new_span.classList.add('white-text');
break;
};
} else {
new_span.classList.add('white-text');
}
new_div.appendChild(new_span);
}
setTimeout(function() {
if (prompt.children.length === PROMPT_MAX_MSG) {
prompt.removeChild(prompt.firstElementChild);
}
prompt.appendChild(new_div);
}, 0);
} else {
console.info(message);
} }
}; };
......
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