spaxel/mothership/internal/simulator/node.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

266 lines
6.6 KiB
Go

package simulator
import (
"encoding/json"
"fmt"
"math/rand"
)
// NodeType represents the type of node
type NodeType string
const (
NodeTypeReal NodeType = "esp32" // Real ESP32-S3 node
NodeTypeVirtual NodeType = "virtual" // Simulated virtual node
NodeTypeAP NodeType = "ap" // Access point (passive radar TX)
)
// NodeRole represents the operational role of a node
type NodeRole string
const (
RoleTX NodeRole = "tx" // Transmit only
RoleRX NodeRole = "rx" // Receive only
RoleTXRX NodeRole = "tx_rx" // Both transmit and receive
RolePassive NodeRole = "passive" // Passive radar (RX only, AP as TX)
RoleIdle NodeRole = "idle" // Disabled
)
// Node represents a virtual or real node in the simulation
type Node struct {
ID string `json:"id"`
Name string `json:"name"`
Type NodeType `json:"type"`
Role NodeRole `json:"role"`
Position Point `json:"position"` // X, Y, Z in meters
Enabled bool `json:"enabled"`
// For AP nodes
APBSSID string `json:"ap_bssid,omitempty"`
APChannel int `json:"ap_channel,omitempty"`
}
// Position returns the node's position as a Point
func (n *Node) PositionVec() Point {
return n.Position
}
// IsVirtual returns true if this is a virtual node
func (n *Node) IsVirtual() bool {
return n.Type == NodeTypeVirtual
}
// IsAP returns true if this is an access point node
func (n *Node) IsAP() bool {
return n.Type == NodeTypeAP
}
// GenerateMAC returns a simulated MAC address for this node
func (n *Node) GenerateMAC() string {
// Generate a deterministic MAC based on node ID
if n.IsAP() && n.APBSSID != "" {
return n.APBSSID
}
// Use a simple MAC pattern based on ID hash
hash := 0
for _, c := range n.ID {
hash += int(c)
}
return fmt.Sprintf("AA:BB:CC:DD:%02X:%02X", (hash&0xFF), ((hash>>8)&0xFF))
}
// NewNode creates a new node at the given position
func NewNode(id, name string, nodeType NodeType, position Point) *Node {
return &Node{
ID: id,
Name: name,
Type: nodeType,
Role: RoleTXRX,
Position: position,
Enabled: true,
}
}
// NewVirtualNode creates a new virtual node for planning
func NewVirtualNode(id, name string, position Point) *Node {
return NewNode(id, name, NodeTypeVirtual, position)
}
// NewAPNode creates a new access point node (for passive radar)
func NewAPNode(id, name, bssid string, channel int, position Point) *Node {
n := NewNode(id, name, NodeTypeAP, position)
n.Role = RoleTX // AP acts as TX in passive radar
n.APBSSID = bssid
n.APChannel = channel
return n
}
// NodeSet is a collection of nodes with helper methods
type NodeSet struct {
nodes []*Node
}
// NewNodeSet creates an empty node set
func NewNodeSet() *NodeSet {
return &NodeSet{nodes: make([]*Node, 0)}
}
// Add adds a node to the set
func (ns *NodeSet) Add(n *Node) {
ns.nodes = append(ns.nodes, n)
}
// AddNode adds a node at the given position
func (ns *NodeSet) AddNode(id, name string, nodeType NodeType, position Point) {
ns.Add(NewNode(id, name, nodeType, position))
}
// AddVirtualNode adds a virtual node at the given position
func (ns *NodeSet) AddVirtualNode(id, name string, position Point) {
ns.Add(NewVirtualNode(id, name, position))
}
// AddAPNode adds an AP node at the given position
func (ns *NodeSet) AddAPNode(id, name, bssid string, channel int, position Point) {
ns.Add(NewAPNode(id, name, bssid, channel, position))
}
// Count returns the number of nodes
func (ns *NodeSet) Count() int {
return len(ns.nodes)
}
// All returns all nodes
func (ns *NodeSet) All() []*Node {
return ns.nodes
}
// Enabled returns only enabled nodes
func (ns *NodeSet) Enabled() []*Node {
result := make([]*Node, 0)
for _, n := range ns.nodes {
if n.Enabled {
result = append(result, n)
}
}
return result
}
// TXNodes returns nodes that can transmit (TX or TX_RX or AP)
func (ns *NodeSet) TXNodes() []*Node {
result := make([]*Node, 0)
for _, n := range ns.Enabled() {
if n.Role == RoleTX || n.Role == RoleTXRX || n.IsAP() {
result = append(result, n)
}
}
return result
}
// RXNodes returns nodes that can receive (RX or TX_RX or Passive)
func (ns *NodeSet) RXNodes() []*Node {
result := make([]*Node, 0)
for _, n := range ns.Enabled() {
if n.Role == RoleRX || n.Role == RoleTXRX || n.Role == RolePassive {
result = append(result, n)
}
}
return result
}
// GetByID returns a node by ID
func (ns *NodeSet) GetByID(id string) *Node {
for _, n := range ns.nodes {
if n.ID == id {
return n
}
}
return nil
}
// Remove removes a node by ID
func (ns *NodeSet) Remove(id string) bool {
for i, n := range ns.nodes {
if n.ID == id {
ns.nodes = append(ns.nodes[:i], ns.nodes[i+1:]...)
return true
}
}
return false
}
// Clear removes all nodes
func (ns *NodeSet) Clear() {
ns.nodes = make([]*Node, 0)
}
// MarshalJSON implements custom JSON marshaling
func (ns *NodeSet) MarshalJSON() ([]byte, error) {
return json.Marshal(ns.nodes)
}
// UnmarshalJSON implements custom JSON unmarshaling
func (ns *NodeSet) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &ns.nodes)
}
// CornerPositions returns suggested node positions at room corners
// for a given space. Useful for quick initial placement.
func CornerPositions(s *Space) []Point {
minX, minY, minZ, maxX, maxY, maxZ := s.Bounds()
height := (maxZ - minZ) / 2 // Average height
return []Point{
{X: minX, Y: minY, Z: height}, // Bottom-left, high
{X: maxX, Y: minY, Z: height}, // Bottom-right, high
{X: minX, Y: maxY, Z: height}, // Top-left, high
{X: maxX, Y: maxY, Z: height}, // Top-right, high
{X: (minX + maxX) / 2, Y: minY, Z: 0.3}, // Bottom-middle, low
{X: (minX + maxX) / 2, Y: maxY, Z: 0.3}, // Top-middle, low
}
}
// SuggestedNodes creates a suggested node set for a space
// with nodes positioned at corners and mid-points
func SuggestedNodes(s *Space, count int) *NodeSet {
ns := NewNodeSet()
positions := CornerPositions(s)
// Use corner positions, then add random positions if needed
for i := 0; i < count; i++ {
var pos Point
if i < len(positions) {
pos = positions[i]
} else {
// Random position within bounds
minX, minY, _, maxX, maxY, maxZ := s.Bounds()
pos = Point{
X: minX + rand.Float64()*(maxX-minX),
Y: minY + rand.Float64()*(maxY-minY),
Z: rand.Float64() * maxZ,
}
}
role := RoleTXRX
// Last node can be AP for passive radar
if i == count-1 {
role = RoleTX
ns.AddAPNode(
fmt.Sprintf("node-%d", i),
fmt.Sprintf("Node %d", i+1),
"AA:BB:CC:DD:EE:FF",
6,
pos,
)
} else {
ns.AddVirtualNode(
fmt.Sprintf("node-%d", i),
fmt.Sprintf("Node %d", i+1),
pos,
)
ns.nodes[i].Role = role
}
}
return ns
}