Compare commits
No commits in common. "72f9b18e94fe19bf294e321a08fe44fdba78c039" and "064ba2de719d6016c127c60ea8e91ba18bc67579" have entirely different histories.
72f9b18e94
...
064ba2de71
30 changed files with 386 additions and 1006 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -3,10 +3,6 @@ skyview
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
# Debian package build artifacts
|
|
||||||
debian/usr/bin/skyview
|
|
||||||
debian/usr/bin/beast-dump
|
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
config.json
|
config.json
|
||||||
|
|
||||||
|
|
|
||||||
21
Makefile
21
Makefile
|
|
@ -1,26 +1,13 @@
|
||||||
PACKAGE_NAME=skyview
|
BINARY_NAME=skyview
|
||||||
BUILD_DIR=build
|
BUILD_DIR=build
|
||||||
VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
VERSION=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||||
LDFLAGS=-w -s -X main.version=$(VERSION)
|
|
||||||
|
|
||||||
.PHONY: build build-all clean run dev test lint deb deb-clean install-deps
|
.PHONY: build clean run dev test lint deb deb-clean install-deps
|
||||||
|
|
||||||
# Build main skyview binary
|
|
||||||
build:
|
build:
|
||||||
@echo "Building skyview..."
|
@echo "Building $(BINARY_NAME)..."
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
go build -ldflags="$(LDFLAGS)" -o $(BUILD_DIR)/skyview ./cmd/skyview
|
go build -ldflags="-w -s -X main.version=$(VERSION)" -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/skyview
|
||||||
|
|
||||||
# Build beast-dump utility binary
|
|
||||||
build-beast-dump:
|
|
||||||
@echo "Building beast-dump..."
|
|
||||||
@mkdir -p $(BUILD_DIR)
|
|
||||||
go build -ldflags="$(LDFLAGS)" -o $(BUILD_DIR)/beast-dump ./cmd/beast-dump
|
|
||||||
|
|
||||||
# Build all binaries
|
|
||||||
build-all: build build-beast-dump
|
|
||||||
@echo "Built all binaries successfully:"
|
|
||||||
@ls -la $(BUILD_DIR)/
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@echo "Cleaning..."
|
@echo "Cleaning..."
|
||||||
|
|
|
||||||
32
README.md
32
README.md
|
|
@ -17,14 +17,13 @@ A high-performance, multi-source ADS-B aircraft tracking application that connec
|
||||||
- **Multi-view Dashboard**: Map, Table, Statistics, Coverage, and 3D Radar views
|
- **Multi-view Dashboard**: Map, Table, Statistics, Coverage, and 3D Radar views
|
||||||
|
|
||||||
### Professional Visualization
|
### Professional Visualization
|
||||||
- **Signal Analysis**: Signal strength visualization and coverage analysis
|
- **Signal Analysis**: Signal strength heatmaps and coverage analysis
|
||||||
- **Range Circles**: Configurable range rings for each receiver
|
- **Range Circles**: Configurable range rings for each receiver
|
||||||
- **Flight Trails**: Historical aircraft movement tracking
|
- **Flight Trails**: Historical aircraft movement tracking
|
||||||
- **3D Radar View**: Three.js-powered 3D visualization
|
- **3D Radar View**: Three.js-powered 3D visualization (optional)
|
||||||
- **Statistics Dashboard**: Aircraft count timeline *(additional charts under construction)* 🚧
|
- **Statistics Dashboard**: Live charts and metrics
|
||||||
- **Smart Origin**: Auto-calculated map center based on receiver locations
|
- **Smart Origin**: Auto-calculated map center based on receiver locations
|
||||||
- **Map Controls**: Center on aircraft, reset to origin, toggle overlays
|
- **Map Controls**: Center on aircraft, reset to origin, toggle overlays
|
||||||
- **Signal Heatmaps**: Coverage heatmap visualization *(under construction)* 🚧
|
|
||||||
|
|
||||||
### Aircraft Data
|
### Aircraft Data
|
||||||
- **Complete Mode S Decoding**: Position, velocity, altitude, heading
|
- **Complete Mode S Decoding**: Position, velocity, altitude, heading
|
||||||
|
|
@ -52,7 +51,7 @@ A high-performance, multi-source ADS-B aircraft tracking application that connec
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install
|
# Install
|
||||||
sudo dpkg -i skyview_0.0.2_amd64.deb
|
sudo dpkg -i skyview_2.0.0_amd64.deb
|
||||||
|
|
||||||
# Configure
|
# Configure
|
||||||
sudo nano /etc/skyview/config.json
|
sudo nano /etc/skyview/config.json
|
||||||
|
|
@ -120,18 +119,9 @@ Access the web interface at `http://localhost:8080`
|
||||||
### Views Available:
|
### Views Available:
|
||||||
- **Map View**: Interactive aircraft tracking with receiver locations
|
- **Map View**: Interactive aircraft tracking with receiver locations
|
||||||
- **Table View**: Sortable aircraft data with multi-source information
|
- **Table View**: Sortable aircraft data with multi-source information
|
||||||
- **Statistics**: Aircraft count timeline *(additional charts planned)* 🚧
|
- **Statistics**: Live metrics and historical charts
|
||||||
- **Coverage**: Signal strength analysis *(heatmaps under construction)* 🚧
|
- **Coverage**: Signal strength analysis and heatmaps
|
||||||
- **3D Radar**: Three-dimensional aircraft visualization *(controls under construction)* 🚧
|
- **3D Radar**: Three-dimensional aircraft visualization
|
||||||
|
|
||||||
### 🚧 Features Under Construction
|
|
||||||
Some advanced features are currently in development:
|
|
||||||
- **Message Rate Charts**: Per-source message rate visualization
|
|
||||||
- **Signal Strength Distribution**: Signal strength histogram analysis
|
|
||||||
- **Altitude Distribution**: Aircraft altitude distribution charts
|
|
||||||
- **Interactive Heatmaps**: Leaflet.heat-based coverage heatmaps
|
|
||||||
- **3D Radar Controls**: Interactive 3D view manipulation (reset, auto-rotate, range)
|
|
||||||
- **Enhanced Error Notifications**: User-friendly toast notifications for issues
|
|
||||||
|
|
||||||
## 🔧 Building
|
## 🔧 Building
|
||||||
|
|
||||||
|
|
@ -203,7 +193,7 @@ make check # Run all checks
|
||||||
### Systemd Service (Debian/Ubuntu)
|
### Systemd Service (Debian/Ubuntu)
|
||||||
```bash
|
```bash
|
||||||
# Install package
|
# Install package
|
||||||
sudo dpkg -i skyview_0.0.2_amd64.deb
|
sudo dpkg -i skyview_2.0.0_amd64.deb
|
||||||
|
|
||||||
# Configure sources in /etc/skyview/config.json
|
# Configure sources in /etc/skyview/config.json
|
||||||
# Start service
|
# Start service
|
||||||
|
|
@ -259,9 +249,9 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
## 🆘 Support
|
## 🆘 Support
|
||||||
|
|
||||||
- [Issues](https://kode.naiv.no/olemd/skyview/issues)
|
- [GitHub Issues](https://github.com/skyview/skyview/issues)
|
||||||
- [Documentation](https://kode.naiv.no/olemd/skyview/wiki)
|
- [Documentation](https://github.com/skyview/skyview/wiki)
|
||||||
- [Configuration Examples](https://kode.naiv.no/olemd/skyview/src/branch/main/examples)
|
- [Configuration Examples](https://github.com/skyview/skyview/tree/main/examples)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
// - index.html: Main web interface with aircraft tracking map
|
// - index.html: Main web interface with aircraft tracking map
|
||||||
// - css/style.css: Styling for the web interface
|
// - css/style.css: Styling for the web interface
|
||||||
// - js/app.js: JavaScript client for WebSocket communication and map rendering
|
// - js/app.js: JavaScript client for WebSocket communication and map rendering
|
||||||
// - icons/*.svg: Type-specific SVG icons for aircraft markers
|
// - aircraft-icon.svg: SVG icon for aircraft markers
|
||||||
// - favicon.ico: Browser icon
|
// - favicon.ico: Browser icon
|
||||||
//
|
//
|
||||||
// The embedded filesystem is used by the HTTP server to serve static content
|
// The embedded filesystem is used by the HTTP server to serve static content
|
||||||
|
|
@ -16,11 +16,11 @@ package assets
|
||||||
import "embed"
|
import "embed"
|
||||||
|
|
||||||
// Static contains all embedded static web assets from the static/ directory.
|
// Static contains all embedded static web assets from the static/ directory.
|
||||||
//
|
//
|
||||||
// Files are embedded at build time and can be accessed using the standard
|
// Files are embedded at build time and can be accessed using the standard
|
||||||
// fs.FS interface. Path names within the embedded filesystem preserve the
|
// fs.FS interface. Path names within the embedded filesystem preserve the
|
||||||
// directory structure, so files are accessed as:
|
// directory structure, so files are accessed as:
|
||||||
// - "static/index.html"
|
// - "static/index.html"
|
||||||
// - "static/css/style.css"
|
// - "static/css/style.css"
|
||||||
// - "static/js/app.js"
|
// - "static/js/app.js"
|
||||||
// - etc.
|
// - etc.
|
||||||
|
|
|
||||||
5
assets/static/aircraft-icon.svg
Normal file
5
assets/static/aircraft-icon.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#00a8ff" stroke="#ffffff" stroke-width="1">
|
||||||
|
<path d="M12 2l-2 16 2-2 2 2-2-16z"/>
|
||||||
|
<path d="M4 10l8-2-1 2-7 0z"/>
|
||||||
|
<path d="M20 10l-8-2 1 2 7 0z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 224 B |
|
|
@ -195,8 +195,8 @@ body {
|
||||||
|
|
||||||
.display-options {
|
.display-options {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 320px;
|
top: 10px;
|
||||||
right: 10px;
|
left: 10px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
background: rgba(45, 45, 45, 0.95);
|
background: rgba(45, 45, 45, 0.95);
|
||||||
border: 1px solid #404040;
|
border: 1px solid #404040;
|
||||||
|
|
@ -417,115 +417,6 @@ body {
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Under Construction Styles */
|
|
||||||
.under-construction {
|
|
||||||
color: #ff8c00;
|
|
||||||
font-size: 0.8em;
|
|
||||||
font-weight: normal;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.construction-notice {
|
|
||||||
background: rgba(255, 140, 0, 0.1);
|
|
||||||
border: 1px solid #ff8c00;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 8px;
|
|
||||||
margin: 8px 0;
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: #ff8c00;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toast Notifications */
|
|
||||||
.toast-notification {
|
|
||||||
position: fixed;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
background: rgba(40, 40, 40, 0.95);
|
|
||||||
border: 1px solid #555;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 12px 20px;
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 0.9em;
|
|
||||||
max-width: 300px;
|
|
||||||
z-index: 10000;
|
|
||||||
transform: translateX(320px);
|
|
||||||
transition: transform 0.3s ease-in-out;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-notification.error {
|
|
||||||
border-color: #ff8c00;
|
|
||||||
background: rgba(255, 140, 0, 0.1);
|
|
||||||
color: #ff8c00;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast-notification.show {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Version Info */
|
|
||||||
.version-info {
|
|
||||||
font-size: 0.6em;
|
|
||||||
color: #888;
|
|
||||||
font-weight: normal;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Repository Link */
|
|
||||||
.repo-link {
|
|
||||||
color: #888;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 0.7em;
|
|
||||||
margin-left: 6px;
|
|
||||||
opacity: 0.6;
|
|
||||||
transition: opacity 0.2s ease, color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repo-link:hover {
|
|
||||||
color: #4a9eff;
|
|
||||||
opacity: 1;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Collapsible Sections */
|
|
||||||
.collapsible-header {
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
padding: 4px 0;
|
|
||||||
border-bottom: 1px solid #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsible-header:hover {
|
|
||||||
color: #4a9eff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapse-indicator {
|
|
||||||
font-size: 0.8em;
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsible-header.collapsed .collapse-indicator {
|
|
||||||
transform: rotate(-90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsible-content {
|
|
||||||
overflow: hidden;
|
|
||||||
transition: max-height 0.3s ease;
|
|
||||||
max-height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsible-content.collapsed {
|
|
||||||
max-height: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-popup-tip {
|
.leaflet-popup-tip {
|
||||||
background: #2d2d2d !important;
|
background: #2d2d2d !important;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<h1>SkyView <span class="version-info">v0.0.2</span> <a href="https://kode.naiv.no/olemd/skyview" target="_blank" class="repo-link" title="Project Repository">⚙</a></h1>
|
<h1>SkyView</h1>
|
||||||
|
|
||||||
<!-- Status indicators -->
|
<!-- Status indicators -->
|
||||||
<div class="status-section">
|
<div class="status-section">
|
||||||
|
|
@ -81,13 +81,10 @@
|
||||||
<button id="toggle-dark-mode" title="Toggle dark/light mode">🌙 Night Mode</button>
|
<button id="toggle-dark-mode" title="Toggle dark/light mode">🌙 Night Mode</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Options -->
|
<!-- Display options -->
|
||||||
<div class="display-options">
|
<div class="display-options">
|
||||||
<h4 class="collapsible-header collapsed" id="display-options-header">
|
<h4>Display Options</h4>
|
||||||
<span>Options</span>
|
<div class="option-group">
|
||||||
<span class="collapse-indicator">▼</span>
|
|
||||||
</h4>
|
|
||||||
<div class="option-group collapsible-content collapsed" id="display-options-content">
|
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="show-site-positions" checked>
|
<input type="checkbox" id="show-site-positions" checked>
|
||||||
<span>Site Positions</span>
|
<span>Site Positions</span>
|
||||||
|
|
@ -208,19 +205,16 @@
|
||||||
<canvas id="aircraft-chart"></canvas>
|
<canvas id="aircraft-chart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-card">
|
<div class="chart-card">
|
||||||
<h3>Message Rate by Source <span class="under-construction">🚧 Under Construction</span></h3>
|
<h3>Message Rate by Source</h3>
|
||||||
<canvas id="message-chart"></canvas>
|
<canvas id="message-chart"></canvas>
|
||||||
<div class="construction-notice">This chart is planned but not yet implemented</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-card">
|
<div class="chart-card">
|
||||||
<h3>Signal Strength Distribution <span class="under-construction">🚧 Under Construction</span></h3>
|
<h3>Signal Strength Distribution</h3>
|
||||||
<canvas id="signal-chart"></canvas>
|
<canvas id="signal-chart"></canvas>
|
||||||
<div class="construction-notice">This chart is planned but not yet implemented</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-card">
|
<div class="chart-card">
|
||||||
<h3>Altitude Distribution <span class="under-construction">🚧 Under Construction</span></h3>
|
<h3>Altitude Distribution</h3>
|
||||||
<canvas id="altitude-chart"></canvas>
|
<canvas id="altitude-chart"></canvas>
|
||||||
<div class="construction-notice">This chart is planned but not yet implemented</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -239,11 +233,10 @@
|
||||||
<!-- 3D Radar View -->
|
<!-- 3D Radar View -->
|
||||||
<div id="radar3d-view" class="view">
|
<div id="radar3d-view" class="view">
|
||||||
<div class="radar3d-controls">
|
<div class="radar3d-controls">
|
||||||
<div class="construction-notice">🚧 3D Controls Under Construction</div>
|
<button id="radar3d-reset">Reset View</button>
|
||||||
<button id="radar3d-reset" disabled>Reset View</button>
|
<button id="radar3d-auto-rotate">Auto Rotate</button>
|
||||||
<button id="radar3d-auto-rotate" disabled>Auto Rotate</button>
|
|
||||||
<label>
|
<label>
|
||||||
<input type="range" id="radar3d-range" min="10" max="500" value="100" disabled>
|
<input type="range" id="radar3d-range" min="10" max="500" value="100">
|
||||||
Range: <span id="radar3d-range-value">100</span> km
|
Range: <span id="radar3d-range-value">100</span> km
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -107,9 +107,6 @@ class SkyView {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup collapsible sections
|
|
||||||
this.setupCollapsibleSections();
|
|
||||||
|
|
||||||
const toggleDarkModeBtn = document.getElementById('toggle-dark-mode');
|
const toggleDarkModeBtn = document.getElementById('toggle-dark-mode');
|
||||||
if (toggleDarkModeBtn) {
|
if (toggleDarkModeBtn) {
|
||||||
toggleDarkModeBtn.addEventListener('click', () => {
|
toggleDarkModeBtn.addEventListener('click', () => {
|
||||||
|
|
@ -461,43 +458,6 @@ class SkyView {
|
||||||
// Clean up old trail data, etc.
|
// Clean up old trail data, etc.
|
||||||
}, 30000);
|
}, 30000);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupCollapsibleSections() {
|
|
||||||
// Setup Display Options collapsible
|
|
||||||
const displayHeader = document.getElementById('display-options-header');
|
|
||||||
const displayContent = document.getElementById('display-options-content');
|
|
||||||
|
|
||||||
if (displayHeader && displayContent) {
|
|
||||||
displayHeader.addEventListener('click', () => {
|
|
||||||
const isCollapsed = displayContent.classList.contains('collapsed');
|
|
||||||
|
|
||||||
if (isCollapsed) {
|
|
||||||
// Expand
|
|
||||||
displayContent.classList.remove('collapsed');
|
|
||||||
displayHeader.classList.remove('collapsed');
|
|
||||||
} else {
|
|
||||||
// Collapse
|
|
||||||
displayContent.classList.add('collapsed');
|
|
||||||
displayHeader.classList.add('collapsed');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save state to localStorage
|
|
||||||
localStorage.setItem('displayOptionsCollapsed', !isCollapsed);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Restore saved state (default to collapsed)
|
|
||||||
const savedState = localStorage.getItem('displayOptionsCollapsed');
|
|
||||||
const shouldCollapse = savedState === null ? true : savedState === 'true';
|
|
||||||
|
|
||||||
if (shouldCollapse) {
|
|
||||||
displayContent.classList.add('collapsed');
|
|
||||||
displayHeader.classList.add('collapsed');
|
|
||||||
} else {
|
|
||||||
displayContent.classList.remove('collapsed');
|
|
||||||
displayHeader.classList.remove('collapsed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize application when DOM is ready
|
// Initialize application when DOM is ready
|
||||||
|
|
|
||||||
|
|
@ -362,14 +362,6 @@ export class AircraftManager {
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<strong>Type:</strong> ${type}
|
<strong>Type:</strong> ${type}
|
||||||
</div>
|
</div>
|
||||||
${aircraft.TransponderCapability ? `
|
|
||||||
<div class="detail-row">
|
|
||||||
<strong>Transponder:</strong> ${aircraft.TransponderCapability}
|
|
||||||
</div>` : ''}
|
|
||||||
${aircraft.SignalQuality ? `
|
|
||||||
<div class="detail-row">
|
|
||||||
<strong>Signal Quality:</strong> ${aircraft.SignalQuality}
|
|
||||||
</div>` : ''}
|
|
||||||
|
|
||||||
<div class="detail-grid">
|
<div class="detail-grid">
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
|
|
|
||||||
|
|
@ -352,14 +352,8 @@ export class MapManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
createHeatmapOverlay(data) {
|
createHeatmapOverlay(data) {
|
||||||
// 🚧 Under Construction: Heatmap visualization not yet implemented
|
// Simplified heatmap implementation
|
||||||
// Planned: Use Leaflet.heat library for proper heatmap rendering
|
// In production, would use proper heatmap library like Leaflet.heat
|
||||||
console.log('Heatmap overlay requested but not yet implemented');
|
|
||||||
|
|
||||||
// Show user-visible notice
|
|
||||||
if (window.uiManager) {
|
|
||||||
window.uiManager.showError('Heatmap visualization is under construction 🚧');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedSource(sourceId) {
|
setSelectedSource(sourceId) {
|
||||||
|
|
|
||||||
|
|
@ -316,22 +316,6 @@ export class UIManager {
|
||||||
|
|
||||||
showError(message) {
|
showError(message) {
|
||||||
console.error(message);
|
console.error(message);
|
||||||
|
// Could implement toast notifications here
|
||||||
// Simple toast notification implementation
|
|
||||||
const toast = document.createElement('div');
|
|
||||||
toast.className = 'toast-notification error';
|
|
||||||
toast.textContent = message;
|
|
||||||
|
|
||||||
// Add to page
|
|
||||||
document.body.appendChild(toast);
|
|
||||||
|
|
||||||
// Show toast with animation
|
|
||||||
setTimeout(() => toast.classList.add('show'), 100);
|
|
||||||
|
|
||||||
// Auto-remove after 5 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.classList.remove('show');
|
|
||||||
setTimeout(() => document.body.removeChild(toast), 300);
|
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BIN
beast-dump-with-heli.bin
Normal file
BIN
beast-dump-with-heli.bin
Normal file
Binary file not shown.
|
|
@ -5,16 +5,14 @@
|
||||||
// in human-readable format on the console.
|
// in human-readable format on the console.
|
||||||
//
|
//
|
||||||
// Usage:
|
// Usage:
|
||||||
//
|
// beast-dump -tcp host:port # Read from TCP socket
|
||||||
// beast-dump -tcp host:port # Read from TCP socket
|
// beast-dump -file path/to/file # Read from file
|
||||||
// beast-dump -file path/to/file # Read from file
|
// beast-dump -verbose # Show detailed message parsing
|
||||||
// beast-dump -verbose # Show detailed message parsing
|
|
||||||
//
|
//
|
||||||
// Examples:
|
// Examples:
|
||||||
//
|
// beast-dump -tcp svovel:30005 # Connect to dump1090 Beast stream
|
||||||
// beast-dump -tcp svovel:30005 # Connect to dump1090 Beast stream
|
// beast-dump -file beast.test # Parse Beast data from file
|
||||||
// beast-dump -file beast.test # Parse Beast data from file
|
// beast-dump -tcp localhost:30005 -verbose # Verbose TCP parsing
|
||||||
// beast-dump -tcp localhost:30005 -verbose # Verbose TCP parsing
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -44,23 +42,23 @@ type BeastDumper struct {
|
||||||
parser *beast.Parser
|
parser *beast.Parser
|
||||||
decoder *modes.Decoder
|
decoder *modes.Decoder
|
||||||
stats struct {
|
stats struct {
|
||||||
totalMessages int64
|
totalMessages int64
|
||||||
validMessages int64
|
validMessages int64
|
||||||
aircraftSeen map[uint32]bool
|
aircraftSeen map[uint32]bool
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
lastMessageTime time.Time
|
lastMessageTime time.Time
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
config := parseFlags()
|
config := parseFlags()
|
||||||
|
|
||||||
if config.TCPAddress == "" && config.FilePath == "" {
|
if config.TCPAddress == "" && config.FilePath == "" {
|
||||||
fmt.Fprintf(os.Stderr, "Error: Must specify either -tcp or -file\n")
|
fmt.Fprintf(os.Stderr, "Error: Must specify either -tcp or -file\n")
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.TCPAddress != "" && config.FilePath != "" {
|
if config.TCPAddress != "" && config.FilePath != "" {
|
||||||
fmt.Fprintf(os.Stderr, "Error: Cannot specify both -tcp and -file\n")
|
fmt.Fprintf(os.Stderr, "Error: Cannot specify both -tcp and -file\n")
|
||||||
flag.Usage()
|
flag.Usage()
|
||||||
|
|
@ -68,7 +66,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
dumper := NewBeastDumper(config)
|
dumper := NewBeastDumper(config)
|
||||||
|
|
||||||
if err := dumper.Run(); err != nil {
|
if err := dumper.Run(); err != nil {
|
||||||
log.Fatalf("Error: %v", err)
|
log.Fatalf("Error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -77,12 +75,12 @@ func main() {
|
||||||
// parseFlags parses command-line flags and returns configuration
|
// parseFlags parses command-line flags and returns configuration
|
||||||
func parseFlags() *Config {
|
func parseFlags() *Config {
|
||||||
config := &Config{}
|
config := &Config{}
|
||||||
|
|
||||||
flag.StringVar(&config.TCPAddress, "tcp", "", "TCP address for Beast stream (e.g., localhost:30005)")
|
flag.StringVar(&config.TCPAddress, "tcp", "", "TCP address for Beast stream (e.g., localhost:30005)")
|
||||||
flag.StringVar(&config.FilePath, "file", "", "File path for Beast data")
|
flag.StringVar(&config.FilePath, "file", "", "File path for Beast data")
|
||||||
flag.BoolVar(&config.Verbose, "verbose", false, "Enable verbose output")
|
flag.BoolVar(&config.Verbose, "verbose", false, "Enable verbose output")
|
||||||
flag.IntVar(&config.Count, "count", 0, "Maximum messages to process (0 = unlimited)")
|
flag.IntVar(&config.Count, "count", 0, "Maximum messages to process (0 = unlimited)")
|
||||||
|
|
||||||
flag.Usage = func() {
|
flag.Usage = func() {
|
||||||
fmt.Fprintf(os.Stderr, "Usage: %s [options]\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, "Usage: %s [options]\n", os.Args[0])
|
||||||
fmt.Fprintf(os.Stderr, "\nBeast format ADS-B data parser and console dumper\n\n")
|
fmt.Fprintf(os.Stderr, "\nBeast format ADS-B data parser and console dumper\n\n")
|
||||||
|
|
@ -93,7 +91,7 @@ func parseFlags() *Config {
|
||||||
fmt.Fprintf(os.Stderr, " %s -file beast.test\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, " %s -file beast.test\n", os.Args[0])
|
||||||
fmt.Fprintf(os.Stderr, " %s -tcp localhost:30005 -verbose -count 100\n", os.Args[0])
|
fmt.Fprintf(os.Stderr, " %s -tcp localhost:30005 -verbose -count 100\n", os.Args[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
@ -104,11 +102,11 @@ func NewBeastDumper(config *Config) *BeastDumper {
|
||||||
config: config,
|
config: config,
|
||||||
decoder: modes.NewDecoder(0.0, 0.0), // beast-dump doesn't have reference position, use default
|
decoder: modes.NewDecoder(0.0, 0.0), // beast-dump doesn't have reference position, use default
|
||||||
stats: struct {
|
stats: struct {
|
||||||
totalMessages int64
|
totalMessages int64
|
||||||
validMessages int64
|
validMessages int64
|
||||||
aircraftSeen map[uint32]bool
|
aircraftSeen map[uint32]bool
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
lastMessageTime time.Time
|
lastMessageTime time.Time
|
||||||
}{
|
}{
|
||||||
aircraftSeen: make(map[uint32]bool),
|
aircraftSeen: make(map[uint32]bool),
|
||||||
startTime: time.Now(),
|
startTime: time.Now(),
|
||||||
|
|
@ -120,10 +118,10 @@ func NewBeastDumper(config *Config) *BeastDumper {
|
||||||
func (d *BeastDumper) Run() error {
|
func (d *BeastDumper) Run() error {
|
||||||
fmt.Printf("Beast Data Dumper\n")
|
fmt.Printf("Beast Data Dumper\n")
|
||||||
fmt.Printf("=================\n\n")
|
fmt.Printf("=================\n\n")
|
||||||
|
|
||||||
var reader io.Reader
|
var reader io.Reader
|
||||||
var closer io.Closer
|
var closer io.Closer
|
||||||
|
|
||||||
if d.config.TCPAddress != "" {
|
if d.config.TCPAddress != "" {
|
||||||
conn, err := d.connectTCP()
|
conn, err := d.connectTCP()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -141,34 +139,34 @@ func (d *BeastDumper) Run() error {
|
||||||
closer = file
|
closer = file
|
||||||
fmt.Printf("Reading file: %s\n", d.config.FilePath)
|
fmt.Printf("Reading file: %s\n", d.config.FilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer closer.Close()
|
defer closer.Close()
|
||||||
|
|
||||||
// Create Beast parser
|
// Create Beast parser
|
||||||
d.parser = beast.NewParser(reader, "beast-dump")
|
d.parser = beast.NewParser(reader, "beast-dump")
|
||||||
|
|
||||||
fmt.Printf("Verbose mode: %t\n", d.config.Verbose)
|
fmt.Printf("Verbose mode: %t\n", d.config.Verbose)
|
||||||
if d.config.Count > 0 {
|
if d.config.Count > 0 {
|
||||||
fmt.Printf("Message limit: %d\n", d.config.Count)
|
fmt.Printf("Message limit: %d\n", d.config.Count)
|
||||||
}
|
}
|
||||||
fmt.Printf("\nStarting Beast data parsing...\n")
|
fmt.Printf("\nStarting Beast data parsing...\n")
|
||||||
fmt.Printf("%-8s %-6s %-12s %-8s %-10s %-6s %s\n",
|
fmt.Printf("%-8s %-6s %-12s %-8s %-10s %-6s %s\n",
|
||||||
"Time", "ICAO", "Type", "Signal", "Data", "Len", "Decoded")
|
"Time", "ICAO", "Type", "Signal", "Data", "Len", "Decoded")
|
||||||
fmt.Printf("%s\n",
|
fmt.Printf("%s\n",
|
||||||
"------------------------------------------------------------------------")
|
"------------------------------------------------------------------------")
|
||||||
|
|
||||||
return d.parseMessages()
|
return d.parseMessages()
|
||||||
}
|
}
|
||||||
|
|
||||||
// connectTCP establishes TCP connection to Beast stream
|
// connectTCP establishes TCP connection to Beast stream
|
||||||
func (d *BeastDumper) connectTCP() (net.Conn, error) {
|
func (d *BeastDumper) connectTCP() (net.Conn, error) {
|
||||||
fmt.Printf("Connecting to %s...\n", d.config.TCPAddress)
|
fmt.Printf("Connecting to %s...\n", d.config.TCPAddress)
|
||||||
|
|
||||||
conn, err := net.DialTimeout("tcp", d.config.TCPAddress, 10*time.Second)
|
conn, err := net.DialTimeout("tcp", d.config.TCPAddress, 10*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,14 +176,14 @@ func (d *BeastDumper) openFile() (*os.File, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check file size
|
// Check file size
|
||||||
stat, err := file.Stat()
|
stat, err := file.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
file.Close()
|
file.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("File size: %d bytes\n", stat.Size())
|
fmt.Printf("File size: %d bytes\n", stat.Size())
|
||||||
return file, nil
|
return file, nil
|
||||||
}
|
}
|
||||||
|
|
@ -198,7 +196,7 @@ func (d *BeastDumper) parseMessages() error {
|
||||||
fmt.Printf("\nReached message limit of %d\n", d.config.Count)
|
fmt.Printf("\nReached message limit of %d\n", d.config.Count)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse Beast message
|
// Parse Beast message
|
||||||
msg, err := d.parser.ReadMessage()
|
msg, err := d.parser.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -211,21 +209,21 @@ func (d *BeastDumper) parseMessages() error {
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
d.stats.totalMessages++
|
d.stats.totalMessages++
|
||||||
d.stats.lastMessageTime = time.Now()
|
d.stats.lastMessageTime = time.Now()
|
||||||
|
|
||||||
// Display Beast message info
|
// Display Beast message info
|
||||||
d.displayMessage(msg)
|
d.displayMessage(msg)
|
||||||
|
|
||||||
// Decode Mode S data if available
|
// Decode Mode S data if available
|
||||||
if msg.Type == beast.BeastModeS || msg.Type == beast.BeastModeSLong {
|
if msg.Type == beast.BeastModeS || msg.Type == beast.BeastModeSLong {
|
||||||
d.decodeAndDisplay(msg)
|
d.decodeAndDisplay(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
d.stats.validMessages++
|
d.stats.validMessages++
|
||||||
}
|
}
|
||||||
|
|
||||||
d.displayStatistics()
|
d.displayStatistics()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -233,7 +231,7 @@ func (d *BeastDumper) parseMessages() error {
|
||||||
// displayMessage shows basic Beast message information
|
// displayMessage shows basic Beast message information
|
||||||
func (d *BeastDumper) displayMessage(msg *beast.Message) {
|
func (d *BeastDumper) displayMessage(msg *beast.Message) {
|
||||||
timestamp := msg.ReceivedAt.Format("15:04:05")
|
timestamp := msg.ReceivedAt.Format("15:04:05")
|
||||||
|
|
||||||
// Extract ICAO if available
|
// Extract ICAO if available
|
||||||
icao := "------"
|
icao := "------"
|
||||||
if msg.Type == beast.BeastModeS || msg.Type == beast.BeastModeSLong {
|
if msg.Type == beast.BeastModeS || msg.Type == beast.BeastModeSLong {
|
||||||
|
|
@ -242,18 +240,18 @@ func (d *BeastDumper) displayMessage(msg *beast.Message) {
|
||||||
d.stats.aircraftSeen[icaoAddr] = true
|
d.stats.aircraftSeen[icaoAddr] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Beast message type
|
// Beast message type
|
||||||
typeStr := d.formatMessageType(msg.Type)
|
typeStr := d.formatMessageType(msg.Type)
|
||||||
|
|
||||||
// Signal strength
|
// Signal strength
|
||||||
signal := msg.GetSignalStrength()
|
signal := msg.GetSignalStrength()
|
||||||
signalStr := fmt.Sprintf("%6.1f", signal)
|
signalStr := fmt.Sprintf("%6.1f", signal)
|
||||||
|
|
||||||
// Data preview
|
// Data preview
|
||||||
dataStr := d.formatDataPreview(msg.Data)
|
dataStr := d.formatDataPreview(msg.Data)
|
||||||
|
|
||||||
fmt.Printf("%-8s %-6s %-12s %-8s %-10s %-6d ",
|
fmt.Printf("%-8s %-6s %-12s %-8s %-10s %-6d ",
|
||||||
timestamp, icao, typeStr, signalStr, dataStr, len(msg.Data))
|
timestamp, icao, typeStr, signalStr, dataStr, len(msg.Data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -268,11 +266,11 @@ func (d *BeastDumper) decodeAndDisplay(msg *beast.Message) {
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display decoded information
|
// Display decoded information
|
||||||
info := d.formatAircraftInfo(aircraft)
|
info := d.formatAircraftInfo(aircraft)
|
||||||
fmt.Printf("%s\n", info)
|
fmt.Printf("%s\n", info)
|
||||||
|
|
||||||
// Verbose details
|
// Verbose details
|
||||||
if d.config.Verbose {
|
if d.config.Verbose {
|
||||||
d.displayVerboseInfo(aircraft, msg)
|
d.displayVerboseInfo(aircraft, msg)
|
||||||
|
|
@ -300,7 +298,7 @@ func (d *BeastDumper) formatDataPreview(data []byte) string {
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
preview := ""
|
preview := ""
|
||||||
for i, b := range data {
|
for i, b := range data {
|
||||||
if i >= 4 { // Show first 4 bytes
|
if i >= 4 { // Show first 4 bytes
|
||||||
|
|
@ -308,33 +306,33 @@ func (d *BeastDumper) formatDataPreview(data []byte) string {
|
||||||
}
|
}
|
||||||
preview += fmt.Sprintf("%02X", b)
|
preview += fmt.Sprintf("%02X", b)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(data) > 4 {
|
if len(data) > 4 {
|
||||||
preview += "..."
|
preview += "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
return preview
|
return preview
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatAircraftInfo creates a summary of decoded aircraft information
|
// formatAircraftInfo creates a summary of decoded aircraft information
|
||||||
func (d *BeastDumper) formatAircraftInfo(aircraft *modes.Aircraft) string {
|
func (d *BeastDumper) formatAircraftInfo(aircraft *modes.Aircraft) string {
|
||||||
parts := []string{}
|
parts := []string{}
|
||||||
|
|
||||||
// Callsign
|
// Callsign
|
||||||
if aircraft.Callsign != "" {
|
if aircraft.Callsign != "" {
|
||||||
parts = append(parts, fmt.Sprintf("CS:%s", aircraft.Callsign))
|
parts = append(parts, fmt.Sprintf("CS:%s", aircraft.Callsign))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Position
|
// Position
|
||||||
if aircraft.Latitude != 0 || aircraft.Longitude != 0 {
|
if aircraft.Latitude != 0 || aircraft.Longitude != 0 {
|
||||||
parts = append(parts, fmt.Sprintf("POS:%.4f,%.4f", aircraft.Latitude, aircraft.Longitude))
|
parts = append(parts, fmt.Sprintf("POS:%.4f,%.4f", aircraft.Latitude, aircraft.Longitude))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Altitude
|
// Altitude
|
||||||
if aircraft.Altitude != 0 {
|
if aircraft.Altitude != 0 {
|
||||||
parts = append(parts, fmt.Sprintf("ALT:%dft", aircraft.Altitude))
|
parts = append(parts, fmt.Sprintf("ALT:%dft", aircraft.Altitude))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Speed and track
|
// Speed and track
|
||||||
if aircraft.GroundSpeed != 0 {
|
if aircraft.GroundSpeed != 0 {
|
||||||
parts = append(parts, fmt.Sprintf("SPD:%dkt", aircraft.GroundSpeed))
|
parts = append(parts, fmt.Sprintf("SPD:%dkt", aircraft.GroundSpeed))
|
||||||
|
|
@ -342,26 +340,26 @@ func (d *BeastDumper) formatAircraftInfo(aircraft *modes.Aircraft) string {
|
||||||
if aircraft.Track != 0 {
|
if aircraft.Track != 0 {
|
||||||
parts = append(parts, fmt.Sprintf("HDG:%d°", aircraft.Track))
|
parts = append(parts, fmt.Sprintf("HDG:%d°", aircraft.Track))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vertical rate
|
// Vertical rate
|
||||||
if aircraft.VerticalRate != 0 {
|
if aircraft.VerticalRate != 0 {
|
||||||
parts = append(parts, fmt.Sprintf("VS:%d", aircraft.VerticalRate))
|
parts = append(parts, fmt.Sprintf("VS:%d", aircraft.VerticalRate))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Squawk
|
// Squawk
|
||||||
if aircraft.Squawk != "" {
|
if aircraft.Squawk != "" {
|
||||||
parts = append(parts, fmt.Sprintf("SQ:%s", aircraft.Squawk))
|
parts = append(parts, fmt.Sprintf("SQ:%s", aircraft.Squawk))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emergency
|
// Emergency
|
||||||
if aircraft.Emergency != "" && aircraft.Emergency != "None" {
|
if aircraft.Emergency != "" && aircraft.Emergency != "None" {
|
||||||
parts = append(parts, fmt.Sprintf("EMG:%s", aircraft.Emergency))
|
parts = append(parts, fmt.Sprintf("EMG:%s", aircraft.Emergency))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(parts) == 0 {
|
if len(parts) == 0 {
|
||||||
return "(no data decoded)"
|
return "(no data decoded)"
|
||||||
}
|
}
|
||||||
|
|
||||||
info := ""
|
info := ""
|
||||||
for i, part := range parts {
|
for i, part := range parts {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
|
|
@ -369,7 +367,7 @@ func (d *BeastDumper) formatAircraftInfo(aircraft *modes.Aircraft) string {
|
||||||
}
|
}
|
||||||
info += part
|
info += part
|
||||||
}
|
}
|
||||||
|
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -379,7 +377,7 @@ func (d *BeastDumper) displayVerboseInfo(aircraft *modes.Aircraft, msg *beast.Me
|
||||||
fmt.Printf(" Raw Data: %s\n", d.formatHexData(msg.Data))
|
fmt.Printf(" Raw Data: %s\n", d.formatHexData(msg.Data))
|
||||||
fmt.Printf(" Timestamp: %s\n", msg.ReceivedAt.Format("15:04:05.000"))
|
fmt.Printf(" Timestamp: %s\n", msg.ReceivedAt.Format("15:04:05.000"))
|
||||||
fmt.Printf(" Signal: %.2f dBFS\n", msg.GetSignalStrength())
|
fmt.Printf(" Signal: %.2f dBFS\n", msg.GetSignalStrength())
|
||||||
|
|
||||||
fmt.Printf(" Aircraft Data:\n")
|
fmt.Printf(" Aircraft Data:\n")
|
||||||
if aircraft.Callsign != "" {
|
if aircraft.Callsign != "" {
|
||||||
fmt.Printf(" Callsign: %s\n", aircraft.Callsign)
|
fmt.Printf(" Callsign: %s\n", aircraft.Callsign)
|
||||||
|
|
@ -420,23 +418,23 @@ func (d *BeastDumper) formatHexData(data []byte) string {
|
||||||
// displayStatistics shows final parsing statistics
|
// displayStatistics shows final parsing statistics
|
||||||
func (d *BeastDumper) displayStatistics() {
|
func (d *BeastDumper) displayStatistics() {
|
||||||
duration := time.Since(d.stats.startTime)
|
duration := time.Since(d.stats.startTime)
|
||||||
|
|
||||||
fmt.Printf("\nStatistics:\n")
|
fmt.Printf("\nStatistics:\n")
|
||||||
fmt.Printf("===========\n")
|
fmt.Printf("===========\n")
|
||||||
fmt.Printf("Total messages: %d\n", d.stats.totalMessages)
|
fmt.Printf("Total messages: %d\n", d.stats.totalMessages)
|
||||||
fmt.Printf("Valid messages: %d\n", d.stats.validMessages)
|
fmt.Printf("Valid messages: %d\n", d.stats.validMessages)
|
||||||
fmt.Printf("Unique aircraft: %d\n", len(d.stats.aircraftSeen))
|
fmt.Printf("Unique aircraft: %d\n", len(d.stats.aircraftSeen))
|
||||||
fmt.Printf("Duration: %v\n", duration.Round(time.Second))
|
fmt.Printf("Duration: %v\n", duration.Round(time.Second))
|
||||||
|
|
||||||
if d.stats.totalMessages > 0 && duration > 0 {
|
if d.stats.totalMessages > 0 && duration > 0 {
|
||||||
rate := float64(d.stats.totalMessages) / duration.Seconds()
|
rate := float64(d.stats.totalMessages) / duration.Seconds()
|
||||||
fmt.Printf("Message rate: %.1f msg/sec\n", rate)
|
fmt.Printf("Message rate: %.1f msg/sec\n", rate)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(d.stats.aircraftSeen) > 0 {
|
if len(d.stats.aircraftSeen) > 0 {
|
||||||
fmt.Printf("\nAircraft seen:\n")
|
fmt.Printf("\nAircraft seen:\n")
|
||||||
for icao := range d.stats.aircraftSeen {
|
for icao := range d.stats.aircraftSeen {
|
||||||
fmt.Printf(" %06X\n", icao)
|
fmt.Printf(" %06X\n", icao)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
15
config.json.example
Normal file
15
config.json.example
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"address": ":8080",
|
||||||
|
"port": 8080
|
||||||
|
},
|
||||||
|
"dump1090": {
|
||||||
|
"host": "192.168.1.100",
|
||||||
|
"data_port": 30003
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"latitude": 37.7749,
|
||||||
|
"longitude": -122.4194,
|
||||||
|
"name": "San Francisco"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
debian/DEBIAN/control
vendored
6
debian/DEBIAN/control
vendored
|
|
@ -1,10 +1,10 @@
|
||||||
Package: skyview
|
Package: skyview
|
||||||
Version: 0.0.2
|
Version: 2.0.0
|
||||||
Section: net
|
Section: net
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Architecture: amd64
|
Architecture: amd64
|
||||||
Depends: systemd
|
Depends: systemd
|
||||||
Maintainer: Ole-Morten Duesund <glemt.net>
|
Maintainer: SkyView Team <admin@skyview.local>
|
||||||
Description: Multi-source ADS-B aircraft tracker with Beast format support
|
Description: Multi-source ADS-B aircraft tracker with Beast format support
|
||||||
SkyView is a standalone application that connects to multiple dump1090 Beast
|
SkyView is a standalone application that connects to multiple dump1090 Beast
|
||||||
format TCP streams and provides a modern web frontend for aircraft tracking.
|
format TCP streams and provides a modern web frontend for aircraft tracking.
|
||||||
|
|
@ -20,4 +20,4 @@ Description: Multi-source ADS-B aircraft tracker with Beast format support
|
||||||
- Mobile-responsive design
|
- Mobile-responsive design
|
||||||
- Systemd integration for service management
|
- Systemd integration for service management
|
||||||
- Beast-dump utility for raw ADS-B data analysis
|
- Beast-dump utility for raw ADS-B data analysis
|
||||||
Homepage: https://kode.naiv.no/olemd/skyview
|
Homepage: https://github.com/skyview/skyview
|
||||||
|
|
|
||||||
33
debian/DEBIAN/postinst
vendored
33
debian/DEBIAN/postinst
vendored
|
|
@ -5,34 +5,35 @@ case "$1" in
|
||||||
configure)
|
configure)
|
||||||
# Create skyview user and group if they don't exist
|
# Create skyview user and group if they don't exist
|
||||||
if ! getent group skyview >/dev/null 2>&1; then
|
if ! getent group skyview >/dev/null 2>&1; then
|
||||||
addgroup --system --quiet skyview
|
addgroup --system skyview
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! getent passwd skyview >/dev/null 2>&1; then
|
if ! getent passwd skyview >/dev/null 2>&1; then
|
||||||
adduser --system --ingroup skyview --home /var/lib/skyview \
|
adduser --system --ingroup skyview --home /var/lib/skyview \
|
||||||
--no-create-home --disabled-password --quiet skyview
|
--no-create-home --disabled-password skyview
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create directories with proper permissions
|
# Create directories with proper permissions
|
||||||
mkdir -p /var/lib/skyview /var/log/skyview >/dev/null 2>&1 || true
|
mkdir -p /var/lib/skyview
|
||||||
chown skyview:skyview /var/lib/skyview /var/log/skyview >/dev/null 2>&1 || true
|
mkdir -p /var/log/skyview
|
||||||
chmod 755 /var/lib/skyview /var/log/skyview >/dev/null 2>&1 || true
|
chown skyview:skyview /var/lib/skyview
|
||||||
|
chown skyview:skyview /var/log/skyview
|
||||||
|
chmod 755 /var/lib/skyview
|
||||||
|
chmod 755 /var/log/skyview
|
||||||
|
|
||||||
# Set permissions on config files
|
# Set permissions on config file
|
||||||
if [ -f /etc/skyview/config.json ]; then
|
if [ -f /etc/skyview/config.json ]; then
|
||||||
chown root:skyview /etc/skyview/config.json >/dev/null 2>&1 || true
|
chown root:skyview /etc/skyview/config.json
|
||||||
chmod 640 /etc/skyview/config.json >/dev/null 2>&1 || true
|
chmod 640 /etc/skyview/config.json
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Enable and start the service
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable skyview.service
|
||||||
|
|
||||||
# Handle systemd service
|
echo "SkyView has been installed and configured."
|
||||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
echo "Edit /etc/skyview/config.json to configure your dump1090 sources."
|
||||||
|
echo "Then run: systemctl start skyview"
|
||||||
# Check if service was previously enabled
|
|
||||||
if systemctl is-enabled skyview >/dev/null 2>&1; then
|
|
||||||
# Service was enabled, restart it
|
|
||||||
systemctl restart skyview >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
|
|
||||||
BIN
debian/usr/bin/beast-dump
vendored
Executable file
BIN
debian/usr/bin/beast-dump
vendored
Executable file
Binary file not shown.
6
debian/usr/share/man/man1/beast-dump.1
vendored
6
debian/usr/share/man/man1/beast-dump.1
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
.TH BEAST-DUMP 1 "2025-08-24" "SkyView 0.0.2" "User Commands"
|
.TH BEAST-DUMP 1 "2024-08-24" "SkyView 2.0.0" "User Commands"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
beast-dump \- Utility for analyzing raw ADS-B data in Beast binary format
|
beast-dump \- Utility for analyzing raw ADS-B data in Beast binary format
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
|
|
@ -90,6 +90,6 @@ Beast format files typically use .bin or .beast extensions.
|
||||||
.BR skyview (1),
|
.BR skyview (1),
|
||||||
.BR dump1090 (1)
|
.BR dump1090 (1)
|
||||||
.SH BUGS
|
.SH BUGS
|
||||||
Report bugs at: https://kode.naiv.no/olemd/skyview/issues
|
Report bugs at: https://github.com/skyview/skyview/issues
|
||||||
.SH AUTHOR
|
.SH AUTHOR
|
||||||
Ole-Morten Duesund <glemt.net>
|
SkyView Team <admin@skyview.local>
|
||||||
6
debian/usr/share/man/man1/skyview.1
vendored
6
debian/usr/share/man/man1/skyview.1
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
.TH SKYVIEW 1 "2025-08-24" "SkyView 0.0.2" "User Commands"
|
.TH SKYVIEW 1 "2024-08-24" "SkyView 2.0.0" "User Commands"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
skyview \- Multi-source ADS-B aircraft tracker with Beast format support
|
skyview \- Multi-source ADS-B aircraft tracker with Beast format support
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
|
|
@ -83,6 +83,6 @@ Coverage heatmaps and range circles
|
||||||
.BR beast-dump (1),
|
.BR beast-dump (1),
|
||||||
.BR dump1090 (1)
|
.BR dump1090 (1)
|
||||||
.SH BUGS
|
.SH BUGS
|
||||||
Report bugs at: https://kode.naiv.no/olemd/skyview/issues
|
Report bugs at: https://github.com/skyview/skyview/issues
|
||||||
.SH AUTHOR
|
.SH AUTHOR
|
||||||
Ole-Morten Duesund <glemt.net>
|
SkyView Team <admin@skyview.local>
|
||||||
|
|
@ -88,12 +88,12 @@ func NewParser(r io.Reader, sourceID string) *Parser {
|
||||||
// ReadMessage reads and parses a single Beast message from the stream.
|
// ReadMessage reads and parses a single Beast message from the stream.
|
||||||
//
|
//
|
||||||
// The parsing process:
|
// The parsing process:
|
||||||
// 1. Search for the escape character (0x1A) that marks message start
|
// 1. Search for the escape character (0x1A) that marks message start
|
||||||
// 2. Read and validate the message type byte
|
// 2. Read and validate the message type byte
|
||||||
// 3. Read the 48-bit timestamp (big-endian, padded to 64-bit)
|
// 3. Read the 48-bit timestamp (big-endian, padded to 64-bit)
|
||||||
// 4. Read the signal level byte
|
// 4. Read the signal level byte
|
||||||
// 5. Read the message payload (length depends on message type)
|
// 5. Read the message payload (length depends on message type)
|
||||||
// 6. Process escape sequences in the payload data
|
// 6. Process escape sequences in the payload data
|
||||||
//
|
//
|
||||||
// The parser can recover from protocol errors by continuing to search for
|
// The parser can recover from protocol errors by continuing to search for
|
||||||
// the next valid message boundary. Status messages are currently skipped
|
// the next valid message boundary. Status messages are currently skipped
|
||||||
|
|
@ -253,7 +253,7 @@ func (msg *Message) GetSignalStrength() float64 {
|
||||||
// The ICAO address is a unique 24-bit identifier assigned to each aircraft.
|
// The ICAO address is a unique 24-bit identifier assigned to each aircraft.
|
||||||
// In Mode S messages, it's located in bytes 1-3 of the message payload:
|
// In Mode S messages, it's located in bytes 1-3 of the message payload:
|
||||||
// - Byte 1: Most significant 8 bits
|
// - Byte 1: Most significant 8 bits
|
||||||
// - Byte 2: Middle 8 bits
|
// - Byte 2: Middle 8 bits
|
||||||
// - Byte 3: Least significant 8 bits
|
// - Byte 3: Least significant 8 bits
|
||||||
//
|
//
|
||||||
// Mode A/C messages don't contain ICAO addresses and will return an error.
|
// Mode A/C messages don't contain ICAO addresses and will return an error.
|
||||||
|
|
|
||||||
|
|
@ -39,15 +39,15 @@ import (
|
||||||
// continuously processes incoming messages until stopped or the source
|
// continuously processes incoming messages until stopped or the source
|
||||||
// becomes unavailable.
|
// becomes unavailable.
|
||||||
type BeastClient struct {
|
type BeastClient struct {
|
||||||
source *merger.Source // Source configuration and status
|
source *merger.Source // Source configuration and status
|
||||||
merger *merger.Merger // Data merger for multi-source fusion
|
merger *merger.Merger // Data merger for multi-source fusion
|
||||||
decoder *modes.Decoder // Mode S/ADS-B message decoder
|
decoder *modes.Decoder // Mode S/ADS-B message decoder
|
||||||
conn net.Conn // TCP connection to Beast source
|
conn net.Conn // TCP connection to Beast source
|
||||||
parser *beast.Parser // Beast format message parser
|
parser *beast.Parser // Beast format message parser
|
||||||
msgChan chan *beast.Message // Buffered channel for parsed messages
|
msgChan chan *beast.Message // Buffered channel for parsed messages
|
||||||
errChan chan error // Error reporting channel
|
errChan chan error // Error reporting channel
|
||||||
stopChan chan struct{} // Shutdown signal channel
|
stopChan chan struct{} // Shutdown signal channel
|
||||||
wg sync.WaitGroup // Wait group for goroutine coordination
|
wg sync.WaitGroup // Wait group for goroutine coordination
|
||||||
|
|
||||||
// Reconnection parameters
|
// Reconnection parameters
|
||||||
reconnectDelay time.Duration // Initial reconnect delay
|
reconnectDelay time.Duration // Initial reconnect delay
|
||||||
|
|
@ -102,9 +102,9 @@ func (c *BeastClient) Start(ctx context.Context) {
|
||||||
// Stop gracefully shuts down the client and all associated goroutines.
|
// Stop gracefully shuts down the client and all associated goroutines.
|
||||||
//
|
//
|
||||||
// The shutdown process:
|
// The shutdown process:
|
||||||
// 1. Signals all goroutines to stop via stopChan
|
// 1. Signals all goroutines to stop via stopChan
|
||||||
// 2. Closes the TCP connection if active
|
// 2. Closes the TCP connection if active
|
||||||
// 3. Waits for all goroutines to complete
|
// 3. Waits for all goroutines to complete
|
||||||
//
|
//
|
||||||
// This method blocks until the shutdown is complete.
|
// This method blocks until the shutdown is complete.
|
||||||
func (c *BeastClient) Stop() {
|
func (c *BeastClient) Stop() {
|
||||||
|
|
@ -118,11 +118,11 @@ func (c *BeastClient) Stop() {
|
||||||
// run implements the main client connection and reconnection loop.
|
// run implements the main client connection and reconnection loop.
|
||||||
//
|
//
|
||||||
// This method handles the complete client lifecycle:
|
// This method handles the complete client lifecycle:
|
||||||
// 1. Connection establishment with timeout
|
// 1. Connection establishment with timeout
|
||||||
// 2. Exponential backoff on connection failures
|
// 2. Exponential backoff on connection failures
|
||||||
// 3. Message parsing and processing goroutine management
|
// 3. Message parsing and processing goroutine management
|
||||||
// 4. Connection monitoring and failure detection
|
// 4. Connection monitoring and failure detection
|
||||||
// 5. Automatic reconnection on disconnection
|
// 5. Automatic reconnection on disconnection
|
||||||
//
|
//
|
||||||
// The exponential backoff starts at reconnectDelay (5s) and doubles on each
|
// The exponential backoff starts at reconnectDelay (5s) and doubles on each
|
||||||
// failure up to maxReconnect (60s), then resets on successful connection.
|
// failure up to maxReconnect (60s), then resets on successful connection.
|
||||||
|
|
@ -210,10 +210,10 @@ func (c *BeastClient) readMessages() {
|
||||||
// processMessages runs in a dedicated goroutine to decode and merge aircraft data.
|
// processMessages runs in a dedicated goroutine to decode and merge aircraft data.
|
||||||
//
|
//
|
||||||
// For each received Beast message, this method:
|
// For each received Beast message, this method:
|
||||||
// 1. Decodes the Mode S/ADS-B message payload
|
// 1. Decodes the Mode S/ADS-B message payload
|
||||||
// 2. Extracts aircraft information (position, altitude, speed, etc.)
|
// 2. Extracts aircraft information (position, altitude, speed, etc.)
|
||||||
// 3. Updates the data merger with new aircraft state
|
// 3. Updates the data merger with new aircraft state
|
||||||
// 4. Updates source statistics (message count)
|
// 4. Updates source statistics (message count)
|
||||||
//
|
//
|
||||||
// Invalid or unparseable messages are silently discarded to maintain
|
// Invalid or unparseable messages are silently discarded to maintain
|
||||||
// system stability. The merger handles data fusion from multiple sources
|
// system stability. The merger handles data fusion from multiple sources
|
||||||
|
|
@ -262,9 +262,9 @@ func (c *BeastClient) processMessages() {
|
||||||
// All clients share the same data merger, enabling automatic data fusion
|
// All clients share the same data merger, enabling automatic data fusion
|
||||||
// and conflict resolution across multiple receivers.
|
// and conflict resolution across multiple receivers.
|
||||||
type MultiSourceClient struct {
|
type MultiSourceClient struct {
|
||||||
clients []*BeastClient // Managed Beast clients
|
clients []*BeastClient // Managed Beast clients
|
||||||
merger *merger.Merger // Shared data merger for all sources
|
merger *merger.Merger // Shared data merger for all sources
|
||||||
mu sync.RWMutex // Protects clients slice
|
mu sync.RWMutex // Protects clients slice
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMultiSourceClient creates a client manager for multiple Beast format sources.
|
// NewMultiSourceClient creates a client manager for multiple Beast format sources.
|
||||||
|
|
@ -292,9 +292,9 @@ func NewMultiSourceClient(merger *merger.Merger) *MultiSourceClient {
|
||||||
// AddSource registers and configures a new Beast format data source.
|
// AddSource registers and configures a new Beast format data source.
|
||||||
//
|
//
|
||||||
// This method:
|
// This method:
|
||||||
// 1. Registers the source with the data merger
|
// 1. Registers the source with the data merger
|
||||||
// 2. Creates a new BeastClient for the source
|
// 2. Creates a new BeastClient for the source
|
||||||
// 3. Adds the client to the managed clients list
|
// 3. Adds the client to the managed clients list
|
||||||
//
|
//
|
||||||
// The source is not automatically started; call Start() to begin connections.
|
// The source is not automatically started; call Start() to begin connections.
|
||||||
// Sources can be added before or after starting the multi-source client.
|
// Sources can be added before or after starting the multi-source client.
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ type CountryInfo struct {
|
||||||
// NewDatabase creates a new ICAO database with comprehensive allocation data
|
// NewDatabase creates a new ICAO database with comprehensive allocation data
|
||||||
func NewDatabase() (*Database, error) {
|
func NewDatabase() (*Database, error) {
|
||||||
allocations := getICAOAllocations()
|
allocations := getICAOAllocations()
|
||||||
|
|
||||||
// Sort allocations by start address for efficient binary search
|
// Sort allocations by start address for efficient binary search
|
||||||
sort.Slice(allocations, func(i, j int) bool {
|
sort.Slice(allocations, func(i, j int) bool {
|
||||||
return allocations[i].StartAddr < allocations[j].StartAddr
|
return allocations[i].StartAddr < allocations[j].StartAddr
|
||||||
|
|
@ -265,4 +265,4 @@ func (d *Database) LookupCountry(icaoHex string) (*CountryInfo, error) {
|
||||||
// Close is a no-op since we don't have any resources to clean up
|
// Close is a no-op since we don't have any resources to clean up
|
||||||
func (d *Database) Close() error {
|
func (d *Database) Close() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -22,7 +22,6 @@ package merger
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"math"
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -34,31 +33,8 @@ import (
|
||||||
const (
|
const (
|
||||||
// MaxDistance represents an infinite distance for initialization
|
// MaxDistance represents an infinite distance for initialization
|
||||||
MaxDistance = float64(999999)
|
MaxDistance = float64(999999)
|
||||||
|
|
||||||
// Position validation constants
|
|
||||||
MaxSpeedKnots = 2000.0 // Maximum plausible aircraft speed (roughly Mach 3 at cruise altitude)
|
|
||||||
MaxDistanceNautMiles = 500.0 // Maximum position jump distance in nautical miles
|
|
||||||
MaxAltitudeFeet = 60000 // Maximum altitude in feet (commercial ceiling ~FL600)
|
|
||||||
MinAltitudeFeet = -500 // Minimum altitude (below sea level but allow for dead sea, etc.)
|
|
||||||
|
|
||||||
// Earth coordinate bounds
|
|
||||||
MinLatitude = -90.0
|
|
||||||
MaxLatitude = 90.0
|
|
||||||
MinLongitude = -180.0
|
|
||||||
MaxLongitude = 180.0
|
|
||||||
|
|
||||||
// Conversion factors
|
|
||||||
KnotsToKmh = 1.852
|
|
||||||
NmToKm = 1.852
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ValidationResult represents the result of position validation checks.
|
|
||||||
type ValidationResult struct {
|
|
||||||
Valid bool // Whether the position passed all validation checks
|
|
||||||
Errors []string // List of validation failures for debugging
|
|
||||||
Warnings []string // List of potential issues (not blocking)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Source represents a data source (dump1090 receiver or similar ADS-B source).
|
// Source represents a data source (dump1090 receiver or similar ADS-B source).
|
||||||
// It contains both static configuration and dynamic status information used
|
// It contains both static configuration and dynamic status information used
|
||||||
// for data fusion decisions and source monitoring.
|
// for data fusion decisions and source monitoring.
|
||||||
|
|
@ -112,33 +88,30 @@ func (a *AircraftState) MarshalJSON() ([]byte, error) {
|
||||||
// Create a struct that mirrors AircraftState but with ICAO24 as string
|
// Create a struct that mirrors AircraftState but with ICAO24 as string
|
||||||
return json.Marshal(&struct {
|
return json.Marshal(&struct {
|
||||||
// From embedded modes.Aircraft
|
// From embedded modes.Aircraft
|
||||||
ICAO24 string `json:"ICAO24"`
|
ICAO24 string `json:"ICAO24"`
|
||||||
Callsign string `json:"Callsign"`
|
Callsign string `json:"Callsign"`
|
||||||
Latitude float64 `json:"Latitude"`
|
Latitude float64 `json:"Latitude"`
|
||||||
Longitude float64 `json:"Longitude"`
|
Longitude float64 `json:"Longitude"`
|
||||||
Altitude int `json:"Altitude"`
|
Altitude int `json:"Altitude"`
|
||||||
BaroAltitude int `json:"BaroAltitude"`
|
BaroAltitude int `json:"BaroAltitude"`
|
||||||
GeomAltitude int `json:"GeomAltitude"`
|
GeomAltitude int `json:"GeomAltitude"`
|
||||||
VerticalRate int `json:"VerticalRate"`
|
VerticalRate int `json:"VerticalRate"`
|
||||||
GroundSpeed int `json:"GroundSpeed"`
|
GroundSpeed int `json:"GroundSpeed"`
|
||||||
Track int `json:"Track"`
|
Track int `json:"Track"`
|
||||||
Heading int `json:"Heading"`
|
Heading int `json:"Heading"`
|
||||||
Category string `json:"Category"`
|
Category string `json:"Category"`
|
||||||
Squawk string `json:"Squawk"`
|
Squawk string `json:"Squawk"`
|
||||||
Emergency string `json:"Emergency"`
|
Emergency string `json:"Emergency"`
|
||||||
OnGround bool `json:"OnGround"`
|
OnGround bool `json:"OnGround"`
|
||||||
Alert bool `json:"Alert"`
|
Alert bool `json:"Alert"`
|
||||||
SPI bool `json:"SPI"`
|
SPI bool `json:"SPI"`
|
||||||
NACp uint8 `json:"NACp"`
|
NACp uint8 `json:"NACp"`
|
||||||
NACv uint8 `json:"NACv"`
|
NACv uint8 `json:"NACv"`
|
||||||
SIL uint8 `json:"SIL"`
|
SIL uint8 `json:"SIL"`
|
||||||
TransponderCapability string `json:"TransponderCapability"`
|
SelectedAltitude int `json:"SelectedAltitude"`
|
||||||
TransponderLevel uint8 `json:"TransponderLevel"`
|
SelectedHeading float64 `json:"SelectedHeading"`
|
||||||
SignalQuality string `json:"SignalQuality"`
|
BaroSetting float64 `json:"BaroSetting"`
|
||||||
SelectedAltitude int `json:"SelectedAltitude"`
|
|
||||||
SelectedHeading float64 `json:"SelectedHeading"`
|
|
||||||
BaroSetting float64 `json:"BaroSetting"`
|
|
||||||
|
|
||||||
// From AircraftState
|
// From AircraftState
|
||||||
Sources map[string]*SourceData `json:"sources"`
|
Sources map[string]*SourceData `json:"sources"`
|
||||||
LastUpdate time.Time `json:"last_update"`
|
LastUpdate time.Time `json:"last_update"`
|
||||||
|
|
@ -159,33 +132,30 @@ func (a *AircraftState) MarshalJSON() ([]byte, error) {
|
||||||
Flag string `json:"flag"`
|
Flag string `json:"flag"`
|
||||||
}{
|
}{
|
||||||
// Copy all fields from Aircraft
|
// Copy all fields from Aircraft
|
||||||
ICAO24: fmt.Sprintf("%06X", a.Aircraft.ICAO24),
|
ICAO24: fmt.Sprintf("%06X", a.Aircraft.ICAO24),
|
||||||
Callsign: a.Aircraft.Callsign,
|
Callsign: a.Aircraft.Callsign,
|
||||||
Latitude: a.Aircraft.Latitude,
|
Latitude: a.Aircraft.Latitude,
|
||||||
Longitude: a.Aircraft.Longitude,
|
Longitude: a.Aircraft.Longitude,
|
||||||
Altitude: a.Aircraft.Altitude,
|
Altitude: a.Aircraft.Altitude,
|
||||||
BaroAltitude: a.Aircraft.BaroAltitude,
|
BaroAltitude: a.Aircraft.BaroAltitude,
|
||||||
GeomAltitude: a.Aircraft.GeomAltitude,
|
GeomAltitude: a.Aircraft.GeomAltitude,
|
||||||
VerticalRate: a.Aircraft.VerticalRate,
|
VerticalRate: a.Aircraft.VerticalRate,
|
||||||
GroundSpeed: a.Aircraft.GroundSpeed,
|
GroundSpeed: a.Aircraft.GroundSpeed,
|
||||||
Track: a.Aircraft.Track,
|
Track: a.Aircraft.Track,
|
||||||
Heading: a.Aircraft.Heading,
|
Heading: a.Aircraft.Heading,
|
||||||
Category: a.Aircraft.Category,
|
Category: a.Aircraft.Category,
|
||||||
Squawk: a.Aircraft.Squawk,
|
Squawk: a.Aircraft.Squawk,
|
||||||
Emergency: a.Aircraft.Emergency,
|
Emergency: a.Aircraft.Emergency,
|
||||||
OnGround: a.Aircraft.OnGround,
|
OnGround: a.Aircraft.OnGround,
|
||||||
Alert: a.Aircraft.Alert,
|
Alert: a.Aircraft.Alert,
|
||||||
SPI: a.Aircraft.SPI,
|
SPI: a.Aircraft.SPI,
|
||||||
NACp: a.Aircraft.NACp,
|
NACp: a.Aircraft.NACp,
|
||||||
NACv: a.Aircraft.NACv,
|
NACv: a.Aircraft.NACv,
|
||||||
SIL: a.Aircraft.SIL,
|
SIL: a.Aircraft.SIL,
|
||||||
TransponderCapability: a.Aircraft.TransponderCapability,
|
SelectedAltitude: a.Aircraft.SelectedAltitude,
|
||||||
TransponderLevel: a.Aircraft.TransponderLevel,
|
SelectedHeading: a.Aircraft.SelectedHeading,
|
||||||
SignalQuality: a.Aircraft.SignalQuality,
|
BaroSetting: a.Aircraft.BaroSetting,
|
||||||
SelectedAltitude: a.Aircraft.SelectedAltitude,
|
|
||||||
SelectedHeading: a.Aircraft.SelectedHeading,
|
|
||||||
BaroSetting: a.Aircraft.BaroSetting,
|
|
||||||
|
|
||||||
// Copy all fields from AircraftState
|
// Copy all fields from AircraftState
|
||||||
Sources: a.Sources,
|
Sources: a.Sources,
|
||||||
LastUpdate: a.LastUpdate,
|
LastUpdate: a.LastUpdate,
|
||||||
|
|
@ -268,7 +238,7 @@ type Merger struct {
|
||||||
sources map[string]*Source // Source ID -> source information
|
sources map[string]*Source // Source ID -> source information
|
||||||
icaoDB *icao.Database // ICAO country lookup database
|
icaoDB *icao.Database // ICAO country lookup database
|
||||||
mu sync.RWMutex // Protects all maps and slices
|
mu sync.RWMutex // Protects all maps and slices
|
||||||
historyLimit int // Maximum history points to retain
|
historyLimit int // Maximum history points to retain
|
||||||
staleTimeout time.Duration // Time before aircraft considered stale (15 seconds)
|
staleTimeout time.Duration // Time before aircraft considered stale (15 seconds)
|
||||||
updateMetrics map[uint32]*updateMetric // ICAO24 -> update rate calculation data
|
updateMetrics map[uint32]*updateMetric // ICAO24 -> update rate calculation data
|
||||||
}
|
}
|
||||||
|
|
@ -321,13 +291,13 @@ func (m *Merger) AddSource(source *Source) {
|
||||||
// UpdateAircraft merges new aircraft data from a source using intelligent fusion strategies.
|
// UpdateAircraft merges new aircraft data from a source using intelligent fusion strategies.
|
||||||
//
|
//
|
||||||
// This is the core method of the merger, handling:
|
// This is the core method of the merger, handling:
|
||||||
// 1. Aircraft state creation for new aircraft
|
// 1. Aircraft state creation for new aircraft
|
||||||
// 2. Source data tracking and statistics
|
// 2. Source data tracking and statistics
|
||||||
// 3. Multi-source data fusion with conflict resolution
|
// 3. Multi-source data fusion with conflict resolution
|
||||||
// 4. Historical data updates with retention limits
|
// 4. Historical data updates with retention limits
|
||||||
// 5. Distance and bearing calculations
|
// 5. Distance and bearing calculations
|
||||||
// 6. Update rate metrics
|
// 6. Update rate metrics
|
||||||
// 7. Source status maintenance
|
// 7. Source status maintenance
|
||||||
//
|
//
|
||||||
// Data fusion strategies:
|
// Data fusion strategies:
|
||||||
// - Position: Use source with strongest signal
|
// - Position: Use source with strongest signal
|
||||||
|
|
@ -356,7 +326,7 @@ func (m *Merger) UpdateAircraft(sourceID string, aircraft *modes.Aircraft, signa
|
||||||
AltitudeHistory: make([]AltitudePoint, 0),
|
AltitudeHistory: make([]AltitudePoint, 0),
|
||||||
SpeedHistory: make([]SpeedPoint, 0),
|
SpeedHistory: make([]SpeedPoint, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lookup country information for new aircraft
|
// Lookup country information for new aircraft
|
||||||
icaoHex := fmt.Sprintf("%06X", aircraft.ICAO24)
|
icaoHex := fmt.Sprintf("%06X", aircraft.ICAO24)
|
||||||
if countryInfo, err := m.icaoDB.LookupCountry(icaoHex); err == nil {
|
if countryInfo, err := m.icaoDB.LookupCountry(icaoHex); err == nil {
|
||||||
|
|
@ -369,7 +339,7 @@ func (m *Merger) UpdateAircraft(sourceID string, aircraft *modes.Aircraft, signa
|
||||||
state.CountryCode = "XX"
|
state.CountryCode = "XX"
|
||||||
state.Flag = "🏳️"
|
state.Flag = "🏳️"
|
||||||
}
|
}
|
||||||
|
|
||||||
m.aircraft[aircraft.ICAO24] = state
|
m.aircraft[aircraft.ICAO24] = state
|
||||||
m.updateMetrics[aircraft.ICAO24] = &updateMetric{
|
m.updateMetrics[aircraft.ICAO24] = &updateMetric{
|
||||||
updates: make([]time.Time, 0),
|
updates: make([]time.Time, 0),
|
||||||
|
|
@ -447,46 +417,28 @@ func (m *Merger) UpdateAircraft(sourceID string, aircraft *modes.Aircraft, signa
|
||||||
// - sourceID: Identifier of source providing new data
|
// - sourceID: Identifier of source providing new data
|
||||||
// - timestamp: Timestamp of new data
|
// - timestamp: Timestamp of new data
|
||||||
func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, sourceID string, timestamp time.Time) {
|
func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, sourceID string, timestamp time.Time) {
|
||||||
// Position - use source with best signal or most recent, but validate first
|
// Position - use source with best signal or most recent
|
||||||
if new.Latitude != 0 && new.Longitude != 0 {
|
if new.Latitude != 0 && new.Longitude != 0 {
|
||||||
// Always validate position before considering update
|
updatePosition := false
|
||||||
validation := m.validatePosition(new, state, timestamp)
|
|
||||||
|
|
||||||
if !validation.Valid {
|
if state.Latitude == 0 {
|
||||||
// Log validation errors and skip position update
|
// First position update
|
||||||
icaoHex := fmt.Sprintf("%06X", new.ICAO24)
|
updatePosition = true
|
||||||
for _, err := range validation.Errors {
|
} else if srcData, ok := state.Sources[sourceID]; ok {
|
||||||
log.Printf("[POSITION_VALIDATION] ICAO %s: REJECTED position update - %s", icaoHex, err)
|
// Use position from source with strongest signal
|
||||||
}
|
currentBest := m.getBestSignalSource(state)
|
||||||
} else {
|
if currentBest == "" || srcData.SignalLevel > state.Sources[currentBest].SignalLevel {
|
||||||
// Position is valid, proceed with normal logic
|
updatePosition = true
|
||||||
updatePosition := false
|
} else if currentBest == sourceID {
|
||||||
|
// Same source as current best - allow updates for moving aircraft
|
||||||
if state.Latitude == 0 {
|
|
||||||
// First position update
|
|
||||||
updatePosition = true
|
updatePosition = true
|
||||||
} else if srcData, ok := state.Sources[sourceID]; ok {
|
|
||||||
// Use position from source with strongest signal
|
|
||||||
currentBest := m.getBestSignalSource(state)
|
|
||||||
if currentBest == "" || srcData.SignalLevel > state.Sources[currentBest].SignalLevel {
|
|
||||||
updatePosition = true
|
|
||||||
} else if currentBest == sourceID {
|
|
||||||
// Same source as current best - allow updates for moving aircraft
|
|
||||||
updatePosition = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if updatePosition {
|
|
||||||
state.Latitude = new.Latitude
|
|
||||||
state.Longitude = new.Longitude
|
|
||||||
state.PositionSource = sourceID
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log warnings even if position is valid
|
if updatePosition {
|
||||||
for _, warning := range validation.Warnings {
|
state.Latitude = new.Latitude
|
||||||
icaoHex := fmt.Sprintf("%06X", new.ICAO24)
|
state.Longitude = new.Longitude
|
||||||
log.Printf("[POSITION_VALIDATION] ICAO %s: WARNING - %s", icaoHex, warning)
|
state.PositionSource = sourceID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -557,27 +509,6 @@ func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, so
|
||||||
if new.BaroSetting != 0 {
|
if new.BaroSetting != 0 {
|
||||||
state.BaroSetting = new.BaroSetting
|
state.BaroSetting = new.BaroSetting
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transponder information - use most recent non-empty
|
|
||||||
if new.TransponderCapability != "" {
|
|
||||||
state.TransponderCapability = new.TransponderCapability
|
|
||||||
}
|
|
||||||
if new.TransponderLevel > 0 {
|
|
||||||
state.TransponderLevel = new.TransponderLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signal quality - use most recent non-empty (prefer higher quality assessments)
|
|
||||||
if new.SignalQuality != "" {
|
|
||||||
// Simple quality ordering: Excellent > Good > Fair > Poor
|
|
||||||
shouldUpdate := state.SignalQuality == "" ||
|
|
||||||
(new.SignalQuality == "Excellent") ||
|
|
||||||
(new.SignalQuality == "Good" && state.SignalQuality != "Excellent") ||
|
|
||||||
(new.SignalQuality == "Fair" && state.SignalQuality == "Poor")
|
|
||||||
|
|
||||||
if shouldUpdate {
|
|
||||||
state.SignalQuality = new.SignalQuality
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateHistories adds data points to historical tracking arrays.
|
// updateHistories adds data points to historical tracking arrays.
|
||||||
|
|
@ -599,31 +530,14 @@ func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, so
|
||||||
// - signal: Signal strength measurement
|
// - signal: Signal strength measurement
|
||||||
// - timestamp: When this data was received
|
// - timestamp: When this data was received
|
||||||
func (m *Merger) updateHistories(state *AircraftState, aircraft *modes.Aircraft, sourceID string, signal float64, timestamp time.Time) {
|
func (m *Merger) updateHistories(state *AircraftState, aircraft *modes.Aircraft, sourceID string, signal float64, timestamp time.Time) {
|
||||||
// Position history with validation
|
// Position history
|
||||||
if aircraft.Latitude != 0 && aircraft.Longitude != 0 {
|
if aircraft.Latitude != 0 && aircraft.Longitude != 0 {
|
||||||
// Validate position before adding to history
|
state.PositionHistory = append(state.PositionHistory, PositionPoint{
|
||||||
validation := m.validatePosition(aircraft, state, timestamp)
|
Time: timestamp,
|
||||||
|
Latitude: aircraft.Latitude,
|
||||||
if validation.Valid {
|
Longitude: aircraft.Longitude,
|
||||||
state.PositionHistory = append(state.PositionHistory, PositionPoint{
|
Source: sourceID,
|
||||||
Time: timestamp,
|
})
|
||||||
Latitude: aircraft.Latitude,
|
|
||||||
Longitude: aircraft.Longitude,
|
|
||||||
Source: sourceID,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Log validation errors for debugging
|
|
||||||
icaoHex := fmt.Sprintf("%06X", aircraft.ICAO24)
|
|
||||||
for _, err := range validation.Errors {
|
|
||||||
log.Printf("[POSITION_VALIDATION] ICAO %s: REJECTED - %s", icaoHex, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log warnings even for valid positions
|
|
||||||
for _, warning := range validation.Warnings {
|
|
||||||
icaoHex := fmt.Sprintf("%06X", aircraft.ICAO24)
|
|
||||||
log.Printf("[POSITION_VALIDATION] ICAO %s: WARNING - %s", icaoHex, warning)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signal history
|
// Signal history
|
||||||
|
|
@ -671,10 +585,10 @@ func (m *Merger) updateHistories(state *AircraftState, aircraft *modes.Aircraft,
|
||||||
// updateUpdateRate calculates and maintains the message update rate for an aircraft.
|
// updateUpdateRate calculates and maintains the message update rate for an aircraft.
|
||||||
//
|
//
|
||||||
// The calculation:
|
// The calculation:
|
||||||
// 1. Records the timestamp of each update
|
// 1. Records the timestamp of each update
|
||||||
// 2. Maintains a sliding 30-second window of updates
|
// 2. Maintains a sliding 30-second window of updates
|
||||||
// 3. Calculates updates per second over this window
|
// 3. Calculates updates per second over this window
|
||||||
// 4. Updates the aircraft's UpdateRate field
|
// 4. Updates the aircraft's UpdateRate field
|
||||||
//
|
//
|
||||||
// This provides real-time feedback on data quality and can help identify
|
// This provides real-time feedback on data quality and can help identify
|
||||||
// aircraft that are updating frequently (close, good signal) vs infrequently
|
// aircraft that are updating frequently (close, good signal) vs infrequently
|
||||||
|
|
@ -730,10 +644,10 @@ func (m *Merger) getBestSignalSource(state *AircraftState) string {
|
||||||
// GetAircraft returns a snapshot of all current aircraft states.
|
// GetAircraft returns a snapshot of all current aircraft states.
|
||||||
//
|
//
|
||||||
// This method:
|
// This method:
|
||||||
// 1. Filters out stale aircraft (older than staleTimeout)
|
// 1. Filters out stale aircraft (older than staleTimeout)
|
||||||
// 2. Calculates current age for each aircraft
|
// 2. Calculates current age for each aircraft
|
||||||
// 3. Determines closest receiver distance and bearing
|
// 3. Determines closest receiver distance and bearing
|
||||||
// 4. Returns copies to prevent external modification
|
// 4. Returns copies to prevent external modification
|
||||||
//
|
//
|
||||||
// The returned map uses ICAO24 addresses as keys and can be safely
|
// The returned map uses ICAO24 addresses as keys and can be safely
|
||||||
// used by multiple goroutines without affecting the internal state.
|
// used by multiple goroutines without affecting the internal state.
|
||||||
|
|
@ -895,135 +809,11 @@ func calculateDistanceBearing(lat1, lon1, lat2, lon2 float64) (float64, float64)
|
||||||
return distance, bearing
|
return distance, bearing
|
||||||
}
|
}
|
||||||
|
|
||||||
// validatePosition performs comprehensive validation of aircraft position data to filter out
|
|
||||||
// obviously incorrect flight paths and implausible position updates.
|
|
||||||
//
|
|
||||||
// This function implements multiple validation checks to improve data quality:
|
|
||||||
//
|
|
||||||
// 1. **Coordinate Validation**: Ensures latitude/longitude are within Earth's bounds
|
|
||||||
// 2. **Altitude Validation**: Rejects impossible altitudes (negative or > FL600)
|
|
||||||
// 3. **Speed Validation**: Calculates implied speed and rejects >Mach 3 movements
|
|
||||||
// 4. **Distance Validation**: Rejects position jumps >500nm without time justification
|
|
||||||
// 5. **Time Validation**: Ensures timestamps are chronologically consistent
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// - aircraft: New aircraft position data to validate
|
|
||||||
// - state: Current aircraft state with position history
|
|
||||||
// - timestamp: Timestamp of the new position data
|
|
||||||
//
|
|
||||||
// Returns:
|
|
||||||
// - ValidationResult with valid flag and detailed error/warning messages
|
|
||||||
func (m *Merger) validatePosition(aircraft *modes.Aircraft, state *AircraftState, timestamp time.Time) *ValidationResult {
|
|
||||||
result := &ValidationResult{
|
|
||||||
Valid: true,
|
|
||||||
Errors: make([]string, 0),
|
|
||||||
Warnings: make([]string, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip validation if no position data
|
|
||||||
if aircraft.Latitude == 0 && aircraft.Longitude == 0 {
|
|
||||||
return result // No position to validate
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Geographic coordinate validation
|
|
||||||
if aircraft.Latitude < MinLatitude || aircraft.Latitude > MaxLatitude {
|
|
||||||
result.Valid = false
|
|
||||||
result.Errors = append(result.Errors, fmt.Sprintf("Invalid latitude: %.6f (must be between %.1f and %.1f)",
|
|
||||||
aircraft.Latitude, MinLatitude, MaxLatitude))
|
|
||||||
}
|
|
||||||
|
|
||||||
if aircraft.Longitude < MinLongitude || aircraft.Longitude > MaxLongitude {
|
|
||||||
result.Valid = false
|
|
||||||
result.Errors = append(result.Errors, fmt.Sprintf("Invalid longitude: %.6f (must be between %.1f and %.1f)",
|
|
||||||
aircraft.Longitude, MinLongitude, MaxLongitude))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Altitude validation
|
|
||||||
if aircraft.Altitude != 0 { // Only validate non-zero altitudes
|
|
||||||
if aircraft.Altitude < MinAltitudeFeet {
|
|
||||||
result.Valid = false
|
|
||||||
result.Errors = append(result.Errors, fmt.Sprintf("Impossible altitude: %d feet (below minimum %d)",
|
|
||||||
aircraft.Altitude, MinAltitudeFeet))
|
|
||||||
}
|
|
||||||
|
|
||||||
if aircraft.Altitude > MaxAltitudeFeet {
|
|
||||||
result.Valid = false
|
|
||||||
result.Errors = append(result.Errors, fmt.Sprintf("Impossible altitude: %d feet (above maximum %d)",
|
|
||||||
aircraft.Altitude, MaxAltitudeFeet))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Speed and distance validation (requires position history)
|
|
||||||
if len(state.PositionHistory) > 0 && state.Latitude != 0 && state.Longitude != 0 {
|
|
||||||
lastPos := state.PositionHistory[len(state.PositionHistory)-1]
|
|
||||||
|
|
||||||
// Calculate distance between positions
|
|
||||||
distance, _ := calculateDistanceBearing(lastPos.Latitude, lastPos.Longitude,
|
|
||||||
aircraft.Latitude, aircraft.Longitude)
|
|
||||||
|
|
||||||
// Calculate time difference
|
|
||||||
timeDiff := timestamp.Sub(lastPos.Time).Seconds()
|
|
||||||
|
|
||||||
if timeDiff > 0 {
|
|
||||||
// Calculate implied speed in knots
|
|
||||||
distanceNm := distance / NmToKm
|
|
||||||
speedKnots := (distanceNm / timeDiff) * 3600 // Convert to knots per hour
|
|
||||||
|
|
||||||
// Distance validation: reject jumps >500nm
|
|
||||||
if distanceNm > MaxDistanceNautMiles {
|
|
||||||
result.Valid = false
|
|
||||||
result.Errors = append(result.Errors, fmt.Sprintf("Impossible position jump: %.1f nm in %.1f seconds (max allowed: %.1f nm)",
|
|
||||||
distanceNm, timeDiff, MaxDistanceNautMiles))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Speed validation: reject >2000 knots (roughly Mach 3)
|
|
||||||
if speedKnots > MaxSpeedKnots {
|
|
||||||
result.Valid = false
|
|
||||||
result.Errors = append(result.Errors, fmt.Sprintf("Impossible speed: %.0f knots (max allowed: %.0f knots)",
|
|
||||||
speedKnots, MaxSpeedKnots))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warning for high but possible speeds (>800 knots)
|
|
||||||
if speedKnots > 800 && speedKnots <= MaxSpeedKnots {
|
|
||||||
result.Warnings = append(result.Warnings, fmt.Sprintf("High speed detected: %.0f knots", speedKnots))
|
|
||||||
}
|
|
||||||
} else if timeDiff < 0 {
|
|
||||||
// 4. Time validation: reject out-of-order timestamps
|
|
||||||
result.Valid = false
|
|
||||||
result.Errors = append(result.Errors, fmt.Sprintf("Out-of-order timestamp: %.1f seconds in the past", -timeDiff))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Aircraft-specific validations based on reported speed vs. position
|
|
||||||
if aircraft.GroundSpeed > 0 && len(state.PositionHistory) > 0 {
|
|
||||||
// Check if reported ground speed is consistent with position changes
|
|
||||||
lastPos := state.PositionHistory[len(state.PositionHistory)-1]
|
|
||||||
distance, _ := calculateDistanceBearing(lastPos.Latitude, lastPos.Longitude,
|
|
||||||
aircraft.Latitude, aircraft.Longitude)
|
|
||||||
timeDiff := timestamp.Sub(lastPos.Time).Seconds()
|
|
||||||
|
|
||||||
if timeDiff > 0 {
|
|
||||||
distanceNm := distance / NmToKm
|
|
||||||
impliedSpeed := (distanceNm / timeDiff) * 3600
|
|
||||||
reportedSpeed := float64(aircraft.GroundSpeed)
|
|
||||||
|
|
||||||
// Warning if speeds differ significantly (>100 knots difference)
|
|
||||||
if math.Abs(impliedSpeed-reportedSpeed) > 100 && reportedSpeed > 50 {
|
|
||||||
result.Warnings = append(result.Warnings,
|
|
||||||
fmt.Sprintf("Speed inconsistency: reported %d knots, implied %.0f knots",
|
|
||||||
aircraft.GroundSpeed, impliedSpeed))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the merger and releases resources
|
// Close closes the merger and releases resources
|
||||||
func (m *Merger) Close() error {
|
func (m *Merger) Close() error {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
if m.icaoDB != nil {
|
if m.icaoDB != nil {
|
||||||
return m.icaoDB.Close()
|
return m.icaoDB.Close()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,16 +56,16 @@ func validateModeSCRC(data []byte) bool {
|
||||||
if len(data) < 4 {
|
if len(data) < 4 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate CRC for all bytes except the last 3 (which contain the CRC)
|
// Calculate CRC for all bytes except the last 3 (which contain the CRC)
|
||||||
crc := uint32(0)
|
crc := uint32(0)
|
||||||
for i := 0; i < len(data)-3; i++ {
|
for i := 0; i < len(data)-3; i++ {
|
||||||
crc = ((crc << 8) ^ crcTable[((crc>>16)^uint32(data[i]))&0xFF]) & 0xFFFFFF
|
crc = ((crc << 8) ^ crcTable[((crc>>16)^uint32(data[i]))&0xFF]) & 0xFFFFFF
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract transmitted CRC from last 3 bytes
|
// Extract transmitted CRC from last 3 bytes
|
||||||
transmittedCRC := uint32(data[len(data)-3])<<16 | uint32(data[len(data)-2])<<8 | uint32(data[len(data)-1])
|
transmittedCRC := uint32(data[len(data)-3])<<16 | uint32(data[len(data)-2])<<8 | uint32(data[len(data)-1])
|
||||||
|
|
||||||
return crc == transmittedCRC
|
return crc == transmittedCRC
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,44 +107,37 @@ const (
|
||||||
// depending on the messages received and aircraft capabilities.
|
// depending on the messages received and aircraft capabilities.
|
||||||
type Aircraft struct {
|
type Aircraft struct {
|
||||||
// Core Identification
|
// Core Identification
|
||||||
ICAO24 uint32 // 24-bit ICAO aircraft address (unique identifier)
|
ICAO24 uint32 // 24-bit ICAO aircraft address (unique identifier)
|
||||||
Callsign string // 8-character flight callsign (from identification messages)
|
Callsign string // 8-character flight callsign (from identification messages)
|
||||||
|
|
||||||
// Position and Navigation
|
// Position and Navigation
|
||||||
Latitude float64 // Position latitude in decimal degrees
|
Latitude float64 // Position latitude in decimal degrees
|
||||||
Longitude float64 // Position longitude in decimal degrees
|
Longitude float64 // Position longitude in decimal degrees
|
||||||
Altitude int // Altitude in feet (barometric or geometric)
|
Altitude int // Altitude in feet (barometric or geometric)
|
||||||
BaroAltitude int // Barometric altitude in feet (QNH corrected)
|
BaroAltitude int // Barometric altitude in feet (QNH corrected)
|
||||||
GeomAltitude int // Geometric altitude in feet (GNSS height)
|
GeomAltitude int // Geometric altitude in feet (GNSS height)
|
||||||
|
|
||||||
// Motion and Dynamics
|
// Motion and Dynamics
|
||||||
VerticalRate int // Vertical rate in feet per minute (climb/descent)
|
VerticalRate int // Vertical rate in feet per minute (climb/descent)
|
||||||
GroundSpeed int // Ground speed in knots (integer)
|
GroundSpeed int // Ground speed in knots (integer)
|
||||||
Track int // Track angle in degrees (0-359, integer)
|
Track int // Track angle in degrees (0-359, integer)
|
||||||
Heading int // Aircraft heading in degrees (magnetic, integer)
|
Heading int // Aircraft heading in degrees (magnetic, integer)
|
||||||
|
|
||||||
// Aircraft Information
|
// Aircraft Information
|
||||||
Category string // Aircraft category (size, type, performance)
|
Category string // Aircraft category (size, type, performance)
|
||||||
Squawk string // 4-digit transponder squawk code (octal)
|
Squawk string // 4-digit transponder squawk code (octal)
|
||||||
|
|
||||||
// Status and Alerts
|
// Status and Alerts
|
||||||
Emergency string // Emergency/priority status description
|
Emergency string // Emergency/priority status description
|
||||||
OnGround bool // Aircraft is on ground (surface movement)
|
OnGround bool // Aircraft is on ground (surface movement)
|
||||||
Alert bool // Alert flag (ATC attention required)
|
Alert bool // Alert flag (ATC attention required)
|
||||||
SPI bool // Special Position Identification (pilot activated)
|
SPI bool // Special Position Identification (pilot activated)
|
||||||
|
|
||||||
// Data Quality Indicators
|
// Data Quality Indicators
|
||||||
NACp uint8 // Navigation Accuracy Category - Position (0-11)
|
NACp uint8 // Navigation Accuracy Category - Position (0-11)
|
||||||
NACv uint8 // Navigation Accuracy Category - Velocity (0-4)
|
NACv uint8 // Navigation Accuracy Category - Velocity (0-4)
|
||||||
SIL uint8 // Surveillance Integrity Level (0-3)
|
SIL uint8 // Surveillance Integrity Level (0-3)
|
||||||
|
|
||||||
// Transponder Information
|
|
||||||
TransponderCapability string // Transponder capability level (from DF11 messages)
|
|
||||||
TransponderLevel uint8 // Transponder level (0-7 from capability field)
|
|
||||||
|
|
||||||
// Combined Data Quality Assessment
|
|
||||||
SignalQuality string // Combined assessment of position/velocity accuracy and integrity
|
|
||||||
|
|
||||||
// Autopilot/Flight Management
|
// Autopilot/Flight Management
|
||||||
SelectedAltitude int // MCP/FCU selected altitude in feet
|
SelectedAltitude int // MCP/FCU selected altitude in feet
|
||||||
SelectedHeading float64 // MCP/FCU selected heading in degrees
|
SelectedHeading float64 // MCP/FCU selected heading in degrees
|
||||||
|
|
@ -170,11 +163,11 @@ type Decoder struct {
|
||||||
cprOddLon map[uint32]float64 // Odd message longitude encoding (ICAO24 -> normalized lon)
|
cprOddLon map[uint32]float64 // Odd message longitude encoding (ICAO24 -> normalized lon)
|
||||||
cprEvenTime map[uint32]int64 // Timestamp of even message (for freshness comparison)
|
cprEvenTime map[uint32]int64 // Timestamp of even message (for freshness comparison)
|
||||||
cprOddTime map[uint32]int64 // Timestamp of odd message (for freshness comparison)
|
cprOddTime map[uint32]int64 // Timestamp of odd message (for freshness comparison)
|
||||||
|
|
||||||
// Reference position for CPR zone ambiguity resolution (receiver location)
|
// Reference position for CPR zone ambiguity resolution (receiver location)
|
||||||
refLatitude float64 // Receiver latitude in decimal degrees
|
refLatitude float64 // Receiver latitude in decimal degrees
|
||||||
refLongitude float64 // Receiver longitude in decimal degrees
|
refLongitude float64 // Receiver longitude in decimal degrees
|
||||||
|
|
||||||
// Mutex to protect concurrent access to CPR maps
|
// Mutex to protect concurrent access to CPR maps
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
@ -206,10 +199,10 @@ func NewDecoder(refLat, refLon float64) *Decoder {
|
||||||
// Decode processes a Mode S message and extracts all available aircraft information.
|
// Decode processes a Mode S message and extracts all available aircraft information.
|
||||||
//
|
//
|
||||||
// This is the main entry point for message decoding. The method:
|
// This is the main entry point for message decoding. The method:
|
||||||
// 1. Validates message length and extracts the Downlink Format (DF)
|
// 1. Validates message length and extracts the Downlink Format (DF)
|
||||||
// 2. Extracts the ICAO24 aircraft address
|
// 2. Extracts the ICAO24 aircraft address
|
||||||
// 3. Routes to appropriate decoder based on message type
|
// 3. Routes to appropriate decoder based on message type
|
||||||
// 4. Returns populated Aircraft struct with available data
|
// 4. Returns populated Aircraft struct with available data
|
||||||
//
|
//
|
||||||
// Different message types provide different information:
|
// Different message types provide different information:
|
||||||
// - DF4/20: Altitude only
|
// - DF4/20: Altitude only
|
||||||
|
|
@ -238,32 +231,14 @@ func (d *Decoder) Decode(data []byte) (*Aircraft, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch df {
|
switch df {
|
||||||
case DF0:
|
|
||||||
// Short Air-Air Surveillance (ACAS)
|
|
||||||
aircraft.Altitude = d.decodeAltitude(data)
|
|
||||||
case DF4, DF20:
|
case DF4, DF20:
|
||||||
aircraft.Altitude = d.decodeAltitude(data)
|
aircraft.Altitude = d.decodeAltitude(data)
|
||||||
case DF5, DF21:
|
case DF5, DF21:
|
||||||
aircraft.Squawk = d.decodeSquawk(data)
|
aircraft.Squawk = d.decodeSquawk(data)
|
||||||
case DF11:
|
|
||||||
// All-Call Reply - extract capability and interrogator identifier
|
|
||||||
d.decodeAllCallReply(data, aircraft)
|
|
||||||
case DF16:
|
|
||||||
// Long Air-Air Surveillance (ACAS with altitude)
|
|
||||||
aircraft.Altitude = d.decodeAltitude(data)
|
|
||||||
case DF17, DF18:
|
case DF17, DF18:
|
||||||
return d.decodeExtendedSquitter(data, aircraft)
|
return d.decodeExtendedSquitter(data, aircraft)
|
||||||
case DF19:
|
|
||||||
// Military Extended Squitter - similar to DF17/18 but with military codes
|
|
||||||
return d.decodeMilitaryExtendedSquitter(data, aircraft)
|
|
||||||
case DF24:
|
|
||||||
// Comm-D Enhanced Length Message - variable length data
|
|
||||||
d.decodeCommD(data, aircraft)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always try to calculate signal quality at the end of decoding
|
|
||||||
d.calculateSignalQuality(aircraft)
|
|
||||||
|
|
||||||
return aircraft, nil
|
return aircraft, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -336,12 +311,6 @@ func (d *Decoder) decodeExtendedSquitter(data []byte, aircraft *Aircraft) (*Airc
|
||||||
d.decodeOperationalStatus(data, aircraft)
|
d.decodeOperationalStatus(data, aircraft)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set baseline signal quality for ADS-B extended squitter
|
|
||||||
aircraft.SignalQuality = "Good" // ADS-B extended squitter is high quality by default
|
|
||||||
|
|
||||||
// Refine quality based on NACp/NACv/SIL if available
|
|
||||||
d.calculateSignalQuality(aircraft)
|
|
||||||
|
|
||||||
return aircraft, nil
|
return aircraft, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -400,10 +369,10 @@ func (d *Decoder) decodeIdentification(data []byte, aircraft *Aircraft) {
|
||||||
// - Even/odd flag for CPR decoding
|
// - Even/odd flag for CPR decoding
|
||||||
//
|
//
|
||||||
// CPR (Compact Position Reporting) Process:
|
// CPR (Compact Position Reporting) Process:
|
||||||
// 1. Extract the even/odd flag and CPR lat/lon values
|
// 1. Extract the even/odd flag and CPR lat/lon values
|
||||||
// 2. Normalize CPR values to 0-1 range (divide by 2^17)
|
// 2. Normalize CPR values to 0-1 range (divide by 2^17)
|
||||||
// 3. Store values for this aircraft's ICAO address
|
// 3. Store values for this aircraft's ICAO address
|
||||||
// 4. Attempt position decoding if both even and odd messages are available
|
// 4. Attempt position decoding if both even and odd messages are available
|
||||||
//
|
//
|
||||||
// The actual position calculation requires both even and odd messages to
|
// The actual position calculation requires both even and odd messages to
|
||||||
// resolve the ambiguity inherent in the compressed encoding format.
|
// resolve the ambiguity inherent in the compressed encoding format.
|
||||||
|
|
@ -434,20 +403,8 @@ func (d *Decoder) decodeAirbornePosition(data []byte, aircraft *Aircraft) {
|
||||||
}
|
}
|
||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
|
|
||||||
// Extract NACp (Navigation Accuracy Category for Position) from position messages
|
|
||||||
// NACp is embedded in airborne position messages in bits 50-53 (data[6] bits 1-4)
|
|
||||||
if tc >= 9 && tc <= 18 {
|
|
||||||
// For airborne position messages TC 9-18, NACp is encoded in the message
|
|
||||||
aircraft.NACp = uint8(tc - 8) // TC 9->NACp 1, TC 10->NACp 2, etc.
|
|
||||||
// Note: This is a simplified mapping. Real NACp extraction is more complex
|
|
||||||
// but this provides useful position accuracy indication
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to decode position if we have both even and odd messages
|
// Try to decode position if we have both even and odd messages
|
||||||
d.decodeCPRPosition(aircraft)
|
d.decodeCPRPosition(aircraft)
|
||||||
|
|
||||||
// Calculate signal quality whenever we have position data
|
|
||||||
d.calculateSignalQuality(aircraft)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// decodeCPRPosition performs CPR (Compact Position Reporting) global position decoding.
|
// decodeCPRPosition performs CPR (Compact Position Reporting) global position decoding.
|
||||||
|
|
@ -499,7 +456,7 @@ func (d *Decoder) decodeCPRPosition(aircraft *Aircraft) {
|
||||||
} else if latEven < -90 {
|
} else if latEven < -90 {
|
||||||
latEven = -180 - latEven
|
latEven = -180 - latEven
|
||||||
}
|
}
|
||||||
|
|
||||||
if latOdd > 90 {
|
if latOdd > 90 {
|
||||||
latOdd = 180 - latOdd
|
latOdd = 180 - latOdd
|
||||||
} else if latOdd < -90 {
|
} else if latOdd < -90 {
|
||||||
|
|
@ -516,7 +473,7 @@ func (d *Decoder) decodeCPRPosition(aircraft *Aircraft) {
|
||||||
// Calculate which decoded latitude is closer to the receiver
|
// Calculate which decoded latitude is closer to the receiver
|
||||||
distToEven := math.Abs(latEven - d.refLatitude)
|
distToEven := math.Abs(latEven - d.refLatitude)
|
||||||
distToOdd := math.Abs(latOdd - d.refLatitude)
|
distToOdd := math.Abs(latOdd - d.refLatitude)
|
||||||
|
|
||||||
// Choose the latitude solution that's closer to the receiver position
|
// Choose the latitude solution that's closer to the receiver position
|
||||||
if distToOdd < distToEven {
|
if distToOdd < distToEven {
|
||||||
aircraft.Latitude = latOdd
|
aircraft.Latitude = latOdd
|
||||||
|
|
@ -544,7 +501,7 @@ func (d *Decoder) decodeCPRPosition(aircraft *Aircraft) {
|
||||||
}
|
}
|
||||||
|
|
||||||
aircraft.Longitude = lon
|
aircraft.Longitude = lon
|
||||||
|
|
||||||
// CPR decoding completed successfully
|
// CPR decoding completed successfully
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -619,13 +576,13 @@ func (d *Decoder) decodeVelocity(data []byte, aircraft *Aircraft) {
|
||||||
|
|
||||||
// Calculate ground speed in knots (rounded to integer)
|
// Calculate ground speed in knots (rounded to integer)
|
||||||
speedKnots := math.Sqrt(ewVel*ewVel + nsVel*nsVel)
|
speedKnots := math.Sqrt(ewVel*ewVel + nsVel*nsVel)
|
||||||
|
|
||||||
// Validate speed range (0-600 knots for civilian aircraft)
|
// Validate speed range (0-600 knots for civilian aircraft)
|
||||||
if speedKnots > 600 {
|
if speedKnots > 600 {
|
||||||
speedKnots = 600 // Cap at reasonable maximum
|
speedKnots = 600 // Cap at reasonable maximum
|
||||||
}
|
}
|
||||||
aircraft.GroundSpeed = int(math.Round(speedKnots))
|
aircraft.GroundSpeed = int(math.Round(speedKnots))
|
||||||
|
|
||||||
// Calculate track in degrees (0-359)
|
// Calculate track in degrees (0-359)
|
||||||
trackDeg := math.Atan2(ewVel, nsVel) * 180 / math.Pi
|
trackDeg := math.Atan2(ewVel, nsVel) * 180 / math.Pi
|
||||||
if trackDeg < 0 {
|
if trackDeg < 0 {
|
||||||
|
|
@ -684,20 +641,20 @@ func (d *Decoder) decodeAltitudeBits(altCode uint16, tc uint8) int {
|
||||||
// Standard altitude encoding with 25 ft increments
|
// Standard altitude encoding with 25 ft increments
|
||||||
// Check Q-bit (bit 4) for encoding type
|
// Check Q-bit (bit 4) for encoding type
|
||||||
qBit := (altCode >> 4) & 1
|
qBit := (altCode >> 4) & 1
|
||||||
|
|
||||||
if qBit == 1 {
|
if qBit == 1 {
|
||||||
// Standard altitude with Q-bit set
|
// Standard altitude with Q-bit set
|
||||||
// Remove Q-bit and reassemble 11-bit altitude code
|
// Remove Q-bit and reassemble 11-bit altitude code
|
||||||
n := ((altCode & 0x1F80) >> 2) | ((altCode & 0x0020) >> 1) | (altCode & 0x000F)
|
n := ((altCode & 0x1F80) >> 2) | ((altCode & 0x0020) >> 1) | (altCode & 0x000F)
|
||||||
alt := int(n)*25 - 1000
|
alt := int(n)*25 - 1000
|
||||||
|
|
||||||
// Validate altitude range
|
// Validate altitude range
|
||||||
if alt < -1000 || alt > 60000 {
|
if alt < -1000 || alt > 60000 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return alt
|
return alt
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gray code altitude (100 ft increments) - legacy encoding
|
// Gray code altitude (100 ft increments) - legacy encoding
|
||||||
// Convert from Gray code to binary
|
// Convert from Gray code to binary
|
||||||
n := altCode
|
n := altCode
|
||||||
|
|
@ -705,7 +662,7 @@ func (d *Decoder) decodeAltitudeBits(altCode uint16, tc uint8) int {
|
||||||
n ^= n >> 4
|
n ^= n >> 4
|
||||||
n ^= n >> 2
|
n ^= n >> 2
|
||||||
n ^= n >> 1
|
n ^= n >> 1
|
||||||
|
|
||||||
// Convert to altitude in feet
|
// Convert to altitude in feet
|
||||||
alt := int(n&0x7FF) * 100
|
alt := int(n&0x7FF) * 100
|
||||||
if alt < 0 || alt > 60000 {
|
if alt < 0 || alt > 60000 {
|
||||||
|
|
@ -878,7 +835,7 @@ func (d *Decoder) decodeTargetState(data []byte, aircraft *Aircraft) {
|
||||||
//
|
//
|
||||||
// Operational status messages (TC 31) contain:
|
// Operational status messages (TC 31) contain:
|
||||||
// - Navigation Accuracy Category for Position (NACp): Position accuracy
|
// - Navigation Accuracy Category for Position (NACp): Position accuracy
|
||||||
// - Navigation Accuracy Category for Velocity (NACv): Velocity accuracy
|
// - Navigation Accuracy Category for Velocity (NACv): Velocity accuracy
|
||||||
// - Surveillance Integrity Level (SIL): System integrity confidence
|
// - Surveillance Integrity Level (SIL): System integrity confidence
|
||||||
//
|
//
|
||||||
// These parameters help receiving systems assess data quality and determine
|
// These parameters help receiving systems assess data quality and determine
|
||||||
|
|
@ -892,9 +849,6 @@ func (d *Decoder) decodeOperationalStatus(data []byte, aircraft *Aircraft) {
|
||||||
aircraft.NACp = (data[7] >> 4) & 0x0F
|
aircraft.NACp = (data[7] >> 4) & 0x0F
|
||||||
aircraft.NACv = data[7] & 0x0F
|
aircraft.NACv = data[7] & 0x0F
|
||||||
aircraft.SIL = (data[8] >> 6) & 0x03
|
aircraft.SIL = (data[8] >> 6) & 0x03
|
||||||
|
|
||||||
// Calculate combined signal quality from NACp, NACv, and SIL
|
|
||||||
d.calculateSignalQuality(aircraft)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// decodeSurfacePosition extracts position and movement data for aircraft on the ground.
|
// decodeSurfacePosition extracts position and movement data for aircraft on the ground.
|
||||||
|
|
@ -986,164 +940,3 @@ func (d *Decoder) decodeGroundSpeed(movement uint8) float64 {
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// decodeAllCallReply extracts capability and interrogator identifier from DF11 messages.
|
|
||||||
//
|
|
||||||
// DF11 All-Call Reply messages contain:
|
|
||||||
// - Capability (CA) field (3 bits): transponder capabilities and modes
|
|
||||||
// - Interrogator Identifier (II) field (4 bits): which radar interrogated
|
|
||||||
// - ICAO24 address (24 bits): aircraft identifier
|
|
||||||
//
|
|
||||||
// The capability field indicates transponder features and operational modes:
|
|
||||||
// - 0: Level 1 transponder
|
|
||||||
// - 1: Level 2 transponder
|
|
||||||
// - 2: Level 2+ transponder with additional capabilities
|
|
||||||
// - 3: Level 2+ transponder with enhanced surveillance
|
|
||||||
// - 4: Level 2+ transponder with enhanced surveillance and extended squitter
|
|
||||||
// - 5: Level 2+ transponder with enhanced surveillance, extended squitter, and enhanced surveillance capability
|
|
||||||
// - 6: Level 2+ transponder with enhanced surveillance, extended squitter, and enhanced surveillance capability
|
|
||||||
// - 7: Level 2+ transponder, downlink request value is 0, or the flight status is alert, SPI, or emergency
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// - data: 7-byte DF11 message
|
|
||||||
// - aircraft: Aircraft struct to populate
|
|
||||||
func (d *Decoder) decodeAllCallReply(data []byte, aircraft *Aircraft) {
|
|
||||||
if len(data) < 7 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract Capability (CA) - bits 6-8 of first byte
|
|
||||||
capability := (data[0] >> 0) & 0x07
|
|
||||||
|
|
||||||
// Extract Interrogator Identifier (II) - would be in control field if present
|
|
||||||
// For DF11, this information is typically implied by the interrogating radar
|
|
||||||
|
|
||||||
// Store transponder capability information in dedicated fields
|
|
||||||
aircraft.TransponderLevel = capability
|
|
||||||
switch capability {
|
|
||||||
case 0:
|
|
||||||
aircraft.TransponderCapability = "Level 1"
|
|
||||||
case 1:
|
|
||||||
aircraft.TransponderCapability = "Level 2"
|
|
||||||
case 2, 3:
|
|
||||||
aircraft.TransponderCapability = "Level 2+"
|
|
||||||
case 4, 5, 6:
|
|
||||||
aircraft.TransponderCapability = "Enhanced"
|
|
||||||
case 7:
|
|
||||||
aircraft.TransponderCapability = "Alert/Emergency"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// decodeMilitaryExtendedSquitter processes DF19 military extended squitter messages.
|
|
||||||
//
|
|
||||||
// DF19 messages have the same structure as DF17/18 ADS-B extended squitter but
|
|
||||||
// may contain military-specific type codes or enhanced data formats.
|
|
||||||
// This implementation treats them similarly to civilian extended squitter
|
|
||||||
// but could be extended for military-specific capabilities.
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// - data: 14-byte DF19 message
|
|
||||||
// - aircraft: Aircraft struct to populate
|
|
||||||
//
|
|
||||||
// Returns updated Aircraft struct or error for malformed messages.
|
|
||||||
func (d *Decoder) decodeMilitaryExtendedSquitter(data []byte, aircraft *Aircraft) (*Aircraft, error) {
|
|
||||||
if len(data) != 14 {
|
|
||||||
return nil, fmt.Errorf("invalid military extended squitter length: %d bytes", len(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
// For now, treat military extended squitter similar to civilian
|
|
||||||
// Could be enhanced to handle military-specific type codes
|
|
||||||
return d.decodeExtendedSquitter(data, aircraft)
|
|
||||||
}
|
|
||||||
|
|
||||||
// decodeCommD extracts data from DF24 Comm-D Enhanced Length Messages.
|
|
||||||
//
|
|
||||||
// DF24 messages are variable-length data link communications that can contain:
|
|
||||||
// - Weather information and updates
|
|
||||||
// - Flight plan modifications
|
|
||||||
// - Controller-pilot data link messages
|
|
||||||
// - Air traffic management information
|
|
||||||
// - Future air navigation system data
|
|
||||||
//
|
|
||||||
// Due to the complexity and variety of DF24 message content, this implementation
|
|
||||||
// provides basic structure extraction. Full decoding would require extensive
|
|
||||||
// knowledge of specific data link protocols and message formats.
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// - data: Variable-length DF24 message (minimum 7 bytes)
|
|
||||||
// - aircraft: Aircraft struct to populate
|
|
||||||
func (d *Decoder) decodeCommD(data []byte, aircraft *Aircraft) {
|
|
||||||
if len(data) < 7 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// DF24 messages contain variable data that would require protocol-specific decoding
|
|
||||||
// For now, we note that this is a data communication message but don't overwrite aircraft category
|
|
||||||
// Could set a separate field for message type if needed in the future
|
|
||||||
|
|
||||||
// The actual message content would require:
|
|
||||||
// - Protocol identifier extraction
|
|
||||||
// - Message type determination
|
|
||||||
// - Format-specific field extraction
|
|
||||||
// - Possible message reassembly for multi-part messages
|
|
||||||
//
|
|
||||||
// This could be extended based on specific requirements and available documentation
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculateSignalQuality combines NACp, NACv, and SIL into an overall data quality assessment.
|
|
||||||
//
|
|
||||||
// This function provides a human-readable quality indicator that considers:
|
|
||||||
// - Position accuracy (NACp): How precise the aircraft's position data is
|
|
||||||
// - Velocity accuracy (NACv): How precise the speed/heading data is
|
|
||||||
// - Surveillance integrity (SIL): How reliable/trustworthy the data is
|
|
||||||
//
|
|
||||||
// The algorithm prioritizes integrity first (SIL), then position accuracy (NACp),
|
|
||||||
// then velocity accuracy (NACv) to provide a meaningful overall assessment.
|
|
||||||
//
|
|
||||||
// Quality levels:
|
|
||||||
// - "Excellent": High integrity with very precise position/velocity
|
|
||||||
// - "Good": Good integrity with reasonable precision
|
|
||||||
// - "Fair": Moderate quality suitable for tracking
|
|
||||||
// - "Poor": Low quality but still usable
|
|
||||||
// - "Unknown": No quality indicators available
|
|
||||||
//
|
|
||||||
// Parameters:
|
|
||||||
// - aircraft: Aircraft struct containing NACp, NACv, and SIL values
|
|
||||||
func (d *Decoder) calculateSignalQuality(aircraft *Aircraft) {
|
|
||||||
nacp := aircraft.NACp
|
|
||||||
nacv := aircraft.NACv
|
|
||||||
sil := aircraft.SIL
|
|
||||||
|
|
||||||
// If no quality indicators are available, don't set anything
|
|
||||||
if nacp == 0 && nacv == 0 && sil == 0 {
|
|
||||||
// Don't overwrite existing quality assessment
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Excellent: High integrity with high accuracy OR very high accuracy alone
|
|
||||||
if (sil >= 2 && nacp >= 9) || nacp >= 10 {
|
|
||||||
aircraft.SignalQuality = "Excellent"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Good: Good integrity with moderate accuracy OR high accuracy alone
|
|
||||||
if (sil >= 2 && nacp >= 6) || (sil >= 1 && nacp >= 9) || nacp >= 8 {
|
|
||||||
aircraft.SignalQuality = "Good"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fair: Some integrity with basic accuracy OR moderate accuracy alone
|
|
||||||
if (sil >= 1 && nacp >= 3) || nacp >= 5 {
|
|
||||||
aircraft.SignalQuality = "Fair"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Poor: Low but usable quality indicators
|
|
||||||
if sil > 0 || nacp >= 1 || nacv > 0 {
|
|
||||||
aircraft.SignalQuality = "Poor"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default fallback
|
|
||||||
aircraft.SignalQuality = ""
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -36,8 +35,8 @@ import (
|
||||||
// This is used as the center point for the web map interface and for
|
// This is used as the center point for the web map interface and for
|
||||||
// distance calculations in coverage analysis.
|
// distance calculations in coverage analysis.
|
||||||
type OriginConfig struct {
|
type OriginConfig struct {
|
||||||
Latitude float64 `json:"latitude"` // Reference latitude in decimal degrees
|
Latitude float64 `json:"latitude"` // Reference latitude in decimal degrees
|
||||||
Longitude float64 `json:"longitude"` // Reference longitude in decimal degrees
|
Longitude float64 `json:"longitude"` // Reference longitude in decimal degrees
|
||||||
Name string `json:"name,omitempty"` // Descriptive name for the origin point
|
Name string `json:"name,omitempty"` // Descriptive name for the origin point
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,12 +51,11 @@ type OriginConfig struct {
|
||||||
// - Concurrent broadcast system for WebSocket clients
|
// - Concurrent broadcast system for WebSocket clients
|
||||||
// - CORS support for cross-origin web applications
|
// - CORS support for cross-origin web applications
|
||||||
type Server struct {
|
type Server struct {
|
||||||
host string // Bind address for HTTP server
|
port int // TCP port for HTTP server
|
||||||
port int // TCP port for HTTP server
|
merger *merger.Merger // Data source for aircraft information
|
||||||
merger *merger.Merger // Data source for aircraft information
|
staticFiles embed.FS // Embedded static web assets
|
||||||
staticFiles embed.FS // Embedded static web assets
|
server *http.Server // HTTP server instance
|
||||||
server *http.Server // HTTP server instance
|
origin OriginConfig // Geographic reference point
|
||||||
origin OriginConfig // Geographic reference point
|
|
||||||
|
|
||||||
// WebSocket management
|
// WebSocket management
|
||||||
wsClients map[*websocket.Conn]bool // Active WebSocket client connections
|
wsClients map[*websocket.Conn]bool // Active WebSocket client connections
|
||||||
|
|
@ -65,8 +63,8 @@ type Server struct {
|
||||||
upgrader websocket.Upgrader // HTTP to WebSocket protocol upgrader
|
upgrader websocket.Upgrader // HTTP to WebSocket protocol upgrader
|
||||||
|
|
||||||
// Broadcast channels for real-time updates
|
// Broadcast channels for real-time updates
|
||||||
broadcastChan chan []byte // Channel for broadcasting updates to all clients
|
broadcastChan chan []byte // Channel for broadcasting updates to all clients
|
||||||
stopChan chan struct{} // Shutdown signal channel
|
stopChan chan struct{} // Shutdown signal channel
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebSocketMessage represents the standard message format for WebSocket communication.
|
// WebSocketMessage represents the standard message format for WebSocket communication.
|
||||||
|
|
@ -87,7 +85,7 @@ type AircraftUpdate struct {
|
||||||
Stats map[string]interface{} `json:"stats"` // System statistics and metrics
|
Stats map[string]interface{} `json:"stats"` // System statistics and metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWebServer creates a new HTTP server instance for serving the SkyView web interface.
|
// NewServer creates a new HTTP server instance for serving the SkyView web interface.
|
||||||
//
|
//
|
||||||
// The server is configured with:
|
// The server is configured with:
|
||||||
// - WebSocket upgrader allowing all origins (suitable for development)
|
// - WebSocket upgrader allowing all origins (suitable for development)
|
||||||
|
|
@ -95,16 +93,14 @@ type AircraftUpdate struct {
|
||||||
// - Read/Write buffers optimized for aircraft data messages
|
// - Read/Write buffers optimized for aircraft data messages
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - host: Bind address (empty for all interfaces, "localhost" for local only)
|
|
||||||
// - port: TCP port number for the HTTP server
|
// - port: TCP port number for the HTTP server
|
||||||
// - merger: Data merger instance providing aircraft information
|
// - merger: Data merger instance providing aircraft information
|
||||||
// - staticFiles: Embedded filesystem containing web assets
|
// - staticFiles: Embedded filesystem containing web assets
|
||||||
// - origin: Geographic reference point for the map interface
|
// - origin: Geographic reference point for the map interface
|
||||||
//
|
//
|
||||||
// Returns a configured but not yet started server instance.
|
// Returns a configured but not yet started server instance.
|
||||||
func NewWebServer(host string, port int, merger *merger.Merger, staticFiles embed.FS, origin OriginConfig) *Server {
|
func NewServer(port int, merger *merger.Merger, staticFiles embed.FS, origin OriginConfig) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
host: host,
|
|
||||||
port: port,
|
port: port,
|
||||||
merger: merger,
|
merger: merger,
|
||||||
staticFiles: staticFiles,
|
staticFiles: staticFiles,
|
||||||
|
|
@ -125,9 +121,9 @@ func NewWebServer(host string, port int, merger *merger.Merger, staticFiles embe
|
||||||
// Start begins serving HTTP requests and WebSocket connections.
|
// Start begins serving HTTP requests and WebSocket connections.
|
||||||
//
|
//
|
||||||
// This method starts several background routines:
|
// This method starts several background routines:
|
||||||
// 1. Broadcast routine - handles WebSocket message distribution
|
// 1. Broadcast routine - handles WebSocket message distribution
|
||||||
// 2. Periodic update routine - sends regular updates to WebSocket clients
|
// 2. Periodic update routine - sends regular updates to WebSocket clients
|
||||||
// 3. HTTP server - serves API endpoints and static files
|
// 3. HTTP server - serves API endpoints and static files
|
||||||
//
|
//
|
||||||
// The method blocks until the server encounters an error or is shut down.
|
// The method blocks until the server encounters an error or is shut down.
|
||||||
// Use Stop() for graceful shutdown.
|
// Use Stop() for graceful shutdown.
|
||||||
|
|
@ -143,15 +139,8 @@ func (s *Server) Start() error {
|
||||||
// Setup routes
|
// Setup routes
|
||||||
router := s.setupRoutes()
|
router := s.setupRoutes()
|
||||||
|
|
||||||
// Format address correctly for IPv6
|
|
||||||
addr := fmt.Sprintf("%s:%d", s.host, s.port)
|
|
||||||
if strings.Contains(s.host, ":") {
|
|
||||||
// IPv6 address needs brackets
|
|
||||||
addr = fmt.Sprintf("[%s]:%d", s.host, s.port)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.server = &http.Server{
|
s.server = &http.Server{
|
||||||
Addr: addr,
|
Addr: fmt.Sprintf(":%d", s.port),
|
||||||
Handler: router,
|
Handler: router,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,9 +150,9 @@ func (s *Server) Start() error {
|
||||||
// Stop gracefully shuts down the server and all background routines.
|
// Stop gracefully shuts down the server and all background routines.
|
||||||
//
|
//
|
||||||
// This method:
|
// This method:
|
||||||
// 1. Signals all background routines to stop via stopChan
|
// 1. Signals all background routines to stop via stopChan
|
||||||
// 2. Shuts down the HTTP server with a 5-second timeout
|
// 2. Shuts down the HTTP server with a 5-second timeout
|
||||||
// 3. Closes WebSocket connections
|
// 3. Closes WebSocket connections
|
||||||
//
|
//
|
||||||
// The shutdown is designed to be safe and allow in-flight requests to complete.
|
// The shutdown is designed to be safe and allow in-flight requests to complete.
|
||||||
func (s *Server) Stop() {
|
func (s *Server) Stop() {
|
||||||
|
|
@ -217,13 +206,13 @@ func (s *Server) setupRoutes() http.Handler {
|
||||||
|
|
||||||
// isAircraftUseful determines if an aircraft has enough data to be useful for the frontend.
|
// isAircraftUseful determines if an aircraft has enough data to be useful for the frontend.
|
||||||
//
|
//
|
||||||
// DESIGN NOTE: We WANT reasonable aircraft to appear in our table view, even if they
|
// DESIGN NOTE: We WANT reasonable aircraft to appear in our table view, even if they
|
||||||
// don't have enough data to appear on the map. This provides users visibility into
|
// don't have enough data to appear on the map. This provides users visibility into
|
||||||
// all tracked aircraft, not just those with complete position data.
|
// all tracked aircraft, not just those with complete position data.
|
||||||
//
|
//
|
||||||
// Aircraft are considered useful if they have ANY of:
|
// Aircraft are considered useful if they have ANY of:
|
||||||
// - Valid position data (both latitude and longitude non-zero) -> Can show on map
|
// - Valid position data (both latitude and longitude non-zero) -> Can show on map
|
||||||
// - Callsign (flight identification) -> Can show in table with "No position" status
|
// - Callsign (flight identification) -> Can show in table with "No position" status
|
||||||
// - Altitude information -> Can show in table as "Aircraft at X feet"
|
// - Altitude information -> Can show in table as "Aircraft at X feet"
|
||||||
// - Any other identifying information that makes it a "real" aircraft
|
// - Any other identifying information that makes it a "real" aircraft
|
||||||
//
|
//
|
||||||
|
|
@ -235,7 +224,7 @@ func (s *Server) isAircraftUseful(aircraft *merger.AircraftState) bool {
|
||||||
hasCallsign := aircraft.Callsign != ""
|
hasCallsign := aircraft.Callsign != ""
|
||||||
hasAltitude := aircraft.Altitude != 0
|
hasAltitude := aircraft.Altitude != 0
|
||||||
hasSquawk := aircraft.Squawk != ""
|
hasSquawk := aircraft.Squawk != ""
|
||||||
|
|
||||||
// Include aircraft with any identifying or operational data
|
// Include aircraft with any identifying or operational data
|
||||||
return hasValidPosition || hasCallsign || hasAltitude || hasSquawk
|
return hasValidPosition || hasCallsign || hasAltitude || hasSquawk
|
||||||
}
|
}
|
||||||
|
|
@ -393,10 +382,10 @@ func (s *Server) handleGetCoverage(w http.ResponseWriter, r *http.Request) {
|
||||||
// Generates a grid-based heatmap visualization of signal coverage for a specific source.
|
// Generates a grid-based heatmap visualization of signal coverage for a specific source.
|
||||||
//
|
//
|
||||||
// The heatmap is computed by:
|
// The heatmap is computed by:
|
||||||
// 1. Finding geographic bounds of all aircraft positions for the source
|
// 1. Finding geographic bounds of all aircraft positions for the source
|
||||||
// 2. Creating a 100x100 grid covering the bounds
|
// 2. Creating a 100x100 grid covering the bounds
|
||||||
// 3. Accumulating signal strength values in each grid cell
|
// 3. Accumulating signal strength values in each grid cell
|
||||||
// 4. Returning the grid data with boundary coordinates
|
// 4. Returning the grid data with boundary coordinates
|
||||||
//
|
//
|
||||||
// This provides a density-based visualization of where the source receives
|
// This provides a density-based visualization of where the source receives
|
||||||
// the strongest signals, useful for coverage analysis and antenna optimization.
|
// the strongest signals, useful for coverage analysis and antenna optimization.
|
||||||
|
|
@ -467,11 +456,11 @@ func (s *Server) handleGetHeatmap(w http.ResponseWriter, r *http.Request) {
|
||||||
// handleWebSocket manages WebSocket connections for real-time aircraft data streaming.
|
// handleWebSocket manages WebSocket connections for real-time aircraft data streaming.
|
||||||
//
|
//
|
||||||
// This handler:
|
// This handler:
|
||||||
// 1. Upgrades the HTTP connection to WebSocket protocol
|
// 1. Upgrades the HTTP connection to WebSocket protocol
|
||||||
// 2. Registers the client for broadcast updates
|
// 2. Registers the client for broadcast updates
|
||||||
// 3. Sends initial data snapshot to the client
|
// 3. Sends initial data snapshot to the client
|
||||||
// 4. Handles client messages (currently just ping/pong for keepalive)
|
// 4. Handles client messages (currently just ping/pong for keepalive)
|
||||||
// 5. Cleans up the connection when the client disconnects
|
// 5. Cleans up the connection when the client disconnects
|
||||||
//
|
//
|
||||||
// WebSocket clients receive periodic updates with current aircraft positions,
|
// WebSocket clients receive periodic updates with current aircraft positions,
|
||||||
// source status, and system statistics. The connection is kept alive until
|
// source status, and system statistics. The connection is kept alive until
|
||||||
|
|
@ -599,11 +588,11 @@ func (s *Server) periodicUpdateRoutine() {
|
||||||
// broadcastUpdate creates and queues an aircraft update message for WebSocket clients.
|
// broadcastUpdate creates and queues an aircraft update message for WebSocket clients.
|
||||||
//
|
//
|
||||||
// This function:
|
// This function:
|
||||||
// 1. Collects current aircraft data from the merger
|
// 1. Collects current aircraft data from the merger
|
||||||
// 2. Filters aircraft to only include "useful" ones (with position or callsign)
|
// 2. Filters aircraft to only include "useful" ones (with position or callsign)
|
||||||
// 3. Formats the data as a WebSocketMessage with type "aircraft_update"
|
// 3. Formats the data as a WebSocketMessage with type "aircraft_update"
|
||||||
// 4. Converts ICAO addresses to hex strings for JSON compatibility
|
// 4. Converts ICAO addresses to hex strings for JSON compatibility
|
||||||
// 5. Queues the message for broadcast (non-blocking)
|
// 5. Queues the message for broadcast (non-blocking)
|
||||||
//
|
//
|
||||||
// If the broadcast channel is full, the update is dropped to prevent blocking.
|
// If the broadcast channel is full, the update is dropped to prevent blocking.
|
||||||
// This ensures the system continues operating even if WebSocket clients
|
// This ensures the system continues operating even if WebSocket clients
|
||||||
|
|
@ -780,11 +769,11 @@ func (s *Server) handleDebugAircraft(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
response := map[string]interface{}{
|
response := map[string]interface{}{
|
||||||
"timestamp": time.Now().Unix(),
|
"timestamp": time.Now().Unix(),
|
||||||
"all_aircraft": allAircraftMap,
|
"all_aircraft": allAircraftMap,
|
||||||
"filtered_aircraft": filteredAircraftMap,
|
"filtered_aircraft": filteredAircraftMap,
|
||||||
"all_count": len(allAircraftMap),
|
"all_count": len(allAircraftMap),
|
||||||
"filtered_count": len(filteredAircraftMap),
|
"filtered_count": len(filteredAircraftMap),
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
|
||||||
BIN
main
Executable file
BIN
main
Executable file
Binary file not shown.
15
old.json
Normal file
15
old.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"address": ":8080",
|
||||||
|
"port": 8080
|
||||||
|
},
|
||||||
|
"dump1090": {
|
||||||
|
"host": "svovel",
|
||||||
|
"data_port": 30003
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"latitude": 59.908127,
|
||||||
|
"longitude": 10.801460,
|
||||||
|
"name": "Etterstadsletta flyplass"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,40 +34,25 @@ mkdir -p "$BUILD_DIR"
|
||||||
# Change to project directory
|
# Change to project directory
|
||||||
cd "$PROJECT_DIR"
|
cd "$PROJECT_DIR"
|
||||||
|
|
||||||
# Build the applications
|
# Build the application
|
||||||
echo_info "Building SkyView applications..."
|
echo_info "Building SkyView application..."
|
||||||
export CGO_ENABLED=0
|
export CGO_ENABLED=0
|
||||||
export GOOS=linux
|
export GOOS=linux
|
||||||
export GOARCH=amd64
|
export GOARCH=amd64
|
||||||
|
|
||||||
VERSION=$(git describe --tags --always --dirty)
|
go build -ldflags="-w -s -X main.version=$(git describe --tags --always --dirty)" \
|
||||||
LDFLAGS="-w -s -X main.version=$VERSION"
|
|
||||||
|
|
||||||
# Build main skyview binary
|
|
||||||
echo_info "Building skyview..."
|
|
||||||
if ! go build -ldflags="$LDFLAGS" \
|
|
||||||
-o "$DEB_DIR/usr/bin/skyview" \
|
-o "$DEB_DIR/usr/bin/skyview" \
|
||||||
./cmd/skyview; then
|
./cmd/skyview
|
||||||
echo_error "Failed to build skyview"
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo_error "Failed to build application"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build beast-dump utility
|
echo_info "Built binary: $(file "$DEB_DIR/usr/bin/skyview")"
|
||||||
echo_info "Building beast-dump..."
|
|
||||||
if ! go build -ldflags="$LDFLAGS" \
|
|
||||||
-o "$DEB_DIR/usr/bin/beast-dump" \
|
|
||||||
./cmd/beast-dump; then
|
|
||||||
echo_error "Failed to build beast-dump"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo_info "Built binaries:"
|
# Set executable permission
|
||||||
echo_info " skyview: $(file "$DEB_DIR/usr/bin/skyview")"
|
|
||||||
echo_info " beast-dump: $(file "$DEB_DIR/usr/bin/beast-dump")"
|
|
||||||
|
|
||||||
# Set executable permissions
|
|
||||||
chmod +x "$DEB_DIR/usr/bin/skyview"
|
chmod +x "$DEB_DIR/usr/bin/skyview"
|
||||||
chmod +x "$DEB_DIR/usr/bin/beast-dump"
|
|
||||||
|
|
||||||
# Get package info
|
# Get package info
|
||||||
VERSION=$(grep "Version:" "$DEB_DIR/DEBIAN/control" | cut -d' ' -f2)
|
VERSION=$(grep "Version:" "$DEB_DIR/DEBIAN/control" | cut -d' ' -f2)
|
||||||
|
|
@ -84,7 +69,9 @@ sed -i "s/Installed-Size:.*/Installed-Size: $INSTALLED_SIZE/" "$DEB_DIR/DEBIAN/c
|
||||||
echo "Installed-Size: $INSTALLED_SIZE" >> "$DEB_DIR/DEBIAN/control"
|
echo "Installed-Size: $INSTALLED_SIZE" >> "$DEB_DIR/DEBIAN/control"
|
||||||
|
|
||||||
# Build the package
|
# Build the package
|
||||||
if dpkg-deb --root-owner-group --build "$DEB_DIR" "$BUILD_DIR/$DEB_FILE"; then
|
dpkg-deb --build "$DEB_DIR" "$BUILD_DIR/$DEB_FILE"
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
echo_info "Successfully created: $BUILD_DIR/$DEB_FILE"
|
echo_info "Successfully created: $BUILD_DIR/$DEB_FILE"
|
||||||
|
|
||||||
# Show package info
|
# Show package info
|
||||||
|
|
|
||||||
BIN
ux.png
Normal file
BIN
ux.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
Loading…
Add table
Add a link
Reference in a new issue