diff --git a/dashboard/index.html b/dashboard/index.html index 582a4b3..8d26ff0 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -1483,6 +1483,20 @@ #room-editor-btn:hover { background: rgba(255,255,255,0.12); color: #ccc; } #room-editor-btn.active { background: rgba(79,195,247,0.2); color: #4fc3f7; border-color: #4fc3f7; } + /* Simulator button */ + #simulator-btn { + background: rgba(255,255,255,0.06); + border: 1px solid rgba(255,255,255,0.12); + color: #888; + font-size: 12px; + padding: 3px 10px; + border-radius: 4px; + cursor: pointer; + transition: background 0.2s, color 0.2s; + } + #simulator-btn:hover { background: rgba(255,255,255,0.12); color: #ccc; } + #simulator-btn.active { background: rgba(156,39,176,0.2); color: #ab47bc; border-color: #ab47bc; } + /* GDOP toggle */ #gdop-toggle-btn { background: rgba(255,255,255,0.06); @@ -1704,6 +1718,14 @@ margin-bottom: 6px; } + #gdop-coverage-score { + font-size: 13px; + font-weight: 600; + color: #22c65e; + margin-bottom: 8px; + text-align: center; + } + #gdop-gradient { width: 140px; height: 12px; @@ -2543,6 +2565,7 @@ +
FPS: 0 @@ -2710,9 +2733,190 @@
+ + +

GDOP Coverage

+
Coverage: --%
Excellent diff --git a/dashboard/js/placement.js b/dashboard/js/placement.js index a485d96..5b3c5e6 100644 --- a/dashboard/js/placement.js +++ b/dashboard/js/placement.js @@ -110,6 +110,10 @@ const Placement = (function () { // ── GDOP overlay ───────────────────────────────────────────────────────── + // Cache for backend GDOP data + var _gdopCache = null; + var _gdopPending = false; + function updateGDOPOverlay() { var nodes = getNodePositions(); @@ -118,32 +122,70 @@ const Placement = (function () { return; } + // Fetch GDOP data from backend API (uses angular diversity algorithm) + if (!_gdopPending) { + _gdopPending = true; + fetch('/api/simulator/gdop/heatmap') + .then(function (resp) { return resp.json(); }) + .then(function (data) { + _gdopCache = data; + renderGDOPFromCache(); + _gdopPending = false; + }) + .catch(function (e) { + console.error('[Placement] GDOP fetch failed:', e); + _gdopPending = false; + }); + } else if (_gdopCache) { + renderGDOPFromCache(); + } + } + + function renderGDOPFromCache() { + if (!_gdopCache || !_roomConfig) return; + + var data = _gdopCache; var w = _roomConfig.width; var d = _roomConfig.depth; var ox = _roomConfig.origin_x || 0; var oz = _roomConfig.origin_z || 0; + // Get grid dimensions from backend response + var gridWidth = data.grid_dimensions ? data.grid_dimensions[0] : GDOP_RES; + var gridDepth = data.grid_dimensions ? data.grid_dimensions[1] : GDOP_RES; + var gdopValues = data.gdop_map || []; + var colors = data.colors || []; + // Create or reuse DataTexture if (!_gdopTexture) { - var data = new Uint8Array(GDOP_RES * GDOP_RES * 4); - _gdopTexture = new THREE.DataTexture(data, GDOP_RES, GDOP_RES, THREE.RGBAFormat); + var texData = new Uint8Array(gridWidth * gridDepth * 4); + _gdopTexture = new THREE.DataTexture(texData, gridWidth, gridDepth, THREE.RGBAFormat); _gdopTexture.magFilter = THREE.LinearFilter; _gdopTexture.minFilter = THREE.LinearFilter; } - var data = _gdopTexture.image.data; + var texData = _gdopTexture.image.data; - for (var iz = 0; iz < GDOP_RES; iz++) { - var pz = oz + (iz / (GDOP_RES - 1)) * d; - for (var ix = 0; ix < GDOP_RES; ix++) { - var px = ox + (ix / (GDOP_RES - 1)) * w; - var hdop = computeHDOP(px, pz, nodes); - var c = gdopToColor(hdop); - var idx = (iz * GDOP_RES + ix) * 4; - data[idx] = c[0]; - data[idx + 1] = c[1]; - data[idx + 2] = c[2]; - data[idx + 3] = c[3]; + // Use backend-provided colors if available, otherwise compute from GDOP values + if (colors.length > 0) { + // Backend provides RGB colors + for (var i = 0; i < colors.length && i < gridWidth * gridDepth; i++) { + var idx = i * 4; + texData[idx] = colors[i][0]; + texData[idx + 1] = colors[i][1]; + texData[idx + 2] = colors[i][2]; + texData[idx + 3] = 150; // Alpha + } + } else if (gdopValues.length > 0) { + // Fallback: compute colors from GDOP values + for (var i = 0; i < gdopValues.length && i < gridWidth * gridDepth; i++) { + var gdop = gdopValues[i]; + var c = gdopToColor(gdop); + var idx = i * 4; + texData[idx] = c[0]; + texData[idx + 1] = c[1]; + texData[idx + 2] = c[2]; + texData[idx + 3] = c[3]; } } @@ -168,6 +210,20 @@ const Placement = (function () { } _gdopMesh.visible = true; + + // Update coverage score display if available + updateCoverageScore(data.coverage_score || data.coverage_percent); + } + + function updateCoverageScore(score) { + var scoreEl = document.getElementById('gdop-coverage-score'); + if (scoreEl) { + var pct = typeof score === 'number' ? score : 0; + scoreEl.textContent = 'Coverage: ' + pct.toFixed(1) + '%'; + + // Color-code the score + scoreEl.style.color = pct >= 70 ? '#22c65e' : pct >= 50 ? '#ffc107' : '#dc3545'; + } } function rebuildGDOPIfDirty() { @@ -211,6 +267,7 @@ const Placement = (function () { obj.position.z = Math.max(oz, Math.min(oz + _roomConfig.depth, obj.position.z)); _gdopDirty = true; + _gdopCache = null; // Clear cache to force refresh with new node positions // Update link lines to follow dragged node if (Viz3D.rebuildLinkLines) Viz3D.rebuildLinkLines(); diff --git a/dashboard/js/simulate.js b/dashboard/js/simulate.js index e15b8a8..c3359ab 100644 --- a/dashboard/js/simulate.js +++ b/dashboard/js/simulate.js @@ -443,12 +443,14 @@ const node = { id: id, name: 'Node ' + (state.nodes.length + 1), + type: 'virtual', position: { x: state.space.width / 2, y: 1.0, z: state.space.depth / 2, }, role: 'tx_rx', + enabled: true, }; state.nodes.push(node); @@ -537,9 +539,14 @@ function addWalker() { const type = elements.walkerType.value; const id = 'walker_' + Date.now(); + + // Map frontend type to backend WalkerType + const backendType = type === 'path' ? 'path_follow' : + type === 'zone' ? 'node_to_node' : 'random_walk'; + const walker = { id: id, - type: type, + type: backendType, position: { x: state.space.width / 2, y: 1.0, @@ -550,19 +557,19 @@ y: 0, z: (Math.random() - 0.5) * CONFIG.walkerSpeed, }, + speed: CONFIG.walkerSpeed, + height: 1.7, }; if (type === 'path') { // Create default path walker.path = [ - { x: 2, y: 1, z: 2 }, - { x: state.space.width - 2, y: 1, z: 2 }, - { x: state.space.width - 2, y: 1, z: state.space.depth - 2 }, - { x: 2, y: 1, z: state.space.depth - 2 }, + { x: 2, y: 1.0, z: 2 }, + { x: state.space.width - 2, y: 1.0, z: 2 }, + { x: state.space.width - 2, y: 1.0, z: state.space.depth - 2 }, + { x: 2, y: 1.0, z: state.space.depth - 2 }, ]; walker.path_index = 0; - } else if (type === 'zone') { - walker.target_zones = []; } state.walkers.push(walker); @@ -636,6 +643,21 @@ } } + // ============================================ + // Panel Toggle + // ============================================ + function togglePanel() { + const panel = document.getElementById('simulator-panel'); + const btn = document.getElementById('simulator-btn'); + if (!panel || !btn) return; + + const isVisible = panel.style.display !== 'none'; + panel.style.display = isVisible ? 'none' : 'block'; + btn.classList.toggle('active', !isVisible); + + console.log('[Simulate] Panel', isVisible ? 'hidden' : 'shown'); + } + // ============================================ // GDOP Visualization // ============================================ diff --git a/dashboard/js/viz3d.js b/dashboard/js/viz3d.js index f550a94..208d424 100644 --- a/dashboard/js/viz3d.js +++ b/dashboard/js/viz3d.js @@ -2142,15 +2142,7 @@ const Viz3D = (function () { * Fetch GDOP heatmap data from API. */ function fetchGDOPData() { - fetch('/api/simulator/gdop/compute', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - cell_size: 0.2, - max_zone: 3, - threshold: 0.02 - }) - }) + fetch('/api/simulator/gdop/heatmap') .then(function(response) { return response.json(); }) .then(function(data) { _gdopData = data; @@ -2166,21 +2158,39 @@ const Viz3D = (function () { * @param {Object} data - GDOP computation results */ function updateGDOPOverlay(data) { - if (!data || !data.gdop_heatmap) { + if (!data || !data.gdop_map) { console.warn('[Viz3D] No GDOP heatmap data in response'); return; } - var heatmap = data.gdop_heatmap; - var width = heatmap.width; - var depth = heatmap.depth; - var cellSize = heatmap.cell_size; - var originX = heatmap.origin_x; - var originY = heatmap.origin_y; + var gridDimensions = data.grid_dimensions || [0, 0, 0]; + var width = gridDimensions[0]; + var depth = gridDimensions[1]; + var cellSize = 0.2; // Default cell size (not provided by this endpoint) // Create texture from GDOP data - var gdopValues = new Float32Array(heatmap.gdop_values); - var colors = new Uint8Array(heatmap.colors.flat()); + var gdopValues = new Float32Array(data.gdop_map); + + // Generate colors from GDOP values using GDOPColorMap + var colors = new Uint8Array(width * depth * 3); + for (var i = 0; i < gdopValues.length; i++) { + var gdop = gdopValues[i]; + var color; + if (gdop >= 9999) { + color = { r: 80, g: 80, b: 80 }; // None - gray + } else if (gdop < 2.0) { + color = { r: 34, g: 197, b: 94 }; // Excellent - green + } else if (gdop < 4.0) { + color = { r: 255, g: 193, b: 7 }; // Good - yellow + } else if (gdop < 8.0) { + color = { r: 255, g: 146, b: 0 }; // Fair - orange + } else { + color = { r: 220, g: 53, b: 69 }; // Poor - red + } + colors[i * 3] = color.r; + colors[i * 3 + 1] = color.g; + colors[i * 3 + 2] = color.b; + } // Create data texture if (_gdopTexture) { @@ -2206,9 +2216,9 @@ const Viz3D = (function () { _gdopMesh = new THREE.Mesh(geo, mat); _gdopMesh.rotation.x = -Math.PI / 2; _gdopMesh.position.set( - originX + (width * cellSize) / 2, + (width * cellSize) / 2, 0.01, // Slightly above floor - originY + (depth * cellSize) / 2 + (depth * cellSize) / 2 ); _scene.add(_gdopMesh); _gdopMesh.visible = _gdopOverlayVisible; @@ -2220,17 +2230,17 @@ const Viz3D = (function () { depth * cellSize ); _gdopMesh.position.set( - originX + (width * cellSize) / 2, + (width * cellSize) / 2, 0.01, - originY + (depth * cellSize) / 2 + (depth * cellSize) / 2 ); _gdopMesh.material.map = _gdopTexture; } // Update or create legend - updateGDOPLegend(data.coverage_score); + updateGDOPLegend(data.coverage_percent || data.coverage_score); - console.log('[Viz3D] GDOP overlay updated:', data.coverage_score.toFixed(1) + '% coverage'); + console.log('[Viz3D] GDOP overlay updated:', (data.coverage_percent || data.coverage_score || 0).toFixed(1) + '% coverage'); } /** diff --git a/mothership/internal/api/simulator.go b/mothership/internal/api/simulator.go index 9956a0c..e8ef3f5 100644 --- a/mothership/internal/api/simulator.go +++ b/mothership/internal/api/simulator.go @@ -3,11 +3,13 @@ package api import ( "encoding/json" + "fmt" "log" "math" "net/http" "strconv" "sync" + "time" "github.com/go-chi/chi/v5" "github.com/spaxel/mothership/internal/simulator" @@ -38,6 +40,7 @@ func (h *SimulatorHandler) RegisterRoutes(r chi.Router) { r.Route("/simulator", func(r chi.Router) { r.Get("/", h.GetState) r.Post("/reset", h.Reset) + r.Post("/session", h.CreateSession) // Space management r.Route("/space", func(r chi.Router) { @@ -108,6 +111,37 @@ func (h *SimulatorHandler) Reset(w http.ResponseWriter, r *http.Request) { respondJSON(w, http.StatusOK, map[string]string{"status": "reset"}) } +// CreateSession creates a new simulator session +func (h *SimulatorHandler) CreateSession(w http.ResponseWriter, r *http.Request) { + var req struct { + Space *simulator.Space `json:"space"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + // Use default space if request body is empty/invalid + req.Space = simulator.DefaultSpace() + } + + if req.Space == nil { + req.Space = simulator.DefaultSpace() + } + + // Update the handler's space if provided + if req.Space != nil { + h.mu.Lock() + h.space = req.Space + h.mu.Unlock() + } + + // Generate session ID + sessionID := fmt.Sprintf("sim_%d", time.Now().UnixNano()) + + respondJSON(w, http.StatusOK, map[string]interface{}{ + "session_id": sessionID, + "space": h.space, + }) +} + // GetSpace returns the current space definition func (h *SimulatorHandler) GetSpace(w http.ResponseWriter, r *http.Request) { h.mu.RLock() diff --git a/mothership/internal/simulator/registry_bridge.go b/mothership/internal/simulator/registry_bridge.go index 8a44e32..66ec734 100644 --- a/mothership/internal/simulator/registry_bridge.go +++ b/mothership/internal/simulator/registry_bridge.go @@ -234,7 +234,7 @@ func (b *FleetRegistryBridge) OptimizeCoverage(space *Space) (*CoverageOptimizat weakAreas := make([]Point, 0) for row := range results { for col := range results[row] { - if results[row][col] > 4 { + if results[row][col].GDOP > 4 { // Calculate position from grid indices x := minX + float64(col)*config.CellSize y := minY + float64(row)*config.CellSize diff --git a/mothership/internal/simulator/space.go b/mothership/internal/simulator/space.go index 1ed696f..e2a5724 100644 --- a/mothership/internal/simulator/space.go +++ b/mothership/internal/simulator/space.go @@ -244,6 +244,12 @@ func (s *Space) TotalVolume() float64 { return v } +// Dimensions returns the overall width, depth, and height of the space +func (s *Space) Dimensions() (width, depth, height float64) { + minX, minY, minZ, maxX, maxY, maxZ := s.Bounds() + return maxX - minX, maxY - minY, maxZ - minZ +} + // GetWalls returns all wall segments from all rooms plus standalone walls func (s *Space) GetWalls() []WallSegment { walls := make([]WallSegment, 0, len(s.Walls))