first init
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
secrets.yml
|
||||
__pycache__
|
||||
venv
|
||||
pids/
|
||||
logs/
|
||||
.ipynb_checkpoints
|
||||
packages
|
||||
*.ipynb
|
||||
0
brain/__init__.py
Normal file
0
brain/__init__.py
Normal file
8
brain/brain.py
Normal file
8
brain/brain.py
Normal 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
74
brain/brain_openai.py
Normal 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
18
brain/prompt.txt
Normal 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
0
commander/__init__.py
Normal file
26
commander/commander.py
Normal file
26
commander/commander.py
Normal 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
56
commander/commands.py
Normal 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}")
|
||||
0
commander/routes/__init__.py
Normal file
0
commander/routes/__init__.py
Normal file
9
commander/routes/base.py
Normal file
9
commander/routes/base.py
Normal 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"])
|
||||
106
commander/routes/route_command.py
Normal file
106
commander/routes/route_command.py
Normal 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"}
|
||||
74
commander/routes/route_test.py
Normal file
74
commander/routes/route_test.py
Normal 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
25
install.sh
Executable 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
158
manage.sh
Executable 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
|
||||
4
network/wpa_supp_djituad0_plus.conf
Normal file
4
network/wpa_supp_djituad0_plus.conf
Normal file
@@ -0,0 +1,4 @@
|
||||
network={
|
||||
ssid="djituad0_plus"
|
||||
psk="0000000000"
|
||||
}
|
||||
4
network/wpa_supp_uadis.conf
Normal file
4
network/wpa_supp_uadis.conf
Normal file
@@ -0,0 +1,4 @@
|
||||
network={
|
||||
ssid="uadis"
|
||||
psk="0000000000"
|
||||
}
|
||||
0
requirements.txt
Normal file
0
requirements.txt
Normal file
12
settings/config.py
Normal file
12
settings/config.py
Normal 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
10
settings/config.yml
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user