spaxel/mothership/internal/simulator/node.go
jedarden 71a7af2102 fix(s simulator): fix bugs in node.go
- Fix NewNode() -> NewNodeSet() in SuggestedNodes()
- Fix CornerPositions() to use minZ instead of minX for height calculation
2026-04-09 12:42:53 -04:00

268 lines
6.7 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,
}
}
nodeType := NodeTypeVirtual
role := RoleTXRX
// Last node can be AP for passive radar
if i == count-1 {
nodeType = NodeTypeAP
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
}