From cffd35cf900a1086be2518c237a334cde1a8cb42 Mon Sep 17 00:00:00 2001 From: Alihan Date: Mon, 15 May 2023 22:41:39 +0300 Subject: [PATCH] first init --- .gitignore | 8 ++ brain/__init__.py | 0 brain/brain.py | 8 ++ brain/brain_openai.py | 74 +++++++++++++ brain/prompt.txt | 18 ++++ commander/__init__.py | 0 commander/commander.py | 26 +++++ commander/commands.py | 56 ++++++++++ commander/routes/__init__.py | 0 commander/routes/base.py | 9 ++ commander/routes/route_command.py | 106 +++++++++++++++++++ commander/routes/route_test.py | 74 +++++++++++++ install.sh | 25 +++++ manage.sh | 158 ++++++++++++++++++++++++++++ network/wpa_supp_djituad0_plus.conf | 4 + network/wpa_supp_uadis.conf | 4 + requirements.txt | 0 settings/config.py | 12 +++ settings/config.yml | 10 ++ 19 files changed, 592 insertions(+) create mode 100644 .gitignore create mode 100644 brain/__init__.py create mode 100644 brain/brain.py create mode 100644 brain/brain_openai.py create mode 100644 brain/prompt.txt create mode 100644 commander/__init__.py create mode 100644 commander/commander.py create mode 100644 commander/commands.py create mode 100644 commander/routes/__init__.py create mode 100644 commander/routes/base.py create mode 100644 commander/routes/route_command.py create mode 100644 commander/routes/route_test.py create mode 100755 install.sh create mode 100755 manage.sh create mode 100644 network/wpa_supp_djituad0_plus.conf create mode 100644 network/wpa_supp_uadis.conf create mode 100644 requirements.txt create mode 100644 settings/config.py create mode 100644 settings/config.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c29e49f --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +secrets.yml +__pycache__ +venv +pids/ +logs/ +.ipynb_checkpoints +packages +*.ipynb \ No newline at end of file diff --git a/brain/__init__.py b/brain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/brain/brain.py b/brain/brain.py new file mode 100644 index 0000000..2ce5999 --- /dev/null +++ b/brain/brain.py @@ -0,0 +1,8 @@ +from brain_openai import CloudChatBrain + + +brain = CloudChatBrain() +while True: + brain.listen() + brain.understand() + brain.command() diff --git a/brain/brain_openai.py b/brain/brain_openai.py new file mode 100644 index 0000000..acd4bbe --- /dev/null +++ b/brain/brain_openai.py @@ -0,0 +1,74 @@ +import os +import sys +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import json +import ast + +import openai + +from commander.commands import CommandHandler +from settings.config import settings + + + +class CloudSTTBrain: + def __init__(self): + print("not implemented") + + +class CloudChatBrain: + def __init__(self): + openai.api_key = settings.OPENAI_API_KEY + self.command_handler = CommandHandler() + + @property + def sys_prompt(self): + return self._read_prompt() + + def _read_prompt(self): + prompt_file_name = "prompt.txt" + for root, dirs, files in os.walk("./"): + if prompt_file_name in files: + prompt_filepath = os.path.join(root, prompt_file_name) + with open(prompt_filepath, "r") as f: + return f.read() + + def _is_valid_json(self, answer): + try: + response_json = json.loads(answer) + return True + except ValueError as e: + print(f"chatgpt failed to return json obj: {answer}") + return False + + def _gc(self): + self.cmd_prompt = None + self.response = None + + def listen(self): + self.cmd_prompt = input("\n\nwhat should I do now?\n\t") + + def understand(self): + self.response = openai.ChatCompletion.create( + model="gpt-3.5-turbo", + temperature=0.2, + messages=[ + {"role": "system", "content": self.sys_prompt}, + {"role": "user", "content": self.cmd_prompt} + ]) + + def command(self): + answer = self.response.choices[0].message.content + if self._is_valid_json(answer): + command = ast.literal_eval(answer) + if command == {}: + print(f"I failed to understand: {command}") + else: + print(f"I will send this command: {command}") + self.command_handler.handle(command) + else: + print(f"\tI will skip this:\n {command}") + self._gc() + + + diff --git a/brain/prompt.txt b/brain/prompt.txt new file mode 100644 index 0000000..f4b65fa --- /dev/null +++ b/brain/prompt.txt @@ -0,0 +1,18 @@ +You are a flight controller API endpoint in a quadcopter. +Your receive commands are in Turkish. +Only provide a RFC8259 compliant JSON response following this format without deviation in order to translate regular speech commands. If you get a command not related to below schema you return empty JSON object {} and nothing else. +CommandSchema: + "command" : string + "direction": string + "distance_angle": int + +You only recognize, +- commands: move, turn, takeoff, land +- direction: up, down, left, right, forward, back +- distance_angle: ant integer value + +For any "move" command distance_angle should be defaulted to 25. +for any "turn" command the distance_angle should be defaulted to 30. +Never mention being a Language Model AI or any notes. You must only respond with JSON. Do no write normal text. + +The JSON response: \ No newline at end of file diff --git a/commander/__init__.py b/commander/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/commander/commander.py b/commander/commander.py new file mode 100644 index 0000000..c214bc1 --- /dev/null +++ b/commander/commander.py @@ -0,0 +1,26 @@ +# source: https://djitellopy.readthedocs.io/en/latest/tello/ + +import sys +import time + +from fastapi import FastAPI +import uvicorn + +from routes.base import api_router + + + +def start_application(): + app = FastAPI() + app.include_router(api_router) + return app + + +if __name__ == "__main__": + if len(sys.argv) == 1: + port = 8889 + else: + port = int(sys.argv[1]) + + app = start_application() + uvicorn.run(app, host="0.0.0.0", port=port, log_level="debug") diff --git a/commander/commands.py b/commander/commands.py new file mode 100644 index 0000000..060842f --- /dev/null +++ b/commander/commands.py @@ -0,0 +1,56 @@ +import requests + + + + +class CommandHandler: + + def __init__(self): + self.COMMANDER_ROOT_URL = "http://0.0.0.0:8889" + self.COMMANDER_COMMANDS_URL = f"{self.COMMANDER_ROOT_URL}/command" + self._check_commander_health() + + def _check_commander_health(self): + response = requests.get(f"{self.COMMANDER_ROOT_URL}/test/health") + status = response.json() + if status["msg"] != "ok": + raise Exception(f"commander service is unavailable: {self.COMMANDER_ROOT_URL}") + + def _move(self, direction, distance): + response = requests.get(f"{self.COMMANDER_COMMANDS_URL}/move/{direction}/{distance}") + print(response.json()) + + def _turn(self, direction, degree): + response = requests.get(f"{self.COMMANDER_COMMANDS_URL}/turn/{direction}/{degree}") + print(response.json()) + + def _takeoff(self): + response = requests.get(f"{self.COMMANDER_COMMANDS_URL}/takeoff") + print(response.json()) + + def _land(self): + response = requests.get(f"{self.COMMANDER_COMMANDS_URL}/land") + print(response.json()) + + def _end_session(self): + response = requests.get(f"{self.COMMANDER_COMMANDS_URL}/end") + print(response.json()) + + def handle(self, cmd: dict): + #print(f"commanding for: {cmd}") + if cmd["command"] == "move": + self._move( + direction=cmd["direction"], + distance=cmd["distance_angle"] + ) + elif cmd["command"] == "turn": + self._turn( + direction=cmd["direction"], + degree=cmd["distance_angle"] + ) + elif cmd["command"] == "takeoff": + self._takeoff() + elif cmd["command"] == "land": + self._land() + else: + raise ValueError(f"Uknown command object: {cmd}") diff --git a/commander/routes/__init__.py b/commander/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/commander/routes/base.py b/commander/routes/base.py new file mode 100644 index 0000000..0f495ad --- /dev/null +++ b/commander/routes/base.py @@ -0,0 +1,9 @@ +from fastapi import APIRouter +from routes import route_test, route_command + + + +api_router = APIRouter() + +api_router.include_router(route_test.router, prefix="/test", tags=["tests"]) +api_router.include_router(route_command.router, prefix="/command", tags=["commands"]) diff --git a/commander/routes/route_command.py b/commander/routes/route_command.py new file mode 100644 index 0000000..d023e8f --- /dev/null +++ b/commander/routes/route_command.py @@ -0,0 +1,106 @@ +import asyncio, time +import threading + +from fastapi import APIRouter, Query, BackgroundTasks +from djitellopy import Tello + + +class FlightStatsCollector: + def __init__(self): + self.stats_thread = None + self.stop_event = threading.Event() + + def start_collecting(self): + self.stats_thread = threading.Thread(target=self.collect_stats) + self.stats_thread.start() + + def stop_collecting(self): + self.stop_event.set() + if self.stats_thread: + self.stats_thread.join() + + def collect_stats(self): + while not self.stop_event.is_set(): + tello.send_command_with_return("command") + bat = tello.get_battery() + temp = tello.get_temperature() + # wsnr = tello.query_wifi_signal_noise_ratio() + print(f"bat: {bat} - temp: {temp}") #- wsnr: {wsnr}") + time.sleep(5) + +router = APIRouter() +tello = Tello() +stats_collector = FlightStatsCollector() + + +def land_on_low_battery(): + bat_level = tello.get_battery() + if bat_level < 20: + tello.land() + + + +@router.get("/takeoff") +def takeoff(): + try: + tello.connect() + except Exception as e: + return {"msg": "failed to connect"} + + if tello.is_flying: + return {"msg": "already flying"} + + tello.takeoff() + stats_collector.start_collecting() + + if tello.is_flying: + return {"msg": "ok"} + else: + return {"msg": "takeoff failed"} + +@router.get("/land") +def land(): + if not tello.is_flying: + return {"msg": "already on land"} + + tello.land() + stats_collector.stop_collecting() + + if not tello.is_flying: + return {"msg": "ok"} + +@router.get("/turn/{direction}/{degree}") +def turn(direction: str, degree: int): + if direction not in ["left", "right"]: + return {"direction must be only left or right"} + if degree < 1 or degree > 360: + return {"degree must be between 1 and 360"} + + if direction == "right": + tello.rotate_clockwise(degree) + elif direction == "left": + tello.rotate_counter_clockwise(degree) + + return {"msg": "ok"} + +@router.get("/move/{direction}/{distance}") +def turn(direction: str, distance: int): + if direction not in ["back", "forward", "left", "right", "up" , "down"]: + return {"direction must be only back, forward, left, right up or down"} + if distance < 20 or distance > 200: + return {"distance must be between 20 and 500"} + + try: + tello.move(direction, distance) + except Exception as e: + return { + "msg": "command failed", + "reason": e + } + + return {"msg": "ok"} + +@router.get("/end") +def end_flight_session(): + tello.end() + return {"msg": "ok"} diff --git a/commander/routes/route_test.py b/commander/routes/route_test.py new file mode 100644 index 0000000..8547a06 --- /dev/null +++ b/commander/routes/route_test.py @@ -0,0 +1,74 @@ +import asyncio, time + +from fastapi.websockets import WebSocket +from fastapi.responses import HTMLResponse +from fastapi import APIRouter +from djitellopy import Tello + + +router = APIRouter() +tello = Tello() + + +@router.get("/health") +def test(): + return {"msg": "ok"} + +@router.get("/flight") +def test_flight(): + try: + tello.connect() + except Exception as e: + return {"msg": "failed to connect"} + tello.takeoff() + if not tello.is_flying: + return {"msg": "failed to take off"} + tello.rotate_counter_clockwise(180) + time.sleep(2) + tello.rotate_clockwise(180) + time.sleep(5) + tello.land() + if not tello.is_flying: + return {"msg": "succesfully landed"} + else: + return {"msg": "landing failed, still flying!!!"} + +@router.get("/continuous-response") +async def test_continuous_response(): + return HTMLResponse(html_template) + +@router.websocket("/ws") +async def websocket_endpoint(websocket: WebSocket): + await websocket.accept() + async for message in generate_output(): + await websocket.send_text(message) + + +async def generate_output(): + for i in range(10): + await asyncio.sleep(1) + yield f"Progress: {i + 1}\n" + yield "Done!\n" + + +html_template = """ + + + + Websocket Example + + + +

Websocket Example

+

Progress:

+
+ + +""" diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..5a904d2 --- /dev/null +++ b/install.sh @@ -0,0 +1,25 @@ +#!/bin/bash + + +ARCH=$(uname -m) + +if [ "$ARCH" = "x86_64" ]; then + ARCH="amd64" + +elif [ "$ARCH" = "armv6l" ]; then + ARCH="arm" + +fi +echo "found architecture $ARCH"; sleep 3 + +rm yq_linux_* && \ +wget https://github.com/mikefarah/yq/releases/download/v4.33.3/yq_linux_$ARCH && \ +mv yq_linux_$ARCH yq && \ +chmod +x yq && \ +mv yq /home/uad/.local/bin + +echo "installing virtualenv"; sleep 3 + +virtualenv venv +pip install -r requirements.txt + diff --git a/manage.sh b/manage.sh new file mode 100755 index 0000000..1b29252 --- /dev/null +++ b/manage.sh @@ -0,0 +1,158 @@ +#!/bin/bash + + +DRONE_INTERFACE=$(yq '.commander.drone_interface' < config.yml) +DRONE_WPA_SUPP_CONF=$(yq '.commander.drone_wpa_supp' < config.yml) +NET_INTERFACE=$(yq '.commander.net_interface' < config.yml) +NET_WPA_SUPP_CONF=$(yq '.commander.net_wpa_supp' < config.yml) + + +list_related_network_interface_status() { + networkctl list +} + +list_wifi_ssid(){ + sudo iw dev $DRONE_INTERFACE scan | grep "SSID: " +} + +connect_using_wpa_supp() { + sudo wpa_supplicant -D nl80211 -i $DRONE_INTERFACE -c network/$DRONE_WPA_SUPP_CONF +} + +get_dhcp_ip () { + sudo dhclient $DRONE_INTERFACE +} + +start_jupyter() { + venv/bin/python -m jupyter lab --ip='0.0.0.0' --NotebookApp.token='' --NotebookApp.password='' --no-browser --port=8888 +} + +start_codeserver(){ + packages/code-server/code-server-4.12.0-linux-amd64/bin/code-server --config /home/uad/misc/tello-commander/packages/code-server/config.yaml --disable-getting-started-override --disable-workspace-trust --disable-telemetry ./ +} + +start_commander_service() { + venv/bin/python commander/commander.py $1 +} + +kill_everything() { + for p in $pids_dir/*.txt; do echo "killing $p"; pkill $(cat "$p"); done +} + + + +pids_dir='./pids' +if [[ ! -d "$pids_dir" ]]; then + mkdir $pids_dir +fi + + + +## NETWORK +if [ "$1" == "list-network" ]; then + list_related_network_interface_status + +elif [ "$1" == "list-wifis" ]; then + list_wifi_ssid #connect $2 $3 + +elif [ "$1" == "connect-drone" ]; then + connect_using_wpa_supp > logs/wpa_supp.log 2>&1 & + wpa_supp_pid=$! + echo "started wpa supplicant to connect drone network with PID $wpa_supp_pid" + echo $wpa_supp_pid > $pids_dir/wpa_supp_pid.txt + +elif [ "$1" == "disconnect-drone" ]; then + wpa_supp_pid_file="$pids_dir/wpa_supp_pid.txt" + if [ -f "$wpa_supp_pid_file" ]; then + pkill -P $(cat $wpa_supp_pid_file) + echo "stopped drone connection via wpa_supp" + fi + +elif [ "$1" == "get-dhcp" ]; then + get_dhcp_ip > logs/dhcp_ip.log 2>&1 & + dhcp_ip_pid=$! + echo "requested ip address from drone router with PID $dhcp_ip_pid" + echo $dhcp_ip_pid > $pids_dir/dhcp_ip_pid.txt + +elif [ "$1" == "kill-dhcp" ]; then + dhcp_ip_pid_file="$pids_dir/dhcp_ip_pid.txt" + if [ -f "$dhcp_ip_pid_file" ]; then + pkill -P $(cat $dhcp_ip_pid_file) + echo "killed dhcp client" + fi + + +## DEV +elif [ "$1" == "start-jupyter" ]; then + start_jupyter > logs/jupyter.log 2>&1 & + jupyter_pid=$! + echo "started jupyter with PID $jupyter_pid" + echo $jupyter_pid > $pids_dir/jupyter_pid.txt + #tail -f logs/jupyter.log + +elif [ "$1" == "stop-jupyter" ]; then + jupyter_pid_file="$pids_dir/jupyter_pid.txt" + if [ -f "$jupyter_pid_file" ]; then + pkill -P $(cat $jupyter_pid_file) + echo "stopped jupyter" + fi + +elif [ "$1" == "start-cs" ]; then + start_codeserver > logs/codeserver.log 2>&1 & + codeserver_pid=$! + echo "started code server with PID $codeserver_pid" + echo $codeserver_pid > $pids_dir/codeserver_pid.txt + #tail -f logs/codeserver.log + +elif [ "$1" == "stop-cs" ]; then + codeserver_pid_file="$pids_dir/codeserver_pid.txt" + if [ -f "$codeserver_pid_file" ]; then + pkill -P $(cat $codeserver_pid_file) + echo "stopped code server" + fi + +elif [ "$1" == "stop-all" ]; then + kill_everything + + + +## DRONE +elif [ "$1" == "start-commander" ]; then + start_commander_service $2 > logs/commander.log 2>&1 & + commander_pid=$! + echo "started commander with PID $commander_pid" + echo $commander_pid > $pids_dir/commander_pid.txt + #tail -f logs/commander.log + +elif [ "$1" == "stop-commander" ]; then + commander_pid_file="$pids_dir/commander_pid.txt" + if [ -f "$commander_pid_file" ]; then + pkill -P $(cat $commander_pid_file) + echo "stopped commander" + fi + +elif [ "$1" == "prepare-flight" ]; then + ./manage.sh connect-drone + ./manage.sh get-dhcp + ./manage.sh start-commander + echo "prepared to flight" + +elif [ "$1" == "finish-flight" ]; then + ./manage.sh disconnect-drone + ./manage.sh kill-dhcp + ./manage.sh stop-commander + echo "flight finished" + exit 0 + +else + echo "Invalid command. Please use one of: + - list-network + - list-wifis + - connect-/ disconnect-drone + - get-/ kill-dhcp + - start-/ stop-jupyter + - start-/ stop-cs + - start-/ stop-commander [port] + - stop-all + - prepare-/ finish-flight" +fi diff --git a/network/wpa_supp_djituad0_plus.conf b/network/wpa_supp_djituad0_plus.conf new file mode 100644 index 0000000..84ab37f --- /dev/null +++ b/network/wpa_supp_djituad0_plus.conf @@ -0,0 +1,4 @@ +network={ + ssid="djituad0_plus" + psk="0000000000" +} diff --git a/network/wpa_supp_uadis.conf b/network/wpa_supp_uadis.conf new file mode 100644 index 0000000..cb376bd --- /dev/null +++ b/network/wpa_supp_uadis.conf @@ -0,0 +1,4 @@ +network={ + ssid="uadis" + psk="0000000000" +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/settings/config.py b/settings/config.py new file mode 100644 index 0000000..dea9f6c --- /dev/null +++ b/settings/config.py @@ -0,0 +1,12 @@ +from dynaconf import Dynaconf + +# --- DynaConf --------------------------------------------------------------------------------------------------------- + +settings = Dynaconf( + envvar_prefix="TELLO_COMMANDER", + environments=True, + settings_files=[ + 'settings/secrets.yml', + ], +) + diff --git a/settings/config.yml b/settings/config.yml new file mode 100644 index 0000000..7a6f111 --- /dev/null +++ b/settings/config.yml @@ -0,0 +1,10 @@ +commander: + drone_interface: + wlx14cc201488aa + net_interface: + wlan0 + drone_wpa_supp: + wpa_supp_djituad0_plus.conf + net_wpa_supp: + wpa_supp_uadis.conf + \ No newline at end of file