You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
549 lines
21 KiB
549 lines
21 KiB
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Flymaster GPS Client</title>
|
|
<!-- Vue.js -->
|
|
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
|
|
<!-- Flymaster Client -->
|
|
<script src="flymaster-client.js"></script>
|
|
<!-- Leaflet CSS -->
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
|
crossorigin=""/>
|
|
<!-- Leaflet JavaScript -->
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
|
crossorigin=""></script>
|
|
<style>
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
.container {
|
|
border: 1px solid #ddd;
|
|
border-radius: 5px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
h1, h2 {
|
|
color: #333;
|
|
}
|
|
button {
|
|
background-color: #4CAF50;
|
|
border: none;
|
|
color: white;
|
|
padding: 10px 15px;
|
|
text-align: center;
|
|
text-decoration: none;
|
|
display: inline-block;
|
|
font-size: 14px;
|
|
margin: 4px 2px;
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
}
|
|
button:disabled {
|
|
background-color: #cccccc;
|
|
cursor: not-allowed;
|
|
}
|
|
.download-button {
|
|
background-color: #2196F3;
|
|
margin-top: 10px;
|
|
margin-bottom: 15px;
|
|
}
|
|
.status {
|
|
margin: 10px 0;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
}
|
|
.status.success {
|
|
background-color: #dff0d8;
|
|
color: #3c763d;
|
|
}
|
|
.status.error {
|
|
background-color: #f2dede;
|
|
color: #a94442;
|
|
}
|
|
.status.info {
|
|
background-color: #d9edf7;
|
|
color: #31708f;
|
|
}
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 15px 0;
|
|
}
|
|
table, th, td {
|
|
border: 1px solid #ddd;
|
|
}
|
|
th, td {
|
|
padding: 8px;
|
|
text-align: left;
|
|
}
|
|
th {
|
|
background-color: #f2f2f2;
|
|
}
|
|
.flight-info-table th {
|
|
width: 200px;
|
|
}
|
|
tr:nth-child(even) {
|
|
background-color: #f9f9f9;
|
|
}
|
|
.progress-container {
|
|
width: 100%;
|
|
background-color: #f1f1f1;
|
|
border-radius: 4px;
|
|
margin: 10px 0;
|
|
}
|
|
.progress-bar {
|
|
height: 20px;
|
|
background-color: #4CAF50;
|
|
border-radius: 4px;
|
|
text-align: center;
|
|
color: white;
|
|
line-height: 20px;
|
|
}
|
|
.track-list {
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
}
|
|
#map-container {
|
|
height: 400px;
|
|
width: 100%;
|
|
margin-top: 15px;
|
|
}
|
|
.action-button {
|
|
margin: 2px;
|
|
padding: 5px 10px;
|
|
font-size: 12px;
|
|
}
|
|
.view-button {
|
|
background-color: #2196F3;
|
|
}
|
|
.save-button {
|
|
background-color: #4CAF50;
|
|
}
|
|
.not-set {
|
|
color: #999;
|
|
font-style: italic;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<h1>Flymaster GPS Client</h1>
|
|
|
|
<div class="container">
|
|
<h2>Connection</h2>
|
|
<button @click="connect" :disabled="isConnected">Connect</button>
|
|
<button @click="disconnect" :disabled="!isConnected">Disconnect</button>
|
|
|
|
<div v-if="statusMessage" :class="['status', statusType]">
|
|
{{ statusMessage }}
|
|
</div>
|
|
|
|
<div v-if="deviceInfo">
|
|
<h3>Device Information</h3>
|
|
<p><strong>Name:</strong> {{ deviceInfo.name }}</p>
|
|
<p><strong>Unit ID:</strong> {{ deviceInfo.unitId }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container" v-if="isConnected">
|
|
<h2>Tracks</h2>
|
|
<button @click="refreshTracks" :disabled="isLoading">Refresh Track List</button>
|
|
|
|
<div v-if="isLoading" class="status info">
|
|
Loading tracks...
|
|
</div>
|
|
|
|
<div v-if="tracks.length > 0" class="track-list">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>#</th>
|
|
<th>Start Date</th>
|
|
<th>End Date</th>
|
|
<th>Duration</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="track in tracks" :key="track.index">
|
|
<td>{{ track.index + 1 }}</td>
|
|
<td>{{ formatDate(track.startDate) }}</td>
|
|
<td>{{ formatDate(track.endDate) }}</td>
|
|
<td>{{ formatDuration(track.duration) }}</td>
|
|
<td>
|
|
<button @click="downloadTrack(track.index, false)" :disabled="isDownloading" class="action-button view-button">
|
|
View
|
|
</button>
|
|
<button @click="downloadTrack(track.index, true)" :disabled="isDownloading" class="action-button save-button">
|
|
Save
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div v-if="isDownloading">
|
|
<h3>Download Progress</h3>
|
|
<div class="progress-container">
|
|
<div class="progress-bar" :style="{ width: downloadProgress + '%' }">
|
|
{{ downloadProgress }}%
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="container" v-if="trackPoints.length > 0">
|
|
<h2>Downloaded Track Data</h2>
|
|
<p>Total points: {{ trackPoints.length }}</p>
|
|
|
|
<button @click="saveTrackToFile" class="download-button">Save Track to File</button>
|
|
|
|
<div v-if="flightInfo">
|
|
<h3>Flight Information</h3>
|
|
<table class="flight-info-table">
|
|
<tbody>
|
|
<tr>
|
|
<th>Software Version</th>
|
|
<td>{{ flightInfo.sw_version }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Hardware Version</th>
|
|
<td>{{ flightInfo.hw_version }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Serial Number</th>
|
|
<td>{{ flightInfo.serial }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Competition Number</th>
|
|
<td :class="{ 'not-set': !flightInfo.compnum }">{{ flightInfo.compnum || 'Not set' }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Pilot Name</th>
|
|
<td :class="{ 'not-set': !flightInfo.pilotname }">{{ flightInfo.pilotname || 'Not set' }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Glider Brand</th>
|
|
<td :class="{ 'not-set': !flightInfo.gliderbrand }">{{ flightInfo.gliderbrand || 'Not set' }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Glider Model</th>
|
|
<td :class="{ 'not-set': !flightInfo.glidermodel }">{{ flightInfo.glidermodel || 'Not set' }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div v-if="trackPoints.length > 0">
|
|
<h3>Sample Points</h3>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>#</th>
|
|
<th>Latitude</th>
|
|
<th>Longitude</th>
|
|
<th>GPS Alt</th>
|
|
<th>Baro Alt</th>
|
|
<th>Time</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(point, index) in samplePoints" :key="index">
|
|
<td>{{ index + 1 }}</td>
|
|
<td>{{ point.lat.toFixed(6) }}</td>
|
|
<td>{{ point.lon.toFixed(6) }}</td>
|
|
<td>{{ point.gpsalt }}m</td>
|
|
<td>{{ point.baroalt.toFixed(1) }}m</td>
|
|
<td>{{ formatDate(new Date(point.time * 1000)) }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<h3>Track Map</h3>
|
|
<div id="map-container" ref="mapContainer"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
new Vue({
|
|
el: '#app',
|
|
data: {
|
|
client: null,
|
|
isConnected: false,
|
|
isLoading: false,
|
|
isDownloading: false,
|
|
statusMessage: '',
|
|
statusType: 'info',
|
|
deviceInfo: null,
|
|
tracks: [],
|
|
trackPoints: [],
|
|
downloadProgress: 0,
|
|
map: null,
|
|
flightInfo: null
|
|
},
|
|
computed: {
|
|
samplePoints() {
|
|
// Return a sample of points (first 10)
|
|
return this.trackPoints.slice(0, 10);
|
|
}
|
|
},
|
|
methods: {
|
|
|
|
async connect() {
|
|
try {
|
|
this.statusMessage = 'Connecting to device...';
|
|
this.statusType = 'info';
|
|
|
|
this.client = new FlymasterClient();
|
|
|
|
const connected = await this.client.connect();
|
|
|
|
if (connected) {
|
|
this.isConnected = true;
|
|
this.statusMessage = 'Connected to device successfully!';
|
|
this.statusType = 'success';
|
|
|
|
// Initialize GPS
|
|
await this.initializeGps();
|
|
} else {
|
|
this.statusMessage = 'Failed to connect to device.';
|
|
this.statusType = 'error';
|
|
}
|
|
} catch (error) {
|
|
console.error('Connection error:', error);
|
|
this.statusMessage = 'Error connecting to device: ' + error.message;
|
|
this.statusType = 'error';
|
|
}
|
|
},
|
|
|
|
async disconnect() {
|
|
try {
|
|
if (this.client) {
|
|
await this.client.disconnect();
|
|
this.isConnected = false;
|
|
this.statusMessage = 'Disconnected from device.';
|
|
this.statusType = 'info';
|
|
this.deviceInfo = null;
|
|
this.flightInfo = null;
|
|
}
|
|
} catch (error) {
|
|
console.error('Disconnection error:', error);
|
|
this.statusMessage = 'Error disconnecting from device: ' + error.message;
|
|
this.statusType = 'error';
|
|
}
|
|
},
|
|
|
|
async initializeGps() {
|
|
try {
|
|
this.isLoading = true;
|
|
this.statusMessage = 'Initializing GPS...';
|
|
this.statusType = 'info';
|
|
|
|
await this.client.initGps();
|
|
|
|
this.deviceInfo = {
|
|
name: this.client.gpsname,
|
|
unitId: this.client.gpsunitid
|
|
};
|
|
|
|
this.refreshTracks();
|
|
|
|
this.statusMessage = 'GPS initialized successfully!';
|
|
this.statusType = 'success';
|
|
} catch (error) {
|
|
console.error('GPS initialization error:', error);
|
|
this.statusMessage = 'Error initializing GPS: ' + error.message;
|
|
this.statusType = 'error';
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
},
|
|
|
|
async refreshTracks() {
|
|
try {
|
|
this.isLoading = true;
|
|
this.statusMessage = 'Loading tracks...';
|
|
this.statusType = 'info';
|
|
|
|
this.tracks = this.client.getTrackList();
|
|
|
|
this.statusMessage = `Loaded ${this.tracks.length} tracks.`;
|
|
this.statusType = 'success';
|
|
} catch (error) {
|
|
console.error('Track loading error:', error);
|
|
this.statusMessage = 'Error loading tracks: ' + error.message;
|
|
this.statusType = 'error';
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
},
|
|
|
|
async downloadTrack(trackIndex, saveToFile) {
|
|
try {
|
|
this.isDownloading = true;
|
|
this.downloadProgress = 0;
|
|
this.statusMessage = 'Downloading track...';
|
|
this.statusType = 'info';
|
|
|
|
// Reset flight info before downloading
|
|
this.flightInfo = null;
|
|
|
|
// Download the single track
|
|
this.trackPoints = await this.client.downloadStrack(trackIndex, (current, total) => {
|
|
this.downloadProgress = Math.floor((current / total) * 100);
|
|
return true; // Continue download
|
|
});
|
|
|
|
// Get flight info if available
|
|
this.flightInfo = this.client.getFlightInfo();
|
|
|
|
this.statusMessage = `Downloaded ${this.trackPoints.length} points successfully!`;
|
|
this.statusType = 'success';
|
|
|
|
if (saveToFile) {
|
|
// Save to file immediately
|
|
this.saveTrackToFile();
|
|
} else {
|
|
// Initialize and update the map with track points
|
|
this.$nextTick(() => {
|
|
this.initMap();
|
|
this.displayTrackOnMap();
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Download error:', error);
|
|
this.statusMessage = 'Error downloading track: ' + error.message;
|
|
this.statusType = 'error';
|
|
} finally {
|
|
this.isDownloading = false;
|
|
}
|
|
},
|
|
|
|
initMap() {
|
|
// If map already exists, remove it and create a new one
|
|
if (this.map) {
|
|
this.map.remove();
|
|
}
|
|
|
|
// Create a new map instance
|
|
this.map = L.map(this.$refs.mapContainer).setView([0, 0], 2);
|
|
|
|
// Add OpenStreetMap tile layer
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
}).addTo(this.map);
|
|
},
|
|
|
|
displayTrackOnMap() {
|
|
if (!this.map || this.trackPoints.length === 0) return;
|
|
|
|
// Create an array of [lat, lng] points for the track
|
|
const trackLatLngs = this.trackPoints.map(point => [point.lat, point.lon]);
|
|
|
|
// Create a polyline from the track points
|
|
const trackLine = L.polyline(trackLatLngs, {
|
|
color: 'blue',
|
|
weight: 3,
|
|
opacity: 0.7
|
|
}).addTo(this.map);
|
|
|
|
// Add markers for start and end points
|
|
const startPoint = this.trackPoints[0];
|
|
const endPoint = this.trackPoints[this.trackPoints.length - 1];
|
|
|
|
L.marker([startPoint.lat, startPoint.lon])
|
|
.bindPopup('Start: ' + this.formatDate(new Date(startPoint.time * 1000)))
|
|
.addTo(this.map);
|
|
|
|
L.marker([endPoint.lat, endPoint.lon])
|
|
.bindPopup('End: ' + this.formatDate(new Date(endPoint.time * 1000)))
|
|
.addTo(this.map);
|
|
|
|
// Fit the map to the track bounds
|
|
this.map.fitBounds(trackLine.getBounds());
|
|
},
|
|
|
|
formatDate(date) {
|
|
return new Date(date).toLocaleString();
|
|
},
|
|
|
|
formatDuration(seconds) {
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
const secs = seconds % 60;
|
|
|
|
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
},
|
|
|
|
saveTrackToFile() {
|
|
if (this.trackPoints.length === 0) {
|
|
this.statusMessage = 'No track data to save.';
|
|
this.statusType = 'error';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Create GPX format
|
|
let gpx = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
gpx += '<gpx version="1.1" creator="Flymaster GPS Client" xmlns="http://www.topografix.com/GPX/1/1">\n';
|
|
gpx += ' <metadata>\n';
|
|
gpx += ` <name>Track from ${this.deviceInfo.name}</name>\n`;
|
|
gpx += ` <time>${new Date().toISOString()}</time>\n`;
|
|
gpx += ' </metadata>\n';
|
|
gpx += ' <trk>\n';
|
|
gpx += ' <name>Flymaster Track</name>\n';
|
|
gpx += ' <trkseg>\n';
|
|
|
|
// Add track points
|
|
this.trackPoints.forEach(point => {
|
|
const time = new Date(point.time * 1000).toISOString();
|
|
gpx += ` <trkpt lat="${point.lat}" lon="${point.lon}">\n`;
|
|
gpx += ` <ele>${point.gpsalt}</ele>\n`;
|
|
gpx += ` <time>${time}</time>\n`;
|
|
gpx += ' </trkpt>\n';
|
|
});
|
|
|
|
gpx += ' </trkseg>\n';
|
|
gpx += ' </trk>\n';
|
|
gpx += '</gpx>';
|
|
|
|
// Create a blob and download link
|
|
const blob = new Blob([gpx], { type: 'application/gpx+xml' });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
// Create a temporary link element and trigger download
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `flymaster_track_${new Date().toISOString().slice(0, 10)}.gpx`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
|
|
// Clean up
|
|
setTimeout(() => {
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}, 100);
|
|
|
|
this.statusMessage = 'Track saved successfully!';
|
|
this.statusType = 'success';
|
|
} catch (error) {
|
|
console.error('Error saving track:', error);
|
|
this.statusMessage = 'Error saving track: ' + error.message;
|
|
this.statusType = 'error';
|
|
}
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|