Commit 649249ca authored by Léo-Paul Géneau's avatar Léo-Paul Géneau 👾 Committed by Thomas Gambier

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

component/mavsdk: v↑ mavsdk (0.39.0 -> 1.4.13)
component/gwsocket: add gwsocket websocket server
software/js-drone: add frontend for subscriber
software/js-drone: use WebSocket for subscriber
slapos/software: run quickjs as a service
parent be75d825
Pipeline #33429 passed with stage
in 0 seconds
# simple, standalone, language-agnostic, RFC6455 compliant WebSocket Server, written in C. https://gwsocket.io
[buildout]
parts = gwsocket
[gwsocket]
recipe = slapos.recipe.cmmi
shared = true
url = https://tar.gwsocket.io/gwsocket-0.4.tar.gz
md5sum = 1367e77c47cb6379025e64deb85fb066
......@@ -4,37 +4,56 @@ extends =
../curl/buildout.cfg
../git/buildout.cfg
../jsoncpp/buildout.cfg
../lxml-python/buildout.cfg
../macros/macro.pythonpath.eggs.cfg
../tinyxml2/buildout.cfg
../zlib/buildout.cfg
parts =
mavsdk
[c-astral-headers]
recipe = slapos.recipe.build:gitclone
repository = https://lab.nexedi.com/nexedi/c-astral-c-library
revision = v1.0
git-executable = ${git:location}/bin/git
[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
revision = v0.39.0
revision = v1.4.13
git-executable = ${git:location}/bin/git
ignore-cloning-submodules = true
[future]
recipe = zc.recipe.egg:custom
egg = future
[mavsdk-env]
CMAKE_INCLUDE_PATH=${curl:location}/include:${jsoncpp:location}/include:${tinyxml2:location}/include
CMAKE_LIBRARY_PATH=${curl:location}/lib:${jsoncpp:location}/lib:${tinyxml2:location}/lib:${zlib:location}/lib
CMAKE_PROGRAM_PATH=${cmake:location}/bin
PATH=${pkgconfig:location}/bin/:${git:location}/bin/:%(PATH)s
LDFLAGS=-L${curl:location}/lib -Wl,-rpath=${curl:location}/lib -L${jsoncpp:location}/lib -Wl,-rpath=${jsoncpp:location}/lib -L${tinyxml2:location}/lib -Wl,-rpath=${tinyxml2:location}/lib -L${zlib:location}/lib -Wl,-rpath=${zlib:location}/lib -Wl,-rpath=@@LOCATION@@/lib
[mavsdk-pythonpath]
<= macro.pythonpath.eggs
environment = mavsdk-env
eggs =
${future:egg}
${lxml-python:egg}
[mavsdk]
recipe = slapos.recipe.cmmi
path = ${mavsdk-source:location}
cmake = ${cmake:location}/bin/cmake
depends = ${mavsdk-pythonpath:recipe}
pre-configure =
${git:location}/bin/git submodule update --init --recursive
cp -r ${c-astral-headers:location}/* ${mavsdk-source:location}/src/third_party/mavlink/include/mavlink/v2.0/
sed -i 's#common/mavlink.h#CAstral/mavlink.h#' ${mavsdk-source:location}/src/core/mavlink_include.h
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 =
......@@ -42,19 +61,23 @@ configure-options =
-DCMAKE_C_FLAGS="${:CMAKE_CFLAGS}"
-DCMAKE_CXX_FLAGS="${:CMAKE_CFLAGS}"
-DCMAKE_INSTALL_PREFIX=@@LOCATION@@
-DCMAKE_INSTALL_RPATH=${:CMAKE_LIBRARY_PATH}:@@LOCATION@@/lib
-DCMAKE_INSTALL_RPATH=${mavsdk-env:CMAKE_LIBRARY_PATH}:@@LOCATION@@/lib
-DPKG_CONFIG_EXECUTABLE=${pkgconfig:location}/bin/pkg-config
-DSUPERBUILD=OFF
-Bbuild/default
-H.
-Wno-dev
make-binary =
${:cmake} --build build/default --target install
environment =
CMAKE_INCLUDE_PATH=${curl:location}/include:${jsoncpp:location}/include:${tinyxml2:location}/include
CMAKE_LIBRARY_PATH=${:CMAKE_LIBRARY_PATH}
CMAKE_PROGRAM_PATH=${cmake:location}/bin
PATH=${pkgconfig:location}/bin/:%(PATH)s
LDFLAGS=-L${curl:location}/lib -Wl,-rpath=${curl:location}/lib -L${jsoncpp:location}/lib -Wl,-rpath=${jsoncpp:location}/lib -L${tinyxml2:location}/lib -Wl,-rpath=${tinyxml2:location}/lib -L${zlib:location}/lib -Wl,-rpath=${zlib:location}/lib -Wl,-rpath=@@LOCATION@@/lib
environment = mavsdk-env
CMAKE_CFLAGS=-I${tinyxml2:location}/include
CMAKE_LIBRARY_PATH=${curl:location}/lib:${jsoncpp:location}/lib:${tinyxml2:location}/lib:${zlib:location}/lib
[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
parts = qjs-wrapper
[qjs-wrapper-source]
recipe = slapos.recipe.build:gitclone
repository = https://lab.nexedi.com/nexedi/qjs-wrapper.git
revision = v2.0
git-executable = ${git:location}/bin/git
[qjs-wrapper]
recipe = slapos.recipe.cmmi
configure-command = true
url = https://lab.nexedi.com/nexedi/qjs-wrapper/-/archive/v1.3/qjs-wrapper-v1.3.tar.gz
md5sum = 5f63356c6a10bf227e2641ea4f78c7a2
path = ${qjs-wrapper-source:location}
environment =
C_INCLUDE_PATH=include:${open62541:location}/include:${open62541:location}/deps:${open62541:location}/src/pubsub:${quickjs:location}/include
CPLUS_INCLUDE_PATH=include:${mavsdk:location}/include:${mavsdk:location}/include/mavsdk
LDFLAGS=-L${open62541:location}/lib -Wl,-rpath=${open62541:location}/lib -L${mavsdk:location}/lib -Wl,-rpath=${mavsdk:location}/lib
LDFLAGS=-L${open62541:location}/lib -Wl,-rpath=${open62541:location}/lib -L${c-astral-wrapper:location}/lib -Wl,-rpath=${c-astral-wrapper:location}/lib
# Javascript drone #
## Presentation ##
* Deploy `user.js` flight script on a drone swarm
* Deploy a GUI on subscribers
* Run the flight script or the GUI as a SlapOS service
* Compile all required libraries to run the flight script
## Parameters ##
* autopilot-ip: 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
* isASimulation: Must be set to 'true' to automatically take off during simulation
* 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
* subscriberGuidList: List of computer id on which a GUI must be deployed
* drone-guid-list: List of computer id on which flight script must be deployed
## How it works ##
* is-a-simulation: Must be set to 'true' to automatically take off during simulation
For each computer listed in `droneGuidList` and `subscriberGuidList` the `peer` SR type will be instanciated.
* multicast-ip: IPv6 of the multicast group of the swarm
Each instance will return a `instance-path`. Under this path one will find `quickjs binary` in `bin` folder
and `scripts` in `etc` folder. Subcribers also return a `httpd-url` (the GUI address) and a `websocket-url` (used by the
GUI).
* net-if: Network interface used for multicast traffic
`quickjs binary location` `scripts location`/main.js `scripts location`/user.js is run as a SlapOS service. This allows
each instance to communicate with the others through OPC-UA pub/sub. For the drones it also establishes a connexion with
the UAV autopilot, for a subscriber it sends the pub/sub messages through the websocket.
* flight-script: URL of user's script to execute to fly drone swarm
* subscriber-guid-list: List of computer id on which subscription script must be deployed
## Web GUI (subcribers)
## How it works ##
For each computer listed in `drone-guid-list` and `subscriber-guid-list` a drone SR will be instanciated.
Each instance will return a `instance-path`. Under this path one will find `quickjs binary` in `bin` folder
and `scripts` in `etc` folder.
Run `quickjs binary location` `scripts location`/main.js `scripts location`/user.js .
### Drones informations
For each drone is displayed:
* the user script and autopilot logs
* the flight state (ready, flying, landing)
* the latitude in degrees
* the longitude in degrees
* the relative altitude in meters
* 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
### Buttons
* Start: sends a "start" message to the swarm and changes into a stop button
* Stop: sends a "stop" message to the swarm
* Switch leader: sends a "switch" message to the swarm, it is usually used to change the leader
* Quit: exits (closes websocket and stops pub/sub)
![GUI screenshot](images/js-drone_GUI_screenshot.png)
......@@ -12,26 +12,34 @@
# Substitution (${...:...}), extension ([buildout] extends = ...) and
# section inheritance (< = ...) are NOT supported (but you should really
# not need these here).
[index-html]
_update_hash_filename_ = web-gui/index.html.jinja2
md5sum = 1eedc017ecc9d1a6761dc2fff3bbab9b
[instance-profile]
filename = instance.cfg
md5sum = 360b58007c25727b7bd8a9154d5cafd4
filename = instance.cfg.in
md5sum = 80dae3e883663311d9814def78ee875a
[instance-default]
filename = instance-default.cfg
md5sum = 903939308701b11b1ff751784a9be110
filename = instance-default.cfg.jinja2
md5sum = 9db922cc0fcaa67006a2d6b9b95b95fe
[instance-drone]
filename = instance-drone.cfg
md5sum = 1ff50063f5a54712a0bc0ff38fa74630
[instance-peer]
filename = instance-peer.cfg.jinja2.in
md5sum = d12fbb134c587173ddff46ff1bc6ffe7
[main]
filename = main.js
md5sum = d0bfcc79cdd7c1e5b8f5d264cc59074e
_update_hash_filename_ = drone-scripts/main.js.jinja2
md5sum = 9a8ec8a2778f63789f39291795f47e98
[pubsub]
filename = pubsub.js
_update_hash_filename_ = drone-scripts/pubsub.js.jinja2
md5sum = 1555496ad591a31a845f33488d5c335d
[script-js]
_update_hash_filename_ = web-gui/script.js.jinja2
md5sum = e28492276416c2d84e770217ae97a88f
[worker]
filename = worker.js
md5sum = e4b4ca3bde1a21f1dbfc4ff7fa3b872c
_update_hash_filename_ = drone-scripts/worker.js.jinja2
md5sum = 48540afedd5437129196d84832d2ed40
/*jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */
/*global arm, console, exit, open, scriptArgs, setTimeout, start, stop,
stopPubsub, takeOffAndWait, Worker*/
/*global arm, console, close, dup2, exit, open, scriptArgs, setTimeout, start,
stop, stopPubsub, takeOffAndWait, Worker, SIGINT, SIGTERM*/
import {
arm,
start,
......@@ -8,17 +8,27 @@ import {
stopPubsub,
takeOffAndWait
} from {{ json_module.dumps(qjs_wrapper) }};
import { setTimeout, Worker } from "os";
import { open, exit } from "std";
(function (arm, console, exit, open, scriptArgs, setTimeout, start, stop,
stopPubsub, takeOffAndWait, Worker) {
import {
Worker,
SIGTERM,
dup2,
setTimeout,
signal
} 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) {
"use strict";
var CONF_PATH = {{ json_module.dumps(configuration) }},
conf_file = open(CONF_PATH, "r"),
configuration = JSON.parse(conf_file.readAsString()),
LOG_FILE = "{{ log_dir }}/mavsdk-log",
MAVSDK_LOG_FILE_PATH =
"{{ log_dir }}/mavsdk_" + new Date().toISOString() + ".log",
LOG_FILE =
open("{{ log_dir }}/quickjs_" + new Date().toISOString() + ".log", "w"),
pubsubWorker,
worker,
user_script = scriptArgs[1],
......@@ -28,6 +38,10 @@ import { open, exit } from "std";
conf_file.close();
// redirect stdout and stderr
dup2(LOG_FILE.fileno(), out.fileno());
dup2(LOG_FILE.fileno(), err.fileno());
// Use a Worker to ensure the user script
// does not block the main script
// (preventing it to be stopped for example)
......@@ -37,17 +51,28 @@ import { open, exit } from "std";
worker = new Worker("{{ worker_script }}");
function quit(is_a_drone, exit_code) {
worker.onmessage = null;
stopPubsub();
if (is_a_drone) {
stop();
}
LOG_FILE.close();
exit(exit_code);
}
function exitWorker(exit_code) {
worker.postMessage({
type: "exit",
code: exit_code
});
}
signal(SIGTERM, exitWorker.bind(null, 0));
function exitOnFail(ret, msg) {
if (ret) {
console.log(msg);
quit(1);
exitWorker(1);
}
}
......@@ -55,7 +80,12 @@ import { open, exit } from "std";
var address = configuration.autopilotIp + ":" + configuration.autopilotPort;
console.log("Will connect to", address);
exitOnFail(
start(configuration.autopilotIp, configuration.autopilotPort, LOG_FILE, 60),
start(
configuration.autopilotIp,
configuration.autopilotPort,
MAVSDK_LOG_FILE_PATH,
60
),
"Failed to connect to " + address
);
}
......@@ -87,7 +117,7 @@ import { open, exit } from "std";
// First argument must provide the user script path
if (user_script === undefined) {
console.log('Please provide the user_script path.');
quit(1);
exitWorker(1);
}
worker.postMessage({
......@@ -138,14 +168,16 @@ import { open, exit } from "std";
// Start the update loop
loop();
} else if (type === 'updated') {
err.flush();
out.flush();
can_update = true;
} else if (type === 'exited') {
worker.onmessage = null;
quit(configuration.isADrone, e.data.exit);
} else {
console.log('Unsupported message type', type);
quit(configuration.isADrone, 1);
exitWorker(1);
}
};
}(arm, console, exit, open, scriptArgs, setTimeout, start, stop, stopPubsub,
takeOffAndWait, Worker));
}(arm, console, dup2, err, exit, open, out, scriptArgs, setTimeout, start, stop,
stopPubsub, takeOffAndWait, Worker, SIGTERM));
/*jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */
/*global console, getAltitude, getAltitudeRel, getInitialAltitude, gpsIsOk,
getLatitude, getLongitude, getYaw, execUserScript, initPubsub, loiter,
setAirSpeed, setMessage, setTargetCoordinates, std, triggerParachute,
updateLogAndProjection, Drone, Worker*/
import {
Drone,
triggerParachute,
getAirspeed,
getAltitude,
getClimbRate,
getInitialAltitude,
gpsIsOk,
getPosition,
getYaw,
initPubsub,
isLanding,
loiter,
setAirSpeed,
setMessage,
setTargetCoordinates,
updateLogAndProjection
} from {{ json_module.dumps(qjs_wrapper) }};
import {
SIGTERM,
WNOHANG,
Worker,
close,
exec,
kill,
pipe,
setReadHandler,
waitpid
} from "os";
import { evalScript, fdopen, loadFile, open } from "std";
(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,
triggerParachute, updateLogAndProjection, waitpid) {
// Every script is evaluated per drone
"use strict";
var CONF_PATH = {{ json_module.dumps(configuration) }},
conf_file = open(CONF_PATH, "r"),
configuration = JSON.parse(conf_file.readAsString()),
clientId,
drone_dict = {},
gwsocket_pid,
gwsocket_r_pipe_fd,
gwsocket_w_pipe_fd,
handleWebSocketMessage,
last_message_timestamp = 0,
last_log_timestamp = 0,
parent = Worker.parent,
peer_dict = {},
user_me = {
//required to fly
triggerParachute: triggerParachute,
exit: exitWorker,
getDroneDict: function () { return drone_dict; },
getAltitudeAbs: getAltitude,
getCurrentPosition: getPosition,
getInitialAltitude: getInitialAltitude,
gpsIsOk: gpsIsOk,
getYaw: getYaw,
getSpeed: getAirspeed,
getClimbRate: getClimbRate,
id: configuration.id,
isLanding: isLanding,
loiter: loiter,
sendMsg: function (msg, id) {
if (id === undefined) { id = -1; }
setMessage(JSON.stringify({
content: msg,
timestamp: Date.now(),
dest_id: id
}));
},
setAirSpeed: setAirSpeed,
setTargetCoordinates: setTargetCoordinates
};
conf_file.close();
function exitWorker(exit_code) {
if (user_me.hasOwnProperty("onWebSocketMessage")) {
stopGwsocket();
}
parent.postMessage({type: "exited", exit: exit_code});
parent.onmessage = null;
}
function readMessage(rd) {
function read4() {
var b1, b2, b3, b4;
b1 = rd.getByte();
b2 = rd.getByte();
b3 = rd.getByte();
b4 = rd.getByte();
return (b1 << 24) | (b2 << 16) | (b3 << 8) | b4;
}
clientId = read4();
var type = read4();
var len = read4();
var data = new ArrayBuffer(len);
rd.read(data, 0, len);
return {
client: clientId,
type: type,
data: String.fromCharCode.apply(null, new Uint8Array(data)).trim()
};
}
function writeMessage(wr, m) {
function write4(v) {
wr.putByte((v >> 24) & 0xFF);
wr.putByte((v >> 16) & 0xFF);
wr.putByte((v >> 8) & 0xFF);
wr.putByte(v & 0xFF);
}
write4(m.client);
write4(m.type);
write4(m.data.byteLength);
wr.write(m.data, 0, m.data.byteLength);
wr.flush();
}
function runGwsocket(onMessage) {
var gwsocket_w_pipe = pipe(),
gwsocket_r_pipe = pipe();
gwsocket_pid = exec([
"gwsocket",
"--port=" + configuration.websocketPort,
"--addr=" + configuration.websocketIp,
"--std",
"--strict"
], {
block: false,
usePath: false,
file: {{ json_module.dumps(gwsocket_bin) }},
stdin: gwsocket_w_pipe[0],
stdout: gwsocket_r_pipe[1]
});
gwsocket_w_pipe_fd = fdopen(gwsocket_w_pipe[1], "w");
gwsocket_r_pipe_fd = fdopen(gwsocket_r_pipe[0], "r");
handleWebSocketMessage = function () {
var message = readMessage(gwsocket_r_pipe_fd).data;
if (message.includes(configuration.websocketIp)) {
return;
}
onMessage(message);
};
user_me.writeWebsocketMessage = function (message) {
var buf = new ArrayBuffer(message.length);
var bufView = new Uint8Array(buf);
for (var i=0; i<message.length; i++) {
bufView[i] = message.charCodeAt(i);
}
writeMessage(gwsocket_w_pipe_fd, {client: clientId, type: 1, data: buf});
}
setReadHandler(gwsocket_r_pipe[0], handleWebSocketMessage);
}
function stopGwsocket() {
handleWebSocketMessage = null;
close(gwsocket_w_pipe_fd);
close(gwsocket_r_pipe_fd);
kill(gwsocket_pid, SIGTERM);
waitpid(gwsocket_pid, WNOHANG);
}
function loadUserScript(path) {
var script_content = loadFile(path);
if (script_content === null) {
console.log("Failed to load user script " + path);
exitWorker(1);
}
try {
evalScript(
"function execUserScript(from, me) {" + script_content + "};"
);
} catch (e) {
console.log("Failed to evaluate user script", e);
exitWorker(1);
}
execUserScript(null, user_me);
if (user_me.hasOwnProperty("onWebSocketMessage")) {
runGwsocket(user_me.onWebSocketMessage);
}
// Call the drone onStart function
if (user_me.hasOwnProperty("onStart")) {
user_me.onStart();
}
}
function handleMainMessage(evt) {
var type = evt.data.type, message, peer_id;
switch (type) {
case "initPubsub":
initPubsub(configuration.numberOfDrone, configuration.numberOfSubscriber);
for (peer_id = 0; peer_id < configuration.numberOfDrone + configuration.numberOfSubscriber; peer_id++) {
peer_dict[peer_id] = new Drone(peer_id);
peer_dict[peer_id].init(peer_id);
if (peer_id < configuration.numberOfDrone) {
drone_dict[peer_id] = peer_dict[peer_id];
}
}
parent.postMessage({type: "initialized"});
break;
case "load":
loadUserScript(evt.data.path);
parent.postMessage({type: "loaded"});
break;
case "update":
Object.entries(peer_dict).forEach(function ([id, peer]) {
message = peer.message;
if (user_me.id !== Number(id) && message.length > 0) {
message = JSON.parse(message);
if (message.timestamp != last_message_timestamp &&
user_me.hasOwnProperty("onGetMsg") &&
[-1, user_me.id].includes(message.dest_id)) {
last_message_timestamp = message.timestamp;
user_me.onGetMsg(message.content);
}
}
});
// Call the drone onStart function
if (user_me.hasOwnProperty("onUpdate")) {
user_me.onUpdate(evt.data.timestamp);
}
if (evt.data.timestamp - last_log_timestamp >= 1000) {
updateLogAndProjection();
last_log_timestamp = evt.data.timestamp;
}
parent.postMessage({type: "updated"});
break;
case "exit":
exitWorker(evt.data.code);
break;
default:
throw new Error("Unsupported message type", type);
};
}
parent.onmessage = function (evt) {
try {
handleMainMessage(evt);
} catch (error) {
// Catch all potential bug to exit the main process
// if it occurs
console.log(error);
exitWorker(1);
}
};
}(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));
{% set autopilot_ip = slapparameter_dict.get('autopilotIp', '192.168.27.1') -%}
{% set autopilot_port = slapparameter_dict.get('autopilotPort', 7909) -%}
{% set flight_script = slapparameter_dict.get('flightScript', 'https://lab.nexedi.com/nexedi/flight-scripts/raw/master/default.js') -%}
{% set is_a_simulation = slapparameter_dict.get('isASimulation', False) -%}
{% set multicast_ip = slapparameter_dict.get('multicastIp', 'ff15::1111') -%}
{% set net_if = slapparameter_dict.get('netIf', 'eth0') -%}
{% set drone_guid_list = slapparameter_dict.get('droneGuidList', []) -%}
{% set subscriber_guid_list = slapparameter_dict.get('subscriberGuidList', []) -%}
{% set guid_list = drone_guid_list + subscriber_guid_list -%}
{% set nb_peer = len(guid_list) -%}
{% set parameter_dict = dict(default_parameter_dict, **parameter_dict) -%}
{% set guid_list = parameter_dict['droneGuidList'] + parameter_dict['subscriberGuidList'] -%}
{% set drone_id_list = [] -%}
{% set subscriber_id_list = [] -%}
{% set part_list = ['publish-connection-information'] -%}
{% for id, guid in enumerate(guid_list) -%}
{% set request_drone_section_title = 'request-drone' ~ id -%}
{% do part_list.append(request_drone_section_title) %}
[{{ request_drone_section_title }}]
{% set request_peer_section_title = 'request-peer' ~ id -%}
{% do part_list.append(request_peer_section_title) %}
[{{ request_peer_section_title }}]
<= slap-connection
recipe = slapos.cookbook:request.serialised
name = Drone{{ id }}
software-url = $${:software-release-url}
software-type = drone
name = Peer{{ id }}
software-url = ${:software-release-url}
software-type = peer
return = instance-path
sla-computer_guid = {{ guid }}
config-autopilotIp = {{ autopilot_ip }}
config-autopilotPort = {{ dumps(autopilot_port) }}
config-numberOfPeers = {{ dumps(nb_peer) }}
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-id = {{ dumps(id) }}
config-isASimulation = {{ dumps(is_a_simulation) }}
{% if guid in drone_guid_list -%}
config-isASimulation = {{ dumps(parameter_dict['isASimulation']) }}
{% if id < len(parameter_dict['droneGuidList']) -%}
{% do drone_id_list.append(id) %}
config-isADrone = {{ dumps(True) }}
config-flightScript = {{ flight_script }}
config-flightScript = {{ parameter_dict['flightScript'] }}
{% else -%}
{% do subscriber_id_list.append(id) %}
config-isADrone = {{ dumps(False) }}
config-flightScript = https://lab.nexedi.com/nexedi/flight-scripts/raw/master/subscribe.js
config-flightScript = https://lab.nexedi.com/nexedi/flight-scripts/-/raw/v2.0/subscribe.js
{% endif -%}
config-multicastIp = {{ multicast_ip }}
config-netIf = {{ net_if }}
config-multicastIp = {{ parameter_dict['multicastIp'] }}
config-netIf = {{ parameter_dict['netIf'] }}
{% endfor %}
[publish-connection-information]
......@@ -50,6 +41,6 @@ subscriber-id-list = {{ dumps(subscriber_id_list) }}
[buildout]
parts =
{%- for part in part_list %}
{% for part in part_list %}
{{ part }}
{%- endfor -%}
{% endfor %}
......@@ -14,7 +14,7 @@
"title": "Port of the drone's autopilot",
"description": "Port on which autopilot service is running.",
"type": "integer",
"default": "7909"
"default": 7909
},
"droneGuidList": {
"title": "List of drones computer ID",
......@@ -44,7 +44,7 @@
"title": "Script's URL of the flight",
"description": "URL of the script which will be executed for the flight. This URL must be publicly accesible so that the drone can fetch the script.",
"type": "string",
"default": "https://lab.nexedi.com/nexedi/flight-scripts/raw/master/default.js"
"default": "https://lab.nexedi.com/nexedi/flight-scripts/-/raw/v2.0/default.js"
},
"subscriberGuidList": {
"title": "List of subscribers computer ID",
......
......@@ -14,9 +14,14 @@
"description": "Port on which autopilot service is running.",
"type": "integer"
},
"numberOfPeers": {
"title": "Number of Peers",
"description": "Number of drones and subscribers in the swarm",
"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": {
......
[buildout]
parts =
main
symlink-quickjs-binary
qjs-launcher
publish-connection-information
[directory]
recipe = slapos.cookbook:mkdirectory
home = $${buildout:directory}
bin = $${:home}/bin
etc = $${:home}/etc
srv = $${:home}/srv
var = $${:home}/var
log = $${:var}/log
public = $${:srv}/public
service = $${:etc}/service
[js-dynamic-template]
recipe = slapos.recipe.template:jinja2
rendered = $${directory:etc}/$${:_buildout_section_name_}.js
template = ${buildout:directory}/$${:_buildout_section_name_}.js
extra-context =
context =
import json_module json
raw gwsocket_bin ${gwsocket:location}/bin/gwsocket
raw qjs_wrapper ${qjs-wrapper:location}/lib/libqjswrapper.so
raw configuration {{ configuration }}
$${:extra-context}
[main]
<= js-dynamic-template
template = ${main:target}
extra-context =
key log_dir directory:log
key pubsub_script pubsub:rendered
......@@ -32,19 +37,58 @@ extra-context =
[pubsub]
<= js-dynamic-template
template = ${pubsub:target}
[worker]
<= js-dynamic-template
template = ${worker:target}
[user]
recipe = slapos.recipe.build:download
url = {{ parameter_dict['flightScript'] }}
destination = $${directory:etc}/user.js
offline = false
[qjs-launcher]
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 }}
[symlink-quickjs-binary]
recipe = slapos.recipe.build
binary-path = ${quickjs:location}/bin/qjs
target = $${directory:bin}/qjs
init =
import os
if not os.path.exists(options['target']):
os.symlink(options['binary-path'], options['target'])
[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 -%}
......@@ -9,7 +9,7 @@ offline = true
[switch-softwaretype]
recipe = slapos.cookbook:switch-softwaretype
default = instance-default:output
drone = instance-drone:output
peer = instance-peer:output
RootSoftwareInstance = $${:default}
[slap-configuration]
......@@ -22,36 +22,58 @@ cert = $${slap_connection:cert_file}
[dynamic-template-base]
recipe = slapos.recipe.template:jinja2
url = ${buildout:directory}/$${:_buildout_section_name_}.cfg
output = $${buildout:directory}/$${:_buildout_section_name_}
output = $${buildout:directory}/$${:_buildout_section_name_}.cfg
extra-context =
context =
jsonkey default_parameter_dict :default-parameters
key parameter_dict slap-configuration:configuration
$${:extra-context}
default-parameters =
{
"autopilotIp": "192.168.27.1",
"autopilotPort": 7909,
"flightScript": "https://lab.nexedi.com/nexedi/flight-scripts/-/raw/v2.0/default.js",
"isASimulation": false,
"multicastIp": "ff15::1111",
"netIf": "eth0",
"droneGuidList": [],
"subscriberGuidList":[]
}
[instance-default]
<= dynamic-template-base
url = ${instance-default:target}
extensions = jinja2.ext.do
context =
key slapparameter_dict slap-configuration:configuration
[instance-drone]
<= dynamic-template-base
context =
key configuration drone-configuration:output
key user-script user:destination
[directory]
recipe = slapos.cookbook:mkdirectory
home = $${buildout:directory}
etc = $${:home}/etc
[drone-configuration]
[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 slapparameter_dict slap-configuration:configuration
inline = {{ json_module.dumps(slapparameter_dict) }}
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) }}
[user]
recipe = slapos.recipe.build:download
url = $${slap-configuration:configuration.flightScript}
destination = $${directory:etc}/user.js
offline = false
[directory]
recipe = slapos.cookbook:mkdirectory
home = $${buildout:directory}
etc = $${:home}/etc
[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
......@@ -5,16 +5,18 @@ extends =
[sqdr-source]
recipe = slapos.recipe.build:gitclone
repository = https://lab.nexedi.com/slaposdrone/squadrone.git
revision = v1.0
revision = v2.0
git-executable = ${git:location}/bin/git
[qjs-wrapper]
[sqdr-wrapper]
recipe = slapos.recipe.cmmi
configure-command = true
url =
path = ${sqdr-source:location}
md5sum =
environment =
CPLUS_INCLUDE_PATH=include:${qjs-wrapper-source:location}/include
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
CPLUS_INCLUDE_PATH=include
LDFLAGS=-L${open62541:location}/lib -Wl,-rpath=${open62541:location}/lib -L${sqdr-source:location}/lib -Wl,-rpath=${sqdr-source:location}/lib
LDFLAGS=-L${open62541:location}/lib -Wl,-rpath=${open62541:location}/lib -L${sqdr-wrapper:location}/lib -Wl,-rpath=${sqdr-wrapper:location}/lib
......@@ -3,42 +3,43 @@ extends =
buildout.hash.cfg
../../stack/slapos.cfg
../../component/qjs-wrapper/buildout.cfg
../../component/gwsocket/buildout.cfg
parts =
instance-profile
instance-default
instance-drone
main
pubsub
worker
slapos-cookbook
[instance-profile]
recipe = slapos.recipe.template
[instance-default]
recipe = slapos.recipe.build:download
url = ${:_profile_base_location_}/${:filename}
output = ${buildout:directory}/template.cfg
[jinja-template-base]
[template-base]
recipe = slapos.recipe.template
url = ${:_profile_base_location_}/${:_buildout_section_name_}.cfg
output = ${buildout:directory}/${:_buildout_section_name_}.cfg
url = ${:_profile_base_location_}/${:filename}
[instance-default]
<= jinja-template-base
[instance-peer]
<= template-base
output = ${buildout:directory}/${:_buildout_section_name_}
[instance-drone]
<= jinja-template-base
[instance-profile]
<= template-base
output = ${buildout:directory}/template.cfg
[download-file-base]
[download]
recipe = slapos.recipe.build:download
url = ${:_profile_base_location_}/${:filename}
destination = ${buildout:directory}/${:filename}
url = ${:_profile_base_location_}/${:_update_hash_filename_}
[index-html]
<= download
[main]
<= download-file-base
<= download
[pubsub]
<= download-file-base
<= download
[script-js]
<= download
[worker]
<= download-file-base
<= download
......@@ -12,11 +12,11 @@
"index": 0
},
"drone": {
"title": "Drone",
"software-type": "drone",
"description": "Drone Instance",
"request": "instance-drone-input-schema.json",
"response": "instance-drone-output-schema.json",
"title": "Peer",
"software-type": "peer",
"description": "Peer Instance",
"request": "instance-peer-input-schema.json",
"response": "instance-peer-output-schema.json",
"index": 1
}
}
......
......@@ -43,7 +43,8 @@ setup(name=name,
install_requires=[
'slapos.core',
'slapos.libnetworkcache',
'erp5.util'
'erp5.util',
'websocket-client',
],
zip_safe=True,
test_suite='test',
......
......@@ -30,12 +30,11 @@ import json
import os
import socket
import struct
import subprocess
import time
import websocket
from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
MAIN_SCRIPT_NAME = 'main.js'
'''
0. positionArray
0.1 latitude
......@@ -52,7 +51,6 @@ MONITORED_ITEM_NB = 3
OPC_UA_PORT = 4840
OPC_UA_NET_IF = 'lo'
MCAST_GRP = 'ff15::1111'
USER_SCRIPT_NAME = 'user.js'
# OPC UA Pub/Sub related constants
VERSION = 1
......@@ -98,63 +96,47 @@ UA_DATETIME_UNIX_EPOCH = 11644473600 * UA_DATETIME_SEC
CONFIG_VERSION_MAJOR_VERSION = 1690792766
CONFIG_VERSION_MINOR_VERSION = 1690781976
POSITION_ARRAY_TYPE = 11 #double
POSITION_ARRAY_VALUES = (45.64, 14.25, 686.61, 91.24)
POSITION_ARRAY_TYPE = 8 #int64
POSITION_ARRAY_INPUT_VALUES = (456400000, 142500000, 686000, 91000, 1697878907)
POSITION_ARRAY_OUTPUT_COEFS = (1e7, 1e7, 1000, 1000)
POSITION_ARRAY_OUTPUT_VALUES = tuple(value / coef for value, coef in zip(POSITION_ARRAY_INPUT_VALUES[:-1], POSITION_ARRAY_OUTPUT_COEFS))
SPEED_ARRAY_TYPE = 10 #float
SPEED_ARRAY_VALUES = (-72.419998, 15.93, -0.015)
STRING_TYPE = 12
TEST_MESSAGE = b'{"content":"{\\"next_checkpoint\\":1}","dest_id":-1}'
MESSAGE_CONTENT = b'{\\"next_checkpoint\\":1}'
TEST_MESSAGE = b'{"content":"' + MESSAGE_CONTENT + b'","dest_id":-1}'
setUpModule, SlapOSInstanceTestCase = makeModuleSetUpAndTestCaseClass(
os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', 'software.cfg')))
class JSDroneTestCase(SlapOSInstanceTestCase):
class SubscriberTestCase(SlapOSInstanceTestCase):
@classmethod
def getInstanceParameterDict(cls):
return {
'_': json.dumps({
'droneGuidList': [cls.slap._computer_id],
'netIf': OPC_UA_NET_IF,
'subscriberGuidList': [cls.slap._computer_id],
})
}
def get_partition(self, instance_type):
def get_partition(self, partition_id):
software_url = self.getSoftwareURL()
for computer_partition in self.slap.computer.getComputerPartitionList():
partition_url = computer_partition.getSoftwareRelease()._software_release
partition_type = computer_partition.getType()
if partition_url == software_url and partition_type == instance_type:
if computer_partition.getId() == partition_id:
return computer_partition
raise Exception("JS-drone %s partition not found" % instance_type)
raise Exception("Partition %s not found" % partition_id)
def setUp(self):
super().setUp()
subscriber_partition = self.get_partition('drone')
instance_path = json.loads(
subscriber_partition.getConnectionParameterDict()['_'])['instance-path']
quickjs_bin = os.path.join(instance_path, 'bin', 'qjs')
script_dir = os.path.join(instance_path, 'etc')
self.qjs_process = subprocess.Popen(
[
quickjs_bin,
os.path.join(script_dir, MAIN_SCRIPT_NAME),
os.path.join(script_dir, USER_SCRIPT_NAME),
],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
time.sleep(0.1)
def tearDown(self):
if self.qjs_process.returncode == None:
self.qjs_process.kill()
self.qjs_process.communicate()
super().tearDown()
subscriber_partition = self.get_partition('SubscriberTestCase-2')
self.websocket_server_address = json.loads(
subscriber_partition.getConnectionParameterDict()['_'])['websocket-url']
time.sleep(0.5)
def ua_networkMessage_encodeHeader(self):
ua_byte1 = int(VERSION)
......@@ -224,8 +206,8 @@ class JSDroneTestCase(SlapOSInstanceTestCase):
data_set_message += struct.pack('H', MONITORED_ITEM_NB)
data_set_message += self.ua_array_encode(
POSITION_ARRAY_TYPE,
'd',
POSITION_ARRAY_VALUES,
'q',
POSITION_ARRAY_INPUT_VALUES,
)
data_set_message += self.ua_array_encode(
SPEED_ARRAY_TYPE,
......@@ -244,25 +226,39 @@ class JSDroneTestCase(SlapOSInstanceTestCase):
s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
s.sendto(ua_message, ('::1', OPC_UA_PORT))
def test_process(self):
expected_process_name_list = [
'qjs-launcher-on-watch',
'http-server-on-watch',
]
with self.slap.instance_supervisor_rpc as supervisor:
process_names = [process['name']
for process in supervisor.getAllProcessInfo()]
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()['_'])
self.assertEqual(connection_parameter_dict['drone-id-list'], [])
self.assertEqual(connection_parameter_dict['subscriber-id-list'], [0])
self.assertEqual(connection_parameter_dict['drone-id-list'], [0])
self.assertEqual(connection_parameter_dict['subscriber-id-list'], [1])
def test_subscriber_instance_parameter_dict(self):
self.assertEqual(
json.loads(self.get_partition('drone').getInstanceParameterDict()['_']),
json.loads(self.get_partition('SubscriberTestCase-2').getInstanceParameterDict()['_']),
{
'autopilotIp': '192.168.27.1',
'autopilotPort': 7909,
'id': 0,
'numberOfDrone': 1,
'numberOfSubscriber': 1,
'id': 1,
'isASimulation': False,
'isADrone': False,
'flightScript': 'https://lab.nexedi.com/nexedi/flight-scripts/raw/master/subscribe.js',
'multicastIp': MCAST_GRP,
'numberOfPeers': 1,
'netIf': OPC_UA_NET_IF
'flightScript': 'https://lab.nexedi.com/nexedi/flight-scripts/-/raw/v2.0/subscribe.js',
'netIf': OPC_UA_NET_IF,
'multicastIp': MCAST_GRP
}
)
......@@ -281,14 +277,32 @@ class JSDroneTestCase(SlapOSInstanceTestCase):
self.assertIn(expected_string, f.readlines())
def test_pubsub_subscription(self):
ws = websocket.WebSocket()
ws.connect(self.websocket_server_address, timeout=5)
# 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
)
self.assertEqual(
ws.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,
))
)
self.send_ua_networkMessage()
time.sleep(0.1)
outs, _ = self.qjs_process.communicate(b'q\n', timeout=15)
decoded_out = outs.decode()
for line in (
'Subscription 0 | MonitoredItem %s' % MONITORED_ITEM_NB,
'Received position of drone 0: %f° %f° %fm %fm' % POSITION_ARRAY_VALUES,
'Received speed of drone 0: %f° %fm/s %fm/s' % SPEED_ARRAY_VALUES,
'Received message for drone 0: %s' % TEST_MESSAGE.decode(),
):
self.assertIn(line, decoded_out)
self.assertEqual(ws.recv_frame().data, MESSAGE_CONTENT.replace(b'\\', b''))
self.assertEqual(
ws.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],
))
)
ws.close()
<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta charset="utf-8">
<title>JS-Drone GUI</title>
<script src="script.js"></script>
<style>
button {
padding: 0.5%;
font-size: 24px;
cursor: pointer;
border: none;
border-radius: 10px;
box-shadow: 0 4px #999;
}
button:active {
box-shadow: 0 2px #666;
transform: translateY(4px);
}
div > * {margin: 1%}
label {margin: 2%}
table {width: 30%}
th, td{
padding: 1%;
text-align: center;
vertical-align: middle;
}
.connected {color: green}
.container {
display: flex;
align-items: center;
justify-content: center;
}
.disconnected {color: red}
.gray-button {background-color: lightgray}
.gray-button:hover {background-color: gray}
.green-button {background-color: #4caf50}
.green-button:hover {background-color: #3e8e41}
.red-button {background-color: red}
.red-button {background-color: #e42828}
</style>
</head>
<body>
<header class="container">
<label for="web-socket-status">web socket status:</label>
<output class="disconnected" id="web-socket-status">Disconnected</output>
</header>
<div class="container">
<table>
<tr>
<th></th>
{% for i in range(int(nb_drones)) -%}
<th>Drone {{ i }}</th>
{% endfor %}
</tr>
<tr>
<th>Flight state</th>
{% for i in range(int(nb_drones)) -%}
<td class="disconnected" id="flight_state_{{ i }}">Unknown</td>
{% endfor %}
</tr>
<tr>
<th>Latitude (°)</th>
{% for i in range(int(nb_drones)) -%}
<td id="latitude_{{ i }}"></td>
{% endfor %}
</tr>
<tr>
<th>Longitude (°)</th>
{% for i in range(int(nb_drones)) -%}
<td id="longitude_{{ i }}"></td>
{% endfor %}
</tr>
<tr>
<th>Altitude (m)</th>
{% for i in range(int(nb_drones)) -%}
<td id="altitude_{{ i }}"></td>
{% endfor %}
</tr>
<tr>
<th>Yaw (°)</th>
{% for i in range(int(nb_drones)) -%}
<td id="yaw_{{ i }}"></td>
{% endfor %}
</tr>
<tr>
<th>Speed (m/s)</th>
{% for i in range(int(nb_drones)) -%}
<td id="speed_{{ i }}"></td>
{% endfor %}
</tr>
<tr>
<th>Climb rate (m/s)</th>
{% for i in range(int(nb_drones)) -%}
<td id="climb_rate_{{ i }}"></td>
{% endfor %}
</tr>
</table>
</div>
<div class="container">
<button id="flight-btn" class="green-button" type="button">
Start
</button>
<button id="switch-btn" class="gray-button" type="button">
Switch leader
</button>
<button id="quit-btn" class="red-button" type="button">
Quit
</button>
</div>
</body>
</html>
/*jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */
(function () {
"use strict";
var ALTITUDE_BASE_ID = "altitude_",
SPEED_BASE_ID = "speed_",
CONNECTED_CLASS_NAME = "connected",
CLIMB_RATE_BASE_ID = "climb_rate_",
DISCONNECTED_CLASS_NAME = "disconnected",
FLIGHT_BTN_ID = "flight-btn",
FLIGHT_STATUS_BASE_ID = "flight_state_",
GREEN_BTN_CLASS_NAME = "green-button",
LATITUDE_BASE_ID = "latitude_",
LONGITUDE_BASE_ID = "longitude_",
QUIT_BTN_ID = "quit-btn",
RED_BTN_CLASS_NAME = "red-button",
SWITCH_BTN_ID = "switch-btn",
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);
}
function setWebSocketStatus(connected, status) {
var status_output = document.getElementById(WEB_SOCKET_STATUS_OUTPUT_ID);
updateConnexionClass(status_output, connected);
status_output.value = status;
}
function stopFlight(event) {
socket.send("stop");
event.target.removeEventListener('click', stopFlight);
}
function startFlight(event) {
var button = event.target;
socket.send("start");
button.removeEventListener('click', startFlight);
button.innerHTML = "Stop";
button.classList.remove(GREEN_BTN_CLASS_NAME);
button.classList.add(RED_BTN_CLASS_NAME);
button.addEventListener('click', stopFlight);
}
socket = new WebSocket('ws://{{ websocket_url }}');
socket.onopen = function(event) {
setWebSocketStatus(true, "Connected");
};
socket.onmessage = function(event) {
var message = JSON.parse(event.data),
flight_state_cell;
if (message.hasOwnProperty("drone_dict")) {
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"];
});
} else if (message.hasOwnProperty("state") && message.hasOwnProperty("id")) {
flight_state_cell = document.getElementById(FLIGHT_STATUS_BASE_ID + message['id']);
flight_state_cell.innerHTML = message['state'];
updateConnexionClass(flight_state_cell, message['inAir']);
} else {
console.info(message);
}
};
socket.onclose = function(event) {
setWebSocketStatus(false, "Closed");
};
socket.onerror = function(event) {
console.error(event.reason);
};
document.addEventListener("DOMContentLoaded", () => {
document.getElementById(FLIGHT_BTN_ID).addEventListener('click', startFlight);
document.getElementById(SWITCH_BTN_ID).addEventListener('click', event => {
socket.send("switch");
});
document.getElementById(QUIT_BTN_ID).addEventListener('click', event => {
socket.send("quit");
});
});
}());
/*jslint nomen: true, indent: 2, maxerr: 3, maxlen: 80 */
/*global console, getAltitude, getAltitudeRel, getInitialAltitude, getLatitude,
getLongitude, getYaw, execUserScript, initPubsub, landed, loiter, setAirspeed,
setMessage, setTargetCoordinates, std, triggerParachute, Drone, Worker*/
import {
Drone,
triggerParachute,
getAirspeed,
getAltitude,
getAltitudeRel,
getClimbRate,
getInitialAltitude,
getLatitude,
getLongitude,
getYaw,
initPubsub,
landed,
loiter,
setAirspeed,
setMessage,
setTargetCoordinates
} from {{ json_module.dumps(qjs_wrapper) }};
import * as std from "std";
import { Worker } from "os";
(function (console, getAltitude, getAltitudeRel, getInitialAltitude,
getLatitude, getLongitude, getYaw, initPubsub, landed, loiter,
setAirspeed, setMessage, setTargetCoordinates, std, triggerParachute,
Drone, Worker) {
// Every script is evaluated per drone
"use strict";
var CONF_PATH = {{ json_module.dumps(configuration) }},
conf_file = std.open(CONF_PATH, "r"),
configuration = JSON.parse(conf_file.readAsString()),
parent = Worker.parent,
user_me = {
//for debugging purpose
fdopen: std.fdopen,
in: std.in,
//required to fly
triggerParachute: triggerParachute,
drone_dict: {},
exit: function (exit_code) {
parent.postMessage({type: "exited", exit: exit_code});
parent.onmessage = null;
},
getAltitudeAbs: getAltitude,
getCurrentPosition: function () {
return {
x: getLatitude(),
y: getLongitude(),
z: getAltitudeRel()
};
},
getInitialAltitude: getInitialAltitude,
getYaw: getYaw,
getSpeed: getAirspeed,
getClimbRate: getClimbRate,
id: configuration.id,
landed: landed,
loiter: loiter,
sendMsg: function (msg, id) {
if (id === undefined) { id = -1; }
setMessage(JSON.stringify({ content: msg, dest_id: id }));
},
setAirspeed: setAirspeed,
setTargetCoordinates: setTargetCoordinates
};
conf_file.close();
function loadUserScript(path) {
var script_content = std.loadFile(path);
if (script_content === null) {
console.log("Failed to load user script " + path);
std.exit(1);
}
try {
std.evalScript(
"function execUserScript(from, me) {" + script_content + "};"
);
} catch (e) {
console.log("Failed to evaluate user script", e);
std.exit(1);
}
execUserScript(null, user_me);
// Call the drone onStart function
if (user_me.hasOwnProperty("onStart")) {
user_me.onStart();
}
}
function handleMainMessage(evt) {
var type = evt.data.type, message, drone_id;
if (type === "initPubsub") {
initPubsub(configuration.numberOfPeers);
for (drone_id = 0; drone_id < configuration.numberOfPeers; drone_id++) {
user_me.drone_dict[drone_id] = new Drone(drone_id);
user_me.drone_dict[drone_id].init(drone_id);
}
parent.postMessage({type: "initialized"});
} else if (type === "load") {
loadUserScript(evt.data.path);
parent.postMessage({type: "loaded"});
} else if (type === "update") {
Object.entries(user_me.drone_dict).forEach(function ([id, drone]) {
message = drone.message;
if (user_me.id !== Number(id) && message.length > 0) {
message = JSON.parse(message);
if (user_me.hasOwnProperty("onGetMsg") &&
[-1, user_me.id].includes(message.dest_id)) {
user_me.onGetMsg(message.content);
}
}
});
// Call the drone onStart function
if (user_me.hasOwnProperty("onUpdate")) {
user_me.onUpdate(evt.data.timestamp);
}
parent.postMessage({type: "updated"});
} else {
throw new Error("Unsupported message type", type);
}
}
parent.onmessage = function (evt) {
try {
handleMainMessage(evt);
} catch (error) {
// Catch all potential bug to exit the main process
// if it occurs
console.log(error);
std.exit(1);
}
};
}(console, getAltitude, getAltitudeRel, getInitialAltitude, getLatitude,
getLongitude, getYaw, initPubsub, landed, loiter, setAirspeed, setMessage,
setTargetCoordinates, std, triggerParachute, Drone, Worker));
......@@ -194,6 +194,7 @@ Flask = 3.0.0:whl
frozenlist = 1.4.0:whl
funcsigs = 1.0.2
functools32 = 3.2.3.post2
future = 0.18.3
gevent = 23.9.1
geventmp = 0.0.1
gitdb = 4.0.10
......
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