Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcf45bff26 | ||
|
|
f0abeaacf7 | ||
|
|
195ddc6ab9 | ||
|
|
f0cbebf874 | ||
|
|
378d63a026 | ||
|
|
3030421efc | ||
|
|
743c0ce540 | ||
|
|
9fee669f44 | ||
|
|
c9690c376f | ||
|
|
303a3f0dbd | ||
|
|
67df381251 | ||
|
|
6ddd24cfd5 | ||
|
|
49d62f894a | ||
|
|
dc96e3a86c | ||
|
|
cc14b4a3eb | ||
|
|
6d74605582 | ||
|
|
9a81018c1d | ||
|
|
14b94a2ec4 | ||
|
|
3573c2532c |
151
README.md
151
README.md
@@ -1 +1,152 @@
|
||||
# 5G RCID
|
||||
## 5G Remote Controlled Inspection Drone
|
||||
By Sem van der Hoeven
|
||||
|
||||
## Table of contents
|
||||
- [Introduction](#introduction)
|
||||
- [EMERGENCY FORCE STOP](#emergency-force-stop)
|
||||
- [Pin layout](#pin-layout)
|
||||
- [Connecting to API](#connecting-to-api)
|
||||
- [Installing API](#installing-api)
|
||||
- [Installation on new flight computer](#installation-on-new-flight-computer)
|
||||
- [Building PX4 firmware](#building-px4-firmware)
|
||||
- [ROS2 nodes overview](#ros2-nodes-overview)
|
||||
- [PX4 parameters](#px4-parameters)
|
||||
- [Optical flow](#optical-flow)
|
||||
|
||||
## Introduction
|
||||
This is the code for the 5G RCID of the 5G Hub. All ROS2 nodes and the API code can be found here. The flight computer currently already contains the latest version of the software.
|
||||
- The code for the ROS2 nodes can be found in the [src folder (src/)](/src/)
|
||||
- The code for the API can be found in the [api folder (api/)](/api/)
|
||||
- Further information about how the drone is built and how it works can be found in my report in the [docs folder (doc/)](/doc/)
|
||||
|
||||
To add new ROS2 functionality, you can edit the code, push to the repository, pull on the flight computer (ubuntu@10.100.0.40, password is `raspberrypi`) and build the ROS2 workspace again. An example of how to do this is shown below:
|
||||
```bash
|
||||
# (On your computer) commit changes
|
||||
git commit -a -m "Added new functionality"
|
||||
git push
|
||||
# (On the flight computer) pull changes
|
||||
cd /home/ubuntu/ros2_ws
|
||||
git fetch
|
||||
git pull
|
||||
# (On the flight computer) build workspace
|
||||
colcon build --allow-overriding drone_services
|
||||
# (On the flight computer) source workspace
|
||||
source install/setup.bash
|
||||
# (On the flight computer) restart ROS2 nodes
|
||||
drone_scripts/restart_services.sh
|
||||
```
|
||||
To add new API functionality, you can do the same, but for the code in the api folder. Then, log in on the edge computer and pull the changes. The API is automatically restarted when the code is changed. If you want to manually restart the API, you can do so using the following command:
|
||||
```bash
|
||||
# (On the edge computer) restart API
|
||||
sudo systemctl restart webserver
|
||||
```
|
||||
## EMERGENCY FORCE STOP
|
||||
If, for whatever reason, the drone does not respond to the API, you can force stop all nodes on the drone and force it to land through the pixhawk using the following commands:
|
||||
```bash
|
||||
# on your computer, ssh into the drone
|
||||
ssh ubuntu@10.100.0.40
|
||||
# cd into the ROS2 workspace
|
||||
cd ros2_ws
|
||||
# stop all ROS2 nodes
|
||||
drone_scripts/stop_services.sh
|
||||
```
|
||||
## Pin layout
|
||||
A pinout of the raspberry pi can be found [here](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html). The connections are visible in the table below:
|
||||
|Raspberry Pi pin|Function|Connected to|
|
||||
|---|---|---|
|
||||
|2|5V|Power input from PDB|
|
||||
|4|5V|Power to relais|
|
||||
|6|GND|Ground from PDB|
|
||||
|8|UART TX|To RX of Pixhawk TELEM 1|
|
||||
|9|GND|Ground to relais|
|
||||
|10|UART RX|To TX of Pixhawk TELEM 1|
|
||||
|11|GPIO 17|Relais|
|
||||
|13|GPIO 27|Relais|
|
||||
|
||||
## Connecting to API
|
||||
To connect to the API, make sure you are connected to the 5G Hub network. Then, the API is located at http://10.1.1.41:8080/. When the drone is finished booting (the relais is switched on), you can connect to the drone using the `Connect` button.
|
||||
|
||||
## Installing API
|
||||
To be able to run the API, npm and nodejs must be installed:
|
||||
```bash
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
|
||||
nvm install node
|
||||
# to be able to run npm and nodejs as sudo, run the following commands:
|
||||
n=$(which node)
|
||||
n=${n%/bin/node}
|
||||
chmod -R 755 $n/bin/*
|
||||
sudo cp -r $n/{bin,lib,share} /usr/local
|
||||
```
|
||||
To install the API on a new edge computer, you must first clone this repository. Then you can run the NodeJS webserver using npm. You can do that using the following commands:
|
||||
```bash
|
||||
git clone https://github.com/etmeddi/5g_drone_ROS2.git
|
||||
cd 5g_drone_ROS2/api
|
||||
npm start
|
||||
```
|
||||
## Installation on new flight computer
|
||||
The drone currently has a Raspberry Pi that contains a ROS2 installation. The Raspberry Pi runs Ubuntu 20.04 and ROS 2 Foxy. If you want to install this on a new Pi, you should [get Ubuntu Server 20.04](https://ubuntu.com/download/server#downloads), and [install ROS2 Foxy](https://docs.ros.org/en/foxy/Installation/Ubuntu-Install-Debians.html) on it. Then, you should clone this repository into a `ros2_ws` folder. You can do that using the following commands:
|
||||
```bash
|
||||
git clone git@github.com:etmeddi/5g_drone_ROS2.git
|
||||
mv 5g_drone_ROS2 ros2_ws
|
||||
```
|
||||
|
||||
After that, to make sure the connection to PX4 works, you should follow the [PX4 ROS2 User Guide](https://docs.px4.io/main/en/ros/ros2_comm.html#installation-setup) provided by PX4.
|
||||
|
||||
## Building PX4 firmware
|
||||
The RCID uses a custom version of the PX4 firmware to include the battery percentage and CPU load of the flight controller. Before building the firmware, make sure the [developer toolchain](https://docs.px4.io/main/en/dev_setup/dev_env.html) is set up correctly. To build the firmware, first clone the repository (from [the PX4 docs](https://docs.px4.io/main/en/dev_setup/building_px4.html)):
|
||||
```bash
|
||||
# enter home directory
|
||||
cd
|
||||
# clone git repository
|
||||
git clone https://github.com/PX4/PX4-Autopilot.git --recursive
|
||||
```
|
||||
In the PX4 directory, edit the file `src/modules/uxrce_dds_client/dds_topics.yaml`. Above the line that says `subscriptions:`, add the following:
|
||||
```yaml
|
||||
- topic: /fmu/out/battery_status
|
||||
type: px4_msgs::msg::BatteryStatus
|
||||
- topic: /fmu/out/cpuload
|
||||
type: px4_msgs::msg::Cpuload
|
||||
```
|
||||
The file can also be found in this repository at [dds_topics.yaml](dds_topics.yaml). After changing the file, you can build the firmware using the following command:
|
||||
```bash
|
||||
# navigate to root of PX4 directory
|
||||
cd ~/PX4-Autopilot
|
||||
# build firmware
|
||||
make px4_fmu-v5_default
|
||||
```
|
||||
The built firmware file will be located at `PX4-Autopilot/build/px4_fmu-v4_default/px4_fmu-v4_default.px4`. You can then flash this to the flight controller using [QGroundControl](http://qgroundcontrol.com/). Make sure to select the [custom firmware](https://docs.px4.io/main/en/config/firmware.html#installing-px4-master-beta-or-custom-firmware) option when flashing, and select the built firmware file.
|
||||
## ROS2 nodes overview
|
||||
An overview of all the ROS2 nodes, services and topics can be found below (also visible in my report)
|
||||
|
||||

|
||||
|
||||
## PX4 parameters
|
||||
To enable communication with the flight computer, the following parameters should be set in QGroundControl:
|
||||
|Parameter|Value|Function|
|
||||
|---|---|---|
|
||||
|UXRCE_DDS_CFG|101|run microxrce-dds on TELEM 1|
|
||||
|MAV_0_CONFIG|TELEM 4|run mavlink on TELEM 4 because XRCE-DDS runs on TELEM 1|
|
||||
|SER_TEL1_BAUD|921600|high baud rate because serial|
|
||||
|COM_RC_IN_MODE|4 (Stick input disabled)|allow arming without GPS|
|
||||
|COM_RCL_EXCEPT|5|Ignore loss of RC remote signal|
|
||||
|
||||
## Optical flow
|
||||
The optical flow sensor is a [Hex HereFlow PMW3901](https://docs.px4.io/main/en/sensor/pmw3901.html#hex-hereflow-pmw3901-optical-flow-sensor) and it's connected to the CAN port of the pixhawk. The parameters to enable the optical flow sensor are:
|
||||
|Parameter|Value|Function|
|
||||
|---|---|---|
|
||||
|EKF2_OF_CTRL |1 (Enabled)|Enable optical flow sensor fusion|
|
||||
|UAVCAN_ENABLE|2 (Sensors automatic config)|Enable UAVCAN for sensors|
|
||||
|UAVCAN_SUB_FLOW|Enabled|subscribe to optical flow messages|
|
||||
|UAVCAN_SUB_GPS|Disabled|subscribe to GPS messages|
|
||||
|
||||
To reverse this, and be able to use GPS, the parameters should be:
|
||||
|
||||
|Parameter|Value|Function|
|
||||
|---|---|---|
|
||||
|EKF2_OF_CTRL |0 (Disabled)|Disable optical flow sensor fusion|
|
||||
|UAVCAN_ENABLE|0 (Disabled)|Disable UAVCAN for sensors|
|
||||
|UAVCAN_SUB_FLOW|Disabled|don't subscribe to optical flow messages|
|
||||
|UAVCAN_SUB_GPS|Enabled|subscribe to GPS messages|
|
||||
|
||||
|
||||
|
||||
169
api/index.js
169
api/index.js
@@ -1,120 +1,11 @@
|
||||
var express = require("express");
|
||||
var app = express();
|
||||
const WebSocket = require("ws");
|
||||
|
||||
//# TODO SSE https://www.digitalocean.com/community/tutorials/nodejs-server-sent-events-build-realtime-app
|
||||
|
||||
var last_status = {};
|
||||
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) {
|
||||
try {
|
||||
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", 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
|
||||
app.set("view engine", "ejs");
|
||||
|
||||
@@ -127,65 +18,5 @@ app.get("/", function (req, res) {
|
||||
|
||||
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);
|
||||
console.log("Server is listening on port 8080");
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
<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>
|
||||
@@ -26,14 +25,6 @@
|
||||
<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">
|
||||
@@ -72,14 +63,10 @@
|
||||
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 = () => {
|
||||
|
||||
@@ -108,55 +95,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
// 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++) {
|
||||
@@ -174,27 +112,6 @@
|
||||
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)
|
||||
@@ -239,6 +156,7 @@
|
||||
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 });
|
||||
@@ -269,46 +187,7 @@
|
||||
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) + "%";
|
||||
@@ -317,16 +196,6 @@
|
||||
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";
|
||||
@@ -336,8 +205,6 @@
|
||||
} else if (data.type == "API_HEARTBEAT") {
|
||||
concole.log("Got heartbeat from API")
|
||||
clearTimeout(api_timout);
|
||||
} else {
|
||||
// decodeBase64Image(data.image, document.getElementById("result-video"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,26 +216,22 @@
|
||||
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";
|
||||
@@ -392,11 +255,8 @@
|
||||
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");
|
||||
@@ -410,9 +270,6 @@
|
||||
};
|
||||
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>
|
||||
BIN
doc/5G RCID Presentatie.pdf
Normal file
BIN
doc/5G RCID Presentatie.pdf
Normal file
Binary file not shown.
BIN
doc/Afstudeerverslag_Sem_van_der_Hoeven_2162609_final.pdf
Normal file
BIN
doc/Afstudeerverslag_Sem_van_der_Hoeven_2162609_final.pdf
Normal file
Binary file not shown.
BIN
doc/ROSNodes.jpg
Normal file
BIN
doc/ROSNodes.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 510 KiB |
@@ -17,21 +17,8 @@ import threading
|
||||
import json
|
||||
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.
|
||||
# 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):
|
||||
"""
|
||||
Enum for the commands that can be sent to the API
|
||||
@@ -39,6 +26,7 @@ class RequestCommand(Enum):
|
||||
GET_COMMANDS_TYPES = -1
|
||||
LAND = 0
|
||||
ARM_DISARM = 1
|
||||
MOVE_POSITION = 2
|
||||
MOVE_DIRECTION = 3
|
||||
EMERGENCY_STOP = 6
|
||||
|
||||
@@ -51,7 +39,6 @@ class ResponseMessage(Enum):
|
||||
MOVE_DIRECTION_RESULT = 2
|
||||
FAILSAFE = 3
|
||||
|
||||
|
||||
class ApiListener(Node):
|
||||
"""
|
||||
Node that listens to the client and sends the commands to the drone
|
||||
@@ -60,25 +47,20 @@ class ApiListener(Node):
|
||||
def __init__(self):
|
||||
super().__init__('api_listener')
|
||||
self.get_logger().info('ApiListener node started')
|
||||
self.drone_status_subscriber = self.create_subscription(
|
||||
DroneStatus, '/drone/status', self.drone_status_callback, 10)
|
||||
self.failsafe_subscriber = self.create_subscription(
|
||||
FailsafeMsg, "/drone/failsafe", self.failsafe_callback, 10)
|
||||
self.drone_status_subscriber = self.create_subscription(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.take_picture_client = self.create_client(
|
||||
TakePicture, '/drone/picture')
|
||||
self.take_picture_client = self.create_client(TakePicture, '/drone/picture')
|
||||
self.wait_for_service(self.take_picture_client, "Take picture")
|
||||
self.take_picture_request = TakePicture.Request()
|
||||
|
||||
self.move_position_client = self.create_client(
|
||||
MovePosition, '/drone/move_position')
|
||||
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.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()
|
||||
|
||||
@@ -86,8 +68,7 @@ class ApiListener(Node):
|
||||
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.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()
|
||||
|
||||
@@ -129,8 +110,7 @@ class ApiListener(Node):
|
||||
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...')
|
||||
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
|
||||
@@ -145,8 +125,7 @@ class ApiListener(Node):
|
||||
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.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
|
||||
@@ -159,8 +138,7 @@ class ApiListener(Node):
|
||||
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}')
|
||||
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
|
||||
@@ -174,8 +152,7 @@ class ApiListener(Node):
|
||||
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}))
|
||||
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
|
||||
@@ -188,8 +165,7 @@ class ApiListener(Node):
|
||||
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))
|
||||
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')
|
||||
|
||||
@@ -200,11 +176,9 @@ class ApiListener(Node):
|
||||
self.status_data_received = False
|
||||
if self.websocket is not None:
|
||||
try:
|
||||
self.message_queue.append(json.dumps(
|
||||
{'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))
|
||||
self.get_logger().error('Something went wrong while publishing the status: ' + str(e))
|
||||
|
||||
def handle_responses(self):
|
||||
"""Thread to handle responses to send to the client
|
||||
@@ -239,8 +213,7 @@ class ApiListener(Node):
|
||||
messagetypes[message_type.name] = message_type.value
|
||||
result['request_commands'] = requestcommands
|
||||
result['response_messages'] = messagetypes
|
||||
self.message_queue.append(json.dumps(
|
||||
{'type': ResponseMessage.ALL_REQUESTS_RESPONSES.name, 'data': result}))
|
||||
self.message_queue.append(json.dumps({'type': ResponseMessage.ALL_REQUESTS_RESPONSES.name, 'data': result}))
|
||||
|
||||
def handle_direction_message(self, message):
|
||||
"""Calls the move position service with the given values
|
||||
@@ -253,8 +226,7 @@ class ApiListener(Node):
|
||||
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.get_logger().info(f'Calling move position service with request: {str(self.move_position_request)}')
|
||||
|
||||
self.send_move_position_request()
|
||||
|
||||
@@ -274,8 +246,7 @@ class ApiListener(Node):
|
||||
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)}')
|
||||
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
|
||||
@@ -284,8 +255,7 @@ class ApiListener(Node):
|
||||
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))
|
||||
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")
|
||||
@@ -300,11 +270,11 @@ class ApiListener(Node):
|
||||
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))
|
||||
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.get_logger().info('Activating emergency stop')
|
||||
self.enable_failsafe("Emergency stop activated")
|
||||
|
||||
def takeoff(self):
|
||||
@@ -324,7 +294,7 @@ class ApiListener(Node):
|
||||
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 = self.land_client.call_async(self.land_request)
|
||||
future.add_done_callback(partial(self.land_service_callback))
|
||||
|
||||
def arm_disarm(self):
|
||||
@@ -348,8 +318,7 @@ class ApiListener(Node):
|
||||
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))
|
||||
self.get_logger().error('Something went wrong while calling the ready service!\n' + str(e))
|
||||
|
||||
def arm_service_callback(self, future):
|
||||
try:
|
||||
@@ -359,8 +328,7 @@ class ApiListener(Node):
|
||||
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))
|
||||
self.get_logger().error('Something went wrong while calling the arm service!\n' + str(e))
|
||||
|
||||
def disarm_service_callback(self, future):
|
||||
try:
|
||||
@@ -371,8 +339,7 @@ class ApiListener(Node):
|
||||
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))
|
||||
self.get_logger().error('Something went wrong while calling the disarm service!\n' + str(e))
|
||||
|
||||
def land_service_callback(self, future):
|
||||
try:
|
||||
@@ -382,8 +349,7 @@ class ApiListener(Node):
|
||||
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))
|
||||
self.get_logger().error('Something went wrong while calling the land service!\n' + str(e))
|
||||
|
||||
def consume_message(self, message):
|
||||
"""Consumes a message from the client"""
|
||||
@@ -414,12 +380,10 @@ class ApiListener(Node):
|
||||
self.get_logger().info('Emergency stop command received')
|
||||
self.emergency_stop()
|
||||
else:
|
||||
self.get_logger().error('Received unknown command ' +
|
||||
str(message_json['command']))
|
||||
self.get_logger().error('Received unknown command ' + str(message_json['command']))
|
||||
self.send_available_commands()
|
||||
except TypeError:
|
||||
self.get_logger().error('Received unknown type: ' +
|
||||
str(type(message)) + " " + str(message))
|
||||
self.get_logger().error('Received unknown type: ' + str(type(message)) + " " + str(message))
|
||||
self.send_available_commands()
|
||||
except json.JSONDecodeError:
|
||||
self.get_logger().error('Received invalid JSON')
|
||||
@@ -427,11 +391,10 @@ class ApiListener(Node):
|
||||
except Exception as e:
|
||||
self.get_logger().error('Something went wrong!')
|
||||
self.get_logger().error(str(type(e)))
|
||||
self.get_logger().error(str(e.with_traceback()))
|
||||
self.get_logger().error(e)
|
||||
|
||||
async def api_handler(self, websocket):
|
||||
"""Handles the websocket connection
|
||||
|
||||
Args:
|
||||
websocket (websockets object): the websocket connection
|
||||
"""
|
||||
@@ -451,7 +414,6 @@ class ApiListener(Node):
|
||||
self.get_logger().error(str(e))
|
||||
self.websocket = None
|
||||
|
||||
|
||||
def main(args=None):
|
||||
"""Main function"""
|
||||
rclpy.init(args=args)
|
||||
@@ -464,6 +426,5 @@ def main(args=None):
|
||||
api_listener.destroy_node()
|
||||
rclpy.shutdown()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "std_msgs/msg/string.hpp"
|
||||
#include "beacon_positioning/msg/tracker_position.hpp"
|
||||
|
||||
#include "rtls_driver/rtls_driver.hpp"
|
||||
|
||||
// from the example: https://github.com/Terabee/positioning_systems_api/blob/master/examples/rtls_tracker_example.cpp
|
||||
#include "serial_communication/serial.hpp"
|
||||
|
||||
#define TRACKER_0_PORT "/dev/ttyUSB0"
|
||||
|
||||
@@ -16,14 +16,13 @@ import requests
|
||||
RES_4K_H = 3496
|
||||
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):
|
||||
def __init__(self):
|
||||
"""Initialize the camera controller node.
|
||||
|
||||
Initializes the service and the video stream for the websocket connection.
|
||||
|
||||
"""
|
||||
super().__init__('camera_controller')
|
||||
|
||||
self.get_logger().info("Camera controller started. Waiting for service call...")
|
||||
@@ -37,21 +36,23 @@ class CameraController(Node):
|
||||
|
||||
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):
|
||||
"""Callback function for shutting down if an error occurred from a different thread."""
|
||||
if self.should_exit:
|
||||
self.get_logger().info("Shutting down...")
|
||||
self.destroy_node()
|
||||
sys.exit(-1)
|
||||
|
||||
def take_picture_callback(self, request, response):
|
||||
"""Callback function for the service call to the /drone/picture service.
|
||||
takes a picture with the given filename and saves it to the drone_img folder.
|
||||
Sends the filename back to the caller.
|
||||
"""
|
||||
if (request.input_name == "default"):
|
||||
self.get_logger().info("Taking picture with default filename")
|
||||
now = datetime.now().strftime("droneimage_%Y-%m-%d_%H-%M-%S")
|
||||
@@ -65,6 +66,7 @@ class CameraController(Node):
|
||||
return response
|
||||
|
||||
def setup_websocket(self):
|
||||
"""Sets up the websocket connection for the video stream on port 9002."""
|
||||
loop = asyncio.new_event_loop()
|
||||
connected = False
|
||||
while not connected:
|
||||
@@ -83,6 +85,9 @@ class CameraController(Node):
|
||||
sys.exit(-1)
|
||||
|
||||
async def websocket_video(self,websocket,path):
|
||||
"""Function for the websocket connection on port 9002.
|
||||
This continuously captures frames from the video and sends them to the websocket client.
|
||||
A resolution of 1920x1080 is chosen for performance reasons."""
|
||||
vid = cv2.VideoCapture(0,cv2.CAP_V4L)
|
||||
|
||||
vid.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
|
||||
@@ -92,12 +97,8 @@ class CameraController(Node):
|
||||
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
|
||||
@@ -109,75 +110,6 @@ class CameraController(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):
|
||||
rclpy.init(args=args)
|
||||
|
||||
@@ -53,7 +53,6 @@ public:
|
||||
|
||||
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));
|
||||
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->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));
|
||||
@@ -113,6 +112,11 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief callback function for the /drone/arm service
|
||||
*
|
||||
* @param future the future object that holds the result of the service call
|
||||
*/
|
||||
void arm_drone_service_callback(rclcpp::Client<drone_services::srv::ArmDrone>::SharedFuture future)
|
||||
{
|
||||
auto status = future.wait_for(1s);
|
||||
@@ -137,6 +141,11 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief callback function for the /drone/set_attitude service
|
||||
*
|
||||
* @param future the future object that holds the result of the service call
|
||||
*/
|
||||
void attitude_message_callback(rclcpp::Client<drone_services::srv::SetAttitude>::SharedFuture future)
|
||||
{
|
||||
auto status = future.wait_for(1s);
|
||||
@@ -220,39 +229,26 @@ public:
|
||||
*/
|
||||
void apply_collision_weights()
|
||||
{
|
||||
|
||||
// if (this->current_speed_x > 0) // if moving forward
|
||||
// {
|
||||
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);
|
||||
}
|
||||
// }
|
||||
// else // moving backward
|
||||
// {
|
||||
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);
|
||||
}
|
||||
// }
|
||||
// if (this->current_speed_y > 0) // moving 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);
|
||||
}
|
||||
// }
|
||||
// else // moving 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);
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -278,6 +274,11 @@ public:
|
||||
apply_collision_weights();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Callback for when a set_attitude request is made while landing
|
||||
*
|
||||
* @param future the future object containing the result of the request.
|
||||
*/
|
||||
void attitude_land_callback(rclcpp::Client<drone_services::srv::SetAttitude>::SharedFuture future)
|
||||
{
|
||||
auto status = future.wait_for(1s);
|
||||
@@ -295,6 +296,11 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Callback for when a set_vehicle_control request is made while landing
|
||||
*
|
||||
* @param future the future object containing the result of the request.
|
||||
*/
|
||||
void vehicle_control_land_request_callback(rclcpp::Client<drone_services::srv::SetVehicleControl>::SharedFuture future)
|
||||
{
|
||||
auto status = future.wait_for(1s);
|
||||
@@ -316,6 +322,11 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Callback method for receiving new height data
|
||||
*
|
||||
* @param message the message containing the height data
|
||||
*/
|
||||
void handle_height_message(const drone_services::msg::HeightData::SharedPtr message)
|
||||
{
|
||||
this->current_height = message->min_height;
|
||||
@@ -445,6 +456,13 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Callback function for when the /drone/land service is called
|
||||
*
|
||||
* @param request_header the header of the request
|
||||
* @param request the request to land
|
||||
* @param response the response to send back.
|
||||
*/
|
||||
void handle_land_request(
|
||||
const std::shared_ptr<rmw_request_id_t> request_header,
|
||||
const std::shared_ptr<drone_services::srv::Land::Request> request,
|
||||
@@ -454,6 +472,13 @@ public:
|
||||
response->is_landing = this->is_landing;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Callback function for when the /drone/ready_drone service is called
|
||||
*
|
||||
* @param request_header the header of the request
|
||||
* @param request the request to ready the drone
|
||||
* @param response the response to send back. Contains true if the request was successful, false otherwise
|
||||
*/
|
||||
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,
|
||||
@@ -474,8 +499,7 @@ public:
|
||||
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;
|
||||
response->success = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import launch
|
||||
@@ -17,8 +15,6 @@ 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(
|
||||
|
||||
@@ -67,17 +67,17 @@ class TestPositionChanger(unittest.TestCase):
|
||||
|
||||
def move_position_callback_up(self, future):
|
||||
self.node.get_logger().info("Callback called")
|
||||
self.assertTrue(future.result().success, "Move service failed")
|
||||
# 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.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.assertTrue(future.result().success, "Arm service failed")
|
||||
self.called_arm_service = True
|
||||
|
||||
def land_callback(self,future):
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import launch
|
||||
@@ -11,7 +10,6 @@ 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
|
||||
|
||||
@@ -396,9 +396,6 @@ private:
|
||||
send_velocity_setpoint();
|
||||
return;
|
||||
}
|
||||
// the spiral, in polar coordinates (theta, rho), is given by
|
||||
// theta = theta_0 + omega*t
|
||||
// rho = rho_0 + K*theta
|
||||
float theta = theta_0 + omega * 0.1;
|
||||
float rho = rho_0 + K * theta;
|
||||
|
||||
@@ -409,7 +406,6 @@ private:
|
||||
|
||||
if (!user_in_control)
|
||||
{
|
||||
// RCLCPP_INFO(this->get_logger(), "Sending idle attitude setpoint");
|
||||
send_attitude_setpoint();
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import launch
|
||||
|
||||
Reference in New Issue
Block a user