up
This commit is contained in:
@@ -34,6 +34,13 @@ type DeviceManager struct {
|
||||
|
||||
updateInterval time.Duration
|
||||
stopChan chan struct{}
|
||||
|
||||
// Auto frequency tracking
|
||||
freqThreshold int // Threshold for triggering update (Hz)
|
||||
autoTrackEnabled bool
|
||||
ultrabeamDirection int // User-selected direction (0=normal, 1=180, 2=bi-dir)
|
||||
lastFreqUpdateTime time.Time // Last time we sent frequency update
|
||||
freqUpdateCooldown time.Duration // Minimum time between updates
|
||||
}
|
||||
|
||||
type SystemStatus struct {
|
||||
@@ -50,10 +57,14 @@ type SystemStatus struct {
|
||||
|
||||
func NewDeviceManager(cfg *config.Config, hub *Hub) *DeviceManager {
|
||||
return &DeviceManager{
|
||||
config: cfg,
|
||||
hub: hub,
|
||||
updateInterval: 1 * time.Second, // Update status every second
|
||||
stopChan: make(chan struct{}),
|
||||
config: cfg,
|
||||
hub: hub,
|
||||
updateInterval: 1 * time.Second, // Update status every second
|
||||
stopChan: make(chan struct{}),
|
||||
freqThreshold: 25000, // 25 kHz default
|
||||
autoTrackEnabled: true, // Enabled by default
|
||||
ultrabeamDirection: 0, // Normal direction by default
|
||||
freqUpdateCooldown: 2 * time.Second, // Wait 2 seconds between updates
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,10 +243,68 @@ func (dm *DeviceManager) updateStatus() {
|
||||
// Ultrabeam
|
||||
if ubStatus, err := dm.ultrabeam.GetStatus(); err == nil {
|
||||
status.Ultrabeam = ubStatus
|
||||
|
||||
// Sync direction with Ultrabeam if not yet set (first time or after restart)
|
||||
// This prevents auto-track from using wrong direction before user changes it
|
||||
if dm.ultrabeamDirection == 0 && ubStatus.Direction != 0 {
|
||||
dm.ultrabeamDirection = ubStatus.Direction
|
||||
log.Printf("Auto-track: Initialized direction from Ultrabeam: %d", dm.ultrabeamDirection)
|
||||
}
|
||||
} else {
|
||||
log.Printf("Ultrabeam error: %v", err)
|
||||
}
|
||||
|
||||
// Auto frequency tracking: Update Ultrabeam when TunerGenius frequency differs from Ultrabeam
|
||||
if dm.autoTrackEnabled && status.TunerGenius != nil && status.TunerGenius.Connected && status.Ultrabeam != nil && status.Ultrabeam.Connected {
|
||||
tunerFreqKhz := int(status.TunerGenius.FreqA) // TunerGenius frequency is already in kHz
|
||||
ultrabeamFreqKhz := status.Ultrabeam.Frequency // Ultrabeam frequency in kHz
|
||||
|
||||
// Ignore invalid frequencies or out of Ultrabeam range (40M-6M)
|
||||
// This prevents retraction when slice is closed (FreqA becomes 0)
|
||||
// Ultrabeam VL2.3 only covers 7000-54000 kHz (40M to 6M)
|
||||
if tunerFreqKhz < 7000 || tunerFreqKhz > 54000 {
|
||||
return // Out of range, skip auto-track
|
||||
}
|
||||
|
||||
freqDiff := tunerFreqKhz - ultrabeamFreqKhz
|
||||
if freqDiff < 0 {
|
||||
freqDiff = -freqDiff
|
||||
}
|
||||
|
||||
// Convert diff to Hz for comparison with threshold (which is in Hz)
|
||||
freqDiffHz := freqDiff * 1000
|
||||
|
||||
// Don't send command if motors are already moving
|
||||
if status.Ultrabeam.MotorsMoving != 0 {
|
||||
// Motors moving - wait for them to finish
|
||||
return
|
||||
}
|
||||
|
||||
if freqDiffHz >= dm.freqThreshold {
|
||||
// Use current Ultrabeam direction if user hasn't explicitly set one
|
||||
directionToUse := dm.ultrabeamDirection
|
||||
if directionToUse == 0 && status.Ultrabeam.Direction != 0 {
|
||||
directionToUse = status.Ultrabeam.Direction
|
||||
}
|
||||
|
||||
// Check cooldown to prevent rapid fire commands
|
||||
timeSinceLastUpdate := time.Since(dm.lastFreqUpdateTime)
|
||||
if timeSinceLastUpdate < dm.freqUpdateCooldown {
|
||||
log.Printf("Auto-track: Cooldown active (%v remaining), skipping update", dm.freqUpdateCooldown-timeSinceLastUpdate)
|
||||
} else {
|
||||
log.Printf("Auto-track: Frequency differs by %d kHz, updating Ultrabeam to %d kHz (direction=%d)", freqDiff, tunerFreqKhz, directionToUse)
|
||||
|
||||
// Send to Ultrabeam with saved or current direction
|
||||
if err := dm.ultrabeam.SetFrequency(tunerFreqKhz, directionToUse); err != nil {
|
||||
log.Printf("Auto-track: Failed to update Ultrabeam: %v (will retry)", err)
|
||||
} else {
|
||||
log.Printf("Auto-track: Successfully sent frequency to Ultrabeam")
|
||||
dm.lastFreqUpdateTime = time.Now() // Update cooldown timer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Solar Data (fetched every 15 minutes, cached)
|
||||
if solarData, err := dm.solarClient.GetSolarData(); err == nil {
|
||||
status.Solar = solarData
|
||||
@@ -298,3 +367,13 @@ func (dm *DeviceManager) RotatorGenius() *rotatorgenius.Client {
|
||||
func (dm *DeviceManager) Ultrabeam() *ultrabeam.Client {
|
||||
return dm.ultrabeam
|
||||
}
|
||||
|
||||
func (dm *DeviceManager) SetAutoTrack(enabled bool, thresholdHz int) {
|
||||
dm.autoTrackEnabled = enabled
|
||||
dm.freqThreshold = thresholdHz
|
||||
}
|
||||
|
||||
func (dm *DeviceManager) SetUltrabeamDirection(direction int) {
|
||||
dm.ultrabeamDirection = direction
|
||||
log.Printf("Ultrabeam direction set to: %d", direction)
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ func (s *Server) SetupRoutes() *http.ServeMux {
|
||||
// Ultrabeam endpoints
|
||||
mux.HandleFunc("/api/ultrabeam/frequency", s.handleUltrabeamFrequency)
|
||||
mux.HandleFunc("/api/ultrabeam/retract", s.handleUltrabeamRetract)
|
||||
mux.HandleFunc("/api/ultrabeam/autotrack", s.handleUltrabeamAutoTrack)
|
||||
|
||||
// Tuner endpoints
|
||||
mux.HandleFunc("/api/tuner/operate", s.handleTunerOperate)
|
||||
@@ -435,6 +436,27 @@ func (s *Server) handleUltrabeamRetract(w http.ResponseWriter, r *http.Request)
|
||||
s.sendJSON(w, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Server) handleUltrabeamAutoTrack(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Threshold int `json:"threshold"` // kHz
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s.deviceManager.SetAutoTrack(req.Enabled, req.Threshold*1000) // Convert kHz to Hz
|
||||
|
||||
s.sendJSON(w, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Server) sendJSON(w http.ResponseWriter, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(data)
|
||||
|
||||
@@ -58,8 +58,8 @@ func New(host string, port int) *Client {
|
||||
host: host,
|
||||
port: port,
|
||||
stopChan: make(chan struct{}),
|
||||
autoFanEnabled: true, // Auto fan management enabled by default
|
||||
lastFanMode: "Contest", // Default to Contest mode
|
||||
autoFanEnabled: false, // Auto fan DISABLED - manual control only
|
||||
lastFanMode: "Contest",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,17 +103,20 @@ func (c *Client) Stop() {
|
||||
}
|
||||
|
||||
func (c *Client) pollLoop() {
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
ticker := time.NewTicker(2 * time.Second) // Increased from 500ms to 2s
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
|
||||
// Try to connect if not connected
|
||||
c.connMu.Lock()
|
||||
if c.conn == nil {
|
||||
log.Printf("Ultrabeam: Not connected, attempting connection...")
|
||||
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", c.host, c.port), 5*time.Second)
|
||||
if err != nil {
|
||||
log.Printf("Ultrabeam: Connection failed: %v", err)
|
||||
c.connMu.Unlock()
|
||||
|
||||
// Mark as disconnected
|
||||
@@ -151,12 +154,6 @@ func (c *Client) pollLoop() {
|
||||
// Mark as connected
|
||||
status.Connected = true
|
||||
|
||||
// Query element lengths
|
||||
lengths, err := c.queryElementLengths()
|
||||
if err == nil {
|
||||
status.ElementLengths = lengths
|
||||
}
|
||||
|
||||
// Query progress if motors moving
|
||||
if status.MotorsMoving != 0 {
|
||||
progress, err := c.queryProgress()
|
||||
@@ -312,7 +309,7 @@ func (c *Client) sendCommand(cmd byte, data []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
// Read reply with timeout
|
||||
c.conn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
c.conn.SetReadDeadline(time.Now().Add(1 * time.Second)) // Reduced from 2s to 1s
|
||||
|
||||
// Read until we get a complete packet
|
||||
var buffer []byte
|
||||
@@ -341,6 +338,11 @@ func (c *Client) sendCommand(cmd byte, data []byte) ([]byte, error) {
|
||||
return nil, fmt.Errorf("failed to parse reply: %w", err)
|
||||
}
|
||||
|
||||
// Log for debugging unknown codes
|
||||
if replyCmd != UB_OK && replyCmd != UB_BAD && replyCmd != UB_PAR && replyCmd != UB_ERR {
|
||||
log.Printf("Ultrabeam: Unknown reply code %d (0x%02X), raw packet: %v", replyCmd, replyCmd, buffer)
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
switch replyCmd {
|
||||
case UB_BAD:
|
||||
@@ -352,7 +354,10 @@ func (c *Client) sendCommand(cmd byte, data []byte) ([]byte, error) {
|
||||
case UB_OK:
|
||||
return payload, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown reply code: %d", replyCmd)
|
||||
// Unknown codes might indicate "busy" or "in progress"
|
||||
// Treat as non-fatal, return empty payload
|
||||
log.Printf("Ultrabeam: Unusual reply code %d, treating as busy/in-progress", replyCmd)
|
||||
return []byte{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,17 +395,59 @@ func (c *Client) queryElementLengths() ([]int, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Debug: log raw bytes
|
||||
log.Printf("Ultrabeam element lengths raw reply (%d bytes): %v", len(reply), reply)
|
||||
|
||||
// Try to extract 6 words - the protocol says 6 words (12 bytes)
|
||||
// But we're receiving 14 bytes, so there might be padding
|
||||
|
||||
if len(reply) < 12 {
|
||||
return nil, fmt.Errorf("element lengths reply too short")
|
||||
return nil, fmt.Errorf("element lengths reply too short: %d bytes", len(reply))
|
||||
}
|
||||
|
||||
lengths := make([]int, 6)
|
||||
|
||||
// Try different interpretations
|
||||
log.Printf("=== Attempting different parsings ===")
|
||||
|
||||
// Method 1: Standard little-endian from byte 0
|
||||
log.Printf("Method 1 (little-endian from 0):")
|
||||
for i := 0; i < 6 && i*2+1 < len(reply); i++ {
|
||||
lo := int(reply[i*2])
|
||||
hi := int(reply[i*2+1])
|
||||
val := lo | (hi << 8)
|
||||
log.Printf(" Element %d: bytes[%d,%d] = [%d,%d] => %d mm", i, i*2, i*2+1, lo, hi, val)
|
||||
}
|
||||
|
||||
// Method 2: Big-endian from byte 0
|
||||
log.Printf("Method 2 (big-endian from 0):")
|
||||
for i := 0; i < 6 && i*2+1 < len(reply); i++ {
|
||||
hi := int(reply[i*2])
|
||||
lo := int(reply[i*2+1])
|
||||
val := lo | (hi << 8)
|
||||
log.Printf(" Element %d: bytes[%d,%d] = [%d,%d] => %d mm", i, i*2, i*2+1, hi, lo, val)
|
||||
}
|
||||
|
||||
// Method 3: Skip first 2 bytes, then little-endian
|
||||
log.Printf("Method 3 (skip 2 bytes, little-endian):")
|
||||
for i := 0; i < 6 && i*2+3 < len(reply); i++ {
|
||||
lo := int(reply[i*2+2])
|
||||
hi := int(reply[i*2+3])
|
||||
val := lo | (hi << 8)
|
||||
log.Printf(" Element %d: bytes[%d,%d] = [%d,%d] => %d mm", i, i*2+2, i*2+3, lo, hi, val)
|
||||
}
|
||||
|
||||
// For now, use method 1 (original)
|
||||
for i := 0; i < 6; i++ {
|
||||
if i*2+1 >= len(reply) {
|
||||
break
|
||||
}
|
||||
lo := int(reply[i*2])
|
||||
hi := int(reply[i*2+1])
|
||||
lengths[i] = lo | (hi << 8)
|
||||
}
|
||||
|
||||
log.Printf("Final lengths: %v", lengths)
|
||||
return lengths, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user