From d26d73740a8ce8fcf1f72576a16eea1378173795 Mon Sep 17 00:00:00 2001 From: Frank Tornack Date: Mon, 16 Feb 2026 14:05:44 +0100 Subject: [PATCH 1/2] Add HFJ-350M antenna calculator panel --- src/DockableApp.jsx | 8 +- src/components/HFJ350MPanel.jsx | 291 ++++++++++++++++++++++++++++++++ src/components/index.js | 3 +- src/layouts/ModernLayout.jsx | 8 +- src/store/layoutStore.js | 1 + 5 files changed, 308 insertions(+), 3 deletions(-) create mode 100644 src/components/HFJ350MPanel.jsx diff --git a/src/DockableApp.jsx b/src/DockableApp.jsx index 411a422f..8d3e140a 100644 --- a/src/DockableApp.jsx +++ b/src/DockableApp.jsx @@ -24,7 +24,8 @@ import { AnalogClockPanel, RigControlPanel, OnAirPanel, - IDTimerPanel + IDTimerPanel, + HFJ350MPanel } from './components'; import { loadLayout, saveLayout, DEFAULT_LAYOUT } from './store/layoutStore.js'; @@ -257,6 +258,7 @@ export const DockableApp = ({ 'contests': { name: 'Contests', icon: '🏆' }, ...(hasAmbient ? { 'ambient': { name: 'Ambient Weather', icon: '🌦️' } } : {}), 'rig-control': { name: 'Rig Control', icon: '📻' }, + 'hfj350m-calc': { name: 'HFJ-350M Calc', icon: '📏' }, 'on-air': { name: 'On Air', icon: '🔴' }, 'id-timer': { name: 'ID Timer', icon: '📢' }, }; @@ -601,6 +603,10 @@ export const DockableApp = ({ content = ; break; + case 'hfj350m-calc': + content = ; + break; + default: content = (
diff --git a/src/components/HFJ350MPanel.jsx b/src/components/HFJ350MPanel.jsx new file mode 100644 index 00000000..04170fb6 --- /dev/null +++ b/src/components/HFJ350MPanel.jsx @@ -0,0 +1,291 @@ +import React, { useState, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +// Antenna data from the manual +const ANTENNA_DATA = [ + { + band: "160m", + freq_range: [1.8, 2.0], + std_freq: 1.8, + coil: "Basis + 3.5 Spule + 1.8 Spule", + jumper: "Kein Jumper", + length_mm: 1170, + radial: "> 20m (ideal 40m)", + change_per_cm: 7, // 7 kHz/cm + note: "Extrem schmalbandig! Tuner fast immer nötig." + }, + { + band: "80m", + freq_range: [3.5, 3.8], + std_freq: 3.5, + coil: "Basis + 3.5 Spule", + jumper: "Kein Jumper", + length_mm: 910, + radial: "ca. 20m", + change_per_cm: 20, // 20 kHz/cm + note: "" + }, + { + band: "40m", + freq_range: [7.0, 7.2], + std_freq: 7.0, + coil: "Basis (Keine Zusatzspule)", + jumper: "Kein Jumper", + length_mm: 960, + radial: "ca. 12m", + change_per_cm: 25, // 25 kHz/cm + note: "Standard-Band für Portable." + }, + { + band: "30m", + freq_range: [10.1, 10.15], + std_freq: 10.1, + coil: "Basis", + jumper: "Terminal 1", + length_mm: 990, + radial: "ca. 7-8m", + change_per_cm: 40, // 40 kHz/cm + note: "" + }, + { + band: "20m", + freq_range: [14.0, 14.35], + std_freq: 14.0, + coil: "Basis", + jumper: "Terminal 2", + length_mm: 800, + radial: "ca. 5m", + change_per_cm: 60, // 60 kHz/cm + note: "" + }, + { + band: "17m", + freq_range: [18.068, 18.168], + std_freq: 18.0, + coil: "Basis", + jumper: "Terminal 3 (oder 2)", + length_mm: 1070, + radial: "ca. 4m", + change_per_cm: 50, + note: "Bei hohem SWR Terminal 2 testen." + }, + { + band: "15m", + freq_range: [21.0, 21.45], + std_freq: 21.0, + coil: "Basis", + jumper: "Terminal 3", + length_mm: 750, + radial: "ca. 3.5m", + change_per_cm: 80, // 80 kHz/cm + note: "" + }, + { + band: "12m", + freq_range: [24.89, 24.99], + std_freq: 24.9, + coil: "Basis", + jumper: "Terminal 3", + length_mm: 530, + radial: "ca. 3m", + change_per_cm: 100, // 100 kHz/cm + note: "" + }, + { + band: "10m", + freq_range: [28.0, 29.7], + std_freq: 28.5, + coil: "Basis", + jumper: "Terminal 4", + length_mm: 1000, + radial: "ca. 2.5m", + change_per_cm: 120, // 120 kHz/cm + note: "Teleskop NICHT voll ausziehen! Reserve ~26cm." + }, + { + band: "6m", + freq_range: [50.0, 52.0], + std_freq: 51.0, + coil: "Basis", + jumper: "Terminal 5", + length_mm: 950, + radial: "ca. 1.5m", + change_per_cm: 100, // 100 kHz/cm + note: "Achtung: Terminal 5 = Common + 5" + } +]; + +export const HFJ350MPanel = () => { + const { t } = useTranslation(); + const [input, setInput] = useState(""); + const [result, setResult] = useState(null); + + const calculate = (query) => { + if (!query) { + setResult(null); + return; + } + const queryStr = String(query).toLowerCase().trim(); + let targetFreq = null; + let data = null; + + // Check if input is a band name + data = ANTENNA_DATA.find(d => { + const bandName = d.band.replace("m", ""); + return queryStr === d.band.toLowerCase() || queryStr === bandName; + }); + + // Check if input is a frequency + if (!data) { + const freq = parseFloat(queryStr.replace(',', '.')); + if (!isNaN(freq)) { + targetFreq = freq; + data = ANTENNA_DATA.find(d => { + const [low, high] = d.freq_range; + return (low - 0.5) <= freq && freq <= (high + 1.0); + }); + } + } + + if (!data) { + setResult({ error: "Keine Konfiguration gefunden." }); + return; + } + + let calcLenMm = data.length_mm; + let diffMm = 0; + let warning = ""; + + if (targetFreq) { + const diffKhz = (targetFreq - data.std_freq) * 1000; + const changeCm = diffKhz / data.change_per_cm; + calcLenMm = Math.round(data.length_mm - (changeCm * 10)); + + if (calcLenMm > 1266) { + warning = "Max überschritten!"; + calcLenMm = 1266; + } else if (calcLenMm < 100) { + warning = "Zu kurz!"; + calcLenMm = 100; + } + diffMm = calcLenMm - data.length_mm; + } + + setResult({ + ...data, + targetFreq, + calcLenMm, + diffMm, + warning + }); + }; + + const handleInputChange = (e) => { + setInput(e.target.value); + calculate(e.target.value); + }; + + const renderBar = (len, maxLen = 1266, color = "var(--accent-blue)") => { + const percent = Math.min(100, Math.max(0, (len / maxLen) * 100)); + return ( +
+
+
+ ); + }; + + return ( +
+
+

+ 📡 HFJ-350M Calculator +

+
+ + + + {result && !result.error && ( +
+
+
+ Band: + {result.band} +
+
+ Range: + {result.freq_range[0]} - {result.freq_range[1]} MHz +
+
+ +
+
SETUP
+
+ Coil: + {result.coil} + Jumper: + {result.jumper} + Radial: + {result.radial} +
+
+ +
+
TELESCOPE LENGTH
+ +
+
+ Standard ({result.std_freq} MHz): + {result.length_mm} mm +
+ {renderBar(result.length_mm, 1266, 'var(--accent-amber)')} +
+ + {result.targetFreq && ( +
+
+ Calc ({result.targetFreq} MHz): + + {result.calcLenMm} mm + {result.warning && ⚠ {result.warning}} + +
+ {renderBar(result.calcLenMm, 1266, 'var(--accent-purple)')} +
0 ? 'var(--accent-green)' : 'var(--accent-red)' }}> + Diff: {result.diffMm > 0 ? '+' : ''}{result.diffMm} mm +
+
+ )} +
+ +
+
Sensitivity: {result.change_per_cm} kHz/cm
+ {result.note &&
⚠ {result.note}
} +
+
+ )} + + {result && result.error && ( +
+ {result.error} +
+ )} +
+ ); +}; + +export default HFJ350MPanel; diff --git a/src/components/index.js b/src/components/index.js index 68317dbb..7bb91ea8 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -29,4 +29,5 @@ export { default as RigControlPanel } from './RigControlPanel.jsx'; export { default as OnAirPanel } from './OnAirPanel.jsx'; export { IDTimerPanel } from './IDTimerPanel.jsx'; export { default as RotatorPanel } from './RotatorPanel.jsx'; -export { default as RotatorMapOverlay } from './RotatorMapOverlay.jsx'; \ No newline at end of file +export { default as RotatorMapOverlay } from './RotatorMapOverlay.jsx'; +export { default as HFJ350MPanel } from './HFJ350MPanel.jsx'; \ No newline at end of file diff --git a/src/layouts/ModernLayout.jsx b/src/layouts/ModernLayout.jsx index ea4e662f..d09296a0 100644 --- a/src/layouts/ModernLayout.jsx +++ b/src/layouts/ModernLayout.jsx @@ -12,7 +12,8 @@ import { DXpeditionPanel, PSKReporterPanel, WeatherPanel, - AnalogClockPanel + AnalogClockPanel, + HFJ350MPanel } from '../components'; import { useRig } from '../contexts/RigContext.jsx'; @@ -268,6 +269,11 @@ export default function ModernLayout(props) { propConfig={config.propagation} /> )} + + {/* HFJ-350M Antenna Calculator */} +
+ +
)} diff --git a/src/store/layoutStore.js b/src/store/layoutStore.js index 190c45b4..0b76d7ce 100644 --- a/src/store/layoutStore.js +++ b/src/store/layoutStore.js @@ -104,6 +104,7 @@ export const PANEL_DEFINITIONS = { 'id-timer': { name: 'ID Timer', icon: '📢', description: '10-minute station identification reminder' }, 'world-map': { name: 'World Map', icon: '🗺️', description: 'Interactive world map' }, 'rig-control': { name: 'Rig Control', icon: '📻', description: 'Transceiver control and feedback' }, + 'hfj350m-calc': { name: 'HFJ-350M Calc', icon: '📏', description: 'Antenna calculator for Comet HFJ-350M' }, 'on-air': { name: 'On Air', icon: '🔴', description: 'Large TX status indicator' }, }; From 3c7d087d0321e7d980312699e8be61c2370aec0f Mon Sep 17 00:00:00 2001 From: Frank Tornack Date: Mon, 16 Feb 2026 17:13:29 +0100 Subject: [PATCH 2/2] Add localStorage persistence to HFJ-350M calculator --- src/components/HFJ350MPanel.jsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/components/HFJ350MPanel.jsx b/src/components/HFJ350MPanel.jsx index 04170fb6..27e5e578 100644 --- a/src/components/HFJ350MPanel.jsx +++ b/src/components/HFJ350MPanel.jsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; // Antenna data from the manual @@ -180,9 +180,20 @@ export const HFJ350MPanel = () => { }); }; + // Load last input from localStorage on mount + useEffect(() => { + const savedInput = localStorage.getItem('hfj350m-last-input'); + if (savedInput) { + setInput(savedInput); + calculate(savedInput); + } + }, []); + const handleInputChange = (e) => { - setInput(e.target.value); - calculate(e.target.value); + const value = e.target.value; + setInput(value); + calculate(value); + localStorage.setItem('hfj350m-last-input', value); }; const renderBar = (len, maxLen = 1266, color = "var(--accent-blue)") => {