diff --git a/software/js-drone/README.md b/software/js-drone/README.md
index 78c9126c0485363bbcf881f3449e6e9fe2d03d85..ccb325564ff6a73e1b8ae5253b740d60a6e3e65a 100644
--- a/software/js-drone/README.md
+++ b/software/js-drone/README.md
@@ -2,7 +2,7 @@
 
 ## Presentation ##
 
-* Deploy 4 different scripts (`cli.js`, `demo.js`, `manual-flight.js` and `subscribe.js`) on a drone to fly it
+* Deploy `main.js` script on a drone to fly it
 
 * Compile all required libraries to run flight scripts
 
@@ -20,8 +20,10 @@
 
 * net-if: Network interface used for multicast traffic
 
-* drone-id-list: Comma seperated list of the drone IDs of the swarm (recommanded to add the current drone ID)
+* drone-id-list: List of the drone IDs of the swarm (recommended to add the current drone ID)
+
+* flight-script: User script to execute to fly drone swarm
 
 ## How it works ##
 
-Run `quickjs binary location` `desired script location`
+Run `quickjs binary location` `scripts location`/main.js
diff --git a/software/js-drone/buildout.hash.cfg b/software/js-drone/buildout.hash.cfg
index 463d7cb3c5637b4064235840168783124a7c0425..9a159be1d4292544d55843a5a06c66b3ceff2ad8 100644
--- a/software/js-drone/buildout.hash.cfg
+++ b/software/js-drone/buildout.hash.cfg
@@ -14,28 +14,12 @@
 # not need these here).
 [instance-profile]
 filename = instance.cfg
-md5sum = 9c754abbc80f795229c3e79a8ede534b
+md5sum = ae1ccd9272303ee0102cdcec8ddef562
 
-[cli]
-filename = cli.js
-md5sum = 33271aeec124301604fdd406f0b339d1
-
-[common]
-filename = common.js
-md5sum = 1a43f5cf3db1256fee985f82d88930a0
-
-[demo]
-filename = demo.js
-md5sum = 31d8511e6d297643e65febe9a3ed2428
-
-[manual-flight]
-filename = manual-flight.js
-md5sum = 175813fc8b2f19f91dae27ad4e14ab03
+[main]
+filename = main.js
+md5sum = 195c4ba934fb05f46dec18a5a132c450
 
 [pubsub]
 filename = pubsub.js
-md5sum = 1d1a2be301d74a619b63a33c4b4039da
-
-[subscribe]
-filename = subscribe.js
-md5sum = 772e84f5584fc66c976778bbd8ae6a65
+md5sum = d8798c3206f129e8715afd3ca23afa1a
diff --git a/software/js-drone/cli.js b/software/js-drone/cli.js
deleted file mode 100644
index 898597e5c6e48e563847bcad9a33b6ba217ef9bd..0000000000000000000000000000000000000000
--- a/software/js-drone/cli.js
+++ /dev/null
@@ -1,146 +0,0 @@
-/*jslint indent2 */
-/*global console, std */
-
-import {
-  loiter,
-  setAirspeed,
-  setAltitude,
-  setTargetLatLong,
-  reboot
-} from "{{ qjs_wrapper }}"; //jslint-quiet
-import {
-  connect,
-  displayDronePositions,
-  land,
-  quit,
-  startPubsub,
-  takeOff
-} from "{{ common }}"; //jslint-quiet
-/*jslint-disable*/
-import * as std from "std";
-/*jslint-enable*/
-
-var running = false;
-const wrongParameters = displayMessage.bind(null, "Wrong parameters");
-
-function checkNumber(value, toExecute) {
-  return (
-    Number.isNaN(value)
-    ? wrongParameters
-    : toExecute.bind(null, value)
-  );
-}
-
-function displayMessage(message) {
-  console.log(message);
-  return 0;
-}
-
-function exit() {
-  running = false;
-  quit();
-  return 0;
-}
-
-function getInput() {
-  let undefined_cmd;
-  let altitude;
-  let cmd;
-  let latitude;
-  let longitude;
-  let s;
-  let speed;
-  
-  const help = `
-    connect
-    takeoff
-    land
-    goto(point)
-    gotoCoord(latitude, longitude)
-    altitude(altitude)
-    speed(speed)
-    positions
-    reboot
-    exit
-    help
-  `;
-
-  const f = std.fdopen(std.in, "r");
-  running = true;
-  while (running) {
-    std.printf("> ");
-    s = f.getline();
-    undefined_cmd = false;
-  
-    switch (s) {
-    case "altitude":
-      std.printf("Altitude: ");
-      altitude = parseFloat(f.getline());
-      cmd = checkNumber(altitude, setAltitude);
-      break;
-  
-    case "connect":
-      cmd = connect;
-      startPubsub();
-      break;
-  
-    case "exit":
-      cmd = exit;
-      break;
-  
-    case "gotoCoord":
-      std.printf("Latitude: ");
-      latitude = parseFloat(f.getline());
-      std.printf("Longitude: ");
-      longitude = parseFloat(f.getline());
-      cmd = checkNumber(longitude, checkNumber(latitude, setTargetLatLong));
-      break;
-  
-    case "help":
-      cmd = displayMessage.bind(null, help);
-      break;
-  
-    case "land":
-      cmd = land;
-      break;
-  
-    case "loiter":
-      cmd = loiter;
-      break;
-  
-    case "positions":
-      cmd = displayDronePositions;
-      break;
-  
-    case "reboot":
-      cmd = reboot;
-      break;
-  
-    case "speed":
-      std.printf("Speed: ");
-      speed = parseFloat(f.getline());
-      cmd = checkNumber(speed, setAirspeed);
-      break;
-  
-    case "takeoff":
-      cmd = takeOff.bind(null, 60);
-      break;
-  
-    default:
-      undefined_cmd = true;
-      cmd = displayMessage.bind(null, "    Undefined command");
-    }
-  
-    let ret = cmd();
-    if (ret) {
-      console.log("    [ERROR] function:\n", cmd, "\nreturn value:", ret);
-    }
-    else if (s !== "help" && !undefined_cmd) {
-      console.log("    Command successful");
-    }
-  };
-
-  f.close();
-}
-
-getInput();
diff --git a/software/js-drone/common.js b/software/js-drone/common.js
deleted file mode 100644
index c950db840f4822faac3e249b6bd5b926b10dd517..0000000000000000000000000000000000000000
--- a/software/js-drone/common.js
+++ /dev/null
@@ -1,184 +0,0 @@
-/*jslint-disable*/
-{% set comma_separated_drone_id_list = ', '.join(drone_id_list.split()) -%}
-/*jslint-enable*/
-
-import {
-  arm,
-  doParachute,
-  getAltitude,
-  getLatitude,
-  getLongitude,
-  getYaw,
-  initPubsub,
-  setAltitude,
-  setTargetLatLong,
-  start,
-  stop,
-  stopPubsub,
-  takeOffAndWait,
-  Drone
-} from "{{ qjs_wrapper }}"; //jslint-quiet
-import {exit} from "std";
-import {sleep, Worker} from "os";
-
-const IP = "{{ autopilot_ip }}";
-const PORT = "7909";
-
-export const IS_LEADER = {{ is_leader }};
-export const LEADER_ID = {{ leader_id }};
-export const SIMULATION = {{ is_a_simulation }};
-
-export const EPSILON = 105;
-const EPSILON_YAW = 6;
-const EPSILON_ALTITUDE = 5;
-const TARGET_YAW = 0;
-export const ALTITUDE_DIFF = 30;
-
-const URL = "udp://" + IP + ":" + PORT;
-const LOG_FILE = "{{ log_dir }}/mavsdk-log";
-
-const droneIdList = [{{ comma_separated_drone_id_list }}];
-const droneDict = {};
-
-var pubsubRunning = false;
-var pubsubWorker;
-
-export function connect() {
-  console.log("Will connect to", URL);
-  exit_on_fail(start(URL, LOG_FILE, 60), "Failed to connect to " + URL);
-}
-
-export function distance(lat1, lon1, lat2, lon2) {
-  const R = 6371e3; // meters
-  const la1 = lat1 * Math.PI/180; // la, lo in radians
-  const la2 = lat2 * Math.PI/180;
-  const lo1 = lon1 * Math.PI/180;
-  const lo2 = lon2 * Math.PI/180;
-
-  //haversine formula
-  const sinLat = Math.sin((la2 - la1)/2);
-  const sinLon = Math.sin((lo2 - lo1)/2);
-  const h = sinLat*sinLat + Math.cos(la1)*Math.cos(la2)*sinLon*sinLon
-  return 2*R*Math.asin(Math.sqrt(h));
-}
-
-export function displayDronePositions() {
-  if(!pubsubRunning)
-    console.log("You must start pubsub first !");
-  else {
-    for (const [id, drone] of Object.entries(droneDict)) {
-      console.log(id, drone.latitude, drone.longitude, drone.altitudeAbs, drone.altitudeRel);
-    }
-  }
-  return 0;
-}
-
-function exit_on_fail(ret, msg) {
-  if(ret) {
-    console.log(msg);
-    quit();
-    exit(-1);
-  }
-}
-
-export function quit() {
-  stop();
-  if(pubsubRunning) {
-    stopPubsub();
-  }
-}
-
-export function goToAltitude(target_altitude, wait, go) {
-  if(go) {
-    exit_on_fail(
-      setAltitude(target_altitude),
-      `Failed to go to altitude ${target_altitude} m`
-    );
-  }
-
-  if(wait) {
-    waitForAltitude(target_altitude);
-  }
-}
-
-export function land() {
-  var yaw;
-
-  while(true) {
-    yaw = getYaw();
-    console.log(`[DEMO] Waiting for yaw... (${yaw} , ${TARGET_YAW})`);
-    if(Math.abs(yaw - TARGET_YAW) < EPSILON_YAW) {
-      break;
-    }
-    sleep(250);
-  }
-
-  console.log("[DEMO] Deploying parachute...");
-  exit_on_fail(doParachute(2), "Failed to deploy parachute");
-}
-
-export function setLatLong(latitude, longitude, target_altitude) {
-  var cur_latitude;
-  var cur_longitude;
-  var d;
-
-  if(target_altitude !== 0) {
-    setAltitude(target_altitude, false, true);
-  }
-
-  console.log(`Going to (${latitude}, ${longitude}) from
-                (${getLatitude()}, ${getLongitude()})`);
-  exit_on_fail(
-    setTargetLatLong(latitude, longitude),
-    `Failed to go to (${latitude}, ${longitude})`
-  );
-  sleep(500);
-
-  while(true) {
-    cur_latitude = getLatitude();
-    cur_longitude = getLongitude();
-    d = distance(cur_latitude, cur_longitude, latitude, longitude);
-    console.log(`Waiting for drone to get to destination (${d} m),
-    (${cur_latitude} , ${cur_longitude}), (${latitude}, ${longitude})`);
-    if(d < EPSILON) {
-      sleep(6000);
-      return;
-    }
-    sleep(1000);
-  }
-}
-
-export function startPubsub() {
-  pubsubWorker = new Worker("{{ pubsub_script }}");
-  pubsubWorker.onmessage = function(e) {
-    if (!e.data.publishing)
-      pubsubWorker.onmessage = null;
-  }
-
-  initPubsub(droneIdList.length);
-  for (let i = 0; i < droneIdList.length; i++) {
-    let id = droneIdList[i]
-    droneDict[id] = new Drone(id);
-    droneDict[id].init(i);
-  }
-
-  pubsubWorker.postMessage({ action: "run", publish: true });
-  pubsubRunning = true;
-  return droneDict;
-}
-
-export function takeOff(altitude) {
-  exit_on_fail(arm(), "Failed to arm");
-  takeOffAndWait();
-  goToAltitude(altitude, true, true);
-}
-
-function waitForAltitude(target_altitude) {
-  var altitude = getAltitude();
-  while(Math.abs(altitude - target_altitude) > EPSILON_ALTITUDE) {
-    console.log(
-      `[DEMO] Waiting for altitude... (${altitude} , ${target_altitude})`);
-    sleep(1000);
-    altitude = getAltitude();
-  }
-}
diff --git a/software/js-drone/demo.js b/software/js-drone/demo.js
deleted file mode 100644
index ce1b5ea53d4a9fe1241d518ec2a81d7b55b2baa0..0000000000000000000000000000000000000000
--- a/software/js-drone/demo.js
+++ /dev/null
@@ -1,195 +0,0 @@
-/*jslint indent2 */
-/*global console */
-import {
-  getAltitude,
-  getAltitudeRel,
-  getInitialAltitude,
-  getLatitude,
-  getLongitude,
-  landed,
-  loiter,
-  setCheckpoint,
-  setTargetCoordinates
-} from "{{ qjs_wrapper }}"; //jslint-quiet
-import {sleep} from "os";
-import {
-  connect,
-  distance,
-  goToAltitude,
-  land,
-  quit,
-  setLatLong,
-  startPubsub,
-  takeOff,
-  ALTITUDE_DIFF,
-  IS_LEADER,
-  LEADER_ID,
-  SIMULATION
-} from "{{ common }}"; //jslint-quiet
-
-const FLIGH_ALTITUDE = 100;
-const PARACHUTE_ALTITUDE = 35;
-
-const checkpointList = [
-  {
-    "latitude": 45.64492790560583,
-    "longitude": 14.25334942966329,
-    "altitude": 585.1806861589965
-  },
-  {
-    "latitude": 45.64316335436476,
-    "longitude": 14.26332880184475,
-    "altitude": 589.8802607573035
-  },
-  {
-    "latitude": 45.64911917196595,
-    "longitude": 14.26214792790128,
-    "altitude": 608.6648153348965
-  },
-  {
-    "latitude": 45.64122685351364,
-    "longitude": 14.26590493128597,
-    "altitude": 606.1448368129072
-  },
-  {
-    "latitude": 45.64543355564817,
-    "longitude": 14.27242391207985,
-    "altitude": 630.0829598206344
-  },
-  {
-    "latitude": 45.6372792927328,
-    "longitude": 14.27533492411138,
-    "altitude": 616.1839898415284
-  },
-  {
-    "latitude": 45.64061299543953,
-    "longitude": 14.26161958465814,
-    "altitude": 598.0603137354178
-  },
-  {
-    "latitude": 45.64032340702919,
-    "longitude": 14.2682896662383,
-    "altitude": 607.1243119862851
-  }
-];
-
-const landingPoint = [
-  {
-    "latitude": 45.6398451,
-    "longitude": 14.2699217
-  }
-];
-
-let INITIAL_ALTITUDE;
-let START_ALTITUDE;
-
-var nextCheckpoint = 0;
-
-var distanceToLandingPoint = 100;
-
-var leaderAltitudeAbs;
-var leaderAltitudeRel;
-var leaderLatitude;
-var leaderLongitude;
-
-function followLeader(leaderId, initialAltitude, altitudeDiff) {
-  goToAltitude(START_ALTITUDE + ALTITUDE_DIFF, false, true);
-
-  while(droneDict[leaderId].altitudeAbs == 0) {
-    console.log("[DEMO] Waiting for leader to send its altitude");
-    sleep(1000);
-  }
-
-  while(droneDict[leaderId].altitudeAbs < initialAltitude) {
-    console.log(`[DEMO] Waiting for leader to reach altitude ${initialAltitude} (currently ${droneDict[leaderId].altitudeAbs})`);
-    sleep(1000);
-  }
-
-  console.log("[DEMO] Switching to following mode...\n");
-  do {
-    leaderAltitudeAbs = droneDict[leaderId].altitudeAbs;
-    leaderAltitudeRel = droneDict[leaderId].altitudeRel;
-    leaderLatitude = droneDict[leaderId].latitude;
-    leaderLongitude = droneDict[leaderId].longitude;
-
-    setTargetCoordinates(
-      leaderLatitude,
-      leaderLongitude,
-      leaderAltitudeAbs + altitudeDiff,
-      0
-    );
-    sleep(500);
-  } while(leaderAltitudeRel > PARACHUTE_ALTITUDE);
-
-  console.log("[DEMO] Stop following...\n");
-  nextCheckpoint = droneDict[leaderId].lastCheckpoint + 1;
-}
-
-function waitForAltitude(altitude) {
-  var curAltitude;
-  do {
-    sleep(1000);
-    curAltitude = getAltitude();
-    console.log(
-      `[DEMO] Waiting for altitude... (${curAltitude} , ${altitude})`);
-  }
-  while(curAltitude < altitude);
-}
-
-function waitForLanding() {
-  while(!landed()) {
-    sleep(1000);
-  }
-}
-
-console.log("[DEMO] Connecting...\n");
-connect();
-
-const droneDict = startPubsub();
-
-INITIAL_ALTITUDE = getInitialAltitude();
-START_ALTITUDE = INITIAL_ALTITUDE + FLIGH_ALTITUDE;
-
-if(SIMULATION) {
-  takeOff(START_ALTITUDE + 1);
-}
-
-waitForAltitude(START_ALTITUDE);
-
-console.log("[DEMO] Setting loiter mode...\n");
-loiter();
-sleep(3000);
-
-if(!IS_LEADER) {
-  followLeader(LEADER_ID, START_ALTITUDE, ALTITUDE_DIFF);
-}
-
-for (let i = nextCheckpoint; i < checkpointList.length; i++) {
-  console.log(`[DEMO] Going to Checkpoint ${i}\n`);
-  setLatLong(checkpointList[i].latitude, checkpointList[i].longitude, checkpointList[i].altitude + FLIGH_ALTITUDE);
-  console.log(`[DEMO] Reached Checkpoint ${i}\n`);
-  setCheckpoint(i);
-  sleep(30000);
-}
-
-console.log("[DEMO] Setting altitude...\n");
-goToAltitude(getAltitude() - getAltitudeRel() + PARACHUTE_ALTITUDE, true, true);
-
-if(!IS_LEADER) {
-  setLatLong(
-    checkpointList[checkpointList.length - 1].latitude,
-    checkpointList[checkpointList.length - 1].longitude,
-    0
-  );
-}
-
-while(distanceToLandingPoint > 20) {
-  console.log(`[DEMO] Waiting to reache landing point (current distance is ${distanceToLandingPoint})`);
-  distanceToLandingPoint = distance(getLatitude(), getLongitude(), landingPoint.latitude, landingPoint.longitude);
-}
-
-console.log("[DEMO] Landing...\n");
-land();
-
-waitForLanding();
-quit();
diff --git a/software/js-drone/instance-input-schema.json b/software/js-drone/instance-input-schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..403be292170f078f9b0ad3f3f3120d1fdb0c6629
--- /dev/null
+++ b/software/js-drone/instance-input-schema.json
@@ -0,0 +1,56 @@
+{
+  "$schema": "http://json-schema.org/draft-06/schema",
+  "type": "object",
+  "description": "Parameters to instantiate JS drone",
+  "additionalProperties": false,
+  "properties": {
+    "autopilot-ip": {
+      "title": "IP address of the drone's autopilot",
+      "description": "IP used to create a connection with the autopilot.",
+      "type": "string",
+      "default": "192.168.27.1"
+    },
+    "id": {
+      "title": "Drone ID",
+      "description": "Unique identifier of the drone.",
+      "type": "integer",
+      "default": 1
+    },
+    "is-a-simulation": {
+      "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
+    },
+    "leader-id": {
+      "title": "Leader ID",
+      "description": "Unique identifier of the drone chosen to be the leader.",
+      "type": "integer",
+      "default": 1
+    },
+    "multicast-ipv6": {
+      "title": "IP of the multicast group",
+      "description": "IP address used to communicate with the other drones.",
+      "type": "string",
+      "default": "ff15::1111"
+    },
+    "net-if": {
+      "title": "Network interface",
+      "description": "Interface used for multicast traffic.",
+      "type": "string",
+      "default": "eth0"
+    },
+    "drone-id-list": {
+      "title": "List of drones IDs",
+      "description": "List of identifiers of drones.",
+      "type": "array",
+      "default": []
+    },
+    "flight-script": {
+      "title": "Script of the flight",
+      "description": "Script which will be executed for the flight",
+      "type": "string",
+      "textarea": true
+    }
+  }
+}
diff --git a/software/js-drone/instance.cfg b/software/js-drone/instance.cfg
index 134a144edc4b53a61299eecdb94a28b0cd042b8c..661e1d9ac88ac9538cd500bf17930dff52c048b4 100644
--- a/software/js-drone/instance.cfg
+++ b/software/js-drone/instance.cfg
@@ -1,9 +1,6 @@
 [buildout]
 parts =
-  cli
-  demo
-  manual-flight
-  subscribe
+  main
 
 eggs-directory = ${buildout:eggs-directory}
 develop-eggs-directory = ${buildout:develop-eggs-directory}
@@ -28,14 +25,31 @@ cert = $${slap_connection:cert_file}
 recipe = slapos.recipe.build
 slapparameter-dict = $${slap-configuration:configuration}
 init =
-  options['autopilot-ip'] = options['slapparameter-dict'].get('autopilot_ip', '192.168.27.1')
-  options['id'] = options['slapparameter-dict'].get('id', '1')
-  options['is-a-simulation'] = options['slapparameter-dict'].get('is_a_simulation', 'false')
-  options['leader-id'] = options['slapparameter-dict'].get('leader_id', '1')
-  options['is-leader'] = 'true' if options['id'] == options['leader-id'] else 'false'
-  options['multicast-ipv6'] = options['slapparameter-dict'].get('multicast_ip', 'ff15::1111')
-  options['net-if'] = options['slapparameter-dict'].get('net_if', 'eth0')
-  options['drone-id-list'] = options['slapparameter-dict'].get('drone_id_list', '')
+  options['autopilot-ip'] = options['slapparameter-dict'].get('autopilot-ip', '192.168.27.1')
+  options['id'] = options['slapparameter-dict'].get('id', 1)
+  options['is-a-simulation'] = options['slapparameter-dict'].get('is-a-simulation', False)
+  options['leader-id'] = options['slapparameter-dict'].get('leader-id', 1)
+  options['is-leader'] = options['id'] == options['leader-id']
+  options['multicast-ipv6'] = options['slapparameter-dict'].get('multicast-ip', 'ff15::1111')
+  options['net-if'] = options['slapparameter-dict'].get('net-if', 'eth0')
+  options['drone-id-list'] = options['slapparameter-dict'].get('drone-id-list', [])
+  options['is-publisher'] = 'flight-script' in options['slapparameter-dict']
+
+  subscription_script = '''
+  me.onStart = function() {
+    const f = std.fdopen(std.in, "r");
+    console.log("Use q to quit");
+  };
+
+  me.onUpdate= function() {
+    while(f.getline() != "q") {
+      continue;
+    }
+    f.close();
+  };
+  '''
+
+  options['flight-script'] = options['slapparameter-dict'].get('flight-script', subscription_script)
 
 [js-dynamic-template]
 recipe = slapos.recipe.template:jinja2
@@ -46,41 +60,22 @@ context =
   raw qjs_wrapper ${qjs-wrapper:location}/lib/libqjswrapper.so
   $${:extra-context}
 
-[common]
-<= js-dynamic-template
-extra-context =
-  key autopilot_ip drone:autopilot-ip
-  key drone_id_list drone:drone-id-list
-  key is_a_simulation drone:is-a-simulation
-  key is_leader drone:is-leader
-  key leader_id drone:leader-id
-  key log_dir directory:log
-  key pubsub_script pubsub:rendered
-
-[cli]
-<= js-dynamic-template
-extra-context =
-  key common common:rendered
-
-[demo]
-<= js-dynamic-template
-extra-context =
-  key common common:rendered
-
-[manual-flight]
-<= js-dynamic-template
-extra-context =
-  key common common:rendered
-
 [pubsub]
 <= js-dynamic-template
 extra-context =
-  key id drone:id
   key ipv6 drone:multicast-ipv6
   key net_if drone:net-if
 
-[subscribe]
+[main]
 <= js-dynamic-template
 extra-context =
+  key autopilot_ip drone:autopilot-ip
   key drone_id_list drone:drone-id-list
+  key flight_script drone:flight-script
+  key id drone:id
+  key is_a_simulation drone:is-a-simulation
+  key is_leader drone:is-leader
+  key is_publisher drone:is-publisher
+  key leader_id drone:leader-id
+  key log_dir directory:log
   key pubsub_script pubsub:rendered
diff --git a/software/js-drone/main.js b/software/js-drone/main.js
new file mode 100644
index 0000000000000000000000000000000000000000..cba4c7e36179c2b9be6135b5b0dce2fe8b3b7e25
--- /dev/null
+++ b/software/js-drone/main.js
@@ -0,0 +1,124 @@
+import {
+  arm,
+  doParachute,
+  getAltitude,
+  getAltitudeRel,
+  getInitialAltitude,
+  getLatitude,
+  getLongitude,
+  getYaw,
+  initPubsub,
+  landed,
+  loiter,
+  reboot,
+  setAirspeed,
+  setAltitude,
+  setCheckpoint,
+  setTargetCoordinates,
+  start,
+  stop,
+  stopPubsub,
+  takeOffAndWait,
+  Drone
+} from "{{ qjs_wrapper }}";
+import {sleep, Worker} from "os";
+import * as std from "std";
+
+const IP = "{{ autopilot_ip }}";
+const URL = "udp://" + IP + ":7909";
+const LOG_FILE = "{{ log_dir }}/mavsdk-log";
+
+const IS_LEADER = {{ 'true' if is_leader else 'false' }};
+const LEADER_ID = {{ leader_id }};
+const IS_PUBLISHER = {{ 'true' if is_publisher else 'false' }}
+const SIMULATION = {{ 'true' if is_a_simulation else 'false' }};
+
+const droneIdList = {{ drone_id_list }};
+const droneDict = {};
+
+const pubsubScript = "{{ pubsub_script }}";
+var pubsubWorker;
+var pubsubRunning = false;
+
+const me = {
+  'id': "{{ id }}",
+  'getCurrentPosition': function() {
+    return {
+      'x': getLatitude(),
+      'y': getLongitude(),
+      'z': getAltitudeRel()
+    };
+  },
+  'onStart': function() {},
+  'onUpdate': function() {},
+  'setAirspeed': setAirspeed,
+  'setTargetCoordinates': setTargetCoordinates
+}
+
+function connect() {
+  console.log("Will connect to", URL);
+  exit_on_fail(start(URL, LOG_FILE, 60), "Failed to connect to " + URL);
+}
+
+function exit_on_fail(ret, msg) {
+  if(ret) {
+    console.log(msg);
+    quit();
+    std.exit(-1);
+  }
+}
+
+function quit() {
+  stop();
+  if(pubsubRunning) {
+    stopPubsub();
+  }
+}
+
+function takeOff() {
+  exit_on_fail(arm(), "Failed to arm");
+  takeOffAndWait();
+}
+
+function waitForLanding() {
+  while(!landed()) {
+    sleep(1000);
+  }
+}
+
+if(IS_PUBLISHER) {
+  console.log("Connecting to aupilot\n");
+  connect();
+}
+
+pubsubWorker = new Worker(pubsubScript);
+pubsubWorker.onmessage = function(e) {
+  if (!e.data.publishing)
+    pubsubWorker.onmessage = null;
+}
+
+initPubsub(droneIdList.length);
+for (let i = 0; i < droneIdList.length; i++) {
+  let id = droneIdList[i]
+  droneDict[id] = new Drone(id);
+  droneDict[id].init(i);
+}
+
+pubsubWorker.postMessage({ action: "run", id: me.id, publish: IS_PUBLISHER });
+pubsubRunning = true;
+
+{{ flight_script }}
+
+if(IS_PUBLISHER && SIMULATION) {
+  takeOff();
+}
+
+me.onStart()
+me.onUpdate();
+
+if(IS_PUBLISHER) {
+  waitForLanding();
+  quit();
+} else {
+  stopPubsub();
+};
diff --git a/software/js-drone/manual-flight.js b/software/js-drone/manual-flight.js
deleted file mode 100644
index afad0c84e897caaba3563b3f93f547c417ce6fa9..0000000000000000000000000000000000000000
--- a/software/js-drone/manual-flight.js
+++ /dev/null
@@ -1,113 +0,0 @@
-/*jslint indent2 */
-/*global console */
-import {
-  getAltitude,
-  getInitialAltitude,
-  landed,
-  loiter,
-  setTargetCoordinates
-} from "{{ qjs_wrapper }}"; //jslint-quiet
-import {sleep} from "os";
-import {
-  connect,
-  goToAltitude,
-  quit,
-  startPubsub,
-  takeOff,
-  ALTITUDE_DIFF,
-  IS_LEADER,
-  LEADER_ID,
-  SIMULATION
-} from "{{ common }}"; //jslint-quiet
-
-const FLIGH_ALTITUDE = 100;
-const PARACHUTE_ALTITUDE = 35;
-
-let INITIAL_ALTITUDE;
-let START_ALTITUDE;
-
-var leaderAltitudeAbs;
-var leaderAltitudeRel;
-var leaderLatitude;
-var leaderLongitude;
-
-function followLeader(leaderId, initialAltitude, altitudeDiff) {
-  goToAltitude(START_ALTITUDE + ALTITUDE_DIFF, false, true);
-
-  while(droneDict[leaderId].altitudeAbs == 0) {
-    console.log("[DEMO] Waiting for leader to send its altitude");
-    sleep(1000);
-  }
-
-  while(droneDict[leaderId].altitudeAbs < initialAltitude) {
-    console.log(`[DEMO] Waiting for leader to reach altitude ${initialAltitude} (currently ${droneDict[leaderId].altitudeAbs})`);
-    sleep(1000);
-  }
-
-  console.log("[DEMO] Switching to following mode...\n");
-  do {
-    leaderAltitudeAbs = droneDict[leaderId].altitudeAbs;
-    leaderAltitudeRel = droneDict[leaderId].altitudeRel;
-    leaderLatitude = droneDict[leaderId].latitude;
-    leaderLongitude = droneDict[leaderId].longitude;
-
-    setTargetCoordinates(
-      leaderLatitude,
-      leaderLongitude,
-      leaderAltitudeAbs + altitudeDiff,
-      0
-    );
-    sleep(500);
-  } while(leaderAltitudeRel > PARACHUTE_ALTITUDE);
-
-  console.log("[DEMO] Stop following...\n");
-}
-
-function waitForAltitude(altitude) {
-  var curAltitude;
-  do {
-    sleep(1000);
-    curAltitude = getAltitude();
-    console.log(
-      `[DEMO] Waiting for altitude... (${curAltitude} , ${altitude})`);
-  }
-  while(curAltitude < altitude);
-}
-
-function waitForLanding() {
-  while(!landed()) {
-    sleep(1000);
-  }
-}
-
-const droneDict = startPubsub();
-
-console.log("[DEMO] Connecting...\n");
-connect();
-
-while(getInitialAltitude() == 0) {
-  console.log("[DEMO] Waiting for first telemetry\n");
-  sleep(1000);
-}
-
-INITIAL_ALTITUDE = getInitialAltitude();
-START_ALTITUDE = INITIAL_ALTITUDE + FLIGH_ALTITUDE;
-
-if(SIMULATION) {
-  takeOff(START_ALTITUDE + 1);
-}
-
-waitForAltitude(START_ALTITUDE);
-
-console.log("[DEMO] Setting loiter mode...\n");
-loiter();
-sleep(3000);
-
-if(!IS_LEADER) {
-  followLeader(LEADER_ID, START_ALTITUDE, ALTITUDE_DIFF);
-}
-
-console.log("[DEMO] Loitering until manual intructions are given\n")
-
-waitForLanding();
-quit();
diff --git a/software/js-drone/pubsub.js b/software/js-drone/pubsub.js
index c2491732487ede460e9f83dd48b9cb550d2817a4..bfe2022bc2794769e264afee346da806422a9d5d 100644
--- a/software/js-drone/pubsub.js
+++ b/software/js-drone/pubsub.js
@@ -9,7 +9,7 @@ var parent = Worker.parent;
 function handle_msg(e) {
   switch(e.data.action) {
     case "run":
-      runPubsub(IPV6, PORT, "{{ net_if }}", {{ id }}, e.data.publish);
+      runPubsub(IPV6, PORT, "{{ net_if }}", e.data.id, e.data.publish);
       parent.postMessage({running: false});
       parent.onmessage = null;
       break;
diff --git a/software/js-drone/software.cfg b/software/js-drone/software.cfg
index c77b4ce3114a00cf9a3fd6e865ad002d2ddd165d..732387fe8cb3b236484166e109f794854e7fe4e4 100644
--- a/software/js-drone/software.cfg
+++ b/software/js-drone/software.cfg
@@ -6,12 +6,8 @@ extends =
 
 parts =
     instance-profile
-    common
-    cli
-    demo
-    manual-flight
+    main
     pubsub
-    subscribe
     slapos-cookbook
 
 [download-file-base]
@@ -24,20 +20,8 @@ recipe = slapos.recipe.template
 url = ${:_profile_base_location_}/${:filename}
 output = ${buildout:directory}/template.cfg
 
-[common]
-<= download-file-base
-
-[cli]
-<= download-file-base
-
-[demo]
-<= download-file-base
-
-[manual-flight]
+[main]
 <= download-file-base
 
 [pubsub]
 <= download-file-base
-
-[subscribe]
-<= download-file-base
diff --git a/software/js-drone/software.cfg.json b/software/js-drone/software.cfg.json
new file mode 100644
index 0000000000000000000000000000000000000000..12c3331fa27a00e2a4029e1ab94af14e71c50551
--- /dev/null
+++ b/software/js-drone/software.cfg.json
@@ -0,0 +1,14 @@
+{
+  "name": "JS Drone",
+  "description": "JS Drone",
+  "serialisation": "xml",
+  "software-type": {
+    "default": {
+      "title": "Default",
+      "software-type": "default",
+      "description": "Default",
+      "request": "instance-input-schema.json",
+      "index": 0
+    }
+  }
+}
diff --git a/software/js-drone/subscribe.js b/software/js-drone/subscribe.js
deleted file mode 100644
index f865224fb35059f5af901e66d026907d0b944b58..0000000000000000000000000000000000000000
--- a/software/js-drone/subscribe.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/*jslint-disable*/
-{% set comma_separated_drone_id_list = ', '.join(drone_id_list.split()) -%}
-/*jslint-enable*/
-
-import {
-  initPubsub,
-  stopPubsub,
-  Drone
-} from "{{ qjs_wrapper }}"; //jslint-quiet
-import {Worker} from "os";
-/*jslint-disable*/
-import * as std from "std";
-/*jslint-enable*/
-
-const droneIdList = [{{ comma_separated_drone_id_list }}];
-const droneDict = {};
-
-var pubsubWorker = new Worker("{{ pubsub_script }}");
-pubsubWorker.onmessage = function(e) {
-  if (!e.data.publishing)
-    pubsubWorker.onmessage = null;
-}
-
-initPubsub(droneIdList.length);
-for (let i = 0; i < droneIdList.length; i++) {
-  let id = droneIdList[i]
-  droneDict[id] = new Drone(id);
-  droneDict[id].init(i);
-}
-
-pubsubWorker.postMessage({ action: "run", publish: false });
-
-const f = std.fdopen(std.in, "r");
-console.log("Use q to quit");
-while (f.getline() != "q") {
-  continue;
-}
-
-stopPubsub();
-f.close();