diff --git a/CHANGELOG.md b/CHANGELOG.md index 43f00b9..9f0123e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,8 +23,10 @@ and [`docs/requirements/system-requirements.md`](docs/requirements/system-requir (Operate/Standby, forward power, SWR, faults) and, in the KPA1500 window, full telemetry plus Operate/Standby, ATU in/bypass and antenna controls. Only the KPA1500 is supported — the KPA500 and KAT500 have no network - interface of their own. (A one-touch ATU tune, which needs the K4 keyed, - comes in a later release.) + interface of their own. A one-touch **ATU TUNE** in the amp window keys the + radio and runs the amplifier's tune, then drops the carrier when it finishes + (or after a safety timeout); it is arm-gated and stopped by the emergency + stop, like every other transmit action. ## [0.9.0] — 2026-07-27 diff --git a/app/src/main.rs b/app/src/main.rs index e608663..d4c8e84 100644 --- a/app/src/main.rs +++ b/app/src/main.rs @@ -91,6 +91,19 @@ fn kpa1500_window_settings() -> iced::window::Settings { } } +/// An in-progress amplifier ATU tune (FR-AMP-04). `saw_tuning` records that the +/// amp has reported its tune actually started (`^TP1`), so completion is "started +/// then stopped" rather than "not yet started". +struct AmpTune { + started: Instant, + saw_tuning: bool, +} + +/// Hard cap on an amplifier tune: the K4 carrier is dropped after this even if +/// the amp never reports completion, so a hung tune can't strand the transmitter +/// on air. +const AMP_TUNE_TIMEOUT: Duration = Duration::from_secs(20); + struct App { // connection form host: String, @@ -232,6 +245,9 @@ struct App { kpa_tx: Sender, kpa: kpa::Shared, kpa_want: bool, + // An amplifier ATU tune in progress (FR-AMP-04): keys the K4 carrier and + // sends `^FT`, then drops the carrier on completion or a hard timeout. + amp_tune: Option, // Diagnostics log: show/hide + follow-newest (auto-scroll). show_log: bool, log_autoscroll: bool, @@ -794,6 +810,7 @@ enum Message { KpaSetMode(bool), KpaSetAtu(bool), KpaAntenna(u8), + KpaTune, ToggleLogAutoscroll, LogFilterChanged(String), /// A text-editor action from the read-only log view (selection/scroll; edits @@ -1050,6 +1067,7 @@ impl App { kpa_tx, kpa: kpa::Shared::default(), kpa_want: false, + amp_tune: None, show_log: false, log_autoscroll: true, log_refresh_div: 0, @@ -2297,7 +2315,14 @@ impl App { } } Message::ToggleKey => self.send(WorkerCmd::Key(!self.ui.transmitting)), - Message::EmergencyStop => self.send(WorkerCmd::EmergencyStop), + Message::EmergencyStop => { + // Abort any amplifier tune too: the worker's e-stop drops the K4 + // carrier (TU0), and we tell the amp to stop searching (`^FE`). + if self.amp_tune.take().is_some() { + self.kpa_send(k4_kpa::cat::cancel_tune()); + } + self.send(WorkerCmd::EmergencyStop); + } Message::PressDown => self.press_at = Some(std::time::Instant::now()), Message::TapOrHold(tap, hold) => { // The radio's own convention: tap and hold are different @@ -2578,6 +2603,23 @@ impl App { self.kpa_send(cmd); } } + Message::KpaTune => { + // An amplifier ATU tune keys the K4 carrier, so it is arm-gated + // exactly like the K4's own tune (FR-TX-SAFE-06): refused and + // flashing ARM TX when disarmed. When armed and the amp is up, + // key a steady carrier (TU1) and start the amp's search (`^FT`); + // the tick reconciler drops the carrier when it finishes. + if !self.ui.tx_armed { + self.arm_flash = ui::ARM_FLASH_TICKS; + } else if self.kpa.conn == kpa::Conn::Connected && self.amp_tune.is_none() { + self.send(WorkerCmd::Tune(k4_protocol::cat::TuneAction::Tune)); + self.kpa_send(k4_kpa::cat::start_tune()); + self.amp_tune = Some(AmpTune { + started: Instant::now(), + saw_tuning: false, + }); + } + } Message::ToggleLogAutoscroll => { self.log_autoscroll = !self.log_autoscroll; if self.log_autoscroll { @@ -2839,6 +2881,37 @@ impl App { let _ = self.kpa_tx.send(kpa::Cmd::Disconnect); } } + // Drive an in-progress amplifier ATU tune to its end (FR-AMP-04): + // when the amp finishes, a hard timeout elapses, or a precondition + // drops, drop the K4 carrier and cancel the amp tune if it is + // still searching. + if self.amp_tune.is_some() { + let amp_tuning_now = self.kpa.state.tuning == Some(true); + if amp_tuning_now { + if let Some(t) = self.amp_tune.as_mut() { + t.saw_tuning = true; + } + } + let t = self.amp_tune.as_ref().expect("checked is_some"); + let end = ui::amp_tune_should_end( + t.saw_tuning, + amp_tuning_now, + t.started.elapsed(), + AMP_TUNE_TIMEOUT, + self.kpa.conn == kpa::Conn::Connected, + self.ui.connected, + self.ui.tx_armed, + ); + if end { + self.amp_tune = None; + self.send(WorkerCmd::Tune(k4_protocol::cat::TuneAction::Exit)); + // If it was still searching (timeout / precondition drop + // rather than natural completion), tell the amp to stop. + if amp_tuning_now { + self.kpa_send(k4_kpa::cat::cancel_tune()); + } + } + } // Seed the config screens from the radio's reported values once // per connection, as the connect GET burst lands (FR-UI-19). if self.ui.phase == ui::ConnPhase::Connected { @@ -6286,7 +6359,17 @@ impl App { // Controls. Operate/Standby keys the amp in/out of line; the ATU and // antenna mirror the front panel. (A full ATU tune needs the K4 keyed // and is deferred to a later slice.) - let controls = Column::new() + // An ATU tune keys the K4, so it is arm-gated: the label reflects an + // in-progress tune, and a hint names the arm requirement (the ARM TX + // control itself lives in the main window). + let tuning = self.amp_tune.is_some(); + let tune_label = if tuning { "TUNING…" } else { "ATU TUNE" }; + let tune_row = Row::new().spacing(6).push(small_btn_stable( + tune_label.to_string(), + &["ATU TUNE", "TUNING…"], + Message::KpaTune, + )); + let mut controls = Column::new() .spacing(6) .push(Text::new("Controls").size(12).color(dim)) .push( @@ -6302,7 +6385,15 @@ impl App { .push(small_btn("ATU BYP", Message::KpaSetAtu(false))) .push(small_btn("ANT 1", Message::KpaAntenna(1))) .push(small_btn("ANT 2", Message::KpaAntenna(2))), + ) + .push(tune_row); + if !self.ui.tx_armed { + controls = controls.push( + Text::new("arm TX (main window) to run a tune") + .size(11) + .color(dim), ); + } col.push(controls).into() } diff --git a/app/src/ui.rs b/app/src/ui.rs index ea59ac8..e2f0e36 100644 --- a/app/src/ui.rs +++ b/app/src/ui.rs @@ -1950,6 +1950,30 @@ pub fn amp_indicator( } } +/// Decide whether an in-progress amplifier ATU tune should **end** and the K4 +/// carrier be dropped (FR-AMP-04). +/// +/// It ends when the amp finished (we saw its tune start, and it is no longer +/// tuning), when a hard safety timeout elapses, or when any precondition falls +/// away (the amp or K4 link dropped, or TX was disarmed mid-tune). The timeout +/// is the safety net: a tune keys the transmitter, so the carrier must never be +/// left on if the amp's tune hangs or its `^TP` reply is missed. +pub fn amp_tune_should_end( + saw_tuning: bool, + amp_tuning_now: bool, + elapsed: std::time::Duration, + timeout: std::time::Duration, + amp_connected: bool, + k4_connected: bool, + tx_armed: bool, +) -> bool { + (saw_tuning && !amp_tuning_now) + || elapsed >= timeout + || !amp_connected + || !k4_connected + || !tx_armed +} + #[cfg(test)] mod tests { use super::*; @@ -2009,6 +2033,40 @@ mod tests { ); } + /// An amp ATU tune ends on completion, on a hard timeout (even if the amp + /// never reported starting), and on any dropped precondition — but keeps + /// running while the amp is mid-tune with everything still up. + /// trace: FR-AMP-04 + #[test] + fn fr_amp_04_tune_end_conditions() { + use std::time::Duration; + let short = Duration::from_secs(1); + let long = Duration::from_secs(30); + // Mid-tune, all preconditions holding, within the timeout → keep going. + assert!(!amp_tune_should_end( + true, true, short, long, true, true, true + )); + // Saw it start and it is no longer tuning → done. + assert!(amp_tune_should_end( + true, false, short, long, true, true, true + )); + // Hard timeout ends it even if the amp never reported tuning (missed + // `^TP`), so the carrier can't be stranded on. + assert!(amp_tune_should_end( + false, true, long, short, true, true, true + )); + // Any dropped precondition ends it immediately. + assert!(amp_tune_should_end( + true, true, short, long, false, true, true + )); + assert!(amp_tune_should_end( + true, true, short, long, true, false, true + )); + assert!(amp_tune_should_end( + true, true, short, long, true, true, false + )); + } + /// Each connection phase reports a distinct status label to the UI, and each /// failure kind maps to a distinguishable human-readable reason. /// diff --git a/docs/requirements/system-requirements.md b/docs/requirements/system-requirements.md index 6508cc8..2f1372b 100644 --- a/docs/requirements/system-requirements.md +++ b/docs/requirements/system-requirements.md @@ -1,7 +1,7 @@ --- title: "System Requirements Specification" status: Draft -version: "0.54" +version: "0.55" updated: 2026-08-06 authors: - Simon Keimer (DC0SK) @@ -299,6 +299,7 @@ syntax per the Programmer's Reference D12, cross-checked vs QK4 (`R-EXT-03`).* | ID | Statement | Up | Pri | Ver | Acceptance criteria | |---|---|---|---|---|---| | `FR-AMP-01` | provide **KPA1500 amplifier support**, opt-in (default off): an enable toggle in Settings and a **separate configuration window** for the amplifier's connection — host, TCP port (default **1500**) and telemetry poll interval — with the settings persisted across sessions. The link is to the KPA1500's **own Ethernet CAT server** (KPA1500 Programming Reference `^CP`/`^IP`), a second connection alongside the K4, **not** the K4's one-way `EC` passthrough (D12 `EC` is SET-only and, per its own note, makes the K4 ignore the amplifier's replies until restart — unusable for a telemetry-aware panel). Live metering and control of the amplifier are a deliberate follow-up, out of scope for this requirement. Only the KPA1500 is in scope; the KPA500 and KAT500 are serial-only, have no Ethernet server, and are excluded. | STK-11 | S | T/D | `Prefs` defaults `kpa1500_enabled=false`, `kpa1500_port=1500`, `kpa1500_poll_ms=500`; the enable flag and host/port/poll survive a TOML round-trip, and a pre-feature config (no KPA fields) loads with support off (test `fr_amp_01_kpa1500_defaults_off_and_persists`); in the app, the Settings toggle enables support and the button opens the separate KPA1500 configuration window (demo). | +| `FR-AMP-04` | provide a **one-touch amplifier ATU tune**: an **ATU TUNE** control in the amp window that keys the K4 with a steady carrier (`TU1`) and starts the amplifier's full-search tune (`^FT`), then **drops the carrier** (`TU0`) when the amp reports completion (`^TP`), when a hard timeout elapses, or when a precondition drops (amp/K4 link down, or TX disarmed). Because it keys the transmitter it is **arm-gated** exactly like the K4's own tune — refused with the ARM-TX flash when disarmed — and it is aborted by the emergency stop (which also sends `^FE` to stop the amp searching). The hard timeout guarantees the carrier is never stranded on air if the amp's tune hangs. | STK-11 | S | T/D | `amp_tune_should_end` ends the tune on completion, on timeout even if the amp never reported starting, and on any dropped precondition, but not while mid-tune with all preconditions holding (test `fr_amp_04_tune_end_conditions`); on a real/mock amp, ATU TUNE keys the carrier + starts the search and the carrier drops on completion, the arm gate refuses when disarmed, and the e-stop aborts it (demo/HIL). | | `FR-AMP-03` | **connect to the KPA1500 and show it live**: a background worker opens the amplifier's TCP command server, polls it on the configured interval, parses the replies (via `FR-AMP-02`) and publishes them to the UI; a **top-status-bar amp indicator** (shown only while support is enabled) reflects Operate/Standby/fault with live power + SWR and opens the amp window on click; the **KPA1500 window** shows the connection state, live telemetry (mode, power, SWR, temperature, band, antenna, ATU state, fan, fault, firmware) and **controls** (Operate/Standby, ATU in/bypass, antenna). The connection is **gated on K4 connectivity** — it comes up only while the K4 is connected and support is enabled with a host set, and drops otherwise; a transport error is surfaced, not silent. A full ATU tune (which needs the K4 keyed) is a later slice. | STK-11 | S | T/D | The amp indicator's colour precedence and wording are a pure function of connection + telemetry — fault (red) beats mode, Operate is green (Caution on SWR ≥ 2.0) with live power/SWR, Standby amber, down-link dim (test `fr_amp_03_amp_indicator_precedence_and_wording`); on a real/mock amplifier the worker connects, the panel fills and the controls key the amp, and the link follows K4 connect/disconnect (demo/HIL). | | `FR-AMP-02` | provide a **pure KPA1500 CAT codec** (`k4-kpa` crate) — **encode** the control commands (operate/standby `^OS`, ATU tune `^FT`/`^FE`, ATU mode `^AM`, antenna `^AN`, power `^ON`) in their exact Programming-Reference wire forms, and **parse** the amplifier's telemetry responses (`^OS` mode, `^WS`/`^PWF`/`^PWR` power, `^SW` SWR, `^TM` temperature, `^FL` fault code + description, `^BN` band, `^AI`/`^AM` ATU state, `^AN` antenna, `^FS` fan, `^TP` tune-in-progress, `^RVM` firmware, `^SN` serial, `^I` identity) into a `KpaState` whose fields stay `None` until first reported. Dependency-free and offline; the socket I/O and poll loop are a later slice. | STK-11 | S | T | Control encoders emit the documented `^…;` strings and antenna select uses the two-digit `^AN01;`..`^AN32;` form (never `^AN0;` = "next"); each telemetry RESP populates its field; fault codes decode from hex and map to text; a malformed or bare-tag fragment neither matches nor blanks an already-known field (tests `fr_amp_02_*`). | @@ -390,6 +391,7 @@ syntax per the Programmer's Reference D12, cross-checked vs QK4 (`R-EXT-03`).* | 2026-07-25 | 0.48 | DC0SK | Added FR-FM-02 as a **DTMF keypad** (`DM`) — the part of the gap-analysis item that is both buildable and useful remotely: sending DTMF for repeater/link control cannot be done any other way over the link. A 4×4 popup opened from the FM panel, one `DM;` per key. **Scoped down from the gap analysis on purpose:** the '6 stored DTMF sequences' are config work deferred to a follow-up, and the **1750 Hz tone burst has no documented CAT command** in D12 (searched), so it is not buildable now rather than guessed at — the `RO`/`RA` lesson. `send_dtmf` refuses a non-DTMF character rather than emitting a malformed `DM`. | | 2026-07-25 | 0.49 | DC0SK | Added FR-XVTR-01 (transverter band setup), the last substantial backlog item — complex and niche (transverter operators), but fully documented so buildable without hardware-guessing. Six `XV*` encoders (`XVN`/`XVM`/`XVR`/`XVI`/`XVO`/`XVP`), a read-back parser for each field, and a setup form on the BAND screen. The design turns on `XVN` being **stateful** — it selects the band the other commands target — so every field send is prefixed with `XVN`, and the form re-reads all fields when a band is picked, keyed on the `XVN` the radio confirms so a stale value never lands. The form outgrew the fixed-height config-screen slot and clipped; fixed by compacting it to three rows and wrapping the BAND screen in a scrollable. Deferred, and said so: the **mW power scale on XVTR bands** (showing mW instead of W when operating on a configured transverter band) — it needs the current-band-is-XVTR state wired through, and is an operating-display concern separate from this setup form. | | 2026-07-26 | 0.50 | DC0SK | Added FR-UI-UPD-02 (automatic update check + top-area notification), requested by DC0SK; **recorded, not yet implemented**. It is a deliberate, operator-chosen relaxation of FR-UI-UPD-01, which made the update check *manual-only* on the reasoning that "a radio-control app should not make unannounced outbound connections, and a remote station may be on a metered link." The automatic check is therefore constrained to bound that cost: default-on but **opt-out in Settings**, **once per start** rather than on a timer, and **silent** unless it finds a substantiated newer release — so a metered link sees at most one small request per launch, and only a real update ever draws attention. The notification lives in the top status area beside the connection indicator (not a modal), as a clickable link to the release page, reusing FR-UI-UPD-01's numeric, never-spurious comparison. Also fixed a stale/duplicated `version` block in this document's YAML frontmatter (a merge artifact: two `version:` keys) — set to 0.50 / 2026-07-26. | +| 2026-08-06 | 0.55 | DC0SK | Added **FR-AMP-04 (amplifier ATU tune)** and built it: a one-touch **ATU TUNE** in the amp window that keys the K4 carrier (`TU1`) + starts the amp search (`^FT`), then drops the carrier (`TU0`) on completion (`^TP` seen start→stop), a **20 s hard timeout**, or any dropped precondition. Reuses the K4 tune's safety model — arm-gated (ARM-TX flash when disarmed, FR-TX-SAFE-06), and the emergency stop aborts it and sends `^FE`. The end-decision is a pure, tested function (`ui::amp_tune_should_end`), the reconciliation runs on the tick. **Deferred no longer** — this closes the item FR-AMP-03 held back. The timeout is the key safety property: a tune keys the transmitter, so the carrier must never be stranded if the amp hangs or a `^TP` reply is missed. Evidence: L1 for the end-logic + UI render; the armed carrier round-trip is a TX path → HIL. | | 2026-08-06 | 0.54 | DC0SK | Added **FR-AMP-03 (KPA1500 live client)** and built it: a background worker (`app/src/kpa.rs`) mirroring the K4 worker's shared-snapshot model — owns the amp's TCP socket, polls `k4_kpa::POLL` on interval, parses replies, and publishes a `KpaState` + connection status the UI copies on tick. A top-bar amp indicator (enabled-only, click opens the amp window) and the amp window's live telemetry + Operate/Standby/ATU/antenna controls. Connection is **reconciled off the tick** against `enabled && K4-connected && host-set`, so it follows the K4 link, the enable toggle and host edits without per-event wiring; transport errors surface in the window. The indicator's colour/wording precedence is a pure, tested function (`fr_amp_03_*`). Deferred: a full ATU tune (needs the K4 keyed — couples to the arm-gated TX path). Evidence: L1 for the indicator; the socket path is D/HIL (a mock or real amp). | | 2026-08-06 | 0.53 | DC0SK | Added **FR-AMP-02 (KPA1500 CAT codec)** and built it: the new dependency-free `k4-kpa` crate, encoders + response parser + `KpaState`, from the KPA1500 Programming Reference V3 wire formats (read end to end — not guessed, the `RO`/`RA` lesson). Notable format facts pinned down: `^WS` returns forward power **and** SWR in one reply (`^WS1204 014;`); `^SW` gives SWR in tenths; `^FL` is a **hex** fault byte with a documented code table; `^AN` two-digit setters (`^AN01;`) avoid the `^AN0;`="next-antenna" trap; `^AMI;`/`^AMB;` are the ATU-mode form (distinct from the `^AI` relay state). Parser keeps every field `None` until first heard and — a bug the tests caught — never blanks a known field on a malformed/bare-tag fragment. Pure L1 slice: **no** socket I/O or UI yet (next slice wires the poll loop + a mini-panel), so this carries unit evidence only. Tests `fr_amp_02_*` in `k4-kpa`. | | 2026-08-06 | 0.52 | DC0SK | Added **section O: FR-AMP-01 (KPA1500 amplifier support)** and implemented its first slice — the enable toggle + a separate configuration window. Decision on record: the KPA1500 is reached over **its own Ethernet CAT server** (KPA1500 Programming Reference `^CP`/`^IP`), a second connection alongside the K4 — **not** the K4's `EC` passthrough. D12's `EC` is SET-only and its own note says the K4 ignores all incoming RS232 data after an `EC` until restart, so it cannot back a telemetry-aware amp panel; the reference project (QK4) confirms the amp's-own-Ethernet approach with a full polling client. Scope this change: opt-in `Prefs` (enabled/host/port=1500/poll=500 ms) with persistence, a Settings toggle, and a detached window mirroring the diagnostics-window pattern. Live metering/control is the next slice. KPA500/KAT500 excluded and said so — serial-only, no Ethernet server, absent even from QK4. Config-layer test `fr_amp_01_kpa1500_defaults_off_and_persists` covers the default-off + round-trip + legacy-config path. | diff --git a/docs/test/coverage.generated.md b/docs/test/coverage.generated.md index 1d19a50..ab09640 100644 --- a/docs/test/coverage.generated.md +++ b/docs/test/coverage.generated.md @@ -7,6 +7,7 @@ Legend: ✅ test-traced · 🟡 waived (see r3-waivers.md) · ⚪ not test-requi | `FR-AMP-01` | S | T/D | ✅ | | `FR-AMP-02` | S | T | ✅ | | `FR-AMP-03` | S | T/D | ✅ | +| `FR-AMP-04` | S | T/D | ✅ | | `FR-ANT-01` | C | T | ✅ | | `FR-ANT-02` | S | T | ✅ | | `FR-ATU-01` | S | T/D | ✅ | diff --git a/docs/test/test-strategy.md b/docs/test/test-strategy.md index 33030da..c4b5725 100644 --- a/docs/test/test-strategy.md +++ b/docs/test/test-strategy.md @@ -1,7 +1,7 @@ --- title: "Test Strategy & Traceability" status: Draft -version: "4.6" +version: "4.7" updated: 2026-08-06 authors: - Simon Keimer (DC0SK) @@ -437,6 +437,7 @@ FR-SES-MULTI, FR-DIAG-02, etc. — get `TC` IDs when promoted to `Approved`.)* | 2026-07-25 | 4.0 | DC0SK | Release **v0.8.0**. Minor: six backlog features and two operating fixes since 0.7.0, nearly all validated on DC0SK's live K4. Added: VFO lock read-back + tuning refusal, DATA rate select, `ACN` antenna names, on-screen macros (Fn → MACROS, reusing the K-Pod table), a DTMF keypad, and transverter band setup (two-column BAND screen). Fixed: TX TEST now flashes distinct from a real transmit (finishing FR-TX-TUNE-01's flashing indication), and DATA sub-mode/rate switching lag — the same read-back fight the sliders had, fixed with the standing optimistic-override pattern. **What is left is now honestly the hard part:** the audio-character (`MX`/`BL`/`FX`/`AL`) and message (`DARM`) items are blocked on two hardware questions only the operator can answer — whether radio-side audio settings reach the remote stream, and whose microphone `DARM` records. Version bumped in Cargo.toml (workspace), lockfile, README, user manual. 326 tests. | | 2026-07-26 | 4.1 | DC0SK | **FR-UI-UPD-02 implemented** — automatic update check + top-area notification. Default-on preference (opt-out in Settings), one start-up check off the UI thread reusing `update::check_now` (so the whole never-spurious comparison from FR-UI-UPD-01 comes for free), and a clickable `● update ` link beside the connection indicator, shown only for an `Available` result. The metered-link caution FR-UI-UPD-01 was written around is met by construction, not overridden: one request per launch, opt-out, silent unless there is a real update. Tested at the config layer (default-on + persistence) and verified on screen — including watching the start-up check overwrite a seeded status, which confirmed it runs. 327 tests. | | 2026-07-27 | 4.2 | DC0SK | Release **v0.9.0**. Minor: the addition since 0.8.0 is FR-UI-UPD-02, the automatic update check (opt-out, once per start, silent unless a real update is found, clickable link in the top status area). Numbered **0.9.0, not 0.8.1** — a new feature is a minor bump under semver, and the changelog files it under Added. It was briefly tagged v0.8.1 by mistake; the tag and its in-flight release build were removed before any release artifact published, and it was retagged. Version bumped in Cargo.toml (workspace), lockfile, README, user manual. 327 tests. | +| 2026-08-06 | 4.7 | DC0SK | **FR-AMP-04 (amplifier ATU tune).** ATU TUNE control that keys the K4 carrier (`TU1`) + starts `^FT`, then drops it (`TU0`) on completion / 20 s hard timeout / dropped precondition; arm-gated like the K4 tune (ARM-TX flash when disarmed) and aborted by the emergency stop (which also sends `^FE`). The end-decision `ui::amp_tune_should_end` is a pure function with 6 cases — `fr_amp_04_tune_end_conditions`: keeps running mid-tune, ends on start→stop, ends on timeout even if the amp never reported starting (so the carrier can't be stranded), ends on each dropped precondition. The tick reconciler and the arm-gate/e-stop wiring are integration paths (TX → HIL). The mock KPA1500 server was extended to simulate a tune (`^FT`→`^TP1` for ~2 s→`^TP0`, `^FE` cancels) for the demo path; the disarmed UI (button + "arm TX to tune" hint) was verified on screen. Build + clippy + trace gate green. | | 2026-08-06 | 4.6 | DC0SK | **FR-AMP-03 (KPA1500 live client).** New `app/src/kpa.rs` worker thread (TCP connect with timeout, interval poll, `k4_kpa::apply` parse, shared-snapshot publish, reconnect backoff, control-command send) wired into the app like the K4 worker; a tick reconciler brings the amp up/down with K4 connectivity + the enable toggle; a top-bar amp indicator and the amp window's live telemetry + Operate/Standby/ATU/antenna controls. Pure indicator logic (fault>mode precedence, Operate/Standby/down colours, Caution on SWR≥2.0) is unit-tested — `ui::amp_indicator` + `fr_amp_03_amp_indicator_precedence_and_wording`. The worker/socket path is **not** unit-covered (it is I/O): evidence there is D/HIL against a real or mock KPA1500, so this slice is L1 for the indicator and awaits an on-amp run for the rest. Deferred: full ATU tune (couples to the K4 arm-gated TX path). Build + clippy + trace gate green. | | 2026-08-06 | 4.5 | DC0SK | **FR-AMP-02 (KPA1500 CAT codec).** New dependency-free `k4-kpa` crate: control encoders (`^OS`/`^FT`/`^FE`/`^AM`/`^AN`/`^ON`) + a response parser into `KpaState`, from the KPA1500 Programming Reference V3 (all 59 pages read). Nine L1 tests (`fr_amp_02_*`): encoder wire forms, two-digit antenna select, each telemetry field, `^WS` combined power+SWR, hex fault decode + description, identity/firmware/serial (RVM not mistaken for RV), parse bounds, and a noise/fragment test that **caught a real clobber bug** — a `parse().ok()` assignment was blanking a good reading when a bare-tag fragment (`^TM` with no value) arrived; fixed so a non-match never overwrites a known field. Pure slice — no socket I/O or UI yet, so evidence is unit-only (L1); the poll loop + mini-panel are the next slice. Crate added to the workspace; `xtask trace` covers FR-AMP-02. | | 2026-08-06 | 4.4 | DC0SK | **FR-AMP-01 (KPA1500 support) — enable + config-window slice.** New opt-in `Prefs` fields (`kpa1500_enabled`/`_host`/`_port`/`_poll_ms`) with serde defaults; a Settings toggle + a "Configuration…" button opening a **detached** KPA1500 window (host/port/poll), built by mirroring the existing diagnostics-window daemon pattern (open/close/ESC/title/view-dispatch). Persistence via `save_config`, with the port/poll buffers falling back to 1500/500 rather than saving an unusable value. Config-layer test `fr_amp_01_kpa1500_defaults_off_and_persists` (L1): default-off, port/poll defaults, TOML round-trip of a configured amp, and a legacy-config load. The amp **client** (its own-Ethernet CAT polling, telemetry, mini-panel, control) is the next slice — this change is the enable + connection surface only. Build + clippy + trace gate green. |