spaxel/mothership/internal/simulator/session.go
jedarden 60a8b89a8e fix: resolve Go compilation errors in simulator and oui packages
- Remove duplicate type declarations from session.go (Space, Wall,
  wallAttenuationDB, Vector3, Walker, WalkerType) — space.go and
  walker.go contain the newer, more complete versions
- Update session.go to use new type names: WalkerTypeRandomWalk,
  WalkerTypePathFollow, WalkerTypeNodeToNode; use Space.Bounds()
  instead of .Width/.Depth; use Point instead of Vector3
- Merge ShoppingList structs: remove duplicate from gdop.go, add
  OptimalPositions []Point to the canonical struct in accuracy.go
- Fix unused variables: minZ/maxZ (accuracy.go), z (accuracy.go),
  nodeType (node.go), maxZ (walker.go), noise (propagation.go),
  lastHealthTime and angle (cmd/sim/main.go), id (virtual_state.go)
- Fix BoundingBox field capitalization in virtual_state.go
- Fix virtualMAC to hash string nodeID to uint32 before bit-shifting
- Fix mrand alias usage in propagation.go (rand -> mrand)
- Fix PhaseAtSubcarrier capitalization in physics.go
- Fix WalkerTypePath/WalkerTypeRandom references in engine.go/handler.go
- Rename Walker to SimWalker in cmd/sim/walker.go to avoid conflict
  with main.go's local Walker type
- Remove 3 duplicate OUI map keys (0x0001C8, 0x080030 ×2)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 19:23:22 -04:00

359 lines
9 KiB
Go

// Package simulator provides pre-deployment simulation capabilities.
//
// It allows users to model their space, place virtual nodes, and run
// synthetic walkers to estimate expected accuracy before purchasing hardware.
package simulator
import (
"encoding/json"
"fmt"
"math"
"sync"
"time"
)
// Session represents a simulation session.
type Session struct {
mu sync.RWMutex
id string
space *Space
nodes []*VirtualNode
walkers []*Walker
params *SimulationParams
state SessionState
created_at int64
updated_at int64
ctx chan struct{}
}
// SessionState is the state of a simulation session.
type SessionState string
const (
StateSetup SessionState = "setup"
StateRunning SessionState = "running"
StatePaused SessionState = "paused"
StateComplete SessionState = "complete"
)
// VirtualNode represents a simulated node.
type VirtualNode struct {
ID string `json:"id"`
Name string `json:"name"`
Position Point `json:"position"` // x, y, z in meters
Role string `json:"role"` // "tx", "rx", "tx_rx"
}
// SimulationParams holds simulation parameters.
type SimulationParams struct {
TickRateHz int `json:"tick_rate_hz"` // 10 Hz default
WalkerSpeed float64 `json:"walker_speed"` // m/s
SignalAmplitude float64 `json:"signal_amplitude"` // 0.05
FresnelSigma float64 `json:"fresnel_sigma"` // 0.3m
NoiseSigma float64 `json:"noise_sigma"` // Gaussian noise std dev
DefaultRSSI float64 `json:"default_rssi"` // -30 dBm at 1m
WallAttenuationDB float64 `json:"wall_attenuation_db"` // default 4 dB
}
// DefaultSimulationParams returns the default simulation parameters.
func DefaultSimulationParams() *SimulationParams {
return &SimulationParams{
TickRateHz: 10,
WalkerSpeed: 1.0,
SignalAmplitude: 0.05,
FresnelSigma: 0.3,
NoiseSigma: 0.01,
DefaultRSSI: -30.0,
WallAttenuationDB: 4.0,
}
}
// NewSession creates a new simulation session.
func NewSession(id string, space *Space) *Session {
return &Session{
id: id,
space: space,
nodes: []*VirtualNode{},
walkers: []*Walker{},
params: DefaultSimulationParams(),
state: StateSetup,
created_at: time.Now().UnixMilli(),
updated_at: time.Now().UnixMilli(),
ctx: make(chan struct{}),
}
}
// ID returns the session ID.
func (s *Session) ID() string {
return s.id
}
// State returns the current session state.
func (s *Session) State() SessionState {
s.mu.RLock()
defer s.mu.RUnlock()
return s.state
}
// AddNode adds a virtual node to the simulation.
func (s *Session) AddNode(node *VirtualNode) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.state != StateSetup {
return fmt.Errorf("cannot add nodes in state %s", s.state)
}
s.nodes = append(s.nodes, node)
s.updated_at = time.Now().UnixMilli()
return nil
}
// RemoveNode removes a virtual node from the simulation.
func (s *Session) RemoveNode(nodeID string) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.state != StateSetup {
return fmt.Errorf("cannot remove nodes in state %s", s.state)
}
for i, node := range s.nodes {
if node.ID == nodeID {
s.nodes = append(s.nodes[:i], s.nodes[i+1:]...)
s.updated_at = time.Now().UnixMilli()
return nil
}
}
return fmt.Errorf("node not found: %s", nodeID)
}
// AddWalker adds a walker to the simulation.
func (s *Session) AddWalker(walker *Walker) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.state != StateSetup {
return fmt.Errorf("cannot add walkers in state %s", s.state)
}
s.walkers = append(s.walkers, walker)
s.updated_at = time.Now().UnixMilli()
return nil
}
// Start starts the simulation.
func (s *Session) Start() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.state != StateSetup && s.state != StatePaused {
return fmt.Errorf("cannot start in state %s", s.state)
}
if len(s.nodes) < 2 {
return fmt.Errorf("need at least 2 nodes for simulation")
}
if len(s.walkers) == 0 {
return fmt.Errorf("need at least 1 walker for simulation")
}
s.state = StateRunning
s.updated_at = time.Now().UnixMilli()
// Start simulation loop in background
go s.simulationLoop()
return nil
}
// Pause pauses the simulation.
func (s *Session) Pause() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.state != StateRunning {
return fmt.Errorf("cannot pause in state %s", s.state)
}
s.state = StatePaused
s.updated_at = time.Now().UnixMilli()
return nil
}
// Stop stops the simulation.
func (s *Session) Stop() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.state != StateRunning && s.state != StatePaused {
return fmt.Errorf("cannot stop in state %s", s.state)
}
s.state = StateComplete
close(s.ctx)
s.updated_at = time.Now().UnixMilli()
return nil
}
// simulationLoop runs the main simulation loop.
func (s *Session) simulationLoop() {
ticker := time.NewTicker(time.Second / time.Duration(s.params.TickRateHz))
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.mu.Lock()
if s.state != StateRunning {
s.mu.Unlock()
continue
}
// Update walker positions
for _, walker := range s.walkers {
s.updateWalkerPosition(walker)
}
s.updated_at = time.Now().UnixMilli()
s.mu.Unlock()
case <-s.ctx:
return
}
}
}
// updateWalkerPosition updates a single walker's position based on its type.
func (s *Session) updateWalkerPosition(walker *Walker) {
switch walker.Type {
case WalkerTypeRandomWalk:
s.updateRandomWalker(walker)
case WalkerTypePathFollow:
s.updatePathWalker(walker)
case WalkerTypeNodeToNode:
s.updateZoneWalker(walker)
}
}
// updateRandomWalker updates a random walker using Gaussian velocity updates.
func (s *Session) updateRandomWalker(walker *Walker) {
// Apply velocity with small random changes
walker.Position.X += walker.Velocity.X / float64(s.params.TickRateHz)
walker.Position.Y += walker.Velocity.Y / float64(s.params.TickRateHz)
// Random velocity changes
velocityChange := 0.5 / float64(s.params.TickRateHz)
walker.Velocity.X += (randFloat64()*2 - 1) * velocityChange
walker.Velocity.Y += (randFloat64()*2 - 1) * velocityChange
// Clamp velocity to max speed
maxSpeed := s.params.WalkerSpeed
speed := math.Sqrt(walker.Velocity.X*walker.Velocity.X + walker.Velocity.Y*walker.Velocity.Y)
if speed > maxSpeed {
scale := maxSpeed / speed
walker.Velocity.X *= scale
walker.Velocity.Y *= scale
}
// Bounce off walls using space bounds
minX, minY, _, maxX, maxY, _ := s.space.Bounds()
if walker.Position.X < minX {
walker.Position.X = minX
walker.Velocity.X *= -1
}
if walker.Position.X > maxX {
walker.Position.X = maxX
walker.Velocity.X *= -1
}
if walker.Position.Y < minY {
walker.Position.Y = minY
walker.Velocity.Y *= -1
}
if walker.Position.Y > maxY {
walker.Position.Y = maxY
walker.Velocity.Y *= -1
}
}
// updatePathWalker updates a path-following walker.
func (s *Session) updatePathWalker(walker *Walker) {
if len(walker.Path) == 0 {
return
}
target := walker.Path[walker.PathIndex]
dx := target.X - walker.Position.X
dy := target.Y - walker.Position.Y
distance := math.Sqrt(dx*dx + dy*dy)
stepSize := s.params.WalkerSpeed / float64(s.params.TickRateHz)
if distance <= stepSize {
// Reached target, move to next waypoint
walker.Position = target
walker.PathIndex = (walker.PathIndex + 1) % len(walker.Path)
} else {
// Move toward target
walker.Position.X += (dx / distance) * stepSize
walker.Position.Y += (dy / distance) * stepSize
}
}
// updateZoneWalker updates a zone-walking walker.
func (s *Session) updateZoneWalker(walker *Walker) {
// For now, treat zone walkers as random walkers
// TODO: Implement zone-based movement
s.updateRandomWalker(walker)
}
// GetSnapshot returns the current simulation state.
func (s *Session) GetSnapshot() *SessionSnapshot {
s.mu.RLock()
defer s.mu.RUnlock()
walkerPositions := make([]WalkerPosition, len(s.walkers))
for i, w := range s.walkers {
walkerPositions[i] = WalkerPosition{
ID: w.ID,
Position: w.Position,
}
}
return &SessionSnapshot{
ID: s.id,
State: s.state,
Space: s.space,
NodeCount: len(s.nodes),
WalkerPositions: walkerPositions,
UpdatedAt: s.updated_at,
}
}
// SessionSnapshot represents a point-in-time snapshot of the simulation.
type SessionSnapshot struct {
ID string `json:"id"`
State SessionState `json:"state"`
Space *Space `json:"space"`
NodeCount int `json:"node_count"`
WalkerPositions []WalkerPosition `json:"walker_positions"`
UpdatedAt int64 `json:"updated_at"`
}
// WalkerPosition represents a walker's position at a point in time.
type WalkerPosition struct {
ID string `json:"id"`
Position Point `json:"position"`
}
// randFloat64 returns a random float64 in [0, 1).
func randFloat64() float64 {
return float64(time.Now().UnixNano()%1000) / 1000.0
}
// ToJSON converts the session to JSON.
func (s *Session) ToJSON() ([]byte, error) {
snapshot := s.GetSnapshot()
return json.Marshal(snapshot)
}