corrected all bugs
This commit is contained in:
@@ -112,13 +112,13 @@
|
||||
<div class="row">
|
||||
<WebSwitch status={status?.webswitch} />
|
||||
<PowerGenius status={status?.power_genius} />
|
||||
<TunerGenius status={status?.tuner_genius} />
|
||||
<TunerGenius status={status?.tuner_genius} flexradio={status?.flexradio} />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<AntennaGenius status={status?.antenna_genius} />
|
||||
<Ultrabeam status={status?.ultrabeam} />
|
||||
<RotatorGenius status={status?.rotator_genius} />
|
||||
<Ultrabeam status={status?.ultrabeam} flexradio={status?.flexradio} />
|
||||
<RotatorGenius status={status?.rotator_genius} ultrabeam={status?.ultrabeam} />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -292,4 +292,4 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -7,15 +7,6 @@
|
||||
$: powerReflected = status?.power_reflected || 0;
|
||||
$: swr = status?.swr || 1.0;
|
||||
|
||||
// Debug logging
|
||||
$: if (status) {
|
||||
console.log('PowerGenius status update:', {
|
||||
powerForward: status.power_forward,
|
||||
swr: status.swr,
|
||||
state: status.state,
|
||||
connected: status.connected
|
||||
});
|
||||
}
|
||||
$: voltage = status?.voltage || 0;
|
||||
$: vdd = status?.vdd || 0;
|
||||
$: current = status?.current || 0;
|
||||
|
||||
@@ -2,31 +2,66 @@
|
||||
import { api } from '../lib/api.js';
|
||||
|
||||
export let status;
|
||||
export let ultrabeam = null;
|
||||
|
||||
let heading = 0;
|
||||
let heading = null; // Start with null instead of 0
|
||||
let connected = false;
|
||||
|
||||
// Update heading only if we have a valid value from status
|
||||
// Get Ultrabeam direction mode: 0=Normal, 1=180°, 2=Bi-Dir
|
||||
$: ultrabeamDirection = ultrabeam?.direction ?? 0;
|
||||
|
||||
// Update heading with detailed logging to debug
|
||||
$: if (status?.heading !== undefined && status?.heading !== null) {
|
||||
heading = status.heading;
|
||||
const newHeading = status.heading;
|
||||
const oldHeading = heading;
|
||||
|
||||
console.log(`RotatorGenius heading update: ${oldHeading} -> ${newHeading}`);
|
||||
|
||||
if (heading === null) {
|
||||
// First time: accept any value
|
||||
heading = newHeading;
|
||||
console.log(` ✓ First load, set to ${heading}°`);
|
||||
} else if (newHeading === 0 && heading > 10 && heading < 350) {
|
||||
// Ignore sudden jump to 0 from middle range (glitch)
|
||||
console.log(` ✗ IGNORED glitch jump from ${heading}° to 0°`);
|
||||
} else {
|
||||
// Normal update
|
||||
heading = newHeading;
|
||||
console.log(` ✓ Updated to ${heading}°`);
|
||||
}
|
||||
}
|
||||
|
||||
// Display heading: use cached value or 0 if never set
|
||||
$: displayHeading = heading !== null ? heading : 0;
|
||||
|
||||
$: connected = status?.connected || false;
|
||||
|
||||
let targetHeading = 0;
|
||||
let hasTarget = false;
|
||||
|
||||
// Clear target when we reach it (within 5 degrees)
|
||||
$: if (hasTarget && heading !== null) {
|
||||
const diff = Math.abs(heading - targetHeading);
|
||||
const wrappedDiff = Math.min(diff, 360 - diff);
|
||||
if (wrappedDiff < 5) {
|
||||
hasTarget = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function goToHeading() {
|
||||
if (targetHeading < 0 || targetHeading > 359) {
|
||||
alert('Heading must be between 0 and 359');
|
||||
// Removed alert popup - check console for errors
|
||||
return;
|
||||
}
|
||||
try {
|
||||
hasTarget = true; // Mark that we have a target
|
||||
// Subtract 10 degrees to compensate for rotator momentum
|
||||
const adjustedHeading = (targetHeading - 10 + 360) % 360;
|
||||
await api.rotator.setHeading(adjustedHeading);
|
||||
} catch (err) {
|
||||
console.error('Failed to set heading:', err);
|
||||
alert('Failed to rotate');
|
||||
hasTarget = false;
|
||||
// Removed alert popup - check console for errors
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +124,12 @@
|
||||
<div class="heading-controls-row">
|
||||
<div class="heading-display-compact">
|
||||
<div class="heading-label">CURRENT HEADING</div>
|
||||
<div class="heading-value">{heading}°</div>
|
||||
<div class="heading-value">
|
||||
{displayHeading}°
|
||||
{#if hasTarget}
|
||||
<span class="target-indicator">→ {targetHeading}°</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls-compact">
|
||||
@@ -107,7 +147,12 @@
|
||||
|
||||
<!-- Map with Beam -->
|
||||
<div class="map-container">
|
||||
<svg viewBox="0 0 300 300" class="map-svg clickable-compass" on:click={handleCompassClick}>
|
||||
<svg viewBox="0 0 300 300" class="map-svg clickable-compass"
|
||||
on:click={handleCompassClick}
|
||||
on:keydown={(e) => e.key === 'Enter' && handleCompassClick(e)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Click to rotate antenna to direction">
|
||||
<defs>
|
||||
<!-- Gradient for beam -->
|
||||
<radialGradient id="beamGradient">
|
||||
@@ -126,30 +171,139 @@
|
||||
|
||||
<!-- Rotated group for beam -->
|
||||
<g transform="translate(150, 150)">
|
||||
<!-- Beam (rotates with heading) -->
|
||||
<g transform="rotate({heading})">
|
||||
<!-- Beam sector (±15° = 30° total beamwidth) -->
|
||||
<path d="M 0,0 L {-Math.sin(15 * Math.PI/180) * 130},{-Math.cos(15 * Math.PI/180) * 130}
|
||||
A 130,130 0 0,1 {Math.sin(15 * Math.PI/180) * 130},{-Math.cos(15 * Math.PI/180) * 130} Z"
|
||||
fill="url(#beamGradient)"
|
||||
opacity="0.85"/>
|
||||
|
||||
<!-- Beam outline -->
|
||||
<line x1="0" y1="0" x2={-Math.sin(15 * Math.PI/180) * 130} y2={-Math.cos(15 * Math.PI/180) * 130}
|
||||
stroke="#4fc3f7" stroke-width="2" opacity="0.9"/>
|
||||
<line x1="0" y1="0" x2={Math.sin(15 * Math.PI/180) * 130} y2={-Math.cos(15 * Math.PI/180) * 130}
|
||||
stroke="#4fc3f7" stroke-width="2" opacity="0.9"/>
|
||||
|
||||
<!-- Direction arrow -->
|
||||
<g transform="translate(0, -110)">
|
||||
<polygon points="0,-20 -8,5 0,0 8,5"
|
||||
fill="#4fc3f7"
|
||||
stroke="#0288d1"
|
||||
stroke-width="2"
|
||||
style="filter: drop-shadow(0 0 10px rgba(79, 195, 247, 1))"/>
|
||||
<!-- Physical antenna direction indicator (only in 180° or Bi-Dir mode) -->
|
||||
{#if ultrabeamDirection === 1 || ultrabeamDirection === 2}
|
||||
<g transform="rotate({displayHeading})">
|
||||
<!-- Gray dashed line showing physical antenna direction -->
|
||||
<line x1="0" y1="0" x2="0" y2="-125"
|
||||
stroke="rgba(255, 255, 255, 0.3)"
|
||||
stroke-width="2"
|
||||
stroke-dasharray="5,5"
|
||||
opacity="0.6"/>
|
||||
<!-- Small triangle at end to show physical direction -->
|
||||
<g transform="translate(0, -125)">
|
||||
<polygon points="0,-8 -5,5 5,5"
|
||||
fill="rgba(255, 255, 255, 0.4)"
|
||||
stroke="rgba(255, 255, 255, 0.5)"
|
||||
stroke-width="1"/>
|
||||
</g>
|
||||
</g>
|
||||
{/if}
|
||||
|
||||
<!-- Beam (rotates with heading) -->
|
||||
<g transform="rotate({displayHeading})">
|
||||
|
||||
<!-- NORMAL MODE (0): Forward beam only -->
|
||||
{#if ultrabeamDirection === 0}
|
||||
<!-- Beam sector (±15° = 30° total beamwidth) -->
|
||||
<path d="M 0,0 L {-Math.sin(15 * Math.PI/180) * 130},{-Math.cos(15 * Math.PI/180) * 130}
|
||||
A 130,130 0 0,1 {Math.sin(15 * Math.PI/180) * 130},{-Math.cos(15 * Math.PI/180) * 130} Z"
|
||||
fill="url(#beamGradient)"
|
||||
opacity="0.85"/>
|
||||
|
||||
<!-- Beam outline -->
|
||||
<line x1="0" y1="0" x2={-Math.sin(15 * Math.PI/180) * 130} y2={-Math.cos(15 * Math.PI/180) * 130}
|
||||
stroke="#4fc3f7" stroke-width="2" opacity="0.9"/>
|
||||
<line x1="0" y1="0" x2={Math.sin(15 * Math.PI/180) * 130} y2={-Math.cos(15 * Math.PI/180) * 130}
|
||||
stroke="#4fc3f7" stroke-width="2" opacity="0.9"/>
|
||||
|
||||
<!-- Direction arrow -->
|
||||
<g transform="translate(0, -110)">
|
||||
<polygon points="0,-20 -8,5 0,0 8,5"
|
||||
fill="#4fc3f7"
|
||||
stroke="#0288d1"
|
||||
stroke-width="2"
|
||||
style="filter: drop-shadow(0 0 10px rgba(79, 195, 247, 1))"/>
|
||||
</g>
|
||||
{/if}
|
||||
|
||||
<!-- 180° MODE (1): Backward beam only -->
|
||||
{#if ultrabeamDirection === 1}
|
||||
<!-- Beam sector pointing BACKWARD (180° opposite) -->
|
||||
<path d="M 0,0 L {-Math.sin(15 * Math.PI/180) * 130},{Math.cos(15 * Math.PI/180) * 130}
|
||||
A 130,130 0 0,0 {Math.sin(15 * Math.PI/180) * 130},{Math.cos(15 * Math.PI/180) * 130} Z"
|
||||
fill="url(#beamGradient)"
|
||||
opacity="0.85"/>
|
||||
|
||||
<!-- Beam outline -->
|
||||
<line x1="0" y1="0" x2={-Math.sin(15 * Math.PI/180) * 130} y2={Math.cos(15 * Math.PI/180) * 130}
|
||||
stroke="#4fc3f7" stroke-width="2" opacity="0.9"/>
|
||||
<line x1="0" y1="0" x2={Math.sin(15 * Math.PI/180) * 130} y2={Math.cos(15 * Math.PI/180) * 130}
|
||||
stroke="#4fc3f7" stroke-width="2" opacity="0.9"/>
|
||||
|
||||
<!-- Direction arrow pointing BACKWARD -->
|
||||
<g transform="translate(0, 110)">
|
||||
<polygon points="0,20 -8,-5 0,0 8,-5"
|
||||
fill="#4fc3f7"
|
||||
stroke="#0288d1"
|
||||
stroke-width="2"
|
||||
style="filter: drop-shadow(0 0 10px rgba(79, 195, 247, 1))"/>
|
||||
</g>
|
||||
{/if}
|
||||
|
||||
<!-- BI-DIRECTIONAL MODE (2): Both forward AND backward beams -->
|
||||
{#if ultrabeamDirection === 2}
|
||||
<!-- Forward beam -->
|
||||
<path d="M 0,0 L {-Math.sin(15 * Math.PI/180) * 130},{-Math.cos(15 * Math.PI/180) * 130}
|
||||
A 130,130 0 0,1 {Math.sin(15 * Math.PI/180) * 130},{-Math.cos(15 * Math.PI/180) * 130} Z"
|
||||
fill="url(#beamGradient)"
|
||||
opacity="0.7"/>
|
||||
|
||||
<!-- Backward beam -->
|
||||
<path d="M 0,0 L {-Math.sin(15 * Math.PI/180) * 130},{Math.cos(15 * Math.PI/180) * 130}
|
||||
A 130,130 0 0,0 {Math.sin(15 * Math.PI/180) * 130},{Math.cos(15 * Math.PI/180) * 130} Z"
|
||||
fill="url(#beamGradient)"
|
||||
opacity="0.7"/>
|
||||
|
||||
<!-- Beam outlines -->
|
||||
<line x1="0" y1="0" x2={-Math.sin(15 * Math.PI/180) * 130} y2={-Math.cos(15 * Math.PI/180) * 130}
|
||||
stroke="#4fc3f7" stroke-width="2" opacity="0.8"/>
|
||||
<line x1="0" y1="0" x2={Math.sin(15 * Math.PI/180) * 130} y2={-Math.cos(15 * Math.PI/180) * 130}
|
||||
stroke="#4fc3f7" stroke-width="2" opacity="0.8"/>
|
||||
<line x1="0" y1="0" x2={-Math.sin(15 * Math.PI/180) * 130} y2={Math.cos(15 * Math.PI/180) * 130}
|
||||
stroke="#4fc3f7" stroke-width="2" opacity="0.8"/>
|
||||
<line x1="0" y1="0" x2={Math.sin(15 * Math.PI/180) * 130} y2={Math.cos(15 * Math.PI/180) * 130}
|
||||
stroke="#4fc3f7" stroke-width="2" opacity="0.8"/>
|
||||
|
||||
<!-- Direction arrows (both directions) -->
|
||||
<g transform="translate(0, -110)">
|
||||
<polygon points="0,-20 -8,5 0,0 8,5"
|
||||
fill="#4fc3f7"
|
||||
stroke="#0288d1"
|
||||
stroke-width="2"
|
||||
style="filter: drop-shadow(0 0 10px rgba(79, 195, 247, 1))"/>
|
||||
</g>
|
||||
<g transform="translate(0, 110)">
|
||||
<polygon points="0,20 -8,-5 0,0 8,-5"
|
||||
fill="#4fc3f7"
|
||||
stroke="#0288d1"
|
||||
stroke-width="2"
|
||||
style="filter: drop-shadow(0 0 10px rgba(79, 195, 247, 1))"/>
|
||||
</g>
|
||||
{/if}
|
||||
|
||||
</g>
|
||||
|
||||
<!-- Target arrow (if we have a target) -->
|
||||
{#if hasTarget}
|
||||
<g transform="rotate({targetHeading})">
|
||||
<line x1="0" y1="0" x2="0" y2="-120"
|
||||
stroke="#ffc107"
|
||||
stroke-width="3"
|
||||
stroke-dasharray="8,4"
|
||||
opacity="0.9"/>
|
||||
<g transform="translate(0, -120)">
|
||||
<polygon points="0,-15 -10,10 0,5 10,10"
|
||||
fill="#ffc107"
|
||||
stroke="#ff9800"
|
||||
stroke-width="2"
|
||||
style="filter: drop-shadow(0 0 10px rgba(255, 193, 7, 0.8))">
|
||||
<animate attributeName="opacity" values="0.8;1;0.8" dur="1s" repeatCount="indefinite"/>
|
||||
</polygon>
|
||||
</g>
|
||||
</g>
|
||||
{/if}
|
||||
|
||||
<!-- Center dot (your QTH - JN36dg) -->
|
||||
<circle cx="0" cy="0" r="5" fill="#f44336" stroke="#fff" stroke-width="2">
|
||||
<animate attributeName="r" values="5;7;5" dur="2s" repeatCount="indefinite"/>
|
||||
@@ -175,6 +329,24 @@
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Legend (only show in 180° or Bi-Dir mode) -->
|
||||
{#if ultrabeamDirection === 1 || ultrabeamDirection === 2}
|
||||
<div class="map-legend">
|
||||
<div class="legend-item">
|
||||
<svg width="30" height="20" viewBox="0 0 30 20">
|
||||
<line x1="5" y1="10" x2="25" y2="10" stroke="rgba(255,255,255,0.3)" stroke-width="2" stroke-dasharray="3,3"/>
|
||||
</svg>
|
||||
<span>Physical antenna</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<svg width="30" height="20" viewBox="0 0 30 20">
|
||||
<line x1="5" y1="10" x2="25" y2="10" stroke="#4fc3f7" stroke-width="2"/>
|
||||
</svg>
|
||||
<span>Radiation pattern</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Go To Heading -->
|
||||
</div>
|
||||
</div>
|
||||
@@ -226,14 +398,6 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Heading Display */
|
||||
.heading-display {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
background: rgba(79, 195, 247, 0.1);
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(79, 195, 247, 0.3);
|
||||
}
|
||||
|
||||
.heading-controls-row {
|
||||
display: flex;
|
||||
@@ -259,45 +423,39 @@
|
||||
.btn-mini {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: none;
|
||||
border: 2px solid rgba(79, 195, 247, 0.3);
|
||||
border-radius: 6px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
background: rgba(79, 195, 247, 0.08);
|
||||
}
|
||||
|
||||
.btn-mini.ccw {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
.btn-mini:hover {
|
||||
border-color: rgba(79, 195, 247, 0.6);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(79, 195, 247, 0.15);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 0 15px rgba(79, 195, 247, 0.3);
|
||||
}
|
||||
|
||||
.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;
|
||||
transform: translateY(-1px) rotate(-5deg);
|
||||
}
|
||||
|
||||
.btn-mini.cw:hover {
|
||||
transform: rotate(15deg) scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(79, 172, 254, 0.4);
|
||||
transform: translateY(-1px) rotate(5deg);
|
||||
}
|
||||
|
||||
.btn-mini.stop:hover {
|
||||
border-color: #f44336;
|
||||
color: #f44336;
|
||||
background: rgba(244, 67, 54, 0.15);
|
||||
}
|
||||
|
||||
.heading-label {
|
||||
@@ -315,6 +473,20 @@
|
||||
text-shadow: 0 0 20px rgba(79, 195, 247, 0.5);
|
||||
}
|
||||
|
||||
.target-indicator {
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
color: #ffc107;
|
||||
margin-left: 20px;
|
||||
text-shadow: 0 0 15px rgba(255, 193, 7, 0.6);
|
||||
animation: targetPulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes targetPulse {
|
||||
0%, 100% { opacity: 0.8; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Map */
|
||||
.map-container {
|
||||
display: flex;
|
||||
@@ -324,6 +496,24 @@
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.map-legend {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
padding: 8px;
|
||||
margin-top: 8px;
|
||||
background: rgba(10, 22, 40, 0.4);
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.map-svg {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
@@ -352,70 +542,4 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Go To Heading */
|
||||
.goto-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.go-btn {
|
||||
padding: 10px 24px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: linear-gradient(135deg, var(--accent-cyan), #0288d1);
|
||||
color: #000;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 4px 12px rgba(79, 195, 247, 0.4);
|
||||
}
|
||||
|
||||
.go-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(79, 195, 247, 0.5);
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.control-btn.stop {
|
||||
background: linear-gradient(135deg, #f44336, #d32f2f);
|
||||
border-color: #f44336;
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(244, 67, 54, 0.4);
|
||||
}
|
||||
|
||||
.control-btn.stop:hover {
|
||||
box-shadow: 0 6px 16px rgba(244, 67, 54, 0.5);
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { api } from '../lib/api.js';
|
||||
|
||||
export let status;
|
||||
export let flexradio = null;
|
||||
|
||||
$: connected = status?.connected || false;
|
||||
$: frequency = status?.frequency || 0;
|
||||
@@ -13,6 +14,21 @@
|
||||
$: elementLengths = status?.element_lengths || [];
|
||||
$: firmwareVersion = status ? `${status.firmware_major}.${status.firmware_minor}` : '0.0';
|
||||
|
||||
// FlexRadio interlock
|
||||
$: interlockConnected = flexradio?.connected || false;
|
||||
$: interlockState = flexradio?.interlock_state || null;
|
||||
$: interlockColor = getInterlockColor(interlockState);
|
||||
|
||||
function getInterlockColor(state) {
|
||||
switch(state) {
|
||||
case 'READY': return '#4caf50';
|
||||
case 'NOT_READY': return '#f44336';
|
||||
case 'PTT_REQUESTED': return '#ffc107';
|
||||
case 'TRANSMITTING': return '#ff9800';
|
||||
default: return 'rgba(255, 255, 255, 0.3)';
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = [
|
||||
@@ -102,7 +118,7 @@
|
||||
await api.ultrabeam.setAutoTrack(autoTrackEnabled, autoTrackThreshold);
|
||||
} catch (err) {
|
||||
console.error('Failed to update auto-track:', err);
|
||||
alert('Failed to update auto-track settings');
|
||||
// Removed alert popup - check console for errors
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +130,7 @@
|
||||
await api.ultrabeam.retract();
|
||||
} catch (err) {
|
||||
console.error('Failed to retract:', err);
|
||||
alert('Failed to retract');
|
||||
// Removed alert popup - check console for errors
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,11 +138,11 @@
|
||||
try {
|
||||
const newLength = elementLengths[selectedElement] + elementAdjustment;
|
||||
// TODO: Add API call when backend supports it
|
||||
alert(`Would adjust element ${selectedElement} by ${elementAdjustment}mm to ${newLength}mm`);
|
||||
// Removed alert popup - check console for errors
|
||||
elementAdjustment = 0;
|
||||
} catch (err) {
|
||||
console.error('Failed to adjust element:', err);
|
||||
alert('Failed to adjust element');
|
||||
// Removed alert popup - check console for errors
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +153,17 @@
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Ultrabeam VL2.3</h2>
|
||||
<span class="status-dot" class:disconnected={!connected}></span>
|
||||
<div class="header-right">
|
||||
{#if interlockConnected && interlockState}
|
||||
<div class="interlock-badge" style="border-color: {interlockColor}; color: {interlockColor}">
|
||||
{interlockState === 'READY' ? '🔓 TX OK' :
|
||||
interlockState === 'NOT_READY' ? '🔒 TX Block' :
|
||||
interlockState === 'PTT_REQUESTED' ? '⏳ PTT' :
|
||||
interlockState === 'TRANSMITTING' ? '📡 TX' : '❓'}
|
||||
</div>
|
||||
{/if}
|
||||
<span class="status-dot" class:disconnected={!connected}></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metrics">
|
||||
@@ -176,30 +202,31 @@
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="direction-buttons">
|
||||
<button
|
||||
class="dir-btn normal"
|
||||
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>
|
||||
|
||||
<!-- Direction buttons on separate line -->
|
||||
<div class="direction-buttons">
|
||||
<button
|
||||
class="dir-btn"
|
||||
class:active={targetDirection === 0}
|
||||
on:click={() => { targetDirection = 0; setDirection(); }}
|
||||
>
|
||||
Normal
|
||||
</button>
|
||||
<button
|
||||
class="dir-btn"
|
||||
class:active={targetDirection === 1}
|
||||
on:click={() => { targetDirection = 1; setDirection(); }}
|
||||
>
|
||||
180°
|
||||
</button>
|
||||
<button
|
||||
class="dir-btn"
|
||||
class:active={targetDirection === 2}
|
||||
on:click={() => { targetDirection = 2; setDirection(); }}
|
||||
>
|
||||
Bi-Dir
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -310,6 +337,24 @@
|
||||
border-bottom: 2px solid rgba(79, 195, 247, 0.3);
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.interlock-badge {
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
border: 2px solid;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
@@ -447,267 +492,76 @@
|
||||
.direction-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr auto;
|
||||
gap: 12px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
input[type="number"],
|
||||
select {
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
border: 1px solid rgba(79, 195, 247, 0.3);
|
||||
padding: 12px 16px;
|
||||
border: 2px solid rgba(79, 195, 247, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
input[type="number"]:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: #4fc3f7;
|
||||
box-shadow: 0 0 12px rgba(79, 195, 247, 0.3);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
button {
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
background: rgba(79, 195, 247, 0.08);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #4fc3f7 0%, #03a9f4 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 16px rgba(79, 195, 247, 0.4);
|
||||
.dir-btn:hover {
|
||||
border-color: rgba(79, 195, 247, 0.6);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(79, 195, 247, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(79, 195, 247, 0.6);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 16px rgba(244, 67, 54, 0.4);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(244, 67, 54, 0.6);
|
||||
}
|
||||
|
||||
.btn-caution {
|
||||
background: linear-gradient(135deg, #ffa726 0%, #fb8c00 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 16px rgba(255, 167, 38, 0.4);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-caution:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(255, 167, 38, 0.6);
|
||||
}
|
||||
|
||||
.btn-toggle {
|
||||
background: rgba(79, 195, 247, 0.1);
|
||||
.dir-btn.active {
|
||||
border-color: #4fc3f7;
|
||||
color: #4fc3f7;
|
||||
border: 1px solid rgba(79, 195, 247, 0.3);
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn-toggle.active {
|
||||
background: rgba(79, 195, 247, 0.2);
|
||||
box-shadow: 0 0 20px rgba(79, 195, 247, 0.4);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Progress */
|
||||
/* Progress Section */
|
||||
.progress-section {
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 193, 7, 0.3);
|
||||
background: rgba(79, 195, 247, 0.1);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid rgba(79, 195, 247, 0.3);
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.progress-section h3 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
color: #4fc3f7;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
border-radius: 12px;
|
||||
height: 20px;
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin: 12px 0;
|
||||
border: 1px solid rgba(79, 195, 247, 0.3);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4fc3f7 0%, #66bb6a 100%);
|
||||
background: linear-gradient(90deg, #4fc3f7, #03a9f4);
|
||||
transition: width 0.3s ease;
|
||||
box-shadow: 0 0 12px rgba(79, 195, 247, 0.6);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
text-align: center;
|
||||
color: #4fc3f7;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Elements */
|
||||
.elements-section {
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(79, 195, 247, 0.2);
|
||||
}
|
||||
|
||||
.elements-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.element-item {
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(79, 195, 247, 0.2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.element-label {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.element-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #66bb6a;
|
||||
}
|
||||
|
||||
/* Calibration */
|
||||
.calibration-section {
|
||||
background: rgba(255, 152, 0, 0.05);
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 152, 0, 0.3);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.calibration-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
background: rgba(255, 152, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid #ffa726;
|
||||
color: #ffa726;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
@@ -715,13 +569,4 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.freq-control {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.elements-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user