Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
57 changes: 28 additions & 29 deletions Sources/LP700App/Views/PowerSWRView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 ?? ""
)
Expand All @@ -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)
Expand Down Expand Up @@ -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
}

Expand Down