From 8eccb02c0357610af99e4d3198c13d0f1d997cbe Mon Sep 17 00:00:00 2001 From: Manoj Kumar R Date: Sun, 17 May 2026 00:21:07 +0530 Subject: [PATCH] PowerSWRView: enable Range in CH Auto + always auto-scale the bargraph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related fixes pushing back on a UX regression introduced by PR #4 (the hardware-style combined Power & SWR card): 1. Range button (and the matching keypad shortcut) was greyed out whenever the meter was in CH Auto, on the conclusion that the firmware silently ignores F3 in that state. The manufacturer's user-guide v5.4 (page 4 + page 9 "Recommended Usage") clarifies that the Range and Alarm buttons are per-channel, and in CH Auto they cycle the *auto-locked channel's* setting — F3 / F4 on the meter's front panel do work in pwr/swr + CH Auto. Confirmed against the bench LP-700 on 2026-05-16. (LP-700-Server PR #3 reverts the matching server-side NACK.) 2. The bargraph's full-scale tracked the meter's manual range setting (e.g. Rng 500W → bar full-scale 500W), which made a 2 W signal show as a 0.4 % sliver and was effectively unusable for low-power tests. The bar now always auto-scales to the smallest standard ≥ the highest power in the current snapshot, independent of the meter's range setting. The Range label still shows the meter's actual setting; only the bar's full-scale changes. The auto-scale ladder is `[1, 2, 5, 10, 25, 50, 100, 250, 500, 1K, 2.5K, 5K, 10K]` — 1-2-5 progression below 10 W for QRP resolution, then matches the LP-700's hardware range ladder above so the App's display steps align with the meter's physical labels. Drops the now-dead `fullScaleW(_:)` helper and the `perChannelLockNote` string that was shown under the disabled Range button. CLAUDE.md updated to replace the "firmware-ignores-F3/F4-in-CH-Auto" quirk note with the corrected per-channel explanation, and to document the always-auto-scale display choice. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 34 ++++++++------ Sources/LP700App/Views/PowerSWRView.swift | 57 +++++++++++------------ 2 files changed, 47 insertions(+), 44 deletions(-) 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 }