Skip to content

Commit cc8f25e

Browse files
committed
Add debug logging and message transformation for device telemetry
- Implement MessageDebugger for capturing and analyzing raw device messages - Add debug logging endpoint to retrieve device message analysis - Enhance telemetry message handling with format transformation - Improve error handling and logging for device message processing - Add support for detecting and converting non-standard telemetry formats
1 parent 3e4c68a commit cc8f25e

5 files changed

Lines changed: 260 additions & 38 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ MANIFEST
2323
.pytest_cache/
2424
.coverage
2525
htmlcov/
26-
26+
debug_logs/
2727
# Virtual Environment
2828
.venv/
2929
env/

examples/simulated_device/simulated_device.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
logger.info(f"websockets version: {pkg_resources.get_distribution('websockets').version}")
3636

3737
# Configuration
38-
WS_SERVER_URL = "ws://localhost:8000/ws/device/{device_id}"
38+
WS_SERVER_URL = "ws://ec2-100-27-211-169.compute-1.amazonaws.com:8000/ws/device/{device_id}"
3939
DEVICE_ID = f"simulated-boat-1"
4040
TELEMETRY_INTERVAL = 1.0 # Send telemetry every 1 second
4141

examples/web_client/static/js/app.js

Lines changed: 30 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -600,42 +600,36 @@ class PiBoatClient {
600600

601601
// Update telemetry UI with the latest data
602602
updateTelemetryUI() {
603-
// GPS data
604-
if (this.telemetryData.sensor_data && this.telemetryData.sensor_data.data) {
605-
const sensorData = this.telemetryData.sensor_data.data;
606-
607-
// GPS
608-
if (sensorData.gps) {
609-
document.getElementById('gps-lat').textContent = `Latitude: ${sensorData.gps.latitude || '--'}`;
610-
document.getElementById('gps-lon').textContent = `Longitude: ${sensorData.gps.longitude || '--'}`;
611-
document.getElementById('gps-heading').textContent = `Heading: ${sensorData.gps.heading || '--'}°`;
612-
document.getElementById('gps-speed').textContent = `Speed: ${sensorData.gps.speed || '--'} knots`;
613-
}
614-
615-
// System status
616-
if (sensorData.status) {
617-
document.getElementById('system-status').textContent = `Status: ${sensorData.status}`;
618-
}
619-
620-
// Battery
621-
if (sensorData.battery) {
622-
document.getElementById('battery-level').textContent = `Battery: ${sensorData.battery.level || '--'}%`;
623-
}
624-
625-
// System data
626-
if (sensorData.system) {
627-
document.getElementById('cpu-temp').textContent = `CPU Temp: ${sensorData.system.cpu_temp || '--'}°C`;
628-
document.getElementById('signal-strength').textContent = `Signal: ${sensorData.system.signal_strength || '--'} dBm`;
629-
}
630-
631-
// Environment data
632-
if (sensorData.environment) {
633-
document.getElementById('water-temp').textContent = `Water Temp: ${sensorData.environment.water_temp || '--'}°C`;
634-
document.getElementById('air-temp').textContent = `Air Temp: ${sensorData.environment.air_temp || '--'}°C`;
635-
document.getElementById('air-pressure').textContent = `Pressure: ${sensorData.environment.air_pressure || '--'} hPa`;
636-
document.getElementById('humidity').textContent = `Humidity: ${sensorData.environment.humidity || '--'}%`;
637-
}
638-
}
603+
// Create a container for all telemetry data
604+
const telemetryContainer = document.querySelector('.telemetry-panels');
605+
606+
// Clear existing content
607+
telemetryContainer.innerHTML = '';
608+
609+
// Create a single panel for raw telemetry
610+
const rawPanel = document.createElement('div');
611+
rawPanel.className = 'telemetry-panel raw-telemetry';
612+
rawPanel.style.width = '100%';
613+
614+
// Create header
615+
const header = document.createElement('h3');
616+
header.textContent = 'Raw Telemetry Data';
617+
rawPanel.appendChild(header);
618+
619+
// Create pre element for formatted JSON
620+
const pre = document.createElement('pre');
621+
pre.className = 'raw-telemetry-data';
622+
pre.style.maxHeight = '300px';
623+
pre.style.overflow = 'auto';
624+
pre.style.whiteSpace = 'pre-wrap';
625+
pre.style.textAlign = 'left';
626+
627+
// Format the telemetry data as JSON
628+
const formattedJson = JSON.stringify(this.telemetryData, null, 2);
629+
pre.textContent = formattedJson || 'No telemetry data available';
630+
631+
rawPanel.appendChild(pre);
632+
telemetryContainer.appendChild(rawPanel);
639633
}
640634

641635
// Select a device

server/debug_tools.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import logging
2+
import os
3+
import json
4+
import time
5+
from datetime import datetime
6+
from pathlib import Path
7+
8+
logger = logging.getLogger(__name__)
9+
10+
class MessageDebugger:
11+
"""Utility to capture and log raw device messages for debugging."""
12+
13+
def __init__(self, debug_dir="debug_logs"):
14+
self.debug_dir = debug_dir
15+
self.ensure_debug_dir()
16+
17+
def ensure_debug_dir(self):
18+
"""Ensure the debug directory exists."""
19+
os.makedirs(self.debug_dir, exist_ok=True)
20+
21+
def capture_device_message(self, device_id, message):
22+
"""Capture a raw device message to a debug file.
23+
Messages are appended to a single file per device."""
24+
try:
25+
filename = f"{self.debug_dir}/device_{device_id}_log.json"
26+
27+
# Create entry for this message
28+
entry = {
29+
"timestamp": time.time(),
30+
"datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
31+
"device_id": device_id,
32+
"message": message
33+
}
34+
35+
# Read existing data if file exists
36+
messages = []
37+
if os.path.exists(filename):
38+
try:
39+
with open(filename, 'r') as f:
40+
messages = json.load(f)
41+
if not isinstance(messages, list):
42+
# Convert old format to new format
43+
messages = [messages]
44+
except Exception as e:
45+
logger.error(f"Error reading existing log file {filename}: {str(e)}")
46+
messages = []
47+
48+
# Append the new message
49+
messages.append(entry)
50+
51+
# Write all messages back to the file
52+
with open(filename, 'w') as f:
53+
json.dump(messages, f, indent=2)
54+
55+
logger.info(f"Appended device message to {filename}")
56+
return filename
57+
except Exception as e:
58+
logger.error(f"Failed to capture device message: {str(e)}")
59+
return None
60+
61+
def analyze_device_messages(self, device_id=None):
62+
"""Analyze captured messages to find patterns and issues."""
63+
results = {
64+
"total_messages": 0,
65+
"messages_by_type": {},
66+
"messages_without_type": 0,
67+
"messages_with_gps": 0,
68+
"example_gps_messages": []
69+
}
70+
71+
path = Path(self.debug_dir)
72+
73+
# Handle both old format (multiple files) and new format (single file per device)
74+
if device_id:
75+
# First check for single log file
76+
log_file = path / f"device_{device_id}_log.json"
77+
if log_file.exists():
78+
self._process_log_file(log_file, results)
79+
else:
80+
# Fall back to old format
81+
for file in path.glob(f"device_{device_id}_*.json"):
82+
self._process_old_format_file(file, results)
83+
else:
84+
# Check for all device log files
85+
for file in path.glob("device_*_log.json"):
86+
self._process_log_file(file, results)
87+
88+
# Also check old format files
89+
for file in path.glob("device_*_20*.json"): # Files with timestamp in name
90+
self._process_old_format_file(file, results)
91+
92+
return results
93+
94+
def _process_log_file(self, file_path, results):
95+
"""Process a log file containing multiple messages."""
96+
try:
97+
with open(file_path, 'r') as f:
98+
messages = json.load(f)
99+
100+
if not isinstance(messages, list):
101+
messages = [messages] # Handle case of single message
102+
103+
for entry in messages:
104+
message = entry.get("message", {})
105+
results["total_messages"] += 1
106+
107+
# Analyze message type
108+
msg_type = message.get("type")
109+
if msg_type:
110+
results["messages_by_type"][msg_type] = results["messages_by_type"].get(msg_type, 0) + 1
111+
else:
112+
results["messages_without_type"] += 1
113+
114+
# Check for GPS data
115+
self._check_for_gps(message, results)
116+
117+
except Exception as e:
118+
logger.error(f"Error analyzing file {file_path}: {str(e)}")
119+
120+
def _process_old_format_file(self, file_path, results):
121+
"""Process a file in the old format (single message per file)."""
122+
try:
123+
with open(file_path, 'r') as f:
124+
data = json.load(f)
125+
126+
message = data.get("message", {})
127+
results["total_messages"] += 1
128+
129+
# Analyze message type
130+
msg_type = message.get("type")
131+
if msg_type:
132+
results["messages_by_type"][msg_type] = results["messages_by_type"].get(msg_type, 0) + 1
133+
else:
134+
results["messages_without_type"] += 1
135+
136+
# Check for GPS data
137+
self._check_for_gps(message, results)
138+
139+
except Exception as e:
140+
logger.error(f"Error analyzing file {file_path}: {str(e)}")
141+
142+
def _check_for_gps(self, message, results):
143+
"""Check if a message contains GPS data."""
144+
has_gps = False
145+
146+
# Option 1: data.gps structure
147+
if "data" in message and isinstance(message["data"], dict) and "gps" in message["data"]:
148+
has_gps = True
149+
150+
# Option 2: direct gps object
151+
elif "gps" in message and isinstance(message["gps"], dict):
152+
has_gps = True
153+
154+
# Option 3: latitude/longitude directly in message
155+
elif "latitude" in message and "longitude" in message:
156+
has_gps = True
157+
158+
if has_gps:
159+
results["messages_with_gps"] += 1
160+
# Store a few examples for analysis
161+
if len(results["example_gps_messages"]) < 3:
162+
results["example_gps_messages"].append(message)
163+
164+
165+
# Singleton instance for global use
166+
message_debugger = MessageDebugger()

server/main.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from server.webrtc_handler import WebRTCHandler
1717
from server.telemetry_handler import TelemetryHandler
1818
from server.command_handler import CommandHandler
19+
from server.debug_tools import message_debugger
1920

2021
# Configure logging
2122
logging.basicConfig(
@@ -77,18 +78,79 @@ async def health_check():
7778
}
7879

7980

81+
@app.get("/debug/device-messages/{device_id}")
82+
async def analyze_device_messages(device_id: str):
83+
"""Analyze captured device messages for debugging."""
84+
results = message_debugger.analyze_device_messages(device_id)
85+
return results
86+
87+
8088
@app.websocket("/ws/device/{device_id}")
8189
async def device_websocket_endpoint(websocket: WebSocket, device_id: str):
8290
"""WebSocket endpoint for device connections."""
8391
await connection_manager.connect_device(websocket, device_id)
8492
try:
8593
while True:
8694
data = await websocket.receive_json()
95+
# Log the raw message for debugging
96+
logger.debug(f"Received raw message from device {device_id}: {data}")
97+
98+
# Capture message for debugging
99+
message_debugger.capture_device_message(device_id, data)
100+
87101
message_type = data.get("type")
88102

103+
# Transform GPS data format for compatibility
104+
if message_type is None:
105+
# Check if it looks like telemetry data with GPS position
106+
has_position = "position" in data and isinstance(data.get("position"), dict)
107+
has_gps_fields = has_position and "latitude" in data["position"] and "longitude" in data["position"]
108+
109+
if has_gps_fields:
110+
logger.info(f"Detected GPS data in non-standard format from device {device_id}, transforming to standard format")
111+
112+
# Create a new properly formatted telemetry message
113+
transformed_data = {
114+
"type": "telemetry",
115+
"subtype": "sensor_data",
116+
"sequence": data.get("sequence", 0),
117+
"timestamp": data.get("timestamp", time.time() * 1000),
118+
"data": {
119+
"gps": {
120+
"latitude": data["position"]["latitude"],
121+
"longitude": data["position"]["longitude"]
122+
}
123+
}
124+
}
125+
126+
# Add additional navigation data if available
127+
if "navigation" in data and isinstance(data["navigation"], dict):
128+
if "heading" in data["navigation"]:
129+
transformed_data["data"]["heading"] = data["navigation"]["heading"]
130+
if "speed" in data["navigation"]:
131+
transformed_data["data"]["speed"] = data["navigation"]["speed"]
132+
133+
# Add battery status if available
134+
if "status" in data and isinstance(data["status"], dict) and "battery" in data["status"]:
135+
transformed_data["data"]["battery"] = data["status"]["battery"]
136+
137+
logger.debug(f"Transformed data: {transformed_data}")
138+
data = transformed_data
139+
message_type = "telemetry"
140+
else:
141+
# Check for missing or null type for other message formats
142+
logger.warning(f"Device {device_id} sent message without valid type field: {data}")
143+
if any(key in data for key in ["gps", "location", "coordinates", "latitude", "longitude"]):
144+
logger.info(f"Message appears to be telemetry data, processing as telemetry: {data}")
145+
# Add type field and process as telemetry
146+
data["type"] = "telemetry"
147+
data["subtype"] = "sensor_data" # Add required subtype field
148+
message_type = "telemetry"
149+
89150
if message_type == "webrtc":
90151
await webrtc_handler.handle_device_message(device_id, data, connection_manager)
91152
elif message_type == "telemetry":
153+
logger.debug(f"Processing telemetry data from device {device_id}: {data}")
92154
await telemetry_handler.process_telemetry(device_id, data, connection_manager)
93155
elif message_type == "pong":
94156
# Update last activity time to prevent timeout

0 commit comments

Comments
 (0)