diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..ecd8ced --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,39 @@ +name: Docker CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Docker + uses: docker/setup-action@v2 + + - name: Build and run containers + run: | + docker-compose up -d + sleep 5 + docker ps + + - name: Check containers status + run: | + if docker ps | grep -q "droner"; then + echo "Containers are running successfully" + else + echo "Error occurred while running containers" + exit 1 + fi + + - name: Run tests + run: | + docker-compose -f path/to/your/docker-compose.yml exec -T your_service_name sh -c "your_test_script.sh" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9701f3e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +avi/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6b01c06 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python + +RUN apt-get update && \ + apt-get install -y python3-pip ffmpeg libsm6 libxext6 + +COPY requirements.txt /tmp/ +RUN pip3 install --no-cache-dir -r /tmp/requirements.txt + +WORKDIR /app + +COPY . /app + +CMD ["python", "run_singel_drone.py"] diff --git a/README.md b/README.md index 621ba3a..765105c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,181 @@ +# Итерация 4 +В результате итерации не удалось запустить рой. Исправлены недочеты прошлых итераций. +Зависимости: +Версия ОС: Ubuntu 22.04.4 LTS +Python 3.10.12 + +## Конфигурирование +Заполнить [конфигурационный файл](https://github.com/OSLL/tello-dev/blob/master/swarm/networks.json) +```json +{ + "ifaces" : { + "iface-name" : { + "ssid" : "", + "password" : "", + "ip": "172.18.0.3" + }, + "iface-name": { + "ssid" : "", + "password" : "", + "ip": "172.18.0.4" + } + } +} +``` +- Необходимо выполнить команду `ip a` и вставить имена необходимых wlan-интерфейсов вместо `iface-name`. +- Установить пакет `network-manager`: + `apt install network-manager=1.36.6-0ubuntu2` +- Выполнить команду `nmcli dev wifi list` и определить SSID wifi сетей причастных к дрону и вставить вместо `ssid` + +**Пример сконфигурированного файла** +```json +{ + "ifaces" : { + "wlo0" : { + "ssid" : "DRONE 1", + "password" : "", + "ip": "172.18.0.3" + }, + "wlo1": { + "ssid" : "DRONE 2", + "password" : "", + "ip": "172.18.0.4" + } + } +} +``` + +## Запуск +Установить зависимости +```bash +python3 -m pip install jsonrpcclient==4.0.3 requests==2.31.0 +``` +- Необходимо выполнить команду `docker compose --file ./swarm/docker-compose.yml up -d --build` (в одном месте монтируется с хоста папка и этого не избежать, иначе из контейнера нельзя будет управлять сетевыми устройствами) +- После запуска необходимо вызвать скрипт *client.py* передав флаги -c --ip , + где: + + --ip - P-адреса сервера JSON-RPC, к которому скрипт будет отправлять запросы, у нас это 127.0.0.1. + + -c - команда для дронов. + +Пример: +```bash +python3 ./swarm/scripts/rpc/client.py -c takeoff --ip 127.0.0.1 +``` +Результат: +```bash +RESULT for 'http://127.0.0.1:65001' : + {'jsonrpc': '2.0', 'result': 'OK', 'id': 1} +``` +## Остановка контейнеров +```bash +docker compose --file ./swarm/docker-compose.yml stop +``` +## Удаление контейнеров +```bash +docker compose --file ./swarm/docker-compose.yml down +``` + +## Презентация +Презентация:[ОУПРПО. Итерация 4. Проект 12](https://docs.google.com/presentation/d/15QLL1ix36oWb84pD0Ya88piNtv9CHaV_Dnr0AQvBDYY/edit?usp=sharing) + + +# Итерация 3 +Зависимости: +Версия ОС: Ubuntu 22.04.4 LTS +Python 3.10.12 + +## Конфигурирование +Заполнить [конфигурационный файл](https://github.com/OSLL/tello-dev/blob/master/swarm/networks.json) +```json +{ + "ifaces" : { + "iface-name" : { + "ssid" : "", + "password" : "", + "ip": "172.18.0.3" + }, + "iface-name": { + "ssid" : "", + "password" : "", + "ip": "172.18.0.4" + } + } +} +``` +- Необходимо выполнить команду `ip a` и вставить имена необходимых wlan-интерфейсов вместо `iface-name`. +- Установить пакет `network-manager`: + `apt install network-manager=1.36.6-0ubuntu2` +- Выполнить команду `nmcli dev wifi list` и определить SSID wifi сетей причастных к дрону и вставить вместо `ssid` + +**Пример сконфигурированного файла** +```json +{ + "ifaces" : { + "wlo0" : { + "ssid" : "DRONE 1", + "password" : "", + "ip": "172.18.0.3" + }, + "wlo1": { + "ssid" : "DRONE 2", + "password" : "", + "ip": "172.18.0.4" + } + } +} +``` + +## Запуск +Установить зависимости +```bash +python3 -m pip install jsonrpcclient==4.0.3 requests==2.31.0 +``` +- Необходимо выполнить команду `docker compose -f ./swarm/docker-compose.yml up -d --build` (в одном месте монтируется с хоста папка и этого не избежать, иначе из контейнера нельзя будет управлять сетевыми устройствами) +- После запуска необходимо вызвать скрипт *client.py* передав флаги -c --ip , + где: + + --ip - P-адреса сервера JSON-RPC, к которому скрипт будет отправлять запросы, у нас это 127.0.0.1. + + -c - команда для дронов. + +Пример: +```bash +python3 ./swarm/scripts/rpc/client.py -c takeoff --ip 127.0.0.1 +``` +Результат: +```bash +RESULT for 'http://127.0.0.1:65001' : + {'jsonrpc': '2.0', 'result': 'OK', 'id': 1} +``` +## Презентация и видео +[Видео запуска из лаборатории](https://drive.google.com/file/d/1AlVOG27zfj0EoQd7CfunbafDo9c3x55g/view?usp=sharing) +[Логи запуска](https://drive.google.com/file/d/1QEGajXq3eJybhvswaCkATVJhRs7hSCsq/view?usp=sharing) +Презентация:[ОУПРПО. Итерация 3. Проект 12](https://drive.google.com/file/d/1SChuYAM4CD_IQHj4QtwAoYbMkWu40w0_/view?usp=sharing) + + +# +# + +# Итерация 2 +## Версия 1 +Чтобы запустить рой из одного дрона [(wiki скрипта)](https://github.com/OSLL/tello-dev/wiki/%D0%97%D0%B0%D0%BF%D1%83%D1%81%D0%BA-%D0%BE%D0%B4%D0%B8%D0%BD%D0%BE%D1%87%D0%BD%D0%BE%D0%B3%D0%BE-%D0%B4%D1%80%D0%BE%D0%BD%D0%B0), необходимо выполнить следующие команды: +`docker build -t single_drone_swarm .` +`sudo docker run single_drone_swarm` +[Видео запуска из лаборатории](https://drive.google.com/file/d/1U_BEifhNsm7GAZkeG5-yhBLDRsZn3LQ_/view) +## Скрипты для создания физических сетевых интерфейсов в сети docker +Инструкция находится на [wiki-странице](https://github.com/OSLL/tello-dev/wiki/%D0%A4%D0%B8%D0%B7%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5-%D1%83%D1%81%D1%82%D1%80%D0%BE%D0%B9%D1%81%D1%82%D0%B2%D0%BE). Работает по wifi-адаптер Realtek 802.11ac. +## Для создания контейнеров для каждого дрона, подключаемых к созданной сети +`docker build -t single_drone_swarm .` +`python run_swarm.py` +Контейнеры отключаются через 21 секунду без дронов. +## Презентация и видео +[Видео запуска из лаборатории](https://drive.google.com/file/d/1U_BEifhNsm7GAZkeG5-yhBLDRsZn3LQ_/view) +Презентация:[ОУПРПО. Итерация 2. Проект 12.pdf](https://github.com/OSLL/tello-dev/files/14780046/2.12.pdf) + +# +# + # Обучение управлению дронами Tello ## Зависимости diff --git a/camera_settings_kamikadze.npy b/camera_settings_kamikadze.npy new file mode 100644 index 0000000..88bb4e3 Binary files /dev/null and b/camera_settings_kamikadze.npy differ diff --git a/config.py b/config.py new file mode 100644 index 0000000..7557406 --- /dev/null +++ b/config.py @@ -0,0 +1,6 @@ +from utils.load_camera_settings import load_camera_settings + + +camera_matrix, camera_distortion = load_camera_settings() + +MARKER_LEN = 6.5 # in cm diff --git a/delete_interface.py b/delete_interface.py new file mode 100644 index 0000000..7bb89fb --- /dev/null +++ b/delete_interface.py @@ -0,0 +1,12 @@ +import subprocess + +def delete_docker_interface(interface_name): + subprocess.run(["ip", "link", "delete", interface_name]) + print(f"Interface {interface_name} deleted.") + +def input_interface(): + interface_name = input("Enter the interface name to delete: ") + delete_docker_interface(interface_name) + +if __name__ == '__main__': + input_interface() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index da738ef..b030f3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,10 @@ numpy opencv-python opencv-contrib-python djitellopy +wifi +netifaces +docker +loguru +jsonrpcclient +jsonrpcserver +requests \ No newline at end of file diff --git a/ros/wiki/axis.md b/ros/wiki/axis.md index 5e656f8..5551a8e 100644 --- a/ros/wiki/axis.md +++ b/ros/wiki/axis.md @@ -7,6 +7,12 @@ * Third value is down/up, where up is positive value * Fourth value is rotation counterclockwise/clockwise, where clockwise is positive value +For all values, the absolute maximum is `100`. + +First three values are measured in *cm/s*. + +Fourth value (*rotation*) is measured in *0.5 degrees/s*. This means that with value of `100`, the drone will rotate clockwise at speed of *`50` degrees per second* + ## Twist axis * `Twist.linear.x` is backward/forward, where forward is positive value diff --git a/run_container.py b/run_container.py new file mode 100644 index 0000000..23c7bdd --- /dev/null +++ b/run_container.py @@ -0,0 +1,27 @@ +import docker + +def run_container(image_name, container_name, requirements_file): + client = docker.from_env() + try: + # Считываем содержимое файла requirements.txt + with open(requirements_file, 'r') as file: + requirements = file.read().splitlines() + + # Строим контейнер + container = client.containers.run( + image=image_name, + name=container_name, + detach=True, + volumes={requirements_file: {'bind': '/requirements.txt', 'mode': 'ro'}}, + command=f"sh -c 'pip install -r /requirements.txt && tail -f /dev/null'" + ) + print(f"Container {container_name} started successfully.") + except docker.errors.APIError as e: + print(f"Error starting container: {e}") + +if __name__ == "__main__": + image_name = "python" + container_name = "droner" + requirements_file = "requirements.txt" + + run_container(image_name, container_name, requirements_file) diff --git a/run_singel_drone.py b/run_singel_drone.py new file mode 100644 index 0000000..a22a9c1 --- /dev/null +++ b/run_singel_drone.py @@ -0,0 +1,5 @@ +from swarm.scripts.solutions.following.swarm_controlls import FollowMarkerWithCoords + +if __name__ == "__main__": + marker_follower = FollowMarkerWithCoords() + marker_follower.main() diff --git a/run_swarm.py b/run_swarm.py new file mode 100644 index 0000000..d28d28e --- /dev/null +++ b/run_swarm.py @@ -0,0 +1,42 @@ +import docker +from loguru import logger +from rtl_device.connect import readNetworkConfiguration + +def createDockerNetwork(name: str) -> None: + client = docker.from_env() + client.networks.create(name) + +def start_containers(image_name, network_name, num_containers): + client = docker.from_env() + try: + network = client.networks.get(network_name) + except docker.errors.NotFound: + logger.warning(f"Network '{network_name}' not found, creating it...") + createDockerNetwork(network_name) # Создаем сеть + network = client.networks.get(network_name) + + for i in range(num_containers): + container_name = f"{image_name}_{i+1}" + + try: + old_container = client.containers.get(container_name) + logger.info(f"Removing old container: {container_name}") + old_container.remove(force=True) + except docker.errors.NotFound: + pass + + container = client.containers.run(image=image_name, detach=True, name=container_name, network=network_name, stdout=True) + logger.info(f"Container {container_name} started and attached to network {network_name}") + + container_logs = container.logs().decode("utf-8") + logger.info(f"Container logs for {container_name}:") + logger.info(container_logs) + +if __name__ == "__main__": + config = readNetworkConfiguration("./rtl_device/networks.json") + image_name = 'single_drone_swarm' + network_name = list(config["docker_networks"].keys())[0] + num_containers = 3 + + start_containers(image_name, network_name, num_containers) + diff --git a/swarm/Dockerfile b/swarm/Dockerfile new file mode 100644 index 0000000..46f043d --- /dev/null +++ b/swarm/Dockerfile @@ -0,0 +1,31 @@ +FROM ubuntu:22.04 + +COPY /scripts/networking/localtime /etc/localtime + +# Общие зависимости +RUN apt update \ + && apt install -y \ + network-manager=1.36.6-0ubuntu2 \ + iptables=1.8.7-1ubuntu5.2 \ + python3=3.10.6-1~22.04 \ + python3-pip=22.0.2+dfsg-1ubuntu0.4 \ + && apt clean + +# Установка Python пакетов +RUN pip install --upgrade pip \ + && pip install jsonschema==4.22.0 \ + && pip install jsonrpcserver==5.0.9 \ + && pip install jsonrpcclient==4.0.3 requests==2.32.2 + +# Слои для каждого сервиса +COPY /scripts/networking/networking.py /networking.py +COPY /scripts/rpc/server.py /server.py +COPY /scripts/rpc/swarm_server.py /swarm_server.py +COPY /networks.json /networks.json + + +# Скрипт для запуска нужного сервиса на основе переменной среды +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/swarm/docker-compose.yml b/swarm/docker-compose.yml new file mode 100644 index 0000000..6e81569 --- /dev/null +++ b/swarm/docker-compose.yml @@ -0,0 +1,81 @@ +version: "3.8" +services: + networking: + build: + context: . + dockerfile: Dockerfile + network_mode: "host" + container_name: networking + restart: no + privileged: true + volumes: + - /var/run/dbus:/var/run/dbus + environment: + - SERVICE_NAME=networking + logging: + driver: "json-file" + options: + max-size: "10m" + + server: + build: + context: . + dockerfile: Dockerfile + container_name: server + depends_on: + - networking + - drone_1 + - drone_2 + networks: + swarm: + ipv4_address: 172.230.220.2 + ports: + - "65001:65001" + environment: + - SERVICE_NAME=server + logging: + driver: "json-file" + options: + max-size: "10m" + + drone_1: + build: + context: . + dockerfile: Dockerfile + container_name: drone_1 + depends_on: + - networking + networks: + swarm: + ipv4_address: 172.230.220.3 + environment: + - SERVICE_NAME=swarm_server + logging: + driver: "json-file" + options: + max-size: "10m" + + drone_2: + build: + context: . + dockerfile: Dockerfile + container_name: drone_2 + depends_on: + - networking + networks: + swarm: + ipv4_address: 172.230.220.4 + environment: + - SERVICE_NAME=swarm_server + logging: + driver: "json-file" + options: + max-size: "10m" + +networks: + swarm: + driver: bridge + ipam: + config: + - subnet: 172.230.220.0/24 + gateway: 172.230.220.1 diff --git a/swarm/entrypoint.sh b/swarm/entrypoint.sh new file mode 100644 index 0000000..1d66884 --- /dev/null +++ b/swarm/entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/sh +case "$SERVICE_NAME" in + networking) + exec python3 /networking.py /networks.json + ;; + server) + exec python3 /server.py + ;; + swarm_server) + exec python3 /swarm_server.py + ;; + *) + echo "Unknown service: $SERVICE_NAME" + exit 1 + ;; +esac diff --git a/swarm/networks.json b/swarm/networks.json new file mode 100644 index 0000000..c9318aa --- /dev/null +++ b/swarm/networks.json @@ -0,0 +1,14 @@ +{ + "ifaces" : { + "" : { + "ssid" : "", + "password" : "", + "ip": "172.230.220.3" + }, + "": { + "ssid" : "", + "password" : "", + "ip": "172.230.220.4" + } + } +} diff --git a/swarm/scripts/networking/localtime b/swarm/scripts/networking/localtime new file mode 100644 index 0000000..ddb3f4e Binary files /dev/null and b/swarm/scripts/networking/localtime differ diff --git a/swarm/scripts/networking/networking.py b/swarm/scripts/networking/networking.py new file mode 100644 index 0000000..282da83 --- /dev/null +++ b/swarm/scripts/networking/networking.py @@ -0,0 +1,150 @@ +import json +import subprocess +import sys +from jsonschema import validate +from jsonschema.validators import Draft202012Validator + + +class Networking: + """ + Class for configuring network and routing based on a JSON configuration file. + + Attributes: + PORTS (list): List of ports for routing. + """ + + PORTS = [8889, 8890, 11111] + + def __init__(self, config: dict): + """ + Initializes the Networking class object. + + Args: + config (dict): Network configuration in dictionary format. + """ + self.__mark = 1 + for iface in config["ifaces"]: + for port in Networking.PORTS: + self.__routing(iface, port, config["ifaces"][iface]["ip"]) + self.__connection(config) + + def __connection(self, config: dict): + """ + Establishes connection to Wi-Fi networks specified in the configuration. + + Args: + config (dict): Network configuration in dictionary format. + """ + for iface in config["ifaces"]: + ssid = config['ifaces'][iface]['ssid'] + password = config['ifaces'][iface]['password'] + try: + output = subprocess.check_output( + ['nmcli', 'device', 'wifi']).decode(encoding='utf-8').replace("\n", "") + + output = subprocess.check_output( + ['nmcli', 'device', 'wifi', 'connect', ssid, 'password', + password, 'ifname', iface]).decode(encoding='utf-8').replace("\n", "") + except subprocess.CalledProcessError as e: + print(f"Failed to connect to {ssid} on {iface}. Error: {e}") + except Exception as e: + print( + f"An error occurred while connecting to {ssid} on {iface}. Error: {e}") + + def __routing(self, iface: str, port: int, ip: str): + """ + Configures routing for a specific network interface, port, and IP address. + + Args: + iface (str): Network interface. + port (int): Port for routing. + ip (str): IP address for routing. + """ + try: + output = subprocess.check_output( + ['iptables', '-t', 'nat', '-A', 'PREROUTING', '-i', iface, '-p', + 'udp', '--dport', str(port), '-j', 'DNAT', '--to-destination', ip] + ).decode(encoding='utf-8').replace("\n", "") + + output = subprocess.check_output( + ['iptables', '-t', 'mangle', '-A', 'PREROUTING', '-s', ip, '-j', 'MARK', + '--set-mark', str(self.__mark)]).decode(encoding='utf-8').replace("\n", "") + + output = subprocess.check_output( + ['ip', 'route', 'add', 'default', 'dev', + iface, 'table', str(self.__mark * 100)]).decode(encoding='utf-8').replace("\n", "") + + output = subprocess.check_output( + ['ip', 'rule', 'add', 'fwmark', str(self.__mark), + 'table', str(self.__mark * 100)]).decode(encoding='utf-8').replace("\n", "") + + self.__mark += 1 + except subprocess.CalledProcessError as e: + print(f"Error occurred while setting up routing: {e}") + except Exception as e: + print(f"An error occurred while setting up routing: {e}") + + +def read_network_configuration(json_path): + """ + Reads and validates the network configuration from a JSON file. + + Args: + json_path (str): Path to the JSON file containing the network configuration. + + Returns: + dict: Network configuration in dictionary format. + """ + schema = { + "type": "object", + "properties": { + "ifaces": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9-]+$": { + "type": "object", + "properties": { + "ssid": {"type": "string"}, + "password": {"type": "string"}, + "ip": {"type": "string", "format": "ipv4"} + }, + "required": ["ssid", "password", "ip"] + } + } + } + }, + "required": ["ifaces"] + } + + try: + with open(json_path) as json_file: + config = json.load(json_file) + validate(instance=config, schema=schema, + format_checker=Draft202012Validator.FORMAT_CHECKER) + return config + except FileNotFoundError: + print(f"File '{json_path}' not found.") + except json.JSONDecodeError as e: + print(f"Error decoding JSON from '{json_path}': {e}") + except Exception as e: + print(f"An error occurred while reading network configuration: {e}") + return {} + + +def main(json_path): + """ + Main function of the program. + + Args: + json_path (str): Path to the JSON file containing the network configuration. + """ + config = read_network_configuration(json_path) + if config: + networking = Networking(config) + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python script.py ") + sys.exit(1) + main(sys.argv[1]) diff --git a/swarm/scripts/rpc/client.py b/swarm/scripts/rpc/client.py new file mode 100644 index 0000000..4c4e054 --- /dev/null +++ b/swarm/scripts/rpc/client.py @@ -0,0 +1,29 @@ +import argparse +import requests +from jsonrpcclient import request + + +def main(): + """ + CLI application to read a command from the user. + """ + parser = argparse.ArgumentParser( + description='CLI application to read a command') + parser.add_argument('-c', '--command', type=str, help='Command to execute') + parser.add_argument('--ip', type=str, help='IP address') + args = parser.parse_args() + + if args.command: + if args.ip: + url = f"http://{args.ip}:65001" + req = request("exec", params={ "command": args.command} ) + response = requests.post(url, json=req) + print(f"RESULT for '{url}' :\n\t {response.json()}") + else: + print('No IP address provided.') + else: + print('You need to pass the --command argument with a command to execute.') + + +if __name__ == "__main__": + main() diff --git a/swarm/scripts/rpc/server.py b/swarm/scripts/rpc/server.py new file mode 100644 index 0000000..504bef4 --- /dev/null +++ b/swarm/scripts/rpc/server.py @@ -0,0 +1,35 @@ +from jsonrpcserver import method, Success, Error, serve +from swarm.scripts.solutions.following.swarm_controlls import FollowMarkerWithCoords + +# Инициализация объекта дрона +drone_controller = FollowMarkerWithCoords() +drone_controller.main() + +@method(name="exec") +def exec(json: dict) -> str: + """ + JSON-RPC method to execute a command. + """ + status = "OK" # Simulate command execution status + + # Print the command received + print(json["command"]) + + if json["command"] == "turn_starting_position": + drone_controller.turn_starting_position(json["coordinates"]) + if json["command"] == "land": + drone_controller.land() + if json["command"] == "set_swarm_is_coordinated": + drone_controller.set_swarm_is_coordinated() + if json["command"] == "save_coord_move_marker": + drone_controller.save_move_marker(json["coordinates"]) + drone_controller.save_coord_all_drones(json["coord_all_drones"]) + if json["command"] == "set_rotate": + drone_controller.set_rotate_flag() + if json["command"] == "reset_rotate": + drone_controller.reset_rotate_flag() + # Return success or error response based on status + return Success(json["command"]) if status == "OK" else Error(1, json["command"]) + +if __name__ == "__main__": + serve(port=65000) diff --git a/swarm/scripts/rpc/swarm_server.py b/swarm/scripts/rpc/swarm_server.py new file mode 100644 index 0000000..ba148ed --- /dev/null +++ b/swarm/scripts/rpc/swarm_server.py @@ -0,0 +1,44 @@ +from jsonrpcserver import method, Success, serve +from jsonrpcclient import request +import requests + +SWARM = ["drone_1", "drone_2"] # List of drones in the swarm +all_drones_coord = [] +move_marker_coord = [] +@method(name="exec") +def exec(json: dict) -> str: + """ + JSON-RPC method to execute a command on all drones in the swarm. + """ + + params = { + "command": json["command"], + } + if json["command"] == "turn_starting_position": + params["coordinates"] = json["coordinates"] + all_drones_coord.append(json["coordinates"]) + if json["command"] == "save_coord_move_marker": + params["coordinates"] = json["coordinates"] + params["coord_all_drones"]= all_drones_coord + move_marker_coord.append(json["coordinates"]) + send_command_all_drones(params) + + if len(all_drones_coord) == len(SWARM): + params = { + "command": "set_swarm_is_coordinated" + } + send_command_all_drones(params) + + + # Return success response + return Success("OK") + +def send_command_all_drones(params): + for node in SWARM: + url = f"http://{node}:65000" + req = request("exec", params) + response = requests.post(url, json=req) + print(f"RESULT for '{url}' : {response}") + +if __name__ == "__main__": + serve(port=65001) diff --git a/swarm/scripts/solutions/following/config.py b/swarm/scripts/solutions/following/config.py new file mode 100644 index 0000000..9538e89 --- /dev/null +++ b/swarm/scripts/solutions/following/config.py @@ -0,0 +1,28 @@ +import cv2 +from utils.load_camera_settings import load_camera_settings + +FRAME_RATE = 30 +FRAME_SIZE = (960, 720) +MARKER_SIZE = 50 # in pixels +MARKER_SIZE_RANGE = 15 +MARKER_NUMBER = 304 +MARKER_CENTER = 304 +MARKER_STOP = 305 # random +MARKER_MOVE = 301 # random +X_RANGE = 125 +Y_RANGE = 125 +TIMEOUT = 20 +STEP = 25 +Z_STEP = 25 +SPEED = 50 +QUEUE_SIZE = 100 +MARKER_LEN = 6.5 +ROTATE_DEGREE = 45 + +camera_matrix, camera_distortion = load_camera_settings() + +DISTANCE = 60 # in cm +DISTANCE_RANGE = 25 + +ARUCO_DICT = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_APRILTAG_36h11) +ARUCO_PARAMS = cv2.aruco.DetectorParameters() diff --git a/swarm/scripts/solutions/following/follow_marker_coords.py b/swarm/scripts/solutions/following/follow_marker_coords.py new file mode 100644 index 0000000..6783b77 --- /dev/null +++ b/swarm/scripts/solutions/following/follow_marker_coords.py @@ -0,0 +1,102 @@ +import cv2 +import time +import math +import numpy as np +from queue import Queue, Empty +from drone.drone import Drone +from swarm.scripts.solutions.following.config import FRAME_RATE, FRAME_SIZE, DISTANCE, DISTANCE_RANGE,\ + ARUCO_DICT, ARUCO_PARAMS, QUEUE_SIZE, TIMEOUT, MARKER_NUMBER, SPEED +from swarm.scripts.solutions.following.config import camera_matrix, camera_distortion, MARKER_LEN +from swarm.scripts.solutions.following.state import State +from utils.write_text import write_status_on_image + + +def dist_beetwen_points(p1, p2): + return math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) + + +class FollowMarkerWithCoords(): + + def __init__(self): + self.video_writer = cv2.VideoWriter('drone_stream.avi', cv2.VideoWriter_fourcc(*'XVID'), FRAME_RATE, FRAME_SIZE) + self.actions = Queue(QUEUE_SIZE) + self.last_state = State.START + self.last_time = time.process_time() + self.last_battery_charge = 0 + self.is_stopped = False + + def stop(self): + self.is_stopped = True + + def get_state(self, tvec): + marker_x, marker_y, marker_z = tvec[2], tvec[0], tvec[1] + x = y = z = 0 + if abs(marker_y) > 20: + y = -int(marker_y) + if abs(marker_z) > 20: + z = -int(marker_z) + if abs(marker_x - DISTANCE) > DISTANCE_RANGE: + x = int(marker_x - DISTANCE) + if x == 0 and y == 0 and z == 0: + return None + return (State.MOVE, np.array([x, y, z], dtype=np.int32)) + + def find_marker(self, frame): + corners, ids, rejected = cv2.aruco.detectMarkers(frame, ARUCO_DICT, parameters=ARUCO_PARAMS) + if ids is not None: + for i in range(len(ids)): + if ids[i] == MARKER_NUMBER: + return (ids[i], corners[i]) + return None + + def move_to_marker(self, m_corner): + self.last_time = time.process_time() + rvecs , tvecs, _ = cv2.aruco.estimatePoseSingleMarkers(np.array([m_corner]), MARKER_LEN, camera_matrix, camera_distortion) + state = self.get_state(tvecs[0][0]) + if state == State.MOVE: + self.actions.put(state) + self.last_state = state + + def process_frame(self, frame): + ids = None + is_find = True + if self.last_state == State.READY: + ret = self.find_marker(frame) + if ret is not None: + marker_id, marker_corner = ret + frame = cv2.aruco.drawDetectedMarkers(frame, np.array([marker_corner]), np.array([marker_id])) + self.move_to_marker(marker_corner) + else: + if time.process_time() - self.last_time > TIMEOUT: + print("Send END") + self.actions.put((State.END, None)) + self.last_state = State.END + write_status_on_image(frame, {"Battery": f"{self.last_battery_charge}%", "Last state": self.last_state}) + self.video_writer.write(cv2.resize(frame, FRAME_SIZE)) + return frame + + def drone_instructions(self, tello): + tello.takeoff() + tello.move_up(50) + self.last_time = time.process_time() + self.last_state = State.READY + while not self.is_stopped: + try: + state, movements = self.actions.get(block=False, timeout=0.25) + self.actions.task_done() + if state == State.END: + break + elif state == State.MOVE: + x, y, z = movements + print(movements) + tello.go_xyz_speed(int(x), int(y), int(z), SPEED) + except Empty: + self.last_state = State.READY + self.last_battery_charge = tello.get_battery() + tello.land() + + def main(self): + self.is_stopped = False + drone = Drone(self.drone_instructions, self.process_frame, show_stream=True) + drone.run_program() + self.video_writer.release() \ No newline at end of file diff --git a/swarm/scripts/solutions/following/state.py b/swarm/scripts/solutions/following/state.py new file mode 100644 index 0000000..01f5dca --- /dev/null +++ b/swarm/scripts/solutions/following/state.py @@ -0,0 +1,9 @@ +from enum import Enum, auto + + +class State(Enum): + START = auto() + END = auto() + READY = auto() + MOVE = auto() + ROTATE = auto() \ No newline at end of file diff --git a/swarm/scripts/solutions/following/swarm_controlls.py b/swarm/scripts/solutions/following/swarm_controlls.py new file mode 100644 index 0000000..0b71985 --- /dev/null +++ b/swarm/scripts/solutions/following/swarm_controlls.py @@ -0,0 +1,321 @@ +import docker +import os +import cv2 +import time +import math +import numpy as np +from queue import Queue, Empty +from drone.drone import Drone +from swarm.scripts.solutions.following.config import FRAME_RATE, FRAME_SIZE, DISTANCE, DISTANCE_RANGE,\ + ARUCO_DICT, ARUCO_PARAMS, QUEUE_SIZE, TIMEOUT, MARKER_CENTER, ROTATE_DEGREE, MARKER_MOVE, MARKER_STOP, SPEED +from swarm.scripts.solutions.following.config import camera_matrix, camera_distortion, MARKER_LEN +from swarm.scripts.solutions.following.state import State +from utils.write_text import write_status_on_image + + +from jsonrpcclient import request +import requests + +def dist_beetwen_points(p1, p2): + return math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) + + +class FollowMarkerWithCoords(): + + def __init__(self): + self.video_writer = cv2.VideoWriter('drone_stream.avi', cv2.VideoWriter_fourcc(*'XVID'), FRAME_RATE, FRAME_SIZE) + self.actions = Queue(QUEUE_SIZE) + self.last_state = State.START + self.last_time = time.process_time() + self.last_battery_charge = 0 + self.is_stopped = False + self.num_of_rotates = 0 + self.flag = 0 + self.flag_two = 0 + self.drone_id = 1 + self.coord_drone = None + self.num_of_rotate = 0 + + self.swarm_coordinated = False + self.coord_drone = None + + self.rotate_flag = 1 + self.move_flag = 0 + self.move_marker = 0 + + def stop(self): + self.is_stopped = True + + + def move_to_marker(self): + x, y, z =self.move_marker + x_un = self.coord_drone[0] - x + y_un = self.coord_drone[1] - y + z_un = self.coord_drone[2] - z + self.coord_drone = (x_un, y_un, z_un) + coord_drone = (x_un, y_un, z_un) + angle_degrees = 360 - self.num_of_rotates + + # Получаем матрицу поворота из вектора ориентации + angle_radians = math.radians(angle_degrees) + + # Преобразуем угол поворота в матрицу поворота + R = np.array([[np.cos(angle_radians), -np.sin(angle_radians), 0], + [np.sin(angle_radians), np.cos(angle_radians), 0], + [0, 0, 1]]) + + # Исходные координаты дрона + drone_coords = np.array(coord_drone) + + # Применяем поворот к координатам дрона + rotated_coords = np.dot(R, drone_coords) + + # Добавляем координаты нулевой позиции дрона + x = int(rotated_coords[0] - 10) + y = int(rotated_coords[1]) + z = int(rotated_coords[2]) + + return (State.MOVE, np.array([x, y, z], dtype=np.int32)) + + def find_marker(self, frame): + corners, ids, rejected = cv2.aruco.detectMarkers(frame, ARUCO_DICT, parameters=ARUCO_PARAMS) + if ids is not None: + for i in range(len(ids)): + if ids[i] == MARKER_CENTER: + return (ids[i], corners[i]) + if ids[i] == MARKER_STOP: + return (ids[i], corners[i]) + if ids[i] == MARKER_MOVE: + return (ids[i], corners[i]) + return None + + def make_action(self, m_corner): + self.last_time = time.process_time() + rvecs, tvecs, _ = cv2.aruco.estimatePoseSingleMarkers(np.array([m_corner]), MARKER_LEN, camera_matrix, + camera_distortion) + state = self.get_state(tvecs[0][0]) + if state == State.MOVE: + self.actions.put((state, None)) + self.last_state = state + + def marker_coords(self, m_corner, marker_id): + self.last_time = time.process_time() + _, tvecs, _ = cv2.aruco.estimatePoseSingleMarkers(np.array([m_corner]), MARKER_LEN, camera_matrix, + camera_distortion) + tvec = tvecs[0][0] + marker_x, marker_y, marker_z = tvec[2], tvec[0], tvec[1] + x = z = y = 0 + drone_x = x + marker_x + drone_y = y + marker_y + drone_z = z + marker_z + print(drone_x, drone_y, drone_x) + angle_radians = math.radians(self.num_of_rotates) + + # Преобразуем угол поворота в матрицу поворота + R = np.array([[np.cos(angle_radians), -np.sin(angle_radians), 0], + [np.sin(angle_radians), np.cos(angle_radians), 0], + [0, 0, 1]]) + + # Исходные координаты дрона + drone_coords = np.array([drone_x, drone_y, drone_z]) + + # Применяем поворот к координатам дрона + rotated_coords = np.dot(R, drone_coords) + + # Добавляем координаты нулевой позиции дрона + drone_x_zero = int(rotated_coords[0]) + drone_y_zero = int(rotated_coords[1]) + drone_z_zero = int(rotated_coords[2]) + + # Добавляем смещение для учета положения дрона относительно маркера + if marker_id == MARKER_CENTER: + x = y = z = 0 # may be z = 50 + x = x + drone_x_zero + y = y + drone_y_zero + z = z + drone_z_zero + self.coord_drone = (x, y, z) + print(f"{self.drone_id}, {x}, {y}, {z}\n") + elif marker_id == MARKER_MOVE: + x = y = z = 0 # may be z = 50 + x = x + drone_x_zero + self.coord_drone[0] + y = y + drone_y_zero + self.coord_drone[1] + z = z + drone_z_zero + self.coord_drone[2] + self.move_marker = (x,y,z) + return (x, y, z) + + def process_frame(self, frame): + ids = None + is_find = True + if self.last_state == State.READY and self.flag == int(self.swarm_coordinated): + print("State.READY") + ret = self.find_marker(frame) + if ret is not None: + print("МАРКЕР НАЙДЕН") + marker_id, marker_corner = ret + print(marker_id) + frame = cv2.aruco.drawDetectedMarkers(frame, np.array([marker_corner]), np.array([marker_id])) + if marker_id == MARKER_CENTER and not self.flag: + print("Send COORDS") + self.marker_coords(marker_corner, MARKER_CENTER) + print("START LINE COORDS") + print("degree rotate - ",self.num_of_rotates) + + # vvvvvvvvvvvv ОПРЕДЕЛИТЬ КОРРЕКТНЫЙ ИРЛ!!! vvvvvvvvvvvvvvvv + url = f"http://{""}:65001" + params = { + "command": "turn_starting_position", + "coordinates": self.coord_drone + } + req = request("exec", params) + response = requests.post(url, json=req) + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + print("END LINE COORDS") + if self.flag == 1 and self.rotate_flag == 0: + if marker_id == MARKER_MOVE: + coord = self.marker_coords(marker_corner, MARKER_MOVE) + # print(мы нашили маркер мув и угол) + url = f"http://{""}:65001" + params = { + "command": "save_coord_move_marker", + "coordinates": coord + } + req = request("exec", params) + response = requests.post(url, json=req) + if self.flag == 1: + if self.move_flag == 1: + state = self.move_to_marker() + self.actions.put(state) + self.last_state = state + self.move_flag = 0 + if marker_id == MARKER_STOP: + print("Send END") + # vvvvvvvvvvvv ОПРЕДЕЛИТЬ КОРРЕКТНЫЙ ИРЛ!!! vvvvvvvvvvvvvvvv + url = f"http://{""}:65001" + params = { + "command": "land" + } + req = request("exec", params) + response = requests.post(url, json=req) + # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + if marker_id == MARKER_CENTER: + if self.rotate_flag == 1: + print("Send ROTATE 2") + self.actions.put((State.ROTATE, None)) + self.last_state = State.ROTATE + self.rotate_flag = 0 + else: + url = f"http://{""}:65001" + params = { + "command": "set_rotate" + } + req = request("exec", params) + response = requests.post(url, json=req) + else: + if self.rotateflag == 1: + print("Send ROTATE 2") + self.actions.put((State.ROTATE, None)) + self.last_state = State.ROTATE + if self.flag == 1: + self.rotate_flag = 0 + + else: + print("Send ROTATE 1") + if self.rotate_flag == 0: + url = f"http://{""}:65001" + params = { + "command": "set_rotate" + } + req = request("exec", params) + response = requests.post(url, json=req) + if self.rotate_flag == 1: + self.actions.put((State.ROTATE, None)) + self.last_state = State.ROTATE + if self.flag == 1: + self.rotate_flag = 0 + if time.process_time() - self.last_time > TIMEOUT: + print("Send END2") + url = f"http://{""}:65001" + params = { + "command": "land", + } + req = request("exec", params) + response = requests.post(url, json=req) + + write_status_on_image(frame, {"Battery": f"{self.last_battery_charge}%", "Last state": self.last_state}) + self.video_writer.write(cv2.resize(frame, FRAME_SIZE)) + return frame + + def drone_instructions(self, tello): + tello.takeoff() + self.last_time = time.process_time() + self.last_state = State.READY + + while not self.is_stopped: + try: + state, movements = self.actions.get(block=False, timeout=0.25) + self.actions.task_done() + if state == State.END: + break + elif state == State.MOVE: + x, y, z = movements + print(movements) + tello.go_xyz_speed(int(x), int(y), int(z), SPEED) + elif state == State.ROTATE: + if self.num_of_rotate == 0: + + if self.num_of_rotates < 360: + x = ROTATE_DEGREE + tello.rotate_clockwise(x) + self.num_of_rotates += x + print("Rotate Degree", self.num_of_rotates) + + self.last_time -= 5 + else: + x = self.num_of_rotate + tello.rotate_clockwise(x) + self.num_of_rotate = 0 + print("Rotate on first pose", self.num_of_rotate) + self.num_of_rotates = 0 + except Empty: + self.last_state = State.READY + self.last_battery_charge = tello.get_battery() + tello.land() + + def turn_starting_position(self, coord): + if not self.flag and self.coord_drone == coord: + self.flag = 1 + self.num_of_rotate = 360 - self.num_of_rotates + self.actions.put((State.ROTATE, None)) + self.last_state = State.ROTATE + self.rotate_flag = 0 + return "OK" + + + def land(self): + self.actions.put((State.END, None)) + self.last_state = State.END + return "OK" + + def set_swarm_is_coordinated(self): + self.swarm_coordinated = True + return "OK" + + def set_rotate_flag(self): + self.rotate_flag = 1 + return "OK" + + def save_move_marker(self, coord): + self.move_flag = 1 + self.move_marker = coord + return "OK" + + def save_coord_all_drones(self, coord_all_drones): + self.coord_all_drones = coord_all_drones + return "OK" + + + def main(self): + self.is_stopped = False + drone = Drone(self.drone_instructions, self.process_frame, show_stream=True) + drone.run_program() + self.video_writer.release() \ No newline at end of file diff --git a/tests/docker_ci_workflow.yml b/tests/docker_ci_workflow.yml new file mode 100644 index 0000000..5df49e8 --- /dev/null +++ b/tests/docker_ci_workflow.yml @@ -0,0 +1,35 @@ +name: Docker CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Docker + uses: docker/setup-action@v2 + + - name: Build and run containers + run: | + docker-compose up -d + sleep 5 + docker ps + + - name: Check containers status + run: | + if docker ps | grep -q "droner"; then + echo "Containers are running successfully" + else + echo "Error occurred while running containers" + exit 1 + fi diff --git a/tests/start_and_check_containers.sh b/tests/start_and_check_containers.sh new file mode 100644 index 0000000..19a0d39 --- /dev/null +++ b/tests/start_and_check_containers.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Запуск контейнеров +docker-compose up -d + +# Проверка статуса контейнеров +if docker ps | grep -q "droner"; then + echo "Контейнеры успешно запущены" +else + echo "Произошла ошибка при запуске контейнеров" + exit 1 +fi diff --git a/wiki/swarm.md b/wiki/swarm.md new file mode 100644 index 0000000..a7bd1b5 --- /dev/null +++ b/wiki/swarm.md @@ -0,0 +1,137 @@ +# Swarm + +This project consists of three Python scripts: client.py, server.py, and swarm_server.py. + +## client.py + +This script is a CLI application that sends commands to a JSON-RPC server. It accepts two command-line arguments: +- -c or --command: The command to execute. +- --ip: The IP address of the JSON-RPC server. + +> Example usage: +python client.py -c "ls -l" --ip "127.0.0.1" + +## server.py + +This script implements a JSON-RPC server with a single method exec, which executes the provided command. + +**Method exec:** +- Description: + - Executes the provided command. +- Parameters: + - command (str): The command to execute. +- Returns: + - str: The executed command. + +**Request:** +```json +{ + "jsonrpc": "2.0", + "method": "exec", + "params": { + "command": "takeoff" + }, + "id": 1 +} +``` +**Response:** +```json + { + "jsonrpc": "2.0", + "result": "OK", + "id": 1 + } +``` +# swarm_server.py + +This script implements a JSON-RPC server similar to server.py, but it is designed to work in a swarm environment. It executes the provided command on all drones in the swarm. + +**Method exec:** +- Description: Executes the provided command on all drones in the swarm. +- Parameters: + - command (str): The command to execute. +- Returns: + - str: The status of the execution. + +**Request:** +```json + { + "jsonrpc": "2.0", + "method": "exec", + "params": { + "command": "ls -l" + }, + "id": 1 + } +``` +**Response:** +```json + { + "jsonrpc": "2.0", + "result": "OK", + "id": 1 + } +``` + +# networking.py - Networking Class + +Class for configuring network and routing based on a JSON configuration file. + +## Attributes +- `PORTS` (list): List of ports for routing. + +## Methods + +### \_\_init\_\_(config: dict) +Initializes the Networking class object. + +**Parameters:** +- `config` (dict): Network configuration in dictionary format. + +### \_\_connection(config: dict) +Establishes connection to Wi-Fi networks specified in the configuration. + +**Parameters:** +- `config` (dict): Network configuration in dictionary format. + +### \_\_routing(iface: str, port: int, ip: str) +Configures routing for a specific network interface, port, and IP address. + +**Parameters:** +- `iface` (str): Network interface. +- `port` (int): Port for routing. +- `ip` (str): IP address for routing. + +## read_network_configuration(json_path: str) -> dict +Reads and validates the network configuration from a JSON file. + +**Parameters:** +- `json_path` (str): Path to the JSON file containing the network configuration. + +**Returns:** +- dict: Network configuration in dictionary format. + +## main(json_path: str) +Main function of the program. + +**Parameters:** +- `json_path` (str): Path to the JSON file containing the network configuration. + + +# Docker Files and Docker Compose + +## Dockerfile.networking + +This Dockerfile is used to build the networking container. It installs necessary networking tools and sets up the environment for networking.py. + +## Dockerfile.swarm_node + +This Dockerfile is used to build a swarm node container. It installs Python and the required dependencies for JSON-RPC server. + +## Dockerfile.swarm_server + +This Dockerfile is used to build the swarm server container. It installs Python and the required dependencies for JSON-RPC server, JSON-RPC client, and requests library. + +## docker-compose.yml + +This Docker Compose file defines the services required for the JSON-RPC application. It includes the networking service, server service, and two drone services. The networking service is configured to run with host network mode and provide necessary network functionalities for the application. The server service is configured to depend on the networking service and two drone services. It exposes port 65001 for external access. The drone services are configured to depend on the networking service and provide IP addresses for the swarm network.