diff --git a/CLAUDE.md b/CLAUDE.md index a3a22a2..e2fcaf5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -174,16 +174,15 @@ checkbox to keep the new tag out of "latest". the banner when empty — don't show a stale message. - **The server fans out a snapshot on connect.** Don't issue `resync` preemptively on connect — the hub already does it. -- **Range and Alarm are per-channel; firmware ignores them in - auto-channel mode.** F3 (`range_step`) and F4 (`alarm_toggle`) are - silently dropped by the LP-500/700 firmware when `auto_channel == true`. - The server NACKs both verbs in this state with a reason (surfaces in - `MeterViewModel.statusBanner`); the UI also greys out the Range / Alarm - cycle buttons inside `PowerSWRView`'s `ControlsCard` (and the duplicate - Range/Alarm keys in `KeypadView`) when `autoChannel == true`, with a - caption pointing at CH 1–4. The Channel button stays enabled so the - user can cycle out. Only `range_step` and `alarm_toggle` are gated — - `peak_toggle`, `channel_step`, `mode_step` work in any channel state. +- **Range and Alarm are per-channel.** The LP-500 user-guide v5.4 + page 4 documents: "The selection is indexed to the current channel + selection and is saved in memory" for both Range and Alarm. The + meter's front-panel F3 / F4 buttons cycle the *auto-locked + channel's* per-channel setting in CH Auto, so the App's + `range_step` / `alarm_toggle` verbs are enabled in any channel + state. (Earlier code gated these in CH Auto based on a misread + empirical probe — see LP-700-Server PR #3 for the corresponding + server-side revert.) - **Single Controls card with cycle-on-press buttons.** As of the v0.3 redesign, `PowerSWRView` no longer has separate Channel pills, a Range card, a Peak-mode segmented control, and an Alarm card. They @@ -218,12 +217,17 @@ checkbox to keep the new tag out of "latest". as `Ref X.X W` next to the small Avg / Pk labels under the big SWR. Returns `nil` (UI shows `— W`) for SWR < 1 / no TX, so we never paint a misleading 0. -- **Auto-scale fallback** for the power bars when `range == "auto"` - (typical CH-Auto case): `autoScale()` picks the smallest standard - scale (5W..10K) ≥ the highest power in the current snapshot, using +- **Bargraphs always auto-scale** for the display, independent of + the meter's range setting. `autoScale()` picks the smallest standard + from `[1, 2, 5, 10, 25, 50, 100, 250, 500, 1K, 2.5K, 5K, 10K]` ≥ + the highest power in the current snapshot, using `peakHoldW` + `powerPeakW` + `powerAvgW` as a max so the scale stays - stable across a transmission envelope. Mirrors how the meter's - hardware auto-range chooses scale. + stable across a transmission envelope. (1-2-5 progression below + 10 W for QRP resolution; matches the meter's hardware range ladder + above.) This is a display-side choice: a 2 W signal on a 500 W + manual meter range would otherwise show as a 0.4 % sliver, which + is illegible. The Range label still shows the meter's actual + setting; the bar's full-scale is computed from live power. - **Window sizing matches Macexpert SPE** (sibling shack utility): `.frame(minWidth: 380, minHeight: 520)` + `.defaultSize(width: 400, height: 580)`. `.windowResizability(.contentMinSize)` so the user diff --git a/Sources/LP700App/Views/PowerSWRView.swift b/Sources/LP700App/Views/PowerSWRView.swift index cfadef8..7e087e9 100644 --- a/Sources/LP700App/Views/PowerSWRView.swift +++ b/Sources/LP700App/Views/PowerSWRView.swift @@ -71,7 +71,15 @@ extension PowerSWRModel { let baseDisabled = !allowControl || !connected || setupOpen let autoCh = snapshot?.autoChannel == true - let scale = fullScaleW(snapshot?.range) ?? autoScale(snapshot) + // Always auto-scale the bargraph for the visual display, even + // when the meter has a manual range set. The meter's range + // setting still matters on the hardware (alarms are indexed + // to it, the meter's LCD bargraph uses it) but is irrelevant + // for the App's display — a 2 W signal on a 500 W manual + // range would otherwise show as a 0.4 % sliver, which is + // illegible. The range label still shows the meter's actual + // setting; the bar's full-scale is computed from live power. + let scale = autoScale(snapshot) let active = activePower(snapshot) let swr = snapshot?.swr ?? 1.0 let swrTint = swrTintColor(swr) @@ -100,10 +108,17 @@ extension PowerSWRModel { alarmLabel: alarmLabel(snapshot), alarmTint: alarmTint(snapshot), channelDisabled: baseDisabled, - rangeDisabled: baseDisabled || autoCh, + // Range is per-channel on the meter, but F3 / range_step + // is accepted by the firmware in pwr/swr mode regardless + // of auto-channel state — pressing it cycles the + // currently auto-locked channel's range. (Earlier this + // was gated with `|| autoCh` based on a misread of an + // empirical probe; corrected 2026-05-16 against the + // bench LP-700.) + rangeDisabled: baseDisabled, peakDisabled: baseDisabled, alarmDisabled: baseDisabled || autoCh, - rangeNote: autoCh ? perChannelLockNote : nil + rangeNote: nil ), statusMessage: snapshot?.statusMessage ?? "" ) @@ -112,8 +127,6 @@ extension PowerSWRModel { // MARK: - Pure helpers -private let perChannelLockNote = "Switch to CH 1–4 to use; auto-channel locks per-channel settings." - private func formatScaleLabel(_ w: Double) -> String { if w >= 1000 { return String(format: "0 / %g kW", w / 1000.0) } return String(format: "0 / %g W", w) @@ -193,34 +206,20 @@ private func powerBar(for watts: Double?, scale: Double, baseTint: Color) -> Bar return BarConfig(fraction: quantized, scale: scale, baseTint: baseTint) } -private func fullScaleW(_ range: String?) -> Double? { - guard let r = range?.lowercased(), !r.isEmpty, r != "auto" else { return nil } - switch r { - case "5w": return 5 - case "10w": return 10 - case "25w": return 25 - case "50w": return 50 - case "100w": return 100 - case "250w": return 250 - case "500w": return 500 - case "1k": return 1000 - case "2.5k": return 2500 - case "5k": return 5000 - case "10k": return 10000 - default: return nil - } -} - -// Fallback when range is "auto" or unknown (the typical CH-Auto case): -// pick the smallest standard scale that comfortably contains the highest -// power in the current snapshot. `peakHoldW` is the firmware-maintained -// sticky peak (server fc9bde0+), which gives a stable scale across the -// natural envelope of a transmission and resets cleanly the moment the +// Bargraph full-scale: pick the smallest standard scale that +// comfortably contains the highest power in the current snapshot. +// `peakHoldW` is the firmware-maintained sticky peak (server +// fc9bde0+), which gives a stable scale across the natural envelope +// of a transmission and resets cleanly the moment the // operator clears Peak Hold on the meter — no client-side decay loop // needed. private func autoScale(_ snap: Snapshot?) -> Double { let peak = max(snap?.powerPeakW ?? 0, snap?.peakHoldW ?? 0, snap?.powerAvgW ?? 0) - let standards: [Double] = [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000] + // 1-2-5 progression below 10W (QRP regime), then matches the + // LP-700's hardware range ladder (10, 25, 50, 100, 250, 500, 1K, + // 2.5K, 5K, 10K) above so the App display steps align with the + // meter's physical range labels. + let standards: [Double] = [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000] return standards.first(where: { $0 >= peak }) ?? 10000 }