Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,17 @@ fabric.properties

# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser

# virtualenv
venv/
__pycache__/

# logs if you prefer download
chatbox/logs/

# env
.env

# node
node_modules/
build/
131 changes: 101 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,48 +1,119 @@
# UAV Log Viewer
# UAVLogViewer Chatbot Extension

![log seeking](preview.gif "Logo Title Text 1")
This repository adds a Groq-powered, agentic chatbot backend to the UAVLogViewer project. It enables natural language queries against parsed MAVLink `.bin` (DataFlash) and `.tlog` (telemetry) log files.

This is a Javascript based log viewer for Mavlink telemetry and dataflash logs.
[Live demo here](http://plot.ardupilot.org).
## Features

## Build Setup
* **Multi-format support:** Parses both `.bin` (ArduPlane DataFlash logs) and `.tlog` (MAVLink telemetry logs).
* **Agentic behavior:** Maintains context across turns and asks clarifying questions when data is missing or ambiguous.
* **Anomaly reasoning:** Provides flexible, model-driven anomaly detection hints (e.g., GPS drops, altitude spikes, battery overheating, RC loss).
* **Example queries:**

``` bash
# install dependencies
npm install
* "What was the highest altitude reached during the flight?"
* "When did the GPS signal first get lost?"
* "What was the maximum battery temperature?"
* "How long was the total flight time?"
* "List all critical errors that happened mid-flight."
* "Are there any anomalies in this flight?"

# serve with hot reload at localhost:8080
npm run dev
## Requirements

# build for production with minification
npm run build
* Python 3.9 or higher
* A valid Groq API Key (sign up at [https://console.groq.com/](https://console.groq.com/))
* Sample `.bin` or `.tlog` files for testing (place in `chatbot/logs/`)

# run unit tests
npm run unit
## Manual Setup Steps

# run e2e tests
npm run e2e
1. **Clone the repository**

# run all tests
npm test
```bash
git clone https://github.com/zhuhaiweiyan/UAVLogViewer.git
cd UAVLogViewer/chatbot
```

2. **Create a virtual environment**

```bash
python -m venv venv
```

3. **Activate the environment**

* On **Linux/macOS/WSL/Git Bash**:

```bash
source venv/bin/activate
```
* On **Windows CMD**:

```cmd
venv\Scripts\activate
```

4. **Install dependencies**

```bash
pip install fastapi uvicorn python-dotenv pyserial pymavlink requests
```

5. **Configure environment variables**

```bash
touch .env
```

Edit `.env` and set:

```ini
GROQ_API_KEY=your_api_key_here
GROQ_MODEL=llama3-70b-8192
```

6. **Place log files**
Copy your `.bin` and `.tlog` files into:

```bash
chatbot/logs/
```

## Running the Chatbot Server

Start the FastAPI backend:

```bash
uvicorn app:app --reload --host 0.0.0.0 --port 5000
```

# Docker
The interactive documentation is available at `http://localhost:5000/docs`.

``` bash
## Sample API Call

# Build Docker Image
docker build -t <your username>/uavlogviewer .
```bash
curl -X POST http://localhost:5000/chat \
-H "Content-Type: application/json" \
-d '{"question":"What was the highest altitude reached during the flight?","history":[]}'
```

## Batch Testing Logs

1. Ensure `.bin`/`.tlog` files are in `chatbot/logs/`.
2. Run the batch test script:

# Run Docker Image
docker run -p 8080:8080 -d <your username>/uavlogviewer
```bash
python -m tests.chatbot_test
```
3. Review generated Markdown reports in `chatbot/tests/` (`chatbot_test_batch_1.md`, etc.).

# View Running Containers
docker ps
## Cleanup

# View Container Log
docker logs <container id>
To remove generated artifacts and environment:

```bash
rm -rf venv
rm -rf chatbot/logs/*.md
```

# Navigate to localhost:8080 in your web browser
---
video link: https://youtu.be/0zR7l74glHQ

```
Maintained by **Your Name** for the Arena Software Engineering Take-Home Challenge. For questions, contact [angeuswork@gmail.com](mailto:angeuswork@gmail.com).
130 changes: 130 additions & 0 deletions chatbot/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import os
import requests
from fastapi import FastAPI
from pydantic import BaseModel
from dotenv import load_dotenv
from log_parser import parse_log

load_dotenv()

GROQ_API_KEY = os.getenv("GROQ_API_KEY")
GROQ_MODEL = os.getenv("GROQ_MODEL", "llama3-70b-8192")
BASE_URL = "https://api.groq.com/openai/v1"
TIMEOUT = 15

if not GROQ_API_KEY:
raise RuntimeError("GROQ_API_KEY missing")

app = FastAPI()


class ChatMessage(BaseModel):
role: str
content: str


class ChatRequest(BaseModel):
question: str
history: list[ChatMessage] = []


@app.post("/chat")
async def chat(request: ChatRequest):
log_dir = os.path.join(os.path.dirname(__file__), "logs")
log_files = [f for f in os.listdir(
log_dir) if f.endswith(".bin") or f.endswith(".tlog")]

if not log_files:
return {
"question": request.question,
"answer": "No .bin or .tlog file found in logs/ directory.",
"error": "Missing flight log"
}

log_path = os.path.join(log_dir, log_files[0])

telemetry = parse_log(log_path)
try:
answer = ask_llm(request.question, request.history, telemetry)
return {
"question": request.question,
"answer": answer,
"altitude_preview": telemetry.get("altitudes", [])[:2],
"gps_preview": telemetry.get("gps_status", [])[:2],
}
except Exception as e:
return {
"question": request.question,
"answer": "Error",
"error": str(e)
}


def ask_llm(question: str, history: list, telemetry: dict) -> str:
altitudes = telemetry.get("altitudes", [])
gps = telemetry.get("gps_status", [])
errors = telemetry.get("errors", [])
rc_loss = telemetry.get("rc_loss", [])
rc_loss_times = telemetry.get("rc_loss_times", [])
battery_temps = telemetry.get("battery_temps", [])
gps_times = telemetry.get("gps_times", [])

alt_values = [a.get("alt_amsl")
for a in altitudes if a.get("alt_amsl") is not None]
max_altitude = max(alt_values) if alt_values else "N/A"
flight_duration = (gps_times[-1] - gps_times[0]) / \
1e6 if len(gps_times) >= 2 else "N/A"

summary = f"""Summary of parsed telemetry data:
- Total altitude samples: {len(altitudes)}
- Max altitude (AMSL): {max_altitude}
- GPS samples: {len(gps)}
- First GPS fix: {gps[0] if gps else 'N/A'}
- Battery temperatures (°C): {battery_temps[:5] if battery_temps else 'None'}
- RC signal losses (count): {len(rc_loss)}
- First RC loss timestamp: {rc_loss_times[0] if rc_loss_times else 'None'}
- Error messages: {errors[:5] if errors else 'None'}
- Estimated flight duration (s): {flight_duration}

Anomaly Detection Hints:
- Look for sudden drops or spikes in altitude.
- Check for high battery temperatures (> 60°C).
- Detect if GPS fix was lost or inconsistent.
- Consider any 'fail', 'error', or 'lost' messages as potential issues.
- RC signal loss indicates a critical control problem.
"""

system_prompt = {
"role": "system",
"content": (
"You are an expert UAV flight assistant with deep knowledge of MAVLink telemetry. "
"You analyze parsed flight data and assist users in understanding flight performance and anomalies. "
"Maintain conversation state across turns; ask clarifying questions if user queries are ambiguous. "
"Reference the provided telemetry summary dynamically. "
"You can answer questions like 'What was the highest altitude reached during the flight?', "
"'When did the GPS signal first get lost?', 'What was the maximum battery temperature?', "
"'How long was the total flight time?', 'List all critical errors that happened mid-flight.', "
"'When was the first instance of RC signal loss?' "
"Additionally, you are flight-aware: reason about anomalies without hard-coded rules, infer patterns, thresholds, and inconsistencies. "
"Explain your reasoning clearly, and if data is insufficient, ask the user for more information."
)
}

messages = [system_prompt] + history + [
{"role": "user", "content": f"{question}\n\n{summary}"}
]

headers = {
"Authorization": f"Bearer {GROQ_API_KEY}",
"Content-Type": "application/json"
}
body = {"model": GROQ_MODEL, "messages": messages}

response = requests.post(
f"{BASE_URL}/chat/completions",
json=body,
headers=headers,
timeout=TIMEOUT
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
57 changes: 57 additions & 0 deletions chatbot/log_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import os
import time
from pymavlink import mavutil

def parse_log(path):
ext = os.path.splitext(path)[1].lower()

if ext == ".bin":
conn = mavutil.mavlink_connection(path, dialect="ardupilotmega", robust_parsing=True)
elif ext == ".tlog":
conn = mavutil.mavlink_connection(path, baud=57600, robust_parsing=True)
else:
return {"error": f"Unsupported: {ext}"}

data = {
"altitudes": [], "gps_status": [], "errors": [],
"rc_loss": [], "battery_temps": [], "gps_times": [], "rc_loss_times": []
}

start, max_dur, max_msgs = time.time(), 5, 2000
count = 0

while count < max_msgs and (time.time() - start) < max_dur:
msg = conn.recv_match(blocking=False)
if not msg or msg.get_type() == "BAD_DATA":
continue
count += 1
t = msg.get_type()

if t == "GPS_RAW_INT":
data["gps_status"].append({
"fix_type": msg.fix_type, "satellites_visible": msg.satellites_visible,
"eph": msg.eph, "epv": msg.epv
})
if hasattr(msg, "time_usec"):
data["gps_times"].append(msg.time_usec)

elif t == "ALTITUDE":
data["altitudes"].append({
"alt_amsl": msg.alt_amsl, "alt_local": msg.alt_local
})

elif t == "STATUSTEXT":
text = msg.text.lower()
if any(k in text for k in ("fail", "lost", "error")):
data["errors"].append(text)
if "rc" in text and "lost" in text:
data["rc_loss"].append(text)
if hasattr(msg, "_timestamp"):
data["rc_loss_times"].append(msg._timestamp)

elif t == "BATTERY_STATUS":
temp = msg.temperature
if temp is not None and temp != -1:
data["battery_temps"].append(temp / 100.0)

return data
Binary file not shown.
Binary file not shown.
Loading