first init

This commit is contained in:
Alihan
2023-05-15 22:41:39 +03:00
commit cffd35cf90
19 changed files with 592 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
secrets.yml
__pycache__
venv
pids/
logs/
.ipynb_checkpoints
packages
*.ipynb

0
brain/__init__.py Normal file
View File

8
brain/brain.py Normal file
View File

@@ -0,0 +1,8 @@
from brain_openai import CloudChatBrain
brain = CloudChatBrain()
while True:
brain.listen()
brain.understand()
brain.command()

74
brain/brain_openai.py Normal file
View File

@@ -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()

18
brain/prompt.txt Normal file
View File

@@ -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:

0
commander/__init__.py Normal file
View File

26
commander/commander.py Normal file
View File

@@ -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")

56
commander/commands.py Normal file
View File

@@ -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}")

View File

9
commander/routes/base.py Normal file
View File

@@ -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"])

View File

@@ -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"}

View File

@@ -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 = """
<!DOCTYPE html>
<html>
<head>
<title>Websocket Example</title>
<script>
var ws = new WebSocket("ws://" + window.location.host + "/test/ws");
ws.onmessage = function(event) {
var progress_log = document.getElementById("progress-log");
progress_log.innerHTML += event.data + "<br>";
}
</script>
</head>
<body>
<h1>Websocket Example</h1>
<p>Progress:</p>
<div id="progress-log"></div>
</body>
</html>
"""

25
install.sh Executable file
View File

@@ -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

158
manage.sh Executable file
View File

@@ -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

View File

@@ -0,0 +1,4 @@
network={
ssid="djituad0_plus"
psk="0000000000"
}

View File

@@ -0,0 +1,4 @@
network={
ssid="uadis"
psk="0000000000"
}

0
requirements.txt Normal file
View File

12
settings/config.py Normal file
View File

@@ -0,0 +1,12 @@
from dynaconf import Dynaconf
# --- DynaConf ---------------------------------------------------------------------------------------------------------
settings = Dynaconf(
envvar_prefix="TELLO_COMMANDER",
environments=True,
settings_files=[
'settings/secrets.yml',
],
)

10
settings/config.yml Normal file
View File

@@ -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