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

Separate root and subscriber SRs

See merge request !1549
parents eae3a24e e5ef7b00
......@@ -15,12 +15,6 @@ parts =
[gcc]
min_version = 7.1
[c-astral-xml-definition]
recipe = slapos.recipe.build:gitclone
repository = https://lab.nexedi.com/nexedi/c-astral-c-library.git
revision = v2.1
git-executable = ${git:location}/bin/git
[mavsdk-source]
recipe = slapos.recipe.build:gitclone
repository = https://github.com/mavlink/MAVSDK.git
......@@ -53,7 +47,6 @@ cmake = ${cmake:location}/bin/cmake
depends = ${mavsdk-pythonpath:recipe}
pre-configure =
${git:location}/bin/git submodule update --init --recursive
sed -i 's#message_definitions/v1.0#${c-astral-xml-definition:location}#' ${mavsdk-source:location}/third_party/mavlink/CMakeLists.txt
configure-command =
${:cmake}
configure-options =
......@@ -72,12 +65,3 @@ make-binary =
environment = mavsdk-env
CMAKE_CFLAGS=-I${tinyxml2:location}/include
[c-astral-wrapper]
recipe = slapos.recipe.cmmi
configure-command = true
url = https://lab.nexedi.com/nexedi/c-astral-wrapper/-/archive/v2.0/c-astral-wrapper-v2.0.tar.gz
md5sum = ee2d05d225a57d17318282ff595fd498
environment =
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
[buildout]
extends =
../git/buildout.cfg
../mavsdk/buildout.cfg
../open62541/buildout.cfg
../quickjs/buildout.cfg
......@@ -10,13 +9,14 @@ parts = qjs-wrapper
[qjs-wrapper-source]
recipe = slapos.recipe.build:gitclone
repository = https://lab.nexedi.com/nexedi/qjs-wrapper.git
revision = v2.0
revision = v2.1
git-executable = ${git:location}/bin/git
[qjs-wrapper]
recipe = slapos.recipe.cmmi
configure-command = true
path = ${qjs-wrapper-source:location}
autopilot-wrapper =
environment =
C_INCLUDE_PATH=include:${open62541:location}/include:${open62541:location}/deps:${open62541:location}/src/pubsub:${quickjs:location}/include
LDFLAGS=-L${open62541:location}/lib -Wl,-rpath=${open62541:location}/lib -L${c-astral-wrapper:location}/lib -Wl,-rpath=${c-astral-wrapper:location}/lib
LDFLAGS=-L${open62541:location}/lib -Wl,-rpath=${open62541:location}/lib -L${:autopilot-wrapper}/lib -Wl,-rpath=${:autopilot-wrapper}/lib
......@@ -10,13 +10,17 @@
## Parameters ##
* autopilotType: Select which autopilot wrapper should be used
* autopilotIp: IPv4 address to identify the autopilot from the companion board
* droneGuidList: List of computer id on which flight script must be deployed
* droneNetIf: Drone network interface used for multicast traffic
* 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
* netIf: Network interface used for multicast traffic
* flightScript: URL of user's script to execute to fly drone swarm
* loopPeriod: Minimal period (in milliseconds) between 2 executions of the flight script loop
* subscriberGuidList: List of computer id on which a GUI must be deployed
* subscriberNetIf: Subscriber network interface used for multicast traffic
## How it works ##
......@@ -46,6 +50,7 @@ For each drone is displayed:
* the yaw angle in degrees
* the speed (ground speed for multicopters, airspeed for fixed wings) in meters per second
* the climb rate in meters per second
* the timestamp of the position in format hh:mm:ss
### Buttons
......
......@@ -14,32 +14,36 @@
# not need these here).
[index-html]
_update_hash_filename_ = web-gui/index.html.jinja2
md5sum = 1eedc017ecc9d1a6761dc2fff3bbab9b
md5sum = 1644d25ea48e35f4b50fbc31e899a74a
[instance-peer-base]
filename = instance-peer-base.cfg.in
md5sum = 01425a1c77e79788e1948398b9136724
[instance-profile]
filename = instance.cfg.in
md5sum = 80dae3e883663311d9814def78ee875a
md5sum = 4733c63573e6812c124b356dc146ffcc
[instance-default]
filename = instance-default.cfg.jinja2
md5sum = 9db922cc0fcaa67006a2d6b9b95b95fe
[instance-root]
filename = instance-root.cfg.jinja2
md5sum = 316f77c655540226f22dc7a6322624f1
[instance-peer]
filename = instance-peer.cfg.jinja2.in
md5sum = d12fbb134c587173ddff46ff1bc6ffe7
[instance-subscriber]
filename = instance-subscriber.cfg.in
md5sum = 8559dc8c95e9232060be6db3e0af4379
[main]
_update_hash_filename_ = drone-scripts/main.js.jinja2
md5sum = 9a8ec8a2778f63789f39291795f47e98
md5sum = 60146505ec8ea50d881d033f63b6725c
[pubsub]
_update_hash_filename_ = drone-scripts/pubsub.js.jinja2
md5sum = 1555496ad591a31a845f33488d5c335d
md5sum = 34a02101a607e60f4e422375beaf7fc2
[script-js]
_update_hash_filename_ = web-gui/script.js.jinja2
md5sum = e28492276416c2d84e770217ae97a88f
md5sum = efd986b3685e50f73c17c9352804bae0
[worker]
_update_hash_filename_ = drone-scripts/worker.js.jinja2
md5sum = 48540afedd5437129196d84832d2ed40
md5sum = 5fc7f9738d8230aeea9a9d25ea30f3f0
/*jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */
{% if isADrone -%}
/*global arm, console, close, dup2, exit, open, scriptArgs, setTimeout, start,
stop, stopPubsub, takeOffAndWait, Worker, SIGINT, SIGTERM*/
{% else -%}
/*global console, close, dup2, exit, open, scriptArgs, setTimeout, stopPubsub,
Worker, SIGINT, SIGTERM*/
{% endif -%}
import {
{% if isADrone -%}
arm,
start,
stop,
{% endif -%}
stopPubsub,
{% if isADrone -%}
takeOffAndWait
} from {{ json_module.dumps(qjs_wrapper) }};
{% endif -%}
} from "{{ qjs_wrapper }}";
import {
Worker,
SIGINT,
SIGTERM,
dup2,
setTimeout,
......@@ -17,30 +27,37 @@ import {
} from "os";
import { err, exit, open, out } from "std";
(function (arm, console, dup2, err, exit, open, out, scriptArgs,
setTimeout, start, stop, stopPubsub, takeOffAndWait, Worker,
SIGTERM) {
{% if isADrone -%}
(function (arm, console, dup2, err, exit, open, out, scriptArgs, setTimeout,
start, stop, stopPubsub, takeOffAndWait, Worker, SIGINT, SIGTERM) {
{% else -%}
(function (console, dup2, err, exit, open, out, scriptArgs, setTimeout,
stopPubsub, Worker, SIGINT, SIGTERM) {
{% endif -%}
"use strict";
var CONF_PATH = {{ json_module.dumps(configuration) }},
var CONF_PATH = "{{ configuration }}",
conf_file = open(CONF_PATH, "r"),
configuration = JSON.parse(conf_file.readAsString()),
AUTOPILOT_CONNECTION_TIMEOUT = 5,
MAVSDK_LOG_FILE_PATH =
"{{ log_dir }}/mavsdk_" + new Date().toISOString() + ".log",
LOG_FILE =
open("{{ log_dir }}/quickjs_" + new Date().toISOString() + ".log", "w"),
QUICKJS_LOG_FILE_PATH =
"{{ log_dir }}/quickjs_" + new Date().toISOString() + ".log",
QUICKJS_LOG_FILE =
open(QUICKJS_LOG_FILE_PATH, "w"),
pubsubWorker,
worker,
user_script = scriptArgs[1],
FPS = 50, // Minimum sampling interval for open62541 monitored items
LOOP_EXECUTION_PERIOD = configuration.loopPeriod,
previous_timestamp,
can_update = false;
conf_file.close();
// redirect stdout and stderr
dup2(LOG_FILE.fileno(), out.fileno());
dup2(LOG_FILE.fileno(), err.fileno());
dup2(QUICKJS_LOG_FILE.fileno(), out.fileno());
dup2(QUICKJS_LOG_FILE.fileno(), err.fileno());
// Use a Worker to ensure the user script
// does not block the main script
......@@ -50,13 +67,13 @@ import { err, exit, open, out } from "std";
// to prevent it to finish (and so, exit the quickjs process)
worker = new Worker("{{ worker_script }}");
function quit(is_a_drone, exit_code) {
function quit(exit_code) {
worker.onmessage = null;
stopPubsub();
if (is_a_drone) {
{% if isADrone -%}
stop();
}
LOG_FILE.close();
{% endif -%}
QUICKJS_LOG_FILE.close();
exit(exit_code);
}
......@@ -68,6 +85,7 @@ import { err, exit, open, out } from "std";
}
signal(SIGTERM, exitWorker.bind(null, 0));
signal(SIGINT, exitWorker.bind(null, 0));
function exitOnFail(ret, msg) {
if (ret) {
......@@ -76,6 +94,7 @@ import { err, exit, open, out } from "std";
}
}
{% if isADrone -%}
function connect() {
var address = configuration.autopilotIp + ":" + configuration.autopilotPort;
console.log("Will connect to", address);
......@@ -84,16 +103,17 @@ import { err, exit, open, out } from "std";
configuration.autopilotIp,
configuration.autopilotPort,
MAVSDK_LOG_FILE_PATH,
60
QUICKJS_LOG_FILE_PATH,
AUTOPILOT_CONNECTION_TIMEOUT,
configuration.debug
),
"Failed to connect to " + address
);
}
if (configuration.isADrone) {
console.log("Connecting to aupilot\n");
connect();
}
{% endif -%}
pubsubWorker = new Worker("{{ pubsub_script }}");
pubsubWorker.onmessage = function (e) {
......@@ -104,15 +124,19 @@ import { err, exit, open, out } from "std";
worker.postMessage({type: "initPubsub"});
{% if isADrone -%}
function takeOff() {
exitOnFail(arm(), "Failed to arm");
takeOffAndWait();
}
{% endif -%}
function load() {
if (configuration.isADrone && configuration.isASimulation) {
{% if isADrone -%}
if (configuration.isASimulation) {
takeOff();
}
{% endif -%}
// First argument must provide the user script path
if (user_script === undefined) {
......@@ -130,23 +154,23 @@ import { err, exit, open, out } from "std";
var timestamp = Date.now(),
timeout;
if (can_update) {
if (FPS <= (timestamp - previous_timestamp)) {
if (LOOP_EXECUTION_PERIOD <= (timestamp - previous_timestamp)) {
// Expected timeout between every update
can_update = false;
worker.postMessage({
type: "update",
timestamp: timestamp
});
// Try to stick to the expected FPS
timeout = FPS - (timestamp - previous_timestamp - FPS);
// Try to stick to the expected LOOP_EXECUTION_PERIOD
timeout = LOOP_EXECUTION_PERIOD - (timestamp - previous_timestamp - LOOP_EXECUTION_PERIOD);
previous_timestamp = timestamp;
} else {
timeout = FPS - (timestamp - previous_timestamp);
timeout = LOOP_EXECUTION_PERIOD - (timestamp - previous_timestamp);
}
} else {
// If timeout occurs, but update is not yet finished
// wait a bit
timeout = FPS / 4;
timeout = LOOP_EXECUTION_PERIOD / 4;
}
// Ensure loop is not done with timeout < 1ms
setTimeout(loop, Math.max(1, timeout));
......@@ -158,12 +182,12 @@ import { err, exit, open, out } from "std";
pubsubWorker.postMessage({
action: "run",
id: configuration.id,
interval: FPS,
interval: LOOP_EXECUTION_PERIOD,
publish: configuration.isADrone
});
load();
} else if (type === 'loaded') {
previous_timestamp = -FPS;
previous_timestamp = -LOOP_EXECUTION_PERIOD;
can_update = true;
// Start the update loop
loop();
......@@ -173,11 +197,16 @@ import { err, exit, open, out } from "std";
can_update = true;
} else if (type === 'exited') {
worker.onmessage = null;
quit(configuration.isADrone, e.data.exit);
quit(e.data.exit);
} else {
console.log('Unsupported message type', type);
exitWorker(1);
}
};
{% if isADrone -%}
}(arm, console, dup2, err, exit, open, out, scriptArgs, setTimeout, start, stop,
stopPubsub, takeOffAndWait, Worker, 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 */
/*global console, open, runPubsub, Worker*/
import {runPubsub} from {{ json_module.dumps(qjs_wrapper) }};
import {runPubsub} from "{{ qjs_wrapper }}";
import {Worker} from "os";
import {open} from "std";
(function (console, open, runPubsub, Worker) {
"use strict";
var CONF_PATH = {{ json_module.dumps(configuration) }},
var CONF_PATH = "{{ configuration }}",
PORT = "4840",
parent = Worker.parent,
conf_file = open(CONF_PATH, "r"),
......
......@@ -5,6 +5,7 @@
updateLogAndProjection, Drone, Worker*/
import {
Drone,
{% if isADrone -%}
triggerParachute,
getAirspeed,
getAltitude,
......@@ -13,14 +14,19 @@ import {
gpsIsOk,
getPosition,
getYaw,
{% endif -%}
initPubsub,
{% if isADrone -%}
isLanding,
loiter,
setAirSpeed,
{% endif -%}
setMessage,
{% if isADrone -%}
setTargetCoordinates,
updateLogAndProjection
} from {{ json_module.dumps(qjs_wrapper) }};
{% endif -%}
} from "{{ qjs_wrapper }}";
import {
SIGTERM,
WNOHANG,
......@@ -34,15 +40,21 @@ import {
} from "os";
import { evalScript, fdopen, loadFile, open } from "std";
{% if isADrone -%}
(function (Drone, SIGTERM, WNOHANG, Worker, close, console, evalScript, exec,
fdopen, getAltitude, getInitialAltitude, gpsIsOk, getPosition,
getYaw, initPubsub, kill, isLanding, loadFile, loiter, open, pipe,
setAirSpeed, setMessage, setReadHandler, setTargetCoordinates,
getYaw, initPubsub, isLanding, kill, loadFile, loiter, open,
pipe, setAirSpeed, setMessage, setReadHandler, setTargetCoordinates,
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
"use strict";
var CONF_PATH = {{ json_module.dumps(configuration) }},
var CONF_PATH = "{{ configuration }}",
conf_file = open(CONF_PATH, "r"),
configuration = JSON.parse(conf_file.readAsString()),
clientId,
......@@ -57,9 +69,12 @@ import { evalScript, fdopen, loadFile, open } from "std";
peer_dict = {},
user_me = {
//required to fly
{% if isADrone -%}
triggerParachute: triggerParachute,
{% endif -%}
exit: exitWorker,
getDroneDict: function () { return drone_dict; },
{% if isADrone -%}
getAltitudeAbs: getAltitude,
getCurrentPosition: getPosition,
getInitialAltitude: getInitialAltitude,
......@@ -67,9 +82,14 @@ import { evalScript, fdopen, loadFile, open } from "std";
getYaw: getYaw,
getSpeed: getAirspeed,
getClimbRate: getClimbRate,
{% endif -%}
id: configuration.id,
{% if isADrone -%}
isLanding: isLanding,
loiter: loiter,
setAirSpeed: setAirSpeed,
setTargetCoordinates: setTargetCoordinates,
{% endif -%}
sendMsg: function (msg, id) {
if (id === undefined) { id = -1; }
setMessage(JSON.stringify({
......@@ -77,9 +97,7 @@ import { evalScript, fdopen, loadFile, open } from "std";
timestamp: Date.now(),
dest_id: id
}));
},
setAirSpeed: setAirSpeed,
setTargetCoordinates: setTargetCoordinates
}
};
conf_file.close();
......@@ -139,7 +157,7 @@ import { evalScript, fdopen, loadFile, open } from "std";
], {
block: false,
usePath: false,
file: {{ json_module.dumps(gwsocket_bin) }},
file: "{{ gwsocket_bin }}",
stdin: gwsocket_w_pipe[0],
stdout: gwsocket_r_pipe[1]
});
......@@ -149,9 +167,6 @@ import { evalScript, fdopen, loadFile, open } from "std";
handleWebSocketMessage = function () {
var message = readMessage(gwsocket_r_pipe_fd).data;
if (message.includes(configuration.websocketIp)) {
return;
}
onMessage(message);
};
user_me.writeWebsocketMessage = function (message) {
......@@ -200,16 +215,16 @@ import { evalScript, fdopen, loadFile, open } from "std";
}
function handleMainMessage(evt) {
var type = evt.data.type, message, peer_id;
var type = evt.data.type, message, peer_id, log;
switch (type) {
case "initPubsub":
initPubsub(configuration.numberOfDrone, configuration.numberOfSubscriber);
for (peer_id = 0; peer_id < configuration.numberOfDrone + configuration.numberOfSubscriber; peer_id++) {
initPubsub(configuration.numberOfDrones, configuration.numberOfSubscribers);
for (peer_id = 0; peer_id < configuration.numberOfDrones + configuration.numberOfSubscribers; peer_id++) {
peer_dict[peer_id] = new Drone(peer_id);
peer_dict[peer_id].init(peer_id);
if (peer_id < configuration.numberOfDrone) {
if (peer_id < configuration.numberOfDrones) {
drone_dict[peer_id] = peer_dict[peer_id];
}
}
......@@ -234,13 +249,16 @@ import { evalScript, fdopen, loadFile, open } from "std";
}
}
});
// Call the drone onStart function
// Call the drone onUpdate function
if (user_me.hasOwnProperty("onUpdate")) {
user_me.onUpdate(evt.data.timestamp);
}
if (evt.data.timestamp - last_log_timestamp >= 1000) {
{% if isADrone -%}
updateLogAndProjection();
{% endif -%}
last_log_timestamp = evt.data.timestamp;
}
......@@ -266,8 +284,14 @@ import { evalScript, fdopen, loadFile, open } from "std";
exitWorker(1);
}
};
{% if isADrone -%}
}(Drone, SIGTERM, WNOHANG, Worker, close, console, evalScript, exec,
fdopen, getAltitude, getInitialAltitude, gpsIsOk, getPosition, getYaw,
initPubsub, isLanding, kill, loadFile, loiter, open, pipe, setAirSpeed,
setMessage, setReadHandler, setTargetCoordinates, triggerParachute,
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 @@
"description": "Parameters to instantiate JS drone",
"additionalProperties": false,
"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": {
"title": "IP address of the drone's autopilot",
"description": "IP used to create a connection with the autopilot.",
......@@ -22,35 +32,53 @@
"type": "array",
"default": []
},
"droneNetIf": {
"title": "Drones Network interface",
"description": "Interface used for multicast traffic.",
"type": "string",
"default": "eth0"
},
"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
},
"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": {
"title": "IP of the multicast group",
"description": "IP address used to communicate with the other drones.",
"type": "string",
"default": "ff15::1111"
},
"netIf": {
"title": "Network interface",
"description": "Interface used for multicast traffic.",
"type": "string",
"default": "eth0"
},
"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/v2.0/default.js"
},
"loopPeriod": {
"title": "Loop execution period",
"description": "Minimal period between 2 executions of flight script loop",
"type": "integer",
"default": 200
},
"subscriberGuidList": {
"title": "List of subscribers computer ID",
"description": "List of computer ID of swarms subscribers",
"description": "List of computer ID of swarms subscribers (entities able to listen/send OPC-UA messages from/to the swarm)",
"type": "array",
"default": []
},
"subscriberNetIf": {
"title": "Subscribers Network interface",
"description": "Interface used for multicast traffic.",
"type": "string",
"default": "eth0"
}
}
}
......@@ -3,6 +3,18 @@ parts =
qjs-launcher
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]
recipe = slapos.cookbook:mkdirectory
......@@ -16,15 +28,26 @@ log = $${:var}/log
public = $${:srv}/public
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]
recipe = slapos.recipe.template:jinja2
rendered = $${directory:etc}/$${:_buildout_section_name_}.js
extra-context =
context =
import json_module json
raw gwsocket_bin ${gwsocket:location}/bin/gwsocket
key configuration peer-configuration:output
key isADrone slap-configuration:configuration.isADrone
raw qjs_wrapper ${qjs-wrapper:location}/lib/libqjswrapper.so
raw configuration {{ configuration }}
$${:extra-context}
[main]
......@@ -42,10 +65,13 @@ template = ${pubsub:target}
[worker]
<= js-dynamic-template
template = ${worker:target}
gwsocket_bin =
extra-context =
key gwsocket_bin :gwsocket_bin
[user]
recipe = slapos.recipe.build:download
url = {{ parameter_dict['flightScript'] }}
url = $${slap-configuration:configuration.flightScript}
destination = $${directory:etc}/user.js
offline = false
......@@ -54,41 +80,6 @@ recipe = slapos.cookbook:wrapper
wrapper-path = $${directory:service}/qjs-launcher
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['numberOfDrone'] }}
[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]
recipe = slapos.cookbook:publish.serialised
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"
},
"numberOfDrone": {
"title": "Number of drone",
"description": "Number of drone in the swarm",
"type": "integer"
},
"numberOfSubscriber": {
"title": "Number of subscriber",
"description": "Number of subscriber of 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"
}
......@@ -10,28 +10,36 @@
[{{ request_peer_section_title }}]
<= slap-connection
recipe = slapos.cookbook:request.serialised
name = Peer{{ id }}
software-url = ${:software-release-url}
software-type = peer
{% if id < len(parameter_dict['droneGuidList']) -%}
{% set sr_name = parameter_dict['autopilotType'] -%}
name = Drone{{ id }}_{{ guid }}
{% else -%}
{% set sr_name = 'subscriber' -%}
name = Subscriber{{ len(parameter_dict['droneGuidList']) - id }}_{{ guid }}
{% endif -%}
software-url = {{ '/'.join(software_url.split('/')[:-1]) + '/software-%s.cfg' % sr_name }}
return = instance-path
sla-computer_guid = {{ guid }}
config-autopilotIp = {{ parameter_dict['autopilotIp'] }}
config-autopilotPort = {{ dumps(parameter_dict['autopilotPort']) }}
config-numberOfDrone = {{ dumps(len(parameter_dict['droneGuidList'])) }}
config-numberOfSubscriber = {{ dumps(len(parameter_dict['subscriberGuidList'])) }}
config-numberOfDrones = {{ dumps(len(parameter_dict['droneGuidList'])) }}
config-numberOfSubscribers = {{ dumps(len(parameter_dict['subscriberGuidList'])) }}
config-id = {{ dumps(id) }}
config-isASimulation = {{ dumps(parameter_dict['isASimulation']) }}
config-debug = {{ dumps(parameter_dict['debug']) }}
config-loopPeriod = {{ dumps(parameter_dict['loopPeriod']) }}
{% if id < len(parameter_dict['droneGuidList']) -%}
{% do drone_id_list.append(id) %}
config-isADrone = {{ dumps(True) }}
config-flightScript = {{ parameter_dict['flightScript'] }}
config-netIf = {{ parameter_dict['droneNetIf'] }}
{% else -%}
{% do subscriber_id_list.append(id) %}
config-isADrone = {{ dumps(False) }}
config-flightScript = https://lab.nexedi.com/nexedi/flight-scripts/-/raw/v2.0/subscribe.js
config-netIf = {{ parameter_dict['subscriberNetIf'] }}
{% endif -%}
config-multicastIp = {{ parameter_dict['multicastIp'] }}
config-netIf = {{ parameter_dict['netIf'] }}
{% endfor %}
[publish-connection-information]
......
[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
[switch-softwaretype]
recipe = slapos.cookbook:switch-softwaretype
default = instance-default:output
peer = instance-peer:output
RootSoftwareInstance = $${:default}
default = instance-root:output
[slap-configuration]
recipe = slapos.cookbook:slapconfiguration.serialised
......@@ -19,61 +18,29 @@ partition = $${slap_connection:partition_id}
url = $${slap_connection:server_url}
key = $${slap_connection:key_file}
cert = $${slap_connection:cert_file}
software = $${slap-connection:software-release-url}
[dynamic-template-base]
[instance-root]
recipe = slapos.recipe.template:jinja2
url = ${instance-root:target}
output = $${buildout:directory}/$${:_buildout_section_name_}.cfg
extra-context =
extensions = jinja2.ext.do
context =
jsonkey default_parameter_dict :default-parameters
key parameter_dict slap-configuration:configuration
$${:extra-context}
key software_url slap-configuration:software
default-parameters =
{
"autopilotType": "c-astral",
"autopilotIp": "192.168.27.1",
"autopilotPort": 7909,
"debug": false,
"droneGuidList": [],
"droneNetIf": "eth0",
"flightScript": "https://lab.nexedi.com/nexedi/flight-scripts/-/raw/v2.0/default.js",
"isASimulation": false,
"loopPeriod": 200,
"multicastIp": "ff15::1111",
"netIf": "eth0",
"droneGuidList": [],
"subscriberGuidList":[]
"subscriberGuidList":[],
"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]
extends =
../../component/mavsdk/buildout.cfg
software-peer-base.cfg
[c-astral-xml-definition]
recipe = slapos.recipe.build:gitclone
repository = https://lab.nexedi.com/nexedi/c-astral-c-library.git
revision = v2.1
git-executable = ${git:location}/bin/git
[mavsdk]
pre-configure +=
sed -i 's#message_definitions/v1.0#${c-astral-xml-definition:location}#' ${mavsdk-source:location}/third_party/mavlink/CMakeLists.txt
[c-astral-wrapper]
recipe = slapos.recipe.cmmi
configure-command = true
url = https://lab.nexedi.com/nexedi/c-astral-wrapper/-/archive/v2.1/c-astral-wrapper-v2.1.tar.gz
md5sum = cca66724e1b7a61c1b9559fde95c420b
environment =
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
[qjs-wrapper]
autopilot-wrapper = ${c-astral-wrapper:location}
make-options = WITH_AUTOPILOT=y
......@@ -3,43 +3,25 @@ extends =
buildout.hash.cfg
../../stack/slapos.cfg
../../component/qjs-wrapper/buildout.cfg
../../component/gwsocket/buildout.cfg
parts =
instance-profile
instance-peer-base
slapos-cookbook
[instance-default]
recipe = slapos.recipe.build:download
url = ${:_profile_base_location_}/${:filename}
[template-base]
[instance-peer-base]
recipe = slapos.recipe.template
url = ${:_profile_base_location_}/${:filename}
[instance-peer]
<= template-base
output = ${buildout:directory}/${:_buildout_section_name_}
[instance-profile]
<= template-base
output = ${buildout:directory}/template.cfg
[download]
recipe = slapos.recipe.build:download
url = ${:_profile_base_location_}/${:_update_hash_filename_}
[index-html]
<= download
[main]
<= download
[pubsub]
<= download
[script-js]
<= download
[worker]
<= 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",
"description": "JS Drone",
"name": "JS Drone Root",
"description": "Root instance requesting drones and subscribers",
"serialisation": "json-in-xml",
"software-type": {
"default": {
......@@ -10,14 +10,6 @@
"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 =
software.cfg
software-peer-base.cfg
[sqdr-source]
recipe = slapos.recipe.build:gitclone
repository = https://lab.nexedi.com/slaposdrone/squadrone.git
revision = v2.0
revision = v2.1
git-executable = ${git:location}/bin/git
[sqdr-wrapper]
......@@ -17,6 +17,5 @@ environment =
LDFLAGS=-L${sqdr-source:location}/lib -Wl,-rpath=${sqdr-source:location}/lib
[qjs-wrapper]
environment =
C_INCLUDE_PATH=include:${open62541:location}/include:${open62541:location}/deps:${open62541:location}/src/pubsub:${quickjs:location}/include
LDFLAGS=-L${open62541:location}/lib -Wl,-rpath=${open62541:location}/lib -L${sqdr-wrapper:location}/lib -Wl,-rpath=${sqdr-wrapper:location}/lib
autopilot-wrapper = ${sqdr-wrapper:location}
make-options = WITH_AUTOPILOT=y
[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
......@@ -31,9 +31,11 @@ import os
import socket
import struct
import time
import websocket
from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
from contextlib import closing
from websocket import create_connection
from slapos.testing.testcase import installSoftwareUrlList, makeModuleSetUpAndTestCaseClass
'''
0. positionArray
......@@ -46,11 +48,13 @@ from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
1.2 air speed
1.3 climb rate
2. message
3. log
'''
MONITORED_ITEM_NB = 3
MONITORED_ITEM_NB = 4
OPC_UA_PORT = 4840
OPC_UA_NET_IF = 'lo'
MCAST_GRP = 'ff15::1111'
LOOP_PERIOD = 200
# OPC UA Pub/Sub related constants
VERSION = 1
......@@ -107,10 +111,27 @@ SPEED_ARRAY_VALUES = (-72.419998, 15.93, -0.015)
STRING_TYPE = 12
MESSAGE_CONTENT = b'{\\"next_checkpoint\\":1}'
TEST_MESSAGE = b'{"content":"' + MESSAGE_CONTENT + b'","dest_id":-1}'
LOG = b''
root_software_release_url = os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', 'software-root.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'))
setUpModule, SlapOSInstanceTestCase = makeModuleSetUpAndTestCaseClass(
os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', 'software.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):
......@@ -119,8 +140,8 @@ class SubscriberTestCase(SlapOSInstanceTestCase):
return {
'_': json.dumps({
'droneGuidList': [cls.slap._computer_id],
'netIf': OPC_UA_NET_IF,
'subscriberGuidList': [cls.slap._computer_id],
'subscriberNetIf': OPC_UA_NET_IF
})
}
......@@ -201,6 +222,12 @@ class SubscriberTestCase(SlapOSInstanceTestCase):
ua_array += struct.pack(struct_type, value)
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):
data_set_message = self.ua_dataSetMessageHeader_encode()
data_set_message += struct.pack('H', MONITORED_ITEM_NB)
......@@ -214,9 +241,8 @@ class SubscriberTestCase(SlapOSInstanceTestCase):
'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
data_set_message += self.ua_string_encode(TEST_MESSAGE)
data_set_message += self.ua_string_encode(LOG)
return data_set_message
def send_ua_networkMessage(self):
......@@ -238,7 +264,6 @@ class SubscriberTestCase(SlapOSInstanceTestCase):
for expected_process_name in expected_process_name_list:
self.assertIn(expected_process_name, process_names)
def test_requested_instances(self):
connection_parameter_dict = json.loads(
self.computer_partition.getConnectionParameterDict()['_'])
......@@ -251,10 +276,12 @@ class SubscriberTestCase(SlapOSInstanceTestCase):
{
'autopilotIp': '192.168.27.1',
'autopilotPort': 7909,
'numberOfDrone': 1,
'numberOfSubscriber': 1,
'numberOfDrones': 1,
'numberOfSubscribers': 1,
'id': 1,
'debug': False,
'isASimulation': False,
'loopPeriod': LOOP_PERIOD,
'isADrone': False,
'flightScript': 'https://lab.nexedi.com/nexedi/flight-scripts/-/raw/v2.0/subscribe.js',
'netIf': OPC_UA_NET_IF,
......@@ -277,32 +304,32 @@ class SubscriberTestCase(SlapOSInstanceTestCase):
self.assertIn(expected_string, f.readlines())
def test_pubsub_subscription(self):
ws = websocket.WebSocket()
ws.connect(self.websocket_server_address, timeout=5)
with closing(create_connection(self.websocket_server_address, timeout=5)) as conn:
# Check if first message is 'Unknown instruction IP' where IP is client IPv6 address
self.assertIn(
b'Unknown instruction %s' % ws.sock.getsockname()[0].encode(),
ws.recv_frame().data
b'Unknown instruction %s' % conn.sock.getsockname()[0].encode(),
conn.recv_frame().data,
)
self.assertEqual(
ws.recv_frame().data,
conn.recv_frame().data,
b''.join((
b'{"drone_dict":{"0":{"latitude":',
b'"%.6f","longitude":"%.6f","altitude":"%.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()
time.sleep(0.1)
self.assertEqual(ws.recv_frame().data, MESSAGE_CONTENT.replace(b'\\', b''))
self.assertEqual(conn.recv_frame().data, MESSAGE_CONTENT.replace(b'\\', b''))
self.assertEqual(
ws.recv_frame().data,
conn.recv_frame().data,
b''.join((
b'{"drone_dict":{"0":{"latitude":',
b'"%.6f","longitude":"%.6f","altitude":"%.2f",' % POSITION_ARRAY_OUTPUT_VALUES[:-1],
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":""}}}',
)),
)
ws.close()
......@@ -9,7 +9,8 @@
<style>
button {
padding: 0.5%;
margin: 2vh;
padding: 2vh;
font-size: 24px;
cursor: pointer;
border: none;
......@@ -20,38 +21,64 @@
box-shadow: 0 2px #666;
transform: translateY(4px);
}
div > * {margin: 1%}
label {margin: 2%}
table {width: 30%}
label {margin: auto 2%}
table {
min-width: 1028px;
height: max-content;
}
textarea {
resize: none;
}
th, td{
padding: 1%;
text-align: center;
vertical-align: middle;
white-space: nowrap;
}
.blue-text {color: blue}
.connected {color: green}
.container {
display: flex;
align-items: center;
justify-content: center;
}
.cyan-text {color: cyan}
.disconnected {color: red}
.gray-button {background-color: lightgray}
.gray-button:hover {background-color: gray}
.green-text {color: green}
.green-button {background-color: #4caf50}
.green-button:hover {background-color: #3e8e41}
.magenta-text {color: magenta}
.red-button {background-color: red}
.red-button {background-color: #e42828}
.red-button:hover {background-color: #e42828}
.red-text {color: red}
.white-text {color: white}
.yellow-text {color: yellow}
#drones-status {height: 50vh}
</style>
</head>
<body>
<header class="container">
<div class="container">
<label for="web-socket-status">web socket status:</label>
<output class="disconnected" id="web-socket-status">Disconnected</output>
</header>
</div>
{% if debug -%}
<div class="container">
<table>
{% for i in range(int(nb_drones)) -%}
<tr>
<th>Drone {{ i }} logs</th>
<td><textarea id="log_{{ i }}" rows="4" cols="100" readonly></textarea><td>
</tr>
{% endfor %}
</table>
</div>
{% endif -%}
<div class="container" id="drones-status">
<table>
<tr>
<th></th>
......@@ -101,6 +128,12 @@
<td id="climb_rate_{{ i }}"></td>
{% endfor %}
</tr>
<tr>
<th>Timestamp (hh:mm:ss)</th>
{% for i in range(int(nb_drones)) -%}
<td id="timestamp_{{ i }}"></td>
{% endfor %}
</tr>
</table>
</div>
......
......@@ -12,17 +12,23 @@
FLIGHT_STATUS_BASE_ID = "flight_state_",
GREEN_BTN_CLASS_NAME = "green-button",
LATITUDE_BASE_ID = "latitude_",
LOG_BASE_ID = "log_",
LONGITUDE_BASE_ID = "longitude_",
QUIT_BTN_ID = "quit-btn",
RED_BTN_CLASS_NAME = "red-button",
SWITCH_BTN_ID = "switch-btn",
TIMESTAMP_BASE_ID = "timestamp_",
WEB_SOCKET_STATUS_OUTPUT_ID = "web-socket-status",
YAW_BASE_ID = "yaw_",
socket;
function updateConnexionClass(element, status) {
element.classList.remove(status ? DISCONNECTED_CLASS_NAME : CONNECTED_CLASS_NAME);
element.classList.add(status ? CONNECTED_CLASS_NAME : DISCONNECTED_CLASS_NAME);
element.classList.remove(
status ? DISCONNECTED_CLASS_NAME : CONNECTED_CLASS_NAME
);
element.classList.add(
status ? CONNECTED_CLASS_NAME : DISCONNECTED_CLASS_NAME
);
}
function setWebSocketStatus(connected, status) {
......@@ -49,21 +55,34 @@
socket = new WebSocket('ws://{{ websocket_url }}');
socket.onopen = function(event) {
socket.onopen = function (event) {
setWebSocketStatus(true, "Connected");
};
socket.onmessage = function(event) {
var message = JSON.parse(event.data),
flight_state_cell;
socket.onmessage = function (event) {
var color_array,
date,
flight_state_cell,
i,
message,
new_div,
new_span,
text_array;
try {
message = JSON.parse(event.data);
if (message.hasOwnProperty("drone_dict")) {
Object.entries(message["drone_dict"]).forEach(function ([id, drone]) {
Object.entries(message.drone_dict).forEach(function ([id, drone]) {
document.getElementById(LATITUDE_BASE_ID + id).innerHTML = drone["latitude"];
document.getElementById(LONGITUDE_BASE_ID + id).innerHTML = drone["longitude"];
document.getElementById(ALTITUDE_BASE_ID + id).innerHTML = drone["altitude"];
document.getElementById(YAW_BASE_ID + id).innerHTML = drone["yaw"];
document.getElementById(SPEED_BASE_ID + id).innerHTML = drone["speed"];
document.getElementById(CLIMB_RATE_BASE_ID + id).innerHTML = drone["climbRate"];
document.getElementById(TIMESTAMP_BASE_ID + id).innerHTML = new Date(drone["timestamp"]).toLocaleTimeString('fr-FR');
{% if debug -%}
document.getElementById(LOG_BASE_ID + id).value += drone["log"];
{% endif -%}
});
} else if (message.hasOwnProperty("state") && message.hasOwnProperty("id")) {
flight_state_cell = document.getElementById(FLIGHT_STATUS_BASE_ID + message['id']);
......@@ -72,6 +91,9 @@
} else {
console.info(message);
}
} catch (error) {
console.error(error, event.data);
}
};
socket.onclose = function(event) {
......
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