- 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>
266 lines
6.6 KiB
Go
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
|
|
}
|