diff --git a/internal/api/device_manager.go b/internal/api/device_manager.go index fd53fa0..ec35333 100644 --- a/internal/api/device_manager.go +++ b/internal/api/device_manager.go @@ -79,7 +79,10 @@ func (dm *DeviceManager) Initialize() error { ) // Initialize Rotator Genius +<<<<<<< HEAD log.Printf("Initializing RotatorGenius: host=%s port=%d", dm.config.Devices.RotatorGenius.Host, dm.config.Devices.RotatorGenius.Port) +======= +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 dm.rotatorGenius = rotatorgenius.New( dm.config.Devices.RotatorGenius.Host, dm.config.Devices.RotatorGenius.Port, @@ -95,6 +98,7 @@ func (dm *DeviceManager) Initialize() error { dm.config.Location.Longitude, ) +<<<<<<< HEAD // Start device polling in background (non-blocking) go func() { if err := dm.powerGenius.Start(); err != nil { @@ -122,6 +126,12 @@ func (dm *DeviceManager) Initialize() error { } }() log.Println("RotatorGenius goroutine launched") +======= + // Start PowerGenius continuous polling + if err := dm.powerGenius.Start(); err != nil { + log.Printf("Warning: Failed to start PowerGenius polling: %v", err) + } +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 log.Println("Device manager initialized") return nil @@ -186,6 +196,7 @@ func (dm *DeviceManager) updateStatus() { log.Printf("Power Genius error: %v", err) } +<<<<<<< HEAD // Tuner Genius if tgStatus, err := dm.tunerGenius.GetStatus(); err == nil { status.TunerGenius = tgStatus @@ -206,6 +217,28 @@ func (dm *DeviceManager) updateStatus() { } else { log.Printf("Rotator Genius error: %v", err) } +======= + // // Tuner Genius + // if tgStatus, err := dm.tunerGenius.GetStatus(); err == nil { + // status.TunerGenius = tgStatus + // } else { + // log.Printf("Tuner Genius error: %v", err) + // } + + // // Antenna Genius + // if agStatus, err := dm.antennaGenius.GetStatus(); err == nil { + // status.AntennaGenius = agStatus + // } else { + // log.Printf("Antenna Genius error: %v", err) + // } + + // // Rotator Genius + // if rgStatus, err := dm.rotatorGenius.GetStatus(); err == nil { + // status.RotatorGenius = rgStatus + // } else { + // log.Printf("Rotator Genius error: %v", err) + // } +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 // Solar Data (fetched every 15 minutes, cached) if solarData, err := dm.solarClient.GetSolarData(); err == nil { diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 479cd22..1e1ef91 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -49,13 +49,18 @@ func (s *Server) SetupRoutes() *http.ServeMux { mux.HandleFunc("/api/webswitch/all/off", s.handleWebSwitchAllOff) // Rotator endpoints +<<<<<<< HEAD mux.HandleFunc("/api/rotator/heading", s.handleRotatorHeading) +======= + mux.HandleFunc("/api/rotator/move", s.handleRotatorMove) +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 mux.HandleFunc("/api/rotator/cw", s.handleRotatorCW) mux.HandleFunc("/api/rotator/ccw", s.handleRotatorCCW) mux.HandleFunc("/api/rotator/stop", s.handleRotatorStop) // Tuner endpoints mux.HandleFunc("/api/tuner/operate", s.handleTunerOperate) +<<<<<<< HEAD mux.HandleFunc("/api/tuner/bypass", s.handleTunerBypass) mux.HandleFunc("/api/tuner/autotune", s.handleTunerAutoTune) @@ -66,6 +71,16 @@ func (s *Server) SetupRoutes() *http.ServeMux { // Power Genius endpoints mux.HandleFunc("/api/power/fanmode", s.handlePowerFanMode) mux.HandleFunc("/api/power/operate", s.handlePowerOperate) +======= + mux.HandleFunc("/api/tuner/tune", s.handleTunerAutoTune) + mux.HandleFunc("/api/tuner/antenna", s.handleTunerAntenna) + + // Antenna Genius endpoints + mux.HandleFunc("/api/antenna/set", s.handleAntennaSet) + + // Power Genius endpoints + mux.HandleFunc("/api/power/fanmode", s.handlePowerFanMode) +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 // Static files (will be frontend) mux.Handle("/", http.FileServer(http.Dir("./web/dist"))) @@ -181,14 +196,23 @@ func (s *Server) handleWebSwitchAllOff(w http.ResponseWriter, r *http.Request) { } // Rotator handlers +<<<<<<< HEAD func (s *Server) handleRotatorHeading(w http.ResponseWriter, r *http.Request) { +======= +func (s *Server) handleRotatorMove(w http.ResponseWriter, r *http.Request) { +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req struct { +<<<<<<< HEAD Heading int `json:"heading"` +======= + Rotator int `json:"rotator"` + Azimuth int `json:"azimuth"` +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -196,7 +220,11 @@ func (s *Server) handleRotatorHeading(w http.ResponseWriter, r *http.Request) { return } +<<<<<<< HEAD if err := s.deviceManager.RotatorGenius().SetHeading(req.Heading); err != nil { +======= + if err := s.deviceManager.RotatorGenius().MoveToAzimuth(req.Rotator, req.Azimuth); err != nil { +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -210,7 +238,17 @@ func (s *Server) handleRotatorCW(w http.ResponseWriter, r *http.Request) { return } +<<<<<<< HEAD if err := s.deviceManager.RotatorGenius().RotateCW(); err != nil { +======= + rotator, err := strconv.Atoi(r.URL.Query().Get("rotator")) + if err != nil || rotator < 1 || rotator > 2 { + http.Error(w, "Invalid rotator number", http.StatusBadRequest) + return + } + + if err := s.deviceManager.RotatorGenius().RotateCW(rotator); err != nil { +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -224,7 +262,17 @@ func (s *Server) handleRotatorCCW(w http.ResponseWriter, r *http.Request) { return } +<<<<<<< HEAD if err := s.deviceManager.RotatorGenius().RotateCCW(); err != nil { +======= + rotator, err := strconv.Atoi(r.URL.Query().Get("rotator")) + if err != nil || rotator < 1 || rotator > 2 { + http.Error(w, "Invalid rotator number", http.StatusBadRequest) + return + } + + if err := s.deviceManager.RotatorGenius().RotateCCW(rotator); err != nil { +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -254,7 +302,11 @@ func (s *Server) handleTunerOperate(w http.ResponseWriter, r *http.Request) { } var req struct { +<<<<<<< HEAD Value int `json:"value"` +======= + Operate bool `json:"operate"` +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -262,6 +314,7 @@ func (s *Server) handleTunerOperate(w http.ResponseWriter, r *http.Request) { return } +<<<<<<< HEAD if err := s.deviceManager.TunerGenius().SetOperate(req.Value); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -286,6 +339,9 @@ func (s *Server) handleTunerBypass(w http.ResponseWriter, r *http.Request) { } if err := s.deviceManager.TunerGenius().SetBypass(req.Value); err != nil { +======= + if err := s.deviceManager.TunerGenius().SetOperate(req.Operate); err != nil { +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -307,15 +363,22 @@ func (s *Server) handleTunerAutoTune(w http.ResponseWriter, r *http.Request) { s.sendJSON(w, map[string]string{"status": "ok"}) } +<<<<<<< HEAD // Antenna Genius handlers func (s *Server) handleAntennaSelect(w http.ResponseWriter, r *http.Request) { +======= +func (s *Server) handleTunerAntenna(w http.ResponseWriter, r *http.Request) { +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req struct { +<<<<<<< HEAD Port int `json:"port"` +======= +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 Antenna int `json:"antenna"` } @@ -324,7 +387,11 @@ func (s *Server) handleAntennaSelect(w http.ResponseWriter, r *http.Request) { return } +<<<<<<< HEAD if err := s.deviceManager.AntennaGenius().SetAntenna(req.Port, req.Antenna); err != nil { +======= + if err := s.deviceManager.TunerGenius().ActivateAntenna(req.Antenna); err != nil { +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -332,13 +399,32 @@ func (s *Server) handleAntennaSelect(w http.ResponseWriter, r *http.Request) { s.sendJSON(w, map[string]string{"status": "ok"}) } +<<<<<<< HEAD func (s *Server) handleAntennaReboot(w http.ResponseWriter, r *http.Request) { +======= +// Antenna Genius handlers +func (s *Server) handleAntennaSet(w http.ResponseWriter, r *http.Request) { +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } +<<<<<<< HEAD if err := s.deviceManager.AntennaGenius().Reboot(); err != nil { +======= + var req struct { + Radio int `json:"radio"` + Antenna int `json:"antenna"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if err := s.deviceManager.AntennaGenius().SetRadioAntenna(req.Radio, req.Antenna); err != nil { +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -370,6 +456,7 @@ func (s *Server) handlePowerFanMode(w http.ResponseWriter, r *http.Request) { s.sendJSON(w, map[string]string{"status": "ok"}) } +<<<<<<< HEAD func (s *Server) handlePowerOperate(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) @@ -393,6 +480,8 @@ func (s *Server) handlePowerOperate(w http.ResponseWriter, r *http.Request) { s.sendJSON(w, map[string]string{"status": "ok"}) } +======= +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 func (s *Server) sendJSON(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(data) diff --git a/internal/devices/antennagenius/antennagenius.go b/internal/devices/antennagenius/antennagenius.go index 2fc21c1..de6dc0d 100644 --- a/internal/devices/antennagenius/antennagenius.go +++ b/internal/devices/antennagenius/antennagenius.go @@ -3,6 +3,7 @@ package antennagenius import ( "bufio" "fmt" +<<<<<<< HEAD "log" "net" "strconv" @@ -52,17 +53,43 @@ type Antenna struct { RX string `json:"rx"` InBand string `json:"in_band"` Hotkey int `json:"hotkey"` +======= + "net" + "strconv" + "strings" + "time" + + . "git.rouggy.com/rouggy/ShackMaster/internal/devices" +) + +type Client struct { + host string + port int + conn net.Conn +} + +type Status struct { + Radio1Antenna int `json:"radio1_antenna"` // 0-7 (antenna index) + Radio2Antenna int `json:"radio2_antenna"` // 0-7 (antenna index) + Connected bool `json:"connected"` +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } func New(host string, port int) *Client { return &Client{ +<<<<<<< HEAD host: host, port: port, stopChan: make(chan struct{}), +======= + host: host, + port: port, +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } } func (c *Client) Connect() error { +<<<<<<< HEAD c.connMu.Lock() defer c.connMu.Unlock() @@ -70,20 +97,26 @@ func (c *Client) Connect() error { return nil } +======= +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", c.host, c.port), 5*time.Second) if err != nil { return fmt.Errorf("failed to connect: %w", err) } c.conn = conn +<<<<<<< HEAD c.reader = bufio.NewReader(c.conn) // Read and discard banner _, _ = c.reader.ReadString('\n') +======= +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 return nil } func (c *Client) Close() error { +<<<<<<< HEAD c.connMu.Lock() defer c.connMu.Unlock() @@ -91,12 +124,15 @@ func (c *Client) Close() error { close(c.stopChan) } +======= +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 if c.conn != nil { return c.conn.Close() } return nil } +<<<<<<< HEAD func (c *Client) Start() error { if c.running { return nil @@ -263,10 +299,46 @@ func (c *Client) sendCommand(cmd string) (string, error) { func (c *Client) getAntennaList() ([]Antenna, error) { resp, err := c.sendCommand("antenna list") +======= +func (c *Client) sendCommand(cmd string) (string, error) { + if c.conn == nil { + if err := c.Connect(); err != nil { + return "", err + } + } + + // Get next command ID from global counter + cmdID := GetGlobalCommandID().GetNextID() + + // Format command with ID: C| + fullCmd := fmt.Sprintf("C%d|%s\n", cmdID, cmd) + + // Send command + _, err := c.conn.Write([]byte(fullCmd)) + if err != nil { + c.conn = nil + return "", fmt.Errorf("failed to send command: %w", err) + } + + // Read response + reader := bufio.NewReader(c.conn) + response, err := reader.ReadString('\n') + if err != nil { + c.conn = nil + return "", fmt.Errorf("failed to read response: %w", err) + } + + return strings.TrimSpace(response), nil +} + +func (c *Client) GetStatus() (*Status, error) { + resp, err := c.sendCommand("status") +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 if err != nil { return nil, err } +<<<<<<< HEAD var antennas []Antenna // Response format: R|0|antenna name= tx= rx= inband= hotkey= @@ -333,10 +405,58 @@ func (c *Client) parseAntennaLine(line string) Antenna { func (c *Client) subscribeToPortUpdates() error { resp, err := c.sendCommand("sub port all") +======= + return c.parseStatus(resp) +} + +func (c *Client) parseStatus(resp string) (*Status, error) { + status := &Status{ + Connected: true, + } + + // Parse response format from 4O3A API + // Expected format will vary - this is a basic parser + pairs := strings.Fields(resp) + + for _, pair := range pairs { + parts := strings.SplitN(pair, "=", 2) + if len(parts) != 2 { + continue + } + + key := parts[0] + value := parts[1] + + switch key { + case "radio1", "r1": + status.Radio1Antenna, _ = strconv.Atoi(value) + case "radio2", "r2": + status.Radio2Antenna, _ = strconv.Atoi(value) + } + } + + return status, nil +} + +// SetRadioAntenna sets which antenna a radio should use +// radio: 1 or 2 +// antenna: 0-7 (antenna index) +func (c *Client) SetRadioAntenna(radio int, antenna int) error { + if radio < 1 || radio > 2 { + return fmt.Errorf("radio must be 1 or 2") + } + if antenna < 0 || antenna > 7 { + return fmt.Errorf("antenna must be between 0 and 7") + } + + cmd := fmt.Sprintf("set radio%d=%d", radio, antenna) + resp, err := c.sendCommand(cmd) +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 if err != nil { return err } +<<<<<<< HEAD // Parse initial port status from subscription response // The response may contain S0|port messages with current status lines := strings.Split(resp, "\n") @@ -345,11 +465,18 @@ func (c *Client) subscribeToPortUpdates() error { if strings.HasPrefix(line, "S0|port") { c.parsePortStatus(line) } +======= + // Check response for success + if !strings.Contains(strings.ToLower(resp), "ok") && resp != "" { + // If response doesn't contain "ok" but isn't empty, assume success + // (some devices may return the new state instead of "ok") +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } return nil } +<<<<<<< HEAD func (c *Client) parsePortStatus(line string) { // Format: S0|port auto=<0|1> source= band= freq= nickname= rxant= txant= inband= tx=<0|1> inhibit= @@ -434,4 +561,21 @@ func (c *Client) SetAntenna(port, antenna int) error { func (c *Client) Reboot() error { _, err := c.sendCommand("reboot") return err +======= +// GetRadioAntenna gets which antenna a radio is currently using +func (c *Client) GetRadioAntenna(radio int) (int, error) { + if radio < 1 || radio > 2 { + return -1, fmt.Errorf("radio must be 1 or 2") + } + + status, err := c.GetStatus() + if err != nil { + return -1, err + } + + if radio == 1 { + return status.Radio1Antenna, nil + } + return status.Radio2Antenna, nil +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } diff --git a/internal/devices/powergenius/powergenius.go b/internal/devices/powergenius/powergenius.go index fbaa72c..a42f212 100644 --- a/internal/devices/powergenius/powergenius.go +++ b/internal/devices/powergenius/powergenius.go @@ -3,6 +3,10 @@ package powergenius import ( "bufio" "fmt" +<<<<<<< HEAD +======= + "log" +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 "math" "net" "strconv" @@ -41,6 +45,10 @@ type Status struct { BandB string `json:"band_b"` FaultPresent bool `json:"fault_present"` Connected bool `json:"connected"` +<<<<<<< HEAD +======= + Meffa string `json:"meffa"` +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } func New(host string, port int) *Client { @@ -88,14 +96,24 @@ func (c *Client) Close() error { // Start begins continuous polling of the device func (c *Client) Start() error { +<<<<<<< HEAD +======= + if err := c.Connect(); err != nil { + return err + } + +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 if c.running { return nil } +<<<<<<< HEAD // Try to connect, but don't fail if it doesn't work // The poll loop will keep trying _ = c.Connect() +======= +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 c.running = true go c.pollLoop() @@ -110,6 +128,7 @@ func (c *Client) pollLoop() { for { select { case <-ticker.C: +<<<<<<< HEAD // Try to reconnect if not connected c.connMu.Lock() if c.conn == nil { @@ -133,6 +152,12 @@ func (c *Client) pollLoop() { status, err := c.queryStatus() if err != nil { // Connection lost, close and retry next tick +======= + status, err := c.queryStatus() + if err != nil { + log.Printf("PowerGenius query error: %v", err) + // Try to reconnect +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 c.connMu.Lock() if c.conn != nil { c.conn.Close() @@ -140,6 +165,7 @@ func (c *Client) pollLoop() { } c.connMu.Unlock() +<<<<<<< HEAD // Mark as disconnected and reset all values c.statusMu.Lock() c.lastStatus = &Status{ @@ -152,6 +178,14 @@ func (c *Client) pollLoop() { // Mark as connected status.Connected = true +======= + if err := c.Connect(); err != nil { + log.Printf("PowerGenius reconnect failed: %v", err) + } + continue + } + +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 // Merge with existing status (spontaneous messages may only update some fields) c.statusMu.Lock() if c.lastStatus != nil { @@ -327,6 +361,11 @@ func (c *Client) parseStatus(resp string) (*Status, error) { } case "vac": status.Voltage, _ = strconv.ParseFloat(value, 64) +<<<<<<< HEAD +======= + case "meffa": + status.Meffa = value +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 case "vdd": status.VDD, _ = strconv.ParseFloat(value, 64) case "id": @@ -399,6 +438,7 @@ func (c *Client) SetFanMode(mode string) error { _, err := c.sendCommand(cmd) return err } +<<<<<<< HEAD // SetOperate sets the operate mode // value can be: 0 (STANDBY) or 1 (OPERATE) @@ -411,3 +451,5 @@ func (c *Client) SetOperate(value int) error { _, err := c.sendCommand(cmd) return err } +======= +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 diff --git a/internal/devices/rotatorgenius/rotatorgenius.go b/internal/devices/rotatorgenius/rotatorgenius.go index ee98fc0..536bed3 100644 --- a/internal/devices/rotatorgenius/rotatorgenius.go +++ b/internal/devices/rotatorgenius/rotatorgenius.go @@ -8,6 +8,8 @@ import ( "strings" "sync" "time" + + . "git.rouggy.com/rouggy/ShackMaster/internal/devices" ) type Client struct { @@ -149,7 +151,18 @@ func (c *Client) sendCommand(cmd string) error { return fmt.Errorf("not connected") } +<<<<<<< HEAD _, err := c.conn.Write([]byte(cmd)) +======= + // Get next command ID from global counter + cmdID := GetGlobalCommandID().GetNextID() + + // Format command with ID: C| + fullCmd := fmt.Sprintf("C%d%s", cmdID, cmd) + + // Send command + _, err := c.conn.Write([]byte(fullCmd)) +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 if err != nil { c.conn = nil c.reader = nil diff --git a/internal/devices/tunergenius/tunergenius.go b/internal/devices/tunergenius/tunergenius.go index bb2b111..77dee31 100644 --- a/internal/devices/tunergenius/tunergenius.go +++ b/internal/devices/tunergenius/tunergenius.go @@ -14,6 +14,7 @@ import ( ) type Client struct { +<<<<<<< HEAD host string port int conn net.Conn @@ -22,6 +23,11 @@ type Client struct { statusMu sync.RWMutex stopChan chan struct{} running bool +======= + host string + port int + conn net.Conn +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } type Status struct { @@ -52,9 +58,14 @@ type Status struct { func New(host string, port int) *Client { return &Client{ +<<<<<<< HEAD host: host, port: port, stopChan: make(chan struct{}), +======= + host: host, + port: port, +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } } @@ -99,6 +110,7 @@ func (c *Client) Start() error { return nil } +<<<<<<< HEAD // Try to connect, but don't fail if it doesn't work // The poll loop will keep trying _ = c.Connect() @@ -209,6 +221,8 @@ func (c *Client) sendCommand(cmd string) (string, error) { return "", fmt.Errorf("not connected") } +======= +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 // Get next command ID from global counter cmdID := GetGlobalCommandID().GetNextID() diff --git a/internal/devices/webswitch/webswitch.go b/internal/devices/webswitch/webswitch.go index dcce155..40d205b 100644 --- a/internal/devices/webswitch/webswitch.go +++ b/internal/devices/webswitch/webswitch.go @@ -142,8 +142,12 @@ func (c *Client) GetStatus() (*Status, error) { // Parse response format: "1,1\n2,1\n3,1\n4,1\n5,0\n" status := &Status{ +<<<<<<< HEAD Relays: make([]RelayState, 0, 5), Connected: true, +======= + Relays: make([]RelayState, 0, 5), +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } lines := strings.Split(strings.TrimSpace(string(body)), "\n") diff --git a/web/index.html b/web/index.html index d89e88a..fc899f8 100644 --- a/web/index.html +++ b/web/index.html @@ -3,7 +3,11 @@ +<<<<<<< HEAD ShackMaster - F4BPO Shack +======= + ShackMaster - XV9Q Shack +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 diff --git a/web/src/app.css b/web/src/app.css index c797f93..d321249 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -1,4 +1,5 @@ :root { +<<<<<<< HEAD /* Modern dark theme inspired by FlexDXCluster */ --bg-primary: #0a1628; --bg-secondary: #1a2332; @@ -435,4 +436,132 @@ select:focus { order: 3; width: 100%; } +======= + --bg-primary: #1a1a1a; + --bg-secondary: #2a2a2a; + --bg-card: #333333; + --text-primary: #ffffff; + --text-secondary: #b0b0b0; + --accent-teal: #00bcd4; + --accent-green: #4caf50; + --accent-red: #f44336; + --accent-blue: #2196f3; + --border-color: #444444; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + background-color: var(--bg-primary); + color: var(--text-primary); + overflow-x: hidden; +} + +#app { + min-height: 100vh; +} + +button { + font-family: inherit; + cursor: pointer; + border: none; + outline: none; + transition: all 0.2s ease; +} + +button:hover { + transform: translateY(-1px); +} + +button:active { + transform: translateY(0); +} + +input, select { + font-family: inherit; + outline: none; +} + +.card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 16px; +} + +.status-indicator { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 50%; + margin-right: 8px; +} + +.status-online { + background-color: var(--accent-green); + box-shadow: 0 0 8px var(--accent-green); +} + +.status-offline { + background-color: var(--text-secondary); +} + +.btn { + padding: 12px 24px; + border-radius: 6px; + font-weight: 500; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 0.5px; + transition: all 0.2s ease; +} + +.btn-primary { + background: var(--accent-green); + color: white; +} + +.btn-primary:hover { + background: #45a049; +} + +.btn-danger { + background: var(--accent-red); + color: white; +} + +.btn-danger:hover { + background: #da190b; +} + +.btn-secondary { + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--border-color); +} + +.btn-secondary:hover { + background: var(--bg-card); +} + +.value-display { + font-size: 24px; + font-weight: 300; + color: var(--accent-teal); +} + +.label { + font-size: 12px; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 4px; +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } \ No newline at end of file diff --git a/web/src/components/AntennaGenius.svelte b/web/src/components/AntennaGenius.svelte index d9c7c2d..9565e57 100644 --- a/web/src/components/AntennaGenius.svelte +++ b/web/src/components/AntennaGenius.svelte @@ -1,5 +1,6 @@ +<<<<<<< HEAD

Antenna Genius

@@ -109,10 +125,53 @@ 🔄 REBOOT +======= +
+

+ AG 8X2 + +

+ +
+
Radio 1 / Radio 2
+ +
+
+
Radio 1
+
+ {#each Array(4) as _, i} + + {/each} +
+
+ +
+
Radio 2
+
+ {#each Array(4) as _, i} + + {/each} +
+
+
+>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7
\ No newline at end of file diff --git a/web/src/components/PowerGenius.svelte b/web/src/components/PowerGenius.svelte index 518f045..b314495 100644 --- a/web/src/components/PowerGenius.svelte +++ b/web/src/components/PowerGenius.svelte @@ -1,4 +1,5 @@ + +
+

+ PGXL + +

+ +
+
+ {displayState} +
+
+ +
+
+
FWD PWR (W)
+
{powerForward.toFixed(1)}
+
+
+
+
+ 0 + 1000 + 2000 +
+
+ +
+
PG XL SWR 1:1.00 use
+
{swr.toFixed(2)}
+
+ +
+
Temp / HL Temp
+
{temperature.toFixed(0)}°C / {harmonicLoadTemp.toFixed(1)}°C
+
+
+
+
+ 25 + 55 + 80 +
+
+ +
+
+
VAC
+
{voltage.toFixed(0)}
+
+
+
VDD
+
{vdd.toFixed(1)}
+
+
+
ID peak
+
{peakCurrent.toFixed(1)}
+
+
+ +
+
Fan Speed
+ +
+ +
+
Band A
+
{bandA}
+
+
+
Band B
+
{bandB}
+
+
+
+ + + + \ No newline at end of file diff --git a/web/src/components/RotatorGenius.svelte b/web/src/components/RotatorGenius.svelte index 7e407bd..1525d1e 100644 --- a/web/src/components/RotatorGenius.svelte +++ b/web/src/components/RotatorGenius.svelte @@ -1,4 +1,5 @@
@@ -423,5 +569,187 @@ .arrow { font-size: 20px; line-height: 1; +======= + + // Preset directions + const presets = [ + { name: 'EU-0', heading: 0 }, + { name: 'JA-35', heading: 35 }, + { name: 'AS-75', heading: 75 }, + { name: 'VK-120', heading: 120 }, + { name: 'AF-180', heading: 180 }, + { name: 'SA-230', heading: 230 }, + { name: 'WI-270', heading: 270 }, + { name: 'NA-300', heading: 300 } + ]; + + async function gotoPreset(heading) { + try { + await api.rotator.move(1, heading); + } catch (err) { + console.error('Failed to move to preset:', err); + } + } + + +
+

+ ROTATOR GENIUS + +

+ +
+ CURRENT HEADING: {currentHeading}° +
+ + {#if moving > 0} +
+ {moving === 1 ? '↻ ROTATING CW' : '↺ ROTATING CCW'} +
+ {/if} + + + +
+
+ + +
+ +
+ + + +
+
+ +
+ {#each presets as preset} + + {/each} +
+
+ + \ No newline at end of file diff --git a/web/src/components/TunerGenius.svelte b/web/src/components/TunerGenius.svelte index 5dd3f1f..571dc5a 100644 --- a/web/src/components/TunerGenius.svelte +++ b/web/src/components/TunerGenius.svelte @@ -1,5 +1,6 @@ +<<<<<<< HEAD

Tuner Genius XL

@@ -440,10 +484,218 @@ border-color: var(--accent-cyan); color: #000; box-shadow: 0 0 15px rgba(79, 195, 247, 0.5); +======= +
+

+ TGXL + +

+ +
+
Power 0.0w
+
1500
+
1650
+
+ +
+
+
TG XL SWR 1.00 use
+
+ +
+ + + +
+ +
+
+
{c1}
+
C1
+
+
+
{l}
+
L
+
+
+
{c2}
+
C2
+
+
+
+ +
+
+
Tuning Status
+
+ {tuningStatus} +
+
+
+ +
+
+
Frequency A
+
{(frequencyA / 1000).toFixed(3)}
+
+
+
Frequency B
+
{(frequencyB / 1000).toFixed(3)}
+
+
+ +
+ + +
+ + +
+ + \ No newline at end of file diff --git a/web/src/components/WebSwitch.svelte b/web/src/components/WebSwitch.svelte index 65e1506..296e1dd 100644 --- a/web/src/components/WebSwitch.svelte +++ b/web/src/components/WebSwitch.svelte @@ -4,7 +4,10 @@ export let status; $: relays = status?.relays || []; +<<<<<<< HEAD $: connected = status?.connected || false; +======= +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 const relayNames = { 1: 'Power Supply', @@ -52,6 +55,7 @@ } +<<<<<<< HEAD

WebSwitch

@@ -95,10 +99,40 @@ ALL OFF
+======= +
+

+ 1216RH + 0} class:status-offline={relays.length === 0}> +

+ +
+ {#each [1, 2, 3, 4, 5] as relayNum} + {@const relay = relays.find(r => r.number === relayNum)} + {@const isOn = relay?.state || false} +
+ {relayNames[relayNum]} + +
+ {/each} +
+ +
+ + +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7
\ No newline at end of file diff --git a/web/src/lib/api.js b/web/src/lib/api.js index 3bdcde9..56121cc 100644 --- a/web/src/lib/api.js +++ b/web/src/lib/api.js @@ -47,6 +47,7 @@ export const api = { // Tuner tuner: { +<<<<<<< HEAD setOperate: (value) => request('/tuner/operate', { method: 'POST', body: JSON.stringify({ value }), @@ -56,15 +57,33 @@ export const api = { body: JSON.stringify({ value }), }), autoTune: () => request('/tuner/autotune', { method: 'POST' }), +======= + operate: (operate) => request('/tuner/operate', { + method: 'POST', + body: JSON.stringify({ operate }), + }), + tune: () => request('/tuner/tune', { method: 'POST' }), + antenna: (antenna) => request('/tuner/antenna', { + method: 'POST', + body: JSON.stringify({ antenna }), + }), +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 }, // Antenna Genius antenna: { +<<<<<<< HEAD selectAntenna: (port, antenna) => request('/antenna/select', { method: 'POST', body: JSON.stringify({ port, antenna }), }), reboot: () => request('/antenna/reboot', { method: 'POST' }), +======= + set: (radio, antenna) => request('/antenna/set', { + method: 'POST', + body: JSON.stringify({ radio, antenna }), + }), +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 }, // Power Genius @@ -73,6 +92,7 @@ export const api = { method: 'POST', body: JSON.stringify({ mode }), }), +<<<<<<< HEAD setOperate: (value) => request('/power/operate', { method: 'POST', body: JSON.stringify({ value }), @@ -88,5 +108,7 @@ export const api = { rotateCW: () => request('/rotator/cw', { method: 'POST' }), rotateCCW: () => request('/rotator/ccw', { method: 'POST' }), stop: () => request('/rotator/stop', { method: 'POST' }), +======= +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 }, }; \ No newline at end of file diff --git a/web/src/lib/websocket.js b/web/src/lib/websocket.js index ea195e3..d56f99f 100644 --- a/web/src/lib/websocket.js +++ b/web/src/lib/websocket.js @@ -28,7 +28,10 @@ class WebSocketService { const message = JSON.parse(event.data); if (message.type === 'update') { +<<<<<<< HEAD console.log('System status updated:', message.data); +======= +>>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 systemStatus.set(message.data); lastUpdate.set(new Date(message.timestamp)); }