180
api/index.js
180
api/index.js
@@ -2,18 +2,118 @@ var express = require("express");
|
|||||||
var app = express();
|
var app = express();
|
||||||
const WebSocket = require("ws");
|
const WebSocket = require("ws");
|
||||||
|
|
||||||
var ws = new WebSocket("ws://10.100.0.40:9001/");
|
//# TODO SSE https://www.digitalocean.com/community/tutorials/nodejs-server-sent-events-build-realtime-app
|
||||||
|
|
||||||
ws.on('open', function open() {
|
var last_status = {};
|
||||||
console.log("connected");
|
var last_image;
|
||||||
|
var received_picture = false;
|
||||||
|
var received_error = false;
|
||||||
|
|
||||||
|
let sse_clients = [];
|
||||||
|
|
||||||
|
app.use(express.static("public"));
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
var ws;
|
||||||
|
var api_connected = false;
|
||||||
|
|
||||||
|
function send_events_to_clients(data) {
|
||||||
|
// console.log("sending events to clients");
|
||||||
|
sse_clients.forEach((client) => {
|
||||||
|
client.response.write("event: message\n");
|
||||||
|
client.response.write("data:" + JSON.stringify(data) + "\n\n");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_sse_client(request, response, next) {
|
||||||
|
console.log("handling sse client");
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "text/event-stream",
|
||||||
|
Connection: "keep-alive",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
};
|
||||||
|
|
||||||
|
response.writeHead(200, headers);
|
||||||
|
response.write(JSON.stringify("yeet") + "\n\n");
|
||||||
|
const clientID = Date.now();
|
||||||
|
const newClient = {
|
||||||
|
id: clientID,
|
||||||
|
response,
|
||||||
|
};
|
||||||
|
|
||||||
|
sse_clients.push(newClient);
|
||||||
|
|
||||||
|
request.on("close", () => {
|
||||||
|
console.log(`${clientID} Connection closed`);
|
||||||
|
sse_clients = sse_clients.filter((client) => client.id !== clientID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var connect_to_api = function () {
|
||||||
|
console.log("Connecting to API");
|
||||||
|
ws = new WebSocket("ws://10.100.0.40:9001/");
|
||||||
|
|
||||||
|
ws.on("open", function open() {
|
||||||
|
console.log("connected with websockets to API!");
|
||||||
|
api_connected = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on("message", function message(message) {
|
ws.on("message", function message(message) {
|
||||||
var msg = JSON.parse(message);
|
try {
|
||||||
console.log("RECEIVED: " + msg);
|
var msg = JSON.parse(message);
|
||||||
});
|
if (msg.type != "IMAGE") {
|
||||||
|
// console.log("got message");
|
||||||
|
send_events_to_clients(msg);
|
||||||
|
} else {
|
||||||
|
console.log("got image");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("could not parse as json");
|
||||||
|
// send_image_data_to_clients(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ws.on("error", console.error);
|
ws.on("error", function error(err) {
|
||||||
|
console.log("there was an error");
|
||||||
|
console.error("error: " + err);
|
||||||
|
received_error = true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function send_image_data_to_clients(videoData) {
|
||||||
|
sse_clients.forEach((client) => {
|
||||||
|
// Set the SSE event name as 'message'
|
||||||
|
client.response.write("event: message\n");
|
||||||
|
|
||||||
|
// Convert the Buffer to a base64-encoded string
|
||||||
|
const base64Data = videoData.toString("base64");
|
||||||
|
|
||||||
|
// Set the SSE event data as the base64-encoded string
|
||||||
|
client.response.write(
|
||||||
|
"data: " + JSON.stringify({ image: base64Data }) + "\n\n"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the endpoint to receive video data
|
||||||
|
app.post("/video", (req, res) => {
|
||||||
|
// console.log("got video endpoint")
|
||||||
|
let videoData = Buffer.from("");
|
||||||
|
|
||||||
|
req.on("data", (chunk) => {
|
||||||
|
// Accumulate the received video data
|
||||||
|
videoData = Buffer.concat([videoData, chunk]);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on("end", () => {
|
||||||
|
// Process the received video data
|
||||||
|
// console.log("Received video data:" + videoData.length);
|
||||||
|
send_image_data_to_clients(videoData);
|
||||||
|
|
||||||
|
// Send a response indicating successful receipt
|
||||||
|
res.sendStatus(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// set the view engine to ejs
|
// set the view engine to ejs
|
||||||
app.set("view engine", "ejs");
|
app.set("view engine", "ejs");
|
||||||
@@ -22,7 +122,69 @@ app.set("view engine", "ejs");
|
|||||||
|
|
||||||
// index page
|
// index page
|
||||||
app.get("/", function (req, res) {
|
app.get("/", function (req, res) {
|
||||||
res.render("index");
|
res.render("index", { api_connected: api_connected });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/events", handle_sse_client);
|
||||||
|
|
||||||
|
app.get("/image", function (req, res) {
|
||||||
|
console.log("got picture request");
|
||||||
|
var request = JSON.stringify({
|
||||||
|
command: 5,
|
||||||
|
});
|
||||||
|
console.log("sending picture request");
|
||||||
|
ws.send(request);
|
||||||
|
res.status(200).send(last_image);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/move", function (req, res) {
|
||||||
|
console.log("got move request");
|
||||||
|
var request = JSON.stringify({
|
||||||
|
command: 3,
|
||||||
|
up_down: req.body.up_down,
|
||||||
|
left_right: req.body.left_right,
|
||||||
|
forward_backward: req.body.forward_backward,
|
||||||
|
yaw: req.body.turn_left_right,
|
||||||
|
});
|
||||||
|
ws.send(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/estop", function (req, res) {
|
||||||
|
console.log("got estop request");
|
||||||
|
var request = JSON.stringify({
|
||||||
|
command: 6,
|
||||||
|
});
|
||||||
|
ws.send(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/land", function (req, res) {
|
||||||
|
console.log("got land request");
|
||||||
|
var request = JSON.stringify({ command: 0 });
|
||||||
|
ws.send(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/arm_disarm", function (req, res) {
|
||||||
|
console.log("got arm/disarm request");
|
||||||
|
var request = JSON.stringify({ command: 1 });
|
||||||
|
ws.send(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/connect", function (req, res) {
|
||||||
|
console.log("got connect request");
|
||||||
|
connect_to_api();
|
||||||
|
setTimeout(function () {
|
||||||
|
if (api_connected) {
|
||||||
|
console.log("Connected to API");
|
||||||
|
res.status(200).json({ connected: true });
|
||||||
|
} else {
|
||||||
|
received_error = false;
|
||||||
|
res.status(400).json({ connected: false });
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/test", function (req, res) {
|
||||||
|
res.render("test");
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(8080);
|
app.listen(8080);
|
||||||
|
|||||||
66
api/public/css/stylesheet.css
Normal file
66
api/public/css/stylesheet.css
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
body {
|
||||||
|
background-color: azure;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
color: black;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video {
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainvideo {
|
||||||
|
width: 50%;
|
||||||
|
float: left;
|
||||||
|
height: 100%;
|
||||||
|
border: 1px solid blue;
|
||||||
|
margin-right: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lastpicture {
|
||||||
|
width: 40%;
|
||||||
|
float: right;
|
||||||
|
/* height: 400px; */
|
||||||
|
border: 1px solid red;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#connectedbuttons {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#buttons {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#connectedstatus {
|
||||||
|
float:left;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#take_picture {
|
||||||
|
float:right;
|
||||||
|
}
|
||||||
|
#arm_disarm {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#button_estop {
|
||||||
|
background-color: red;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerimg {
|
||||||
|
height: 200px;
|
||||||
|
/* float: right; */
|
||||||
|
}
|
||||||
BIN
api/public/img/droneimage_2023-05-30_14-55-57.jpg
Normal file
BIN
api/public/img/droneimage_2023-05-30_14-55-57.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 MiB |
@@ -1,12 +1,418 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>5G drone API</title>
|
<link rel='stylesheet' href='/css/stylesheet.css' />
|
||||||
</head>
|
|
||||||
<body>
|
<title>5G drone API</title>
|
||||||
<div>Hello World!</div>
|
</head>
|
||||||
</body>
|
|
||||||
</html>
|
<body style="height: 100%;">
|
||||||
|
<div>
|
||||||
|
<h1 class="header">5G Drone API</h1>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="video"> -->
|
||||||
|
<div class="mainvideo">
|
||||||
|
<p id="cameraview">Camera view: Not connected</p>
|
||||||
|
<canvas id="result-video" style="border: 1px solid blue;" width="800" height="600"></canvas>
|
||||||
|
<div id="connectedbuttons">
|
||||||
|
<div id="connectedstatus">
|
||||||
|
<h2 id="connectedlabel">Not connected to drone</h2>
|
||||||
|
<button id="connectbutton" onclick="local_connect()">Connect</button>
|
||||||
|
</div>
|
||||||
|
<div id="buttons">
|
||||||
|
<button id="take_picture" onclick="take_picture()">Take picture</button>
|
||||||
|
<br>
|
||||||
|
<button id="arm_disarm" onclick="arm_disarm()">Arm/Disarm</button>
|
||||||
|
<!-- <br>
|
||||||
|
<label for="control_mode">Control mode:</label>
|
||||||
|
<br>
|
||||||
|
<select name="control_mode" id="control_mode">
|
||||||
|
<option value="attitude">Attitude</option>
|
||||||
|
<option value="velocity">Velocity</option>
|
||||||
|
<option value="position">Position</option>
|
||||||
|
</select> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="controls">
|
||||||
|
<h2>Controls</h2>
|
||||||
|
<div id="buttons">
|
||||||
|
<button class="movebutton" id="button_turnleft">Turn left</button>
|
||||||
|
<button class="movebutton" id="button_turnright">Turn right</button>
|
||||||
|
<button class="movebutton" id="button_up">Up</button>
|
||||||
|
<button class="movebutton" id="button_down">Down</button>
|
||||||
|
<button class="movebutton" id="button_forward">Forward</button>
|
||||||
|
<button class="movebutton" id="button_backward">Backward</button>
|
||||||
|
<button class="movebutton" id="button_left">Left</button>
|
||||||
|
<button class="movebutton" id="button_right">Right</button>
|
||||||
|
<button id="button_land" onclick="land()">Land</button>
|
||||||
|
<button id="button_estop" onclick="estop()"><strong>Emergency Stop</strong></button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="lastpicture">
|
||||||
|
<p>Last picture:</p>
|
||||||
|
<div id="lastimg">
|
||||||
|
<img id="picture" style="width: 400px;">
|
||||||
|
</div>
|
||||||
|
<h2>Drone status</h2>
|
||||||
|
<p id="batterypercentage">Battery percentage</p>
|
||||||
|
<p id="cpuload">CPU load</p>
|
||||||
|
<p id="armed"></p>
|
||||||
|
<p id="control_mode"></p>
|
||||||
|
<p id="speed">Current speed</p>
|
||||||
|
<p id="position">Current position</p>
|
||||||
|
<p id="height">Height</p>
|
||||||
|
<p id="failsafe">Failsafe not activated</p>
|
||||||
|
<img class="headerimg"
|
||||||
|
src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/e9/Ericsson_logo.svg/2341px-Ericsson_logo.svg.png"
|
||||||
|
alt="ericsson logo">
|
||||||
|
<img class="headerimg" src="https://hightechcampus.com/storage/1069/5ghub-logo.png" alt="5g hub logo">
|
||||||
|
</div>
|
||||||
|
<!-- </div> -->
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var ws;
|
||||||
|
var api_timout;
|
||||||
|
var checked_for_connection = false;
|
||||||
|
var connected_to_api = false;
|
||||||
|
assign_button_callbacks();
|
||||||
|
openSocket = () => {
|
||||||
|
|
||||||
|
document.getElementById("cameraview").innerHTML = "Camera view: Connecting...";
|
||||||
|
socket = new WebSocket("ws://10.100.0.40:9002/");
|
||||||
|
let msg = document.getElementById("result-video");
|
||||||
|
socket.addEventListener('open', (e) => {
|
||||||
|
console.log("Connected to video")
|
||||||
|
document.getElementById("cameraview").innerHTML = "Camera view: Connected";
|
||||||
|
});
|
||||||
|
socket.addEventListener('message', (e) => {
|
||||||
|
let ctx = msg.getContext("2d");
|
||||||
|
let image = new Image();
|
||||||
|
image.src = URL.createObjectURL(e.data);
|
||||||
|
image.addEventListener("load", (e) => {
|
||||||
|
ctx.drawImage(image, 0, 0, 800, 600);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
socket.addEventListener('close', (e) => {
|
||||||
|
console.log("Disconnected from video")
|
||||||
|
document.getElementById("cameraview").innerHTML = "Camera view: Disconnected. Reload the page to reconnect";
|
||||||
|
});
|
||||||
|
socket.addEventListener('error', (e) => {
|
||||||
|
console.log("Error in video connection")
|
||||||
|
document.getElementById("cameraview").innerHTML = "Camera view: Error. Reload the page to reconnect";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to decode base64 image and set it as source of <img> element
|
||||||
|
function decodeBase64Image(base64Data, imgElement) {
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
|
img.onload = function () {
|
||||||
|
// Once the image has loaded, set it as the source of the <img> element
|
||||||
|
imgElement.src = this.src;
|
||||||
|
// console.log("set image data src")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set the base64-encoded image data as the source of the image
|
||||||
|
img.src = 'data:image/jpeg;base64,' + base64Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onload = function () {
|
||||||
|
const events = new EventSource("/events");
|
||||||
|
|
||||||
|
events.onopen = () => {
|
||||||
|
console.log("OPENED EVENT");
|
||||||
|
}
|
||||||
|
|
||||||
|
events.onmessage = (event) => {
|
||||||
|
console.log("MESSAGE RECEIVED")
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.type == "STATUS") {
|
||||||
|
document.getElementById("batterypercentage").innerHTML = "Battery percentage: " + data.data.battery_percentage.toString().substring(0, 4) + "%";
|
||||||
|
document.getElementById("cpuload").innerHTML = "CPU load: " + data.data.cpu_usage.toString().substring(0, 6).substring(2, 4) + "%";
|
||||||
|
document.getElementById("armed").innerHTML = "Armed: " + data.data.armed;
|
||||||
|
document.getElementById("control_mode").innerHTML = "Control mode: " + data.data.control_mode;
|
||||||
|
document.getElementById("speed").innerHTML = "Current speed (m/s): x: " + data.data.velocity[0].toString().substring(0, 4) + " y: " + data.data.velocity[1].toString().substring(0, 4) + " z: " + data.data.velocity[2].toString().substring(0, 4);
|
||||||
|
document.getElementById("position").innerHTML = "Current position (m): x: " + data.data.position[0].toString().substring(0, 4) + " y: " + data.data.position[1].toString().substring(0, 4) + " z: " + data.data.position[2].toString().substring(0, 4);
|
||||||
|
document.getElementById("height").innerHTML = "Height (m): " + data.data.height;
|
||||||
|
} else if (data.type == "FAILSAFE") {
|
||||||
|
document.getElementById("failsafe").innerHTML = "Failsafe: ACTIVATED";
|
||||||
|
document.getElementById("failsafe").style.backgroundColor = "red";
|
||||||
|
document.getElementById("failsafe").style.color = "white";
|
||||||
|
document.getElementById("failsafe").style.textDecoration = "bold";
|
||||||
|
alert("Failsafe enabled! Drone is landing. The failsafe message is:\n" + data.message);
|
||||||
|
} else {
|
||||||
|
// decodeBase64Image(data.image, document.getElementById("result-video"));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Received an error")
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function assign_button_callbacks() {
|
||||||
|
var buttons = document.getElementsByClassName("movebutton");
|
||||||
|
for (var i = 0; i < buttons.length; i++) {
|
||||||
|
buttons[i].addEventListener("mouseup", function () {
|
||||||
|
stop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.getElementById("button_forward").addEventListener("mousedown", function () { forward(); });
|
||||||
|
document.getElementById("button_backward").addEventListener("mousedown", function () { backward(); });
|
||||||
|
document.getElementById("button_left").addEventListener("mousedown", function () { left(); });
|
||||||
|
document.getElementById("button_right").addEventListener("mousedown", function () { right(); });
|
||||||
|
document.getElementById("button_turnleft").addEventListener("mousedown", function () { turn_left(); });
|
||||||
|
document.getElementById("button_turnright").addEventListener("mousedown", function () { turn_right(); });
|
||||||
|
document.getElementById("button_up").addEventListener("mousedown", function () { up(); });
|
||||||
|
document.getElementById("button_down").addEventListener("mousedown", function () { down(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_status() {
|
||||||
|
// {"battery_percentage": 100.0, "cpu_usage": 0.0, "armed": false, "control_mode": "attitude", "route_setpoint": 0}}
|
||||||
|
// console.log("updating status")
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("GET", "/status", true);
|
||||||
|
xhr.onreadystatechange = function () {
|
||||||
|
if (this.status == 200) {
|
||||||
|
// console.log(this.responseText);
|
||||||
|
if (this.responseText.length > 0) {
|
||||||
|
var status = JSON.parse(this.responseText);
|
||||||
|
// console.log(status)
|
||||||
|
document.getElementById("batterypercentage").innerHTML = "Battery percentage: " + status.battery_percentage;
|
||||||
|
document.getElementById("cpuload").innerHTML = "CPU load: " + status.cpu_usage;
|
||||||
|
document.getElementById("armed").innerHTML = "Armed: " + status.armed;
|
||||||
|
document.getElementById("control_mode").innerHTML = "Control mode: " + status.control_mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
function send_move_request(data) {
|
||||||
|
console.log("sending move request");
|
||||||
|
if (ws != null)
|
||||||
|
ws.send(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function turn_left() {
|
||||||
|
console.log("turnleft");
|
||||||
|
send_move_request(JSON.stringify({ "command": 3, "up_down": 0.0, "forward_backward": 0.0, "left_right": 0.0, "yaw": -10.0 }));
|
||||||
|
}
|
||||||
|
function turn_right() {
|
||||||
|
console.log("turnright");
|
||||||
|
send_move_request(JSON.stringify({ "command": 3, "up_down": 0.0, "forward_backward": 0.0, "left_right": 0.0, "yaw": 10.0 }));
|
||||||
|
}
|
||||||
|
function up() {
|
||||||
|
console.log("up");
|
||||||
|
send_move_request(JSON.stringify({ "command": 3, "up_down": 1.0, "forward_backward": 0.0, "left_right": 0.0, "yaw": 0.0 }));
|
||||||
|
|
||||||
|
}
|
||||||
|
function down() {
|
||||||
|
console.log("down");
|
||||||
|
send_move_request(JSON.stringify({ "command": 3, "up_down": -1.0, "forward_backward": 0.0, "left_right": 0.0, "yaw": 0.0 }));
|
||||||
|
|
||||||
|
}
|
||||||
|
function forward() {
|
||||||
|
console.log("forward"); send_move_request(JSON.stringify({ "command": 3, "up_down": 0.0, "forward_backward": 1.0, "left_right": 0.0, "yaw": 0.0 }));
|
||||||
|
|
||||||
|
}
|
||||||
|
function backward() {
|
||||||
|
console.log("backward");
|
||||||
|
send_move_request(JSON.stringify({ "command": 3, "up_down": 0.0, "forward_backward": -1.0, "left_right": 0.0, "yaw": 0.0 }));
|
||||||
|
}
|
||||||
|
function left() {
|
||||||
|
console.log("left");
|
||||||
|
send_move_request(JSON.stringify({ "command": 3, "up_down": 0.0, "forward_backward": 0.0, "left_right": -1.0, "yaw": 0.0 }));
|
||||||
|
}
|
||||||
|
function right() {
|
||||||
|
console.log("right");
|
||||||
|
send_move_request(JSON.stringify({ "command": 3, "up_down": 0.0, "forward_backward": 0.0, "left_right": 1.0, "yaw": 0.0 }));
|
||||||
|
}
|
||||||
|
function stop() {
|
||||||
|
console.log("stop");
|
||||||
|
send_move_request(JSON.stringify({ "command": 3, "up_down": 0.0, "forward_backward": 0.0, "left_right": 0.0, "yaw": 0.0 }));
|
||||||
|
}
|
||||||
|
function land() {
|
||||||
|
console.log("land");
|
||||||
|
var request = JSON.stringify({ command: 0 });
|
||||||
|
ws.send(request);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function estop() {
|
||||||
|
console.log("estop");
|
||||||
|
var request = JSON.stringify({
|
||||||
|
command: 6,
|
||||||
|
});
|
||||||
|
ws.send(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
function take_picture() {
|
||||||
|
console.log("take picture");
|
||||||
|
var image = new Image();
|
||||||
|
image.src = document.getElementById("result-video").toDataURL();
|
||||||
|
image.width = 400;
|
||||||
|
document.getElementById("lastimg").innerHTML = "";
|
||||||
|
document.getElementById("lastimg").appendChild(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
function arm_disarm() {
|
||||||
|
console.log("arm/disarm");
|
||||||
|
var request = JSON.stringify({ command: 1 });
|
||||||
|
ws.send(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect_to_video_stream() {
|
||||||
|
console.log("Connecting to websocket")
|
||||||
|
const video_holder = document.getElementById("result-video");
|
||||||
|
const socket = new WebSocket('ws://10.100.0.40:9002/');
|
||||||
|
|
||||||
|
socket.addEventListener('open', (event) => {
|
||||||
|
console.log('Connected to WebSocket server');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.addEventListener('message', (event) => {
|
||||||
|
// Assuming the received data is an ArrayBuffer or Uint8Array
|
||||||
|
const imageData = event.data;
|
||||||
|
|
||||||
|
// Convert the image data to a Blob
|
||||||
|
const blob = new Blob([imageData], { type: 'image/jpeg' });
|
||||||
|
|
||||||
|
// Generate a temporary URL for the Blob
|
||||||
|
const imageURL = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Set the src attribute of the <img> element
|
||||||
|
video_holder.src = imageURL;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connection closed
|
||||||
|
socket.addEventListener('close', (event) => {
|
||||||
|
console.log('Disconnected from WebSocket server');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error occurred
|
||||||
|
socket.addEventListener('error', (event) => {
|
||||||
|
console.error('WebSocket error:', event.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// var set_timout = false;
|
||||||
|
|
||||||
|
function handle_ws_message(data) {
|
||||||
|
// console.log("Handling message " + data);
|
||||||
|
// clearTimeout(api_timout);
|
||||||
|
set_timout = false;
|
||||||
|
if (data.type == "STATUS") {
|
||||||
|
document.getElementById("batterypercentage").innerHTML = "Battery percentage: " + data.data.battery_percentage.toString().substring(0, 4) + "%";
|
||||||
|
document.getElementById("cpuload").innerHTML = "CPU load: " + data.data.cpu_usage.toString().substring(0, 6).substring(2, 4) + "%";
|
||||||
|
document.getElementById("armed").innerHTML = "Armed: " + data.data.armed;
|
||||||
|
document.getElementById("control_mode").innerHTML = "Control mode: " + data.data.control_mode;
|
||||||
|
document.getElementById("speed").innerHTML = "Current speed (m/s): x: " + data.data.velocity[0].toString().substring(0,4) + " y: " + data.data.velocity[1].toString().substring(0,4) + " z: " + data.data.velocity[2].toString().substring(0,4);
|
||||||
|
document.getElementById("position").innerHTML = "Current position (m): x: " + data.data.position[0].toString().substring(0,4) + " y: " + data.data.position[1].toString().substring(0,4) + " z: " + data.data.position[2].toString().substring(0,4);
|
||||||
|
// TODO fix
|
||||||
|
// if (set_timout == false) {
|
||||||
|
// api_timeout = setTimeout(function () {
|
||||||
|
// set_timout = true;
|
||||||
|
// console.log("API timed out")
|
||||||
|
// alert("Connection to API timed out!");
|
||||||
|
// document.getElementById("connectedlabel").innerHTML = "Not connected to drone";
|
||||||
|
// document.getElementById("connectbutton").disabled = false;
|
||||||
|
// }, 5000);
|
||||||
|
// }
|
||||||
|
} else if (data.type == "FAILSAFE") {
|
||||||
|
document.getElementById("failsafe").innerHTML = "Failsafe: ACTIVATED";
|
||||||
|
document.getElementById("failsafe").style.backgroundColor = "red";
|
||||||
|
document.getElementById("failsafe").style.color = "white";
|
||||||
|
document.getElementById("failsafe").style.textDecoration = "bold";
|
||||||
|
alert("Failsafe enabled! Drone is landing. The failsafe message is:\n" + data.message);
|
||||||
|
} else if (data.type == "API_HEARTBEAT") {
|
||||||
|
concole.log("Got heartbeat from API")
|
||||||
|
clearTimeout(api_timout);
|
||||||
|
} else {
|
||||||
|
// decodeBase64Image(data.image, document.getElementById("result-video"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function local_connect() {
|
||||||
|
console.log("Connecting to API");
|
||||||
|
ws = new WebSocket("ws://10.100.0.40:9001/");
|
||||||
|
|
||||||
|
ws.addEventListener("open", function open() {
|
||||||
|
console.log("connected with websockets to API!");
|
||||||
|
document.getElementById("connectedlabel").innerHTML = "Connected to drone";
|
||||||
|
document.getElementById("connectbutton").disabled = true;
|
||||||
|
connected_to_api = true;
|
||||||
|
openSocket();
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener("message", function message(message) {
|
||||||
|
try {
|
||||||
|
// console.log(message.data)
|
||||||
|
var msg = JSON.parse(message.data);
|
||||||
|
handle_ws_message(msg);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log("could not parse as json");
|
||||||
|
console.error(error)
|
||||||
|
// send_image_data_to_clients(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener("error", function error(err) {
|
||||||
|
console.log("there was an error! " + err);
|
||||||
|
// console.error("error: " + err);
|
||||||
|
console.log("Showing alert!");
|
||||||
|
alert("There was an error connecting to the API!");
|
||||||
|
document.getElementById("connectedlabel").innerHTML = "Not connected to drone";
|
||||||
|
document.getElementById("connectbutton").disabled = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener("close", function close(event) {
|
||||||
|
console.log("connection closed");
|
||||||
|
alert("Connection to API closed!")
|
||||||
|
document.getElementById("connectedlabel").innerHTML = "Not connected to drone";
|
||||||
|
document.getElementById("connectbutton").disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
var received = false;
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("GET", "/connect", true);
|
||||||
|
xhr.onreadystatechange = function () {
|
||||||
|
if (this.status == 200) {
|
||||||
|
console.log(this.responseText);
|
||||||
|
if (this.responseText.length > 0) {
|
||||||
|
var status = JSON.parse(this.responseText);
|
||||||
|
// console.log(status)
|
||||||
|
document.getElementById("connectedlabel").innerHTML = "Connected to drone";
|
||||||
|
document.getElementById("connectbutton").disabled = true;
|
||||||
|
// connect_to_video_stream();
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("error");
|
||||||
|
document.getElementById("connectedlabel").innerHTML = "Not connected to drone";
|
||||||
|
if (!received) {
|
||||||
|
alert("Could not connect to API!");
|
||||||
|
received = true;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
}
|
||||||
|
// window onload function die elke seconde een request doet om te kijken of er al nieuwe foto is
|
||||||
|
// function die elke 100 ms een request doet om de status te updaten
|
||||||
|
// button callbacks
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</html>
|
||||||
37
api/views/test.ejs
Normal file
37
api/views/test.ejs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Python_Websocket_Live_Streaming</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="style.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body onload="openSocket()">
|
||||||
|
<div id="status">
|
||||||
|
Connection failed. Somebody may be using the socket.
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center">
|
||||||
|
<canvas id="msg" width="960" height="720" style="display:inline-block" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
openSocket = () => {
|
||||||
|
|
||||||
|
socket = new WebSocket("ws://10.100.0.40:9001/");
|
||||||
|
let msg = document.getElementById("msg");
|
||||||
|
socket.addEventListener('open', (e) => {
|
||||||
|
document.getElementById("status").innerHTML = "Opened";
|
||||||
|
});
|
||||||
|
socket.addEventListener('message', (e) => {
|
||||||
|
let ctx = msg.getContext("2d");
|
||||||
|
let image = new Image();
|
||||||
|
image.src = URL.createObjectURL(e.data);
|
||||||
|
image.addEventListener("load", (e) => {
|
||||||
|
ctx.drawImage(image, 0, 0, msg.width, msg.height);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</html>
|
||||||
14
drone_scripts/restart_services.sh
Executable file
14
drone_scripts/restart_services.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
echo "waiting 5 secs before restarting..."
|
||||||
|
sleep 5
|
||||||
|
echo "restarting"
|
||||||
|
|
||||||
|
sudo systemctl restart drone_api.service
|
||||||
|
sudo systemctl restart drone_camera.service
|
||||||
|
sudo systemctl restart drone_failsafe.service
|
||||||
|
sudo systemctl restart drone_find_usb_devices.service
|
||||||
|
sudo systemctl restart drone_height_sensor.service
|
||||||
|
sudo systemctl restart drone_lidar.service
|
||||||
|
sudo systemctl restart drone_positionchanger.service
|
||||||
|
sudo systemctl restart drone_px4_connection.service
|
||||||
|
sudo systemctl restart drone_relais.service
|
||||||
|
sudo systemctl restart drone_status.service
|
||||||
7
drone_scripts/start_api.sh
Executable file
7
drone_scripts/start_api.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
. /home/ubuntu/source_ros2.sh
|
||||||
|
|
||||||
|
ros2 run api_communication api_listener
|
||||||
|
|
||||||
|
|
||||||
7
drone_scripts/start_camera.sh
Executable file
7
drone_scripts/start_camera.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
. /home/ubuntu/source_ros2.sh
|
||||||
|
|
||||||
|
ros2 run camera camera_controller
|
||||||
|
|
||||||
|
|
||||||
7
drone_scripts/start_failsafe.sh
Executable file
7
drone_scripts/start_failsafe.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
. /home/ubuntu/source_ros2.sh
|
||||||
|
|
||||||
|
ros2 run failsafe failsafe
|
||||||
|
|
||||||
|
|
||||||
7
drone_scripts/start_positionchanger.sh
Executable file
7
drone_scripts/start_positionchanger.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
. /home/ubuntu/source_ros2.sh
|
||||||
|
|
||||||
|
ros2 run drone_controls position_changer
|
||||||
|
|
||||||
|
|
||||||
7
drone_scripts/start_px4_connection.sh
Executable file
7
drone_scripts/start_px4_connection.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
. /home/ubuntu/source_ros2.sh
|
||||||
|
|
||||||
|
ros2 launch px4_connection px4_controller_heardbeat_launch.py
|
||||||
|
|
||||||
|
|
||||||
7
drone_scripts/start_relais_controller.sh
Executable file
7
drone_scripts/start_relais_controller.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
. /home/ubuntu/source_ros2.sh
|
||||||
|
|
||||||
|
ros2 run relais_control relais_controller
|
||||||
|
|
||||||
|
|
||||||
7
drone_scripts/start_status.sh
Executable file
7
drone_scripts/start_status.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
. /home/ubuntu/source_ros2.sh
|
||||||
|
|
||||||
|
ros2 run drone_status drone_status
|
||||||
|
|
||||||
|
|
||||||
10
drone_scripts/stop_services.sh
Executable file
10
drone_scripts/stop_services.sh
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
sudo systemctl stop drone_api.service
|
||||||
|
sudo systemctl stop drone_camera.service
|
||||||
|
sudo systemctl stop drone_failsafe.service
|
||||||
|
sudo systemctl stop drone_find_usb_devices.service
|
||||||
|
sudo systemctl stop drone_height_sensor.service
|
||||||
|
sudo systemctl stop drone_lidar.service
|
||||||
|
sudo systemctl stop drone_positionchanger.service
|
||||||
|
sudo systemctl stop drone_px4_connection.service
|
||||||
|
sudo systemctl stop drone_relais.service
|
||||||
|
sudo systemctl stop drone_status.service
|
||||||
10
restart_services.sh
Executable file
10
restart_services.sh
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
sudo systemctl restart drone_api.service
|
||||||
|
sudo systemctl restart drone_camera.service
|
||||||
|
sudo systemctl restart drone_failsafe.service
|
||||||
|
sudo systemctl restart drone_find_usb_devices.service
|
||||||
|
sudo systemctl restart drone_height_sensor.service
|
||||||
|
sudo systemctl restart drone_lidar.service
|
||||||
|
sudo systemctl restart drone_positionchanger.service
|
||||||
|
sudo systemctl restart drone_px4_connection.service
|
||||||
|
sudo systemctl restart drone_relais.service
|
||||||
|
sudo systemctl restart drone_status.service
|
||||||
@@ -2,46 +2,103 @@ import rclpy
|
|||||||
from rclpy.node import Node
|
from rclpy.node import Node
|
||||||
|
|
||||||
from drone_services.msg import DroneStatus
|
from drone_services.msg import DroneStatus
|
||||||
|
from drone_services.msg import FailsafeMsg
|
||||||
from drone_services.srv import TakePicture
|
from drone_services.srv import TakePicture
|
||||||
|
from drone_services.srv import MovePosition
|
||||||
|
from drone_services.srv import EnableFailsafe
|
||||||
|
from drone_services.srv import ArmDrone
|
||||||
|
from drone_services.srv import DisarmDrone
|
||||||
|
from drone_services.srv import ReadyDrone
|
||||||
|
from drone_services.srv import Land
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import websockets.server
|
import websockets.server
|
||||||
import threading
|
import threading
|
||||||
import json
|
import json
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from functools import partial
|
||||||
|
import base64
|
||||||
|
import cv2
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
# communication: client always sends commands that have a command id.
|
# communication: client always sends commands that have a command id.
|
||||||
# server always sends messages back that have a message type
|
# server always sends messages back that have a message type
|
||||||
|
|
||||||
|
# TODO send video https://github.com/Jatin1o1/Python-Javascript-Websocket-Video-Streaming-/tree/main
|
||||||
|
|
||||||
|
RES_4K_H = 3496
|
||||||
|
RES_4K_W = 4656
|
||||||
|
|
||||||
|
# TODO change video to be handled by camera controller through websocket with different port
|
||||||
|
|
||||||
|
|
||||||
class RequestCommand(Enum):
|
class RequestCommand(Enum):
|
||||||
GET_COMMANDS_TYPES = -1 # to get the available commands and types
|
"""
|
||||||
TAKEOFF = 0
|
Enum for the commands that can be sent to the API
|
||||||
LAND = 1
|
"""
|
||||||
MOVE_POSITION = 2
|
GET_COMMANDS_TYPES = -1
|
||||||
|
LAND = 0
|
||||||
|
ARM_DISARM = 1
|
||||||
MOVE_DIRECTION = 3
|
MOVE_DIRECTION = 3
|
||||||
TAKE_PICTURE = 5
|
EMERGENCY_STOP = 6
|
||||||
|
|
||||||
|
|
||||||
class ResponseMessage(Enum):
|
class ResponseMessage(Enum):
|
||||||
|
"""
|
||||||
|
Enum for the messages that can be sent to the client
|
||||||
|
"""
|
||||||
ALL_REQUESTS_RESPONSES = -1
|
ALL_REQUESTS_RESPONSES = -1
|
||||||
STATUS = 0
|
STATUS = 0
|
||||||
IMAGE = 1
|
MOVE_DIRECTION_RESULT = 2
|
||||||
|
FAILSAFE = 3
|
||||||
|
|
||||||
|
|
||||||
class ApiListener(Node):
|
class ApiListener(Node):
|
||||||
|
"""
|
||||||
|
Node that listens to the client and sends the commands to the drone
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__('api_listener')
|
super().__init__('api_listener')
|
||||||
self.get_logger().info('ApiListener node started')
|
self.get_logger().info('ApiListener node started')
|
||||||
self.drone_status_subscriber = self.create_subscription(
|
self.drone_status_subscriber = self.create_subscription(
|
||||||
DroneStatus, '/drone/status', self.drone_status_callback, 10)
|
DroneStatus, '/drone/status', self.drone_status_callback, 10)
|
||||||
|
self.failsafe_subscriber = self.create_subscription(
|
||||||
|
FailsafeMsg, "/drone/failsafe", self.failsafe_callback, 10)
|
||||||
|
|
||||||
self.timer = self.create_timer(1, self.publish_status)
|
self.timer = self.create_timer(1, self.publish_status)
|
||||||
|
|
||||||
self.take_picture_client = self.create_client(
|
self.take_picture_client = self.create_client(
|
||||||
TakePicture, '/drone/picture')
|
TakePicture, '/drone/picture')
|
||||||
while not self.take_picture_client.wait_for_service(timeout_sec=1.0):
|
self.wait_for_service(self.take_picture_client, "Take picture")
|
||||||
self.get_logger().info('Take picture service not available, waiting again...')
|
|
||||||
self.take_picture_request = TakePicture.Request()
|
self.take_picture_request = TakePicture.Request()
|
||||||
|
|
||||||
|
self.move_position_client = self.create_client(
|
||||||
|
MovePosition, '/drone/move_position')
|
||||||
|
self.wait_for_service(self.move_position_client, "Move position")
|
||||||
|
self.move_position_request = MovePosition.Request()
|
||||||
|
|
||||||
|
self.enable_failsafe_client = self.create_client(
|
||||||
|
EnableFailsafe, "/drone/enable_failsafe")
|
||||||
|
self.wait_for_service(self.enable_failsafe_client, "Enable failsafe")
|
||||||
|
self.enable_failsafe_request = EnableFailsafe.Request()
|
||||||
|
|
||||||
|
self.arm_drone_client = self.create_client(ArmDrone, "/drone/arm")
|
||||||
|
self.wait_for_service(self.arm_drone_client, "Arm drone")
|
||||||
|
self.arm_drone_request = ArmDrone.Request()
|
||||||
|
|
||||||
|
self.disarm_drone_client = self.create_client(
|
||||||
|
DisarmDrone, "/drone/disarm")
|
||||||
|
self.wait_for_service(self.disarm_drone_client, "Disarm drone")
|
||||||
|
self.disarm_drone_request = DisarmDrone.Request()
|
||||||
|
|
||||||
|
self.ready_drone_client = self.create_client(ReadyDrone, "/drone/ready")
|
||||||
|
self.wait_for_service(self.ready_drone_client, "Ready drone")
|
||||||
|
self.ready_drone_request = ReadyDrone.Request()
|
||||||
|
|
||||||
|
self.land_client = self.create_client(Land, "/drone/land")
|
||||||
|
self.wait_for_service(self.land_client, "Land drone")
|
||||||
|
self.land_request = Land.Request()
|
||||||
|
|
||||||
self.status_data = {}
|
self.status_data = {}
|
||||||
self.status_data_received = False
|
self.status_data_received = False
|
||||||
self.last_message = ""
|
self.last_message = ""
|
||||||
@@ -57,53 +114,121 @@ class ApiListener(Node):
|
|||||||
target=self.handle_responses, daemon=True)
|
target=self.handle_responses, daemon=True)
|
||||||
self.response_thread.start()
|
self.response_thread.start()
|
||||||
|
|
||||||
def drone_status_callback(self, msg):
|
self.event_loop = None
|
||||||
self.status_data_received = True
|
self.armed = False
|
||||||
self.status_data['battery_percentage'] = msg.battery_percentage
|
self.failsafe_enabled = False
|
||||||
self.status_data['cpu_usage'] = msg.cpu_usage
|
self.has_sent_failsafe_msg = False
|
||||||
self.status_data['armed'] = msg.armed
|
|
||||||
self.status_data['control_mode'] = msg.control_mode
|
|
||||||
self.status_data['route_setpoint'] = msg.route_setpoint
|
|
||||||
|
|
||||||
def publish_message(self, message):
|
def wait_for_service(self, client, service_name):
|
||||||
self.get_logger().info(f'Publishing message: {message}')
|
"""Waits for a client service to be available
|
||||||
asyncio.run(self.websocket.send(message))
|
|
||||||
|
Args:
|
||||||
|
client (ROS2 service client): The client to use to wait fo the service
|
||||||
|
service_name (str): The client name for logging
|
||||||
|
"""
|
||||||
|
waiting = 0
|
||||||
|
while not client.wait_for_service(timeout_sec=1.0):
|
||||||
|
if (waiting > 10):
|
||||||
|
self.get_logger().error(
|
||||||
|
service_name + ' service not available, exiting...')
|
||||||
|
exit(-1)
|
||||||
|
self.get_logger().info(service_name + 'service not available, waiting again...')
|
||||||
|
waiting = waiting + 1
|
||||||
|
|
||||||
|
def drone_status_callback(self, msg):
|
||||||
|
"""Callback for when a drone_status message is received
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg (DroneStatus): The received message
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.status_data_received = True
|
||||||
|
self.status_data['battery_percentage'] = msg.battery_percentage
|
||||||
|
if msg.battery_percentage < 15:
|
||||||
|
self.enable_failsafe(
|
||||||
|
"Battery level too low! Failsafe enabled to prevent damage to battery (" + str(msg.battery_percentage ) + "%)")
|
||||||
|
self.status_data['cpu_usage'] = msg.cpu_usage
|
||||||
|
self.status_data['armed'] = msg.armed
|
||||||
|
self.armed = msg.armed
|
||||||
|
self.status_data['control_mode'] = msg.control_mode
|
||||||
|
self.status_data['route_setpoint'] = msg.route_setpoint
|
||||||
|
self.status_data['velocity'] = [float(msg.velocity[0]), float(
|
||||||
|
msg.velocity[1]), float(msg.velocity[2])]
|
||||||
|
self.status_data['position'] = [float(msg.position[0]), float(
|
||||||
|
msg.position[1]), float(msg.position[2])]
|
||||||
|
self.status_data['failsafe'] = msg.failsafe
|
||||||
|
self.status_data['height'] = msg.height
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().error(
|
||||||
|
f'Error while parsing drone status message: {e}')
|
||||||
|
|
||||||
|
def failsafe_callback(self, msg):
|
||||||
|
"""Callback for when the failsafe gets enabled. Queues a FAILSAFE message to the client
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg (FailSAfe): The message that was received
|
||||||
|
"""
|
||||||
|
if self.failsafe_enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.has_sent_failsafe_msg:
|
||||||
|
self.has_sent_failsafe_msg = True
|
||||||
|
self.status_data['failsafe'] = msg.enabled
|
||||||
|
self.message_queue.append(json.dumps(
|
||||||
|
{'type': ResponseMessage.FAILSAFE.name, 'message': msg.msg}))
|
||||||
|
|
||||||
|
async def publish_message(self, message):
|
||||||
|
"""publishes a message to the NodeJS client
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message (JSON object): the message to send
|
||||||
|
"""
|
||||||
|
# self.get_logger().info(f'Publishing message: {message}')
|
||||||
|
if self.websocket is not None:
|
||||||
|
try:
|
||||||
|
await self.websocket.send(message)
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().error(
|
||||||
|
'Something went wrong while sending a message to the websocket: ' + str(e))
|
||||||
|
else:
|
||||||
|
self.get_logger().error('Trying to publish message but no websocket connection')
|
||||||
|
|
||||||
def publish_status(self):
|
def publish_status(self):
|
||||||
|
"""publishes the current status to the client by queueing a STATUS message
|
||||||
|
"""
|
||||||
if self.status_data_received:
|
if self.status_data_received:
|
||||||
|
self.status_data_received = False
|
||||||
if self.websocket is not None:
|
if self.websocket is not None:
|
||||||
self.message_queue.append(json.dumps(
|
try:
|
||||||
{'type': ResponseMessage.STATUS.name, 'data': self.status_data}))
|
self.message_queue.append(json.dumps(
|
||||||
|
{'type': ResponseMessage.STATUS.name, 'data': self.status_data}))
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().error(
|
||||||
|
'Something went wrong while publishing the status: ' + str(e))
|
||||||
|
|
||||||
def handle_responses(self):
|
def handle_responses(self):
|
||||||
|
"""Thread to handle responses to send to the client
|
||||||
|
"""
|
||||||
while True:
|
while True:
|
||||||
if len(self.message_queue) > 0:
|
if len(self.message_queue) > 0 and self.websocket is not None:
|
||||||
self.publish_message(self.message_queue.pop(0))
|
# self.get_logger().info("sending message")
|
||||||
|
asyncio.run(self.publish_message(self.message_queue.pop(0)))
|
||||||
|
|
||||||
def start_api_thread(self):
|
def start_api_thread(self):
|
||||||
|
"""Starts the API thread"""
|
||||||
asyncio.run(self.handle_api())
|
asyncio.run(self.handle_api())
|
||||||
|
|
||||||
async def handle_api(self):
|
async def handle_api(self):
|
||||||
|
"""Handles the API requests and starts the websockets server"""
|
||||||
self.get_logger().info('Starting API')
|
self.get_logger().info('Starting API')
|
||||||
|
self.event_loop = asyncio.get_event_loop()
|
||||||
self.server = await websockets.serve(self.api_handler, '0.0.0.0', 9001)
|
self.server = await websockets.serve(self.api_handler, '0.0.0.0', 9001)
|
||||||
self.get_logger().info('API started on port 9001')
|
self.get_logger().info('API started on port 9001')
|
||||||
await self.server.wait_closed()
|
await self.server.wait_closed()
|
||||||
|
|
||||||
def process_image_request(self, message_json):
|
|
||||||
self.get_logger().info('Take picture command received')
|
|
||||||
if message_json['filename']:
|
|
||||||
self.get_logger().info(
|
|
||||||
f'Filename: {message_json["filename"]}')
|
|
||||||
self.take_picture_request.input_name = message_json['filename']
|
|
||||||
self.future = self.cli.call_async(self.take_picture_request)
|
|
||||||
rclpy.spin_until_future_complete(self, self.future)
|
|
||||||
result_filename = self.future.result()
|
|
||||||
with open(result_filename, 'rb') as f:
|
|
||||||
image_data = f.read()
|
|
||||||
self.message_queue.append(json.dumps(
|
|
||||||
{'type': ResponseMessage.IMAGE.name, 'image': image_data}))
|
|
||||||
|
|
||||||
def send_available_commands(self):
|
def send_available_commands(self):
|
||||||
|
"""Sends the available commands to the client
|
||||||
|
"""
|
||||||
print('Sending available commands')
|
print('Sending available commands')
|
||||||
requestcommands = {}
|
requestcommands = {}
|
||||||
messagetypes = {}
|
messagetypes = {}
|
||||||
@@ -117,7 +242,151 @@ class ApiListener(Node):
|
|||||||
self.message_queue.append(json.dumps(
|
self.message_queue.append(json.dumps(
|
||||||
{'type': ResponseMessage.ALL_REQUESTS_RESPONSES.name, 'data': result}))
|
{'type': ResponseMessage.ALL_REQUESTS_RESPONSES.name, 'data': result}))
|
||||||
|
|
||||||
|
def handle_direction_message(self, message):
|
||||||
|
"""Calls the move position service with the given values
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message (JSON object): the message containing the direction values
|
||||||
|
"""
|
||||||
|
self.move_position_request.up_down = float(message['up_down'])
|
||||||
|
self.move_position_request.left_right = float(message['left_right'])
|
||||||
|
self.move_position_request.front_back = float(
|
||||||
|
message['forward_backward'])
|
||||||
|
self.move_position_request.angle = float(message['yaw'])
|
||||||
|
self.get_logger().info(
|
||||||
|
f'Calling move position service with request: {str(self.move_position_request)}')
|
||||||
|
|
||||||
|
self.send_move_position_request()
|
||||||
|
|
||||||
|
def send_move_position_request(self):
|
||||||
|
"""Sends the move position request to the move position service"""
|
||||||
|
future = self.move_position_client.call_async(
|
||||||
|
self.move_position_request)
|
||||||
|
future.add_done_callback(partial(self.move_position_service_callback))
|
||||||
|
|
||||||
|
def move_position_service_callback(self, future):
|
||||||
|
"""Callback for the move position service
|
||||||
|
|
||||||
|
Args:
|
||||||
|
future (Future): Future object that holds the result
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = future.result()
|
||||||
|
message_result = {}
|
||||||
|
message_result['type'] = ResponseMessage.MOVE_DIRECTION_RESULT.name
|
||||||
|
self.get_logger().info(
|
||||||
|
f'Move position service call result: {str(result)}')
|
||||||
|
if result.success == True:
|
||||||
|
self.get_logger().info('Move position service call success')
|
||||||
|
message_result['success'] = True
|
||||||
|
else:
|
||||||
|
self.get_logger().error('Move position service call failed')
|
||||||
|
message_result['success'] = False
|
||||||
|
self.message_queue.append(json.dumps(message_result))
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().error(
|
||||||
|
'Something went wrong while sending a move position request and waiting for the response: %r' % (e))
|
||||||
|
|
||||||
|
def enable_failsafe(self, message):
|
||||||
|
self.get_logger().info("Enabling failsafe")
|
||||||
|
self.enable_failsafe_request.message = message
|
||||||
|
future = self.enable_failsafe_client.call_async(
|
||||||
|
self.enable_failsafe_request)
|
||||||
|
future.add_done_callback(partial(self.enable_failsafe_callback))
|
||||||
|
|
||||||
|
def enable_failsafe_callback(self, future):
|
||||||
|
try:
|
||||||
|
result = future.result()
|
||||||
|
if (result.enabled == True):
|
||||||
|
self.get_logger().info("Failsafe activated")
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().error(
|
||||||
|
"Something went wrong while trying to enable failsafe!\n" + str(e))
|
||||||
|
|
||||||
|
def emergency_stop(self):
|
||||||
|
"""Sends an emergency stop request to the failsafe service"""
|
||||||
|
self.enable_failsafe("Emergency stop activated")
|
||||||
|
|
||||||
|
def takeoff(self):
|
||||||
|
"""Sends a takeoff request to the move position service"""
|
||||||
|
self.get_logger().info('Takeoff command received')
|
||||||
|
self.move_position_request.up_down = 0.1
|
||||||
|
self.move_position_request.left_right = 0.0
|
||||||
|
self.move_position_request.front_back = 0.0
|
||||||
|
self.move_position_request.angle = 0.0
|
||||||
|
self.send_move_position_request()
|
||||||
|
|
||||||
|
def land(self):
|
||||||
|
"""Sends a land request to the move position service"""
|
||||||
|
self.get_logger().info('Land command received')
|
||||||
|
self.move_position_request.up_down = -0.1
|
||||||
|
self.move_position_request.left_right = 0.0
|
||||||
|
self.move_position_request.front_back = 0.0
|
||||||
|
self.move_position_request.angle = 0.0
|
||||||
|
self.send_move_position_request()
|
||||||
|
future = self.land_drone_client.call_async(self.land_drone_request)
|
||||||
|
future.add_done_callback(partial(self.land_service_callback))
|
||||||
|
|
||||||
|
def arm_disarm(self):
|
||||||
|
"""Sends an arm or disarm request to the PX4Controller"""
|
||||||
|
if self.armed:
|
||||||
|
self.get_logger().info('Disarm command received')
|
||||||
|
future = self.disarm_drone_client.call_async(
|
||||||
|
self.disarm_drone_request)
|
||||||
|
future.add_done_callback(partial(self.disarm_service_callback))
|
||||||
|
else:
|
||||||
|
self.get_logger().info('Arm command received')
|
||||||
|
future = self.ready_drone_client.call_async(
|
||||||
|
self.ready_drone_request)
|
||||||
|
future.add_done_callback(partial(self.ready_drone_callback))
|
||||||
|
|
||||||
|
def ready_drone_callback(self, future):
|
||||||
|
try:
|
||||||
|
result = future.result()
|
||||||
|
if result.success:
|
||||||
|
self.get_logger().info('Ready service call success')
|
||||||
|
else:
|
||||||
|
self.get_logger().error('Ready service call failed')
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().error(
|
||||||
|
'Something went wrong while calling the ready service!\n' + str(e))
|
||||||
|
|
||||||
|
def arm_service_callback(self, future):
|
||||||
|
try:
|
||||||
|
result = future.result()
|
||||||
|
if result.success:
|
||||||
|
self.get_logger().info('Arm service call success')
|
||||||
|
else:
|
||||||
|
self.get_logger().error('Arm service call failed')
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().error(
|
||||||
|
'Something went wrong while calling the arm service!\n' + str(e))
|
||||||
|
|
||||||
|
def disarm_service_callback(self, future):
|
||||||
|
try:
|
||||||
|
result = future.result()
|
||||||
|
if result.success:
|
||||||
|
self.get_logger().info('Disarm service call success')
|
||||||
|
self.armed = False
|
||||||
|
else:
|
||||||
|
self.get_logger().error('Disarm service call failed')
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().error(
|
||||||
|
'Something went wrong while calling the disarm service!\n' + str(e))
|
||||||
|
|
||||||
|
def land_service_callback(self, future):
|
||||||
|
try:
|
||||||
|
result = future.result()
|
||||||
|
if result.is_landing:
|
||||||
|
self.get_logger().info('Land service call success')
|
||||||
|
else:
|
||||||
|
self.get_logger().error('Land service call failed')
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().error(
|
||||||
|
'Something went wrong while calling the land service!\n' + str(e))
|
||||||
|
|
||||||
def consume_message(self, message):
|
def consume_message(self, message):
|
||||||
|
"""Consumes a message from the client"""
|
||||||
self.get_logger().info(f'Consuming message: {message}')
|
self.get_logger().info(f'Consuming message: {message}')
|
||||||
try:
|
try:
|
||||||
message_json = json.loads(str(message))
|
message_json = json.loads(str(message))
|
||||||
@@ -129,37 +398,44 @@ class ApiListener(Node):
|
|||||||
else:
|
else:
|
||||||
self.get_logger().info(
|
self.get_logger().info(
|
||||||
f'Received command: {message_json["command"]}')
|
f'Received command: {message_json["command"]}')
|
||||||
if message_json['command'] == RequestCommand.TAKEOFF.value:
|
if message_json['command'] == RequestCommand.LAND.value:
|
||||||
self.get_logger().info('Takeoff command received')
|
|
||||||
elif message_json['command'] == RequestCommand.LAND.value:
|
|
||||||
self.get_logger().info('Land command received')
|
self.get_logger().info('Land command received')
|
||||||
elif message_json['command'] == RequestCommand.MOVE_POSITION.value:
|
self.land()
|
||||||
self.get_logger().info('Move position command received')
|
elif message_json['command'] == RequestCommand.ARM_DISARM.value:
|
||||||
|
self.get_logger().info('Arm/disarm command received')
|
||||||
|
self.arm_disarm()
|
||||||
elif message_json['command'] == RequestCommand.MOVE_DIRECTION.value:
|
elif message_json['command'] == RequestCommand.MOVE_DIRECTION.value:
|
||||||
self.get_logger().info('Move direction command received')
|
self.get_logger().info('Move direction command received')
|
||||||
elif message_json['command'] == RequestCommand.TAKE_PICTURE.value:
|
self.handle_direction_message(message_json)
|
||||||
self.process_image_request(message_json)
|
elif message_json['command'] == RequestCommand.GET_COMMANDS_TYPES.value:
|
||||||
elif message_json['command'] == RequestCommand.GET.value:
|
|
||||||
self.get_logger().info('Get command received')
|
self.get_logger().info('Get command received')
|
||||||
self.send_available_commands()
|
self.send_available_commands()
|
||||||
|
elif message_json['command'] == RequestCommand.EMERGENCY_STOP.value:
|
||||||
|
self.get_logger().info('Emergency stop command received')
|
||||||
|
self.emergency_stop()
|
||||||
else:
|
else:
|
||||||
self.get_logger().error('Received unknown command')
|
self.get_logger().error('Received unknown command ' +
|
||||||
|
str(message_json['command']))
|
||||||
self.send_available_commands()
|
self.send_available_commands()
|
||||||
except TypeError:
|
except TypeError:
|
||||||
self.get_logger().error('Received unknown command')
|
self.get_logger().error('Received unknown type: ' +
|
||||||
|
str(type(message)) + " " + str(message))
|
||||||
self.send_available_commands()
|
self.send_available_commands()
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
self.get_logger().error('Received invalid JSON')
|
self.get_logger().error('Received invalid JSON')
|
||||||
self.send_available_commands()
|
self.send_available_commands()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.get_logger().error('Something went wrong!')
|
self.get_logger().error('Something went wrong!')
|
||||||
self.get_logger().error(str(e))
|
self.get_logger().error(str(type(e)))
|
||||||
|
self.get_logger().error(str(e.with_traceback()))
|
||||||
|
|
||||||
async def api_handler(self, websocket):
|
async def api_handler(self, websocket):
|
||||||
|
"""Handles the websocket connection
|
||||||
|
|
||||||
|
Args:
|
||||||
|
websocket (websockets object): the websocket connection
|
||||||
|
"""
|
||||||
self.get_logger().info('New connection')
|
self.get_logger().info('New connection')
|
||||||
# if self.websocket is not None:
|
|
||||||
# self.get_logger().error('Got a new websocket connection but I am already connected!')
|
|
||||||
# return
|
|
||||||
|
|
||||||
self.websocket = websocket
|
self.websocket = websocket
|
||||||
try:
|
try:
|
||||||
@@ -177,11 +453,13 @@ class ApiListener(Node):
|
|||||||
|
|
||||||
|
|
||||||
def main(args=None):
|
def main(args=None):
|
||||||
|
"""Main function"""
|
||||||
rclpy.init(args=args)
|
rclpy.init(args=args)
|
||||||
|
|
||||||
api_listener = ApiListener()
|
api_listener = ApiListener()
|
||||||
|
multithreaded_executor = rclpy.executors.MultiThreadedExecutor()
|
||||||
rclpy.spin(api_listener)
|
multithreaded_executor.add_node(api_listener)
|
||||||
|
multithreaded_executor.spin()
|
||||||
|
|
||||||
api_listener.destroy_node()
|
api_listener.destroy_node()
|
||||||
rclpy.shutdown()
|
rclpy.shutdown()
|
||||||
|
|||||||
@@ -8,10 +8,6 @@
|
|||||||
<license>Apache License 2.0</license>
|
<license>Apache License 2.0</license>
|
||||||
<depend>rclpy</depend>
|
<depend>rclpy</depend>
|
||||||
<depend>drone_services</depend>
|
<depend>drone_services</depend>
|
||||||
|
|
||||||
<test_depend>ament_copyright</test_depend>
|
|
||||||
<test_depend>ament_flake8</test_depend>
|
|
||||||
<test_depend>ament_pep257</test_depend>
|
|
||||||
<test_depend>python3-pytest</test_depend>
|
<test_depend>python3-pytest</test_depend>
|
||||||
|
|
||||||
<export>
|
<export>
|
||||||
|
|||||||
110
src/api_communication/test/test_api_listener.py
Normal file
110
src/api_communication/test/test_api_listener.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
import time
|
||||||
|
|
||||||
|
import launch
|
||||||
|
import launch_ros
|
||||||
|
import launch_ros.actions
|
||||||
|
import launch_testing.actions
|
||||||
|
import pytest
|
||||||
|
import rclpy
|
||||||
|
|
||||||
|
from drone_services.msg import FailsafeMsg
|
||||||
|
from drone_services.msg import DroneStatus
|
||||||
|
|
||||||
|
@pytest.mark.rostest
|
||||||
|
def generate_test_description():
|
||||||
|
file_path = os.path.dirname(__file__)
|
||||||
|
api_listener_node = launch_ros.actions.Node(
|
||||||
|
executable=sys.executable,
|
||||||
|
arguments=[os.path.join(
|
||||||
|
file_path, '..', 'api_communication', 'api_listener.py')],
|
||||||
|
additional_env={'PYTHONUNBUFFERED': '1'}
|
||||||
|
)
|
||||||
|
failsafe_node = launch_ros.actions.Node(
|
||||||
|
package='failsafe', executable='failsafe')
|
||||||
|
camera_node = launch_ros.actions.Node(
|
||||||
|
package='camera', executable='camera_controller')
|
||||||
|
position_changer_node = launch_ros.actions.Node(
|
||||||
|
package='drone_controls', executable='position_changer')
|
||||||
|
px4_controller_node = launch_ros.actions.Node(
|
||||||
|
package='px4_connection', executable='px4_controller')
|
||||||
|
heartbeat_node = launch_ros.actions.Node(
|
||||||
|
package='px4_connection', executable='heartbeat')
|
||||||
|
|
||||||
|
return (
|
||||||
|
launch.LaunchDescription([
|
||||||
|
api_listener_node,
|
||||||
|
failsafe_node,
|
||||||
|
camera_node,
|
||||||
|
position_changer_node,
|
||||||
|
px4_controller_node,
|
||||||
|
heartbeat_node,
|
||||||
|
launch_testing.actions.ReadyToTest(),
|
||||||
|
]),
|
||||||
|
{
|
||||||
|
'api_listener_node': api_listener_node,
|
||||||
|
'failsafe_node': failsafe_node,
|
||||||
|
'camera_node': camera_node,
|
||||||
|
'position_changer_node': position_changer_node,
|
||||||
|
'px4_controller_node': px4_controller_node,
|
||||||
|
'heartbeat_node': heartbeat_node
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ApiListenerTest(unittest.TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
rclpy.init()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
rclpy.shutdown()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.node = rclpy.create_node('test_api_listener')
|
||||||
|
self.published_battery_status = False
|
||||||
|
self.received_failsafe_callback = False
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.node.destroy_node()
|
||||||
|
|
||||||
|
def failsafe_callback(self,msg):
|
||||||
|
self.assertTrue(msg.enabled, "Failsafe was not enabled!")
|
||||||
|
self.assertTrue("Battery level too low! Failsafe enabled to prevent damage to battery" in msg.msg, "Failsafe message was not correct!")
|
||||||
|
self.received_failsafe_callback = True
|
||||||
|
|
||||||
|
def status_callback(self,msg):
|
||||||
|
self.node.get_logger().info("Received status callback " + str(msg))
|
||||||
|
|
||||||
|
def test_api_listener_battery(self, api_listener_node, proc_output):
|
||||||
|
failsafe_subscriber = self.node.create_subscription(FailsafeMsg, '/drone/failsafe', self.failsafe_callback, 10)
|
||||||
|
drone_status_publisher = self.node.create_publisher(DroneStatus, '/drone/status', 10)
|
||||||
|
msg = DroneStatus()
|
||||||
|
msg.battery_percentage = 10.0
|
||||||
|
msg.armed = True
|
||||||
|
msg.height = 10.0
|
||||||
|
msg.control_mode = "attitude"
|
||||||
|
msg.cpu_usage = 10.0
|
||||||
|
msg.route_setpoint = 0
|
||||||
|
msg.position = [0.0,0.0,0.0]
|
||||||
|
msg.velocity = [0.0,0.0,0.0]
|
||||||
|
|
||||||
|
time.sleep(5) # wait for nodes to start
|
||||||
|
self.node.get_logger().info("Starting publishing")
|
||||||
|
end_time = time.time() + 10.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
while time.time() < end_time:
|
||||||
|
rclpy.spin_once(self.node, timeout_sec=0.1)
|
||||||
|
# self.node.get_logger().info("publishing message")
|
||||||
|
drone_status_publisher.publish(msg)
|
||||||
|
if self.received_failsafe_callback:
|
||||||
|
break
|
||||||
|
self.assertTrue(self.received_failsafe_callback, "Failsafe was not enabled!")
|
||||||
|
finally:
|
||||||
|
self.node.destroy_subscription(failsafe_subscriber)
|
||||||
|
self.node.destroy_publisher(drone_status_publisher)
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Copyright 2015 Open Source Robotics Foundation, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
from ament_copyright.main import main
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.copyright
|
|
||||||
@pytest.mark.linter
|
|
||||||
def test_copyright():
|
|
||||||
rc = main(argv=['.', 'test'])
|
|
||||||
assert rc == 0, 'Found errors'
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Copyright 2017 Open Source Robotics Foundation, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
from ament_flake8.main import main_with_errors
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.flake8
|
|
||||||
@pytest.mark.linter
|
|
||||||
def test_flake8():
|
|
||||||
rc, errors = main_with_errors(argv=[])
|
|
||||||
assert rc == 0, \
|
|
||||||
'Found %d code style errors / warnings:\n' % len(errors) + \
|
|
||||||
'\n'.join(errors)
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Copyright 2015 Open Source Robotics Foundation, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
from ament_pep257.main import main
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.linter
|
|
||||||
@pytest.mark.pep257
|
|
||||||
def test_pep257():
|
|
||||||
rc = main(argv=['.', 'test'])
|
|
||||||
assert rc == 0, 'Found code style errors / warnings'
|
|
||||||
@@ -5,17 +5,51 @@ from sensor_msgs.msg import Image
|
|||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import websockets.server
|
||||||
|
import threading
|
||||||
|
import cv2
|
||||||
|
import sys
|
||||||
|
import requests
|
||||||
|
|
||||||
#resolution of the camera
|
#resolution of the camera
|
||||||
RES_4K_H = 3496
|
RES_4K_H = 3496
|
||||||
RES_4K_W = 4656
|
RES_4K_W = 4656
|
||||||
|
|
||||||
|
video_url = "http://10.1.1.41:8080/video"
|
||||||
|
# Set the headers for the POST request
|
||||||
|
headers = {'Content-Type': 'application/octet-stream'}
|
||||||
|
|
||||||
|
#TODO change to serve video stream through websockets connection
|
||||||
|
|
||||||
class CameraController(Node):
|
class CameraController(Node):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__('camera_controller')
|
super().__init__('camera_controller')
|
||||||
|
|
||||||
self.get_logger().info("Camera controller started. Waiting for service call...")
|
self.get_logger().info("Camera controller started. Waiting for service call...")
|
||||||
self.srv = self.create_service(
|
self.srv = self.create_service(
|
||||||
TakePicture, 'drone/picture', self.take_picture_callback)
|
TakePicture, '/drone/picture', self.take_picture_callback)
|
||||||
|
|
||||||
|
self.websocket = None
|
||||||
|
self.server = None
|
||||||
|
self.event_loop = None
|
||||||
|
self.should_exit = False
|
||||||
|
|
||||||
|
self.timer = self.create_timer(1, self.timer_callback)
|
||||||
|
|
||||||
|
# self.websocket_thread = threading.Thread(target=self.start_listening)
|
||||||
|
# self.websocket_thread.start()
|
||||||
|
|
||||||
|
self.video_thread = threading.Thread(target=self.setup_websocket)
|
||||||
|
self.video_thread.start()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def timer_callback(self):
|
||||||
|
if self.should_exit:
|
||||||
|
self.get_logger().info("Shutting down...")
|
||||||
|
self.destroy_node()
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
def take_picture_callback(self, request, response):
|
def take_picture_callback(self, request, response):
|
||||||
if (request.input_name == "default"):
|
if (request.input_name == "default"):
|
||||||
@@ -25,11 +59,126 @@ class CameraController(Node):
|
|||||||
response.filename = imagename
|
response.filename = imagename
|
||||||
else:
|
else:
|
||||||
response.filename = request.input_name
|
response.filename = request.input_name
|
||||||
os.system('fswebcam -r ' + RES_4K_W + 'x' + RES_4K_H + ' ' + response.filename)
|
os.system('fswebcam -r ' + str(RES_4K_W) + 'x' + str(RES_4K_H) + ' ' + response.filename)
|
||||||
self.get_logger().info("Picture saved as " + response.filename)
|
self.get_logger().info("Picture saved as " + response.filename)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def setup_websocket(self):
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
connected = False
|
||||||
|
while not connected:
|
||||||
|
try:
|
||||||
|
start_server = websockets.serve(self.websocket_video, "0.0.0.0", 9002,loop=loop)
|
||||||
|
connected = True
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().error("error " + str(e))
|
||||||
|
connected = False
|
||||||
|
loop.run_until_complete(start_server)
|
||||||
|
loop.run_forever()
|
||||||
|
try:
|
||||||
|
self.destroy_node()
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().error("error " + str(e))
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
async def websocket_video(self,websocket,path):
|
||||||
|
vid = cv2.VideoCapture(0,cv2.CAP_V4L)
|
||||||
|
|
||||||
|
vid.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
|
||||||
|
vid.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
|
||||||
|
error_amount = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
while(vid.isOpened()):
|
||||||
|
img, frame = vid.read()
|
||||||
|
# self.get_logger().info("frame before: " + str(frame.shape))
|
||||||
|
#frame = cv2.resize(frame,(RES_4K_W,RES_4K_H))
|
||||||
|
#print("frame after: " + str(frame.shape))
|
||||||
|
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 100]
|
||||||
|
man = cv2.imencode('.jpg', frame)[1]
|
||||||
|
#sender(man)
|
||||||
|
await websocket.send(man.tobytes())
|
||||||
|
self.get_logger().error("Not opened")
|
||||||
|
error_amount += 1
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().error("error " + str(e))
|
||||||
|
error_amount += 1
|
||||||
|
if error_amount > 20:
|
||||||
|
self.get_logger().error("Too many errors, closing node")
|
||||||
|
self.should_exit = True
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_video_connection(self):
|
||||||
|
self.get_logger().info('Starting sending video')
|
||||||
|
vid = cv2.VideoCapture(0, cv2.CAP_DSHOW)
|
||||||
|
|
||||||
|
vid.set(cv2.CAP_PROP_FRAME_WIDTH, RES_4K_W)
|
||||||
|
vid.set(cv2.CAP_PROP_FRAME_HEIGHT, RES_4K_H)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
while vid.isOpened():
|
||||||
|
pass
|
||||||
|
ret, frame = vid.read()
|
||||||
|
|
||||||
|
if not ret:
|
||||||
|
# If reading the frame failed, break the loop
|
||||||
|
break
|
||||||
|
|
||||||
|
# Convert the frame to bytes
|
||||||
|
_, img_encoded = cv2.imencode('.jpg', frame)
|
||||||
|
frame_data = img_encoded.tobytes()
|
||||||
|
|
||||||
|
# Send the frame data as the request body
|
||||||
|
response = requests.post(video_url, data=frame_data, headers=headers)
|
||||||
|
|
||||||
|
# Check the response status
|
||||||
|
if response.status_code == 200:
|
||||||
|
print('Frame sent successfully.')
|
||||||
|
else:
|
||||||
|
print('Failed to send frame.')
|
||||||
|
# if self.websocket is not None:
|
||||||
|
# img,frame = vid.read()
|
||||||
|
# frame = cv2.resize(frame,(640,480))
|
||||||
|
# encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 65]
|
||||||
|
# man = cv2.imencode('.jpg', frame, encode_param)[1]
|
||||||
|
# self.get_logger().info('Sending video')
|
||||||
|
# asyncio.ensure_future(self.websocket.send(man.tobytes()),loop=self.event_loop)
|
||||||
|
# await asyncio.sleep(1)
|
||||||
|
# else:
|
||||||
|
# self.get_logger().info('No websocket connection')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().error('Something went wrong while reading and sending video: ' + str(e))
|
||||||
|
|
||||||
|
def start_listening(self):
|
||||||
|
self.get_logger().info('Starting listening for websocket connections')
|
||||||
|
asyncio.run(self.start_websocket_server())
|
||||||
|
|
||||||
|
async def start_websocket_server(self):
|
||||||
|
self.get_logger().info('Starting websocket server for video')
|
||||||
|
self.event_loop = asyncio.get_event_loop()
|
||||||
|
self.server = await websockets.server.serve(self.websocket_handler, '0.0.0.0', 9002)
|
||||||
|
await self.server.wait_closed()
|
||||||
|
|
||||||
|
async def websocket_handler(self,websocket):
|
||||||
|
self.get_logger().info('New connection')
|
||||||
|
|
||||||
|
self.websocket = websocket
|
||||||
|
try:
|
||||||
|
async for message in websocket:
|
||||||
|
self.get_logger().info(f"Received message: {message}")
|
||||||
|
|
||||||
|
except websockets.exceptions.ConnectionClosed:
|
||||||
|
self.get_logger().info('Connection closed')
|
||||||
|
self.websocket = None
|
||||||
|
except Exception as e:
|
||||||
|
self.get_logger().error('Something went wrong!')
|
||||||
|
self.get_logger().error(str(e))
|
||||||
|
self.websocket = None
|
||||||
|
|
||||||
|
|
||||||
def main(args=None):
|
def main(args=None):
|
||||||
rclpy.init(args=args)
|
rclpy.init(args=args)
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,6 @@
|
|||||||
<exec_depend>drone_services</exec_depend>
|
<exec_depend>drone_services</exec_depend>
|
||||||
<exec_depend>sensor_msgs</exec_depend>
|
<exec_depend>sensor_msgs</exec_depend>
|
||||||
|
|
||||||
<test_depend>ament_copyright</test_depend>
|
|
||||||
<test_depend>ament_flake8</test_depend>
|
|
||||||
<test_depend>ament_pep257</test_depend>
|
|
||||||
<test_depend>python3-pytest</test_depend>
|
<test_depend>python3-pytest</test_depend>
|
||||||
|
|
||||||
<export>
|
<export>
|
||||||
|
|||||||
72
src/camera/test/test_camera.py
Normal file
72
src/camera/test/test_camera.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
import time
|
||||||
|
|
||||||
|
import launch
|
||||||
|
import launch_ros
|
||||||
|
import launch_ros.actions
|
||||||
|
import launch_testing.actions
|
||||||
|
import pytest
|
||||||
|
import rclpy
|
||||||
|
|
||||||
|
from drone_services.srv import TakePicture
|
||||||
|
|
||||||
|
@pytest.mark.rostest
|
||||||
|
def generate_test_description():
|
||||||
|
file_path = os.path.dirname(__file__)
|
||||||
|
camera_node = launch_ros.actions.Node(
|
||||||
|
executable=sys.executable,
|
||||||
|
arguments=[os.path.join(file_path, '..', 'camera', 'camera_controller.py')],
|
||||||
|
additional_env={'PYTHONUNBUFFERED': '1'}
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
launch.LaunchDescription([
|
||||||
|
camera_node,
|
||||||
|
launch_testing.actions.ReadyToTest(),
|
||||||
|
]),
|
||||||
|
{
|
||||||
|
'camera_node': camera_node,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
class CameraUnitTest(unittest.TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
rclpy.init()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
rclpy.shutdown()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.node = rclpy.create_node('test_camera')
|
||||||
|
self.service_called = False
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.node.destroy_node()
|
||||||
|
|
||||||
|
def service_call_callback(self,future):
|
||||||
|
self.assertIsNotNone(future.result())
|
||||||
|
self.assertEqual(future.result().filename, "/home/ubuntu/test_image.jpg")
|
||||||
|
self.assertTrue(os.path.exists("/home/ubuntu/test_image.jpg"))
|
||||||
|
self.service_called = True
|
||||||
|
|
||||||
|
def test_camera_save_image(self,camera_node,proc_output):
|
||||||
|
# call camera service
|
||||||
|
camera_client = self.node.create_client(TakePicture, '/drone/picture')
|
||||||
|
while not camera_client.wait_for_service(timeout_sec=1.0):
|
||||||
|
self.node.get_logger().info('camera service not available, waiting again...')
|
||||||
|
request = TakePicture.Request()
|
||||||
|
request.input_name = "/home/ubuntu/test_image.jpg"
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
rclpy.spin_once(self.node,timeout_sec=0.1)
|
||||||
|
if not self.service_called:
|
||||||
|
camera_client.call_async(request).add_done_callback(self.service_call_callback)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
self.node.destroy_client(camera_client)
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Copyright 2015 Open Source Robotics Foundation, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
from ament_copyright.main import main
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.copyright
|
|
||||||
@pytest.mark.linter
|
|
||||||
def test_copyright():
|
|
||||||
rc = main(argv=['.', 'test'])
|
|
||||||
assert rc == 0, 'Found errors'
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Copyright 2017 Open Source Robotics Foundation, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
from ament_flake8.main import main_with_errors
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.flake8
|
|
||||||
@pytest.mark.linter
|
|
||||||
def test_flake8():
|
|
||||||
rc, errors = main_with_errors(argv=[])
|
|
||||||
assert rc == 0, \
|
|
||||||
'Found %d code style errors / warnings:\n' % len(errors) + \
|
|
||||||
'\n'.join(errors)
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Copyright 2015 Open Source Robotics Foundation, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
from ament_pep257.main import main
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.linter
|
|
||||||
@pytest.mark.pep257
|
|
||||||
def test_pep257():
|
|
||||||
rc = main(argv=['.', 'test'])
|
|
||||||
assert rc == 0, 'Found code style errors / warnings'
|
|
||||||
@@ -2,11 +2,12 @@ cmake_minimum_required(VERSION 3.8)
|
|||||||
project(drone_controls)
|
project(drone_controls)
|
||||||
|
|
||||||
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||||
add_compile_options(-Wall -Wextra -Wpedantic)
|
add_compile_options(-Wall -Wextra -Wpedantic)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# find dependencies
|
# find dependencies
|
||||||
find_package(ament_cmake REQUIRED)
|
find_package(ament_cmake REQUIRED)
|
||||||
|
|
||||||
# uncomment the following section in order to fill in
|
# uncomment the following section in order to fill in
|
||||||
# further dependencies manually.
|
# further dependencies manually.
|
||||||
# find_package(<dependency> REQUIRED)
|
# find_package(<dependency> REQUIRED)
|
||||||
@@ -25,15 +26,18 @@ install(TARGETS position_changer
|
|||||||
DESTINATION lib/${PROJECT_NAME})
|
DESTINATION lib/${PROJECT_NAME})
|
||||||
|
|
||||||
if(BUILD_TESTING)
|
if(BUILD_TESTING)
|
||||||
find_package(ament_lint_auto REQUIRED)
|
# find_package(ament_lint_auto REQUIRED)
|
||||||
# the following line skips the linter which checks for copyrights
|
|
||||||
# comment the line when a copyright and license is added to all source files
|
# the following line skips the linter which checks for copyrights
|
||||||
set(ament_cmake_copyright_FOUND TRUE)
|
# comment the line when a copyright and license is added to all source files
|
||||||
# the following line skips cpplint (only works in a git repo)
|
# set(ament_cmake_copyright_FOUND TRUE)
|
||||||
# comment the line when this package is in a git repo and when
|
# the following line skips cpplint (only works in a git repo)
|
||||||
# a copyright and license is added to all source files
|
# comment the line when this package is in a git repo and when
|
||||||
set(ament_cmake_cpplint_FOUND TRUE)
|
# a copyright and license is added to all source files
|
||||||
ament_lint_auto_find_test_dependencies()
|
# set(ament_cmake_cpplint_FOUND TRUE)
|
||||||
|
# ament_lint_auto_find_test_dependencies()
|
||||||
|
find_package(launch_testing_ament_cmake REQUIRED)
|
||||||
|
add_launch_test(test/test_positionchanger.py)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
ament_package()
|
ament_package()
|
||||||
|
|||||||
@@ -5,7 +5,11 @@
|
|||||||
#include <drone_services/msg/height_data.hpp>
|
#include <drone_services/msg/height_data.hpp>
|
||||||
#include <drone_services/srv/set_vehicle_control.hpp>
|
#include <drone_services/srv/set_vehicle_control.hpp>
|
||||||
#include <drone_services/srv/set_trajectory.hpp>
|
#include <drone_services/srv/set_trajectory.hpp>
|
||||||
|
#include <drone_services/srv/set_attitude.hpp>
|
||||||
#include <drone_services/srv/move_position.hpp>
|
#include <drone_services/srv/move_position.hpp>
|
||||||
|
#include <drone_services/srv/ready_drone.hpp>
|
||||||
|
#include <drone_services/srv/arm_drone.hpp>
|
||||||
|
#include <drone_services/srv/land.hpp>
|
||||||
|
|
||||||
#include <drone_services/srv/enable_failsafe.hpp>
|
#include <drone_services/srv/enable_failsafe.hpp>
|
||||||
#include <drone_services/msg/failsafe_msg.hpp>
|
#include <drone_services/msg/failsafe_msg.hpp>
|
||||||
@@ -25,10 +29,13 @@
|
|||||||
|
|
||||||
#define MIN_DISTANCE 1.0 // in meters
|
#define MIN_DISTANCE 1.0 // in meters
|
||||||
|
|
||||||
|
#define CONTROL_MODE_ATTITUDE 4 // attitude control mode bitmask
|
||||||
#define DEFAULT_CONTROL_MODE 16 // default velocity control bitmask
|
#define DEFAULT_CONTROL_MODE 16 // default velocity control bitmask
|
||||||
// converts bitmask control mode to control mode used by PX4Controller
|
// converts bitmask control mode to control mode used by PX4Controller
|
||||||
#define PX4_CONTROLLER_CONTROL_MODE(x) ((x) == 4 ? 1 : ((x) == 16 ? 2 : ((x) == 32 ? 3 : -1)))
|
#define PX4_CONTROLLER_CONTROL_MODE(x) ((x) == 4 ? 1 : ((x) == 16 ? 2 : ((x) == 32 ? 3 : -1)))
|
||||||
|
|
||||||
|
#define HEIGHT_DELTA 0.1
|
||||||
|
|
||||||
using namespace std::chrono_literals;
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
struct Quaternion
|
struct Quaternion
|
||||||
@@ -45,26 +52,37 @@ public:
|
|||||||
auto qos = rclcpp::QoS(rclcpp::QoSInitialization(qos_profile.history, 5), qos_profile);
|
auto qos = rclcpp::QoS(rclcpp::QoSInitialization(qos_profile.history, 5), qos_profile);
|
||||||
|
|
||||||
this->lidar_subscription = this->create_subscription<drone_services::msg::LidarReading>("/drone/object_detection", qos, std::bind(&PositionChanger::handle_lidar_message, this, std::placeholders::_1));
|
this->lidar_subscription = this->create_subscription<drone_services::msg::LidarReading>("/drone/object_detection", qos, std::bind(&PositionChanger::handle_lidar_message, this, std::placeholders::_1));
|
||||||
|
this->height_subscription = this->create_subscription<drone_services::msg::HeightData>("/drone/height", qos, std::bind(&PositionChanger::handle_height_message, this, std::placeholders::_1));
|
||||||
// vehicle_local_position_subscription_ = this->create_subscription<px4_msgs::msg::VehicleLocalPosition>("/fmu/out/vehicle_local_position", qos, std::bind(&PX4Controller::on_local_position_receive, this, std::placeholders::_1));
|
// vehicle_local_position_subscription_ = this->create_subscription<px4_msgs::msg::VehicleLocalPosition>("/fmu/out/vehicle_local_position", qos, std::bind(&PX4Controller::on_local_position_receive, this, std::placeholders::_1));
|
||||||
this->odometry_subscription = this->create_subscription<px4_msgs::msg::VehicleOdometry>("/fmu/out/vehicle_odometry", qos, std::bind(&PositionChanger::handle_odometry_message, this, std::placeholders::_1));
|
this->odometry_subscription = this->create_subscription<px4_msgs::msg::VehicleOdometry>("/fmu/out/vehicle_odometry", qos, std::bind(&PositionChanger::handle_odometry_message, this, std::placeholders::_1));
|
||||||
this->move_position_service = this->create_service<drone_services::srv::MovePosition>("/drone/move_position", std::bind(&PositionChanger::handle_move_position_request, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
|
this->move_position_service = this->create_service<drone_services::srv::MovePosition>("/drone/move_position", std::bind(&PositionChanger::handle_move_position_request, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
|
||||||
|
this->ready_drone_service = this->create_service<drone_services::srv::ReadyDrone>("/drone/ready", std::bind(&PositionChanger::handle_ready_drone_request, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
|
||||||
|
this->land_service = this->create_service<drone_services::srv::Land>("/drone/land", std::bind(&PositionChanger::handle_land_request, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
|
||||||
|
|
||||||
this->failsafe_publisher = this->create_publisher<drone_services::msg::FailsafeMsg>("/drone/failsafe", 10);
|
this->failsafe_publisher = this->create_publisher<drone_services::msg::FailsafeMsg>("/drone/failsafe", 10);
|
||||||
|
|
||||||
this->trajectory_client = this->create_client<drone_services::srv::SetTrajectory>("/drone/set_trajectory");
|
this->trajectory_client = this->create_client<drone_services::srv::SetTrajectory>("/drone/set_trajectory");
|
||||||
|
this->attitude_client = this->create_client<drone_services::srv::SetAttitude>("/drone/set_attitude");
|
||||||
this->vehicle_control_client = this->create_client<drone_services::srv::SetVehicleControl>("/drone/set_vehicle_control");
|
this->vehicle_control_client = this->create_client<drone_services::srv::SetVehicleControl>("/drone/set_vehicle_control");
|
||||||
this->failsafe_client = this->create_client<drone_services::srv::EnableFailsafe>("/drone/enable_failsafe");
|
this->failsafe_client = this->create_client<drone_services::srv::EnableFailsafe>("/drone/enable_failsafe");
|
||||||
|
this->arm_drone_client = this->create_client<drone_services::srv::ArmDrone>("/drone/arm");
|
||||||
|
|
||||||
RCLCPP_INFO(this->get_logger(), "waiting for trajectory service...");
|
RCLCPP_INFO(this->get_logger(), "waiting for trajectory service...");
|
||||||
wait_for_service<rclcpp::Client<drone_services::srv::SetTrajectory>::SharedPtr>(this->trajectory_client);
|
wait_for_service<rclcpp::Client<drone_services::srv::SetTrajectory>::SharedPtr>(this->trajectory_client, "/drone/set_trajectory");
|
||||||
|
RCLCPP_INFO(this->get_logger(), "waiting for attitude service...");
|
||||||
|
wait_for_service<rclcpp::Client<drone_services::srv::SetAttitude>::SharedPtr>(this->attitude_client, "/drone/set_attitude");
|
||||||
RCLCPP_INFO(this->get_logger(), "waiting for vehicle control service...");
|
RCLCPP_INFO(this->get_logger(), "waiting for vehicle control service...");
|
||||||
wait_for_service<rclcpp::Client<drone_services::srv::SetVehicleControl>::SharedPtr>(this->vehicle_control_client);
|
wait_for_service<rclcpp::Client<drone_services::srv::SetVehicleControl>::SharedPtr>(this->vehicle_control_client, "/drone/set_vehicle_control");
|
||||||
RCLCPP_INFO(this->get_logger(), "waiting for failsafe service...");
|
RCLCPP_INFO(this->get_logger(), "waiting for failsafe service...");
|
||||||
wait_for_service<rclcpp::Client<drone_services::srv::EnableFailsafe>::SharedPtr>(this->failsafe_client);
|
wait_for_service<rclcpp::Client<drone_services::srv::EnableFailsafe>::SharedPtr>(this->failsafe_client, "/drone/enable_failsafe");
|
||||||
|
RCLCPP_INFO(this->get_logger(), "waiting for arm service...");
|
||||||
|
wait_for_service<rclcpp::Client<drone_services::srv::ArmDrone>::SharedPtr>(this->arm_drone_client, "/drone/arm");
|
||||||
|
|
||||||
this->trajectory_request = std::make_shared<drone_services::srv::SetTrajectory::Request>();
|
this->trajectory_request = std::make_shared<drone_services::srv::SetTrajectory::Request>();
|
||||||
|
this->attitude_request = std::make_shared<drone_services::srv::SetAttitude::Request>();
|
||||||
this->vehicle_control_request = std::make_shared<drone_services::srv::SetVehicleControl::Request>();
|
this->vehicle_control_request = std::make_shared<drone_services::srv::SetVehicleControl::Request>();
|
||||||
this->failsafe_request = std::make_shared<drone_services::srv::EnableFailsafe::Request>();
|
this->failsafe_request = std::make_shared<drone_services::srv::EnableFailsafe::Request>();
|
||||||
|
this->arm_drone_request = std::make_shared<drone_services::srv::ArmDrone::Request>();
|
||||||
|
|
||||||
lidar_health_timer = this->create_wall_timer(1s, std::bind(&PositionChanger::check_lidar_health, this));
|
lidar_health_timer = this->create_wall_timer(1s, std::bind(&PositionChanger::check_lidar_health, this));
|
||||||
|
|
||||||
@@ -84,6 +102,47 @@ public:
|
|||||||
if (status == std::future_status::ready)
|
if (status == std::future_status::ready)
|
||||||
{
|
{
|
||||||
RCLCPP_INFO(this->get_logger(), "Vehicle control mode set result: %d", future.get()->success);
|
RCLCPP_INFO(this->get_logger(), "Vehicle control mode set result: %d", future.get()->success);
|
||||||
|
if (this->got_ready_drone_request && future.get()->success)
|
||||||
|
{
|
||||||
|
auto arm_response = this->arm_drone_client->async_send_request(this->arm_drone_request, std::bind(&PositionChanger::arm_drone_service_callback, this, std::placeholders::_1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Service In-Progress...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void arm_drone_service_callback(rclcpp::Client<drone_services::srv::ArmDrone>::SharedFuture future)
|
||||||
|
{
|
||||||
|
auto status = future.wait_for(1s);
|
||||||
|
if (status == std::future_status::ready)
|
||||||
|
{
|
||||||
|
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Arm result: %d", future.get()->success);
|
||||||
|
if (this->got_ready_drone_request)
|
||||||
|
{
|
||||||
|
this->got_ready_drone_request = false;
|
||||||
|
|
||||||
|
this->attitude_request->pitch = 0.0;
|
||||||
|
this->attitude_request->yaw = 0.0;
|
||||||
|
this->attitude_request->roll = 0.0;
|
||||||
|
this->attitude_request->thrust = 0.15;
|
||||||
|
auto attitude_response = this->attitude_client->async_send_request(this->attitude_request, std::bind(&PositionChanger::attitude_message_callback, this, std::placeholders::_1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Service In-Progress...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void attitude_message_callback(rclcpp::Client<drone_services::srv::SetAttitude>::SharedFuture future)
|
||||||
|
{
|
||||||
|
auto status = future.wait_for(1s);
|
||||||
|
if (status == std::future_status::ready)
|
||||||
|
{
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Attitude set result: %d", future.get()->success);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -119,7 +178,7 @@ public:
|
|||||||
auto status = future.wait_for(1s);
|
auto status = future.wait_for(1s);
|
||||||
if (status == std::future_status::ready)
|
if (status == std::future_status::ready)
|
||||||
{
|
{
|
||||||
RCLCPP_INFO(this->get_logger(), "Set failsafe enabled: %d, message: ", future.get()->enabled, future.get()->message);
|
RCLCPP_INFO(this->get_logger(), "Set failsafe enabled: %d, message: %s", future.get()->enabled, future.get()->message);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -133,7 +192,6 @@ public:
|
|||||||
*/
|
*/
|
||||||
void send_trajectory_message()
|
void send_trajectory_message()
|
||||||
{
|
{
|
||||||
check_move_direction_allowed();
|
|
||||||
this->trajectory_request->values[0] = this->current_speed_x;
|
this->trajectory_request->values[0] = this->current_speed_x;
|
||||||
this->trajectory_request->values[1] = this->current_speed_y;
|
this->trajectory_request->values[1] = this->current_speed_y;
|
||||||
this->trajectory_request->values[2] = this->current_speed_z;
|
this->trajectory_request->values[2] = this->current_speed_z;
|
||||||
@@ -143,7 +201,12 @@ public:
|
|||||||
auto trajectory_response = this->trajectory_client->async_send_request(this->trajectory_request, std::bind(&PositionChanger::trajectory_message_callback, this, std::placeholders::_1));
|
auto trajectory_response = this->trajectory_client->async_send_request(this->trajectory_request, std::bind(&PositionChanger::trajectory_message_callback, this, std::placeholders::_1));
|
||||||
}
|
}
|
||||||
|
|
||||||
void enable_failsafe(std::string message)
|
/**
|
||||||
|
* @brief Enables the failsafe with the specified message
|
||||||
|
*
|
||||||
|
* @param message the message indicating the cause of the failsafe
|
||||||
|
*/
|
||||||
|
void enable_failsafe(std::u16string message)
|
||||||
{
|
{
|
||||||
this->failsafe_enabled = true;
|
this->failsafe_enabled = true;
|
||||||
this->failsafe_request->message = message;
|
this->failsafe_request->message = message;
|
||||||
@@ -157,34 +220,39 @@ public:
|
|||||||
*/
|
*/
|
||||||
void apply_collision_weights()
|
void apply_collision_weights()
|
||||||
{
|
{
|
||||||
if (this->current_speed_x > 0) // if moving forward
|
|
||||||
|
// if (this->current_speed_x > 0) // if moving forward
|
||||||
|
// {
|
||||||
|
if (!this->move_direction_allowed[MOVE_DIRECTION_FRONT])
|
||||||
{
|
{
|
||||||
if (!this->move_direction_allowed[MOVE_DIRECTION_FRONT])
|
RCLCPP_INFO(this->get_logger(), "Collision prevention front: %f", collision_prevention_weights[MOVE_DIRECTION_FRONT]);
|
||||||
{
|
get_x_y_with_rotation(collision_prevention_weights[MOVE_DIRECTION_FRONT], 0, this->current_yaw, &this->current_speed_x, &this->current_speed_y);
|
||||||
this->current_speed_x = collision_prevention_weights[MOVE_DIRECTION_FRONT];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else // moving backward
|
// }
|
||||||
|
// else // moving backward
|
||||||
|
// {
|
||||||
|
if (!this->move_direction_allowed[MOVE_DIRECTION_BACK])
|
||||||
{
|
{
|
||||||
if (!this->move_direction_allowed[MOVE_DIRECTION_BACK])
|
RCLCPP_INFO(this->get_logger(), "Collision prevention back: %f", collision_prevention_weights[MOVE_DIRECTION_BACK]);
|
||||||
{
|
get_x_y_with_rotation(collision_prevention_weights[MOVE_DIRECTION_BACK], 0, this->current_yaw, &this->current_speed_x, &this->current_speed_y);
|
||||||
this->current_speed_x = collision_prevention_weights[MOVE_DIRECTION_BACK];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (this->current_speed_y > 0) // moving right
|
// }
|
||||||
|
// if (this->current_speed_y > 0) // moving right
|
||||||
|
// {
|
||||||
|
if (!this->move_direction_allowed[MOVE_DIRECTION_RIGHT])
|
||||||
{
|
{
|
||||||
if (!this->move_direction_allowed[MOVE_DIRECTION_RIGHT])
|
RCLCPP_INFO(this->get_logger(), "Collision prevention right: %f", collision_prevention_weights[MOVE_DIRECTION_RIGHT]);
|
||||||
{
|
get_x_y_with_rotation(0, collision_prevention_weights[MOVE_DIRECTION_RIGHT], this->current_yaw, &this->current_speed_x, &this->current_speed_y);
|
||||||
this->current_speed_y = collision_prevention_weights[MOVE_DIRECTION_RIGHT];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else // moving left
|
// }
|
||||||
|
// else // moving left
|
||||||
|
// {
|
||||||
|
if (!this->move_direction_allowed[MOVE_DIRECTION_LEFT])
|
||||||
{
|
{
|
||||||
if (!this->move_direction_allowed[MOVE_DIRECTION_LEFT])
|
RCLCPP_INFO(this->get_logger(), "Collision prevention left: %f", collision_prevention_weights[MOVE_DIRECTION_LEFT]);
|
||||||
{
|
get_x_y_with_rotation(0, collision_prevention_weights[MOVE_DIRECTION_LEFT], this->current_yaw, &this->current_speed_x, &this->current_speed_y);
|
||||||
this->current_speed_y = collision_prevention_weights[MOVE_DIRECTION_LEFT];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -207,10 +275,65 @@ public:
|
|||||||
: (MIN_DISTANCE - std::min(this->lidar_sensor_values[LIDAR_SENSOR_RL], this->lidar_sensor_values[LIDAR_SENSOR_RR]));
|
: (MIN_DISTANCE - std::min(this->lidar_sensor_values[LIDAR_SENSOR_RL], this->lidar_sensor_values[LIDAR_SENSOR_RR]));
|
||||||
collision_prevention_weights[MOVE_DIRECTION_RIGHT] = this->move_direction_allowed[MOVE_DIRECTION_RIGHT] ? 0
|
collision_prevention_weights[MOVE_DIRECTION_RIGHT] = this->move_direction_allowed[MOVE_DIRECTION_RIGHT] ? 0
|
||||||
: -(MIN_DISTANCE - std::min(this->lidar_sensor_values[LIDAR_SENSOR_RR], this->lidar_sensor_values[LIDAR_SENSOR_FR]));
|
: -(MIN_DISTANCE - std::min(this->lidar_sensor_values[LIDAR_SENSOR_RR], this->lidar_sensor_values[LIDAR_SENSOR_FR]));
|
||||||
|
|
||||||
apply_collision_weights();
|
apply_collision_weights();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void attitude_land_callback(rclcpp::Client<drone_services::srv::SetAttitude>::SharedFuture future)
|
||||||
|
{
|
||||||
|
auto status = future.wait_for(1s);
|
||||||
|
if (status == std::future_status::ready)
|
||||||
|
{
|
||||||
|
if (future.get()->success)
|
||||||
|
{
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Attitude set to 0 for landing, landing done");
|
||||||
|
this->has_landed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Service In-Progress...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void vehicle_control_land_request_callback(rclcpp::Client<drone_services::srv::SetVehicleControl>::SharedFuture future)
|
||||||
|
{
|
||||||
|
auto status = future.wait_for(1s);
|
||||||
|
if (status == std::future_status::ready)
|
||||||
|
{
|
||||||
|
if (future.get()->success)
|
||||||
|
{
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Vehicle Control mode set to attitude for landing");
|
||||||
|
this->attitude_request->pitch = 0;
|
||||||
|
this->attitude_request->roll = 0;
|
||||||
|
this->attitude_request->yaw = 0;
|
||||||
|
this->attitude_request->thrust = 0;
|
||||||
|
auto attitude_response = this->attitude_client->async_send_request(this->attitude_request, std::bind(&PositionChanger::attitude_land_callback, this, std::placeholders::_1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Service In-Progress...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handle_height_message(const drone_services::msg::HeightData::SharedPtr message)
|
||||||
|
{
|
||||||
|
this->current_height = message->min_height;
|
||||||
|
if (!this->got_ready_drone_request)
|
||||||
|
{
|
||||||
|
this->start_height = message->min_height;
|
||||||
|
}
|
||||||
|
if (this->is_landing)
|
||||||
|
{
|
||||||
|
if (this->current_height <= this->start_height + HEIGHT_DELTA)
|
||||||
|
{
|
||||||
|
this->vehicle_control_request->control = 4;
|
||||||
|
auto control_mode_response = this->vehicle_control_client->async_send_request(this->vehicle_control_request,
|
||||||
|
std::bind(&PositionChanger::vehicle_control_land_request_callback, this, std::placeholders::_1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Callback function for receiving new LIDAR data
|
* @brief Callback function for receiving new LIDAR data
|
||||||
*
|
*
|
||||||
@@ -218,6 +341,7 @@ public:
|
|||||||
*/
|
*/
|
||||||
void handle_lidar_message(const drone_services::msg::LidarReading::SharedPtr message)
|
void handle_lidar_message(const drone_services::msg::LidarReading::SharedPtr message)
|
||||||
{
|
{
|
||||||
|
this->has_received_first_lidar_message = true;
|
||||||
|
|
||||||
this->received_lidar_message = true;
|
this->received_lidar_message = true;
|
||||||
if (message->sensor_1 > 0)
|
if (message->sensor_1 > 0)
|
||||||
@@ -247,14 +371,17 @@ public:
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Checks if the LIDAR is still sending messages. If not, enable failsafe
|
* @brief Checks if the LIDAR is still sending messages. If not, enable failsafe
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
void check_lidar_health()
|
void check_lidar_health()
|
||||||
{
|
{
|
||||||
if (!this->received_lidar_message)
|
if (this->has_received_first_lidar_message)
|
||||||
{
|
{
|
||||||
RCLCPP_WARN(this->get_logger(), "Lidar not sending messages, enabling failsafe");
|
if (!this->received_lidar_message)
|
||||||
enable_failsafe("No healthy connection to LIDAR!");
|
{
|
||||||
|
RCLCPP_WARN(this->get_logger(), "Lidar not sending messages, enabling failsafe");
|
||||||
|
enable_failsafe(u"No healthy connection to LIDAR! Check the LIDAR USB cable and restart the drone.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this->received_lidar_message = false;
|
this->received_lidar_message = false;
|
||||||
}
|
}
|
||||||
@@ -286,6 +413,11 @@ public:
|
|||||||
{
|
{
|
||||||
if (!this->failsafe_enabled)
|
if (!this->failsafe_enabled)
|
||||||
{
|
{
|
||||||
|
if (!this->has_received_first_lidar_message)
|
||||||
|
{
|
||||||
|
this->enable_failsafe(u"Waiting for LIDAR timed out! Check the LIDAR USB connection and consider restarting the drone.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
RCLCPP_INFO(this->get_logger(), "Incoming request\nfront_back: %f\nleft_right: %f\nup_down: %f\nangle: %f", request->front_back, request->left_right, request->up_down, request->angle);
|
RCLCPP_INFO(this->get_logger(), "Incoming request\nfront_back: %f\nleft_right: %f\nup_down: %f\nangle: %f", request->front_back, request->left_right, request->up_down, request->angle);
|
||||||
if (request->angle > 360 || request->angle < -360)
|
if (request->angle > 360 || request->angle < -360)
|
||||||
{
|
{
|
||||||
@@ -293,17 +425,59 @@ public:
|
|||||||
response->success = false;
|
response->success = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this->vehicle_control_request->control != DEFAULT_CONTROL_MODE)
|
||||||
|
{
|
||||||
|
this->vehicle_control_request->control = DEFAULT_CONTROL_MODE;
|
||||||
|
auto control_mode_response = this->vehicle_control_client->async_send_request(this->vehicle_control_request,
|
||||||
|
std::bind(&PositionChanger::vehicle_control_service_callback, this, std::placeholders::_1));
|
||||||
|
}
|
||||||
this->current_yaw += request->angle * (M_PI / 180.0); // get the angle in radians
|
this->current_yaw += request->angle * (M_PI / 180.0); // get the angle in radians
|
||||||
this->current_speed_z = request->up_down;
|
this->current_speed_z = request->up_down;
|
||||||
get_x_y_with_rotation(request->front_back, request->left_right, this->current_yaw, &this->current_speed_x, &this->current_speed_y);
|
get_x_y_with_rotation(request->front_back, request->left_right, this->current_yaw, &this->current_speed_x, &this->current_speed_y);
|
||||||
|
check_move_direction_allowed();
|
||||||
RCLCPP_INFO(this->get_logger(), "Calculated speed x: %f, y: %f", this->current_speed_x, this->current_speed_y);
|
RCLCPP_INFO(this->get_logger(), "Calculated speed x: %f, y: %f", this->current_speed_x, this->current_speed_y);
|
||||||
send_trajectory_message();
|
send_trajectory_message();
|
||||||
response->success = true;
|
response->success = true;
|
||||||
} else {
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
response->success = false;
|
response->success = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void handle_land_request(
|
||||||
|
const std::shared_ptr<rmw_request_id_t> request_header,
|
||||||
|
const std::shared_ptr<drone_services::srv::Land::Request> request,
|
||||||
|
const std::shared_ptr<drone_services::srv::Land::Response> response)
|
||||||
|
{
|
||||||
|
this->is_landing = true;
|
||||||
|
response->is_landing = this->is_landing;
|
||||||
|
}
|
||||||
|
|
||||||
|
void handle_ready_drone_request(
|
||||||
|
const std::shared_ptr<rmw_request_id_t> request_header,
|
||||||
|
const std::shared_ptr<drone_services::srv::ReadyDrone::Request> request,
|
||||||
|
const std::shared_ptr<drone_services::srv::ReadyDrone::Response> response)
|
||||||
|
{
|
||||||
|
if (this->failsafe_enabled)
|
||||||
|
{
|
||||||
|
response->success = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this->has_received_first_lidar_message)
|
||||||
|
{
|
||||||
|
this->enable_failsafe(u"Waiting for LIDAR timed out! Check the LIDAR USB connection and consider restarting the drone.");
|
||||||
|
response->success = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->got_ready_drone_request = true;
|
||||||
|
this->vehicle_control_request->control = CONTROL_MODE_ATTITUDE;
|
||||||
|
auto control_mode_response = this->vehicle_control_client->async_send_request(this->vehicle_control_request,
|
||||||
|
std::bind(&PositionChanger::vehicle_control_service_callback, this, std::placeholders::_1));
|
||||||
|
// TODO set motors to spin at 30%
|
||||||
|
response->success = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get the yaw angle from a specified normalized quaternion.
|
* @brief Get the yaw angle from a specified normalized quaternion.
|
||||||
* Uses the theory from https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles
|
* Uses the theory from https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles
|
||||||
@@ -338,18 +512,24 @@ private:
|
|||||||
rclcpp::Publisher<drone_services::msg::FailsafeMsg>::SharedPtr failsafe_publisher;
|
rclcpp::Publisher<drone_services::msg::FailsafeMsg>::SharedPtr failsafe_publisher;
|
||||||
rclcpp::Subscription<drone_services::msg::LidarReading>::SharedPtr lidar_subscription;
|
rclcpp::Subscription<drone_services::msg::LidarReading>::SharedPtr lidar_subscription;
|
||||||
rclcpp::Subscription<px4_msgs::msg::VehicleOdometry>::SharedPtr odometry_subscription;
|
rclcpp::Subscription<px4_msgs::msg::VehicleOdometry>::SharedPtr odometry_subscription;
|
||||||
|
rclcpp::Subscription<drone_services::msg::HeightData>::SharedPtr height_subscription;
|
||||||
|
|
||||||
rclcpp::Client<drone_services::srv::SetTrajectory>::SharedPtr trajectory_client;
|
rclcpp::Client<drone_services::srv::SetTrajectory>::SharedPtr trajectory_client;
|
||||||
|
rclcpp::Client<drone_services::srv::SetAttitude>::SharedPtr attitude_client;
|
||||||
rclcpp::Client<drone_services::srv::SetVehicleControl>::SharedPtr vehicle_control_client;
|
rclcpp::Client<drone_services::srv::SetVehicleControl>::SharedPtr vehicle_control_client;
|
||||||
rclcpp::Client<drone_services::srv::EnableFailsafe>::SharedPtr failsafe_client;
|
rclcpp::Client<drone_services::srv::EnableFailsafe>::SharedPtr failsafe_client;
|
||||||
|
rclcpp::Client<drone_services::srv::ArmDrone>::SharedPtr arm_drone_client;
|
||||||
|
|
||||||
rclcpp::Service<drone_services::srv::MovePosition>::SharedPtr move_position_service;
|
rclcpp::Service<drone_services::srv::MovePosition>::SharedPtr move_position_service;
|
||||||
|
rclcpp::Service<drone_services::srv::ReadyDrone>::SharedPtr ready_drone_service;
|
||||||
|
rclcpp::Service<drone_services::srv::Land>::SharedPtr land_service;
|
||||||
rclcpp::TimerBase::SharedPtr lidar_health_timer;
|
rclcpp::TimerBase::SharedPtr lidar_health_timer;
|
||||||
|
|
||||||
drone_services::srv::SetTrajectory::Request::SharedPtr trajectory_request;
|
drone_services::srv::SetTrajectory::Request::SharedPtr trajectory_request;
|
||||||
|
drone_services::srv::SetAttitude::Request::SharedPtr attitude_request;
|
||||||
drone_services::srv::SetVehicleControl::Request::SharedPtr vehicle_control_request;
|
drone_services::srv::SetVehicleControl::Request::SharedPtr vehicle_control_request;
|
||||||
drone_services::srv::EnableFailsafe::Request::SharedPtr failsafe_request;
|
drone_services::srv::EnableFailsafe::Request::SharedPtr failsafe_request;
|
||||||
|
drone_services::srv::ArmDrone::Request::SharedPtr arm_drone_request;
|
||||||
|
|
||||||
float lidar_sensor_values[4]{MIN_DISTANCE}; // last distance measured by the lidar sensors
|
float lidar_sensor_values[4]{MIN_DISTANCE}; // last distance measured by the lidar sensors
|
||||||
float lidar_imu_readings[4]; // last imu readings from the lidar sensors
|
float lidar_imu_readings[4]; // last imu readings from the lidar sensors
|
||||||
@@ -357,10 +537,17 @@ private:
|
|||||||
float current_speed_x = 0;
|
float current_speed_x = 0;
|
||||||
float current_speed_y = 0;
|
float current_speed_y = 0;
|
||||||
float current_speed_z = 0;
|
float current_speed_z = 0;
|
||||||
|
float current_height = 0;
|
||||||
|
float start_height = -1;
|
||||||
|
bool is_landing = false;
|
||||||
|
bool has_landed = false;
|
||||||
bool move_direction_allowed[4] = {true}; // whether the drone can move in a certain direction
|
bool move_direction_allowed[4] = {true}; // whether the drone can move in a certain direction
|
||||||
float collision_prevention_weights[4] = {0}; // the amount to move away from an object in a certain direction if the drone is too close
|
float collision_prevention_weights[4] = {0}; // the amount to move away from an object in a certain direction if the drone is too close
|
||||||
bool failsafe_enabled = false;
|
bool failsafe_enabled = false;
|
||||||
bool received_lidar_message = false;
|
bool received_lidar_message = false;
|
||||||
|
int lidar_health_checks = 0;
|
||||||
|
bool has_received_first_lidar_message = false;
|
||||||
|
bool got_ready_drone_request = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief waits for a service to be available
|
* @brief waits for a service to be available
|
||||||
@@ -369,7 +556,7 @@ private:
|
|||||||
* @param client the client object to wait for the service
|
* @param client the client object to wait for the service
|
||||||
*/
|
*/
|
||||||
template <class T>
|
template <class T>
|
||||||
void wait_for_service(T client)
|
void wait_for_service(T client, std::string service_name)
|
||||||
{
|
{
|
||||||
while (!client->wait_for_service(1s))
|
while (!client->wait_for_service(1s))
|
||||||
{
|
{
|
||||||
@@ -378,7 +565,7 @@ private:
|
|||||||
RCLCPP_ERROR(this->get_logger(), "Interrupted while waiting for the service. Exiting.");
|
RCLCPP_ERROR(this->get_logger(), "Interrupted while waiting for the service. Exiting.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
RCLCPP_INFO(this->get_logger(), "service not available, waiting again...");
|
RCLCPP_INFO(this->get_logger(), "service not available, waiting again on service %s", service_name.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
168
src/drone_controls/test/test_positionchanger.py
Normal file
168
src/drone_controls/test/test_positionchanger.py
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import launch
|
||||||
|
import launch_ros
|
||||||
|
import launch_ros.actions
|
||||||
|
import launch_testing.actions
|
||||||
|
import pytest
|
||||||
|
import rclpy
|
||||||
|
import time
|
||||||
|
|
||||||
|
from drone_services.srv import MovePosition
|
||||||
|
from drone_services.msg import FailsafeMsg
|
||||||
|
from drone_services.msg import LidarReading
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.rostest
|
||||||
|
def generate_test_description():
|
||||||
|
file_path = os.path.dirname(__file__)
|
||||||
|
# device under test
|
||||||
|
positionchanger_node = launch_ros.actions.Node(
|
||||||
|
package='drone_controls', executable='position_changer')
|
||||||
|
failsafe_node = launch_ros.actions.Node(
|
||||||
|
package='failsafe', executable='failsafe')
|
||||||
|
px4_controller_node = launch_ros.actions.Node(
|
||||||
|
package='px4_connection', executable='px4_controller')
|
||||||
|
heartbeat_node = launch_ros.actions.Node(
|
||||||
|
package='px4_connection', executable='heartbeat')
|
||||||
|
|
||||||
|
return (
|
||||||
|
launch.LaunchDescription([
|
||||||
|
positionchanger_node,
|
||||||
|
failsafe_node,
|
||||||
|
px4_controller_node,
|
||||||
|
heartbeat_node,
|
||||||
|
launch_testing.actions.ReadyToTest(),
|
||||||
|
]),
|
||||||
|
{
|
||||||
|
'positionchanger_node': positionchanger_node,
|
||||||
|
'failsafe_node': failsafe_node,
|
||||||
|
'px4_controller_node': px4_controller_node,
|
||||||
|
'heartbeat_node': heartbeat_node
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPositionChanger(unittest.TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
rclpy.init()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
rclpy.shutdown()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.node = rclpy.create_node('test_positionchanger')
|
||||||
|
self.called_positionchanger_service = False
|
||||||
|
self.received_failsafe_callback = False
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.node.destroy_node()
|
||||||
|
|
||||||
|
def failsafe_callback(self, msg):
|
||||||
|
self.assertTrue(msg.enabled, "Failsafe was not enabled!")
|
||||||
|
self.received_failsafe_callback = True
|
||||||
|
|
||||||
|
def move_position_callback(self, future):
|
||||||
|
self.assertFalse(future.result(
|
||||||
|
).success, "MovePosition service call was successful, but should have failed!")
|
||||||
|
self.called_positionchanger_service = True
|
||||||
|
|
||||||
|
def test_positionchanger_no_lidar_data(self, positionchanger_node, proc_output):
|
||||||
|
self.received_failsafe_callback = False
|
||||||
|
self.called_positionchanger_service = False
|
||||||
|
failsafe_subscriber = self.node.create_subscription(
|
||||||
|
FailsafeMsg, '/drone/failsafe', self.failsafe_callback, 10)
|
||||||
|
move_position_client = self.node.create_client(
|
||||||
|
MovePosition, '/drone/move_position')
|
||||||
|
while not move_position_client.wait_for_service(timeout_sec=1.0):
|
||||||
|
self.node.get_logger().info('move_position service not available, waiting again...')
|
||||||
|
|
||||||
|
request = MovePosition.Request()
|
||||||
|
request.front_back = 1.0
|
||||||
|
request.left_right = 1.0
|
||||||
|
request.up_down = 1.0
|
||||||
|
request.angle = 1.0
|
||||||
|
|
||||||
|
end_time = time.time() + 10.0
|
||||||
|
try:
|
||||||
|
while time.time() < end_time:
|
||||||
|
rclpy.spin_once(self.node)
|
||||||
|
if not self.called_positionchanger_service:
|
||||||
|
future = move_position_client.call_async(request)
|
||||||
|
future.add_done_callback(self.move_position_callback)
|
||||||
|
elif not self.received_failsafe_callback:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
self.assertTrue(self.received_failsafe_callback,
|
||||||
|
"Failsafe callback was not received!")
|
||||||
|
self.assertTrue(self.called_positionchanger_service,
|
||||||
|
"MovePosition service was not called!")
|
||||||
|
finally:
|
||||||
|
self.node.destroy_client(move_position_client)
|
||||||
|
self.node.destroy_subscription(failsafe_subscriber)
|
||||||
|
|
||||||
|
def test_positionchanger_lidar_stops(self, positionchanger_node, proc_output):
|
||||||
|
self.node.get_logger().info("STARTING TEST test_positionchanger_lidar_stops")
|
||||||
|
self.received_failsafe_callback = False
|
||||||
|
self.called_positionchanger_service = False
|
||||||
|
failsafe_subscriber = self.node.create_subscription(
|
||||||
|
FailsafeMsg, '/drone/failsafe', self.failsafe_callback, 10)
|
||||||
|
lidar_publisher = self.node.create_publisher(
|
||||||
|
LidarReading, '/drone/object_detection', 10)
|
||||||
|
move_position_client = self.node.create_client(
|
||||||
|
MovePosition, '/drone/move_position')
|
||||||
|
|
||||||
|
while not move_position_client.wait_for_service(timeout_sec=1.0):
|
||||||
|
self.node.get_logger().info('move_position service not available, waiting again...')
|
||||||
|
|
||||||
|
request = MovePosition.Request()
|
||||||
|
request.front_back = 1.0
|
||||||
|
request.left_right = 1.0
|
||||||
|
request.up_down = 1.0
|
||||||
|
request.angle = 1.0
|
||||||
|
|
||||||
|
lidar_msg = LidarReading()
|
||||||
|
lidar_msg.sensor_1 = 2.0
|
||||||
|
lidar_msg.sensor_2 = 2.0
|
||||||
|
lidar_msg.sensor_3 = 2.0
|
||||||
|
lidar_msg.sensor_4 = 2.0
|
||||||
|
lidar_msg.imu_data = [1.0, 1.0, 1.0, 1.0]
|
||||||
|
sent_lidar_msg = False
|
||||||
|
|
||||||
|
# wait for nodes to become active
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# wait 5 seconds for the failsafe to trigger
|
||||||
|
wait_time = time.time() + 5.0
|
||||||
|
end_time = time.time() + 10.0
|
||||||
|
try:
|
||||||
|
self.node.get_logger().info('STARTING WHILE LOOP')
|
||||||
|
while time.time() < end_time:
|
||||||
|
rclpy.spin_once(self.node, timeout_sec=0.1)
|
||||||
|
if not sent_lidar_msg:
|
||||||
|
lidar_publisher.publish(lidar_msg)
|
||||||
|
sent_lidar_msg = True
|
||||||
|
# wait 5 seconds before sending the move_position request
|
||||||
|
if not self.called_positionchanger_service and time.time() > wait_time:
|
||||||
|
self.node.get_logger().info('Sending move_position request')
|
||||||
|
future = move_position_client.call_async(request)
|
||||||
|
future.add_done_callback(self.move_position_callback)
|
||||||
|
elif not self.received_failsafe_callback:
|
||||||
|
continue
|
||||||
|
self.node.get_logger().info('END OF WHILE LOOP')
|
||||||
|
self.assertTrue(self.called_positionchanger_service,
|
||||||
|
"MovePosition service was not called!")
|
||||||
|
self.assertTrue(self.received_failsafe_callback,
|
||||||
|
"Failsafe was not activated!")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.node.get_logger().info('Cleaning up')
|
||||||
|
self.node.destroy_client(move_position_client)
|
||||||
|
self.node.destroy_subscription(failsafe_subscriber)
|
||||||
|
self.node.destroy_publisher(lidar_publisher)
|
||||||
158
src/drone_controls/test/test_positionchanger_height.py
Normal file
158
src/drone_controls/test/test_positionchanger_height.py
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import launch
|
||||||
|
import launch_ros
|
||||||
|
import launch_ros.actions
|
||||||
|
import launch_testing.actions
|
||||||
|
import pytest
|
||||||
|
import rclpy
|
||||||
|
import time
|
||||||
|
|
||||||
|
from drone_services.srv import MovePosition
|
||||||
|
from drone_services.srv import ArmDrone
|
||||||
|
from drone_services.srv import Land
|
||||||
|
from drone_services.msg import HeightData
|
||||||
|
|
||||||
|
@pytest.mark.rostest
|
||||||
|
def generate_test_description():
|
||||||
|
file_path = os.path.dirname(__file__)
|
||||||
|
# device under test
|
||||||
|
positionchanger_node = launch_ros.actions.Node(
|
||||||
|
package='drone_controls', executable='position_changer')
|
||||||
|
failsafe_node = launch_ros.actions.Node(
|
||||||
|
package='failsafe', executable='failsafe')
|
||||||
|
px4_controller_node = launch_ros.actions.Node(
|
||||||
|
package='px4_connection', executable='px4_controller')
|
||||||
|
heartbeat_node = launch_ros.actions.Node(
|
||||||
|
package='px4_connection', executable='heartbeat')
|
||||||
|
|
||||||
|
return (
|
||||||
|
launch.LaunchDescription([
|
||||||
|
positionchanger_node,
|
||||||
|
failsafe_node,
|
||||||
|
px4_controller_node,
|
||||||
|
heartbeat_node,
|
||||||
|
launch_testing.actions.ReadyToTest(),
|
||||||
|
]),
|
||||||
|
{
|
||||||
|
'positionchanger_node': positionchanger_node,
|
||||||
|
'failsafe_node': failsafe_node,
|
||||||
|
'px4_controller_node': px4_controller_node,
|
||||||
|
'heartbeat_node': heartbeat_node
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
class TestPositionChanger(unittest.TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
rclpy.init()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
rclpy.shutdown()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.node = rclpy.create_node('test_positionchanger')
|
||||||
|
self.called_arm_service = False
|
||||||
|
self.called_land_service = False
|
||||||
|
self.called_move_service_up = False
|
||||||
|
self.called_move_service_stop = False
|
||||||
|
self.moved_up = False
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.node.destroy_node()
|
||||||
|
|
||||||
|
def move_position_callback_up(self, future):
|
||||||
|
self.node.get_logger().info("Callback called")
|
||||||
|
self.assertTrue(future.result().success, "Move service failed")
|
||||||
|
self.called_move_service_up = True
|
||||||
|
|
||||||
|
def move_position_callback_stop(self, future):
|
||||||
|
self.node.get_logger().info("Callback called")
|
||||||
|
self.assertTrue(future.result().success, "Move service failed")
|
||||||
|
self.called_move_service_stop = True
|
||||||
|
|
||||||
|
def arm_callback(self, future):
|
||||||
|
self.node.get_logger().info("Arm Callback called")
|
||||||
|
self.assertTrue(future.result().success, "Arm service failed")
|
||||||
|
self.called_arm_service = True
|
||||||
|
|
||||||
|
def land_callback(self,future):
|
||||||
|
self.node.get_logger().info("Land Callback called")
|
||||||
|
self.assertTrue(future.result().is_landing, "Drone is not landing")
|
||||||
|
self.called_land_service = True
|
||||||
|
|
||||||
|
def test_positionchanger_land(self, proc_output):
|
||||||
|
self.node.get_logger().info("starting land test")
|
||||||
|
height_data_publisher = self.node.create_publisher(HeightData, '/drone/height', 10)
|
||||||
|
height_msg = HeightData()
|
||||||
|
height_msg.heights = [0.2,0.2,0.2,0.2]
|
||||||
|
height_msg.min_height = 0.2
|
||||||
|
arm_client = self.node.create_client(ArmDrone, '/drone/arm')
|
||||||
|
while not arm_client.wait_for_service(timeout_sec=1.0):
|
||||||
|
self.node.get_logger().info('arm service not available, waiting again...')
|
||||||
|
land_client = self.node.create_client(Land, '/drone/land')
|
||||||
|
while not land_client.wait_for_service(timeout_sec=1.0):
|
||||||
|
self.node.get_logger().info('land service not available, waiting again...')
|
||||||
|
move_client = self.node.create_client(MovePosition, '/drone/move_position')
|
||||||
|
while not move_client.wait_for_service(timeout_sec=1.0):
|
||||||
|
self.node.get_logger().info('move_position service not available, waiting again...')
|
||||||
|
|
||||||
|
arm_request = ArmDrone.Request()
|
||||||
|
land_request = Land.Request()
|
||||||
|
move_request = MovePosition.Request()
|
||||||
|
move_request.front_back = 0.0
|
||||||
|
move_request.left_right = 0.0
|
||||||
|
move_request.up_down = 5.0
|
||||||
|
move_request.angle = 0.0
|
||||||
|
|
||||||
|
start_height_msgs_published = 0
|
||||||
|
moving_height_msgs_published = 0
|
||||||
|
landing_height_msgs_published = 0
|
||||||
|
|
||||||
|
end_time = time.time() + 20.0
|
||||||
|
try:
|
||||||
|
while time.time() < end_time:
|
||||||
|
rclpy.spin_once(self.node, timeout_sec=0.1)
|
||||||
|
if start_height_msgs_published < 10:
|
||||||
|
height_data_publisher.publish(height_msg)
|
||||||
|
start_height_msgs_published += 1
|
||||||
|
elif not self.called_arm_service:
|
||||||
|
arm_future = arm_client.call_async(arm_request)
|
||||||
|
arm_future.add_done_callback(self.arm_callback)
|
||||||
|
elif not self.called_move_service_up:
|
||||||
|
move_future = move_client.call_async(move_request)
|
||||||
|
move_future.add_done_callback(self.move_position_callback_up)
|
||||||
|
elif moving_height_msgs_published < 10:
|
||||||
|
height_msg.min_height += 0.1
|
||||||
|
height_data_publisher.publish(height_msg)
|
||||||
|
moving_height_msgs_published += 1
|
||||||
|
elif not self.called_move_service_stop:
|
||||||
|
move_request.up_down = 0.0
|
||||||
|
move_future = move_client.call_async(move_request)
|
||||||
|
move_future.add_done_callback(self.move_position_callback_stop)
|
||||||
|
elif not self.called_land_service:
|
||||||
|
land_future = land_client.call_async(land_request)
|
||||||
|
land_future.add_done_callback(self.land_callback)
|
||||||
|
elif landing_height_msgs_published < 10:
|
||||||
|
height_msg.min_height -= 0.15
|
||||||
|
height_data_publisher.publish(height_msg)
|
||||||
|
landing_height_msgs_published += 1
|
||||||
|
|
||||||
|
launch_testing.asserts.assertInStderr(proc_output, "Attitude set to 0 for landing, landing done", 'position_changer-1')
|
||||||
|
launch_testing.asserts.assertInStderr(proc_output, "Vehicle Control mode set to attitude for landing", 'position_changer-1')
|
||||||
|
finally:
|
||||||
|
self.node.get_logger().info("shutting down")
|
||||||
|
self.node.destroy_client(arm_client)
|
||||||
|
self.node.destroy_client(land_client)
|
||||||
|
self.node.destroy_client(move_client)
|
||||||
|
self.node.destroy_publisher(height_data_publisher)
|
||||||
|
#publish height data as start value
|
||||||
|
#arm
|
||||||
|
#move up
|
||||||
|
#hang still
|
||||||
|
#land
|
||||||
|
#check if landed
|
||||||
242
src/drone_controls/test/test_positionchanger_lidar.py
Normal file
242
src/drone_controls/test/test_positionchanger_lidar.py
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import launch
|
||||||
|
import launch_ros
|
||||||
|
import launch_ros.actions
|
||||||
|
import launch_testing.actions
|
||||||
|
import pytest
|
||||||
|
import rclpy
|
||||||
|
import time
|
||||||
|
|
||||||
|
from drone_services.srv import MovePosition
|
||||||
|
from drone_services.msg import FailsafeMsg
|
||||||
|
from drone_services.msg import LidarReading
|
||||||
|
|
||||||
|
@pytest.mark.rostest
|
||||||
|
def generate_test_description():
|
||||||
|
file_path = os.path.dirname(__file__)
|
||||||
|
# device under test
|
||||||
|
positionchanger_node = launch_ros.actions.Node(
|
||||||
|
package='drone_controls', executable='position_changer')
|
||||||
|
failsafe_node = launch_ros.actions.Node(
|
||||||
|
package='failsafe', executable='failsafe')
|
||||||
|
px4_controller_node = launch_ros.actions.Node(
|
||||||
|
package='px4_connection', executable='px4_controller')
|
||||||
|
heartbeat_node = launch_ros.actions.Node(
|
||||||
|
package='px4_connection', executable='heartbeat')
|
||||||
|
|
||||||
|
return (
|
||||||
|
launch.LaunchDescription([
|
||||||
|
positionchanger_node,
|
||||||
|
failsafe_node,
|
||||||
|
px4_controller_node,
|
||||||
|
heartbeat_node,
|
||||||
|
launch_testing.actions.ReadyToTest(),
|
||||||
|
]),
|
||||||
|
{
|
||||||
|
'positionchanger_node': positionchanger_node,
|
||||||
|
'failsafe_node': failsafe_node,
|
||||||
|
'px4_controller_node': px4_controller_node,
|
||||||
|
'heartbeat_node': heartbeat_node
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
class TestPositionChanger(unittest.TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
rclpy.init()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
rclpy.shutdown()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.node = rclpy.create_node('test_positionchanger')
|
||||||
|
self.called_positionchanger_service = False
|
||||||
|
self.received_failsafe_callback = False
|
||||||
|
self.lidar_publisher = self.node.create_publisher(
|
||||||
|
LidarReading, '/drone/object_detection', 10)
|
||||||
|
self.move_position_client = self.node.create_client(
|
||||||
|
MovePosition, '/drone/move_position')
|
||||||
|
while not self.move_position_client.wait_for_service(timeout_sec=1.0):
|
||||||
|
self.node.get_logger().info('move_position service not available, waiting again...')
|
||||||
|
self.request = MovePosition.Request()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.node.destroy_client(self.move_position_client)
|
||||||
|
self.node.destroy_publisher(self.lidar_publisher)
|
||||||
|
self.node.destroy_node()
|
||||||
|
|
||||||
|
def toRadians(self, degrees) -> float:
|
||||||
|
return degrees * 3.14159265359 / 180
|
||||||
|
|
||||||
|
|
||||||
|
def move_position_callback(self, future):
|
||||||
|
self.node.get_logger().info("Callback called")
|
||||||
|
self.called_positionchanger_service = True
|
||||||
|
|
||||||
|
def test_positionchanger_lidar_moves_away_front(self, proc_output):
|
||||||
|
self.node.get_logger().info("starting front test")
|
||||||
|
self.request.front_back = 1.0
|
||||||
|
self.request.left_right = 0.0
|
||||||
|
self.request.up_down = 0.0
|
||||||
|
self.request.angle = 0.0
|
||||||
|
|
||||||
|
lidar_msgs_sent = 0
|
||||||
|
|
||||||
|
lidar_msg = LidarReading()
|
||||||
|
lidar_msg.sensor_1 = 0.5 # front right
|
||||||
|
lidar_msg.sensor_2 = 2.0 # front left
|
||||||
|
lidar_msg.sensor_3 = 2.0 # rear left
|
||||||
|
lidar_msg.sensor_4 = 2.0 # rear right
|
||||||
|
lidar_msg.imu_data = [1.0, 1.0, 1.0, 1.0]
|
||||||
|
end_time = time.time() + 10.0
|
||||||
|
|
||||||
|
while time.time() < end_time:
|
||||||
|
rclpy.spin_once(self.node, timeout_sec=0.1)
|
||||||
|
self.lidar_publisher.publish(lidar_msg)
|
||||||
|
lidar_msgs_sent += 1
|
||||||
|
if (lidar_msgs_sent == 10):
|
||||||
|
lidar_msg.sensor_1 = 1.0
|
||||||
|
lidar_msg.sensor_2 = 0.3
|
||||||
|
elif (lidar_msgs_sent == 20):
|
||||||
|
break
|
||||||
|
if not self.called_positionchanger_service:
|
||||||
|
future = self.move_position_client.call_async(self.request)
|
||||||
|
future.add_done_callback(self.move_position_callback)
|
||||||
|
launch_testing.asserts.assertInStderr(proc_output, "Collision prevention front: -0.5", 'position_changer-1')
|
||||||
|
launch_testing.asserts.assertInStderr(proc_output, "Collision prevention front: -0.7", 'position_changer-1')
|
||||||
|
|
||||||
|
def test_positionchanger_lidar_moves_away_right(self, proc_output):
|
||||||
|
self.node.get_logger().info("starting right test")
|
||||||
|
self.request.front_back = 0.0
|
||||||
|
self.request.left_right = 1.0
|
||||||
|
self.request.up_down = 0.0
|
||||||
|
self.request.angle = 0.0
|
||||||
|
|
||||||
|
lidar_msgs_sent = 0
|
||||||
|
|
||||||
|
lidar_msg = LidarReading()
|
||||||
|
lidar_msg.sensor_1 = 0.4 # front right
|
||||||
|
lidar_msg.sensor_2 = 2.0 # front left
|
||||||
|
lidar_msg.sensor_3 = 2.0 # rear left
|
||||||
|
lidar_msg.sensor_4 = 2.0 # rear right
|
||||||
|
lidar_msg.imu_data = [1.0, 1.0, 1.0, 1.0]
|
||||||
|
end_time = time.time() + 10.0
|
||||||
|
|
||||||
|
while time.time() < end_time:
|
||||||
|
rclpy.spin_once(self.node, timeout_sec=0.1)
|
||||||
|
self.lidar_publisher.publish(lidar_msg)
|
||||||
|
lidar_msgs_sent += 1
|
||||||
|
if (lidar_msgs_sent == 10):
|
||||||
|
lidar_msg.sensor_1 = 2.0
|
||||||
|
lidar_msg.sensor_4 = 0.29
|
||||||
|
elif (lidar_msgs_sent == 20):
|
||||||
|
break
|
||||||
|
if not self.called_positionchanger_service:
|
||||||
|
future = self.move_position_client.call_async(self.request)
|
||||||
|
future.add_done_callback(self.move_position_callback)
|
||||||
|
launch_testing.asserts.assertInStderr(proc_output, "Collision prevention right: -0.6", 'position_changer-1')
|
||||||
|
launch_testing.asserts.assertInStderr(proc_output, "Collision prevention right: -0.71", 'position_changer-1')
|
||||||
|
|
||||||
|
def test_positionchanger_lidar_moves_away_left(self, proc_output):
|
||||||
|
self.node.get_logger().info("starting left test")
|
||||||
|
self.request.front_back = 0.0
|
||||||
|
self.request.left_right = -1.0
|
||||||
|
self.request.up_down = 0.0
|
||||||
|
self.request.angle = 0.0
|
||||||
|
|
||||||
|
lidar_msgs_sent = 0
|
||||||
|
|
||||||
|
lidar_msg = LidarReading()
|
||||||
|
lidar_msg.sensor_1 = 2.0 # front right
|
||||||
|
lidar_msg.sensor_2 = 0.11 # front left
|
||||||
|
lidar_msg.sensor_3 = 2.0 # rear left
|
||||||
|
lidar_msg.sensor_4 = 2.0 # rear right
|
||||||
|
lidar_msg.imu_data = [1.0, 1.0, 1.0, 1.0]
|
||||||
|
end_time = time.time() + 10.0
|
||||||
|
|
||||||
|
while time.time() < end_time:
|
||||||
|
rclpy.spin_once(self.node, timeout_sec=0.1)
|
||||||
|
self.lidar_publisher.publish(lidar_msg)
|
||||||
|
lidar_msgs_sent += 1
|
||||||
|
if (lidar_msgs_sent == 10):
|
||||||
|
lidar_msg.sensor_2 = 2.0
|
||||||
|
lidar_msg.sensor_3 = 0.78
|
||||||
|
elif (lidar_msgs_sent == 20):
|
||||||
|
break
|
||||||
|
if not self.called_positionchanger_service:
|
||||||
|
future = self.move_position_client.call_async(self.request)
|
||||||
|
future.add_done_callback(self.move_position_callback)
|
||||||
|
launch_testing.asserts.assertInStderr(proc_output, "Collision prevention left: 0.89", 'position_changer-1')
|
||||||
|
launch_testing.asserts.assertInStderr(proc_output, "Collision prevention left: 0.22", 'position_changer-1')
|
||||||
|
|
||||||
|
def test_positionchanger_lidar_moves_away_back(self, proc_output):
|
||||||
|
self.node.get_logger().info("starting back test")
|
||||||
|
self.request.front_back = -1.0
|
||||||
|
self.request.left_right = 0.0
|
||||||
|
self.request.up_down = 0.0
|
||||||
|
self.request.angle = 0.0
|
||||||
|
|
||||||
|
lidar_msgs_sent = 0
|
||||||
|
|
||||||
|
lidar_msg = LidarReading()
|
||||||
|
lidar_msg.sensor_1 = 2.0 # front right
|
||||||
|
lidar_msg.sensor_2 = 2.0 # front left
|
||||||
|
lidar_msg.sensor_3 = 0.36 # rear left
|
||||||
|
lidar_msg.sensor_4 = 2.0 # rear right
|
||||||
|
lidar_msg.imu_data = [1.0, 1.0, 1.0, 1.0]
|
||||||
|
#this test is carried out first, so wait for nodes to start
|
||||||
|
time.sleep(5)
|
||||||
|
end_time = time.time() + 20.0
|
||||||
|
|
||||||
|
while time.time() < end_time:
|
||||||
|
rclpy.spin_once(self.node, timeout_sec=0.1)
|
||||||
|
self.lidar_publisher.publish(lidar_msg)
|
||||||
|
lidar_msgs_sent += 1
|
||||||
|
if (lidar_msgs_sent == 10):
|
||||||
|
lidar_msg.sensor_3 = 2.0
|
||||||
|
lidar_msg.sensor_4 = 0.12
|
||||||
|
elif (lidar_msgs_sent == 20):
|
||||||
|
break
|
||||||
|
if not self.called_positionchanger_service:
|
||||||
|
future = self.move_position_client.call_async(self.request)
|
||||||
|
future.add_done_callback(self.move_position_callback)
|
||||||
|
launch_testing.asserts.assertInStderr(proc_output, "Collision prevention back: 0.64", 'position_changer-1')
|
||||||
|
launch_testing.asserts.assertInStderr(proc_output, "Collision prevention back: 0.88", 'position_changer-1')
|
||||||
|
|
||||||
|
def test_positionchanger_lidar_moves_away_still(self, proc_output):
|
||||||
|
self.node.get_logger().info("starting back test")
|
||||||
|
self.request.front_back = 0.0
|
||||||
|
self.request.left_right = 0.0
|
||||||
|
self.request.up_down = 0.0
|
||||||
|
self.request.angle = 0.0
|
||||||
|
|
||||||
|
lidar_msgs_sent = 0
|
||||||
|
|
||||||
|
lidar_msg = LidarReading()
|
||||||
|
lidar_msg.sensor_1 = 2.0 # front right
|
||||||
|
lidar_msg.sensor_2 = 2.0 # front left
|
||||||
|
lidar_msg.sensor_3 = 2.0 # rear left
|
||||||
|
lidar_msg.sensor_4 = 0.12 # rear right
|
||||||
|
lidar_msg.imu_data = [1.0, 1.0, 1.0, 1.0]
|
||||||
|
end_time = time.time() + 10.0
|
||||||
|
|
||||||
|
while time.time() < end_time:
|
||||||
|
rclpy.spin_once(self.node, timeout_sec=0.1)
|
||||||
|
self.lidar_publisher.publish(lidar_msg)
|
||||||
|
lidar_msgs_sent += 1
|
||||||
|
if (lidar_msgs_sent == 10):
|
||||||
|
lidar_msg.sensor_4 = 2.0
|
||||||
|
lidar_msg.sensor_3 = 0.36
|
||||||
|
elif (lidar_msgs_sent == 20):
|
||||||
|
break
|
||||||
|
if not self.called_positionchanger_service:
|
||||||
|
future = self.move_position_client.call_async(self.request)
|
||||||
|
future.add_done_callback(self.move_position_callback)
|
||||||
|
launch_testing.asserts.assertInStderr(proc_output, "Collision prevention back: 0.64", 'position_changer-1')
|
||||||
|
launch_testing.asserts.assertInStderr(proc_output, "Collision prevention back: 0.88", 'position_changer-1')
|
||||||
|
|
||||||
@@ -31,6 +31,8 @@ rosidl_generate_interfaces(${PROJECT_NAME}
|
|||||||
"srv/ControlRelais.srv"
|
"srv/ControlRelais.srv"
|
||||||
"srv/MovePosition.srv"
|
"srv/MovePosition.srv"
|
||||||
"srv/EnableFailsafe.srv"
|
"srv/EnableFailsafe.srv"
|
||||||
|
"srv/ReadyDrone.srv"
|
||||||
|
"srv/Land.srv"
|
||||||
"msg/DroneControlMode.msg"
|
"msg/DroneControlMode.msg"
|
||||||
"msg/DroneArmStatus.msg"
|
"msg/DroneArmStatus.msg"
|
||||||
"msg/DroneRouteStatus.msg"
|
"msg/DroneRouteStatus.msg"
|
||||||
@@ -38,6 +40,7 @@ rosidl_generate_interfaces(${PROJECT_NAME}
|
|||||||
"msg/HeightData.msg"
|
"msg/HeightData.msg"
|
||||||
"msg/LidarReading.msg"
|
"msg/LidarReading.msg"
|
||||||
"msg/MultiflexReading.msg"
|
"msg/MultiflexReading.msg"
|
||||||
|
"msg/FailsafeMsg.msg"
|
||||||
)
|
)
|
||||||
|
|
||||||
if(BUILD_TESTING)
|
if(BUILD_TESTING)
|
||||||
|
|||||||
@@ -2,4 +2,8 @@ float32 battery_percentage
|
|||||||
float32 cpu_usage
|
float32 cpu_usage
|
||||||
int32 route_setpoint # -1 if no route
|
int32 route_setpoint # -1 if no route
|
||||||
wstring control_mode
|
wstring control_mode
|
||||||
bool armed
|
bool armed
|
||||||
|
float32[3] velocity
|
||||||
|
float32[3] position
|
||||||
|
float32 height
|
||||||
|
bool failsafe
|
||||||
2
src/drone_services/srv/Land.srv
Normal file
2
src/drone_services/srv/Land.srv
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
---
|
||||||
|
bool is_landing
|
||||||
2
src/drone_services/srv/ReadyDrone.srv
Normal file
2
src/drone_services/srv/ReadyDrone.srv
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
---
|
||||||
|
bool success
|
||||||
@@ -6,8 +6,10 @@ from drone_services.msg import DroneStatus
|
|||||||
from drone_services.msg import DroneControlMode
|
from drone_services.msg import DroneControlMode
|
||||||
from drone_services.msg import DroneArmStatus
|
from drone_services.msg import DroneArmStatus
|
||||||
from drone_services.msg import DroneRouteStatus
|
from drone_services.msg import DroneRouteStatus
|
||||||
|
from drone_services.msg import HeightData
|
||||||
from px4_msgs.msg import BatteryStatus
|
from px4_msgs.msg import BatteryStatus
|
||||||
from px4_msgs.msg import Cpuload
|
from px4_msgs.msg import Cpuload
|
||||||
|
from px4_msgs.msg import VehicleOdometry
|
||||||
|
|
||||||
CONTROL_MODE_ATTITUDE = 1
|
CONTROL_MODE_ATTITUDE = 1
|
||||||
CONTROL_MODE_VELOCITY = 2
|
CONTROL_MODE_VELOCITY = 2
|
||||||
@@ -32,17 +34,23 @@ class DroneStatusNode(Node):
|
|||||||
DroneArmStatus, '/drone/arm_status', self.arm_status_callback, 10)
|
DroneArmStatus, '/drone/arm_status', self.arm_status_callback, 10)
|
||||||
self.route_status_subscriber = self.create_subscription(
|
self.route_status_subscriber = self.create_subscription(
|
||||||
DroneRouteStatus, '/drone/route_status', self.route_status_callback, 10)
|
DroneRouteStatus, '/drone/route_status', self.route_status_callback, 10)
|
||||||
|
self.height_data_subscriber = self.create_subscription(HeightData, '/drone/height', self.height_data_callback, 10)
|
||||||
self.battery_status_subscriber = self.create_subscription(
|
self.battery_status_subscriber = self.create_subscription(
|
||||||
BatteryStatus, '/fmu/out/battery_status', self.battery_status_callback, qos_profile=qos_profile)
|
BatteryStatus, '/fmu/out/battery_status', self.battery_status_callback, qos_profile=qos_profile)
|
||||||
self.cpu_load_subscriber = self.create_subscription(
|
self.cpu_load_subscriber = self.create_subscription(
|
||||||
Cpuload, '/fmu/out/cpuload', self.cpu_load_callback, qos_profile=qos_profile)
|
Cpuload, '/fmu/out/cpuload', self.cpu_load_callback, qos_profile=qos_profile)
|
||||||
|
self.vehicle_odometry_subscriber = self.create_subscription(
|
||||||
|
VehicleOdometry, "/fmu/out/vehicle_odometry", self.vehicle_odometry_callback, qos_profile=qos_profile)
|
||||||
# publish every 0.5 seconds
|
# publish every 0.5 seconds
|
||||||
self.timer = self.create_timer(0.5, self.publish_status)
|
self.timer = self.create_timer(0.5, self.publish_status)
|
||||||
self.armed = False
|
self.armed = False
|
||||||
|
self.height = 0.0
|
||||||
self.control_mode = "attitude"
|
self.control_mode = "attitude"
|
||||||
self.battery_percentage = 100.0
|
self.battery_percentage = 100.0
|
||||||
self.cpu_usage = 0.0
|
self.cpu_usage = 0.0
|
||||||
self.route_setpoint = 0
|
self.route_setpoint = 0
|
||||||
|
self.position = []
|
||||||
|
self.velocity = []
|
||||||
|
|
||||||
def publish_status(self):
|
def publish_status(self):
|
||||||
msg = DroneStatus()
|
msg = DroneStatus()
|
||||||
@@ -51,10 +59,14 @@ class DroneStatusNode(Node):
|
|||||||
msg.battery_percentage = self.battery_percentage
|
msg.battery_percentage = self.battery_percentage
|
||||||
msg.cpu_usage = self.cpu_usage
|
msg.cpu_usage = self.cpu_usage
|
||||||
msg.route_setpoint = self.route_setpoint
|
msg.route_setpoint = self.route_setpoint
|
||||||
|
msg.position = self.position
|
||||||
|
msg.velocity = self.velocity
|
||||||
|
msg.height = self.height
|
||||||
self.publisher.publish(msg)
|
self.publisher.publish(msg)
|
||||||
self.get_logger().info('Publishing status message')
|
# self.get_logger().info('Publishing status message')
|
||||||
|
|
||||||
def control_mode_callback(self, msg):
|
def control_mode_callback(self, msg):
|
||||||
|
self.get_logger().info('Got control mode callback!')
|
||||||
if msg.control_mode == CONTROL_MODE_ATTITUDE:
|
if msg.control_mode == CONTROL_MODE_ATTITUDE:
|
||||||
self.control_mode = "attitude"
|
self.control_mode = "attitude"
|
||||||
elif msg.control_mode == CONTROL_MODE_VELOCITY:
|
elif msg.control_mode == CONTROL_MODE_VELOCITY:
|
||||||
@@ -64,7 +76,13 @@ class DroneStatusNode(Node):
|
|||||||
else:
|
else:
|
||||||
self.control_mode = "unknown"
|
self.control_mode = "unknown"
|
||||||
|
|
||||||
|
def height_data_callback(self, msg):
|
||||||
|
self.height = msg.min_height
|
||||||
|
|
||||||
def arm_status_callback(self, msg):
|
def arm_status_callback(self, msg):
|
||||||
|
self.get_logger().info("Got arm status callback!")
|
||||||
|
if msg.armed:
|
||||||
|
self.get_logger().info("DRONE IS ARMED")
|
||||||
self.armed = msg.armed
|
self.armed = msg.armed
|
||||||
|
|
||||||
def route_status_callback(self, msg):
|
def route_status_callback(self, msg):
|
||||||
@@ -76,6 +94,11 @@ class DroneStatusNode(Node):
|
|||||||
def cpu_load_callback(self, msg):
|
def cpu_load_callback(self, msg):
|
||||||
self.cpu_usage = msg.load
|
self.cpu_usage = msg.load
|
||||||
|
|
||||||
|
def vehicle_odometry_callback(self, msg):
|
||||||
|
self.position = msg.position
|
||||||
|
self.velocity = msg.velocity
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main(args=None):
|
def main(args=None):
|
||||||
rclpy.init(args=args)
|
rclpy.init(args=args)
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ from drone_services.srv import EnableFailsafe
|
|||||||
from drone_services.msg import FailsafeMsg
|
from drone_services.msg import FailsafeMsg
|
||||||
class FailSafe(Node):
|
class FailSafe(Node):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().init("failsafe")
|
super().__init__("failsafe")
|
||||||
self.failsafe_enabled = False
|
self.failsafe_enabled = False
|
||||||
self.failsafe_msg = ""
|
self.failsafe_msg = ""
|
||||||
self.get_logger().info("Failsafe node started")
|
self.get_logger().info("Failsafe node started")
|
||||||
# create service on /drone/failsafe topic
|
|
||||||
self.service = self.create_service(
|
self.service = self.create_service(
|
||||||
EnableFailsafe, "/drone/enable_failsafe", self.failsafe_callback)
|
EnableFailsafe, "/drone/enable_failsafe", self.failsafe_callback)
|
||||||
self.failsafe_publisher = self.create_publisher(FailsafeMsg, "/drone/failsafe", 10)
|
self.failsafe_publisher = self.create_publisher(FailsafeMsg, "/drone/failsafe", 10)
|
||||||
@@ -19,14 +18,14 @@ class FailSafe(Node):
|
|||||||
self.failsafe_msg = request.message
|
self.failsafe_msg = request.message
|
||||||
response.enabled = self.failsafe_enabled
|
response.enabled = self.failsafe_enabled
|
||||||
response.message = self.failsafe_msg
|
response.message = self.failsafe_msg
|
||||||
self.get_logger().info("Failsafe triggered")
|
self.get_logger().info("Failsafe triggered! Message: " + self.failsafe_msg)
|
||||||
self.publish_failsafe()
|
self.publish_failsafe()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def publish_failsafe(self):
|
def publish_failsafe(self):
|
||||||
msg = FailsafeMsg()
|
msg = FailsafeMsg()
|
||||||
msg.enabled = self.failsafe_enabled
|
msg.enabled = self.failsafe_enabled
|
||||||
msg.message = self.failsafe_msg
|
msg.msg = self.failsafe_msg
|
||||||
self.failsafe_publisher.publish(msg)
|
self.failsafe_publisher.publish(msg)
|
||||||
self.get_logger().info("Publishing failsafe message")
|
self.get_logger().info("Publishing failsafe message")
|
||||||
|
|
||||||
@@ -34,10 +33,9 @@ class FailSafe(Node):
|
|||||||
def main(args=None):
|
def main(args=None):
|
||||||
rclpy.init(args=args)
|
rclpy.init(args=args)
|
||||||
failsafe_node = FailSafe()
|
failsafe_node = FailSafe()
|
||||||
failsafe_node.spin()
|
rclpy.spin(failsafe_node)
|
||||||
failsafe_node.destroy_node()
|
failsafe_node.destroy_node()
|
||||||
rclpy.shutdown()
|
rclpy.shutdown()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -8,10 +8,6 @@
|
|||||||
<license>Apache License 2.0</license>
|
<license>Apache License 2.0</license>
|
||||||
<depend>rclpy</depend>
|
<depend>rclpy</depend>
|
||||||
<depend>drone_services</depend>
|
<depend>drone_services</depend>
|
||||||
|
|
||||||
<test_depend>ament_copyright</test_depend>
|
|
||||||
<test_depend>ament_flake8</test_depend>
|
|
||||||
<test_depend>ament_pep257</test_depend>
|
|
||||||
<test_depend>python3-pytest</test_depend>
|
<test_depend>python3-pytest</test_depend>
|
||||||
|
|
||||||
<export>
|
<export>
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
# Copyright 2015 Open Source Robotics Foundation, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
from ament_copyright.main import main
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.copyright
|
|
||||||
@pytest.mark.linter
|
|
||||||
def test_copyright():
|
|
||||||
rc = main(argv=['.', 'test'])
|
|
||||||
assert rc == 0, 'Found errors'
|
|
||||||
86
src/failsafe/test/test_failsafe.py
Normal file
86
src/failsafe/test/test_failsafe.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
import time
|
||||||
|
|
||||||
|
import launch
|
||||||
|
import launch_ros
|
||||||
|
import launch_ros.actions
|
||||||
|
import launch_testing.actions
|
||||||
|
import pytest
|
||||||
|
import rclpy
|
||||||
|
|
||||||
|
from drone_services.srv import EnableFailsafe
|
||||||
|
from drone_services.msg import FailsafeMsg
|
||||||
|
|
||||||
|
# launch node
|
||||||
|
@pytest.mark.rostest
|
||||||
|
def generate_test_description():
|
||||||
|
file_path = os.path.dirname(__file__)
|
||||||
|
failsafe_node = launch_ros.actions.Node(
|
||||||
|
executable=sys.executable,
|
||||||
|
arguments=[os.path.join(file_path, '..', 'failsafe', 'failsafe.py')],
|
||||||
|
additional_env={'PYTHONUNBUFFERED': '1'}
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
launch.LaunchDescription([
|
||||||
|
failsafe_node,
|
||||||
|
launch_testing.actions.ReadyToTest(),
|
||||||
|
]),
|
||||||
|
{
|
||||||
|
'failsafe_node': failsafe_node,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
class FailsafeUnitTest(unittest.TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
rclpy.init()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
rclpy.shutdown()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.node = rclpy.create_node('test_failsafe')
|
||||||
|
self.service_called = False
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.node.destroy_node()
|
||||||
|
|
||||||
|
def service_call_callback(self,future):
|
||||||
|
self.assertIsNotNone(future.result())
|
||||||
|
self.assertTrue(future.result().enabled)
|
||||||
|
self.assertEqual(future.result().message, "test")
|
||||||
|
self.service_called = True
|
||||||
|
|
||||||
|
def test_failsafe_node_enables(self,failsafe_node,proc_output):
|
||||||
|
failsafe_msgs = []
|
||||||
|
failsafe_subscription = self.node.create_subscription(FailsafeMsg, "/drone/failsafe", lambda msg: failsafe_msgs.append(msg), 10)
|
||||||
|
failsafe_client = self.node.create_client(EnableFailsafe, "/drone/enable_failsafe")
|
||||||
|
while not failsafe_client.wait_for_service(timeout_sec=1.0):
|
||||||
|
self.node.get_logger().info("service not available, waiting again...")
|
||||||
|
request = EnableFailsafe.Request()
|
||||||
|
request.message = "test"
|
||||||
|
|
||||||
|
end_time = time.time() + 10.0
|
||||||
|
|
||||||
|
try:
|
||||||
|
while time.time() < end_time:
|
||||||
|
rclpy.spin_once(self.node, timeout_sec=0.1)
|
||||||
|
if (not self.service_called):
|
||||||
|
future = failsafe_client.call_async(request)
|
||||||
|
future.add_done_callback(self.service_call_callback)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
self.assertTrue(failsafe_msgs[0].enabled)
|
||||||
|
self.assertEqual(failsafe_msgs[0].msg, "test")
|
||||||
|
self.assertTrue(self.service_called)
|
||||||
|
finally:
|
||||||
|
self.node.destroy_subscription(failsafe_subscription)
|
||||||
|
self.node.destroy_client(failsafe_client)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Copyright 2017 Open Source Robotics Foundation, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
from ament_flake8.main import main_with_errors
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.flake8
|
|
||||||
@pytest.mark.linter
|
|
||||||
def test_flake8():
|
|
||||||
rc, errors = main_with_errors(argv=[])
|
|
||||||
assert rc == 0, \
|
|
||||||
'Found %d code style errors / warnings:\n' % len(errors) + \
|
|
||||||
'\n'.join(errors)
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Copyright 2015 Open Source Robotics Foundation, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
from ament_pep257.main import main
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.linter
|
|
||||||
@pytest.mark.pep257
|
|
||||||
def test_pep257():
|
|
||||||
rc = main(argv=['.', 'test'])
|
|
||||||
assert rc == 0, 'Found code style errors / warnings'
|
|
||||||
@@ -60,8 +60,12 @@ install(
|
|||||||
DESTINATION share/${PROJECT_NAME}
|
DESTINATION share/${PROJECT_NAME}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
install(FILES
|
||||||
|
test/test_failsafe_enabled.py
|
||||||
|
DESTINATION lib/${PROJECT_NAME})
|
||||||
|
|
||||||
if(BUILD_TESTING)
|
if(BUILD_TESTING)
|
||||||
find_package(ament_lint_auto REQUIRED)
|
# find_package(ament_lint_auto REQUIRED)
|
||||||
|
|
||||||
# the following line skips the linter which checks for copyrights
|
# the following line skips the linter which checks for copyrights
|
||||||
# uncomment the line when a copyright and license is not present in all source files
|
# uncomment the line when a copyright and license is not present in all source files
|
||||||
@@ -69,7 +73,9 @@ if(BUILD_TESTING)
|
|||||||
# the following line skips cpplint (only works in a git repo)
|
# the following line skips cpplint (only works in a git repo)
|
||||||
# uncomment the line when this package is not in a git repo
|
# uncomment the line when this package is not in a git repo
|
||||||
# set(ament_cmake_cpplint_FOUND TRUE)
|
# set(ament_cmake_cpplint_FOUND TRUE)
|
||||||
ament_lint_auto_find_test_dependencies()
|
# ament_lint_auto_find_test_dependencies()
|
||||||
|
find_package(launch_testing_ament_cmake REQUIRED)
|
||||||
|
add_launch_test(test/test_failsafe_enabled.py)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
ament_package()
|
ament_package()
|
||||||
|
|||||||
@@ -15,9 +15,6 @@
|
|||||||
<depend>drone_services</depend>
|
<depend>drone_services</depend>
|
||||||
<depend>std_srvs</depend>
|
<depend>std_srvs</depend>
|
||||||
|
|
||||||
<test_depend>ament_lint_auto</test_depend>
|
|
||||||
<test_depend>ament_lint_common</test_depend>
|
|
||||||
|
|
||||||
<export>
|
<export>
|
||||||
<build_type>ament_cmake</build_type>
|
<build_type>ament_cmake</build_type>
|
||||||
</export>
|
</export>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public:
|
|||||||
// create a publisher on the offboard control mode topic
|
// create a publisher on the offboard control mode topic
|
||||||
offboard_control_mode_publisher_ = this->create_publisher<px4_msgs::msg::OffboardControlMode>("/fmu/in/offboard_control_mode", 10);
|
offboard_control_mode_publisher_ = this->create_publisher<px4_msgs::msg::OffboardControlMode>("/fmu/in/offboard_control_mode", 10);
|
||||||
// create timer to send heartbeat messages (offboard control) every 100ms
|
// create timer to send heartbeat messages (offboard control) every 100ms
|
||||||
timer_ = this->create_wall_timer(100ms, std::bind(&HeartBeat::send_heartbeat, this));
|
timer_ = this->create_wall_timer(10ms, std::bind(&HeartBeat::send_heartbeat, this));
|
||||||
start_time = this->get_clock()->now().seconds();
|
start_time = this->get_clock()->now().seconds();
|
||||||
RCLCPP_INFO(this->get_logger(), "done initializing at %d seconds. Sending heartbeat...", start_time);
|
RCLCPP_INFO(this->get_logger(), "done initializing at %d seconds. Sending heartbeat...", start_time);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ public:
|
|||||||
vehicle_local_position_subscription_ = this->create_subscription<px4_msgs::msg::VehicleLocalPosition>("/fmu/out/vehicle_local_position", qos, std::bind(&PX4Controller::on_local_position_receive, this, std::placeholders::_1));
|
vehicle_local_position_subscription_ = this->create_subscription<px4_msgs::msg::VehicleLocalPosition>("/fmu/out/vehicle_local_position", qos, std::bind(&PX4Controller::on_local_position_receive, this, std::placeholders::_1));
|
||||||
// subscription on receiving a new control mode
|
// subscription on receiving a new control mode
|
||||||
control_mode_subscription_ = this->create_subscription<drone_services::msg::DroneControlMode>("/drone/control_mode", qos, std::bind(&PX4Controller::on_control_mode_receive, this, std::placeholders::_1));
|
control_mode_subscription_ = this->create_subscription<drone_services::msg::DroneControlMode>("/drone/control_mode", qos, std::bind(&PX4Controller::on_control_mode_receive, this, std::placeholders::_1));
|
||||||
failsafe_subscription = this->create_subscription<drone_services::msg::FailsafeMsg>("/drone/failsafe", qos, std::bind(&PX4Controller::on_failsafe_receive, this, std::placeholders::_1));
|
failsafe_subscription_ = this->create_subscription<drone_services::msg::FailsafeMsg>("/drone/failsafe", qos, std::bind(&PX4Controller::on_failsafe_receive, this, std::placeholders::_1));
|
||||||
// services for controlling the drone
|
// services for controlling the drone
|
||||||
set_attitude_service_ = this->create_service<drone_services::srv::SetAttitude>("/drone/set_attitude", std::bind(&PX4Controller::handle_attitude_setpoint, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
|
set_attitude_service_ = this->create_service<drone_services::srv::SetAttitude>("/drone/set_attitude", std::bind(&PX4Controller::handle_attitude_setpoint, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
|
||||||
set_trajectory_service_ = this->create_service<drone_services::srv::SetTrajectory>("/drone/set_trajectory", std::bind(&PX4Controller::handle_trajectory_setpoint, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
|
set_trajectory_service_ = this->create_service<drone_services::srv::SetTrajectory>("/drone/set_trajectory", std::bind(&PX4Controller::handle_trajectory_setpoint, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
|
||||||
@@ -102,18 +102,17 @@ private:
|
|||||||
|
|
||||||
char current_control_mode = CONTROL_MODE_ATTITUDE; // start with attitude control
|
char current_control_mode = CONTROL_MODE_ATTITUDE; // start with attitude control
|
||||||
|
|
||||||
const float omega = 0.3; // angular speed of the POLAR trajectory
|
const float omega = 0.3; // angular speed of the POLAR trajectory
|
||||||
const float K = 0; // [m] gain that regulates the spiral pitch
|
const float K = 0; // [m] gain that regulates the spiral pitch
|
||||||
|
|
||||||
float rho_0 = 0; // initial rho of polar coordinate
|
float rho_0 = 0; // initial rho of polar coordinate
|
||||||
float theta_0 = 0; // initial theta of polar coordinate
|
float theta_0 = 0; // initial theta of polar coordinate
|
||||||
float p0_z = -0.0; // initial height
|
float p0_z = -0.0; // initial height
|
||||||
float des_x = p0_x, des_y = p0_y, des_z = p0_z; // desired position
|
float des_x = 0.0, des_y = 0.0, des_z = p0_z; // desired position
|
||||||
float dot_des_x = 0.0, dot_des_y = 0.0; // desired velocity
|
float gamma = M_PI_4; // desired heading direction
|
||||||
float gamma = M_PI_4; // desired heading direction
|
|
||||||
|
|
||||||
float local_x = 0; // local position x
|
float local_x = 0; // local position x
|
||||||
float local_y = 0; // local position y
|
float local_y = 0; // local position y
|
||||||
|
|
||||||
bool failsafe_enabled = false;
|
bool failsafe_enabled = false;
|
||||||
|
|
||||||
@@ -131,6 +130,12 @@ private:
|
|||||||
const std::shared_ptr<drone_services::srv::SetAttitude::Request> request,
|
const std::shared_ptr<drone_services::srv::SetAttitude::Request> request,
|
||||||
const std::shared_ptr<drone_services::srv::SetAttitude::Response> response)
|
const std::shared_ptr<drone_services::srv::SetAttitude::Response> response)
|
||||||
{
|
{
|
||||||
|
if (this->failsafe_enabled)
|
||||||
|
{
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Failsafe enabled, ignoring attitude setpoint");
|
||||||
|
response->success = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (armed)
|
if (armed)
|
||||||
{
|
{
|
||||||
if (request->yaw == 0 && request->pitch == 0 && request->roll == 0 && request->thrust == 0)
|
if (request->yaw == 0 && request->pitch == 0 && request->roll == 0 && request->thrust == 0)
|
||||||
@@ -180,6 +185,12 @@ private:
|
|||||||
const std::shared_ptr<drone_services::srv::SetTrajectory::Request> request,
|
const std::shared_ptr<drone_services::srv::SetTrajectory::Request> request,
|
||||||
const std::shared_ptr<drone_services::srv::SetTrajectory::Response> response)
|
const std::shared_ptr<drone_services::srv::SetTrajectory::Response> response)
|
||||||
{
|
{
|
||||||
|
if (this->failsafe_enabled)
|
||||||
|
{
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Failsafe enabled, ignoring trajectory setpoint");
|
||||||
|
response->success = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!(request->control_mode == CONTROL_MODE_VELOCITY || request->control_mode == CONTROL_MODE_POSITION))
|
if (!(request->control_mode == CONTROL_MODE_VELOCITY || request->control_mode == CONTROL_MODE_POSITION))
|
||||||
{
|
{
|
||||||
RCLCPP_INFO(this->get_logger(), "Got invalid trajectory control mode: %d", request->control_mode);
|
RCLCPP_INFO(this->get_logger(), "Got invalid trajectory control mode: %d", request->control_mode);
|
||||||
@@ -232,6 +243,7 @@ private:
|
|||||||
user_in_control = false;
|
user_in_control = false;
|
||||||
publish_vehicle_command(px4_msgs::msg::VehicleCommand::VEHICLE_CMD_COMPONENT_ARM_DISARM, 0.0, 0);
|
publish_vehicle_command(px4_msgs::msg::VehicleCommand::VEHICLE_CMD_COMPONENT_ARM_DISARM, 0.0, 0);
|
||||||
|
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Publishing disarm status message");
|
||||||
auto msg = drone_services::msg::DroneArmStatus();
|
auto msg = drone_services::msg::DroneArmStatus();
|
||||||
msg.armed = false;
|
msg.armed = false;
|
||||||
arm_status_publisher_->publish(msg);
|
arm_status_publisher_->publish(msg);
|
||||||
@@ -258,7 +270,11 @@ private:
|
|||||||
const std::shared_ptr<drone_services::srv::ArmDrone::Response> response)
|
const std::shared_ptr<drone_services::srv::ArmDrone::Response> response)
|
||||||
{
|
{
|
||||||
RCLCPP_INFO(this->get_logger(), "Got arm request...");
|
RCLCPP_INFO(this->get_logger(), "Got arm request...");
|
||||||
|
if (this->failsafe_enabled)
|
||||||
|
{
|
||||||
|
response->success = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!armed)
|
if (!armed)
|
||||||
{
|
{
|
||||||
this->publish_vehicle_command(px4_msgs::msg::VehicleCommand::VEHICLE_CMD_DO_SET_MODE, 1, 6);
|
this->publish_vehicle_command(px4_msgs::msg::VehicleCommand::VEHICLE_CMD_DO_SET_MODE, 1, 6);
|
||||||
@@ -270,6 +286,7 @@ private:
|
|||||||
this->last_thrust = -0.1; // spin motors at 10% thrust
|
this->last_thrust = -0.1; // spin motors at 10% thrust
|
||||||
armed = true;
|
armed = true;
|
||||||
|
|
||||||
|
RCLCPP_INFO(this->get_logger(), "Publishing arm status message");
|
||||||
auto msg = drone_services::msg::DroneArmStatus();
|
auto msg = drone_services::msg::DroneArmStatus();
|
||||||
msg.armed = true;
|
msg.armed = true;
|
||||||
arm_status_publisher_->publish(msg);
|
arm_status_publisher_->publish(msg);
|
||||||
@@ -343,7 +360,6 @@ private:
|
|||||||
RCLCPP_INFO(this->get_logger(), "Sending position setpoint: %f %f %f", des_x, des_y, des_z);
|
RCLCPP_INFO(this->get_logger(), "Sending position setpoint: %f %f %f", des_x, des_y, des_z);
|
||||||
RCLCPP_INFO(this->get_logger(), "local position: %f %f", local_x, local_y);
|
RCLCPP_INFO(this->get_logger(), "local position: %f %f", local_x, local_y);
|
||||||
msg.position = {local_x, local_y, des_z};
|
msg.position = {local_x, local_y, des_z};
|
||||||
msg.velocity = {dot_des_x, dot_des_y, 0.0};
|
|
||||||
msg.yaw = gamma; //-3.14; // [-PI:PI]
|
msg.yaw = gamma; //-3.14; // [-PI:PI]
|
||||||
|
|
||||||
msg.timestamp = this->get_clock()->now().nanoseconds() / 1000;
|
msg.timestamp = this->get_clock()->now().nanoseconds() / 1000;
|
||||||
@@ -391,13 +407,6 @@ private:
|
|||||||
des_y = rho * sin(theta);
|
des_y = rho * sin(theta);
|
||||||
des_z = position[2]; // the z position can be set to the received height
|
des_z = position[2]; // the z position can be set to the received height
|
||||||
|
|
||||||
// velocity computation
|
|
||||||
float dot_rho = K * omega;
|
|
||||||
dot_des_x = dot_rho * cos(theta) - rho * sin(theta) * omega;
|
|
||||||
dot_des_y = dot_rho * sin(theta) + rho * cos(theta) * omega;
|
|
||||||
// desired heading direction
|
|
||||||
gamma = atan2(dot_des_y, dot_des_x);
|
|
||||||
|
|
||||||
if (!user_in_control)
|
if (!user_in_control)
|
||||||
{
|
{
|
||||||
// RCLCPP_INFO(this->get_logger(), "Sending idle attitude setpoint");
|
// RCLCPP_INFO(this->get_logger(), "Sending idle attitude setpoint");
|
||||||
@@ -491,7 +500,12 @@ private:
|
|||||||
RCLCPP_INFO(this->get_logger(), "Control mode set to %d", current_control_mode);
|
RCLCPP_INFO(this->get_logger(), "Control mode set to %d", current_control_mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
void on_failsafe_received(const drone_services::msg::FailsafeMsg::SharedPtr msg)
|
/**
|
||||||
|
* @brief Callback function for receiving failsafe messages
|
||||||
|
*
|
||||||
|
* @param msg the message indicating that the failsafe was enabled
|
||||||
|
*/
|
||||||
|
void on_failsafe_receive(const drone_services::msg::FailsafeMsg::SharedPtr msg)
|
||||||
{
|
{
|
||||||
if (msg->enabled)
|
if (msg->enabled)
|
||||||
{
|
{
|
||||||
|
|||||||
173
src/px4_connection/test/test_failsafe_enabled.py
Normal file
173
src/px4_connection/test/test_failsafe_enabled.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import launch
|
||||||
|
import launch_ros
|
||||||
|
import launch_ros.actions
|
||||||
|
import launch_testing.actions
|
||||||
|
import pytest
|
||||||
|
import rclpy
|
||||||
|
import time
|
||||||
|
|
||||||
|
from drone_services.srv import EnableFailsafe
|
||||||
|
from drone_services.srv import SetTrajectory
|
||||||
|
from drone_services.srv import SetAttitude
|
||||||
|
from drone_services.srv import ArmDrone
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.rostest
|
||||||
|
def generate_test_description():
|
||||||
|
file_path = os.path.dirname(__file__)
|
||||||
|
px4_controller_node = launch_ros.actions.Node(
|
||||||
|
package='px4_connection', executable='px4_controller')
|
||||||
|
failsafe_node = launch_ros.actions.Node(
|
||||||
|
package='failsafe', executable='failsafe')
|
||||||
|
|
||||||
|
return (
|
||||||
|
launch.LaunchDescription([
|
||||||
|
px4_controller_node,
|
||||||
|
failsafe_node,
|
||||||
|
launch_testing.actions.ReadyToTest(),
|
||||||
|
]),
|
||||||
|
{
|
||||||
|
'px4_controller_node': px4_controller_node,
|
||||||
|
'failsafe_node': failsafe_node,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPx4Failsafe(unittest.TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
rclpy.init()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
rclpy.shutdown()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.node = rclpy.create_node('test_px4_failsafe')
|
||||||
|
self.called_failsafe_service = False
|
||||||
|
self.called_trajectory_service = False
|
||||||
|
self.called_arm_service = False
|
||||||
|
self.called_attitude_service = False
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.node.destroy_node()
|
||||||
|
|
||||||
|
def failsafe_service_callback(self,future):
|
||||||
|
self.called_failsafe_service = True
|
||||||
|
|
||||||
|
def trajectory_service_callback(self,future):
|
||||||
|
self.called_trajectory_service = True
|
||||||
|
self.assertFalse(future.result().success)
|
||||||
|
|
||||||
|
def attitude_service_callback(self,future):
|
||||||
|
self.called_attitude_service = True
|
||||||
|
self.assertFalse(future.result().success)
|
||||||
|
|
||||||
|
def arm_service_callback(self,future):
|
||||||
|
self.called_arm_service = True
|
||||||
|
self.assertFalse(future.result().success)
|
||||||
|
|
||||||
|
def test_failsafe_enabled_set_trajectory_returns_false(self, px4_controller_node, proc_output):
|
||||||
|
self.called_failsafe_service = False
|
||||||
|
failsafe_client = self.node.create_client(EnableFailsafe, '/drone/enable_failsafe')
|
||||||
|
set_trajectory_client = self.node.create_client(SetTrajectory, '/drone/set_trajectory')
|
||||||
|
while not failsafe_client.wait_for_service(timeout_sec=1.0):
|
||||||
|
self.node.get_logger().info('failsafe service not available, waiting again...')
|
||||||
|
while not set_trajectory_client.wait_for_service(timeout_sec=1.0):
|
||||||
|
self.node.get_logger().info('set_trajectory service not available, waiting again...')
|
||||||
|
failsafe_request = EnableFailsafe.Request()
|
||||||
|
failsafe_request.message = "test"
|
||||||
|
set_trajectory_request = SetTrajectory.Request()
|
||||||
|
set_trajectory_request.control_mode = 2
|
||||||
|
set_trajectory_request.values = [1.0,1.0,1.0]
|
||||||
|
set_trajectory_request.yaw = 0.0
|
||||||
|
|
||||||
|
end_time = time.time() + 10.0
|
||||||
|
try:
|
||||||
|
while time.time() < end_time:
|
||||||
|
rclpy.spin_once(self.node, timeout_sec=0.1)
|
||||||
|
if not self.called_failsafe_service:
|
||||||
|
failsafe_future = failsafe_client.call_async(failsafe_request)
|
||||||
|
failsafe_future.add_done_callback(self.failsafe_service_callback)
|
||||||
|
elif not self.called_trajectory_service:
|
||||||
|
trajectory_future = set_trajectory_client.call_async(set_trajectory_request)
|
||||||
|
trajectory_future.add_done_callback(self.trajectory_service_callback)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
self.assertTrue(self.called_failsafe_service, "Failsafe service was not called")
|
||||||
|
self.assertTrue(self.called_trajectory_service, "Trajectory service was not called")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.node.destroy_client(failsafe_client)
|
||||||
|
self.node.destroy_client(set_trajectory_client)
|
||||||
|
|
||||||
|
|
||||||
|
def test_failsafe_enabled_set_attitude_returns_false(self, px4_controller_node, proc_output):
|
||||||
|
self.called_failsafe_service = False
|
||||||
|
failsafe_client = self.node.create_client(EnableFailsafe, '/drone/enable_failsafe')
|
||||||
|
set_attitude_client = self.node.create_client(SetAttitude, '/drone/set_attitude')
|
||||||
|
while not failsafe_client.wait_for_service(timeout_sec=1.0):
|
||||||
|
self.node.get_logger().info('failsafe not available, waiting again...')
|
||||||
|
while not set_attitude_client.wait_for_service(timeout_sec=1.0):
|
||||||
|
self.node.get_logger().info('attitude not available, waiting again...')
|
||||||
|
failsafe_request = EnableFailsafe.Request()
|
||||||
|
failsafe_request.message = "test"
|
||||||
|
set_attitude_request = SetAttitude.Request()
|
||||||
|
set_attitude_request.pitch = 1.0
|
||||||
|
set_attitude_request.yaw = 1.0
|
||||||
|
set_attitude_request.roll = 1.0
|
||||||
|
set_attitude_request.thrust = 0.5
|
||||||
|
|
||||||
|
end_time = time.time() + 10.0
|
||||||
|
try:
|
||||||
|
while time.time() < end_time:
|
||||||
|
rclpy.spin_once(self.node, timeout_sec=0.1)
|
||||||
|
if not self.called_failsafe_service:
|
||||||
|
failsafe_future = failsafe_client.call_async(failsafe_request)
|
||||||
|
failsafe_future.add_done_callback(self.failsafe_service_callback)
|
||||||
|
elif not self.called_attitude_service:
|
||||||
|
attitude_future = set_attitude_client.call_async(set_attitude_request)
|
||||||
|
attitude_future.add_done_callback(self.attitude_service_callback)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
self.assertTrue(self.called_failsafe_service, "Failsafe service was not called")
|
||||||
|
self.assertTrue(self.called_attitude_service, "Attitude service was not called")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.node.destroy_client(failsafe_client)
|
||||||
|
self.node.destroy_client(set_attitude_client)
|
||||||
|
|
||||||
|
def test_failsafe_enabled_arm_returns_false(self, px4_controller_node, proc_output):
|
||||||
|
self.called_failsafe_service = False
|
||||||
|
failsafe_client = self.node.create_client(EnableFailsafe, '/drone/enable_failsafe')
|
||||||
|
arm_client = self.node.create_client(ArmDrone, '/drone/arm')
|
||||||
|
while not failsafe_client.wait_for_service(timeout_sec=1.0):
|
||||||
|
self.node.get_logger().info('failsafe not available, waiting again...')
|
||||||
|
while not arm_client.wait_for_service(timeout_sec=1.0):
|
||||||
|
self.node.get_logger().info('arm not available, waiting again...')
|
||||||
|
failsafe_request = EnableFailsafe.Request()
|
||||||
|
failsafe_request.message = "test"
|
||||||
|
arm_request = ArmDrone.Request()
|
||||||
|
|
||||||
|
end_time = time.time() + 10.0
|
||||||
|
try:
|
||||||
|
while time.time() < end_time:
|
||||||
|
rclpy.spin_once(self.node, timeout_sec=0.1)
|
||||||
|
if not self.called_failsafe_service:
|
||||||
|
failsafe_future = failsafe_client.call_async(failsafe_request)
|
||||||
|
failsafe_future.add_done_callback(self.failsafe_service_callback)
|
||||||
|
elif not self.called_arm_service:
|
||||||
|
arm_future = arm_client.call_async(arm_request)
|
||||||
|
arm_future.add_done_callback(self.arm_service_callback)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
self.assertTrue(self.called_failsafe_service, "Failsafe service was not called")
|
||||||
|
self.assertTrue(self.called_arm_service, "Arm service was not called")
|
||||||
|
finally:
|
||||||
|
self.node.destroy_client(failsafe_client)
|
||||||
|
self.node.destroy_client(arm_client)
|
||||||
103
src/px4_connection/test/test_heartbeat_control_mode.py
Normal file
103
src/px4_connection/test/test_heartbeat_control_mode.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import launch
|
||||||
|
import launch_ros
|
||||||
|
import launch_ros.actions
|
||||||
|
import launch_testing.actions
|
||||||
|
import pytest
|
||||||
|
import rclpy
|
||||||
|
|
||||||
|
from drone_services.srv import SetVehicleControl
|
||||||
|
from drone_services.msg import DroneControlMode
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.rostest
|
||||||
|
def generate_test_description():
|
||||||
|
heartbeat_node = launch_ros.actions.Node(
|
||||||
|
package='px4_connection', executable='heartbeat')
|
||||||
|
|
||||||
|
return (
|
||||||
|
launch.LaunchDescription([
|
||||||
|
heartbeat_node,
|
||||||
|
launch_testing.actions.ReadyToTest(),
|
||||||
|
]),
|
||||||
|
{
|
||||||
|
'heartbeat_node': heartbeat_node,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
class TestHeartbeatControlMode(unittest.TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
rclpy.init()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
rclpy.shutdown()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.node = rclpy.create_node('test_heartbeat_control_mode')
|
||||||
|
self.called_control_mode_service = False
|
||||||
|
self.received_control_mode = False
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.node.destroy_node()
|
||||||
|
|
||||||
|
def control_mode_callback(self,msg):
|
||||||
|
self.received_control_mode = True
|
||||||
|
self.assertEqual(msg.control_mode,1)
|
||||||
|
|
||||||
|
def heartbeat_service_callback_correct(self,future):
|
||||||
|
self.called_control_mode_service = True
|
||||||
|
self.assertIsNotNone(future.result())
|
||||||
|
self.assertTrue(future.result().success)
|
||||||
|
|
||||||
|
def heartbeat_service_callback_wrong(self,future):
|
||||||
|
self.called_control_mode_service = True
|
||||||
|
self.assertIsNotNone(future.result())
|
||||||
|
self.assertFalse(future.result().success)
|
||||||
|
|
||||||
|
def test_set_vehicle_control_mode_proper_value(self,heartbeat_node,proc_output):
|
||||||
|
self.called_control_mode_service = False
|
||||||
|
heartbeat_client = self.node.create_client(SetVehicleControl, '/drone/set_vehicle_control')
|
||||||
|
while not heartbeat_client.wait_for_service(timeout_sec=1.0):
|
||||||
|
self.node.get_logger().info('heartbeat service not available, waiting again...')
|
||||||
|
heartbeat_sub = self.node.create_subscription(DroneControlMode,"/drone/control_mode",self.control_mode_callback,10)
|
||||||
|
request = SetVehicleControl.Request()
|
||||||
|
request.control= 4 # attitude control
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
rclpy.spin_once(self.node,timeout_sec=0.1)
|
||||||
|
if not self.called_control_mode_service:
|
||||||
|
vehicle_control_future = heartbeat_client.call_async(request)
|
||||||
|
vehicle_control_future.add_done_callback(self.heartbeat_service_callback_correct)
|
||||||
|
elif not self.received_control_mode:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
finally:
|
||||||
|
self.node.destroy_client(heartbeat_client)
|
||||||
|
|
||||||
|
def test_set_vehicle_control_mode_wrong_value(self,heartbeat_node,proc_output):
|
||||||
|
self.called_control_mode_service = False
|
||||||
|
heartbeat_client = self.node.create_client(SetVehicleControl, '/drone/set_vehicle_control')
|
||||||
|
while not heartbeat_client.wait_for_service(timeout_sec=1.0):
|
||||||
|
self.node.get_logger().info('heartbeat service not available, waiting again...')
|
||||||
|
request = SetVehicleControl.Request()
|
||||||
|
request.control = 33 # wrong control mode
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
rclpy.spin_once(self.node,timeout_sec=0.1)
|
||||||
|
if not self.called_control_mode_service:
|
||||||
|
vehicle_control_future = heartbeat_client.call_async(request)
|
||||||
|
vehicle_control_future.add_done_callback(self.heartbeat_service_callback_wrong)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
self.node.destroy_client(heartbeat_client)
|
||||||
46
test_stream.py
Normal file
46
test_stream.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import cv2
|
||||||
|
import requests
|
||||||
|
|
||||||
|
video_url = "http://10.1.1.41:8080/video"
|
||||||
|
# Set the headers for the POST request
|
||||||
|
headers = {'Content-Type': 'application/octet-stream'}
|
||||||
|
|
||||||
|
vid = cv2.VideoCapture(0)
|
||||||
|
|
||||||
|
# vid.set(cv2.CAP_PROP_FRAME_WIDTH, RES_4K_W)
|
||||||
|
# vid.set(cv2.CAP_PROP_FRAME_HEIGHT, RES_4K_H)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
while vid.isOpened():
|
||||||
|
pass
|
||||||
|
ret, frame = vid.read()
|
||||||
|
|
||||||
|
if not ret:
|
||||||
|
# If reading the frame failed, break the loop
|
||||||
|
break
|
||||||
|
|
||||||
|
# Convert the frame to bytes
|
||||||
|
_, img_encoded = cv2.imencode('.jpg', frame)
|
||||||
|
frame_data = img_encoded.tobytes()
|
||||||
|
|
||||||
|
# Send the frame data as the request body
|
||||||
|
response = requests.post(video_url, data=frame_data, headers=headers)
|
||||||
|
|
||||||
|
# Check the response status
|
||||||
|
if response.status_code == 200:
|
||||||
|
print('Frame sent successfully.')
|
||||||
|
else:
|
||||||
|
print('Failed to send frame.')
|
||||||
|
# if self.websocket is not None:
|
||||||
|
# img,frame = vid.read()
|
||||||
|
# frame = cv2.resize(frame,(640,480))
|
||||||
|
# encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 65]
|
||||||
|
# man = cv2.imencode('.jpg', frame, encode_param)[1]
|
||||||
|
# self.get_logger().info('Sending video')
|
||||||
|
# asyncio.ensure_future(self.websocket.send(man.tobytes()),loop=self.event_loop)
|
||||||
|
# await asyncio.sleep(1)
|
||||||
|
# else:
|
||||||
|
# self.get_logger().info('No websocket connection')
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print('Something went wrong while reading and sending video: ' + str(e))
|
||||||
Reference in New Issue
Block a user