Compare commits
332 Commits
position_c
...
api
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93ac0ad866 | ||
|
|
953c7d6884 | ||
|
|
f671fa0535 | ||
|
|
30fe217df0 | ||
|
|
95b19be4be | ||
|
|
3d51438fa1 | ||
|
|
eece400e32 | ||
|
|
85188015e2 | ||
|
|
0eef99b793 | ||
|
|
e55a2cf53a | ||
|
|
b377155baa | ||
|
|
887d36a229 | ||
|
|
ac3c6860ce | ||
|
|
f3e84d5382 | ||
|
|
6074fb52c1 | ||
|
|
3629424089 | ||
|
|
6dcaaef638 | ||
|
|
8034702db0 | ||
|
|
9cf5eae78f | ||
|
|
8eea606db3 | ||
|
|
5b7f239f8f | ||
|
|
62c7e67e82 | ||
|
|
d451b78e96 | ||
|
|
21683cb92c | ||
|
|
f23e56a9a0 | ||
|
|
8d2bb7446f | ||
|
|
f56bd7f735 | ||
|
|
8cb03ec4e7 | ||
|
|
b7cba3e2ec | ||
|
|
d0d27552cf | ||
|
|
d986b96faf | ||
|
|
5c46b857e0 | ||
|
|
3d18a14dcf | ||
|
|
100c19db79 | ||
|
|
2155e4a8e0 | ||
|
|
1f3a2754ea | ||
|
|
7cee4c1543 | ||
|
|
e706a38ea4 | ||
|
|
bcc14c9e0d | ||
|
|
8cc120c10f | ||
|
|
2f9d45c79d | ||
|
|
2aff2bcf12 | ||
|
|
f0bb159c4b | ||
|
|
e139d111f6 | ||
|
|
89473e4612 | ||
|
|
87608ff637 | ||
|
|
da0a35f98f | ||
|
|
bf48ee04c3 | ||
|
|
473de62451 | ||
|
|
defd91b473 | ||
|
|
a3fb87ead3 | ||
|
|
cc78321421 | ||
|
|
4daf79cdc0 | ||
|
|
ca2049b0db | ||
|
|
d1e2b5960e | ||
|
|
fcf9f15fb3 | ||
|
|
17bfcc323c | ||
|
|
e3babad383 | ||
|
|
e31b0346bc | ||
|
|
032dc1dea9 | ||
|
|
6b21465a35 | ||
|
|
dd38be123f | ||
|
|
4138673893 | ||
|
|
ba0fdc7863 | ||
|
|
a7c22aa362 | ||
|
|
545119633c | ||
|
|
74353bb128 | ||
|
|
f50e2d0d57 | ||
|
|
b22d8228aa | ||
|
|
9826afbd49 | ||
|
|
07f2320e91 | ||
|
|
abbd8717a8 | ||
|
|
6569261ab2 | ||
|
|
d246d0b5f9 | ||
|
|
8a85857155 | ||
|
|
e48efb93e1 | ||
|
|
19787aa2ca | ||
|
|
35331e3714 | ||
|
|
bc55fbc757 | ||
|
|
94eae1d8a9 | ||
|
|
9a207dabcc | ||
|
|
328f439270 | ||
|
|
970d093a31 | ||
|
|
5707bb74c7 | ||
|
|
39929b6d58 | ||
|
|
4686a90e0a | ||
|
|
9dbd747295 | ||
|
|
a76b99789c | ||
|
|
f63814c6f2 | ||
|
|
e55da2375e | ||
|
|
4a82a3d602 | ||
|
|
b3c2505c0a | ||
|
|
7ca1452908 | ||
|
|
4ec2c8c79b | ||
|
|
56a6c579af | ||
|
|
7dfa1e77df | ||
|
|
d0b8f3dafa | ||
|
|
a740fa7c45 | ||
|
|
f846d31ac2 | ||
|
|
0fa5751c29 | ||
|
|
e141326733 | ||
|
|
7e8da56382 | ||
|
|
49bb33b275 | ||
|
|
e673846113 | ||
|
|
35ee9611e4 | ||
|
|
47b4ed15f8 | ||
|
|
7966bdec9e | ||
|
|
745b5bc4e9 | ||
|
|
4c61e4e1e2 | ||
|
|
cc9f4e7f5c | ||
|
|
a8facbf521 | ||
|
|
91d1762687 | ||
|
|
909504d305 | ||
|
|
ebf4264ae3 | ||
|
|
9b05ac0fa0 | ||
|
|
a2ffd58069 | ||
|
|
6f08d3ff32 | ||
|
|
4bbbaa2183 | ||
|
|
3ab0304473 | ||
|
|
1535e9e480 | ||
|
|
6f4a9cd7b2 | ||
|
|
355d31d97c | ||
|
|
8c6219965f | ||
|
|
c4451eede0 | ||
|
|
6550713780 | ||
|
|
d6f9f15205 | ||
|
|
d6020cf904 | ||
|
|
d3ac417f6f | ||
|
|
3012e611cd | ||
|
|
258e5cf289 | ||
|
|
1c855540ef | ||
|
|
3dea3e6d5d | ||
|
|
60d2aeaa7f | ||
|
|
d876a9ba24 | ||
|
|
ee79147c8c | ||
|
|
4998acc7d2 | ||
|
|
07e9357a1f | ||
|
|
b9270f7745 | ||
|
|
41209ceab0 | ||
|
|
231556683d | ||
|
|
842e3e4534 | ||
|
|
e808e93cf3 | ||
|
|
edde183c7b | ||
|
|
5ca202e8c0 | ||
|
|
902adb42f3 | ||
|
|
bcd4f891d2 | ||
|
|
8b2dfd27e2 | ||
|
|
096cee40be | ||
|
|
56b8f147c4 | ||
|
|
ef0c6222ea | ||
|
|
a725c593ff | ||
|
|
49be78f16c | ||
|
|
0aff792254 | ||
|
|
b4ae183829 | ||
|
|
7669514ff6 | ||
|
|
ab3a1725a3 | ||
|
|
45426bea1e | ||
|
|
b7037a3e6f | ||
|
|
cbf01d7875 | ||
|
|
0548955969 | ||
|
|
a6a57b9689 | ||
|
|
582e65126e | ||
|
|
c967eea3f2 | ||
|
|
4916584e13 | ||
|
|
561daf035c | ||
|
|
d80b20e05a | ||
|
|
b565832773 | ||
|
|
2b36a9a383 | ||
|
|
273d979beb | ||
|
|
267e818d66 | ||
|
|
508cbfe720 | ||
|
|
f4d57025a4 | ||
|
|
4684b7adb2 | ||
|
|
57f60fe44a | ||
|
|
98efc65b86 | ||
|
|
4ce6ff67da | ||
|
|
402fcbab96 | ||
|
|
9a255bad12 | ||
|
|
ce65da2ed2 | ||
|
|
e0ab883144 | ||
|
|
f29509e16d | ||
|
|
38c9266ceb | ||
|
|
2ac3f22ea7 | ||
|
|
5df44d0fb4 | ||
|
|
8db7fec055 | ||
|
|
0397c23a62 | ||
|
|
b2b8c2081f | ||
|
|
822f63aaa1 | ||
|
|
331d543300 | ||
|
|
fe086b2ef9 | ||
|
|
36a262dcb5 | ||
|
|
2c9b12cd8d | ||
|
|
8fca2086ac | ||
|
|
f30a51ca68 | ||
|
|
1a7efcfa23 | ||
|
|
50eafe6720 | ||
|
|
e2f4cde884 | ||
|
|
ca3fdcf760 | ||
|
|
49456cca65 | ||
|
|
86ec261e81 | ||
|
|
4182be2872 | ||
|
|
140aced268 | ||
|
|
0144fa9a51 | ||
|
|
e4a5a3aeac | ||
|
|
53ab71036f | ||
|
|
1237bc4c78 | ||
|
|
1850a490f1 | ||
|
|
dd364da5be | ||
|
|
823a8ead89 | ||
|
|
c5fbefa497 | ||
|
|
7c0333a18f | ||
|
|
1e1b9c0e93 | ||
|
|
f197f595c5 | ||
|
|
99dbd03edf | ||
|
|
8045c0c96e | ||
|
|
9cbd7f1be8 | ||
|
|
575950a945 | ||
|
|
79a9e2d853 | ||
|
|
cd653fc026 | ||
|
|
f76943b36c | ||
|
|
8ed216785c | ||
|
|
0a70283df0 | ||
|
|
ae4d2a980d | ||
|
|
d9a4e87756 | ||
|
|
13e3ebc900 | ||
|
|
109827b897 | ||
|
|
28b4309fbf | ||
|
|
43b39f4002 | ||
|
|
72123d0f10 | ||
|
|
560d6ca866 | ||
|
|
baa0f0c35f | ||
|
|
5031dd9aa8 | ||
|
|
d1b51dd7f4 | ||
|
|
49a6991bb2 | ||
|
|
e1c3864811 | ||
|
|
ea5d9429d8 | ||
|
|
05abff6c67 | ||
|
|
3d5b593941 | ||
|
|
df8f391c9f | ||
|
|
bdcdb19d96 | ||
|
|
5fdc4d6642 | ||
|
|
caa8c98afd | ||
|
|
209828d35a | ||
|
|
2839e0cfcf | ||
|
|
f117ead9b1 | ||
|
|
1c8229ad98 | ||
|
|
7a7d0c4f01 | ||
|
|
294e45b9b7 | ||
|
|
4163113a52 | ||
|
|
f115d3e60c | ||
|
|
6c0aa9e15f | ||
|
|
e6e1b11369 | ||
|
|
48e1ce8f5b | ||
|
|
30c6c8499d | ||
|
|
c3ab03b617 | ||
|
|
213d753168 | ||
|
|
a58556a8a5 | ||
|
|
59e9ff84f8 | ||
|
|
0cc0d16091 | ||
|
|
23619a9f72 | ||
|
|
00f6307909 | ||
|
|
bc1bc0dae6 | ||
|
|
5843575649 | ||
|
|
02875313f6 | ||
|
|
753760302e | ||
|
|
e4f06440fb | ||
|
|
c871da0641 | ||
|
|
bd0d12cf67 | ||
|
|
e3b2e067f6 | ||
|
|
ad2ee62ead | ||
|
|
b2798a8c8a | ||
|
|
87b951060d | ||
|
|
c8c7723ad7 | ||
|
|
8c8abba2b4 | ||
|
|
9ce69bf93e | ||
|
|
91c02d2abe | ||
|
|
cb37cbc02c | ||
|
|
62f5dfe06b | ||
|
|
02f278e61c | ||
|
|
c481260638 | ||
|
|
ea87fbc98c | ||
|
|
fe958e7e05 | ||
|
|
a6226ef99a | ||
|
|
e8fc4e9275 | ||
|
|
338ed03004 | ||
|
|
3f3ee06925 | ||
|
|
6010f903cb | ||
|
|
b502c5b285 | ||
|
|
b5271fe5f3 | ||
|
|
a3cfc5cd97 | ||
|
|
c12f016545 | ||
|
|
6e3e3dc022 | ||
|
|
ba31680b67 | ||
|
|
56d39487a6 | ||
|
|
4b3eb9a8ce | ||
|
|
6e0b0da412 | ||
|
|
c76b66e8b2 | ||
|
|
bf9dc4de72 | ||
|
|
cdc6fecce7 | ||
|
|
98a8808bed | ||
|
|
a7e836593a | ||
|
|
084fa9dc77 | ||
|
|
a0c2c8e05f | ||
|
|
0054ba6500 | ||
|
|
b69bc88b66 | ||
|
|
8e4e42cf0a | ||
|
|
f98b44917c | ||
|
|
73709a257f | ||
|
|
1f4fbb2246 | ||
|
|
369a4c2b75 | ||
|
|
d656338993 | ||
|
|
98133c8815 | ||
|
|
7c1bb248e7 | ||
|
|
340419a187 | ||
|
|
3b97c44ed8 | ||
|
|
c67d4ac73a | ||
|
|
0ac76f788a | ||
|
|
8d021678da | ||
|
|
a539dff7ea | ||
|
|
133c2a04ab | ||
|
|
37437f9a53 | ||
|
|
0c3f561c38 | ||
|
|
a1643f9a5c | ||
|
|
78421ccf6f | ||
|
|
4b18669bb3 | ||
|
|
34e4748737 | ||
|
|
bb8182baa0 | ||
|
|
641dd746c4 | ||
|
|
437ea4f536 | ||
|
|
d5ce727c3d | ||
|
|
7fc72064c1 | ||
|
|
581f53735b |
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_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
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
{
|
{
|
||||||
@@ -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;
|
||||||
@@ -145,10 +203,10 @@ public:
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Enables the failsafe with the specified message
|
* @brief Enables the failsafe with the specified message
|
||||||
*
|
*
|
||||||
* @param message the message indicating the cause of the failsafe
|
* @param message the message indicating the cause of the failsafe
|
||||||
*/
|
*/
|
||||||
void enable_failsafe(std::string message)
|
void enable_failsafe(std::u16string message)
|
||||||
{
|
{
|
||||||
this->failsafe_enabled = true;
|
this->failsafe_enabled = true;
|
||||||
this->failsafe_request->message = message;
|
this->failsafe_request->message = message;
|
||||||
@@ -162,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];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -212,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
|
||||||
*
|
*
|
||||||
@@ -223,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)
|
||||||
@@ -252,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;
|
||||||
}
|
}
|
||||||
@@ -291,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)
|
||||||
{
|
{
|
||||||
@@ -298,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
|
||||||
@@ -343,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
|
||||||
@@ -362,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
|
||||||
@@ -374,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))
|
||||||
{
|
{
|
||||||
@@ -383,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,7 +5,7 @@ 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")
|
||||||
@@ -18,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")
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ 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()
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,17 +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 = 0.0, des_y = 0.0, des_z = p0_z; // desired position
|
float des_x = 0.0, des_y = 0.0, des_z = p0_z; // desired position
|
||||||
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;
|
||||||
|
|
||||||
@@ -130,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)
|
||||||
@@ -179,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);
|
||||||
@@ -231,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);
|
||||||
@@ -273,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);
|
||||||
@@ -488,10 +502,10 @@ private:
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Callback function for receiving failsafe messages
|
* @brief Callback function for receiving failsafe messages
|
||||||
*
|
*
|
||||||
* @param msg the message indicating that the failsafe was enabled
|
* @param msg the message indicating that the failsafe was enabled
|
||||||
*/
|
*/
|
||||||
void on_failsafe_received(const drone_services::msg::FailsafeMsg::SharedPtr msg)
|
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)
|
||||||
Submodule src/px4_msgs updated: ffc3a4cd57...b64ef0475c
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