up
This commit is contained in:
7
cmd/server/web/dist/assets/index-DIrlWzGj.js
vendored
Normal file
7
cmd/server/web/dist/assets/index-DIrlWzGj.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
cmd/server/web/dist/assets/index-DvnnYzjx.css
vendored
Normal file
1
cmd/server/web/dist/assets/index-DvnnYzjx.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
cmd/server/web/dist/index.html
vendored
4
cmd/server/web/dist/index.html
vendored
@@ -7,8 +7,8 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
<script type="module" crossorigin src="/assets/index-8_72Rq0c.js"></script>
|
<script type="module" crossorigin src="/assets/index-DIrlWzGj.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Ml--d1Bc.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DvnnYzjx.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ type DeviceManager struct {
|
|||||||
|
|
||||||
updateInterval time.Duration
|
updateInterval time.Duration
|
||||||
stopChan chan struct{}
|
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 {
|
type SystemStatus struct {
|
||||||
@@ -50,10 +57,14 @@ type SystemStatus struct {
|
|||||||
|
|
||||||
func NewDeviceManager(cfg *config.Config, hub *Hub) *DeviceManager {
|
func NewDeviceManager(cfg *config.Config, hub *Hub) *DeviceManager {
|
||||||
return &DeviceManager{
|
return &DeviceManager{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
hub: hub,
|
hub: hub,
|
||||||
updateInterval: 1 * time.Second, // Update status every second
|
updateInterval: 1 * time.Second, // Update status every second
|
||||||
stopChan: make(chan struct{}),
|
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
|
// Ultrabeam
|
||||||
if ubStatus, err := dm.ultrabeam.GetStatus(); err == nil {
|
if ubStatus, err := dm.ultrabeam.GetStatus(); err == nil {
|
||||||
status.Ultrabeam = ubStatus
|
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 {
|
} else {
|
||||||
log.Printf("Ultrabeam error: %v", err)
|
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)
|
// Solar Data (fetched every 15 minutes, cached)
|
||||||
if solarData, err := dm.solarClient.GetSolarData(); err == nil {
|
if solarData, err := dm.solarClient.GetSolarData(); err == nil {
|
||||||
status.Solar = solarData
|
status.Solar = solarData
|
||||||
@@ -298,3 +367,13 @@ func (dm *DeviceManager) RotatorGenius() *rotatorgenius.Client {
|
|||||||
func (dm *DeviceManager) Ultrabeam() *ultrabeam.Client {
|
func (dm *DeviceManager) Ultrabeam() *ultrabeam.Client {
|
||||||
return dm.ultrabeam
|
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
|
// Ultrabeam endpoints
|
||||||
mux.HandleFunc("/api/ultrabeam/frequency", s.handleUltrabeamFrequency)
|
mux.HandleFunc("/api/ultrabeam/frequency", s.handleUltrabeamFrequency)
|
||||||
mux.HandleFunc("/api/ultrabeam/retract", s.handleUltrabeamRetract)
|
mux.HandleFunc("/api/ultrabeam/retract", s.handleUltrabeamRetract)
|
||||||
|
mux.HandleFunc("/api/ultrabeam/autotrack", s.handleUltrabeamAutoTrack)
|
||||||
|
|
||||||
// Tuner endpoints
|
// Tuner endpoints
|
||||||
mux.HandleFunc("/api/tuner/operate", s.handleTunerOperate)
|
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"})
|
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{}) {
|
func (s *Server) sendJSON(w http.ResponseWriter, data interface{}) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(data)
|
json.NewEncoder(w).Encode(data)
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ func New(host string, port int) *Client {
|
|||||||
host: host,
|
host: host,
|
||||||
port: port,
|
port: port,
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
autoFanEnabled: true, // Auto fan management enabled by default
|
autoFanEnabled: false, // Auto fan DISABLED - manual control only
|
||||||
lastFanMode: "Contest", // Default to Contest mode
|
lastFanMode: "Contest",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,17 +103,20 @@ func (c *Client) Stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) pollLoop() {
|
func (c *Client) pollLoop() {
|
||||||
ticker := time.NewTicker(500 * time.Millisecond)
|
ticker := time.NewTicker(2 * time.Second) // Increased from 500ms to 2s
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
|
|
||||||
// Try to connect if not connected
|
// Try to connect if not connected
|
||||||
c.connMu.Lock()
|
c.connMu.Lock()
|
||||||
if c.conn == nil {
|
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)
|
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", c.host, c.port), 5*time.Second)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("Ultrabeam: Connection failed: %v", err)
|
||||||
c.connMu.Unlock()
|
c.connMu.Unlock()
|
||||||
|
|
||||||
// Mark as disconnected
|
// Mark as disconnected
|
||||||
@@ -151,12 +154,6 @@ func (c *Client) pollLoop() {
|
|||||||
// Mark as connected
|
// Mark as connected
|
||||||
status.Connected = true
|
status.Connected = true
|
||||||
|
|
||||||
// Query element lengths
|
|
||||||
lengths, err := c.queryElementLengths()
|
|
||||||
if err == nil {
|
|
||||||
status.ElementLengths = lengths
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query progress if motors moving
|
// Query progress if motors moving
|
||||||
if status.MotorsMoving != 0 {
|
if status.MotorsMoving != 0 {
|
||||||
progress, err := c.queryProgress()
|
progress, err := c.queryProgress()
|
||||||
@@ -312,7 +309,7 @@ func (c *Client) sendCommand(cmd byte, data []byte) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read reply with timeout
|
// 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
|
// Read until we get a complete packet
|
||||||
var buffer []byte
|
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)
|
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
|
// Check for errors
|
||||||
switch replyCmd {
|
switch replyCmd {
|
||||||
case UB_BAD:
|
case UB_BAD:
|
||||||
@@ -352,7 +354,10 @@ func (c *Client) sendCommand(cmd byte, data []byte) ([]byte, error) {
|
|||||||
case UB_OK:
|
case UB_OK:
|
||||||
return payload, nil
|
return payload, nil
|
||||||
default:
|
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
|
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 {
|
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)
|
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++ {
|
for i := 0; i < 6; i++ {
|
||||||
|
if i*2+1 >= len(reply) {
|
||||||
|
break
|
||||||
|
}
|
||||||
lo := int(reply[i*2])
|
lo := int(reply[i*2])
|
||||||
hi := int(reply[i*2+1])
|
hi := int(reply[i*2+1])
|
||||||
lengths[i] = lo | (hi << 8)
|
lengths[i] = lo | (hi << 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("Final lengths: %v", lengths)
|
||||||
return lengths, nil
|
return lengths, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,11 +117,8 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<AntennaGenius status={status?.antenna_genius} />
|
<AntennaGenius status={status?.antenna_genius} />
|
||||||
<RotatorGenius status={status?.rotator_genius} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<Ultrabeam status={status?.ultrabeam} />
|
<Ultrabeam status={status?.ultrabeam} />
|
||||||
|
<RotatorGenius status={status?.rotator_genius} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -181,13 +178,41 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.solar-item {
|
.solar-item {
|
||||||
color: rgba(255, 255, 255, 0.8);
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.solar-item .value {
|
.solar-item .value {
|
||||||
color: var(--accent-teal);
|
font-weight: 700;
|
||||||
font-weight: 500;
|
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solar-item:nth-child(1) .value { /* SFI */
|
||||||
|
color: #ffa726;
|
||||||
|
text-shadow: 0 0 8px rgba(255, 167, 38, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.solar-item:nth-child(2) .value { /* Spots */
|
||||||
|
color: #66bb6a;
|
||||||
|
text-shadow: 0 0 8px rgba(102, 187, 106, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.solar-item:nth-child(3) .value { /* A */
|
||||||
|
color: #42a5f5;
|
||||||
|
text-shadow: 0 0 8px rgba(66, 165, 245, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.solar-item:nth-child(4) .value { /* K */
|
||||||
|
color: #ef5350;
|
||||||
|
text-shadow: 0 0 8px rgba(239, 83, 80, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.solar-item:nth-child(5) .value { /* G */
|
||||||
|
color: #ab47bc;
|
||||||
|
text-shadow: 0 0 8px rgba(171, 71, 188, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-right {
|
.header-right {
|
||||||
|
|||||||
@@ -62,40 +62,26 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="metrics">
|
<div class="metrics">
|
||||||
<!-- Power Display - Big and Bold -->
|
<!-- Power Display + SWR Side by Side -->
|
||||||
<div class="power-display">
|
<div class="power-swr-row">
|
||||||
<div class="power-main">
|
<div class="power-section">
|
||||||
<div class="power-value">{powerForward.toFixed(0)}<span class="unit">W</span></div>
|
<div class="power-header">
|
||||||
<div class="power-label">Forward Power</div>
|
<span class="power-label-inline">Power</span>
|
||||||
</div>
|
<span class="power-value-inline">{powerForward.toFixed(0)} W</span>
|
||||||
<div class="power-bar">
|
|
||||||
<div class="power-bar-fill" style="width: {powerPercent}%">
|
|
||||||
<div class="power-bar-glow"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="power-scale">
|
<div class="power-bar-container">
|
||||||
<span>0</span>
|
<div class="power-bar-bg">
|
||||||
<span>1000</span>
|
<div class="power-bar-fill" style="width: {powerPercent}%">
|
||||||
<span>2000</span>
|
<div class="power-bar-glow"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- SWR Circle Indicator -->
|
<!-- SWR Circle Compact -->
|
||||||
<div class="swr-container">
|
<div class="swr-circle-compact" style="--swr-color: {swrColor}">
|
||||||
<div class="swr-circle" style="--swr-color: {swrColor}">
|
<div class="swr-value-compact">{swr.toFixed(2)}</div>
|
||||||
<div class="swr-value">{swr.toFixed(2)}</div>
|
<div class="swr-label-compact">SWR</div>
|
||||||
<div class="swr-label">SWR</div>
|
|
||||||
</div>
|
|
||||||
<div class="swr-status">
|
|
||||||
{#if swr < 1.5}
|
|
||||||
<span class="status-text good">Excellent</span>
|
|
||||||
{:else if swr < 2.0}
|
|
||||||
<span class="status-text ok">Good</span>
|
|
||||||
{:else if swr < 3.0}
|
|
||||||
<span class="status-text warning">Caution</span>
|
|
||||||
{:else}
|
|
||||||
<span class="status-text danger">High!</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -147,8 +133,8 @@
|
|||||||
|
|
||||||
<!-- Fan Control -->
|
<!-- Fan Control -->
|
||||||
<div class="fan-control">
|
<div class="fan-control">
|
||||||
<label class="control-label">Fan Mode</label>
|
<label for="fan-mode-select" class="control-label">Fan Mode</label>
|
||||||
<select value={fanMode} on:change={(e) => setFanMode(e.target.value)}>
|
<select id="fan-mode-select" value={fanMode} on:change={(e) => setFanMode(e.target.value)}>
|
||||||
<option value="STANDARD">Standard</option>
|
<option value="STANDARD">Standard</option>
|
||||||
<option value="CONTEST">Contest</option>
|
<option value="CONTEST">Contest</option>
|
||||||
<option value="BROADCAST">Broadcast</option>
|
<option value="BROADCAST">Broadcast</option>
|
||||||
@@ -235,10 +221,108 @@
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Power Display */
|
/* Power Display */
|
||||||
|
/* Power + SWR Row */
|
||||||
|
.power-swr-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.power-section {
|
||||||
|
flex: 1;
|
||||||
|
background: rgba(15, 23, 42, 0.6);
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(79, 195, 247, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.power-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.power-label-inline {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.power-value-inline {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #66bb6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.power-bar-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.power-bar-bg {
|
||||||
|
width: 100%;
|
||||||
|
height: 28px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.power-bar-fill {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #4caf50, #ffc107, #ff9800, #f44336);
|
||||||
|
border-radius: 14px;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.power-bar-glow {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 20px;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.5));
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { transform: translateX(-100%); }
|
||||||
|
100% { transform: translateX(100%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.swr-circle-compact {
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(79, 195, 247, 0.1), transparent);
|
||||||
|
border: 4px solid var(--swr-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 0 25px var(--swr-color);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swr-value-compact {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--swr-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.swr-label-compact {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.power-display {
|
.power-display {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -258,7 +342,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.power-value .unit {
|
.power-value .unit {
|
||||||
font-size: 24px;
|
font-size: 20px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
@@ -314,7 +398,7 @@
|
|||||||
.swr-container {
|
.swr-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.swr-circle {
|
.swr-circle {
|
||||||
@@ -331,7 +415,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.swr-value {
|
.swr-value {
|
||||||
font-size: 24px;
|
font-size: 20px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
color: var(--swr-color);
|
color: var(--swr-color);
|
||||||
}
|
}
|
||||||
@@ -422,7 +506,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.param-value {
|
.param-value {
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
|||||||
@@ -53,6 +53,29 @@
|
|||||||
console.error('Failed to stop:', err);
|
console.error('Failed to stop:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle click on compass to set heading
|
||||||
|
function handleCompassClick(event) {
|
||||||
|
const svg = event.currentTarget;
|
||||||
|
const rect = svg.getBoundingClientRect();
|
||||||
|
const centerX = rect.width / 2;
|
||||||
|
const centerY = rect.height / 2;
|
||||||
|
|
||||||
|
// Get click position relative to center
|
||||||
|
const x = event.clientX - rect.left - centerX;
|
||||||
|
const y = event.clientY - rect.top - centerY;
|
||||||
|
|
||||||
|
// Calculate angle (0° = North/top, clockwise)
|
||||||
|
let angle = Math.atan2(x, -y) * (180 / Math.PI);
|
||||||
|
if (angle < 0) angle += 360;
|
||||||
|
|
||||||
|
// Round to nearest 5 degrees
|
||||||
|
const roundedHeading = Math.round(angle / 5) * 5;
|
||||||
|
|
||||||
|
// Set target and go
|
||||||
|
targetHeading = roundedHeading;
|
||||||
|
goToHeading();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -62,15 +85,29 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="metrics">
|
<div class="metrics">
|
||||||
<!-- Current Heading Display -->
|
<!-- Current Heading Display with Compact Controls -->
|
||||||
<div class="heading-display">
|
<div class="heading-controls-row">
|
||||||
<div class="heading-label">CURRENT HEADING</div>
|
<div class="heading-display-compact">
|
||||||
<div class="heading-value">{heading}°</div>
|
<div class="heading-label">CURRENT HEADING</div>
|
||||||
|
<div class="heading-value">{heading}°</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls-compact">
|
||||||
|
<button class="btn-mini ccw" on:click={rotateCCW} title="Rotate Counter-Clockwise">
|
||||||
|
↺
|
||||||
|
</button>
|
||||||
|
<button class="btn-mini stop" on:click={stop} title="Stop Rotation">
|
||||||
|
■
|
||||||
|
</button>
|
||||||
|
<button class="btn-mini cw" on:click={rotateCW} title="Rotate Clockwise">
|
||||||
|
↻
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Map with Beam -->
|
<!-- Map with Beam -->
|
||||||
<div class="map-container">
|
<div class="map-container">
|
||||||
<svg viewBox="0 0 300 300" class="map-svg">
|
<svg viewBox="0 0 300 300" class="map-svg clickable-compass" on:click={handleCompassClick}>
|
||||||
<defs>
|
<defs>
|
||||||
<!-- Gradient for beam -->
|
<!-- Gradient for beam -->
|
||||||
<radialGradient id="beamGradient">
|
<radialGradient id="beamGradient">
|
||||||
@@ -139,32 +176,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Go To Heading -->
|
<!-- Go To Heading -->
|
||||||
<div class="goto-container">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
max="359"
|
|
||||||
bind:value={targetHeading}
|
|
||||||
placeholder="Enter heading"
|
|
||||||
class="heading-input"
|
|
||||||
/>
|
|
||||||
<button class="go-btn" on:click={goToHeading}>GO</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Control Buttons -->
|
|
||||||
<div class="controls">
|
|
||||||
<button class="control-btn ccw" on:click={rotateCCW}>
|
|
||||||
<span class="arrow">↺</span>
|
|
||||||
CCW
|
|
||||||
</button>
|
|
||||||
<button class="control-btn stop" on:click={stop}>
|
|
||||||
STOP
|
|
||||||
</button>
|
|
||||||
<button class="control-btn cw" on:click={rotateCW}>
|
|
||||||
<span class="arrow">↻</span>
|
|
||||||
CW
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -212,7 +223,7 @@
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Heading Display */
|
/* Heading Display */
|
||||||
@@ -224,6 +235,71 @@
|
|||||||
border: 1px solid rgba(79, 195, 247, 0.3);
|
border: 1px solid rgba(79, 195, 247, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.heading-controls-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 8px;
|
||||||
|
background: rgba(79, 195, 247, 0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(79, 195, 247, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-display-compact {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-compact {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-mini {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-mini.ccw {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-mini.ccw:hover {
|
||||||
|
transform: rotate(-15deg) scale(1.05);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-mini.stop {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-mini.stop:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 4px 12px rgba(245, 87, 108, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-mini.cw {
|
||||||
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-mini.cw:hover {
|
||||||
|
transform: rotate(15deg) scale(1.05);
|
||||||
|
box-shadow: 0 4px 12px rgba(79, 172, 254, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
.heading-label {
|
.heading-label {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
@@ -254,9 +330,18 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clickable-compass {
|
||||||
|
cursor: crosshair;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable-compass:hover {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
.cardinal {
|
.cardinal {
|
||||||
fill: var(--accent-cyan);
|
fill: var(--accent-cyan);
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-shadow: 0 0 10px rgba(79, 195, 247, 0.8);
|
text-shadow: 0 0 10px rgba(79, 195, 247, 0.8);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,40 +57,26 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="metrics">
|
<div class="metrics">
|
||||||
<!-- Power Display -->
|
<!-- Power Display + SWR Side by Side -->
|
||||||
<div class="power-display">
|
<div class="power-swr-row">
|
||||||
<div class="power-main">
|
<div class="power-section">
|
||||||
<div class="power-value">{powerForward.toFixed(0)}<span class="unit">W</span></div>
|
<div class="power-header">
|
||||||
<div class="power-label">Forward Power</div>
|
<span class="power-label-inline">Power</span>
|
||||||
</div>
|
<span class="power-value-inline">{powerForward.toFixed(0)} W</span>
|
||||||
<div class="power-bar">
|
|
||||||
<div class="power-bar-fill" style="width: {powerPercent}%">
|
|
||||||
<div class="power-bar-glow"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="power-scale">
|
<div class="power-bar-container">
|
||||||
<span>0</span>
|
<div class="power-bar-bg">
|
||||||
<span>1000</span>
|
<div class="power-bar-fill" style="width: {powerPercent}%">
|
||||||
<span>2000</span>
|
<div class="power-bar-glow"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- SWR Circle -->
|
<!-- SWR Circle Compact -->
|
||||||
<div class="swr-container">
|
<div class="swr-circle-compact" style="--swr-color: {swrColor}">
|
||||||
<div class="swr-circle" style="--swr-color: {swrColor}">
|
<div class="swr-value-compact">{swr.toFixed(2)}</div>
|
||||||
<div class="swr-value">{swr.toFixed(2)}</div>
|
<div class="swr-label-compact">SWR</div>
|
||||||
<div class="swr-label">SWR</div>
|
|
||||||
</div>
|
|
||||||
<div class="swr-status">
|
|
||||||
{#if swr < 1.5}
|
|
||||||
<span class="status-text good">Excellent</span>
|
|
||||||
{:else if swr < 2.0}
|
|
||||||
<span class="status-text ok">Good</span>
|
|
||||||
{:else if swr < 3.0}
|
|
||||||
<span class="status-text warning">Caution</span>
|
|
||||||
{:else}
|
|
||||||
<span class="status-text danger">High!</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -219,55 +205,63 @@
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Power Display */
|
/* Power Display */
|
||||||
.power-display {
|
/* Power + SWR Row */
|
||||||
|
.power-swr-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
gap: 16px;
|
||||||
gap: 8px;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.power-main {
|
.power-section {
|
||||||
text-align: center;
|
flex: 1;
|
||||||
|
background: rgba(15, 23, 42, 0.6);
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(79, 195, 247, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.power-value {
|
.power-header {
|
||||||
font-size: 48px;
|
display: flex;
|
||||||
font-weight: 200;
|
justify-content: space-between;
|
||||||
color: var(--accent-cyan);
|
align-items: center;
|
||||||
line-height: 1;
|
margin-bottom: 10px;
|
||||||
text-shadow: 0 0 20px rgba(79, 195, 247, 0.5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.power-value .unit {
|
.power-label-inline {
|
||||||
font-size: 24px;
|
font-size: 12px;
|
||||||
color: var(--text-secondary);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.power-label {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 0.5px;
|
||||||
margin-top: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.power-bar {
|
.power-value-inline {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #66bb6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.power-bar-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 8px;
|
}
|
||||||
background: var(--bg-tertiary);
|
|
||||||
border-radius: 4px;
|
.power-bar-bg {
|
||||||
|
width: 100%;
|
||||||
|
height: 28px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 14px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.power-bar-fill {
|
.power-bar-fill {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, #4caf50, #ffc107, #ff9800, #f44336);
|
background: linear-gradient(90deg, #4caf50, #ffc107, #ff9800, #f44336);
|
||||||
border-radius: 4px;
|
border-radius: 14px;
|
||||||
transition: width 0.3s ease;
|
transition: width 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,19 +280,44 @@
|
|||||||
100% { transform: translateX(100%); }
|
100% { transform: translateX(100%); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.power-scale {
|
.swr-circle-compact {
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(79, 195, 247, 0.1), transparent);
|
||||||
|
border: 4px solid var(--swr-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-direction: column;
|
||||||
font-size: 9px;
|
align-items: center;
|
||||||
color: var(--text-muted);
|
justify-content: center;
|
||||||
margin-top: 4px;
|
box-shadow: 0 0 25px var(--swr-color);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swr-value-compact {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--swr-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.swr-label-compact {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.power-display {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* SWR Circle */
|
/* SWR Circle */
|
||||||
.swr-container {
|
.swr-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.swr-circle {
|
.swr-circle {
|
||||||
@@ -315,7 +334,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.swr-value {
|
.swr-value {
|
||||||
font-size: 24px;
|
font-size: 20px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
color: var(--swr-color);
|
color: var(--swr-color);
|
||||||
}
|
}
|
||||||
@@ -361,7 +380,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cap-value {
|
.cap-value {
|
||||||
font-size: 32px;
|
font-size: 20px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
text-shadow: 0 0 15px rgba(79, 195, 247, 0.5);
|
text-shadow: 0 0 15px rgba(79, 195, 247, 0.5);
|
||||||
@@ -472,6 +491,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tune-icon {
|
.tune-icon {
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -13,34 +13,96 @@
|
|||||||
$: elementLengths = status?.element_lengths || [];
|
$: elementLengths = status?.element_lengths || [];
|
||||||
$: firmwareVersion = status ? `${status.firmware_major}.${status.firmware_minor}` : '0.0';
|
$: firmwareVersion = status ? `${status.firmware_major}.${status.firmware_minor}` : '0.0';
|
||||||
|
|
||||||
// Band names mapping
|
// Band names mapping - VL2.3 covers 6M to 40M only
|
||||||
|
// Band 0=6M, 1=10M, 2=12M, 3=15M, 4=17M, 5=20M, 6=30M, 7=40M
|
||||||
const bandNames = [
|
const bandNames = [
|
||||||
'160M', '80M', '60M', '40M', '30M', '20M',
|
'6M', '10M', '12M', '15M', '17M', '20M', '30M', '40M'
|
||||||
'17M', '15M', '12M', '10M', '6M'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Detect band from frequency
|
||||||
|
$: detectedBand = detectBandFromFrequency(frequency, band);
|
||||||
|
|
||||||
|
function detectBandFromFrequency(freq, bandIndex) {
|
||||||
|
// If band index is valid (0-7), use it directly
|
||||||
|
if (bandIndex >= 0 && bandIndex <= 7) {
|
||||||
|
return bandNames[bandIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise detect from frequency (in kHz)
|
||||||
|
if (freq >= 7000 && freq <= 7300) return '40M';
|
||||||
|
if (freq >= 10100 && freq <= 10150) return '30M';
|
||||||
|
if (freq >= 14000 && freq <= 14350) return '20M';
|
||||||
|
if (freq >= 18068 && freq <= 18168) return '17M';
|
||||||
|
if (freq >= 21000 && freq <= 21450) return '15M';
|
||||||
|
if (freq >= 24890 && freq <= 24990) return '12M';
|
||||||
|
if (freq >= 28000 && freq <= 29700) return '10M';
|
||||||
|
if (freq >= 50000 && freq <= 54000) return '6M';
|
||||||
|
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
// Direction names
|
// Direction names
|
||||||
const directionNames = ['Normal', '180°', 'Bi-Dir'];
|
const directionNames = ['Normal', '180°', 'Bi-Dir'];
|
||||||
|
|
||||||
|
// Auto-track threshold options
|
||||||
|
const thresholdOptions = [
|
||||||
|
{ value: 25, label: '25 kHz' },
|
||||||
|
{ value: 50, label: '50 kHz' },
|
||||||
|
{ value: 100, label: '100 kHz' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Auto-track state
|
||||||
|
let autoTrackEnabled = true; // Default enabled
|
||||||
|
let autoTrackThreshold = 25; // Default 25 kHz
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
let targetFreq = 0;
|
|
||||||
let targetDirection = 0;
|
let targetDirection = 0;
|
||||||
|
|
||||||
|
// Auto-update targetDirection when status changes
|
||||||
|
$: targetDirection = direction;
|
||||||
|
|
||||||
|
// Element names based on band (corrected order: 0=6M ... 10=160M)
|
||||||
|
$: elementNames = getElementNames(band);
|
||||||
|
|
||||||
|
function getElementNames(band) {
|
||||||
|
// 30M (band 6) and 40M (band 7): Reflector (inverted), Radiator (inverted)
|
||||||
|
if (band === 6 || band === 7) {
|
||||||
|
return ['Radiator (30/40M)', 'Reflector (30/40M)', null];
|
||||||
|
}
|
||||||
|
// 6M to 20M (bands 0-5): Reflector, Radiator, Director 1
|
||||||
|
if (band >= 0 && band <= 5) {
|
||||||
|
return ['Reflector', 'Radiator', 'Director 1'];
|
||||||
|
}
|
||||||
|
// Default
|
||||||
|
return ['Element 1', 'Element 2', 'Element 3'];
|
||||||
|
}
|
||||||
|
|
||||||
// Element calibration state
|
// Element calibration state
|
||||||
let calibrationMode = false;
|
let calibrationMode = false;
|
||||||
let selectedElement = 0;
|
let selectedElement = 0;
|
||||||
let elementAdjustment = 0;
|
let elementAdjustment = 0;
|
||||||
|
|
||||||
async function setFrequency() {
|
async function setDirection() {
|
||||||
if (targetFreq < 1800 || targetFreq > 30000) {
|
if (frequency === 0) {
|
||||||
alert('Frequency must be between 1.8 MHz and 30 MHz');
|
return; // Silently skip if no frequency
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await api.ultrabeam.setFrequency(targetFreq, targetDirection);
|
// Send command to antenna with current frequency and new direction
|
||||||
|
await api.ultrabeam.setFrequency(frequency, targetDirection);
|
||||||
|
// Also save direction preference for auto-track
|
||||||
|
await api.ultrabeam.setDirection(targetDirection);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to set frequency:', err);
|
// Log error but don't alert - code 30 (busy) is normal
|
||||||
alert('Failed to set frequency');
|
console.log('Direction change sent (may show code 30 if busy):', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateAutoTrack() {
|
||||||
|
try {
|
||||||
|
await api.ultrabeam.setAutoTrack(autoTrackEnabled, autoTrackThreshold);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update auto-track:', err);
|
||||||
|
alert('Failed to update auto-track settings');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,50 +150,56 @@
|
|||||||
|
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
<div class="status-label">Band</div>
|
<div class="status-label">Band</div>
|
||||||
<div class="status-value band">{bandNames[band] || 'Unknown'}</div>
|
<div class="status-value band">{detectedBand}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
<div class="status-label">Direction</div>
|
<div class="status-label">Direction</div>
|
||||||
<div class="status-value direction">{directionNames[direction]}</div>
|
<div class="status-value direction">{directionNames[direction]}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="status-item">
|
|
||||||
<div class="status-label">Firmware</div>
|
|
||||||
<div class="status-value fw">v{firmwareVersion}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Frequency Control -->
|
<!-- Auto-Track Control -->
|
||||||
<div class="control-section">
|
<div class="control-section compact">
|
||||||
<h3>Frequency Control</h3>
|
<h3>Auto Tracking</h3>
|
||||||
<div class="freq-control">
|
<div class="auto-track-controls">
|
||||||
<div class="input-group">
|
<label class="toggle-label">
|
||||||
<label for="target-freq">Target Frequency (KHz)</label>
|
<input type="checkbox" bind:checked={autoTrackEnabled} on:change={updateAutoTrack} />
|
||||||
<input
|
<span>Enable Auto-Track from Tuner</span>
|
||||||
id="target-freq"
|
</label>
|
||||||
type="number"
|
|
||||||
bind:value={targetFreq}
|
|
||||||
min="1800"
|
|
||||||
max="30000"
|
|
||||||
step="1"
|
|
||||||
placeholder="e.g. 14200"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="input-group">
|
<div class="threshold-group">
|
||||||
<label for="target-dir">Direction</label>
|
<label for="threshold-select">Threshold:</label>
|
||||||
<select id="target-dir" bind:value={targetDirection}>
|
<select id="threshold-select" bind:value={autoTrackThreshold} on:change={updateAutoTrack}>
|
||||||
<option value={0}>Normal</option>
|
{#each thresholdOptions as option}
|
||||||
<option value={1}>180°</option>
|
<option value={option.value}>{option.label}</option>
|
||||||
<option value={2}>Bi-Directional</option>
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn-primary" on:click={setFrequency}>
|
<div class="direction-buttons">
|
||||||
<span class="icon">📡</span>
|
<button
|
||||||
Set Frequency
|
class="dir-btn normal"
|
||||||
</button>
|
class:active={targetDirection === 0}
|
||||||
|
on:click={() => { targetDirection = 0; setDirection(); }}
|
||||||
|
>
|
||||||
|
Normal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="dir-btn rotate180"
|
||||||
|
class:active={targetDirection === 1}
|
||||||
|
on:click={() => { targetDirection = 1; setDirection(); }}
|
||||||
|
>
|
||||||
|
180°
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="dir-btn bidir"
|
||||||
|
class:active={targetDirection === 2}
|
||||||
|
on:click={() => { targetDirection = 2; setDirection(); }}
|
||||||
|
>
|
||||||
|
Bi-Dir
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -146,22 +214,25 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Element Lengths Display -->
|
<!-- Element Lengths Display - HIDDEN: Command 9 returns status instead -->
|
||||||
|
<!--
|
||||||
<div class="elements-section">
|
<div class="elements-section">
|
||||||
<h3>Element Lengths (mm)</h3>
|
<h3>Element Lengths (mm)</h3>
|
||||||
<div class="elements-grid">
|
<div class="elements-grid">
|
||||||
{#each elementLengths as length, i}
|
{#each elementLengths.slice(0, 3) as length, i}
|
||||||
{#if length > 0}
|
{#if length > 0 && elementNames[i]}
|
||||||
<div class="element-item">
|
<div class="element-item">
|
||||||
<div class="element-label">Element {i + 1}</div>
|
<div class="element-label">{elementNames[i]}</div>
|
||||||
<div class="element-value">{length} mm</div>
|
<div class="element-value">{length} mm</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- Calibration Mode -->
|
<!-- Calibration Mode - HIDDEN: Command 9 doesn't work -->
|
||||||
|
<!--
|
||||||
<div class="calibration-section">
|
<div class="calibration-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3>Calibration</h3>
|
<h3>Calibration</h3>
|
||||||
@@ -179,9 +250,9 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label for="element-select">Element</label>
|
<label for="element-select">Element</label>
|
||||||
<select id="element-select" bind:value={selectedElement}>
|
<select id="element-select" bind:value={selectedElement}>
|
||||||
{#each elementLengths as length, i}
|
{#each elementLengths.slice(0, 3) as length, i}
|
||||||
{#if length > 0}
|
{#if length > 0 && elementNames[i]}
|
||||||
<option value={i}>Element {i + 1} ({length}mm)</option>
|
<option value={i}>{elementNames[i]} ({length}mm)</option>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
@@ -209,6 +280,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
@@ -224,7 +296,7 @@
|
|||||||
.card {
|
.card {
|
||||||
background: linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(15, 23, 42, 0.98) 100%);
|
background: linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(15, 23, 42, 0.98) 100%);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 24px;
|
padding: 16px;
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
border: 1px solid rgba(79, 195, 247, 0.2);
|
border: 1px solid rgba(79, 195, 247, 0.2);
|
||||||
}
|
}
|
||||||
@@ -240,7 +312,7 @@
|
|||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 24px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background: linear-gradient(135deg, #4fc3f7 0%, #03a9f4 100%);
|
background: linear-gradient(135deg, #4fc3f7 0%, #03a9f4 100%);
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
@@ -278,14 +350,14 @@
|
|||||||
.metrics {
|
.metrics {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 24px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Status Grid */
|
/* Status Grid */
|
||||||
.status-grid {
|
.status-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
gap: 16px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-item {
|
.status-item {
|
||||||
@@ -304,32 +376,140 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status-value {
|
.status-value {
|
||||||
font-size: 20px;
|
font-size: 22px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: #4fc3f7;
|
color: #4fc3f7;
|
||||||
|
text-shadow: 0 0 10px rgba(79, 195, 247, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-value.freq {
|
.status-value.freq {
|
||||||
color: #66bb6a;
|
color: #66bb6a;
|
||||||
font-size: 24px;
|
font-size: 22px;
|
||||||
|
text-shadow: 0 0 10px rgba(102, 187, 106, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-value.band {
|
.status-value.band {
|
||||||
color: #ffa726;
|
color: #ffa726;
|
||||||
|
text-shadow: 0 0 10px rgba(255, 167, 38, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-value.direction {
|
.status-value.direction {
|
||||||
color: #ab47bc;
|
color: #ab47bc;
|
||||||
|
text-shadow: 0 0 10px rgba(171, 71, 188, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Control Section */
|
/* Control Section */
|
||||||
.control-section {
|
.control-section {
|
||||||
background: rgba(15, 23, 42, 0.4);
|
background: rgba(15, 23, 42, 0.4);
|
||||||
padding: 20px;
|
padding: 12px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid rgba(79, 195, 247, 0.2);
|
border: 1px solid rgba(79, 195, 247, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.control-section.compact {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-track-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label input[type="checkbox"] {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.threshold-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.threshold-group label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.direction-buttons {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-btn {
|
||||||
|
padding: 14px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
color: white;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-btn.normal {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-btn.normal:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-btn.normal.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
box-shadow: 0 0 25px rgba(102, 126, 234, 0.8), 0 6px 20px rgba(102, 126, 234, 0.5);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-btn.rotate180 {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-btn.rotate180:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(245, 87, 108, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-btn.rotate180.active {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
box-shadow: 0 0 25px rgba(245, 87, 108, 0.8), 0 6px 20px rgba(245, 87, 108, 0.5);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-btn.bidir {
|
||||||
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-btn.bidir:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(79, 172, 254, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dir-btn.bidir.active {
|
||||||
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||||
|
box-shadow: 0 0 25px rgba(79, 172, 254, 0.8), 0 6px 20px rgba(79, 172, 254, 0.5);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
.freq-control {
|
.freq-control {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2fr 1fr auto;
|
grid-template-columns: 2fr 1fr auto;
|
||||||
@@ -437,7 +617,7 @@
|
|||||||
/* Progress */
|
/* Progress */
|
||||||
.progress-section {
|
.progress-section {
|
||||||
background: rgba(15, 23, 42, 0.4);
|
background: rgba(15, 23, 42, 0.4);
|
||||||
padding: 20px;
|
padding: 12px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid rgba(255, 193, 7, 0.3);
|
border: 1px solid rgba(255, 193, 7, 0.3);
|
||||||
}
|
}
|
||||||
@@ -468,7 +648,7 @@
|
|||||||
/* Elements */
|
/* Elements */
|
||||||
.elements-section {
|
.elements-section {
|
||||||
background: rgba(15, 23, 42, 0.4);
|
background: rgba(15, 23, 42, 0.4);
|
||||||
padding: 20px;
|
padding: 12px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid rgba(79, 195, 247, 0.2);
|
border: 1px solid rgba(79, 195, 247, 0.2);
|
||||||
}
|
}
|
||||||
@@ -502,7 +682,7 @@
|
|||||||
/* Calibration */
|
/* Calibration */
|
||||||
.calibration-section {
|
.calibration-section {
|
||||||
background: rgba(255, 152, 0, 0.05);
|
background: rgba(255, 152, 0, 0.05);
|
||||||
padding: 20px;
|
padding: 12px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid rgba(255, 152, 0, 0.3);
|
border: 1px solid rgba(255, 152, 0, 0.3);
|
||||||
}
|
}
|
||||||
@@ -517,7 +697,7 @@
|
|||||||
.calibration-controls {
|
.calibration-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning-text {
|
.warning-text {
|
||||||
|
|||||||
@@ -97,5 +97,9 @@ export const api = {
|
|||||||
body: JSON.stringify({ frequency, direction }),
|
body: JSON.stringify({ frequency, direction }),
|
||||||
}),
|
}),
|
||||||
retract: () => request('/ultrabeam/retract', { method: 'POST' }),
|
retract: () => request('/ultrabeam/retract', { method: 'POST' }),
|
||||||
|
setAutoTrack: (enabled, threshold) => request('/ultrabeam/autotrack', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ enabled, threshold }),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user