diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d0af34d3c..4e8a18c3bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1086,9 +1086,15 @@ set(CORE_SOURCES src/core/SpeProtocol.cpp src/core/SpeLegacyProtocol.cpp src/core/SpeConnection.cpp + src/models/Kxpa100RadioAssociation.cpp + src/models/Kxpa100RadioAssociation.h src/models/Kat500TxController.cpp src/models/Kat500TxController.h + src/models/Kxpa100Model.cpp + src/models/Kxpa100Model_Maintenance.cpp src/models/Kat500Model.cpp + src/core/Kxpa100Protocol.cpp + src/core/Kxpa100Maintenance.cpp src/core/Kat500Protocol.cpp src/core/Kat500Maintenance.cpp src/core/Kpa500Protocol.cpp @@ -1388,7 +1394,9 @@ set(GUI_SOURCES src/gui/AmpApplet.cpp src/gui/AcomApplet.cpp src/gui/SpeApplet.cpp + src/gui/Kxpa100Applet.cpp src/gui/Kat500Applet.cpp + src/gui/Kxpa100AdvancedWidget.cpp src/gui/Kat500AdvancedWidget.cpp src/gui/Kpa500Applet.cpp src/gui/Kpa500AdvancedWidget.cpp diff --git a/docs/architecture/aetherd-touchpoint-tags.json b/docs/architecture/aetherd-touchpoint-tags.json index e283eadf93..bc91c63b24 100644 --- a/docs/architecture/aetherd-touchpoint-tags.json +++ b/docs/architecture/aetherd-touchpoint-tags.json @@ -1696,5 +1696,23 @@ "note": "Engine-owned external tuner sequence through typed RadioModel and captured TX input; KAT500 remains an independent peripheral. No vendor radio wire or GUI dependency.", "split": "", "confidence": "high" + }, + "models/Kxpa100Model.h": { + "tag": "peripheral(elecraft)", + "note": "Confirmed state and bounded operations for the independent Elecraft KXPA100 serial/TCP accessory; no radio wire or GUI dependency.", + "split": "", + "confidence": "high" + }, + "models/Kxpa100Types.h": { + "tag": "peripheral(elecraft)", + "note": "Typed KXPA100 accessory state, configuration, backup and fault records; no radio-family protocol.", + "split": "", + "confidence": "high" + }, + "models/Kxpa100RadioAssociation.h": { + "tag": "peripheral(elecraft)", + "note": "Explicit per-radio amplifier telemetry association; no radio or tuner commands.", + "split": "", + "confidence": "high" } } diff --git a/docs/architecture/aetherd-touchpoints.md b/docs/architecture/aetherd-touchpoints.md index 068dd906e5..6f7bbdf473 100644 --- a/docs/architecture/aetherd-touchpoints.md +++ b/docs/architecture/aetherd-touchpoints.md @@ -4,7 +4,7 @@ Burndown manifest for the engine/UI decoupling ([RFC](../aetherd-headless-engine-design.md) §2, §10). One row per engine header the UI includes; converting a touchpoint means the UI reaches that surface through the versioned protocol instead of the header. -**Totals:** 268 touchpoint headers (220 core, 48 models) — 268/268 tagged, 0/268 converted. +**Totals:** 271 touchpoint headers (220 core, 51 models) — 271/271 tagged, 0/271 converted. | Header | Includers | Tag | Status | |---|---:|---|---| @@ -18,7 +18,7 @@ Burndown manifest for the engine/UI decoupling ([RFC](../aetherd-headless-engine | `core/AetherTxProfiles.h` | 1 | universal — Named-profile save/recall/import-export for the transmit DSP chain; operates on core-profile DSP state only. Mirror of core/AetherRxProfiles.h, and shares ChannelStripPresets' TX capture/apply. | unconverted | | `core/AgcTCalibrator.h` | 1 | universal — Engine algo sweeping slice AGC threshold vs audio RMS/S-meter to recommend a value; only canonical state. | unconverted | | `core/AppActivity.h` | 3 | ui-support — Process-lifetime activity hold (App Nap opt-out on macOS, process power-throttling opt-out on Windows; never blocks sleep) and startStreamThread(), the High-QoS thread start for stream threads; app plumbing, not radio state. | unconverted | -| `core/AppSettings.h` | 110 | ui-support — Client-side XML settings store (SSDR.settings-style key/value persistence); app plumbing, not radio state. | unconverted | +| `core/AppSettings.h` | 111 | ui-support — Client-side XML settings store (SSDR.settings-style key/value persistence); app plumbing, not radio state. | unconverted | | `core/AudioDevicePreferencePolicy.h` | 1 | ui-support — Header-only policy over PC audio device ids: whether a device-list change keeps the engine's device, falls back to the system default or returns to the saved device, and whether a selection is written to the saved id (#6040); also which enumerable ids count as present and which as arrivals. Host audio, no radio state. | unconverted | | `core/AudioEngine.h` | 48 | mixed(flex) — Client audio I/O + full RX/TX DSP chain (universal) fused with Flex VITA-49/DAX/Opus TX and Kiwi buffering | unconverted | | `core/AudioOutputRouter.h` | 1 | ui-support — Registry fanning the user-selected QAudioDevice to local playback sinks; OS device plumbing, no radio state | unconverted | @@ -168,7 +168,7 @@ Burndown manifest for the engine/UI decoupling ([RFC](../aetherd-headless-engine | `core/TailnetShimTokenStore.h` | 1 | peripheral(tailnet-shim) — OS-keychain storage of the tailnet shim's admin token, keyed by the Flex chassis serial (RFC #6271). Not SmartSDR wire: AetherSDR's own HTTP API to a separate service (the shim) that happens to run in the radio's container runtime, so a direct-transport peripheral, NOT behind the IRadioBackend radio seam (same shape as core/PgxlConnection.h). | unconverted | | `core/TciServer.h` | 3 | mixed(flex) — TCI WebSocket server for WSJT-X et al: protocol surface is canonical radio state, but audio/IQ rides Flex DAX | unconverted | | `core/TgxlConnection.h` | 3 | peripheral(4o3a) — Direct TCP client for the 4O3A Tuner Genius XL (port 9010, relay/autotune), reverse-engineered from the 4O3A management app — a standalone accessory transport, not SmartSDR. Not radio-family wire; a peripheral accessory, NOT behind the IRadioBackend radio seam (reclassified from vendor(flex), #4087 follow-up). | unconverted | -| `core/ThemeManager.h` | 181 | ui-support — Qt token-based theming singleton (colors/fonts/QSS, theme files, editor hooks) — pure client GUI plumbing, no radio state. | unconverted | +| `core/ThemeManager.h` | 182 | ui-support — Qt token-based theming singleton (colors/fonts/QSS, theme files, editor hooks) — pure client GUI plumbing, no radio state. | unconverted | | `core/ThreadCpuRing.h` | 2 | ui-support — Short host-thread CPU history used by Runtime Monitor peak and sparkline presentation. Diagnostic UI support, not radio state. | unconverted | | `core/TimeFrameVoter.h` | 1 | universal — Shared AetherClock time-frame types plus confidence-weighted cross-frame bit voting over a sliding window. Map-agnostic pure DSP/logic — no Qt, no GUI, no vendor ties. | unconverted | | `core/TxKeyingMarker.h` | 16 | ui-support — QWidget property marker guarding TX-keying controls from the automation bridge; GUI-shell plumbing, no radio state. | unconverted | @@ -252,6 +252,9 @@ Burndown manifest for the engine/UI decoupling ([RFC](../aetherd-headless-engine | `models/Kat500Model.h` | 4 | peripheral(elecraft) — Typed model facade for the independent Elecraft KAT500 serial/raw-TCP tuner, with confirmed operating/configuration state and bounded maintenance operations. No radio-family wire or carrier commands are exposed to GUI. | unconverted | | `models/Kat500TxController.h` | 2 | peripheral(elecraft) — Engine-owned external tuner sequence through typed RadioModel and captured TX input; KAT500 remains an independent peripheral. No vendor radio wire or GUI dependency. | unconverted | | `models/Kat500Types.h` | 2 | peripheral(elecraft) — Model-owned KAT500 status, configuration and maintenance value types for the typed peripheral facade. No transport or raw command path in GUI. | unconverted | +| `models/Kxpa100Model.h` | 4 | peripheral(elecraft) — Confirmed state and bounded operations for the independent Elecraft KXPA100 serial/TCP accessory; no radio wire or GUI dependency. | unconverted | +| `models/Kxpa100RadioAssociation.h` | 2 | peripheral(elecraft) — Explicit per-radio amplifier telemetry association; no radio or tuner commands. | unconverted | +| `models/Kxpa100Types.h` | 2 | peripheral(elecraft) — Typed KXPA100 accessory state, configuration, backup and fault records; no radio-family protocol. | unconverted | | `models/MemoryEntry.h` | 1 | universal — POD memory-channel record (freq/mode/offset/tone/squelch/filter/digital offsets) — canonical memories surface | unconverted | | `models/MeterModel.h` | 7 | mixed(flex) — Meter registry+values are core-profile; VITA-49 raw scaling, TGXL/PGXL amp routing, COMPPEAK quirks are Flex | unconverted | | `models/ModeVocabulary.h` | 1 | universal — Canonical mode vocabulary: which slice modes count as voice (SSB, AM/SAM, the FM family) and which as CW (CW/CWU/CWL). Radio-family-neutral state and validation data - every backend reports these spellings - split out of gui/VoiceModeGate.h so the engine can answer the question without reaching into the UI. Same precedent as core/CtcssTones.h. The panel-policy half (shouldAutoHideCopyAssist) stayed in gui. | unconverted | diff --git a/docs/user/docs/kxpa100-amplifier-control.md b/docs/user/docs/kxpa100-amplifier-control.md new file mode 100644 index 0000000000..07df95d0bc --- /dev/null +++ b/docs/user/docs/kxpa100-amplifier-control.md @@ -0,0 +1,122 @@ +--- +title: "KXPA100 Amplifier and Tuner" +slug: "/kxpa100-amplifier-control" +description: "Connect an Elecraft KXPA100, control its amplifier and optional tuner, and associate its amplifier metering with a radio." +status: "Experimental" +applies_to: ["All radios"] +platforms: ["Linux", "macOS", "Windows"] +--- + +:::info[Status] + +**Status:** Experimental · **Applies to:** All radios · **Platforms:** Linux, macOS, Windows + +::: + +The **KX100** applet combines KXPA100 amplifier controls, meters and optional KXAT100 tuner controls. Selections and measurements come from device readback. Connecting does not key the radio or load stored client-side amplifier settings. + +## Requirements + +Use a local serial connection or raw TCP serial bridge such as ser2net or the companion **aether-serial-bridge** project for remote access. Only one client can own the serial port. Network mode does not support Telnet/RFC 2217 negotiation. The bridge controls baud, 8N1 format and handshake lines. + +Configuration, backup restore and memory tools support the verified firmware **01.39** layout. Other firmware shows an availability explanation. Use Elecraft's official Utility for firmware updates; AetherSDR has no firmware updater or raw Command Tester. + +Connect the radio PTT output to the KXPA100 **PA KEY** input as required by your station. Software association does not create that physical keying connection. Turn on the amplifier locally: the protocol has no remote power command. **Standby** bypasses amplification; it does not turn off DC power. + +## Setup + +1. Open **Settings → Radio Setup... → Peripherals**, click **Add**, and select **Elecraft KXPA100**. +2. Select **Serial** and the port/baud, or **Network** and your bridge host/port. Click **Connect**. +3. The **KX100** applet button becomes available after identification. It remains available while the transport is open, including stale replies, and disappears when the connection closes. +4. To use amplifier output on the shared **VU** and **PWR** meters, select the intended radio and click **Use with current radio** in the peripheral page or applet **Settings**. This explicitly associates the amplifier serial number with that radio. + +Connection preferences are client settings. The metering association belongs to the selected radio and works with or without the optional KXAT100 tuner. Switching radios does not transfer that association. + +For **Network**, enter a hostname or IP address without a URL scheme, path, spaces or control characters, and a port from 1 to 65535. Invalid entries are rejected before connecting. + +## Using the applet + +**Standby** and **Operate** control the PA. **Auto**, **Manual** and **Bypass** control the separate tuner mode. **ANT 1** and **ANT 2** select the tuner antenna. The selected state changes after readback. Missing tuner hardware leaves tuner controls dimmed with an explanation while PA controls remain available. + +**Tune** starts a tuner search; it does not generate a radio carrier. Supply RF according to the Elecraft instructions. **Cancel** requests an end to the search. **Clear fault** clears the current fault, leaving stored fault history intact. + +Full view shows output power and SWR, followed by **DRV** input power measured by the KXPA100 (0–10 W), reflected power, dissipated power, temperature, supply voltage and drain current. Power meters use watts; SWR uses a 1–5 scale. The pill retains the exact reading beyond the scale. SWR is the last observed measurement and can remain after RF stops. Press and hold the **SWR** pill to preview bypass SWR without changing any relays; release to return to adjusted SWR. A dash means the reading is unavailable. + +**Mini** keeps the **OPR/STBY** PA toggle, **Tune/Cancel**, a mode/antenna/status readout, output power and SWR. **Full** restores the other controls. Expanded drawers and small popouts scroll when needed. + +The **ATU BYP** pill toggles the physical matching-network bypass relay. It leaves Auto/Manual mode unchanged; a recall or tune may engage the network again. Use the main **Bypass** mode button to disable tuning. **ATT** toggles the input attenuator when software control is available. The rear-panel switch or firmware protection can override it. Hover over these pills for the complete meaning. Requests require fresh, idle readback with no RF. + +KXPA100 Full applet with amplifier and tuner controls, DRV and output meters; illustrative replay values. + +*Full view with replayed telemetry illustrating the meters.* + +KXPA100 Mini view with PA, tune, mode and antenna status, output power and SWR. + +### Manual, History and Settings + +**Manual** exposes capacitance in pF and inductance in nH, with each relay labeled by its component value. Numeric **Set** selects the nearest available relay combination to the entered total. The capacitor-side control changes which side of the matching network contains the capacitors. **Match frequency** selects the kHz frequency for **Recall** and **Memorize**. These commands require 1800–54000 kHz; an unknown frequency sends nothing. Recall loads a stored match; Memorize saves and verifies the current match. Neither generates RF. + +**History** plots forward power, reflected power and last-observed SWR, with a selectable time window, scales and legends. Missing replies leave gaps. The graph shrinks as well as grows with its popout. + +**Settings** shows radio association, identity, firmware, serial, DIRECT/NETWORK endpoint, freshness and operation results. **Open Advanced settings** opens the same maintenance page as Peripherals. **Debug** shows bounded diagnostics; **Verbose** includes wire traffic. New entries follow the bottom unless you scroll up or pause. Debug TX means serial output, not RF transmission. + +### Radio controls and shared metering + +**TX Controls** always operates the radio's own tuner and memories. Connecting or associating a KXPA100 never replaces ATU/MEM, adds an overlay, or bypasses the internal tuner. A radio without an internal tuner shows those controls dimmed with an explanation. Use **KX100 → Tune** and **Manual → Memorize** for the external tuner. + +The shared **VU** and **PWR** meters use associated KXPA100 output on the amplifier scale when PA Operate and idle-tuner readback are fresh. This does not require an installed KXAT100. An operating radio-integrated amplifier has priority, followed by an operating KPA500; confirmed KAT500 KEY BLOCK retains the radio fallback for that KPA500 path. Otherwise fresh eligible KXPA100 telemetry is used, falling back to the radio when unavailable. The KX100 applet always shows the amplifier's own readings. **TX Controls** keeps radio watts/SWR and the radio scale; its RF Power and Tune Pwr readouts show percentages. + +Radio-side TX Controls with ATU/MEM, radio power and SWR scales, and percentage power sliders. + +*Current Mac capture while disconnected; no RF or simulated meter values.* + +## Using Advanced + +Open **Peripherals → Elecraft KXPA100 → Advanced**, or the applet shortcut. Opening the page performs no maintenance action. + +### Configuration + +Click **Read configuration** before editing. The matrix shows enabled ANT1/ANT2 choices for eleven bands and an **All bands** row. Checkboxes stage edits; **Readback** stays authoritative. Mixed values remain mixed until edited. At least one antenna must remain enabled on every band. **Apply changed fields** sends only deliberate changes and verifies readback. **Cancel edits** discards staging. + +### Backup and restore + +**Read and save backup…** reads both EEPROM regions, totaling 17,408 bytes, and atomically writes a native `.KXPAConfig` file. **Choose backup…** validates size, coverage and checksum locally and previews identity. Choosing a file sends nothing to the amplifier. + +Restore requires firmware 01.39, matching backup serial/firmware, fresh identification, idle state and no RF. Review the preview, check **Confirm backup replacement**, and click **Restore and verify**. Each permitted block is acknowledged and read back; identity and fault-history regions are preserved. Confirmed blocks remain applied if canceled or interrupted. There is no blind retry. Successful restore restarts the device; reconnect and inspect readbacks before use. + +### Faults, memories and reset + +**Read fault history** shows newest records first, elapsed device seconds, frequency, fault-specific value/unit and raw bytes. Elapsed time is not a calendar timestamp. Unknown records remain visible. **Export CSV…** saves the displayed records. + +**Inspect bin** reads stored matches at the selected kHz frequency without changing the live matching network. For erasure, choose band and antenna, review the target, check **Confirm selected erase**, and click **Erase selected memories**. Single-frequency erasure is not offered. **Factory reset** requires its separate confirmation and clears settings and matches, then restarts the unit. These actions never run merely because the device connects. + +## Troubleshooting + +### A control is disabled + +Read its tooltip or open Settings for the reason. Wait for identification and fresh idle measurements. Stop RF before switching antennas, relays or configuration. Tuner-only controls require the optional KXAT100. Advanced schema-dependent tools require firmware 01.39. + +### VU/PWR does not show KXPA100 output + +Confirm **Use with current radio** was applied to the intended radio, the amplifier reports Operate, and power/idle readbacks are fresh. A higher-priority amplifier or active tuner search prevents KXPA100 metering. A missing physical PA KEY cable cannot be fixed by association. + +### Radio association cannot be saved + +A rejected settings write leaves the previous association in effect and logs an `aether.kxpa100` warning. Applet **Settings** also displays the failure beside the operation result. Resolve the read-only or busy settings store, then click **Use with current radio** again. Association changes are not retried automatically. + +### An operation fails or the connection closes + +A timeout closes the stream to prevent a late response from confirming a later request. Reconnect, inspect the current state and make a new deliberate request. Writes are not replayed automatically. A canceled restore can leave some confirmed blocks applied; use the retained backup to recover deliberately. + +For a repeated idle disconnect, enable **Settings → Debug → Verbose** and retain the replies around the timeout. SWR replies such as `^SW01.3;` and `^SW013;` both mean 1.3:1 and are supported. The documented zero reply `^SW00.0;` is also valid and does not cause a disconnect. + +### ANT1 appears after power-off + +The physical off-state routes to ANT1. Reconfirm the desired antenna after power cycling and before transmitting. Do not infer ANT2 from a previous session or saved UI preference. + +## See also + +- [Peripherals](./peripherals.md) +- [Amplifiers](./amplifiers.md) +- [KAT500 Tuner Control](./kat500-tuner-control.md) +- [Elecraft KXPA100 software](https://elecraft.com/pages/kxpa100-software) diff --git a/docs/user/docs/peripherals.md b/docs/user/docs/peripherals.md index 0debab0b1e..ff2b4d2d14 100644 --- a/docs/user/docs/peripherals.md +++ b/docs/user/docs/peripherals.md @@ -74,6 +74,8 @@ device's own web interface. Connection defaults for every device type are under [Reference](#reference). +The **Elecraft KXPA100** page combines amplifier and optional tuner controls. Click **Use with current radio** to associate it explicitly; this enables its VU/PWR metering and applet frequency context for that radio. TX Controls remains radio-side. The **Advanced** section provides antenna configuration, native backup/restore, faults and memory tools. See [KXPA100 Amplifier and Tuner](./kxpa100-amplifier-control.md). + ### Connecting automatically TGXL, PGXL, Antenna Genius and ShackSwitch each have a toggle, **on by @@ -268,6 +270,7 @@ The fields are locked while the device is connected. - [Amplifiers](./amplifiers.md) - [TGXL Tuner Control](./tgxl-tuner-control.md) - [KAT500 Tuner Control](./kat500-tuner-control.md) +- [KXPA100 Amplifier and Tuner](./kxpa100-amplifier-control.md) - [ShackSwitch](./shackswitch.md) - [Green Heron Everyware](./green-heron-everyware.md) - [AetherSweep](./aethersweep.md) diff --git a/docs/user/sidebars.js b/docs/user/sidebars.js index 66f0d94e29..5ecfc7e0f2 100644 --- a/docs/user/sidebars.js +++ b/docs/user/sidebars.js @@ -116,6 +116,7 @@ const sidebars = { "amplifiers", "tgxl-tuner-control", "kat500-tuner-control", + "kxpa100-amplifier-control", "shackswitch", "green-heron-everyware", "usb-cable-management" diff --git a/docs/user/static/img/kxpa100/kxpa100-advanced.png b/docs/user/static/img/kxpa100/kxpa100-advanced.png new file mode 100644 index 0000000000..be0731fc1d Binary files /dev/null and b/docs/user/static/img/kxpa100/kxpa100-advanced.png differ diff --git a/docs/user/static/img/kxpa100/kxpa100-full-replay.png b/docs/user/static/img/kxpa100/kxpa100-full-replay.png new file mode 100644 index 0000000000..a2cfee608a Binary files /dev/null and b/docs/user/static/img/kxpa100/kxpa100-full-replay.png differ diff --git a/docs/user/static/img/kxpa100/kxpa100-mini.png b/docs/user/static/img/kxpa100/kxpa100-mini.png new file mode 100644 index 0000000000..c157110f7b Binary files /dev/null and b/docs/user/static/img/kxpa100/kxpa100-mini.png differ diff --git a/docs/user/static/img/kxpa100/tx-controls-radio-only.png b/docs/user/static/img/kxpa100/tx-controls-radio-only.png new file mode 100644 index 0000000000..ca0bf7a353 Binary files /dev/null and b/docs/user/static/img/kxpa100/tx-controls-radio-only.png differ diff --git a/src/core/Kxpa100Maintenance.cpp b/src/core/Kxpa100Maintenance.cpp new file mode 100644 index 0000000000..062789fc48 --- /dev/null +++ b/src/core/Kxpa100Maintenance.cpp @@ -0,0 +1,206 @@ +#include "Kxpa100Maintenance.h" +#include +#include +#include + +namespace AetherSDR::Kxpa100 { +namespace { +std::optional unhex(const QByteArray& hex) +{ + if (hex.size() % 2) { return {}; } + for (const char c : hex) { + if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))) { return {}; } + } + return QByteArray::fromHex(hex); +} +quint32 crc32(quint32 crc, const QByteArray& bytes) +{ + // Utility 1.14.3.4: reflected IEEE, initial FFFFFFFF, no final XOR; + // line endings do not participate. All other bytes, including comments, do. + for (const char byte : bytes) { + crc ^= static_cast(byte); + for (int bit = 0; bit < 8; ++bit) { crc = (crc >> 1) ^ ((crc & 1U) ? 0xedb88320U : 0U); } + } + return crc; +} +bool identityValid(const Backup& backup) +{ + static const QRegularExpression firmware(QStringLiteral("^[0-9]{2}\\.[0-9]{2}$")); + static const QRegularExpression serial(QStringLiteral("^[0-9]{5}$")); + return firmware.match(backup.firmware).hasMatch() && serial.match(backup.serial).hasMatch(); +} +QByteArray framed(const QByteArray& prefix, int address, int length, const QByteArray& data = {}) +{ + QByteArray bytes; + bytes.append(static_cast(address >> 8)); bytes.append(static_cast(address)); + bytes.append(static_cast(length)); bytes += data; + int sum = 0; for (const char byte : bytes) { sum += static_cast(byte); } + bytes.append(static_cast(-sum)); + return prefix + bytes.toHex().toUpper() + ';'; +} +bool rangeValid(bool external, int address, int length) +{ + return address >= 0 && length > 0 && length <= 64 && address <= (external ? 16384 : 1024) - length; +} +} +QByteArray buildEepromRead(bool external, int address, int length) +{ + return rangeValid(external,address,length) ? framed(external ? "^ERS" : "^ERP", address,length) : QByteArray{}; +} +QByteArray buildEepromWrite(bool external, int address, const QByteArray& data) +{ + if (!rangeValid(external, address, static_cast(data.size())) || + address < (external ? 0 : 32) || address + data.size() > (external ? 0x3800 : 0x400) || + address / 256 != (address + data.size() - 1) / 256) { return {}; } + return framed(external ? "^EWS" : "^EWP", address,static_cast(data.size()),data); +} +std::optional parseEepromReply(const QByteArray& frame) +{ + if (frame.size() < 12 || frame.size() > 140 || + !(frame.startsWith("^ERP") || frame.startsWith("^ERS") || frame.startsWith("^EWP") || frame.startsWith("^EWS"))) { return {}; } + const auto bytes = unhex(frame.mid(4)); + if (!bytes || bytes->size() < 4) { return {}; } + int sum = 0; for (const char byte : *bytes) { sum += static_cast(byte); } + if (sum % 256) { return {}; } + EepromReply reply; + reply.external = frame[3] == 'S'; reply.writeAck = frame[2] == 'W'; + reply.address = static_cast((*bytes)[0]) * 256 + static_cast((*bytes)[1]); + reply.length = static_cast((*bytes)[2]); + if (!rangeValid(reply.external,reply.address,reply.length) || bytes->size() != (reply.writeAck ? 4 : reply.length + 4)) { return {}; } + if (!reply.writeAck) { reply.data = bytes->mid(3,reply.length); } + return reply; +} +QByteArray encodeBackup(const Backup& backup, const QDateTime& savedAt) +{ + if (!identityValid(backup) || backup.internal.size() != 1024 || backup.external.size() != 16384) { return {}; } + QList lines{ + "*KXPA Configuration saved on " + savedAt.toString(QStringLiteral("yyyy-MM-dd 'at' HH:mm:ss")).toLatin1(), + "*Do not edit this file!", "KXPA Utility Revision = AetherSDR", + "KXPA Firmware Revision = " + backup.firmware.toLatin1(), "KXPA Serial Number = " + backup.serial.toLatin1()}; + for (int address = 0; address < 1024; address += 32) { + const QByteArray hex = QByteArray::number(address,16).rightJustified(4,'0').toUpper(); + lines.append("MEM2 " + hex.left(2) + ' ' + hex.mid(2) + " 20 " + backup.internal.mid(address,32).toHex().toUpper()); + } + for (int address = 0; address < 16384; address += 64) { + lines.append("MEM3 " + QByteArray::number(address,16).rightJustified(4,'0').toUpper() + " 40 " + backup.external.mid(address,64).toHex().toUpper()); + } + QByteArray result; quint32 crc = 0xffffffffU; + for (const QByteArray& line : lines) { crc = crc32(crc,line); result += line + "\r\n"; } + result += "CRC32 = 0x" + QByteArray::number(crc,16).rightJustified(8,'0').toUpper() + "\r\n"; + return result; +} +std::optional decodeBackup(const QByteArray& bytes, QString* error) +{ + auto fail = [error](const QString& message) -> std::optional { if (error) { *error = message; } return {}; }; + if (bytes.isEmpty() || bytes.size() > 64 * 1024) { return fail(QStringLiteral("Invalid backup file size.")); } + Backup backup; backup.internal.resize(1024); backup.external.resize(16384); + QSet internalSeen, externalSeen; + bool trailer = false, utility = false; + quint32 crc = 0xffffffffU; + const QList lines = bytes.split('\n'); + for (int index = 0; index < lines.size(); ++index) { + QByteArray line = lines[index]; if (line.endsWith('\r')) { line.chop(1); } + if (line.isEmpty() && index == lines.size()-1) { continue; } + if (line.isEmpty() || line.size() > 512 || trailer) { return fail(QStringLiteral("Invalid backup line or trailing content.")); } + for (const char c : line) { if (c < 32 || c > 126) { return fail(QStringLiteral("Backup must contain ASCII text.")); } } + if (line.startsWith("CRC32 = 0x")) { + const auto value = unhex(line.mid(10)); + bool ok = false; const quint32 expected = line.mid(10).toUInt(&ok,16); + if (!value || value->size() != 4 || !ok || expected != crc) { return fail(QStringLiteral("Backup checksum mismatch.")); } + trailer = true; continue; + } + crc = crc32(crc,line); + if (line.startsWith('*')) { continue; } + if (line.startsWith("KXPA Utility Revision = ")) { + if (utility) { return fail(QStringLiteral("Duplicate metadata.")); } utility = true; continue; + } + if (line.startsWith("KXPA Firmware Revision = ")) { + if (!backup.firmware.isEmpty()) { return fail(QStringLiteral("Duplicate firmware.")); } + backup.firmware = QString::fromLatin1(line.mid(25)); continue; + } + if (line.startsWith("KXPA Serial Number = ")) { + if (!backup.serial.isEmpty()) { return fail(QStringLiteral("Duplicate serial.")); } + backup.serial = QString::fromLatin1(line.mid(21)); continue; + } + const QList fields = line.split(' '); + const bool internal = fields.value(0) == "MEM2"; + if ((!internal && fields.value(0) != "MEM3") || fields.size() != (internal ? 5 : 4)) { return fail(QStringLiteral("Unknown backup record.")); } + const QByteArray addressText = internal ? fields[1] + fields[2] : fields[1]; + const QByteArray countText = fields[internal ? 3 : 2]; + if (addressText.size() != 4 || countText.size() != 2 || !unhex(addressText) || !unhex(countText)) { return fail(QStringLiteral("Invalid record address.")); } + const int address = addressText.toInt(nullptr,16); const int count = countText.toInt(nullptr,16); + const auto data = unhex(fields.last()); + if (!data || data->size() != count || !rangeValid(!internal,address,count)) { return fail(QStringLiteral("Invalid record length.")); } + QByteArray& memory = internal ? backup.internal : backup.external; + QSet& seen = internal ? internalSeen : externalSeen; + for (int offset = 0; offset < count; ++offset) { + if (seen.contains(address + offset)) { return fail(QStringLiteral("Overlapping backup records.")); } + seen.insert(address + offset); memory[address + offset] = (*data)[offset]; + } + } + if (!trailer || !utility || !identityValid(backup) || internalSeen.size() != 1024 || externalSeen.size() != 16384) { + return fail(QStringLiteral("Backup is incomplete or has invalid identity metadata.")); + } + if (error) { error->clear(); } return backup; +} +bool saveBackup(const QString& path, const Backup& backup, QString* error) +{ + const QByteArray bytes = encodeBackup(backup,QDateTime::currentDateTime()); + if (bytes.isEmpty()) { if (error) { *error = QStringLiteral("Cannot save an incomplete backup."); } return false; } + QSaveFile file(path); + if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) { + if (error) { *error = file.errorString(); } return false; + } + return true; +} +QString restoreAllowed(const Backup& backup, const QString& serial, const QString& firmware) +{ + if (!identityValid(backup) || backup.internal.size() != 1024 || backup.external.size() != 16384) { + return QStringLiteral("Backup is incomplete."); + } + if (backup.serial != serial) { return QStringLiteral("This backup belongs to another amplifier and contains its calibration."); } + if (backup.firmware != firmware) { return QStringLiteral("Backup firmware does not match the connected amplifier."); } + if (firmware != QStringLiteral("01.39")) { return QStringLiteral("Restore is verified only for firmware 01.39."); } + return {}; +} +QList decodeFaultHistory(int newestSlot, const QByteArray& bytes) +{ + QList records; + if (newestSlot < 0 || newestSlot > 255 || bytes.size() != 2048) { return records; } + for (int offset = 0; offset < 256; ++offset) { + FaultRecord record; record.slot = (newestSlot - offset + 256) % 256; + record.raw = bytes.mid(record.slot * 8, 8); + if (record.raw == QByteArray(8, char(0xff))) { continue; } + const auto byte = [&](int i) { return static_cast(record.raw[i]); }; + const auto word = [&](int i) { return byte(i) + 256 * byte(i + 1); }; + record.code = record.raw[0]; + record.elapsedSeconds = byte(1) + 256U * byte(2) + 65536U * byte(3); + record.frequencyKhz = word(4); + // Utility 1.14.3.4 binary history uses centiwatts; live ASCII FL uses tenths. + double divisor = 1; + switch (record.code) { + case 'A': case 'S': case 'C': case 'T': divisor = 10; break; + case 'D': case 'P': case 'R': divisor = 100; break; + case 'H': case 'L': divisor = 1000; break; + default: break; + } + record.value = word(6) / divisor; + record.unit = record.code == 'I' ? QStringLiteral("raw") : faultUnit(record.code); + record.description = record.code == 'N' ? QStringLiteral("Power On") : currentFaultName(record.code); + records.append(record); + } + return records; +} +QByteArray faultHistoryCsv(const QList& records) +{ + QByteArray csv("Slot,Elapsed seconds,Code,Fault,Frequency kHz,Value,Unit,Raw\r\n"); + for (const FaultRecord& record : records) { + QString description = record.description; description.replace('"', QStringLiteral("\"\"")); + csv += QByteArray::number(record.slot) + ',' + QByteArray::number(record.elapsedSeconds) + ',' + + QByteArray::number(quint8(record.code)) + ",\"" + description.toUtf8() + "\"," + + QByteArray::number(record.frequencyKhz) + ',' + QByteArray::number(record.value, 'f', 3) + ',' + + record.unit.toUtf8() + ',' + record.raw.toHex().toUpper() + "\r\n"; + } + return csv; +} +} // namespace AetherSDR::Kxpa100 diff --git a/src/core/Kxpa100Maintenance.h b/src/core/Kxpa100Maintenance.h new file mode 100644 index 0000000000..217167d650 --- /dev/null +++ b/src/core/Kxpa100Maintenance.h @@ -0,0 +1,22 @@ +#pragma once +#include "models/Kxpa100Types.h" + +namespace AetherSDR::Kxpa100 { +struct EepromReply { + bool external = false; + bool writeAck = false; + int address = 0; + int length = 0; + QByteArray data; +}; +QByteArray buildEepromRead(bool external, int address, int length); +// Captured firmware 01.39 restore regions: P[0020,0400), S[0000,3800). +QByteArray buildEepromWrite(bool external, int address, const QByteArray& data); +std::optional parseEepromReply(const QByteArray& frame); +QByteArray encodeBackup(const Backup& backup, const QDateTime& savedAt); +std::optional decodeBackup(const QByteArray& bytes, QString* error = nullptr); +bool saveBackup(const QString& path, const Backup& backup, QString* error = nullptr); +QString restoreAllowed(const Backup& backup, const QString& serial, const QString& firmware); +QList decodeFaultHistory(int newestSlot, const QByteArray& bytes); +QByteArray faultHistoryCsv(const QList& records); +} // namespace AetherSDR::Kxpa100 diff --git a/src/core/Kxpa100Protocol.cpp b/src/core/Kxpa100Protocol.cpp new file mode 100644 index 0000000000..040d230f89 --- /dev/null +++ b/src/core/Kxpa100Protocol.cpp @@ -0,0 +1,232 @@ +#include "Kxpa100Protocol.h" +#include +#include +#include +#include + +namespace AetherSDR::Kxpa100 { +namespace { +std::optional number(const QByteArray& text, int width, int low, int high, int base = 10) +{ + if (text.size() != width) { return {}; } + for (const char c : text) { + if (!((c >= '0' && c <= '9') || (base == 16 && c >= 'A' && c <= 'F'))) { return {}; } + } + bool ok = false; + const int value = text.toInt(&ok, base); + return ok && value >= low && value <= high ? std::optional(value) : std::nullopt; +} +QByteArray bandCode(int band) { return QByteArray::number(band).rightJustified(2, '0'); } +} +void FrameParser::feed(const QByteArray& bytes) +{ + for (const char c : bytes) { + if (c == ';') { + const QByteArray frame = m_buffer.trimmed(); + m_buffer.clear(); + const bool accepted = !m_discard; + m_discard = false; + if (accepted && m_callback) { m_callback(frame); } + } else if (c == '\r' || c == '\n') { + if (!m_discard && !m_buffer.isEmpty() && !m_buffer.endsWith(' ')) { m_buffer.append(' '); } + } else if (c < 32 || c > 126 || m_buffer.size() >= 512) { + m_buffer.clear(); m_discard = true; + } else if (!m_discard) { + m_buffer.append(c); + } + } +} +bool applyStatus(const QByteArray& frame, Status& status) +{ + if (frame == "^IKXPA100") { status.identity = QStringLiteral("KXPA100"); return true; } + if (frame.startsWith("^RV")) { + static const QRegularExpression version(QStringLiteral("^\\^RV[0-9]{2}\\.[0-9]{2}$")); + if (!version.match(QString::fromLatin1(frame)).hasMatch()) { return false; } + status.firmware = QString::fromLatin1(frame.mid(3)); return true; + } + if (frame.startsWith("^SN")) { + if (!number(frame.mid(3), 5, 0, 99999)) { return false; } + status.serial = QString::fromLatin1(frame.mid(3)); return true; + } + const std::array*>, 4> booleans{{ + {"^OP", &status.operate}, {"^TU", &status.tunerPresent}, {"^TP", &status.tuning}, {"^XP", &status.transceiverPolling}}}; + for (const auto& [prefix, target] : booleans) { + const QByteArray key(prefix); + if (frame == key + '0' || frame == key + '1') { *target = frame.endsWith('1'); return true; } + } + if (frame == "^MDA" || frame == "^MDM" || frame == "^MDB") { + status.mode = frame.endsWith('A') ? Mode::Auto : frame.endsWith('M') ? Mode::Manual : Mode::Bypass; return true; + } + if (frame == "^BYB" || frame == "^BYN") { status.bypassed = frame.endsWith('B'); return true; } + if (frame == "^SIT" || frame == "^SIA") { + status.side = frame.endsWith('T') ? CapacitorSide::Transmitter : CapacitorSide::Antenna; return true; + } + struct Integer { const char* prefix; int width, low, high, base; std::optional* target; }; + const std::array integers{{ + {"^AN",1,1,2,10,&status.antenna}, {"^BN",2,0,10,10,&status.band}, + {"^AT",1,0,2,10,&status.attenuator}, {"^F",5,0,99999,10,&status.frequencyKhz}, + {"^CR",2,0,255,16,&status.capacitorMask}, {"^LR",2,0,255,16,&status.inductorMask}, + {"^XI",1,0,9,10,&status.transceiverInterface}}}; + for (const Integer& field : integers) { + const QByteArray prefix(field.prefix); + if (frame.startsWith(prefix)) { + if (const auto value = number(frame.mid(prefix.size()), field.width, field.low, field.high, field.base)) { + *field.target = *value; return true; + } + } + } + struct Measurement { const char* prefix; int width; double scale; std::optional* target; }; + const std::array measurements{{ + {"^PI",4,10,&status.inputWatts}, {"^PF",4,10,&status.forwardWatts}, + {"^PV",4,10,&status.reflectedWatts}, {"^PD",4,10,&status.dissipatedWatts}, + {"^PC",4,10,&status.drainAmps}, {"^TM",4,10,&status.temperatureC}, + {"^SV",5,1000,&status.supplyVolts}, {"^SB",3,10,&status.bypassSwr}}}; + for (const Measurement& field : measurements) { + const QByteArray prefix(field.prefix); + if (frame.startsWith(prefix)) { + if (const auto value = number(frame.mid(prefix.size()), field.width, 0, field.width == 5 ? 99999 : 9999)) { + *field.target = *value / field.scale; return true; + } + } + } + if (frame.startsWith("^SW")) { + QByteArray digits = frame.mid(3); + // The documented nn.n format includes zero; compact firmware replies use nnn. + const bool decimal = digits.size() == 4 && digits[2] == '.'; + if (decimal) { digits.remove(2, 1); } + const auto value = number(digits, 3, decimal ? 0 : 10, 999); + if (!value) { return false; } + status.swr = *value / 10.0; return true; + } + if (frame.size() == 4 && frame.startsWith("^AD") && QByteArray("DFIJNV").contains(frame[3])) { + status.attenuatorReason = frame[3]; return true; + } + if (frame.startsWith("^FL") && (frame.size() == 9 || frame == "^FLN")) { + const char code = frame[3]; + if (!QByteArray("NACDHILPRST").contains(code)) { return false; } + const auto value = frame == "^FLN" ? std::optional(0) : number(frame.mid(4), 5, 0, 99999); + if (!value) { return false; } + status.faultCode = code; + status.faultDetail = *value / (code == 'N' ? 1.0 : code == 'H' || code == 'L' ? 1000.0 : 10.0); + return true; + } + return false; +} +bool matchesStatusQuery(const QByteArray& query, const QByteArray& frame) +{ + if (query == ";") { return frame.isEmpty(); } + if (!query.endsWith(';')) { return false; } + const QByteArray prefix = query.chopped(1); + if (!frame.startsWith(prefix)) { return false; } + if (query == "^F;" && (frame.size() != 7 || frame[2] < '0' || frame[2] > '9')) { return false; } + Status scratch; + return applyStatus(frame, scratch); +} +QByteArray buildFrequencyCommand(const QByteArray& command, int khz) +{ + if (!isSupportedFrequency(khz) || (command != "^SM" && command != "^MT" && command != "^DM")) { return {}; } + return command + QByteArray::number(khz).rightJustified(5, '0') + ';'; +} +QByteArray buildErase(int band, int antenna) +{ + if (!((band >= 0 && band <= 10) || band == 15) || antenna < 0 || antenna > 2) { return {}; } + return "^EM" + bandCode(band) + QByteArray::number(antenna) + ';'; +} +QByteArray configurationSet(int band, int mask) +{ + return band >= 0 && band < 11 && mask >= 1 && mask <= 3 + ? "^AE" + bandCode(band) + QByteArray::number(mask) + ';' : QByteArray{}; +} +std::optional parseConfiguration(const QByteArray& frame) +{ + if (!frame.startsWith("^AEA") || frame.size() != 15) { return {}; } + Configuration result; + for (int band = 0; band < 11; ++band) { + const auto value = number(frame.mid(4 + band, 1), 1, 1, 3); + if (!value) { return {}; } + result[band] = *value; + } + return result; +} +QString bandName(int band) +{ + static const QStringList names{QStringLiteral("160m"),QStringLiteral("80m"),QStringLiteral("60m"),QStringLiteral("40m"), + QStringLiteral("30m"),QStringLiteral("20m"),QStringLiteral("17m"),QStringLiteral("15m"),QStringLiteral("12m"),QStringLiteral("10m"),QStringLiteral("6m")}; + return band >= 0 && band < names.size() ? names[band] : QStringLiteral("Unknown band"); +} +int relayTotal(bool capacitor, int mask) +{ + constexpr std::array capacitors{10,22,40,82,150,300,660,1360}; + constexpr std::array inductors{50,110,230,480,1000,2100,4400,9000}; + if (mask < 0 || mask > 255) { return 0; } + int total = 0; + for (int bit = 0; bit < 8; ++bit) { if (mask & (1 << bit)) { total += (capacitor ? capacitors : inductors)[bit]; } } + return total; +} +int relayMaskForValue(bool capacitor, int value) +{ + int closest = 0; + qint64 distance = std::numeric_limits::max(); + for (int mask = 0; mask < 256; ++mask) { + const qint64 delta = std::abs(qint64(relayTotal(capacitor, mask)) - value); + if (delta < distance) { closest = mask; distance = delta; } + } + return closest; +} +QString currentFaultName(char code) +{ + switch (code) { + case 'N': return QStringLiteral("No fault"); + case 'A': return QStringLiteral("No tuner match"); + case 'C': return QStringLiteral("High drain current"); + case 'D': return QStringLiteral("High dissipation"); + case 'H': return QStringLiteral("High supply voltage"); + case 'I': return QStringLiteral("High input power"); + case 'L': return QStringLiteral("Low supply voltage"); + case 'P': return QStringLiteral("High output power"); + case 'R': return QStringLiteral("High reflected power"); + case 'S': return QStringLiteral("High SWR"); + case 'T': return QStringLiteral("High temperature"); + default: return QStringLiteral("Unknown fault (%1)").arg(quint8(code)); + } +} +QString faultUnit(char code) +{ + switch (code) { + case 'N': return QStringLiteral("power-ons"); + case 'A': case 'S': return QStringLiteral(":1"); + case 'C': return QStringLiteral("A"); + case 'D': case 'P': case 'R': case 'I': return QStringLiteral("W"); + case 'H': case 'L': return QStringLiteral("V"); + case 'T': return QStringLiteral("°C"); + default: return QStringLiteral("raw"); + } +} +std::optional decodeMemoryBin(const QByteArray& frame) +{ + static const QRegularExpression pattern(QStringLiteral( + "^\\^DM F([0-9]{5}) BN([0-9]{2}) BIN([0-9]{3}) FR([0-9]{5})-([0-9]{5})\\s+" + "AN1 (EMPTY|BY[BN] SI[TA] CR[0-9A-F]{2} LR[0-9A-F]{2})\\s+" + "AN2 (EMPTY|BY[BN] SI[TA] CR[0-9A-F]{2} LR[0-9A-F]{2})$")); + const auto match = pattern.match(QString::fromLatin1(frame).simplified()); + if (!match.hasMatch()) { return {}; } + MemoryBin result; + result.frequencyKhz = match.captured(1).toInt(); result.band = match.captured(2).toInt(); + result.index = match.captured(3).toInt(); result.firstKhz = match.captured(4).toInt(); result.lastKhz = match.captured(5).toInt(); + if (result.band > 10 || result.firstKhz < 1500 || result.lastKhz > 60000 || result.firstKhz > result.lastKhz || + result.frequencyKhz < result.firstKhz || result.frequencyKhz > result.lastKhz) { return {}; } + for (int antenna = 1; antenna <= 2; ++antenna) { + MemoryMatch entry; entry.antenna = antenna; + const QString text = match.captured(5 + antenna); + entry.empty = text == QStringLiteral("EMPTY"); + if (!entry.empty) { + const QStringList fields = text.split(' '); + entry.bypassed = fields[0].endsWith('B'); + entry.side = fields[1].endsWith('T') ? CapacitorSide::Transmitter : CapacitorSide::Antenna; + entry.capacitorMask = fields[2].mid(2).toInt(nullptr, 16); entry.inductorMask = fields[3].mid(2).toInt(nullptr, 16); + } + result.matches.append(entry); + } + return result; +} +} diff --git a/src/core/Kxpa100Protocol.h b/src/core/Kxpa100Protocol.h new file mode 100644 index 0000000000..4402e5f22c --- /dev/null +++ b/src/core/Kxpa100Protocol.h @@ -0,0 +1,24 @@ +#pragma once +#include "models/Kxpa100Types.h" +#include +#include + +namespace AetherSDR::Kxpa100 { +class FrameParser { +public: + void setCallback(std::function callback) { m_callback = std::move(callback); } + void feed(const QByteArray& bytes); + void reset() { m_buffer.clear(); m_discard = false; } +private: + QByteArray m_buffer; + bool m_discard = false; + std::function m_callback; +}; +bool applyStatus(const QByteArray& frame, Status& status); +bool matchesStatusQuery(const QByteArray& query, const QByteArray& frame); +QByteArray buildFrequencyCommand(const QByteArray& command, int khz); +QByteArray buildErase(int band, int antenna); +QByteArray configurationSet(int band, int mask); +std::optional parseConfiguration(const QByteArray& frame); +std::optional decodeMemoryBin(const QByteArray& frame); +} diff --git a/src/gui/AppletPanel.cpp b/src/gui/AppletPanel.cpp index 6158f77ee8..db852b4617 100644 --- a/src/gui/AppletPanel.cpp +++ b/src/gui/AppletPanel.cpp @@ -17,6 +17,7 @@ #include "VkampApplet.h" #include "Kpa500Applet.h" #include "Kat500Applet.h" +#include "Kxpa100Applet.h" #include "Kpa1500Applet.h" #include "LpMeterApplet.h" #include "TxApplet.h" @@ -927,6 +928,23 @@ AppletPanel::AppletPanel(QWidget* parent) : QWidget(parent) m_appletOrder.append(entry); } + m_kxpa100Applet = new Kxpa100Applet; + { + auto entry = makeEntry("KXPA100", "Elecraft KXPA100 Amplifier / Tuner", m_kxpa100Applet, + false, m_drawer, m_drawerLayout, "KX100"); + entry.btn->setObjectName(QStringLiteral("kxpa100BarButton")); + entry.btn->setAccessibleName(tr("Elecraft KXPA100 amplifier and tuner")); + markHardwareConditional("KXPA100"); + if (auto* container = qobject_cast(entry.widget)) { + container->setDefaultFloatingSize(QSize(560, 720)); + connect(container, &ContainerWidget::dockModeChanged, m_kxpa100Applet, + [this](ContainerWidget::DockMode mode) { + m_kxpa100Applet->setFloating(mode != ContainerWidget::DockMode::PanelDocked); + }); + } + m_appletOrder.append(entry); + } + // Elecraft KPA1500 — independent of AMP (PGXL), ACOM, SPE and VKAMP // for the same reason they are independent of each other: a station can // have any combination of them connected at once. Deliberately NOT in @@ -1458,6 +1476,7 @@ QList AppletPanel::appletCatalog() const {QStringLiteral("SPE"), QStringLiteral("Amplifiers")}, {QStringLiteral("KPA500"), QStringLiteral("Amplifiers")}, {QStringLiteral("KAT500"), QStringLiteral("Transmit")}, + {QStringLiteral("KXPA100"), QStringLiteral("Transmit")}, {QStringLiteral("VKAMP"), QStringLiteral("Amplifiers")}, {QStringLiteral("KPA1500"), QStringLiteral("Amplifiers")}, {QStringLiteral("EQ"), QStringLiteral("Audio & DSP")}, @@ -1902,6 +1921,12 @@ void AppletPanel::setKat500Visible(bool visible) applyBarLayout(); } +void AppletPanel::setKxpa100Visible(bool visible) +{ + updateHardwareAvailability("KXPA100", "Applet_KXPA100", visible); + applyBarLayout(); +} + void AppletPanel::setKpa500Visible(bool visible) { updateHardwareAvailability("KPA500", "Applet_KPA500", visible); diff --git a/src/gui/AppletPanel.h b/src/gui/AppletPanel.h index 6c7a695f99..2d80a60be1 100644 --- a/src/gui/AppletPanel.h +++ b/src/gui/AppletPanel.h @@ -38,6 +38,7 @@ class SpeApplet; class VkampApplet; class Kpa500Applet; class Kat500Applet; +class Kxpa100Applet; class Kpa1500Applet; class LpMeterApplet; class TxApplet; @@ -112,6 +113,7 @@ class AppletPanel : public QWidget { SpeApplet* speApplet() { return m_speApplet; } VkampApplet* vkampApplet() { return m_vkampApplet; } Kat500Applet* kat500Applet() { return m_kat500Applet; } + Kxpa100Applet* kxpa100Applet() { return m_kxpa100Applet; } Kpa500Applet* kpa500Applet() { return m_kpa500Applet; } Kpa1500Applet* kpa1500Applet() { return m_kpa1500Applet; } LpMeterApplet* lpMeterApplet() { return m_lpMeterApplet; } @@ -201,6 +203,7 @@ class AppletPanel : public QWidget { // serial amplifier connection. Fully independent of all other amp applets. void setKpa500Visible(bool visible); void setKat500Visible(bool visible); + void setKxpa100Visible(bool visible); // Show/hide the KPA1500 button and applet based on a direct Elecraft // KPA1500 connection (#4097). Independent of every other amplifier // applet for the same reason setVkampVisible is: these are separate, @@ -429,6 +432,7 @@ class AppletPanel : public QWidget { LpMeterApplet* m_lpMeterApplet{nullptr}; QPushButton* m_vkampBtn{nullptr}; Kat500Applet* m_kat500Applet{nullptr}; + Kxpa100Applet* m_kxpa100Applet{nullptr}; Kpa500Applet* m_kpa500Applet{nullptr}; QPushButton* m_kpa500Btn{nullptr}; TxApplet* m_txApplet{nullptr}; diff --git a/src/gui/Kxpa100AdvancedWidget.cpp b/src/gui/Kxpa100AdvancedWidget.cpp new file mode 100644 index 0000000000..12891e796d --- /dev/null +++ b/src/gui/Kxpa100AdvancedWidget.cpp @@ -0,0 +1,633 @@ +#include "Kxpa100AdvancedWidget.h" +#include "CanonIndicators.h" +#include "models/Kxpa100Model.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace AetherSDR +{ +namespace +{ +void available(QWidget* widget, const QString& reason) +{ + widget->setEnabled(reason.isEmpty()); + widget->setToolTip(reason); + widget->setAccessibleDescription(reason); +} +bool save(const QString& path, const QByteArray& data, QString* error) +{ + QSaveFile file(path); + if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.commit()) { + *error = file.errorString(); + return false; + } + return true; +} +void fillTable(QTableWidget* table, int row, const QStringList& values) +{ + for (int c = 0; c < values.size(); ++c) { + table->setItem(row, c, new QTableWidgetItem(values[c])); + } +} +} // namespace +Kxpa100AdvancedWidget::Kxpa100AdvancedWidget(Kxpa100Model* model, QWidget* parent) + : QWidget(parent), m_model(model) +{ + setObjectName("kxpa100Advanced"); + setAccessibleName(tr("KXPA100 Advanced settings and maintenance")); + auto* root = new QVBoxLayout(this); + root->setContentsMargins(0, 4, 0, 0); + root->setSpacing(6); + const auto label = [this](const QString& text, const char* name, QLayout* layout) { + auto* l = new QLabel(text, this); + l->setObjectName(name); + l->setTextFormat(Qt::PlainText); + l->setWordWrap(true); + layout->addWidget(l); + return l; + }; + const auto button = [this](const QString& title, const char* name, QLayout* layout) { + auto* b = new QPushButton(title, this); + b->setObjectName(name); + b->setAccessibleName(title); + b->setAutoDefault(false); + b->setFixedHeight(22); + layout->addWidget(b); + return b; + }; + applyCanonSheet(this, [] { + return QStringLiteral( + "QWidget#kxpa100Advanced, QWidget#kxpa100Advanced QLabel, QWidget#kxpa100Advanced QCheckBox, " + "QWidget#kxpa100Advanced QComboBox, QWidget#kxpa100Advanced QSpinBox, " + "QWidget#kxpa100Advanced QTableWidget, QWidget#kxpa100Advanced QHeaderView::section, " + "QWidget#kxpa100Advanced QLineEdit, QWidget#kxpa100Advanced QProgressBar { font-size: 10px; }" + "QWidget#kxpa100Advanced QLabel { color: {{color.canon.muted}}; }" + "QWidget#kxpa100Advanced QGroupBox { font-size: 10px; font-weight: bold; }" + "QWidget#kxpa100Advanced QPushButton { font-size: 10px; padding: 0 4px; }" + "QWidget#kxpa100Advanced QTableWidget::item { padding: 1px 3px; }" + "QWidget#kxpa100Advanced QHeaderView::section { padding: 2px 3px; }"); + }); + m_identity = label({}, "kxAdvancedIdentity", root); + m_availability = label({}, "kxAdvancedAvailability", root); + auto* config = new QGroupBox(tr("Configuration — staged edits"), this); + auto* configuration = new QVBoxLayout(config); + auto* actions = new QHBoxLayout; + auto* read = button(tr("Read configuration"), "kxReadConfiguration", actions); + configuration->addLayout(actions); + label(tr("Read before editing. ANT1 and ANT2 checkboxes stage enabled antennas per band. " + "The All bands row applies a deliberate change to every band. At least one antenna must remain enabled. " + "Apply verifies device readback; Cancel edits discards only staging."),"kxConfigurationHelp",configuration); + m_configuration = new QTableWidget(12,4,this); + m_configuration->setObjectName("kxConfiguration"); + m_configuration->setAccessibleName(tr("Confirmed and staged KXPA100 antenna enables")); + m_configuration->setHorizontalHeaderLabels({tr("Band"),tr("ANT1"),tr("ANT2"),tr("Readback")}); + m_configuration->verticalHeader()->hide(); + m_configuration->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + m_configuration->verticalHeader()->setMinimumSectionSize(22); + m_configuration->verticalHeader()->setDefaultSectionSize(22); + m_configuration->verticalHeader()->setSectionResizeMode(QHeaderView::Fixed); + m_configuration->setMinimumHeight(292); m_configuration->setMaximumHeight(310); + m_configuration->setWordWrap(false); + configuration->addWidget(m_configuration); + auto* edits = new QVBoxLayout; + auto* apply = button(tr("Apply changed fields"), "kxApplyConfiguration", edits); + auto* discard = button(tr("Cancel edits"), "kxDiscardConfiguration", edits); + configuration->addLayout(edits); + root->addWidget(config); + auto* backupGroup = new QGroupBox(tr("Complete native backup and restore"), this); + auto* backup = new QVBoxLayout(backupGroup); + auto* files = new QVBoxLayout; + auto* saveBackup = button(tr("Read and save backup…"), "kxSaveBackup", files); + auto* choose = button(tr("Choose backup…"), "kxChooseBackup", files); + backup->addLayout(files); + m_preview = label(tr("No backup selected."), "kxBackupPreview", backup); + label(tr("A .KXPAConfig backup contains both EEPROM regions (17,408 bytes). Restore verifies each " + "allowed block and excludes amplifier identity and fault history. Cancellation or failure cannot " + "undo blocks already written. A successful verified restore requests a restart; reconnect to inspect settings."), + "kxBackupHelp", backup); + m_restoreConfirm = + new QCheckBox(tr("Confirm backup replacement"), this); + m_restoreConfirm->setObjectName("kxConfirmRestore"); + backup->addWidget(m_restoreConfirm); + label(tr("Replace this tuner's settings and stored matches with the previewed backup."), + "kxRestoreConfirmationHelp", backup); + auto* restore = button(tr("Restore and verify"), "kxRestoreBackup", backup); + root->addWidget(backupGroup); + auto* faultGroup = new QGroupBox(tr("Stored fault history — newest first"), this); + auto* faults = new QVBoxLayout(faultGroup); + auto* faultActions = new QVBoxLayout; + auto* readFaultsButton = button(tr("Read fault history"), "kxReadFaults", faultActions); + auto* exportFaults = button(tr("Export CSV…"), "kxExportFaults", faultActions); + faults->addLayout(faultActions); + label( + tr("Elapsed device time is not a calendar timestamp. Power On history is distinct from the current fault state. " + "Unknown records retain raw bytes. Clear fault in the applet clears only the current fault."), + "kxFaultHistoryHelp", faults); + m_faults = new QTableWidget(0, 7, this); + m_faults->setObjectName("kxStoredFaults"); + m_faults->setAccessibleName(tr("Stored tuner fault history")); + m_faults->setHorizontalHeaderLabels({tr("Slot"), tr("Elapsed s"), tr("Fault"), tr("Freq kHz"), tr("Value"), tr("Unit"), tr("Raw bytes")}); + faults->addWidget(m_faults); + root->addWidget(faultGroup); + auto* memoriesGroup = new QGroupBox(tr("Memories and reset"), this); + auto* memories = new QVBoxLayout(memoriesGroup); + auto* memoryActions = new QVBoxLayout; + m_memoryFrequency = new QSpinBox(this); + m_memoryFrequency->setObjectName("kxInspectFrequency"); + m_memoryFrequency->setAccessibleName( + tr("Memory inspection frequency in kHz")); + m_memoryFrequency->setRange(0, 54000); + m_memoryFrequency->setSuffix(tr(" kHz")); + m_memoryFrequency->setSpecialValueText(tr("Select frequency")); + memoryActions->addWidget(m_memoryFrequency); + auto* inspect = button(tr("Inspect bin"), "kxInspectMemories", memoryActions); + memories->addLayout(memoryActions); + m_memorySummary = label(tr("No bin inspected."), "kxMemorySummary", memories); + m_memories = new QTableWidget(0, 6, this); + m_memories->setObjectName("kxStoredMemories"); + m_memories->setAccessibleName(tr("Stored matches, independent of live tuner state")); + m_memories->setHorizontalHeaderLabels( + {tr("ANT"), tr("Bypassed"), tr("C mask"), tr("L mask"), tr("C side"), tr("State")}); + memories->addWidget(m_memories); + auto* eraseSelectors = new QHBoxLayout; + m_eraseBand = new QComboBox(this); + m_eraseBand->setObjectName("kxEraseBand"); + m_eraseBand->setAccessibleName(tr("Band to erase")); + m_eraseBand->addItem(tr("All 11 bands"), 15); + for (int i = 0; i < 11; ++i) { + m_eraseBand->addItem(Kxpa100::bandName(i), i); + } + eraseSelectors->addWidget(m_eraseBand); + m_eraseAntenna = new QComboBox(this); + m_eraseAntenna->setObjectName("kxEraseAntenna"); + m_eraseAntenna->setAccessibleName(tr("Antenna memories to erase")); + m_eraseAntenna->addItem(tr("All antennas"), 0); + for (int i = 1; i <= 2; ++i) { + m_eraseAntenna->addItem(tr("ANT %1").arg(i), i); + } + eraseSelectors->addWidget(m_eraseAntenna); + memories->addLayout(eraseSelectors); + auto* eraseTarget = label({}, "kxEraseTarget", memories); + m_eraseConfirm = new QCheckBox(tr("Confirm selected erase"), this); + m_eraseConfirm->setObjectName("kxConfirmErase"); + memories->addWidget(m_eraseConfirm); + auto* erase = button(tr("Erase selected memories"), "kxEraseMemories", memories); + m_resetConfirm = new QCheckBox( + tr("Confirm factory reset"), this); + m_resetConfirm->setObjectName("kxConfirmReset"); + memories->addWidget(m_resetConfirm); + label(tr("Erase settings and stored matches, restart, and reconnect at the default baud."), + "kxResetConfirmationHelp", memories); + auto* reset = button(tr("Factory reset"), "kxFactoryReset", memories); + root->addWidget(memoriesGroup); + for (auto* table : {m_faults, m_memories}) { + table->setEditTriggers(QAbstractItemView::NoEditTriggers); + table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + table->horizontalHeader()->setStretchLastSection(true); + table->setMinimumHeight(150); + table->setMaximumHeight(230); + } + m_progress = new QProgressBar(this); + m_progress->setObjectName("kxMaintenanceProgress"); + m_progress->setAccessibleName(tr("Tuner operation progress")); + root->addWidget(m_progress); + m_status = label(tr("Ready. No operation has run."), "kxMaintenanceStatus", root); + auto* cancel = button(tr("Cancel operation"), "kxCancelMaintenance", root); + connect(m_configuration,&QTableWidget::itemChanged,this,[this](QTableWidgetItem* item) { + if (item->column()==1 || item->column()==2) { stageField(item->row(),item->column()); } + }); + connect(m_memoryFrequency,&QSpinBox::valueChanged,this,&Kxpa100AdvancedWidget::refreshAvailability); + connect(discard, &QPushButton::clicked, this, [this] { + m_staged.reset(); + renderConfiguration(); + refreshAvailability(); + }); + connect(read, &QPushButton::clicked, this, [this] { + if (m_model) { + m_staged.reset(); + m_ownOperation = true; + m_model->readConfiguration(); + } + }); + connect(apply, &QPushButton::clicked, this, [this] { + if (m_model && m_staged.has_value()) { + m_applying = true; + m_ownOperation = true; + m_model->applyConfiguration(*m_staged); + } + }); + connect(saveBackup, &QPushButton::clicked, this, [this] { openFileDialog(FileAction::Backup); }); + connect(choose, &QPushButton::clicked, this, [this] { openFileDialog(FileAction::Choose); }); + connect(exportFaults, &QPushButton::clicked, this, [this] { openFileDialog(FileAction::Faults); }); + connect(restore, &QPushButton::clicked, this, [this] { + if (m_model && m_backup && restoreReason().isEmpty() && m_restoreConfirm->isChecked()) { + m_ownOperation = true; + m_model->restoreBackup(*m_backup, true); + } + }); + connect(readFaultsButton, &QPushButton::clicked, this, [this] { + if (m_model) { + m_ownOperation = true; + m_model->readFaultHistory(); + } + }); + connect(inspect, &QPushButton::clicked, this, [this] { + if (m_model) { + m_ownOperation = true; + m_model->inspectMemories(m_memoryFrequency->value()); + } + }); + connect(erase, &QPushButton::clicked, this, [this] { + if (m_model && m_eraseConfirm->isChecked()) { + m_ownOperation = true; + m_model->eraseMemories(m_eraseBand->currentData().toInt(), m_eraseAntenna->currentData().toInt(), + true); + } + }); + connect(reset, &QPushButton::clicked, this, [this] { + if (m_model && m_resetConfirm->isChecked()) { + m_ownOperation = true; + m_model->factoryReset(true); + } + }); + connect(cancel, &QPushButton::clicked, this, [this] { + if (m_model && m_ownOperation) { + m_model->cancelOperation(); + } + }); + for (auto* check : {m_restoreConfirm, m_eraseConfirm, m_resetConfirm}) { + check->setAccessibleName(check == m_restoreConfirm + ? tr("Confirm replacing this tuner's settings and stored matches with the previewed backup") + : check == m_resetConfirm + ? tr("Confirm factory reset: erase settings and matches, restart, and reconnect at the default baud") + : check->text()); + connect(check, &QCheckBox::toggled, this, &Kxpa100AdvancedWidget::refreshAvailability); + } + const auto targetChanged = [this, eraseTarget] { + m_eraseConfirm->setChecked(false); + const QString target = tr("Erase target: %1 / %2").arg(m_eraseBand->currentText(), m_eraseAntenna->currentText()); + eraseTarget->setText(target); + m_eraseConfirm->setAccessibleName(tr("Confirm selected erase. %1").arg(target)); + refreshAvailability(); + }; + connect(m_eraseBand, &QComboBox::currentIndexChanged, this, targetChanged); + connect(m_eraseAntenna, &QComboBox::currentIndexChanged, this, targetChanged); + targetChanged(); + if (model) { + connect(model, &Kxpa100Model::configurationRead, this, &Kxpa100AdvancedWidget::configurationRead); + connect(model, &Kxpa100Model::availabilityChanged, this, &Kxpa100AdvancedWidget::refreshAvailability); + connect(model, &Kxpa100Model::statusUpdated, this, &Kxpa100AdvancedWidget::refreshAvailability); + connect(model, &Kxpa100Model::operationBusyChanged, this, &Kxpa100AdvancedWidget::refreshAvailability); + connect(model, &Kxpa100Model::connected, this, &Kxpa100AdvancedWidget::clearDeviceData); + connect(model, &Kxpa100Model::disconnected, this, &Kxpa100AdvancedWidget::clearDeviceData); + connect(model, &Kxpa100Model::operationProgress, this, [this](int done, int total) { + if (m_ownOperation) { + m_progress->setRange(0, total); + m_progress->setValue(done); + } + }); + connect(model, &Kxpa100Model::operationFinished, this, [this](const QString& message) { + if (m_ownOperation) { + if (m_applying) { + m_staged.reset(); + m_confirmed = m_model->configuration(); + renderConfiguration(); + } + m_applying = false; + m_ownOperation = false; + m_restoreConfirm->setChecked(false); + m_eraseConfirm->setChecked(false); + m_resetConfirm->setChecked(false); + showStatus(message); + refreshAvailability(); + } + }); + connect(model, &Kxpa100Model::operationFailed, this, [this](const QString& reason) { + if (m_ownOperation) { + m_applying = false; + m_ownOperation = false; + m_savePath.clear(); + showStatus( + reason + + tr(" Confirmed changes may already have applied; review readbacks before retrying.")); + renderConfiguration(); + refreshAvailability(); + } + }); + connect(model, &Kxpa100Model::backupRead, this, [this](const Kxpa100::Backup& data) { + if (m_savePath.isEmpty()) { + return; + } + QString error; + const QString path = m_savePath; + m_savePath.clear(); + const bool saved = Kxpa100Model::saveBackupFile(path, data, &error); + showStatus(saved ? tr("Complete backup saved: %1").arg(path) + : tr("Backup save failed: %1").arg(error)); + }); + connect(model, &Kxpa100Model::faultHistoryRead, this, &Kxpa100AdvancedWidget::readFaults); + connect(model, &Kxpa100Model::memoriesRead, this, &Kxpa100AdvancedWidget::readMemories); + connect(model, &QObject::destroyed, this, [this] { + m_model = nullptr; + clearDeviceData(); + }); + } + renderConfiguration(); + refreshAvailability(); +} +Kxpa100AdvancedWidget::~Kxpa100AdvancedWidget() +{ + if (m_fileDialog) { + QFileDialog* dialog = m_fileDialog.data(); + m_fileDialog.clear(); + disconnect(dialog, nullptr, this, nullptr); + dialog->reject(); + // Native sheets can still be unwinding their completion callback. + // QWidget child teardown must not delete them synchronously. + dialog->setParent(nullptr); + dialog->deleteLater(); + } + if (m_model && m_ownOperation) { + m_model->cancelOperation(); + } +} +void Kxpa100AdvancedWidget::showStatus(const QString& message) +{ + m_status->setText(message); + m_status->setAccessibleDescription(message); + QAccessibleEvent event(m_status, QAccessible::NameChanged); + QAccessible::updateAccessibility(&event); +} +QString Kxpa100AdvancedWidget::schemaReason(bool writing) const +{ + if (!m_model) { + return tr("Connect a KXPA100 first."); + } + const QString reason = m_model->unavailableReason(writing); + if (!reason.isEmpty()) { + return reason; + } + if (m_model->status().firmware != "01.39") { + return tr("Configuration and memory schema support is verified for firmware 01.39 only. Use the " + "official Utility for other firmware."); + } + return {}; +} +QString Kxpa100AdvancedWidget::restoreReason() const +{ + const QString reason = schemaReason(true); + if (!reason.isEmpty()) { + return reason; + } + if (!m_backup) { + return tr("Choose a complete validated .KXPAConfig backup first."); + } + if (m_backup->firmware != m_model->status().firmware || m_backup->serial != m_model->status().serial) { + return tr("Backup firmware and serial must match this amplifier."); + } + return {}; +} +void Kxpa100AdvancedWidget::refreshAvailability() +{ + const QString readReason = schemaReason(false), writeReason = schemaReason(true); + const QString tunerWrite = !writeReason.isEmpty() ? writeReason + : m_model->unavailableReason(true, true); + const QString tunerRead = !readReason.isEmpty() ? readReason + : m_model->unavailableReason(false, true); + const bool busy = m_model && m_model->operationBusy(); + m_identity->setText(m_model ? tr("%1 · %2 · S/N %3 · Firmware %4") + .arg(m_model->description(), m_model->status().identity, + m_model->status().serial, m_model->status().firmware) + : tr("Disconnected")); + m_availability->setText(!readReason.isEmpty() ? readReason + : writeReason.isEmpty() + ? tr("Ready. Apply sends changed fields and verifies readback.") + : writeReason); + for (const char* name : + {"kxReadConfiguration", "kxSaveBackup", "kxReadFaults", "kxInspectMemories"}) { + available(findChild(name), readReason); + } + available(m_configuration, !tunerRead.isEmpty() ? tunerRead + : !m_confirmed + ? tr("Read complete configuration before editing.") + : QString()); + available(findChild("kxApplyConfiguration"), !tunerWrite.isEmpty() ? tunerWrite + : !m_staged + ? tr("Deliberately edit a field first.") + : QString()); + available(findChild("kxDiscardConfiguration"), + busy ? tr("Wait for the operation to finish.") : QString()); + available(findChild("kxChooseBackup"), + busy ? tr("Wait for the operation to finish.") : QString()); + available(m_restoreConfirm, restoreReason()); + available(findChild("kxRestoreBackup"), + !restoreReason().isEmpty() ? restoreReason() + : !m_restoreConfirm->isChecked() ? tr("Confirm replacement of stored settings first.") + : QString()); + available(findChild("kxExportFaults"), !m_faultsLoaded ? tr("Read fault history first.") + : busy ? tr("Wait for the operation to finish.") + : QString()); + available(findChild("kxReadConfiguration"),tunerRead); + available(findChild("kxInspectMemories"),!tunerRead.isEmpty() ? tunerRead + : Kxpa100::isSupportedFrequency(m_memoryFrequency->value()) ? QString() : tr("Select 1800–54000 kHz.")); + available(m_eraseConfirm, tunerWrite); + available(m_resetConfirm, writeReason); + available(findChild("kxEraseMemories"), + !tunerWrite.isEmpty() ? tunerWrite + : !m_eraseConfirm->isChecked() ? tr("Confirm the selected band and antenna erase target first.") + : QString()); + available(findChild("kxFactoryReset"), + !writeReason.isEmpty() ? writeReason + : !m_resetConfirm->isChecked() ? tr("Confirm factory reset and default-baud reconnect first.") + : QString()); + available(findChild("kxCancelMaintenance"), + !busy || !m_ownOperation ? tr("No operation is running from this panel.") : QString()); + m_progress->setVisible(busy && m_ownOperation); + available(m_eraseBand, busy ? tr("Wait for the operation to finish.") : QString()); + available(m_eraseAntenna, busy ? tr("Wait for the operation to finish.") : QString()); +} +void Kxpa100AdvancedWidget::configurationRead(const Kxpa100::Configuration& values) +{ + m_confirmed=values; + if (m_staged && *m_staged==values) { m_staged.reset(); } + renderConfiguration(); refreshAvailability(); +} +void Kxpa100AdvancedWidget::renderConfiguration() +{ + const QSignalBlocker blocker(m_configuration); + for (int row=0;row<12;++row) { + auto* name=new QTableWidgetItem(row==0 ? tr("All bands") : Kxpa100::bandName(row-1)); + name->setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable); m_configuration->setItem(row,0,name); + const auto values=m_staged ? m_staged : m_confirmed; + for (int antenna=1;antenna<=2;++antenna) { + auto* item=new QTableWidgetItem; + item->setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable|Qt::ItemIsUserCheckable); + int on=0; const int start=row==0 ? 0 : row-1, end=row==0 ? 10 : row-1; + for (int band=start;band<=end;++band) { if (values && ((*values)[band] & (1<<(antenna-1)))) { ++on; } } + item->setCheckState(on==0 ? Qt::Unchecked : on==end-start+1 ? Qt::Checked : Qt::PartiallyChecked); + item->setData(Qt::AccessibleTextRole,tr("%1 ANT%2 enabled").arg(name->text()).arg(antenna)); + item->setToolTip(tr("Stage this antenna enable for %1; Apply sends it to the device.").arg(name->text())); + item->setData(Qt::AccessibleDescriptionRole,item->toolTip()); m_configuration->setItem(row,antenna,item); + } + QString readback=tr("Unknown"); + if (m_confirmed) { + const auto value=(*m_confirmed)[row==0 ? 0 : row-1]; + const bool mixed=row==0 && std::any_of(m_confirmed->begin(),m_confirmed->end(),[value](int mask) { return mask!=value; }); + readback=mixed ? tr("Mixed") : value==3 ? tr("ANT1 + 2") : tr("ANT%1").arg(value); + } + auto* actual=new QTableWidgetItem(readback); actual->setFlags(Qt::ItemIsEnabled|Qt::ItemIsSelectable); + m_configuration->setItem(row,3,actual); + } +} +void Kxpa100AdvancedWidget::stageField(int row,int column) +{ + if (!m_confirmed || row<0 || row>=12 || column<1 || column>2) { return; } + const bool on=m_configuration->item(row,column)->checkState()==Qt::Checked; + Kxpa100::Configuration staged=m_staged.value_or(*m_confirmed); + const int start=row==0 ? 0 : row-1, end=row==0 ? 10 : row-1, bit=1<<(column-1); + for (int band=start;band<=end;++band) { + staged[band]=on ? staged[band]|bit : staged[band]&~bit; + if (staged[band]==0) { showStatus(tr("At least one antenna must remain enabled on every band.")); renderConfiguration(); return; } + } + m_staged=staged==*m_confirmed ? std::nullopt : std::optional(staged); + renderConfiguration(); refreshAvailability(); showStatus(tr("Antenna changes staged. Nothing has been sent.")); +} +void Kxpa100AdvancedWidget::clearDeviceData() +{ + m_confirmed.reset(); + m_staged.reset(); + m_savePath.clear(); + m_faultRecords.clear(); + m_faultsLoaded = false; + m_faults->setRowCount(0); + m_memories->setRowCount(0); + m_memorySummary->setText(tr("No bin inspected for this session.")); + m_restoreConfirm->setChecked(false); + m_eraseConfirm->setChecked(false); + m_resetConfirm->setChecked(false); + m_applying = false; + m_ownOperation = false; + renderConfiguration(); + refreshAvailability(); +} +void Kxpa100AdvancedWidget::openFileDialog(FileAction action) +{ + if (m_fileDialog) { + m_fileDialog->raise(); + m_fileDialog->activateWindow(); + return; + } + auto* dialog = new QFileDialog(this); + m_fileDialog = dialog; + dialog->setObjectName("kxMaintenanceFileDialog"); + dialog->setAccessibleName(tr("KXPA100 maintenance file selection")); + dialog->setWindowTitle(action == FileAction::Choose ? tr("Choose KXPA100 backup") + : action == FileAction::Backup ? tr("Save complete KXPA100 backup") + : tr("Export fault history")); + dialog->setNameFilter(action == FileAction::Faults + ? tr("CSV files (*.csv)") + : tr("KXPA100 configuration (*.KXPAConfig);;All files (*)")); + dialog->setAcceptMode(action == FileAction::Choose ? QFileDialog::AcceptOpen : QFileDialog::AcceptSave); + dialog->setFileMode(action == FileAction::Choose ? QFileDialog::ExistingFile : QFileDialog::AnyFile); + dialog->setDefaultSuffix(action == FileAction::Faults ? "csv" : "KXPAConfig"); + if (action != FileAction::Choose) { + dialog->selectFile( + action == FileAction::Faults + ? "kxpa100-fault-history.csv" + : QStringLiteral("SN%1.KXPAConfig").arg(m_model ? m_model->status().serial : QString())); + } + connect(dialog, &QDialog::finished, this, [this, dialog, action](int result) { + const QStringList paths = dialog->selectedFiles(); + m_fileDialog.clear(); + dialog->deleteLater(); + if (result != QDialog::Accepted || paths.isEmpty()) { + return; + } + if (action == FileAction::Choose) { + loadBackup(paths.first()); + } else if (action == FileAction::Backup) { + const QString reason = schemaReason(false); + if (!reason.isEmpty()) { + showStatus(reason); + return; + } + m_savePath = paths.first(); + m_ownOperation = true; + if (!m_model->readBackup()) { + m_savePath.clear(); + } + } else { + if (!m_faultsLoaded) { + showStatus(tr("Read fault history again before exporting.")); + return; + } + QString error; + const bool saved = save(paths.first(), Kxpa100Model::faultsCsv(m_faultRecords), &error); + showStatus(saved ? tr("Fault history exported.") : error); + } + }); + dialog->open(); +} +bool Kxpa100AdvancedWidget::loadBackup(const QString& path) +{ + m_backup.reset(); + m_restoreConfirm->setChecked(false); + QFile file(path); + QString error; + if (!file.open(QIODevice::ReadOnly)) { + error = file.errorString(); + } else if (file.size() > 64 * 1024) { + error = tr("Backup exceeds the 64 KiB limit."); + } else { + m_backup = Kxpa100Model::decodeBackupFile(file.read(64 * 1024 + 1), &error); + } + m_preview->setText(m_backup ? tr("%1\nS/N %2 · Firmware %3 · 17,408 bytes · complete coverage and CRC " + "verified\nRestore excludes identity and fault history.") + .arg(QFileInfo(path).fileName(), m_backup->serial, m_backup->firmware) + : tr("No valid backup selected.")); + showStatus(m_backup ? tr("Backup validated for preview. No tuner settings changed.") : error); + refreshAvailability(); + return m_backup.has_value(); +} +void Kxpa100AdvancedWidget::readFaults(const QList& records) +{ + m_faultRecords=records; m_faultsLoaded=true; m_faults->setRowCount(records.size()); + for (int i=0;isetText(tr("%1–%2 kHz · %3 · bin %4. Read-only inspection; live relays are unchanged.") + .arg(bin.firstKhz).arg(bin.lastKhz).arg(Kxpa100::bandName(bin.band)).arg(bin.index)); + m_memories->setRowCount(bin.matches.size()); + for (int i=0;i +#include +#include +class QLabel; +class QPushButton; +class QCheckBox; +class QComboBox; +class QTableWidget; +class QProgressBar; +class QFileDialog; +class QSpinBox; +namespace AetherSDR +{ +class Kxpa100Model; +class Kxpa100AdvancedWidget : public QWidget +{ + Q_OBJECT + public: + explicit Kxpa100AdvancedWidget(Kxpa100Model* model, QWidget* parent = nullptr); + ~Kxpa100AdvancedWidget() override; + + private: + friend class Kxpa100AdvancedWidgetTest; + enum class FileAction { Backup, Choose, Faults }; + void refreshAvailability(); + void renderConfiguration(); + void configurationRead(const Kxpa100::Configuration& values); + void stageField(int row, int column); + void clearDeviceData(); + void showStatus(const QString& message); + void openFileDialog(FileAction action); + bool loadBackup(const QString& path); + void readFaults(const QList& records); + void readMemories(const Kxpa100::MemoryBin& bin); + QString schemaReason(bool writing) const; + QString restoreReason() const; + QPointer m_model; + QPointer m_fileDialog; + std::optional m_confirmed, m_staged; + std::optional m_backup; + QString m_savePath; + QList m_faultRecords; + bool m_ownOperation = false, m_applying = false, m_faultsLoaded = false; + QLabel* m_identity; + QLabel* m_availability; + QLabel* m_status; + QLabel* m_preview; + QLabel* m_memorySummary; + QComboBox* m_eraseBand; + QComboBox* m_eraseAntenna; + QTableWidget* m_configuration; + QTableWidget* m_faults; + QTableWidget* m_memories; + QCheckBox* m_restoreConfirm; + QCheckBox* m_eraseConfirm; + QCheckBox* m_resetConfirm; + QSpinBox* m_memoryFrequency; + QProgressBar* m_progress; +}; +} // namespace AetherSDR diff --git a/src/gui/Kxpa100Applet.cpp b/src/gui/Kxpa100Applet.cpp new file mode 100644 index 0000000000..bec737d2f9 --- /dev/null +++ b/src/gui/Kxpa100Applet.cpp @@ -0,0 +1,1013 @@ +#include "Kxpa100Applet.h" +#include "CanonIndicators.h" +#include "CanonWindow.h" +#include "HGauge.h" +#include "core/AppSettings.h" +#include "core/ThemeManager.h" +#include "models/Kxpa100Model.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace AetherSDR +{ +class Kxpa100DebugOutput : public QPlainTextEdit +{ + public: + explicit Kxpa100DebugOutput(QWidget* parent) : QPlainTextEdit(parent) + { + connect(verticalScrollBar(), &QScrollBar::valueChanged, this, [this](int value) { + if (!m_updating) { + m_followLatest = value == verticalScrollBar()->maximum(); + } + }); + } + void appendDiagnostic(const QString& line) + { + const QScopedValueRollback guard(m_updating, true); + appendPlainText(line); + followLatest(); + } + void showLatest(const QString& content) + { + const QScopedValueRollback guard(m_updating, true); + m_followLatest = true; + setPlainText(content); + followLatest(); + } + + protected: + void resizeEvent(QResizeEvent* event) override + { + const QScopedValueRollback guard(m_updating, true); + QPlainTextEdit::resizeEvent(event); + followLatest(); + } + void showEvent(QShowEvent* event) override + { + const QScopedValueRollback guard(m_updating, true); + QPlainTextEdit::showEvent(event); + followLatest(); + } + + private: + void followLatest() + { + if (m_followLatest) { + verticalScrollBar()->setValue(verticalScrollBar()->maximum()); + } + } + bool m_followLatest = true; + bool m_updating = false; +}; +namespace +{ +class HistoryGraphLabel : public QLabel +{ + public: + using QLabel::QLabel; + // The rendered pixmap must not become the layout's preferred size. + QSize sizeHint() const override { return QSize(220, 270); } + QSize minimumSizeHint() const override { return sizeHint(); } +}; +void availability(QWidget* widget, const QString& reason) +{ + widget->setEnabled(reason.isEmpty()); + widget->setToolTip(reason); + widget->setAccessibleDescription(reason); +} +QString flag(std::optional value) +{ + return value ? (*value ? QStringLiteral("ON") : QStringLiteral("OFF")) : QStringLiteral("unknown"); +} +QString reading(std::optional value) +{ + return value ? QString::number(*value, 'f', 2) : QStringLiteral("—"); +} +template +void text(TextWidget* label, const QString& value) +{ + if (label->text() == value) { + return; + } + label->setText(value); + label->setAccessibleName(value); + QAccessibleEvent event(label, QAccessible::NameChanged); + QAccessible::updateAccessibility(&event); +} +template +void pill(TextWidget* label, const QString& value, const QString& detail, const char* tone) +{ + text(label, value); + label->setToolTip(detail); + label->setAccessibleDescription(detail); + const QString state = QString::fromLatin1(tone); + if (label->property("kxTone").toString() != state) { + label->setProperty("kxTone", state); + label->style()->unpolish(label); + label->style()->polish(label); + label->update(); + } +} +QString checkRules() +{ + auto& theme = ThemeManager::instance(); + QString rules = canonIndicatorRules(); + for (bool enabled : {true, false}) { + for (bool checked : {false, true}) { + const bool active = enabled && checked; + const QColor fill = theme.color(active ? "color.accessory.key.meffa.active.background" + : "color.canon.control"); + const QColor edge = theme.color(enabled ? "color.canon.inkSoft" : "color.canon.muted"); + const QColor ink = theme.color(active ? "color.accessory.key.meffa.active.foreground" + : "color.canon.muted"); + const QString key = fill.name(QColor::HexArgb) + edge.name(QColor::HexArgb) + + ink.name(QColor::HexArgb); + const QString path = themedImagePath( + "kx-checks", checked ? "checked" : "unchecked", key, QSize(18, 18), 3, + [fill, edge, ink, checked](QPainter& painter) { + painter.setPen(QPen(edge, 1.5)); + painter.setBrush(fill); + painter.drawRoundedRect(QRectF(1.5, 1.5, 15, 15), 3, 3); + if (checked) { + QPainterPath tick; + tick.moveTo(5, 9); + tick.lineTo(8, 12); + tick.lineTo(13, 6); + painter.setBrush(Qt::NoBrush); + painter.setPen(QPen(ink, 2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); + painter.drawPath(tick); + } + }); + if (!path.isEmpty()) { + rules += QStringLiteral("QCheckBox::indicator:%1:%2 { image: url(\"%3\"); }") + .arg(checked ? "checked" : "unchecked", enabled ? "enabled" : "disabled", path); + } + } + } + return rules; +} +QJsonObject preferences() +{ + return QJsonDocument::fromJson(AppSettings::instance().value("Kxpa100Applet").toString().toUtf8()) + .object(); +} +} // namespace +Kxpa100Applet::Kxpa100Applet(QWidget* parent) : CanonApplet(parent) +{ + setObjectName("Kxpa100Applet"); + setAccessibleName(tr("Elecraft KXPA100 amplifier and tuner")); + setCloseButtonVisible(false); + theme::setContainer(this, QStringLiteral("canon/kxpa100")); + setMinimumWidth(238); + m_clock.start(); + const QJsonObject prefs = preferences(); + auto* body = new QVBoxLayout(bodyWidget()); + body->setContentsMargins(0, 0, 0, 0); + m_scroll = new QScrollArea(bodyWidget()); + m_scroll->setObjectName("kxScroll"); + m_scroll->setAccessibleName(tr("Tuner controls and telemetry")); + m_scroll->setWidgetResizable(true); + m_scroll->setFrameShape(QFrame::NoFrame); + m_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_content = new QWidget; + m_content->setObjectName("kxContent"); + m_scroll->viewport()->setObjectName("kxViewport"); + m_scroll->setWidget(m_content); + body->addWidget(m_scroll); + auto* root = new QVBoxLayout(m_content); + root->setContentsMargins(4, 2, 4, 2); + root->setSpacing(2); + root->setSizeConstraint(QLayout::SetMinAndMaxSize); + const auto button = [this](const QString& label, const QString& name, QLayout* layout) { + auto* b = new QPushButton(label, this); + b->setObjectName(name); + b->setAccessibleName(label); + b->setAutoDefault(false); + b->setFixedHeight(22); + layout->addWidget(b); + return b; + }; + const auto label = [this](const QString& name, QLayout* layout) { + auto* l = new QLabel(this); + l->setObjectName(name); + l->setWordWrap(true); + l->setTextFormat(Qt::PlainText); + l->setAccessibleName(name); + layout->addWidget(l); + return l; + }; + const auto statusPill = [label](const QString& name, QLayout* layout) { + QLabel* cell = label(name, layout); + cell->setProperty("kxPill", true); + cell->setWordWrap(false); + cell->setAlignment(Qt::AlignCenter); + cell->setFixedHeight(18); + return cell; + }; + auto* header = new QHBoxLayout; + header->setSpacing(4); + m_power = button(tr("—"), "kxPower", header); + m_power->setCheckable(true); + m_power->setFixedWidth(44); + m_frequency = statusPill("kxFrequency", header); + header->setStretch(1, 1); + m_mini = button(tr("Mini"), "kxMini", header); + m_mini->setCheckable(true); + m_mini->setFixedWidth(38); + root->addLayout(header); + m_paPane = new QWidget(this); + auto* pa = new QHBoxLayout(m_paPane); + pa->setContentsMargins(0,0,0,0); pa->setSpacing(2); + auto* standby = button(tr("Standby"), "kxStandby", pa); + auto* operate = button(tr("Operate"), "kxOperate", pa); + standby->setCheckable(true); operate->setCheckable(true); + connect(standby,&QPushButton::clicked,this,[this] { if (m_model) { m_model->setOperate(false); } refresh(); }); + connect(operate,&QPushButton::clicked,this,[this] { if (m_model) { m_model->setOperate(true); } refresh(); }); + root->addWidget(m_paPane); + auto* tuning = new QHBoxLayout; + tuning->setSpacing(4); + m_tune = button(tr("Tune"), "kxTune", tuning); + m_tune->setProperty("aetherTxKeying", true); + m_tune->setFixedWidth(60); + m_fault = statusPill("kxFault", tuning); + tuning->setStretch(1, 1); + auto* clear = button(tr("Clear fault"), "kxClearFault", tuning); + clear->setFixedWidth(72); + root->addLayout(tuning); + m_miniState = statusPill("kxMiniState", root); + m_modesPane = new QWidget(this); + auto* modes = new QHBoxLayout(m_modesPane); + modes->setContentsMargins(0, 0, 0, 0); + modes->setSpacing(2); + const QStringList names{tr("Auto"), tr("Manual"), tr("Bypass")}; + for (int i = 0; i < 3; ++i) { + m_modes[i] = button(names[i], "kx" + names[i], modes); + m_modes[i]->setCheckable(true); + connect(m_modes[i], &QPushButton::clicked, this, [this, i] { + if (m_model) { + m_model->setMode(static_cast(i)); + } + refresh(); + }); + } + root->addWidget(m_modesPane); + m_antennasPane = new QWidget(this); + auto* ants = new QHBoxLayout(m_antennasPane); + ants->setContentsMargins(0, 0, 0, 0); + ants->setSpacing(2); + for (int i = 0; i < 2; ++i) { + m_antennas[i] = button(tr("ANT %1").arg(i + 1), QStringLiteral("kxAntenna%1").arg(i + 1), ants); + m_antennas[i]->setCheckable(true); + connect(m_antennas[i], &QPushButton::clicked, this, [this, i] { + if (m_model) { + m_model->setAntenna(i + 1); + } + refresh(); + }); + } + root->addWidget(m_antennasPane); + const auto meter = [this, statusPill](int index, const QString& name, float maximum, QVBoxLayout* layout) { + auto* row = new QHBoxLayout; row->setSpacing(4); + m_meterLabels[index] = statusPill(QStringLiteral("kxMeterLabel%1").arg(index), row); + m_meterLabels[index]->setFixedWidth(72); + QVector ticks; + for (int step = 0; step <= 20; ++step) { + const float value = maximum * step / 20; + ticks.append({value, step % 5 == 0 ? QString::number(value,'g',3) : QString()}); + } + m_meters[index] = new HGauge(0,maximum,maximum*0.9f,"","",ticks,this,maximum*0.75f); + m_meters[index]->setObjectName(name); m_meters[index]->setAccessibleName(name); + m_meters[index]->setFixedHeight(24); row->addWidget(m_meters[index],1); layout->addLayout(row); + }; + meter(0,"kxForwardGauge",100,root); + auto* swrRow = new QHBoxLayout; + m_swr = button(tr("SWR —"), "kxSwr", swrRow); + m_swr->setProperty("kxPill", true); + m_swr->setFixedHeight(18); + m_swr->installEventFilter(this); + connect(m_swr, &QPushButton::pressed, this, &Kxpa100Applet::refresh); + connect(m_swr, &QPushButton::released, this, &Kxpa100Applet::refresh); + m_swr->setFixedWidth(72); + swrRow->setSpacing(4); + QVector swrTicks; + for (int step = 10; step <= 50; ++step) { + const float value = step / 10.0f; + swrTicks.append({value, step % 10 == 0 ? QString::number(value) : QString()}); + } + m_swrGauge = new HGauge(1, 5, 3.0f, "", "", swrTicks, this, 2.0f); + m_swrGauge->setFixedHeight(24); + m_swrGauge->setObjectName("kxSwrGauge"); + swrRow->addWidget(m_swrGauge, 1); + root->addLayout(swrRow); + m_auxPane = new QWidget(this); + auto* auxiliary = new QVBoxLayout(m_auxPane); auxiliary->setContentsMargins(0,0,0,0); auxiliary->setSpacing(2); + meter(1,"kxInputGauge",10,auxiliary); meter(2,"kxReflectedGauge",20,auxiliary); + meter(3,"kxDissipatedGauge",100,auxiliary); meter(4,"kxTemperatureGauge",100,auxiliary); + meter(5,"kxSupplyGauge",16,auxiliary); meter(6,"kxCurrentGauge",25,auxiliary); + root->addWidget(m_auxPane); + m_physicalPane = new QWidget(this); + m_physicalPane->setObjectName("kxPhysicalState"); + auto* states = new QHBoxLayout(m_physicalPane); + states->setContentsMargins(0, 0, 0, 0); + states->setSpacing(3); + const QStringList physicalNames{"kxBypassedStatus", "kxAttenuatorStatus"}; + for (int i = 0; i < 2; ++i) { + m_physical[i] = button(QString(), physicalNames[i], states); + m_physical[i]->setProperty("kxPill", true); + m_physical[i]->setProperty("kxSignalToggle", true); + m_physical[i]->setCheckable(true); + m_physical[i]->setFixedHeight(18); + m_physical[i]->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed); + states->setStretch(i, 1); + connect(m_physical[i], &QPushButton::clicked, this, [this, i] { + if (m_model) { + const auto& status = m_model->status(); + if (i == 0 && status.bypassed) { + m_model->setBypassed(!*status.bypassed); + } else if (i == 1 && status.attenuator) { + m_model->setAttenuator(!*status.attenuator); + + } + } + refresh(); + }); + } + m_readiness = statusPill("kxReadiness", states); + root->addWidget(m_physicalPane); + auto* tools = new QHBoxLayout; + tools->setSpacing(3); + m_manualToggle = button(tr("▸ Manual"), "kxManualToggle", tools); + m_manualToggle->setAccessibleName(tr("Manual controls")); + m_manualToggle->setCheckable(true); + m_manualPane = new QWidget(this); + m_manualPane->setObjectName("kxManualPane"); + auto* manual = new QVBoxLayout(m_manualPane); + manual->setContentsMargins(0, 0, 0, 0); + auto* explanation = label("kxManualHelp", manual); + explanation->setText( + tr("Relay and frequency requests change the signal path. Controls show confirmed readback.")); + for (int kind = 0; kind < 2; ++kind) { + auto* row = new QHBoxLayout; + row->addWidget(new QLabel(kind == 0 ? tr("C (pF)") : tr("L (nH)"), this)); + auto* numeric = new QSpinBox(this); + numeric->setObjectName(kind == 0 ? "kxCapacitance" : "kxInductance"); + numeric->setAccessibleName(kind == 0 ? tr("Capacitance in picofarads") + : tr("Inductance in nanohenries")); + numeric->setRange(0, Kxpa100::relayTotal(kind == 0, 255)); + row->addWidget(numeric, 1); + if (kind == 0) { + m_cap = numeric; + } else { + m_ind = numeric; + } + auto* request = button(tr("Set"), kind == 0 ? "kxSetCapacitance" : "kxSetInductance", row); + manual->addLayout(row); + connect(request, &QPushButton::clicked, this, [this, numeric, kind] { + if (m_model) { + const int mask = Kxpa100::relayMaskForValue(kind == 0, numeric->value()); + if (kind == 0) { + m_model->setCapacitorMask(mask); + } else { + m_model->setInductorMask(mask); + } + } + refresh(); + }); + auto* bits = new QGridLayout; + bits->setHorizontalSpacing(2); + for (int position = 0; position < 8; ++position) { + const int bit = 7 - position; + const int value = Kxpa100::relayTotal(kind == 0, 1 << bit); + auto* check = new QCheckBox(QString::number(value), this); + check->setProperty("kxRelay", true); + check->setObjectName(QStringLiteral("kx%1Bit%2").arg(kind == 0 ? "C" : "L").arg(bit)); + check->setAccessibleName( + (kind == 0 ? tr("Capacitor %1 picofarads, relay bit %2") + : tr("Inductor %1 nanohenries, relay bit %2")).arg(value).arg(bit)); + bits->addWidget(check, position / 4, position % 4); + (kind == 0 ? m_capBits : m_indBits)[bit] = check; + connect(check, &QCheckBox::clicked, this, [this, kind, bit](bool on) { + if (m_model) { + const auto value = + kind == 0 ? m_model->status().capacitorMask : m_model->status().inductorMask; + if (value) { + const int mask = on ? (*value | (1 << bit)) : (*value & ~(1 << bit)); + if (kind == 0) { + m_model->setCapacitorMask(mask); + } else { + m_model->setInductorMask(mask); + } + } + } + refresh(); + }); + } + manual->addLayout(bits); + } + auto* frequencyRow = new QHBoxLayout; + m_frequencyRecall = new QSpinBox(this); + m_frequencyRecall->setObjectName("kxRecallFrequency"); + m_frequencyRecall->setAccessibleName(tr("Memory frequency in kHz")); + m_frequencyRecall->setRange(0, 99999); + m_frequencyRecall->setSpecialValueText(tr("Select frequency")); + m_frequencyRecall->setToolTip(tr("Starts at the confirmed tuner frequency. Enter 1800–54000 kHz to choose a different memory; edits are preserved.")); + m_frequencyRecall->setAccessibleDescription(m_frequencyRecall->toolTip()); + connect(m_frequencyRecall, &QSpinBox::valueChanged, this, [this] { + m_frequencyEdited = true; + refresh(); + }); + m_frequencyRecall->setSuffix(tr(" kHz")); + auto* memoryRow = new QHBoxLayout; + auto* memoryLabel = new QLabel(tr("Match frequency"), this); + memoryLabel->setObjectName("kxRecallFrequencyLabel"); + memoryLabel->setBuddy(m_frequencyRecall); + memoryRow->addWidget(memoryLabel); + memoryRow->addWidget(m_frequencyRecall, 1); + manual->addLayout(memoryRow); + auto* recall = button(tr("Recall"), "kxRecall", frequencyRow); + auto* memorize = button(tr("Memorize"), "kxMemorize", frequencyRow); + manual->addLayout(frequencyRow); + connect(recall, &QPushButton::clicked, this, [this] { + if (m_model) { + m_model->recallFrequency(m_frequencyRecall->value()); + } + }); + connect(memorize, &QPushButton::clicked, this, [this] { + if (m_model) { + m_model->memorize(m_frequencyRecall->value()); + } + }); + m_historyToggle = button(tr("▸ History"), "kxHistory", tools); + m_settings = button(tr("▸ Settings"), "kxSettings", tools); + root->addLayout(tools); + root->addWidget(m_manualPane); + m_historyToggle->setCheckable(true); + m_settings->setCheckable(true); + m_historyPane = new QWidget(this); + auto* history = new QVBoxLayout(m_historyPane); + history->setContentsMargins(0, 0, 0, 0); + m_historyWindow = new QComboBox(this); + m_historyWindow->setAccessibleName(tr("History time window")); + m_historyWindow->addItem(tr("Last minute"),60000); m_historyWindow->addItem(tr("Last 5 minutes"),300000); + m_historyWindow->addItem(tr("Last 15 minutes"),900000); m_historyWindow->setCurrentIndex(1); + history->addWidget(m_historyWindow); + connect(m_historyWindow,&QComboBox::currentIndexChanged,this,&Kxpa100Applet::renderHistory); + m_graph = new HistoryGraphLabel(this); + m_graph->setObjectName("kxHistoryGraph"); + m_graph->setAccessibleName(tr("SWR and forward/reflected power history")); + history->addWidget(m_graph); + m_graph->setMinimumHeight(270); + m_graph->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Expanding); + m_graph->installEventFilter(this); + root->addWidget(m_historyPane, 1); + m_settingsPane = new QWidget(this); + auto* settings = new QVBoxLayout(m_settingsPane); + settings->setContentsMargins(0, 0, 0, 0); + m_association = label("kxRadioAssociation",settings); + m_association->setText(tr("No associated radio. Select a radio, then associate this amplifier.")); + m_associate = button(tr("Use with current radio"),"kxAssociateRadio",settings); + availability(m_associate,tr("Select a radio first.")); + connect(m_associate,&QPushButton::clicked,this,&Kxpa100Applet::associateRadioRequested); + auto* signalPathTitle = label("kxSignalPathTitle", manual); + signalPathTitle->setText(tr("Signal path")); + auto* signalPath = new QVBoxLayout; + signalPath->setSpacing(2); + manual->addLayout(signalPath); + m_side = new QCheckBox(tr("Capacitor on antenna side"), this); + m_side->setObjectName("kxCapacitorSide"); + signalPath->addWidget(m_side); + connect(m_side, &QCheckBox::clicked, this, [this](bool on) { + if (m_model) { + m_model->setCapacitorSide(on ? Kxpa100::CapacitorSide::Antenna + : Kxpa100::CapacitorSide::Transmitter); + } + refresh(); + }); + m_identity = label("kxIdentity", settings); + m_observed = label("kxLastObserved", settings); + m_availability = label("kxAvailability", settings); + m_result = label("kxOperationResult", settings); + auto* help = label("kxHistoryHelp", settings); + help->setText(tr("History shows measured forward/reflected watts and last observed SWR. Gaps mean missing replies. " + "Hold the SWR pill with the mouse to compare bypass SWR; release to return. " + "Last SWR and RF-counter frequency can remain after RF stops. Debug TX means serial writes, not RF.")); + auto* advanced = button(tr("Open Advanced settings"), "kxAdvancedSettings", settings); + connect(advanced, &QPushButton::clicked, this, &Kxpa100Applet::advancedSettingsRequested); + m_debugToggle = button(tr("▸ Debug"), "kxDebug", settings); + m_debugToggle->setCheckable(true); + m_debugPane = new QWidget(this); + auto* debug = new QVBoxLayout(m_debugPane); + debug->setContentsMargins(0, 0, 0, 0); + m_verbose = new QCheckBox(tr("Verbose"), this); + m_verbose->setObjectName("kxVerbose"); + m_verbose->setChecked(prefs.value("Verbose").toBool(false)); + debug->addWidget(m_verbose); + auto* debugActions = new QHBoxLayout; + m_pause = new QCheckBox(tr("Pause display"), this); + debug->addWidget(m_pause); + auto* copy = button(tr("Copy"), "kxDebugCopy", debugActions); + auto* save = button(tr("Save…"), "kxDebugSave", debugActions); + auto* clearDebug = button(tr("Clear"), "kxDebugClear", debugActions); + debug->addLayout(debugActions); + m_debug = new Kxpa100DebugOutput(this); + m_debug->setObjectName("kxDebugOutput"); + m_debug->setAccessibleName(tr("Tuner protocol diagnostics")); + m_debug->setReadOnly(true); + m_debug->setLineWrapMode(QPlainTextEdit::NoWrap); + m_debug->setMaximumBlockCount(1000); + m_debug->setFixedHeight(160); + debug->addWidget(m_debug); + settings->addWidget(m_debugPane); + root->addWidget(m_settingsPane); + root->addStretch(); + connect(m_power, &QPushButton::clicked, this, [this] { + if (m_model && m_model->status().operate) { + m_model->setOperate(!*m_model->status().operate); + } + refresh(); + }); + connect(m_tune, &QPushButton::clicked, this, [this] { + if (m_model) { + if (m_model->status().tuning.value_or(false)) { + m_model->cancelTune(); + } else { + m_model->startTune(); + } + } + refresh(); + }); + connect(clear, &QPushButton::clicked, this, [this] { + if (m_model) { + m_model->clearFault(); + } + }); + for (auto* b : {m_mini, m_manualToggle, m_settings, m_historyToggle, m_debugToggle}) { + connect(b, &QPushButton::toggled, this, [this] { + savePreferences(); + presentation(); + }); + } + connect(m_verbose, &QCheckBox::toggled, this, [this](bool on) { + if (m_model) { + m_model->setVerbose(on); + } + savePreferences(); + }); + connect(m_pause, &QCheckBox::toggled, this, [this](bool paused) { + if (!paused && m_model) { + m_debug->showLatest(m_model->debugText()); + } + }); + connect(copy, &QPushButton::clicked, this, + [this] { QApplication::clipboard()->setText(m_debug->toPlainText()); }); + connect(save, &QPushButton::clicked, this, &Kxpa100Applet::saveDebug); + connect(clearDebug, &QPushButton::clicked, this, [this] { + if (m_model) { + m_model->clearDebug(); + } + }); + m_mini->setChecked(prefs.value("MiniMode").toBool(false)); + m_historyToggle->setChecked(prefs.value("HistoryVisible").toBool(false)); + m_timer.setInterval(500); + connect(&m_timer, &QTimer::timeout, this, &Kxpa100Applet::refresh); + m_timer.start(); + applyCanonSheet(bodyWidget(), [this] { + for (auto* meter : m_meters) { meter->setTickMarkColor(ThemeManager::instance().color(this,"color.canon.muted")); } + m_swrGauge->setTickMarkColor(ThemeManager::instance().color(this, "color.canon.muted")); + return checkRules() + QStringLiteral( + "QWidget#kxContent, QWidget#kxViewport, QScrollArea#kxScroll { background: {{color.canon.ground}}; border: none; }" + "QLabel, QCheckBox { color: {{color.canon.muted}}; font-size: 10px; }" + "QCheckBox[kxRelay=true] { spacing: 2px; }" + "QLabel[kxPill=true] { font-size: 9px; font-weight: bold; background: {{color.canon.nested}}; " + "border: 1px solid {{color.canon.lineHi}}; border-radius: 3px; padding: 0 2px; }" + "QLabel[kxTone=ok] { color: {{color.accent.success}}; }" + "QLabel[kxTone=warn] { color: {{color.accent.warning}}; }" + "QLabel[kxTone=error] { color: {{color.accent.danger}}; }" + "QPushButton { padding: 0 4px; font-size: 10px; font-weight: bold; " + "color: {{color.canon.inkSoft}}; background: {{color.canon.control}}; " + "border-radius: 3px; border: 1px solid {{color.canon.lineHi}}; } " + "QPushButton:hover { background: {{color.canon.nested}}; color: {{color.canon.aqua}}; } " + "QSpinBox { font-size: 10px; min-height: 18px; color: {{color.canon.inkSoft}}; " + "background: {{color.canon.nested}}; border: 1px solid {{color.canon.inkSoft}}; border-radius: 3px; } " + "QSpinBox:focus { border-color: {{color.canon.cyan}}; } " + "QSpinBox:disabled { color: {{color.canon.muted}}; border-color: {{color.canon.muted}}; } " + "QPushButton[kxPill=true] { font-size: 9px; padding: 0 2px; color: {{color.canon.muted}}; " + "background: {{color.canon.ground}}; border-color: {{color.canon.line}}; } " + "QPushButton[kxPill=true]:focus { border-color: {{color.canon.cyan}}; } " + "QPushButton[kxTone=warn] { color: {{color.accent.warning}}; } " + "QPushButton[kxTone=live] { color: {{color.canon.inkSoft}}; } " + "QPlainTextEdit { font-size: 10px; font-family: monospace; color: {{color.canon.inkSoft}}; } " + "QPushButton:checked:enabled { background: " + "{{color.accessory.key.meffa.active.background}}; color: " + "{{color.accessory.key.meffa.active.foreground}}; } QPushButton#kxBypass:checked:enabled { " + "background: {{color.accessory.key.standby.background}}; color: " + "{{color.accessory.key.standby.foreground}}; } QPushButton:disabled { color: " + "{{color.text.secondary}}; background: {{color.background.2}}; } " + "QPushButton:checked:disabled { background: {{color.accessory.key.meffa.active.background}}; " + "color: {{color.text.secondary}}; border-color: {{color.accessory.key.meffa.active.border}}; } " + "QPushButton#kxBypass:checked:disabled { background: {{color.accessory.key.standby.background}}; " + "color: {{color.text.secondary}}; border-color: {{color.accessory.key.standby.foreground}}; }" + "QPushButton[kxSignalToggle=true] { background: {{color.canon.nested}}; border-color: {{color.canon.lineHi}}; }" + "QPushButton[kxSignalToggle=true]:checked { background: {{color.canon.nested}}; " + "color: {{color.accent.warning}}; border-color: {{color.canon.lineHi}}; }" + "QPushButton[kxSignalToggle=true]:hover:enabled { border-color: {{color.canon.cyan}}; }" + "QPushButton[kxSignalToggle=true]:disabled { color: {{color.canon.muted}}; " + "background: {{color.canon.nested}}; border-color: {{color.canon.line}}; }"); + }); + presentation(); + refresh(); +} +Kxpa100Applet::~Kxpa100Applet() +{ + if (m_fileDialog) { + QFileDialog* dialog = m_fileDialog.data(); + m_fileDialog.clear(); + disconnect(dialog, nullptr, this, nullptr); + dialog->reject(); + // Let the native sheet finish closing before QObject destruction. + dialog->setParent(nullptr); + dialog->deleteLater(); + } +} +void Kxpa100Applet::setModel(Kxpa100Model* model) +{ + if (m_model) { + disconnect(m_model, nullptr, this, nullptr); + } + m_model = model; + if (!m_pause->isChecked()) { + m_debug->showLatest(model ? model->debugText() : QString()); + } + m_frequencyEdited = false; + { const QSignalBlocker blocker(m_frequencyRecall); m_frequencyRecall->setValue(0); } + if (model) { + model->setVerbose(m_verbose->isChecked()); + connect(model, &Kxpa100Model::statusUpdated, this, &Kxpa100Applet::refresh); + connect(model, &Kxpa100Model::operationFinished, this, + [this](const QString& message) { text(m_result, message); }); + connect(model, &Kxpa100Model::operationFailed, this, + [this](const QString& message) { text(m_result, message); }); + connect(model, &Kxpa100Model::connectionFailed, this, + [this](const QString& message) { text(m_result, message); }); + connect(model, &Kxpa100Model::availabilityChanged, this, &Kxpa100Applet::refresh); + connect(model, &Kxpa100Model::configurationRead, this, &Kxpa100Applet::refresh); + connect(model, &Kxpa100Model::diagnostic, this, [this](const QString& line) { + if (!m_pause->isChecked()) { + m_debug->appendDiagnostic(line); + } + }); + connect(model, &Kxpa100Model::debugChanged, this, [this] { + if (m_model && !m_pause->isChecked() && m_model->debugText().isEmpty()) { + m_debug->showLatest(QString()); + } + }); + connect(model, &Kxpa100Model::telemetryReceived, this, + [this](const QString& command, const Kxpa100::Status& state) { + const int i = command == "^SW" ? 0 : command == "^PF" ? 1 : command == "^PV" ? 2 : -1; + if (i < 0) { + return; + } + std::optional value; + if (i == 0) { + value = state.swr; + } else { + const auto count = i == 1 ? state.forwardWatts : state.reflectedWatts; + if (count) { + value = *count; + } + } + m_history[i].push_back({m_clock.elapsed(), value}); + while (m_history[i].size() > 4096) { + m_history[i].pop_front(); + } + }); + connect(model, &Kxpa100Model::disconnected, this, [this] { + for (auto& series : m_history) { + series.push_back({m_clock.elapsed(), std::nullopt}); + } + refresh(); + }); + } + refresh(); +} +void Kxpa100Applet::setRadioContext(const QString& radioName, int txFrequencyKhz, bool associated) +{ + m_txFrequencyKhz = txFrequencyKhz; m_associated = associated; + text(m_association,radioName.isEmpty() ? tr("No radio selected.") : associated + ? tr("Associated with %1. VU/PWR metering and frequency context apply only to this radio.").arg(radioName) + : tr("Selected radio: %1. Associate it for VU/PWR metering and frequency context.").arg(radioName)); + availability(m_associate,radioName.isEmpty() ? tr("Select a radio first.") : QString()); + m_associate->setText(associated ? tr("Remove radio association") : tr("Use with current radio")); + refresh(); +} +void Kxpa100Applet::setAssociationResult(bool saved) +{ + text(m_result, saved ? tr("Radio association updated.") + : tr("Radio association was not saved. Connect an identified radio and KXPA100, and check that settings are writable. The previous association is unchanged.")); +} +void Kxpa100Applet::refresh() +{ + const Kxpa100::Status s = m_model ? m_model->status() : Kxpa100::Status{}; + const QString reason = m_model ? m_model->unavailableReason(true) : tr("Connect a KXPA100 first."); + const QString tunerReason = m_model ? m_model->unavailableReason(true,true) : reason; + const bool responding = m_model && m_model->isResponding(); + const bool tuning = s.tuning == true; + const auto fresh = [this](const char* key) { const auto age=m_model ? m_model->fieldAgeMs(key) : -1; return age>=0 && age<=5000; }; + const bool rf = responding && s.forwardWatts.value_or(0)>0 && fresh("^PF"); + const int frequency = m_associated && m_txFrequencyKhz>0 ? m_txFrequencyKhz : s.frequencyKhz.value_or(0); + text(m_frequency,tr("%1 · %2").arg(s.band ? Kxpa100::bandName(*s.band) : tr("Band —"), + frequency>0 ? tr("%1 kHz").arg(frequency) : tr("Freq —"))); + m_frequency->setToolTip(m_associated ? tr("Associated radio TX frequency; band is device readback.") : tr("Last RF-counter frequency; not an indication of current transmission.")); + m_frequency->setAccessibleDescription(m_frequency->toolTip()); + text(m_power,s.operate ? (*s.operate ? tr("OPR") : tr("STBY")) : tr("PA ?")); + m_power->setChecked(s.operate==true); availability(m_power,reason); + m_power->setAccessibleName(tr("PA operate/standby, confirmed %1").arg(m_power->text())); + for (const bool operate : {false,true}) { + auto* control=findChild(operate ? "kxOperate" : "kxStandby"); + control->setChecked(s.operate && *s.operate==operate); availability(control,reason); + } + text(m_tune,tuning ? tr("Cancel") : tr("Tune")); + availability(m_tune,tuning && responding && !m_model->operationBusy() ? QString() : tunerReason); + for (int i=0;i<3;++i) { + m_modes[i]->setChecked(s.mode && static_cast(*s.mode)==i); availability(m_modes[i],tunerReason); + } + for (int i=0;i<2;++i) { + m_antennas[i]->setChecked(s.antenna==i+1); + QString blocked=tunerReason; + if (blocked.isEmpty() && m_model->configuration() && s.band && !((*m_model->configuration())[*s.band] & (1<("kxClearFault"),reason); + const QString fault=s.faultCode ? Kxpa100::currentFaultName(*s.faultCode) : tr("Fault unknown"); + pill(m_fault,s.faultCode ? (*s.faultCode=='N' ? tr("NO FAULT") : tr("FAULT %1").arg(QChar(*s.faultCode))) : tr("FAULT ?"), + fault,s.faultCode && *s.faultCode=='N' ? "ok" : "error"); + const std::array,7> values{s.forwardWatts,s.inputWatts,s.reflectedWatts,s.dissipatedWatts,s.temperatureC,s.supplyVolts,s.drainAmps}; + const QStringList names{tr("OUT"),tr("DRV"),tr("REFL"),tr("DISS"),tr("TEMP"),tr("SUPPLY"),tr("DRAIN")}; + const QStringList units{tr("W"),tr("W"),tr("W"),tr("W"),tr("°C"),tr("V"),tr("A")}; + for (int i=0;i<7;++i) { + const QString value=values[i] ? QString::number(*values[i],'f',1) : QStringLiteral("—"); + const QString label=tr("%1 %2%3").arg(names[i],value,units[i]); + pill(m_meterLabels[i],label,tr("KXPA100 %1: %2 %3. %4").arg(names[i],value,units[i],responding ? QString() : tr("Readback is stale.")),"muted"); + m_meters[i]->setValue(values[i].value_or(0)); m_meters[i]->setAccessibleName(label); + if (i == 1) { + const QString source = tr("DRV: input power measured by the KXPA100, in watts. Scale 0–10 W."); + const QString description = source + " " + m_meterLabels[i]->accessibleDescription(); + m_meterLabels[i]->setToolTip(description); + m_meterLabels[i]->setAccessibleDescription(description); + m_meters[i]->setToolTip(description); + m_meters[i]->setAccessibleDescription(description); + } + } + const bool preview=m_swr->isDown(); const auto swr=preview ? s.bypassSwr : s.swr; + const QString swrText=tr("%1 %2").arg(preview ? tr("BYP") : tr("SWR"),reading(swr)); + const QString swrHelp=tr("Last observed %1 SWR. Hold with the mouse to preview bypass SWR; release to return. " + "This changes the display only. SWR may remain after RF stops. Scale 1–5; the pill preserves values above 5.").arg(preview ? tr("bypass") : tr("adjusted")); + pill(m_swr,swrText,swrHelp,preview ? "warn" : rf ? "live" : "muted"); + if (preview!=m_showingBypass) { m_swrGauge->setValueImmediate(swr.value_or(1)); } + else { m_swrGauge->setValue(swr.value_or(1)); } + m_showingBypass=preview; m_swrGauge->setAccessibleName(swrText); m_swrGauge->setAccessibleDescription(swrHelp); + const std::array,2> physical{s.bypassed,s.attenuator ? std::optional(*s.attenuator!=0) : std::nullopt}; + const QStringList descriptions{ + tr("ATU matching-network bypass relay. Auto/Manual mode stays unchanged; a recall or tune may engage it again. Click to toggle."), + tr("Input attenuator. Firmware may engage it for protection. The rear-panel switch overrides software control. Click to toggle.")}; + for (int i=0;i<2;++i) { + QString blocked=i==0 ? tunerReason : reason; + if (blocked.isEmpty() && !physical[i]) { blocked=tr("Wait for confirmed state."); } + if (i==1 && s.attenuator==2) { blocked=tr("The rear-panel switch holds the attenuator on."); } + m_physical[i]->setChecked(physical[i]==true); availability(m_physical[i],blocked); + pill(m_physical[i],tr("%1 %2").arg(i==0 ? tr("ATU BYP") : tr("ATT"),flag(physical[i])), + descriptions[i]+QStringLiteral("\n")+blocked,physical[i]==true ? "warn" : "muted"); + } + const QString readiness=!m_model || !m_model->isDetected() ? tr("OFFLINE") : !responding ? tr("STALE") : tuning ? tr("TUNING") : rf ? tr("RF") : tr("IDLE"); + pill(m_readiness,readiness,tr("RF is based on amplifier power readback, not radio PTT."),tuning || !responding ? "warn" : "ok"); + const QStringList modes{tr("AUTO"),tr("MANUAL"),tr("BYPASS")}; + const QString mode=s.tunerPresent==false ? tr("NO ATU") : s.mode ? modes[int(*s.mode)] : tr("ATU ?"); + pill(m_miniState,tr("%1 · ANT %2 · %3").arg(mode,s.antenna ? QString::number(*s.antenna) : QStringLiteral("?"), + s.faultCode && *s.faultCode!='N' ? fault : readiness),tr("Tuner mode and antenna are readbacks. Physical ATU bypass: %1.").arg(flag(s.bypassed)), + s.mode==Kxpa100::Mode::Bypass || s.bypassed==true || !responding ? "warn" : "ok"); + text(m_identity,m_model ? tr("%1\n%2 · Firmware %3 · S/N %4").arg(m_model->description(),s.identity,s.firmware,s.serial) : tr("No amplifier connected.")); + text(m_availability,tr("%1\nPower / SWR reply age: %2 / %3 ms").arg(reason.isEmpty() ? tr("Ready for an operator request.") : reason) + .arg(m_model ? m_model->fieldAgeMs("^PF") : -1).arg(m_model ? m_model->fieldAgeMs("^SW") : -1)); + text(m_observed,tr("Last RF frequency: %1\nAdjusted / bypass SWR: %2 / %3\nOptional tuner: %4") + .arg(s.frequencyKhz.value_or(0)>0 ? tr("%1 kHz").arg(*s.frequencyKhz) : tr("Unavailable"),reading(s.swr),reading(s.bypassSwr),flag(s.tunerPresent))); + for (auto* control : m_manualPane->findChildren()) { + if (qobject_cast(control) || qobject_cast(control) || qobject_cast(control)) { availability(control,tunerReason); } + } + if (!m_frequencyEdited) { + const QSignalBlocker blocker(m_frequencyRecall); + m_frequencyRecall->setValue(Kxpa100::isSupportedFrequency(frequency) ? frequency : 0); + } + const QString frequencyReason=!tunerReason.isEmpty() ? tunerReason : Kxpa100::isSupportedFrequency(m_frequencyRecall->value()) ? QString() : tr("Select 1800–54000 kHz before recall or save."); + availability(findChild("kxRecall"),frequencyReason); availability(findChild("kxMemorize"),frequencyReason); + for (int kind=0;kind<2;++kind) { + const auto mask=kind==0 ? s.capacitorMask : s.inductorMask; auto* numeric=kind==0 ? m_cap : m_ind; + const QSignalBlocker blocker(numeric); + if (mask && mask!=m_lastRelayMasks[kind]) { numeric->setValue(Kxpa100::relayTotal(kind==0,*mask)); } + m_lastRelayMasks[kind]=mask; + for (int bit=0;bit<8;++bit) { + auto* check=(kind==0 ? m_capBits : m_indBits)[bit]; const QSignalBlocker blocked(check); + check->setChecked(mask && (*mask & (1<setChecked(s.side==Kxpa100::CapacitorSide::Antenna); } + if (m_historyPane->isVisible()) { renderHistory(); } +} +void Kxpa100Applet::presentation() +{ + const QStringList titles{tr("Manual"), tr("History"), tr("Settings"), tr("Debug")}; + const std::array drawers{m_manualToggle, m_historyToggle, m_settings, m_debugToggle}; + for (int i = 0; i < 4; ++i) { + drawers[i]->setText(QStringLiteral("%1 %2").arg(drawers[i]->isChecked() ? "▾" : "▸", titles[i])); + } + const bool mini = m_mini->isChecked(); + m_mini->setText(mini ? tr("Full") : tr("Mini")); + m_mini->setAccessibleName(mini ? tr("Show full tuner controls") : tr("Show mini tuner controls")); + m_paPane->setVisible(!mini); + m_power->setVisible(mini); + m_auxPane->setVisible(!mini); + m_modesPane->setVisible(!mini); + m_antennasPane->setVisible(!mini); + m_physicalPane->setVisible(!mini); + findChild("kxClearFault")->setVisible(!mini); + m_miniState->setVisible(mini); + m_manualToggle->setVisible(!mini); + m_manualPane->setVisible(!mini && m_manualToggle->isChecked()); + m_settings->setVisible(!mini); + m_historyToggle->setVisible(!mini); + m_settingsPane->setVisible(!mini && m_settings->isChecked()); + m_historyPane->setVisible(!mini && m_historyToggle->isChecked()); + m_debugPane->setVisible(m_debugToggle->isChecked()); + updateGeometry(); +} +void Kxpa100Applet::savePreferences() +{ + QJsonObject p; + p["MiniMode"] = m_mini->isChecked(); + p["HistoryVisible"] = m_historyToggle->isChecked(); + p["Verbose"] = m_verbose->isChecked(); + AppSettings::instance().setValue("Kxpa100Applet", + QString::fromUtf8(QJsonDocument(p).toJson(QJsonDocument::Compact))); + AppSettings::instance().save(); +} +void Kxpa100Applet::setFloating(bool floating) +{ + m_floating = floating; + updateGeometry(); +} +QSize Kxpa100Applet::sizeHint() const +{ + return QSize(300, std::min(700, m_content->sizeHint().height() + 2)); +} +QSize Kxpa100Applet::minimumSizeHint() const +{ + return QSize(238, m_mini && m_mini->isChecked() ? 124 : 376); +} +bool Kxpa100Applet::eventFilter(QObject* watched, QEvent* event) +{ + if (watched == m_swr && (event->type() == QEvent::FocusOut || event->type() == QEvent::Hide || + event->type() == QEvent::WindowDeactivate) && m_swr->isDown()) { + m_swr->setDown(false); + refresh(); + } + if (watched == m_graph && event->type() == QEvent::Resize) { + renderHistory(); + } + return CanonApplet::eventFilter(watched, event); +} +void Kxpa100Applet::renderHistory() +{ + const QSize size = m_graph->size(); + if (size.isEmpty()) { + return; + } + const qreal dpr = m_graph->devicePixelRatioF(); + QPixmap pixmap(size * dpr); + pixmap.setDevicePixelRatio(dpr); + auto& theme = ThemeManager::instance(); + pixmap.fill(theme.color(this, "color.canon.ground")); + QPainter p(&pixmap); + QFont font = m_graph->font(); + font.setPixelSize(9); + p.setFont(font); + const QStringList names{tr("SWR · last observed"), tr("Forward · W"), tr("Reflected · W")}; + const qint64 now = m_clock.elapsed(); + const int window = m_historyWindow->currentData().toInt(); + QStringList description{tr("Telemetry history in watts. " + "SWR is last observed, not proof of RF. Gaps mean missing replies.")}; + for (int i = 0; i < 3; ++i) { + const qreal top = i * size.height() / 3.0; + const QRectF area(4, top + 20, size.width() - 38, size.height() / 3.0 - 37); + const double minimum = i == 0 ? 1 : 0; + double maximum = i == 0 ? 5 : 1; + std::optional latest; + for (const Point& point : m_history[i]) { + if (point.time >= now - window) { + latest = point.value; + if (i != 0 && point.value) { + maximum = std::max(maximum, *point.value); + } + } + } + const QColor trace = theme.color(this, i == 0 ? "color.canon.cyan" : "color.canon.aqua"); + const QString current = latest ? QString::number(*latest, 'f', i == 0 ? 2 : 0) : QStringLiteral("—"); + p.setPen(QPen(trace, 2)); + p.drawLine(QPointF(4, top + 10), QPointF(13, top + 10)); + p.drawText(QRectF(18, top, size.width() - 65, 18), Qt::AlignVCenter, names[i]); + p.drawText(QRectF(size.width() - 46, top, 42, 18), Qt::AlignVCenter | Qt::AlignRight, current); + p.setPen(theme.color(this, "color.canon.lineHi")); + p.drawRect(area); + p.drawLine(QPointF(area.center().x(), area.top()), QPointF(area.center().x(), area.bottom())); + p.drawLine(QPointF(area.left(), area.center().y()), QPointF(area.right(), area.center().y())); + p.setPen(theme.color(this, "color.canon.muted")); + p.drawText(QRectF(area.right() + 3, area.top() - 5, 30, 12), Qt::AlignLeft, QString::number(maximum)); + p.drawText(QRectF(area.right() + 3, area.bottom() - 8, 30, 12), Qt::AlignLeft, QString::number(minimum)); + p.drawText(QRectF(area.left(), area.bottom() + 2, area.width(), 12), Qt::AlignLeft, tr("−%1 min").arg(window/60000)); + p.drawText(QRectF(area.left(), area.bottom() + 2, area.width(), 12), Qt::AlignRight, tr("now")); + QPainterPath path; + bool segment = false; + qint64 prior = -10000; + p.setPen(QPen(trace, 1.5)); + for (const Point& point : m_history[i]) { + if (point.time < now - window) { + continue; + } + if (!point.value) { + segment = false; + continue; + } + const QPointF pos(area.left() + area.width() * (point.time - (now - window)) / double(window), + area.bottom() - area.height() * + std::clamp((*point.value - minimum) / (maximum - minimum), 0.0, 1.0)); + if (!segment || point.time - prior > 5000) { + path.moveTo(pos); + p.drawPoint(pos); + } else { + path.lineTo(pos); + } + segment = true; + prior = point.time; + } + p.drawPath(path); + description.append(tr("%1: latest %2; scale %3 to %4.") + .arg(names[i], current, QString::number(minimum), QString::number(maximum))); + } + m_graph->setAccessibleDescription(description.join(QLatin1Char(' '))); + m_graph->setPixmap(pixmap); +} +void Kxpa100Applet::saveDebug() +{ + if (m_fileDialog) { + m_fileDialog->raise(); + return; + } + auto* dialog = new QFileDialog(this); + m_fileDialog = dialog; + dialog->setObjectName("kxDebugFileDialog"); + dialog->setWindowTitle(tr("Save KXPA100 debug log")); + dialog->setNameFilter(tr("Text files (*.txt)")); + dialog->setAcceptMode(QFileDialog::AcceptSave); + dialog->selectFile("kxpa100-debug.txt"); + connect(dialog, &QDialog::finished, this, [this, dialog](int result) { + const QStringList paths = dialog->selectedFiles(); + m_fileDialog.clear(); + dialog->deleteLater(); + if (result != QDialog::Accepted || paths.isEmpty()) { + return; + } + QSaveFile file(paths.first()); + const QByteArray data = m_debug->toPlainText().toUtf8(); + if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size() || !file.commit()) { + text(m_availability, tr("Debug save failed: %1").arg(file.errorString())); + } + }); + dialog->open(); +} +} // namespace AetherSDR diff --git a/src/gui/Kxpa100Applet.h b/src/gui/Kxpa100Applet.h new file mode 100644 index 0000000000..2a4207e375 --- /dev/null +++ b/src/gui/Kxpa100Applet.h @@ -0,0 +1,107 @@ +#pragma once +#include "CanonSurface.h" +#include "models/Kxpa100Types.h" +#include +#include +#include +#include +#include +class QLabel; +class QPushButton; +class QCheckBox; +class QScrollArea; +class QSpinBox; +class QFileDialog; +class QComboBox; +namespace AetherSDR +{ +class Kxpa100Model; +class Kxpa100DebugOutput; +class HGauge; +class Kxpa100Applet : public CanonApplet +{ + Q_OBJECT + public: + explicit Kxpa100Applet(QWidget* parent = nullptr); + ~Kxpa100Applet() override; + void setModel(Kxpa100Model* model); + void setFloating(bool floating); + void setRadioContext(const QString& radioName, int txFrequencyKhz, bool associated); + void setAssociationResult(bool saved); + QSize sizeHint() const override; + QSize minimumSizeHint() const override; + signals: + void advancedSettingsRequested(); + void associateRadioRequested(); + + protected: + bool eventFilter(QObject* watched, QEvent* event) override; + + private: + void refresh(); + void presentation(); + void savePreferences(); + void renderHistory(); + void saveDebug(); + QPointer m_model; + QPointer m_fileDialog; + QScrollArea* m_scroll; + QWidget* m_content; + QWidget* m_modesPane; + QWidget* m_paPane; + QWidget* m_auxPane; + QComboBox* m_historyWindow; + QLabel* m_association; + QPushButton* m_associate; + std::array m_meterLabels{}; + std::array m_meters{}; + QWidget* m_antennasPane; + QWidget* m_physicalPane; + QWidget* m_manualPane; + QWidget* m_settingsPane; + QWidget* m_historyPane; + QWidget* m_debugPane; + QPushButton* m_power; + QPushButton* m_tune; + QPushButton* m_mini = nullptr; + QPushButton* m_manualToggle; + QPushButton* m_settings; + QPushButton* m_historyToggle; + QPushButton* m_debugToggle; + std::array m_modes{}; + std::array m_antennas{}; + std::array m_capBits{}, m_indBits{}; + QCheckBox* m_side; + QCheckBox* m_verbose; + QCheckBox* m_pause; + QSpinBox* m_cap; + QSpinBox* m_ind; + QSpinBox* m_frequencyRecall; + QLabel* m_frequency; + QPushButton* m_swr; + std::array m_physical{}; + QLabel* m_readiness; + QLabel* m_miniState; + QLabel* m_fault; + QLabel* m_identity; + QLabel* m_observed; + QLabel* m_availability; + QLabel* m_result; + QLabel* m_graph; + HGauge* m_swrGauge; + Kxpa100DebugOutput* m_debug; + struct Point { + qint64 time; + std::optional value; + }; + std::array, 3> m_history; + QElapsedTimer m_clock; + QTimer m_timer; + std::array, 2> m_lastRelayMasks{}; + bool m_frequencyEdited = false; + bool m_floating = false; + bool m_showingBypass = false; + int m_txFrequencyKhz = 0; + bool m_associated = false; +}; +} // namespace AetherSDR diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index f3f677daf6..e753e55d15 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -3583,7 +3583,7 @@ RadioSetupDialog* MainWindow::openRadioSetupPage(const QString& page) &m_tgxlConn, &m_pgxlConn, &m_antennaGenius, m_kiwiSdrManager, &m_acomConn, &m_speConn, &m_vkampConn, &m_lpMeterConn, &m_kpa1500Conn, &m_kpa500Conn, - this, &m_kat500Model); + this, &m_kat500Model, &m_kxpa100Model); dlg->setAttribute(Qt::WA_DeleteOnClose); m_radioSetupDialog = dlg; } diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 246c03341c..3d20b83ea9 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -97,6 +97,7 @@ #include "core/VkampConnection.h" #include "core/Kpa500Connection.h" #include "models/Kat500Model.h" +#include "models/Kxpa100Model.h" #include "core/Kpa1500Connection.h" #include "core/DxccColorProvider.h" @@ -1339,6 +1340,8 @@ private slots: SpeConnection m_speConn; // SPE Expert amplifier, serial or ser2net VkampConnection m_vkampConn; // VK3AMP amplifier, TCP control/status + UDP telemetry Kat500Model m_kat500Model; + Kxpa100Model m_kxpa100Model; + class Kxpa100RadioAssociation* m_kxpa100Association = nullptr; QPointer m_kat500PreTuneDialog; Kpa500Connection m_kpa500Conn; // Elecraft KPA500 amplifier, serial or raw TCP Kpa1500Connection m_kpa1500Conn; // Elecraft KPA1500 amplifier, TCP control/status on port 1500 (#4097) diff --git a/src/gui/MainWindow_Wiring.cpp b/src/gui/MainWindow_Wiring.cpp index e1cdef686e..1d7a3d8469 100644 --- a/src/gui/MainWindow_Wiring.cpp +++ b/src/gui/MainWindow_Wiring.cpp @@ -44,6 +44,8 @@ #include "LpMeterApplet.h" #include "Kpa500Applet.h" #include "Kat500Applet.h" +#include "Kxpa100Applet.h" +#include "models/Kxpa100RadioAssociation.h" #include "AtuPreTuneDialog.h" #include "models/Kat500TxController.h" #include "HealthApplet.h" @@ -1765,6 +1767,12 @@ bool MainWindow::reattachSliceVisualsToPanadapter(SliceModel* s) void MainWindow::refreshTunerPortFrequency() { + if (m_appletPanel && m_appletPanel->kxpa100Applet()) { + m_appletPanel->kxpa100Applet()->setRadioContext( + m_radioModel.isConnected() ? m_radioModel.nickname() : QString(), + Kxpa100RadioAssociation::effectiveTxFrequencyKhz(&m_radioModel), + (m_kxpa100Association && m_kxpa100Association->associated())); + } // Match the active plan's segments to the KPA's fixed band selectors. // These broad search windows classify bands; the plan owns their edges. SliceModel* kpaTx = m_radioModel.isConnected() && m_radioModel.backendCanTransmit() @@ -6451,7 +6459,9 @@ SharedTxMeterSelection MainWindow::sharedTxMeterSelection() && m_radioModel.amplifier().operate(); const bool kpa500Active = m_kpa500Conn.hasLiveTxMeters(); return selectSharedTxMeters(maxWatts, amplifierActive, kpa500Active, - m_kat500Model.ampKeyBlockConfirmed()); + m_kat500Model.ampKeyBlockConfirmed(), + m_kxpa100Association && m_kxpa100Association->associated() + && m_kxpa100Model.amplifierMeterEligible()); } // One owner for amplifier TX meters on the shared readouts (S-Meter, @@ -6543,7 +6553,8 @@ void MainWindow::wireMeters() // (~100 W) and amp (~1500 W) values race into the same widget. (#2927) connect(&m_radioModel.meterModel(), &MeterModel::txMetersChanged, this, [this](float fwd, float swr, bool swrValid) { - if (sharedTxMeterSelection().source == SharedTxMeterSource::Amplifier) { + if (sharedTxMeterSelection().source == SharedTxMeterSource::Amplifier + || sharedTxMeterSelection().source == SharedTxMeterSource::Kxpa100) { return; } // Absent SWR is forwarded as 1.0: RadioSwrValidityFilter downstream @@ -6565,7 +6576,8 @@ void MainWindow::wireMeters() &MeterModel::directionalPowerMetersChanged, this, [this](float fwd, float reflected, float swr, bool swrValid, bool reflectedPowerMeasured) { - if (sharedTxMeterSelection().source == SharedTxMeterSource::Amplifier) { + if (sharedTxMeterSelection().source == SharedTxMeterSource::Amplifier + || sharedTxMeterSelection().source == SharedTxMeterSource::Kxpa100) { return; } // The cross-needle already clamps sub-1.0 values to 1.0 (its rest @@ -6660,6 +6672,7 @@ void MainWindow::wireMeters() m_kpa500Conn.setAutoReconnect( ar && PeripheralSettings::autoConnect(QStringLiteral("kpa500"))); m_kat500Model.setAutoReconnect(ar && PeripheralSettings::autoConnect(QStringLiteral("kat500"))); + m_kxpa100Model.setAutoReconnect(ar && PeripheralSettings::autoConnect(QStringLiteral("kxpa100"))); } // Wire TgxlConnection to TunerModel @@ -7646,6 +7659,39 @@ void MainWindow::wireMeters() #endif } + m_kxpa100Association = new Kxpa100RadioAssociation(&m_radioModel, &m_kxpa100Model, this); + connect(m_kxpa100Association, &Kxpa100RadioAssociation::changed, + this, &MainWindow::refreshTunerPortFrequency); + auto* kx = m_appletPanel->kxpa100Applet(); + kx->setModel(&m_kxpa100Model); + connect(&m_kxpa100Model, &Kxpa100Model::detectedChanged, m_appletPanel, &AppletPanel::setKxpa100Visible); + connect(kx, &Kxpa100Applet::advancedSettingsRequested, this, [this] { + if (auto* dialog = openRadioSetupPage(QStringLiteral("Peripherals"))) { dialog->revealKxpa100AdvancedSettings(); } + }); + connect(kx, &Kxpa100Applet::associateRadioRequested, this, [this, kx] { + const bool saved = m_kxpa100Association && m_kxpa100Association->associated() + ? Kxpa100RadioAssociation::dissociate(&m_radioModel, &m_kxpa100Model) + : Kxpa100RadioAssociation::associate(&m_radioModel, &m_kxpa100Model); + refreshTunerPortFrequency(); + kx->setAssociationResult(saved); + }); + connect(&m_kxpa100Model, &Kxpa100Model::associationChanged, this, &MainWindow::refreshTunerPortFrequency); + connect(&m_kxpa100Model, &Kxpa100Model::availabilityChanged, this, &MainWindow::refreshTunerPortFrequency); + connect(&m_radioModel, &RadioModel::infoChanged, this, &MainWindow::refreshTunerPortFrequency); + if (PeripheralSettings::autoConnect(QStringLiteral("kxpa100"))) { + const QString mode = PeripheralSettings::deviceString("Kxpa100", "ConnectionMode", "Serial"); + if (mode == "Network") { + const QString host = PeripheralSettings::deviceString("Kxpa100", "ManualIp").trimmed(); + const int port = PeripheralSettings::deviceInt("Kxpa100", "ManualPort", 2000); + if (!host.isEmpty() && port > 0 && port <= 65535) { m_kxpa100Model.connectToHost(host, static_cast(port)); } + } else { +#ifdef HAVE_SERIALPORT + const QString port = PeripheralSettings::deviceString("Kxpa100", "SerialPort"); + if (!port.isEmpty()) { m_kxpa100Model.connectSerial(port, PeripheralSettings::deviceInt("Kxpa100", "BaudRate", 38400)); } +#endif + } + } + // Data and scale use the same confirmed selection. auto updatePowerScale = [this]() { const SharedTxMeterSelection selection = sharedTxMeterSelection(); @@ -7671,6 +7717,21 @@ void MainWindow::wireMeters() m_appletPanel->healthApplet()->setPowerScale(radioMaxWatts, integratedAmp); m_appletPanel->setMeterPowerScale(selection.maxWatts, selection.highPowerAmplifier); }; + connect(&m_kxpa100Model, &Kxpa100Model::statusUpdated, this, updatePowerScale); + connect(&m_kxpa100Model, &Kxpa100Model::availabilityChanged, this, updatePowerScale); + connect(m_kxpa100Association, &Kxpa100RadioAssociation::changed, this, updatePowerScale); + connect(&m_kxpa100Model, &Kxpa100Model::statusUpdated, this, [this](const Kxpa100::Status& status) { + if (sharedTxMeterSelection().source != SharedTxMeterSource::Kxpa100) { return; } + const float watts = status.forwardWatts.value_or(0); + const float swr = watts > 0 ? status.swr.value_or(1) : 1; + m_appletPanel->setMeterTxValues(watts, swr); + const bool reflectedFresh = m_kxpa100Model.fieldAgeMs("^PV") >= 0 && m_kxpa100Model.fieldAgeMs("^PV") <= 5000; + m_appletPanel->setCrossNeedleDirectionalValues(watts, status.reflectedWatts.value_or(0), swr, reflectedFresh); +#ifdef HAVE_HIDAPI + m_tmate2TxWatts = watts; + if (m_radioModel.transmitModel().isTransmitting()) { updateTMate2Display(); updateTMate2Indicators(); } +#endif + }); connect(&m_kat500Model, &Kat500Model::statusUpdated, this, updatePowerScale); connect(&m_kat500Model, &Kat500Model::availabilityChanged, this, updatePowerScale); updatePowerScale(); diff --git a/src/gui/RadioSetupDialog.cpp b/src/gui/RadioSetupDialog.cpp index 1220d14a83..4e786af7a5 100644 --- a/src/gui/RadioSetupDialog.cpp +++ b/src/gui/RadioSetupDialog.cpp @@ -1272,12 +1272,12 @@ RadioSetupDialog::RadioSetupDialog(RadioModel* model, AudioEngine* audio, Kpa1500Connection* kpa1500, Kpa500Connection* kpa500, QWidget* parent, - Kat500Model* kat500) + Kat500Model* kat500, Kxpa100Model* kxpa100) : CanonWindow(QStringLiteral("Radio Setup"), parent, Kind::Workspace), m_model(model), m_audio(audio), m_tgxl(tgxl), m_pgxl(pgxl), m_ag(ag), m_kiwiSdrManager(kiwiSdrManager), m_acom(acom), m_spe(spe), m_vkamp(vkamp), m_lpMeter(lpMeter), - m_kpa1500(kpa1500), m_kpa500(kpa500), m_kat500(kat500) + m_kpa1500(kpa1500), m_kpa500(kpa500), m_kat500(kat500), m_kxpa100(kxpa100) { theme::setContainer(this, QStringLiteral("canon/radioSetup")); // Size and position persist under the key it used as a PersistentDialog, diff --git a/src/gui/RadioSetupDialog.h b/src/gui/RadioSetupDialog.h index 976f14fa95..0eb02d03ed 100644 --- a/src/gui/RadioSetupDialog.h +++ b/src/gui/RadioSetupDialog.h @@ -43,6 +43,7 @@ class VkampConnection; class Kpa1500Connection; class Kpa500Connection; class Kat500Model; +class Kxpa100Model; class LpMeterConnection; struct PeripheralDeviceUi; @@ -64,7 +65,8 @@ class RadioSetupDialog : public CanonWindow { Kpa1500Connection* kpa1500 = nullptr, Kpa500Connection* kpa500 = nullptr, QWidget* parent = nullptr, - Kat500Model* kat500 = nullptr); + Kat500Model* kat500 = nullptr, + Kxpa100Model* kxpa100 = nullptr); void selectTab(const QString& tabName); void done(int result) override; // Like selectTab("Serial & Controllers"), but also scrolls the page so @@ -74,6 +76,7 @@ class RadioSetupDialog : public CanonWindow { void revealFlexControlSettings(); void revealKpa500AdvancedSettings(); void revealKat500AdvancedSettings(); + void revealKxpa100AdvancedSettings(); void refreshFlexControlButtonActions(); void setFlexControlConnectionStatus(bool connected, const QString& port = {}); // Result of the automation-bridge start the Network-tab toggle kicked off @@ -217,6 +220,9 @@ class RadioSetupDialog : public CanonWindow { void buildKat500Device(PeripheralDeviceUi& ui, QWidget* stackParent, const std::function& refresh, const std::shared_ptr>>& pageReseeds); + void buildKxpa100Device(PeripheralDeviceUi& ui, QWidget* stackParent, + const std::function& refresh, + const std::shared_ptr>>& pageReseeds); void buildKpa1500Device(PeripheralDeviceUi& ui, QWidget* stackParent, const std::function& refresh); PeripheralDeviceUi* peripheralDevice(const QString& id) const; @@ -261,6 +267,7 @@ class RadioSetupDialog : public CanonWindow { Kpa1500Connection* m_kpa1500{nullptr}; Kpa500Connection* m_kpa500{nullptr}; Kat500Model* m_kat500{nullptr}; + Kxpa100Model* m_kxpa100{nullptr}; QTreeWidget* m_navigation{nullptr}; QStackedWidget* m_pages{nullptr}; QLabel* m_pageTitle{nullptr}; diff --git a/src/gui/RadioSetupDialog_Peripherals.cpp b/src/gui/RadioSetupDialog_Peripherals.cpp index 16cf97ca46..a623646049 100644 --- a/src/gui/RadioSetupDialog_Peripherals.cpp +++ b/src/gui/RadioSetupDialog_Peripherals.cpp @@ -14,6 +14,9 @@ #include "RadioSetupDialog.h" #include "RadioSetupDialogCommon.h" #include "Kpa500AdvancedWidget.h" +#include "Kxpa100AdvancedWidget.h" +#include "models/Kxpa100Model.h" +#include "models/Kxpa100RadioAssociation.h" #include "Kat500AdvancedWidget.h" #include "models/Kat500Model.h" #include "PeripheralAuthConnectFlow.h" @@ -87,6 +90,7 @@ struct PeripheralDeviceUi { SerialOrNetwork, // ACOM, SPE Expert, LP-100A: serial port or ser2net host Vkamp, // host + port, plus the amplifier model Kpa1500, // host + port + Kxpa100, // KXPA100 amplifier with optional tuner Kat500, // KAT500 serial or TCP tuner Kpa500, // Serial with selectable baud, or raw TCP }; @@ -505,7 +509,7 @@ void finishDetailPage(DetailPage& detail, PeripheralDeviceUi& ui, detail.layout->addWidget(detail.group, 0, Qt::AlignTop); if (ui.kind == PeripheralDeviceUi::Kind::AuthNetwork - || (ui.kind == PeripheralDeviceUi::Kind::Kpa500 || ui.kind == PeripheralDeviceUi::Kind::Kat500)) { + || (ui.kind == PeripheralDeviceUi::Kind::Kpa500 || ui.kind == PeripheralDeviceUi::Kind::Kat500 || ui.kind == PeripheralDeviceUi::Kind::Kxpa100)) { // TGXL and PGXL can also be reached through the radio, so for them the // toggle chooses the direct link over the radio's relay. const bool relayed = ui.id == QLatin1String("tgxl") || ui.id == QLatin1String("pgxl"); @@ -524,7 +528,7 @@ void finishDetailPage(DetailPage& detail, PeripheralDeviceUi& ui, : RadioSetupDialog::tr( "Connect to %1 on startup, when discovery reports it, and after a drop. " "Turn off to connect only when you click Connect.").arg(ui.label)); - if ((ui.kind == PeripheralDeviceUi::Kind::Kpa500 || ui.kind == PeripheralDeviceUi::Kind::Kat500)) { + if ((ui.kind == PeripheralDeviceUi::Kind::Kpa500 || ui.kind == PeripheralDeviceUi::Kind::Kat500 || ui.kind == PeripheralDeviceUi::Kind::Kxpa100)) { autoConnect->setAccessibleDescription(RadioSetupDialog::tr( "Connect to %1 on startup. Turn off to connect only when you click Connect.") .arg(ui.label)); @@ -1217,7 +1221,7 @@ void RadioSetupDialog::buildSerialNetworkDevice(PeripheralDeviceUi& ui, QWidget* setTone(ui.deviceInfoLabel, Tone::Value); addFormRow(detail, tr("Device Information"), ui.deviceInfoLabel); } - if ((ui.kind == PeripheralDeviceUi::Kind::Kpa500 || ui.kind == PeripheralDeviceUi::Kind::Kat500)) { + if ((ui.kind == PeripheralDeviceUi::Kind::Kpa500 || ui.kind == PeripheralDeviceUi::Kind::Kat500 || ui.kind == PeripheralDeviceUi::Kind::Kxpa100)) { auto* help = new QLabel(ui.networkTooltip); help->setWordWrap(true); setTone(help, Tone::Hint); @@ -1486,6 +1490,88 @@ void RadioSetupDialog::revealKat500AdvancedSettings() }); } +void RadioSetupDialog::buildKxpa100Device(PeripheralDeviceUi& ui, QWidget* stackParent, + const std::function& refresh, + const std::shared_ptr>>& pageReseeds) +{ +#ifdef HAVE_SERIALPORT + ui.baudCombo = new QComboBox; + ui.baudCombo->setObjectName(QStringLiteral("peripheralBaud_%1").arg(ui.id)); + ui.baudCombo->setAccessibleName(tr("KXPA100 baud rate")); + ui.baudCombo->setAccessibleDescription(tr("Local serial speed; must match the tuner. 8 data bits, no parity, 1 stop bit.")); + for (int baud : {4800, 9600, 19200, 38400}) { + ui.baudCombo->addItem(QString::number(baud), baud); + } + const int savedBaud = PeripheralSettings::deviceInt(ui.settingsGroup, "BaudRate", 38400); + const int baudIndex = ui.baudCombo->findData(savedBaud); + ui.baudCombo->setCurrentIndex(baudIndex >= 0 ? baudIndex : 0); + ui.connectSerial = [this, u = &ui](const QString& port) { + const int baud = u->baudCombo->currentData().toInt(); + PeripheralSettings::setDeviceInt(u->settingsGroup, "BaudRate", baud); + m_kxpa100->connectSerial(port, baud); + }; +#endif + ui.networkPlaceholder = tr("ser2net host, raw TCP mode"); + ui.networkDescription = tr("IP address or host name of the raw TCP serial proxy"); + ui.networkTooltip = tr("Use a raw TCP ser2net endpoint, with 8N1 and the amplifier's baud rate configured on the server. " + "Telnet/RFC 2217 and Elecraft Remote Server are not supported by this connection."); + buildSerialNetworkDevice(ui, stackParent, refresh, pageReseeds); + auto* advancedToggle = new QPushButton(tr("Advanced"), ui.page); + advancedToggle->setObjectName(QStringLiteral("peripheralAdvanced_kxpa100")); + advancedToggle->setAccessibleName(tr("KXPA100 advanced maintenance")); + advancedToggle->setAccessibleDescription(tr("Antenna configuration, backup and restore, stored fault history and tuner memories.")); + advancedToggle->setCheckable(true); + auto* advanced = new Kxpa100AdvancedWidget(m_kxpa100, ui.page); + advanced->hide(); + auto* layout = qobject_cast(ui.page->layout()); + auto* associate = new QPushButton(tr("Use with current radio"), ui.page); + associate->setObjectName("kxAssociateRadio"); + associate->setAccessibleName(tr("Associate KXPA100 with the connected radio")); + associate->setToolTip(tr("Bind this KXPA100 serial to this radio for VU/PWR metering and applet frequency context.")); + layout->insertWidget(layout->count() - 1, associate); + connect(associate, &QPushButton::clicked, this, [this, associate] { + const bool saved = Kxpa100RadioAssociation::associate(m_model, m_kxpa100); + associate->setToolTip(saved ? tr("Association saved for the current radio.") : tr("Connect an identified radio and KXPA100 before associating them. Settings must be writable.")); + }); + layout->insertWidget(layout->count() - 1, advancedToggle, 0, Qt::AlignLeft); + layout->insertWidget(layout->count() - 1, advanced); + connect(advancedToggle, &QPushButton::toggled, advanced, &QWidget::setVisible); + connect(ui.autoConnectCheck, &QCheckBox::toggled, this, [this](bool on) { + m_kxpa100->setAutoReconnect(on && PeripheralSettings::autoReconnect()); + }); +} + +void RadioSetupDialog::revealKxpa100AdvancedSettings() +{ + selectTab(QStringLiteral("Peripherals")); + if (QListWidget* devices = findChild(QStringLiteral("peripheralDeviceList"))) { + for (int row = 0; row < devices->count(); ++row) { + if (devices->item(row)->data(Qt::UserRole).toString() == QStringLiteral("kxpa100")) { + devices->setCurrentRow(row); + break; + } + } + } + QPointer toggle = findChild(QStringLiteral("peripheralAdvanced_kxpa100")); + if (!toggle) { + return; + } + toggle->setChecked(true); + toggle->setFocus(Qt::OtherFocusReason); + QTimer::singleShot(0, this, [toggle] { + if (!toggle) { + return; + } + for (QWidget* parent = toggle->parentWidget(); parent; parent = parent->parentWidget()) { + if (auto* area = qobject_cast(parent); area && area->widget()) { + const int y = toggle->mapTo(area->widget(), QPoint(0, 0)).y(); + area->verticalScrollBar()->setValue(qMax(0, y - 8)); + return; + } + } + }); +} + void RadioSetupDialog::buildKpa1500Device(PeripheralDeviceUi& ui, QWidget* stackParent, const std::function& refresh) { @@ -1985,6 +2071,34 @@ QWidget* RadioSetupDialog::buildPeripheralsTab() ? tr("tuner not answering") : QString(); buildKat500Device(*ui, detailStack, refresh, serialReseeds); } + if (m_kxpa100) { + auto ui = addDevice(QStringLiteral("kxpa100"), tr("Elecraft KXPA100"), + QStringLiteral("KXPA100 amplifier / tuner"), + PeripheralDeviceUi::Kind::Kxpa100, 2000); + ui->settingsGroup = QStringLiteral("Kxpa100"); + ui->isConnected = [this]() { return m_kxpa100->isConnected(); }; + ui->isConnecting = [this]() { return m_kxpa100->isConnecting(); }; + ui->isNetworkConnection = [this]() { return m_kxpa100->isNetworkConnection(); }; + ui->connectNetwork = [this](const QString& host, quint16 port) { m_kxpa100->connectToHost(host, port); }; + ui->deviceInfo = [this]() { + const Kxpa100::Status& status = m_kxpa100->status(); + return tr("%1\nFirmware: %2 · Serial: %3\n%4") + .arg(m_kxpa100->isConnected() ? m_kxpa100->description() : tr("Disconnected"), + status.firmware.isEmpty() ? tr("Not reported") : status.firmware, + status.serial.isEmpty() ? tr("Not reported") : status.serial, + m_kxpa100->unavailableReason(false)); + }; + ui->disconnectNow = [this]() { m_kxpa100->disconnectDevice(); }; + ui->description = [this]() { return m_kxpa100->description(); }; + ui->afterRemoval = [this]() { + m_kxpa100->disconnectDevice(); + m_kxpa100->setAutoReconnect(PeripheralSettings::autoReconnect() + && PeripheralSettings::autoConnect("kxpa100")); + }; + ui->state.note = m_kxpa100->isConnected() && !m_kxpa100->isResponding() + ? tr("amplifier not answering") : QString(); + buildKxpa100Device(*ui, detailStack, refresh, serialReseeds); + } if (m_lpMeter) { auto ui = serialDevice(QStringLiteral("lp100a"), tr("LP-100A Meter"), QStringLiteral("LP-100A meter"), QStringLiteral("Lp100a"), 2000, QStringLiteral("115200 8N1"), @@ -2034,6 +2148,9 @@ QWidget* RadioSetupDialog::buildPeripheralsTab() if (m_kat500) { m_kat500->setAutoReconnect(on && PeripheralSettings::autoConnect("kat500")); } + if (m_kxpa100) { + m_kxpa100->setAutoReconnect(on && PeripheralSettings::autoConnect("kxpa100")); + } // VK3AMP takes this setting only at startup (#4919). }); @@ -2271,7 +2388,7 @@ QWidget* RadioSetupDialog::buildPeripheralsTab() } } else { PeripheralSettings::clearDeviceConnection(ui.settingsGroup); - if ((ui.kind == PeripheralDeviceUi::Kind::Kpa500 || ui.kind == PeripheralDeviceUi::Kind::Kat500)) { + if ((ui.kind == PeripheralDeviceUi::Kind::Kpa500 || ui.kind == PeripheralDeviceUi::Kind::Kat500 || ui.kind == PeripheralDeviceUi::Kind::Kxpa100)) { PeripheralSettings::clearDeviceField(ui.settingsGroup, "BaudRate"); PeripheralSettings::resetAutoConnect(ui.id); const QSignalBlocker blocked(ui.autoConnectCheck); @@ -2579,7 +2696,7 @@ QWidget* RadioSetupDialog::buildPeripheralsTab() const bool connected = ui.isConnected(); ui.state.source = sourceOf(ui); const bool connecting = effectivelyConnecting(ui); - const bool kpaConnecting = (ui.kind == PeripheralDeviceUi::Kind::Kpa500 || ui.kind == PeripheralDeviceUi::Kind::Kat500) && connecting; + const bool kpaConnecting = (ui.kind == PeripheralDeviceUi::Kind::Kpa500 || ui.kind == PeripheralDeviceUi::Kind::Kat500 || ui.kind == PeripheralDeviceUi::Kind::Kxpa100) && connecting; ui.connectButton->setText(connected ? tr("Disconnect") : kpaConnecting ? tr("Cancel") : tr("Connect")); applyStatusLabel(ui); @@ -2807,6 +2924,11 @@ QWidget* RadioSetupDialog::buildPeripheralsTab() connect(m_kat500, &Kat500Model::statusUpdated, this, refresh); connect(m_kat500, &Kat500Model::availabilityChanged, this, refresh); } + if (m_kxpa100) { + wireLinkDevice(QStringLiteral("kxpa100"), m_kxpa100); + connect(m_kxpa100, &Kxpa100Model::statusUpdated, this, refresh); + connect(m_kxpa100, &Kxpa100Model::availabilityChanged, this, refresh); + } if (m_kpa500) { wireLinkDevice(QStringLiteral("kpa500"), m_kpa500); connect(m_kpa500, &Kpa500Connection::statusUpdated, this, refresh); diff --git a/src/gui/SharedTxMeterSelection.h b/src/gui/SharedTxMeterSelection.h index f03a120971..afe0ed805a 100644 --- a/src/gui/SharedTxMeterSelection.h +++ b/src/gui/SharedTxMeterSelection.h @@ -2,7 +2,7 @@ namespace AetherSDR { -enum class SharedTxMeterSource { Radio, Amplifier, RadioKeyBlocked }; +enum class SharedTxMeterSource { Radio, Amplifier, RadioKeyBlocked, Kxpa100 }; struct SharedTxMeterSelection { SharedTxMeterSource source{SharedTxMeterSource::Radio}; @@ -13,7 +13,8 @@ struct SharedTxMeterSelection { // The KAT500 key-line override applies to the KPA500 path. A separately // selected radio-integrated amplifier retains its existing meter priority. inline SharedTxMeterSelection selectSharedTxMeters(int radioMaxWatts, bool amplifierActive, - bool kpa500Active, bool keyBlockConfirmed) + bool kpa500Active, bool keyBlockConfirmed, + bool kxpa100Active = false) { if (amplifierActive) { return {SharedTxMeterSource::Amplifier, radioMaxWatts, true}; @@ -24,6 +25,9 @@ inline SharedTxMeterSelection selectSharedTxMeters(int radioMaxWatts, bool ampli } return {SharedTxMeterSource::Amplifier, 500, false}; } + if (kxpa100Active) { + return {SharedTxMeterSource::Kxpa100, 100, false}; + } return {SharedTxMeterSource::Radio, radioMaxWatts, false}; } diff --git a/src/models/Kxpa100Model.cpp b/src/models/Kxpa100Model.cpp new file mode 100644 index 0000000000..6d92fd3de1 --- /dev/null +++ b/src/models/Kxpa100Model.cpp @@ -0,0 +1,468 @@ +#include "Kxpa100Model.h" +#include "core/Kxpa100Protocol.h" +#include "core/Kxpa100Maintenance.h" +#include +#include +#include +#ifdef HAVE_SERIALPORT +#include +#endif +#include +#include + +Q_LOGGING_CATEGORY(lcKxpa100, "aether.kxpa100") +namespace AetherSDR { +using namespace Kxpa100; +namespace { +const QList kPolls{"^OP;","^MD;","^AN;","^BY;","^AT;","^CR;","^LR;","^SI;","^SB;", + "^PV;","^PD;","^PC;","^SV;","^TM;","^F;","^BN;","^AD;","^TU;","^I;","^RV;","^SN;"}; +QByteArray hexMask(int mask) { return QByteArray::number(mask,16).rightJustified(2,'0').toUpper(); } +} +Kxpa100Model::Kxpa100Model(QObject* parent) : QObject(parent), m_parser(std::make_unique()), + m_socket(std::make_unique()) +{ + m_socket->setReadBufferSize(4096); + m_clock.start(); m_now = [this] { return m_clock.elapsed(); }; + m_timer.setInterval(50); m_reconnectTimer.setSingleShot(true); m_reconnectTimer.setInterval(5000); + connect(&m_timer,&QTimer::timeout,this,&Kxpa100Model::tick); + connect(&m_reconnectTimer,&QTimer::timeout,this,&Kxpa100Model::reconnect); + m_parser->setCallback([this](const QByteArray& frame) { receiveFrame(frame); }); + connect(m_socket.get(),&QTcpSocket::connected,this,&Kxpa100Model::transportOpened); + connect(m_socket.get(),&QTcpSocket::readyRead,this,[this] { receiveBytes(m_socket->readAll()); }); + connect(m_socket.get(),&QTcpSocket::disconnected,this,[this] { transportClosed(); }); + connect(m_socket.get(),&QTcpSocket::errorOccurred,this,[this](QAbstractSocket::SocketError) { + transportClosed(m_socket->errorString()); + }); +#ifdef HAVE_SERIALPORT + m_serial = std::make_unique(); + connect(m_serial.get(),&QSerialPort::readyRead,this,[this] { receiveBytes(m_serial->readAll()); }); + connect(m_serial.get(),&QSerialPort::errorOccurred,this,[this](QSerialPort::SerialPortError error) { + if (error == QSerialPort::ResourceError || error == QSerialPort::DeviceNotFoundError || error == QSerialPort::PermissionError) { + transportClosed(m_serial->errorString()); + } + }); +#endif +} +Kxpa100Model::~Kxpa100Model() { m_autoReconnect = false; disconnectDevice(); } +void Kxpa100Model::connectToHost(const QString& host, quint16 port) +{ + if (m_connected || m_connecting) { return; } + m_reconnectTimer.stop(); + const QString trimmedHost = host.trimmed(); + bool invalidHost = trimmedHost.isEmpty() || trimmedHost.size() > 253 || trimmedHost.contains('/'); + for (QChar character : trimmedHost) { + invalidHost = invalidHost || character.isSpace() || !character.isPrint(); + } + if (invalidHost || port == 0) { + emit connectionFailed(tr("Enter a TCP host and a port from 1 to 65535.")); + return; + } + m_network = true; m_host = trimmedHost; m_port = port; m_connecting = true; + m_deliberateDisconnect = false; m_connectStarted = m_now(); m_timer.start(); + m_socket->connectToHost(m_host,m_port); emit availabilityChanged(); +} +void Kxpa100Model::connectSerial(const QString& portName, int baudRate) +{ +#ifdef HAVE_SERIALPORT + if (m_connected || m_connecting || portName.trimmed().isEmpty() || + (baudRate != 4800 && baudRate != 9600 && baudRate != 19200 && baudRate != 38400)) { return; } + m_network = false; m_portName = portName; m_baud = baudRate; m_deliberateDisconnect = false; + m_serial->setPortName(portName); m_serial->setBaudRate(baudRate); + m_serial->setDataBits(QSerialPort::Data8); m_serial->setParity(QSerialPort::NoParity); + m_serial->setStopBits(QSerialPort::OneStop); m_serial->setFlowControl(QSerialPort::NoFlowControl); + if (!m_serial->open(QIODevice::ReadWrite)) { transportClosed(m_serial->errorString()); return; } + transportOpened(); +#else + Q_UNUSED(portName); Q_UNUSED(baudRate); + emit connectionFailed(QStringLiteral("This build does not include serial-port support.")); +#endif +} +void Kxpa100Model::disconnectDevice() +{ + m_deliberateDisconnect = true; m_reconnectTimer.stop(); + transportClosed(); +} +void Kxpa100Model::reconnect() +{ + if (!m_autoReconnect || m_deliberateDisconnect || m_connected || m_connecting) { return; } + if (m_network) { connectToHost(m_host,m_port); } else { connectSerial(m_portName,m_baud); } +} +void Kxpa100Model::transportOpened() +{ + ++m_generation; m_connected = true; m_connecting = false; m_detected = false; + m_deferredTuneCancel = false; m_tuneEnded = false; m_status = {}; m_configuration.reset(); + m_received.clear(); m_queue.clear(); m_active.reset(); m_parser->reset(); + m_operation.clear(); m_settleGetter.clear(); m_settleVerify = {}; m_tunePermit = {}; + m_lastReply = -10000; m_nextPoll = 0; m_pollIndex = 0; m_timer.start(); queueIdentity(); + report(QStringLiteral("Transport connected: %1").arg(description())); emit connected(); emit availabilityChanged(); +} +void Kxpa100Model::transportClosed(const QString& reason) +{ + if (m_closing) { return; } + m_closing = true; + const bool wasOpen = m_connected || m_connecting; + m_connected = false; m_connecting = false; m_timer.stop(); ++m_generation; + m_socket->abort(); +#ifdef HAVE_SERIALPORT + if (m_serial->isOpen()) { m_serial->close(); } +#endif + if (operationBusy()) { fail(reason.isEmpty() ? QStringLiteral("Transport closed; pending operations discarded.") : reason); } + m_queue.clear(); m_active.reset(); m_parser->reset(); m_received.clear(); m_configuration.reset(); + m_deferredTuneCancel = false; + if (m_detected) { m_detected = false; emit detectedChanged(false); } + if (wasOpen) { report(QStringLiteral("Transport closed.")); emit disconnected(); } + if (!reason.isEmpty()) { report(reason); emit connectionFailed(reason); } + m_closing = false; emit availabilityChanged(); + if (m_autoReconnect && !m_deliberateDisconnect) { m_reconnectTimer.start(); } +} +bool Kxpa100Model::isResponding() const { return m_connected && m_now() - m_lastReply <= 5000; } +QString Kxpa100Model::description() const +{ + return m_network ? QStringLiteral("NETWORK %1:%2 (bridge controls baud)").arg(m_host).arg(m_port) + : QStringLiteral("DIRECT %1 at %2 baud").arg(m_portName).arg(m_baud); +} +qint64 Kxpa100Model::fieldAgeMs(const QString& command) const +{ + const auto it = m_received.constFind(command); + return it == m_received.cend() ? -1 : std::max(0, m_now() - *it); +} +bool Kxpa100Model::fresh(const QString& key, int maxAge) const +{ + const qint64 age = fieldAgeMs(key); + return age >= 0 && age <= maxAge; +} +bool Kxpa100Model::idle() const +{ + return m_status.tuning == false && m_status.inputWatts == 0 && m_status.forwardWatts == 0 && + fresh(QStringLiteral("^TP")) && fresh(QStringLiteral("^PI")) && fresh(QStringLiteral("^PF")); +} +QString Kxpa100Model::unavailableReason(bool writing, bool tuner) const +{ + if (!m_connected) { return QStringLiteral("Connect the KXPA100 first."); } + if (!m_detected) { return QStringLiteral("Waiting for KXPA100 identity, serial and firmware."); } + if (!isResponding()) { return QStringLiteral("KXPA100 readback is stale."); } + if (operationBusy() || m_deferredTuneCancel) { return QStringLiteral("Another KXPA100 operation is in progress."); } + if (tuner && m_status.tunerPresent != true) { return QStringLiteral("The optional KXAT100 tuner is absent or not yet identified."); } + if (writing && !idle()) { return QStringLiteral("Fresh idle tuner and zero input/output power are required."); } + return {}; +} +bool Kxpa100Model::tunerEligible() const +{ + return m_connected && m_detected && isResponding() && m_status.tunerPresent == true && + fresh(QStringLiteral("^TU")) && fresh(QStringLiteral("^MD")) && fresh(QStringLiteral("^AN")) && + m_status.mode && m_status.mode != Mode::Bypass && m_status.antenna.has_value(); +} +bool Kxpa100Model::amplifierMeterEligible() const +{ + return m_connected && m_detected && isResponding() && m_status.operate == true && m_status.tuning == false && + fresh(QStringLiteral("^OP")) && fresh(QStringLiteral("^TP")) && fresh(QStringLiteral("^PF")); +} + +void Kxpa100Model::report(const QString& text, bool polling) +{ + if (polling && !m_verbose) { return; } + qCDebug(lcKxpa100).noquote() << text; + const QString line = QDateTime::currentDateTime().toString(QStringLiteral("HH:mm:ss.zzz ")) + text; + m_debug.append(line); while (m_debug.size() > 1000) { m_debug.removeFirst(); } + emit diagnostic(line); emit debugChanged(); +} +bool Kxpa100Model::write(const QByteArray& bytes) +{ + if (!m_connected || bytes.isEmpty()) { return false; } + bool success; + if (m_write) { success = m_write(bytes); } + else { + QIODevice* device = m_socket.get(); +#ifdef HAVE_SERIALPORT + if (!m_network) { device = m_serial.get(); } +#endif + success = device->bytesToWrite() < 1024 && device->write(bytes) == bytes.size(); + } + if (!success) { transportClosed(QStringLiteral("KXPA100 transport write failed.")); return false; } + report(QStringLiteral("TX %1").arg(QString::fromLatin1(bytes)), !m_operationWriting); return true; +} +void Kxpa100Model::enqueue(Request request) +{ + if (!m_connected || (request.mutation && !operationBusy())) { return; } + if (request.wire.isEmpty() || m_queue.size() >= 64) { fail(QStringLiteral("Invalid or excessive KXPA100 request queue.")); return; } + m_queue.enqueue(std::move(request)); +} +void Kxpa100Model::query(const QByteArray& wire, std::function complete) +{ + enqueue({wire, wire.chopped(1), [wire](const QByteArray& frame) { return matchesStatusQuery(wire,frame); }, + [complete = std::move(complete)](const QByteArray&) { if (complete) { complete(); } }}); +} +void Kxpa100Model::queueIdentity() +{ + for (const QByteArray& command : {QByteArray("^I;"),QByteArray("^RV;"),QByteArray("^SN;"),QByteArray("^TU;"), + QByteArray("^TP;"),QByteArray("^PI;"),QByteArray("^PF;"),QByteArray("^MD;"),QByteArray("^AN;")}) { query(command); } +} +void Kxpa100Model::tick() +{ + const qint64 now = m_now(); + if (m_connecting && now - m_connectStarted > 10000) { transportClosed(QStringLiteral("Connection timed out.")); return; } + if (!m_connected) { return; } + emit availabilityChanged(); + if (!m_connected) { return; } + if (m_active) { + if (now - m_sentAt >= m_active->timeoutMs) { + // This protocol has no transaction IDs. Closing the transport is the + // only way to keep a timed-out response from satisfying a later GET. + transportClosed(QStringLiteral("KXPA100 reply timed out; completion is uncertain. No write was retried.")); + } + return; + } + if (!m_settleGetter.isEmpty()) { + if (now >= m_settleDeadline) { fail(QStringLiteral("Requested value was not confirmed; no write was retried.")); return; } + if (m_queue.isEmpty() && now >= m_settleNext) { pollSettle(); } + } + if (m_deferredTuneCancel && !operationBusy()) { + m_deferredTuneCancel = false; cancelTune(); + } + if (m_queue.isEmpty()) { + if (operationBusy() || now < m_nextPoll) { return; } + const QList> priority{{"^TP;",750},{"^PI;",750},{"^PF;",750},{"^SW;",1500}, + {"^MD;",2500},{"^AN;",2500},{"^OP;",2500},{"^TU;",2500},{"^FL;",2500}}; + for (const auto& [command, age] : priority) { + if (!fresh(QString::fromLatin1(command.chopped(1)),age)) { query(command); break; } + } + if (m_queue.isEmpty()) { query(kPolls[m_pollIndex++ % kPolls.size()]); } + m_nextPoll = now + 50; + } + if (m_queue.isEmpty()) { return; } + m_active = m_queue.dequeue(); m_sentAt = now; + if (m_active->mutation && ((m_active->requiresIdle && !idle()) || + m_status.serial != m_operationSerial || m_status.firmware != m_operationFirmware)) { + fail(QStringLiteral("Write stopped: device identity or idle/RF readback is unsafe or stale.")); return; + } + if (m_active->mutation && m_tunePermit && !m_tunePermit()) { fail(QStringLiteral("Operation intent expired; request discarded.")); return; } + const QByteArray wire = m_active->wire; write(wire); +} +void Kxpa100Model::receiveBytes(const QByteArray& bytes) { m_parser->feed(bytes); } +void Kxpa100Model::receiveFrame(const QByteArray& frame) +{ + if (!m_connected) { return; } + report(QStringLiteral("RX %1;").arg(QString::fromLatin1(frame)), true); + if (frame == "^FE") { + m_tuneEnded = true; emit telemetryReceived(QStringLiteral("^FE"), m_status); return; + } + Status parsed = m_status; + const bool telemetry = applyStatus(frame,parsed); + if (telemetry && ((!m_status.identity.isEmpty() && parsed.identity != m_status.identity) || + (!m_status.serial.isEmpty() && parsed.serial != m_status.serial) || + (!m_status.firmware.isEmpty() && parsed.firmware != m_status.firmware))) { + transportClosed(QStringLiteral("KXPA100 identity changed; pending operations discarded.")); return; + } + if (!m_active || !m_active->matches(frame)) { return; } + if (m_now() - m_sentAt >= m_active->timeoutMs) { + transportClosed(QStringLiteral("KXPA100 response arrived after its deadline; completion discarded.")); return; + } + Request request = std::move(*m_active); m_active.reset(); + const quint64 generation = m_generation; + m_lastReply = m_now(); + if (telemetry && !request.telemetryKey.isEmpty()) { + m_status = parsed; m_received[QString::fromLatin1(request.telemetryKey)] = m_now(); + if (!m_detected && m_status.identity == QStringLiteral("KXPA100") && !m_status.serial.isEmpty() && !m_status.firmware.isEmpty()) { + m_detected = true; emit detectedChanged(true); + } + emit statusUpdated(m_status); emit telemetryReceived(QString::fromLatin1(request.telemetryKey),m_status); + } + if (generation == m_generation && m_connected && request.complete) { request.complete(frame); } +} +bool Kxpa100Model::begin(const QString& name, bool writing, bool tuner) +{ + const QString reason = unavailableReason(writing,tuner); + if (!reason.isEmpty()) { emit operationFailed(reason); return false; } + ++m_generation; m_queue.clear(); m_operation = name; m_operationWriting = writing; + m_operationSerial = m_status.serial; m_operationFirmware = m_status.firmware; + m_progress = 0; m_total = 0; + const quint64 generation = m_generation; + report(name); emit operationBusyChanged(true); emit availabilityChanged(); + return m_connected && generation == m_generation && operationBusy(); +} +void Kxpa100Model::finish(const QString& message) +{ + m_operation.clear(); m_operationWriting = false; m_tunePermit = {}; + m_settleGetter.clear(); m_settleVerify = {}; m_edits.clear(); m_regionComplete = {}; + m_nextPoll = m_now() + 50; + report(message); emit operationBusyChanged(false); emit operationFinished(message); emit availabilityChanged(); +} +void Kxpa100Model::fail(const QString& reason) +{ + ++m_generation; + const bool busy = operationBusy(); + m_queue.clear(); m_active.reset(); m_operation.clear(); m_operationWriting = false; + m_tunePermit = {}; m_settleGetter.clear(); m_settleVerify = {}; m_edits.clear(); + m_regionComplete = {}; m_regionData.clear(); m_pendingBackup = {}; + m_nextPoll = m_now() + 50; + qCWarning(lcKxpa100).noquote() << reason; + report(reason); if (busy) { emit operationBusyChanged(false); } emit operationFailed(reason); emit availabilityChanged(); +} +void Kxpa100Model::cancelOperation() +{ + if (!operationBusy()) { return; } + if (m_active) { transportClosed(QStringLiteral("Operation canceled during a request. Already applied writes remain; reconnect to inspect.")); } + else { fail(QStringLiteral("Operation canceled. Already confirmed writes remain applied.")); } +} +void Kxpa100Model::preflight(std::function ready, bool requireIdle) +{ + query("^I;"); query("^RV;"); query("^SN;"); query("^TP;"); query("^PI;"); + query("^PF;",[this,ready=std::move(ready),requireIdle] { + if (!operationBusy()) { return; } + if (m_status.serial != m_operationSerial || m_status.firmware != m_operationFirmware || (requireIdle && !idle())) { + fail(QStringLiteral("Operation stopped: device identity changed or RF/tuning is active.")); return; + } + ready(); + }); +} +bool Kxpa100Model::requestValue(const QByteArray& setter, const QByteArray& getter, + std::function verify, const QString& name, bool tuner) +{ + if (setter.isEmpty() || !begin(name,true,tuner)) { return false; } + preflight([this,setter,getter,verify=std::move(verify)] { settle(setter,getter,verify); }); return true; +} +void Kxpa100Model::settle(const QByteArray& setter, const QByteArray& getter, + std::function verify, bool requireIdle) +{ + m_settleGetter = getter; m_settleVerify = std::move(verify); m_settleDeadline = m_now() + 2500; + m_settleRequiresIdle = requireIdle; + enqueue({setter + getter,getter.chopped(1),[getter](const QByteArray& frame) { return matchesStatusQuery(getter,frame); }, + [this](const QByteArray&) { confirmSettle(); },2000,true,requireIdle}); +} +void Kxpa100Model::confirmSettle() +{ + if (m_settleGetter.isEmpty() || !operationBusy()) { return; } + if (m_now() >= m_settleDeadline) { fail(QStringLiteral("Readback arrived after the operation deadline.")); return; } + if (m_settleVerify(m_status)) { finish(QStringLiteral("Device readback confirmed.")); return; } + m_settleNext = m_now() + 100; +} +void Kxpa100Model::pollSettle() +{ + if (m_settleRequiresIdle && !idle()) { fail(QStringLiteral("RF or tuning became active while confirming the change.")); return; } + query(m_settleGetter,[this] { confirmSettle(); }); +} +bool Kxpa100Model::setOperate(bool value) +{ + return requestValue(value ? "^OP1;" : "^OP0;","^OP;",[value](const Status& s) { return s.operate == value; },QStringLiteral("Set PA operation")); +} +bool Kxpa100Model::setMode(Mode value) +{ + const QByteArray command = value == Mode::Auto ? "^MDA;" : value == Mode::Manual ? "^MDM;" : "^MDB;"; + return requestValue(command,"^MD;",[value](const Status& s) { return s.mode == value; },QStringLiteral("Set tuner mode"),true); +} +bool Kxpa100Model::setAntenna(int value) +{ + if (value < 1 || value > 2) { return false; } + if (m_configuration && m_status.band && !((*m_configuration)[*m_status.band] & (1 << (value - 1)))) { + emit operationFailed(QStringLiteral("This antenna is disabled on the current band.")); return false; + } + return requestValue("^AN" + QByteArray::number(value) + ';',"^AN;",[value](const Status& s) { return s.antenna == value; },QStringLiteral("Select antenna"),true); +} +bool Kxpa100Model::setCapacitorMask(int value) +{ + if (value < 0 || value > 255) { return false; } + return requestValue("^CR" + hexMask(value) + ';',"^CR;",[value](const Status& s) { return s.capacitorMask == value; },QStringLiteral("Set capacitance"),true); +} +bool Kxpa100Model::setInductorMask(int value) +{ + if (value < 0 || value > 255) { return false; } + return requestValue("^LR" + hexMask(value) + ';',"^LR;",[value](const Status& s) { return s.inductorMask == value; },QStringLiteral("Set inductance"),true); +} +bool Kxpa100Model::setCapacitorSide(CapacitorSide value) +{ + return requestValue(value == CapacitorSide::Transmitter ? "^SIT;" : "^SIA;","^SI;", + [value](const Status& s) { return s.side == value; },QStringLiteral("Set capacitor side"),true); +} +bool Kxpa100Model::setBypassed(bool value) +{ + return requestValue(value ? "^BYB;" : "^BYN;","^BY;",[value](const Status& s) { return s.bypassed == value; },QStringLiteral("Set physical ATU bypass"),true); +} +bool Kxpa100Model::setAttenuator(bool value) +{ + if (m_status.attenuator == 2) { emit operationFailed(QStringLiteral("The rear-panel switch holds the attenuator on.")); return false; } + return requestValue(value ? "^AT1;" : "^AT0;","^AT;",[value](const Status& s) { return s.attenuator == int(value); },QStringLiteral("Set attenuator")); +} +bool Kxpa100Model::clearFault() +{ + return requestValue("^FLC;","^FL;",[](const Status& s) { return s.faultCode == 'N'; },QStringLiteral("Clear current fault")); +} +bool Kxpa100Model::setBand(int band) +{ + if (band < 0 || band > 10) { return false; } + return requestValue("^BN" + QByteArray::number(band).rightJustified(2,'0') + ';',"^BN;", + [band](const Status& s) { return s.band == band; },QStringLiteral("Select band")); +} +bool Kxpa100Model::startTune(std::function permit) +{ + if (!begin(QStringLiteral("Start fresh tune"),true,true)) { return false; } + m_tunePermit = std::move(permit); m_tuneEnded = false; + preflight([this] { settle("^FT;","^TP;",[this](const Status& s) { return s.tuning == true || m_tuneEnded; }); }); return true; +} +bool Kxpa100Model::cancelTune() +{ + if (!begin(QStringLiteral("Cancel tuner search"),false,true)) { return false; } + m_operationWriting = true; + settle("^FE;","^TP;",[](const Status& s) { return s.tuning == false; },false); return true; +} +void Kxpa100Model::cancelTuneAfterPendingOperation() +{ + if (!m_connected || !m_detected) { return; } + if (operationBusy()) { m_deferredTuneCancel = true; return; } + cancelTune(); +} +void Kxpa100Model::readMatch(std::function complete) +{ + for (const QByteArray& command : {QByteArray("^TP;"),QByteArray("^MD;"),QByteArray("^AN;"),QByteArray("^BY;"), + QByteArray("^SI;"),QByteArray("^CR;"),QByteArray("^SW;"),QByteArray("^SB;")}) { query(command); } + query("^LR;",std::move(complete)); +} +bool Kxpa100Model::readMatchStatus() +{ + if (!begin(QStringLiteral("Read tuner match"),false,true)) { return false; } + readMatch([this] { finish(QStringLiteral("Tuner match readback complete.")); }); return true; +} +void Kxpa100Model::requestMemory(int frequencyKhz, const QByteArray& prefix, std::function complete) +{ + enqueue({prefix + buildFrequencyCommand("^DM",frequencyKhz),{},[](const QByteArray& frame) { return decodeMemoryBin(frame).has_value(); }, + [this,frequencyKhz,complete=std::move(complete)](const QByteArray& frame) { + const MemoryBin bin = *decodeMemoryBin(frame); + if (bin.frequencyKhz != frequencyKhz) { fail(QStringLiteral("Memory reply is for a different frequency.")); return; } + m_memories = bin; emit memoriesRead(bin); + if (operationBusy() && complete) { complete(); } + },4000,!prefix.isEmpty()}); +} +bool Kxpa100Model::inspectMemories(int frequencyKhz) +{ + if (!isSupportedFrequency(frequencyKhz) || !begin(QStringLiteral("Inspect tuner memories"),false,true)) { return false; } + requestMemory(frequencyKhz,{},[this] { finish(QStringLiteral("Memory readback complete.")); }); return true; +} +bool Kxpa100Model::memorize(int frequencyKhz, std::function permit) +{ + if (!isSupportedFrequency(frequencyKhz) || !begin(QStringLiteral("Save current match"),true,true)) { return false; } + m_tunePermit = std::move(permit); + preflight([this,frequencyKhz] { + readMatch([this,frequencyKhz] { + const Status expected = m_status; + if (!expected.antenna || !expected.bypassed || !expected.side || !expected.capacitorMask || !expected.inductorMask) { + fail(QStringLiteral("Current match is incomplete.")); return; + } + requestMemory(frequencyKhz,buildFrequencyCommand("^SM",frequencyKhz),[this,expected] { + const bool found = std::any_of(m_memories.matches.cbegin(),m_memories.matches.cend(),[&expected](const MemoryMatch& match) { + return !match.empty && match.antenna == expected.antenna && match.bypassed == expected.bypassed && + (match.bypassed || (match.side == expected.side && match.capacitorMask == expected.capacitorMask && match.inductorMask == expected.inductorMask)); + }); + if (!found) { fail(QStringLiteral("Saved match was not confirmed by memory readback.")); return; } + finish(QStringLiteral("Saved match confirmed by memory readback.")); + }); + }); + }); return true; +} +bool Kxpa100Model::recallFrequency(int frequencyKhz) +{ + if (!isSupportedFrequency(frequencyKhz) || !begin(QStringLiteral("Recall frequency match"),true,true)) { return false; } + preflight([this,frequencyKhz] { + enqueue({buildFrequencyCommand("^MT",frequencyKhz) + "^BN;","^BN",[](const QByteArray& frame) { return matchesStatusQuery("^BN;",frame); }, + [this](const QByteArray&) { readMatch([this] { finish(QStringLiteral("Recall processed; current tuner settings read back.")); }); },2000,true}); + }); return true; +} +} // namespace AetherSDR diff --git a/src/models/Kxpa100Model.h b/src/models/Kxpa100Model.h new file mode 100644 index 0000000000..944165ded0 --- /dev/null +++ b/src/models/Kxpa100Model.h @@ -0,0 +1,178 @@ +#pragma once +#include "Kxpa100Types.h" +#include +#include +#include +#include +#include +#include +#include +#include + +class QTcpSocket; +class QSerialPort; +namespace AetherSDR::Kxpa100 { class FrameParser; } +namespace AetherSDR { +class Kxpa100Model : public QObject { + Q_OBJECT +public: + explicit Kxpa100Model(QObject* parent = nullptr); + ~Kxpa100Model() override; + void connectToHost(const QString& host, quint16 port); + void connectSerial(const QString& portName, int baudRate = 38400); + void disconnectDevice(); + void setAutoReconnect(bool enabled) { m_autoReconnect = enabled; } + bool isConnected() const { return m_connected; } + bool isConnecting() const { return m_connecting; } + bool isDetected() const { return m_detected; } + bool isResponding() const; + bool isNetworkConnection() const { return m_network; } + QString description() const; + const Kxpa100::Status& status() const { return m_status; } + qint64 fieldAgeMs(const QString& command) const; + QString unavailableReason(bool writing = false, bool tuner = false) const; + bool tunerEligible() const; + bool amplifierMeterEligible() const; + bool operationBusy() const { return !m_operation.isEmpty(); } + QString operationName() const { return m_operation; } + bool setOperate(bool operate); + bool setMode(Kxpa100::Mode mode); + bool setAntenna(int antenna); + bool setCapacitorMask(int mask); + bool setInductorMask(int mask); + bool setCapacitorSide(Kxpa100::CapacitorSide side); + bool setBypassed(bool bypassed); + bool setAttenuator(bool enabled); + bool clearFault(); + bool startTune(std::function permit = {}); + bool cancelTune(); + void cancelTuneAfterPendingOperation(); + bool readMatchStatus(); + bool memorize(int frequencyKhz, std::function permit = {}); + bool recallFrequency(int frequencyKhz); + bool setBand(int band); + const std::optional& configuration() const { return m_configuration; } + bool readConfiguration(); + bool applyConfiguration(const Kxpa100::Configuration& configuration); + bool readBackup(); + bool restoreBackup(const Kxpa100::Backup& backup, bool confirmed); + const Kxpa100::Backup& backup() const { return m_backup; } + static QByteArray encodeBackupFile(const Kxpa100::Backup& backup); + static std::optional decodeBackupFile(const QByteArray& data, QString* error = nullptr); + static bool saveBackupFile(const QString& path, const Kxpa100::Backup& backup, QString* error = nullptr); + bool readFaultHistory(); + const QList& faults() const { return m_faults; } + static QByteArray faultsCsv(const QList& faults); + bool inspectMemories(int frequencyKhz); + const Kxpa100::MemoryBin& memories() const { return m_memories; } + bool eraseMemories(int band, int antenna, bool confirmed); + bool factoryReset(bool confirmed); + void cancelOperation(); + void setVerbose(bool enabled) { m_verbose = enabled; } + bool verbose() const { return m_verbose; } + QString debugText() const { return m_debug.join(QLatin1Char('\n')); } + void clearDebug() { m_debug.clear(); emit debugChanged(); } +signals: + void associationChanged(); + void connected(); + void disconnected(); + void connectionFailed(const QString& reason); + void detectedChanged(bool detected); + void statusUpdated(const AetherSDR::Kxpa100::Status& status); + void telemetryReceived(const QString& command, const AetherSDR::Kxpa100::Status& status); + void availabilityChanged(); + void diagnostic(const QString& line); + void debugChanged(); + void operationBusyChanged(bool busy); + void operationProgress(int completed, int total); + void operationFinished(const QString& message); + void operationFailed(const QString& reason); + void configurationRead(const AetherSDR::Kxpa100::Configuration& configuration); + void backupRead(const AetherSDR::Kxpa100::Backup& backup); + void faultHistoryRead(const QList& records); + void memoriesRead(const AetherSDR::Kxpa100::MemoryBin& bin); +private: + friend class Kxpa100ModelTest; + struct Request { + QByteArray wire, telemetryKey; + std::function matches; + std::function complete; + int timeoutMs = 2000; + bool mutation = false, requiresIdle = true; + }; + void transportOpened(); + void transportClosed(const QString& reason = {}); + void reconnect(); + void receiveBytes(const QByteArray& bytes); + void receiveFrame(const QByteArray& frame); + void tick(); + bool fresh(const QString& key, int maxAge = 5000) const; + bool idle() const; + bool write(const QByteArray& bytes); + void enqueue(Request request); + void query(const QByteArray& wire, std::function complete = {}); + void queueIdentity(); + bool begin(const QString& name, bool writing, bool tuner = false); + void finish(const QString& message); + void fail(const QString& reason); + void report(const QString& text, bool polling = false); + void preflight(std::function ready, bool requireIdle = true); + void readMatch(std::function complete); + bool requestValue(const QByteArray& setter, const QByteArray& getter, + std::function verify, const QString& name, bool tuner = false); + void settle(const QByteArray& setter, const QByteArray& getter, + std::function verify, bool requireIdle = true); + void pollSettle(); + void confirmSettle(); + void requestMemory(int frequencyKhz, const QByteArray& prefix, std::function complete); + void applyNextSetting(); + void readRegion(bool external, int address, int end, std::function complete); + void readRegionBlock(); + void restoreBlock(); + void restoreNextRegion(); + void restartAfterMaintenance(const QString& message); + + std::unique_ptr m_parser; + std::unique_ptr m_socket; +#ifdef HAVE_SERIALPORT + std::unique_ptr m_serial; +#endif + QElapsedTimer m_clock; + QTimer m_timer, m_reconnectTimer; + std::function m_now; + std::function m_write; + QQueue m_queue; + std::optional m_active; + quint64 m_generation = 0; + qint64 m_sentAt = 0, m_lastReply = -10000, m_nextPoll = 0, m_connectStarted = 0; + int m_pollIndex = 0; + bool m_connected = false, m_connecting = false, m_detected = false, m_closing = false; + bool m_network = true, m_autoReconnect = false, m_deliberateDisconnect = false; + bool m_verbose = false, m_deferredTuneCancel = false, m_tuneEnded = false; + QString m_host, m_portName; + quint16 m_port = 0; + int m_baud = 38400; + Kxpa100::Status m_status; + QMap m_received; + QStringList m_debug; + QString m_operation, m_operationSerial, m_operationFirmware; + bool m_operationWriting = false; + int m_progress = 0, m_total = 0; + QByteArray m_settleGetter; + std::function m_settleVerify; + std::function m_tunePermit; + qint64 m_settleDeadline = 0, m_settleNext = 0; + bool m_settleRequiresIdle = true; + std::optional m_configuration; + QQueue> m_edits; + Kxpa100::Backup m_backup, m_pendingBackup, m_restore; + QList m_faults; + Kxpa100::MemoryBin m_memories; + bool m_regionExternal = false; + int m_regionAddress = 0, m_regionEnd = 0; + QByteArray m_regionData; + std::function m_regionComplete; + bool m_restoreExternal = false; + int m_restoreAddress = 0; +}; +} diff --git a/src/models/Kxpa100Model_Maintenance.cpp b/src/models/Kxpa100Model_Maintenance.cpp new file mode 100644 index 0000000000..dedc64f6a6 --- /dev/null +++ b/src/models/Kxpa100Model_Maintenance.cpp @@ -0,0 +1,183 @@ +#include "Kxpa100Model.h" +#include "core/Kxpa100Protocol.h" +#include "core/Kxpa100Maintenance.h" +#include +#include +namespace AetherSDR { +using namespace Kxpa100; +void Kxpa100Model::readRegion(bool external, int address, int end, std::function complete) +{ + m_regionExternal = external; m_regionAddress = address; m_regionEnd = end; + m_regionData.clear(); m_regionComplete = std::move(complete); readRegionBlock(); +} +void Kxpa100Model::readRegionBlock() +{ + if (!m_connected || !operationBusy()) { return; } + if (m_regionAddress >= m_regionEnd) { + auto complete = std::move(m_regionComplete); QByteArray data = std::move(m_regionData); + if (complete) { complete(data); } return; + } + const int address = m_regionAddress; const int length = std::min(64,m_regionEnd-address); const bool external = m_regionExternal; + enqueue({buildEepromRead(external,address,length),{},[external,address,length](const QByteArray& frame) { + const auto reply = parseEepromReply(frame); + return reply && !reply->writeAck && reply->external == external && reply->address == address && reply->length == length; + },[this,length](const QByteArray& frame) { + m_regionData += parseEepromReply(frame)->data; m_regionAddress += length; + m_progress += length; emit operationProgress(m_progress,m_total); readRegionBlock(); + }}); +} +bool Kxpa100Model::readBackup() +{ + if (!begin(QStringLiteral("Read complete backup"),false)) { return false; } + if (m_status.firmware != QStringLiteral("01.39")) { fail(QStringLiteral("Complete EEPROM support is verified only for firmware 01.39.")); return false; } + m_backup = {}; m_pendingBackup = {}; m_total = 1024 + 16384; + preflight([this] { + m_pendingBackup.serial = m_status.serial; m_pendingBackup.firmware = m_status.firmware; + readRegion(false,0,1024,[this](QByteArray data) { + m_pendingBackup.internal = std::move(data); + readRegion(true,0,16384,[this](QByteArray data) { + m_pendingBackup.external = std::move(data); + preflight([this] { + m_backup = std::move(m_pendingBackup); + const Backup result = m_backup; + finish(QStringLiteral("Complete backup read, including fault history.")); emit backupRead(result); + }); + }); + }); + }); return true; +} +bool Kxpa100Model::restoreBackup(const Backup& backup, bool confirmed) +{ + if (!confirmed) { emit operationFailed(QStringLiteral("Explicit restore confirmation is required.")); return false; } + const QString reason = restoreAllowed(backup,m_status.serial,m_status.firmware); + if (!reason.isEmpty()) { emit operationFailed(reason); return false; } + if (!begin(QStringLiteral("Restore and verify backup"),true)) { return false; } + m_restore = backup; m_restoreExternal = false; m_restoreAddress = 32; m_total = 992 + 14336; + preflight([this] { restoreBlock(); }); return true; +} +void Kxpa100Model::restoreBlock() +{ + if (!m_connected || !operationBusy()) { return; } + const int end = m_restoreExternal ? 0x3800 : 1024; + if (m_restoreAddress >= end) { restoreNextRegion(); return; } + if (!fresh(QStringLiteral("^PI"),1000) || !fresh(QStringLiteral("^PF"),1000) || !fresh(QStringLiteral("^TP"),1000)) { + preflight([this] { restoreBlock(); }); return; + } + const int address = m_restoreAddress; const bool external = m_restoreExternal; + const int count = std::min({64, end-address, 256-(address%256)}); + const QByteArray data = (external ? m_restore.external : m_restore.internal).mid(address,count); + const int length = data.size(); + enqueue({buildEepromWrite(external,address,data),{},[external,address,length](const QByteArray& frame) { + const auto reply = parseEepromReply(frame); return reply && reply->writeAck && reply->external == external && reply->address == address && reply->length == length; + },[this,external,address,data,length](const QByteArray&) { + enqueue({buildEepromRead(external,address,length),{},[external,address,length](const QByteArray& frame) { + const auto reply = parseEepromReply(frame); return reply && !reply->writeAck && reply->external == external && reply->address == address && reply->length == length; + },[this,data,length](const QByteArray& frame) { + if (parseEepromReply(frame)->data != data) { fail(QStringLiteral("EEPROM readback mismatch. Restore stopped without retry or restart.")); return; } + m_restoreAddress += length; m_progress += length; emit operationProgress(m_progress,m_total); restoreBlock(); + }}); + },3000,true}); +} +void Kxpa100Model::restoreNextRegion() +{ + if (!m_connected || !operationBusy()) { return; } + if (!m_restoreExternal) { m_restoreExternal = true; m_restoreAddress = 0; restoreBlock(); } + else { + m_configuration.reset(); + restartAfterMaintenance(QStringLiteral("Restore readback verified; identity and fault history preserved. Restart requested. Reconnect to verify restored settings.")); + } +} +bool Kxpa100Model::readFaultHistory() +{ + if (!begin(QStringLiteral("Read fault history"),false)) { return false; } + if (m_status.firmware != QStringLiteral("01.39")) { fail(QStringLiteral("Fault-history decoding is verified only for firmware 01.39.")); return false; } + m_total = 2049; + preflight([this] { + readRegion(false,31,32,[this](QByteArray pointer) { + const int slot = static_cast(pointer[0]); + readRegion(true,0x3800,0x4000,[this,slot](QByteArray data) { + m_faults = decodeFaultHistory(slot,data); const QList result = m_faults; + finish(QStringLiteral("Fault history read; current fault unchanged.")); emit faultHistoryRead(result); + }); + }); + }); return true; +} +bool Kxpa100Model::readConfiguration() +{ + if (!begin(QStringLiteral("Read antenna configuration"),false,true)) { return false; } + enqueue({"^AEA;",{},[](const QByteArray& frame) { return parseConfiguration(frame).has_value(); }, + [this](const QByteArray& frame) { + m_configuration = parseConfiguration(frame); + const Configuration result = *m_configuration; + finish(QStringLiteral("Antenna configuration read.")); emit configurationRead(result); + }}); return true; +} +bool Kxpa100Model::applyConfiguration(const Configuration& configuration) +{ + if (!m_configuration) { emit operationFailed(QStringLiteral("Read antenna configuration before applying changes.")); return false; } + for (const int mask : configuration) { + if (mask < 1 || mask > 3) { emit operationFailed(QStringLiteral("Enable at least one antenna on every band.")); return false; } + } + if (!begin(QStringLiteral("Apply antenna configuration"),true,true)) { return false; } + m_edits.clear(); + for (int band = 0; band < 11; ++band) { + if ((*m_configuration)[band] != configuration[band]) { m_edits.enqueue({band,configuration[band]}); } + } + m_total = m_edits.size(); preflight([this] { applyNextSetting(); }); return true; +} +void Kxpa100Model::applyNextSetting() +{ + if (!m_connected || !operationBusy()) { return; } + if (m_edits.isEmpty()) { + const Configuration result = *m_configuration; + finish(QStringLiteral("All changed antenna settings confirmed.")); emit configurationRead(result); return; + } + if (!fresh(QStringLiteral("^PI"),1000) || !fresh(QStringLiteral("^PF"),1000) || !fresh(QStringLiteral("^TP"),1000)) { + preflight([this] { applyNextSetting(); }); return; + } + const auto [band,mask] = m_edits.dequeue(); + enqueue({configurationSet(band,mask) + "^AEA;",{},[](const QByteArray& frame) { return parseConfiguration(frame).has_value(); }, + [this,band,mask](const QByteArray& frame) { + m_configuration = parseConfiguration(frame); + const Configuration confirmed = *m_configuration; + const quint64 generation = m_generation; + emit configurationRead(confirmed); + if (!m_connected || generation != m_generation || !operationBusy()) { return; } + if (confirmed[band] != mask) { fail(QStringLiteral("Antenna setting was not confirmed; remaining edits discarded.")); return; } + emit operationProgress(++m_progress,m_total); applyNextSetting(); + },2000,true}); +} +bool Kxpa100Model::eraseMemories(int band, int antenna, bool confirmed) +{ + const QByteArray command = buildErase(band,antenna); + if (!confirmed || command.isEmpty() || !begin(QStringLiteral("Erase selected tuner memories"),true,true)) { return false; } + preflight([this,command] { + enqueue({command + "^RV;","^RV",[](const QByteArray& frame) { return matchesStatusQuery("^RV;",frame); }, + [this](const QByteArray&) { finish(QStringLiteral("Erase command processed. Inspect affected frequencies to verify memory contents.")); },6000,true}); + }); return true; +} +void Kxpa100Model::restartAfterMaintenance(const QString& message) +{ + if (!m_connected || !operationBusy()) { return; } + if (!idle()) { fail(QStringLiteral("Restart stopped: RF or tuning is active or stale. Confirmed writes remain applied.")); return; } + if (!write("^RS;")) { return; } + finish(message); + // Reset drops the serial session and can change its baud. Never replay writes + // or enable RF on reconnect; identification starts afresh. + disconnectDevice(); +} +bool Kxpa100Model::factoryReset(bool confirmed) +{ + if (!confirmed || !begin(QStringLiteral("Reset configuration"),true)) { return false; } + preflight([this] { + enqueue({"^EC;^RV;","^RV",[](const QByteArray& frame) { return matchesStatusQuery("^RV;",frame); }, + [this](const QByteArray&) { + preflight([this] { restartAfterMaintenance(QStringLiteral("Reset and restart requested. Reconnect at the device's default baud and verify configuration.")); }); + },6000,true}); + }); return true; +} +QByteArray Kxpa100Model::encodeBackupFile(const Backup& backup) { return encodeBackup(backup,QDateTime::currentDateTime()); } +std::optional Kxpa100Model::decodeBackupFile(const QByteArray& bytes, QString* error) { return decodeBackup(bytes,error); } +bool Kxpa100Model::saveBackupFile(const QString& path, const Backup& backup, QString* error) { return saveBackup(path,backup,error); } +QByteArray Kxpa100Model::faultsCsv(const QList& faults) { return faultHistoryCsv(faults); } +} // namespace AetherSDR diff --git a/src/models/Kxpa100RadioAssociation.cpp b/src/models/Kxpa100RadioAssociation.cpp new file mode 100644 index 0000000000..47d9e0ee88 --- /dev/null +++ b/src/models/Kxpa100RadioAssociation.cpp @@ -0,0 +1,95 @@ +#include "Kxpa100RadioAssociation.h" +#include "Kxpa100Model.h" +#include "RadioModel.h" +#include "SliceModel.h" +#include +#include + +namespace AetherSDR { +namespace { +Q_LOGGING_CATEGORY(lcKxpa100Association, "aether.kxpa100") +} +int Kxpa100RadioAssociation::effectiveTxFrequencyKhz(RadioModel* radio) +{ + const SliceModel* slice = radio && radio->isConnected() ? radio->txSlice() : nullptr; + if (!slice) { return 0; } + const double mhz = slice->frequency() + + ((slice->mode() == "FM" || slice->mode() == "NFM") ? slice->txOffsetFreq() : 0.0) + + (slice->xitOn() ? slice->xitFreq() / 1.0e6 : 0.0); + return std::isfinite(mhz) && mhz >= 0.001 && mhz < 100.0 + ? static_cast(std::llround(mhz * 1000.0)) : 0; +} +Kxpa100RadioAssociation::Kxpa100RadioAssociation(RadioModel* radio, Kxpa100Model* amplifier, QObject* parent) + : QObject(parent), m_radio(radio), m_tuner(amplifier) +{ + if (amplifier) { + connect(amplifier, &Kxpa100Model::associationChanged, this, &Kxpa100RadioAssociation::loadAssociation); + connect(amplifier, &Kxpa100Model::detectedChanged, this, &Kxpa100RadioAssociation::loadAssociation); + connect(amplifier, &Kxpa100Model::statusUpdated, this, [this](const Kxpa100::Status& status) { + if (status.serial != m_observedSerial) { + m_observedSerial = status.serial; + loadAssociation(); + } + }); + } + if (radio) { + connect(radio, &RadioModel::connectionStateChanged, this, &Kxpa100RadioAssociation::loadAssociation); + connect(radio, &RadioModel::infoChanged, this, &Kxpa100RadioAssociation::loadAssociation); + } + loadAssociation(); +} +bool Kxpa100RadioAssociation::associated() const +{ + if (!m_radio || !m_tuner || !m_radio->isConnected() || m_boundSerial.isEmpty()) { return false; } + const auto scope = m_radio->settingsScope(); + return scope.family() == m_boundFamily && scope.radioId() == m_boundRadio + && m_tuner->status().serial == m_boundSerial; +} +bool Kxpa100RadioAssociation::associate(RadioModel* radio, Kxpa100Model* tuner) +{ + if (!radio || !radio->isConnected() || !tuner || !tuner->isDetected() || !tuner->isResponding()) { return false; } + const auto scope = radio->settingsScope(); + if (!scope.isValid() || scope.radioId().isEmpty()) { return false; } + int version = 0; + AppSettings::FeatureReadStatus state; + QJsonObject doc = scope.featureExact("kxpa100", &version, &state); + if (version > 1 || state == AppSettings::FeatureReadStatus::Corrupt || state == AppSettings::FeatureReadStatus::Unavailable) { return false; } + doc["serial"] = tuner->status().serial; + doc.remove("useTxControls"); + if (!scope.setFeature("kxpa100", 1, doc)) { + qCWarning(lcKxpa100Association) << "KXPA100: radio association could not be saved; existing association retained"; + return false; + } + emit tuner->associationChanged(); + return true; +} +void Kxpa100RadioAssociation::loadAssociation() +{ + m_boundFamily.clear(); m_boundRadio.clear(); m_boundSerial.clear(); + if (m_radio && m_tuner) { + const auto scope = m_radio->settingsScope(); + int version = 0; + const QJsonObject doc = scope.featureExact("kxpa100", &version); + if (version == 1 && !scope.radioId().isEmpty() && doc.value("serial").toString() == m_tuner->status().serial) { + m_boundFamily = scope.family(); m_boundRadio = scope.radioId(); m_boundSerial = m_tuner->status().serial; + } + } + emit changed(); +} +bool Kxpa100RadioAssociation::dissociate(RadioModel* radio, Kxpa100Model* tuner) +{ + if (!radio || !tuner) { return false; } + const auto scope = radio->settingsScope(); + if (scope.radioId().isEmpty()) { return false; } + int version = 0; + QJsonObject doc = scope.featureExact("kxpa100", &version); + if (version != 1) { return false; } + doc.remove("serial"); + if (!scope.setFeature("kxpa100", 1, doc)) { + qCWarning(lcKxpa100Association) << "KXPA100: radio association could not be saved; existing association retained"; + return false; + } + emit tuner->associationChanged(); + return true; +} +} // namespace AetherSDR diff --git a/src/models/Kxpa100RadioAssociation.h b/src/models/Kxpa100RadioAssociation.h new file mode 100644 index 0000000000..fe86e1225d --- /dev/null +++ b/src/models/Kxpa100RadioAssociation.h @@ -0,0 +1,28 @@ +#pragma once +#include +#include +#include + +namespace AetherSDR { +class RadioModel; +class Kxpa100Model; + +// Associates amplifier telemetry with one radio. Never sends device commands +// or changes radio tuner state, and does not require the optional KXAT100. +class Kxpa100RadioAssociation final : public QObject { + Q_OBJECT +public: + Kxpa100RadioAssociation(RadioModel* radio, Kxpa100Model* amplifier, QObject* parent = nullptr); + static bool associate(RadioModel* radio, Kxpa100Model* amplifier); + static bool dissociate(RadioModel* radio, Kxpa100Model* amplifier); + static int effectiveTxFrequencyKhz(RadioModel* radio); + bool associated() const; +signals: + void changed(); +private: + void loadAssociation(); + QPointer m_radio; + QPointer m_tuner; + QString m_boundFamily, m_boundRadio, m_boundSerial, m_observedSerial; +}; +} // namespace AetherSDR diff --git a/src/models/Kxpa100Types.h b/src/models/Kxpa100Types.h new file mode 100644 index 0000000000..bebd4d0bf1 --- /dev/null +++ b/src/models/Kxpa100Types.h @@ -0,0 +1,62 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +namespace AetherSDR::Kxpa100 { +constexpr bool isSupportedFrequency(int khz) { return khz >= 1800 && khz <= 54000; } +enum class Mode { Auto, Manual, Bypass }; +enum class CapacitorSide { Transmitter, Antenna }; +struct Status { + QString identity, firmware, serial; + std::optional operate, tunerPresent, tuning, bypassed; + std::optional mode; + std::optional side; + std::optional antenna, band, capacitorMask, inductorMask, attenuator, frequencyKhz; + std::optional inputWatts, forwardWatts, reflectedWatts, dissipatedWatts; + std::optional drainAmps, supplyVolts, temperatureC, swr, bypassSwr; + std::optional faultCode, attenuatorReason; + std::optional faultDetail; + std::optional transceiverInterface; + std::optional transceiverPolling; +}; +// Bits 0/1 enable ANT1/ANT2. Missing configuration is represented by optional. +using Configuration = std::array; +QString bandName(int band); +int relayTotal(bool capacitor, int mask); +int relayMaskForValue(bool capacitor, int value); +QString currentFaultName(char code); +QString faultUnit(char code); +struct Backup { + QByteArray internal, external; + QString firmware, serial; +}; +struct FaultRecord { + int slot = 0; + char code = 0; + quint32 elapsedSeconds = 0; + int frequencyKhz = 0; + double value = 0; + QString unit, description; + QByteArray raw; +}; +struct MemoryMatch { + int antenna = 0; + bool empty = true, bypassed = false; + CapacitorSide side = CapacitorSide::Transmitter; + int capacitorMask = 0, inductorMask = 0; +}; +struct MemoryBin { + int frequencyKhz = 0, band = 0, index = 0, firstKhz = 0, lastKhz = 0; + QList matches; +}; +} +Q_DECLARE_METATYPE(AetherSDR::Kxpa100::Status) +Q_DECLARE_METATYPE(AetherSDR::Kxpa100::Configuration) +Q_DECLARE_METATYPE(AetherSDR::Kxpa100::Backup) +Q_DECLARE_METATYPE(AetherSDR::Kxpa100::MemoryBin) +Q_DECLARE_METATYPE(QList) diff --git a/tests/fixtures/kxpa100/duplicate.KXPAConfig b/tests/fixtures/kxpa100/duplicate.KXPAConfig new file mode 100644 index 0000000000..adcfc4741a --- /dev/null +++ b/tests/fixtures/kxpa100/duplicate.KXPAConfig @@ -0,0 +1,296 @@ +*KXPA Configuration saved on 2026-10-14 at 02:07:26 +*Do not edit this file! +*Make configuration changes using the KXPA front panel and save the resulting configuration +KXPA Utility Revision = Elecraft KXPA Utility Revision 1.14.3.4 +KXPA Firmware Revision = 01.39 +KXPA Serial Number = 12345 +MEM2 00 00 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 00 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 20 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 40 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 60 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 80 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 A0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 C0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 E0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 00 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 20 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 40 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 60 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 80 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 A0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 C0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 E0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 00 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 20 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 40 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 60 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 80 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 A0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 C0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 E0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 00 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 20 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 40 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 60 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 80 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 A0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 C0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 E0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM3 0000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0040 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0080 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 00C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0100 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0140 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0180 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 01C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0200 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0240 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0280 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 02C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0300 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0340 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0380 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 03C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0400 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0440 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0480 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 04C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0500 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0540 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0580 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 05C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0600 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0640 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0680 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 06C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0700 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0740 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0780 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 07C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0800 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0840 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0880 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 08C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0900 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0940 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0980 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 09C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0A00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0A40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0A80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0AC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0B00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0B40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0B80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0BC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0C00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0C40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0C80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0CC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0D00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0D40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0D80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0DC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0E00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0E40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0E80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0EC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0F00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0F40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0F80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0FC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1040 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1080 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 10C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1100 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1140 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1180 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 11C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1200 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1240 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1280 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 12C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1300 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1340 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1380 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 13C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1400 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1440 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1480 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 14C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1500 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1540 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1580 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 15C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1600 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1640 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1680 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 16C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1700 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1740 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1780 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 17C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1800 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1840 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1880 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 18C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1900 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1940 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1980 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 19C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1A00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1A40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1A80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1AC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1B00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1B40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1B80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1BC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1C00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1C40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1C80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1CC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1D00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1D40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1D80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1DC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1E00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1E40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1E80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1EC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1F00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1F40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1F80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1FC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2040 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2080 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 20C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2100 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2140 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2180 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 21C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2200 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2240 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2280 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 22C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2300 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2340 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2380 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 23C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2400 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2440 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2480 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 24C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2500 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2540 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2580 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 25C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2600 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2640 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2680 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 26C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2700 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2740 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2780 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 27C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2800 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2840 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2880 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 28C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2900 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2940 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2980 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 29C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2A00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2A40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2A80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2AC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2B00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2B40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2B80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2BC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2C00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2C40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2C80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2CC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2D00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2D40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2D80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2DC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2E00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2E40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2E80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2EC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2F00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2F40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2F80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2FC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3040 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3080 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 30C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3100 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3140 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3180 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 31C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3200 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3240 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3280 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 32C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3300 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3340 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3380 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 33C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3400 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3440 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3480 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 34C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3500 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3540 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3580 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 35C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3600 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3640 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3680 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 36C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3700 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3740 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3780 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 37C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3800 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3840 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3880 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 38C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3900 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3940 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3980 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 39C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3A00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3A40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3A80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3AC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3B00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3B40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3B80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3BC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3C00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3C40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3C80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3CC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3D00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3D40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3D80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3DC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3E00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3E40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3E80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3EC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3F00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3F40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3F80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3FC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +CRC32 = 0x9FFE1F65 diff --git a/tests/fixtures/kxpa100/invalid-metadata.KXPAConfig b/tests/fixtures/kxpa100/invalid-metadata.KXPAConfig new file mode 100644 index 0000000000..2abbbdd3df --- /dev/null +++ b/tests/fixtures/kxpa100/invalid-metadata.KXPAConfig @@ -0,0 +1,295 @@ +*KXPA Configuration saved on 2026-10-14 at 02:07:26 +*Do not edit this file! +*Make configuration changes using the KXPA front panel and save the resulting configuration +KXPA Utility Revision = Elecraft KXPA Utility Revision 1.14.3.4 +KXPA Firmware Revision = 01.39 +KXPA Serial Number = 12X45 +MEM2 00 00 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 20 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 40 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 60 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 80 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 A0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 C0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 E0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 00 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 20 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 40 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 60 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 80 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 A0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 C0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 E0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 00 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 20 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 40 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 60 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 80 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 A0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 C0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 E0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 00 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 20 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 40 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 60 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 80 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 A0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 C0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 E0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM3 0000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0040 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0080 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 00C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0100 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0140 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0180 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 01C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0200 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0240 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0280 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 02C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0300 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0340 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0380 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 03C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0400 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0440 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0480 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 04C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0500 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0540 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0580 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 05C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0600 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0640 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0680 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 06C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0700 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0740 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0780 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 07C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0800 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0840 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0880 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 08C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0900 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0940 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0980 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 09C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0A00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0A40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0A80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0AC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0B00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0B40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0B80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0BC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0C00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0C40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0C80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0CC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0D00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0D40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0D80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0DC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0E00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0E40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0E80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0EC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0F00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0F40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0F80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0FC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1040 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1080 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 10C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1100 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1140 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1180 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 11C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1200 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1240 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1280 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 12C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1300 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1340 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1380 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 13C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1400 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1440 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1480 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 14C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1500 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1540 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1580 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 15C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1600 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1640 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1680 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 16C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1700 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1740 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1780 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 17C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1800 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1840 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1880 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 18C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1900 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1940 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1980 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 19C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1A00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1A40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1A80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1AC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1B00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1B40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1B80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1BC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1C00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1C40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1C80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1CC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1D00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1D40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1D80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1DC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1E00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1E40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1E80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1EC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1F00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1F40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1F80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1FC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2040 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2080 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 20C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2100 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2140 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2180 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 21C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2200 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2240 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2280 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 22C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2300 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2340 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2380 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 23C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2400 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2440 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2480 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 24C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2500 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2540 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2580 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 25C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2600 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2640 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2680 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 26C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2700 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2740 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2780 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 27C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2800 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2840 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2880 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 28C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2900 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2940 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2980 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 29C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2A00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2A40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2A80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2AC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2B00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2B40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2B80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2BC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2C00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2C40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2C80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2CC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2D00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2D40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2D80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2DC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2E00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2E40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2E80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2EC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2F00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2F40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2F80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2FC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3040 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3080 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 30C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3100 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3140 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3180 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 31C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3200 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3240 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3280 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 32C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3300 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3340 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3380 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 33C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3400 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3440 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3480 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 34C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3500 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3540 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3580 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 35C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3600 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3640 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3680 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 36C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3700 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3740 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3780 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 37C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3800 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3840 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3880 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 38C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3900 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3940 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3980 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 39C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3A00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3A40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3A80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3AC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3B00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3B40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3B80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3BC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3C00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3C40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3C80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3CC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3D00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3D40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3D80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3DC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3E00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3E40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3E80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3EC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3F00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3F40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3F80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3FC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +CRC32 = 0xD30DA1FA diff --git a/tests/fixtures/kxpa100/missing.KXPAConfig b/tests/fixtures/kxpa100/missing.KXPAConfig new file mode 100644 index 0000000000..6110b44fef --- /dev/null +++ b/tests/fixtures/kxpa100/missing.KXPAConfig @@ -0,0 +1,294 @@ +*KXPA Configuration saved on 2026-10-14 at 02:07:26 +*Do not edit this file! +*Make configuration changes using the KXPA front panel and save the resulting configuration +KXPA Utility Revision = Elecraft KXPA Utility Revision 1.14.3.4 +KXPA Firmware Revision = 01.39 +KXPA Serial Number = 12345 +MEM2 00 20 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 40 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 60 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 80 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 A0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 C0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 E0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 00 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 20 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 40 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 60 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 80 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 A0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 C0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 E0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 00 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 20 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 40 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 60 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 80 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 A0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 C0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 E0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 00 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 20 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 40 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 60 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 80 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 A0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 C0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 E0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM3 0000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0040 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0080 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 00C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0100 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0140 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0180 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 01C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0200 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0240 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0280 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 02C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0300 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0340 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0380 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 03C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0400 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0440 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0480 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 04C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0500 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0540 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0580 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 05C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0600 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0640 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0680 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 06C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0700 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0740 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0780 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 07C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0800 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0840 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0880 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 08C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0900 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0940 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0980 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 09C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0A00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0A40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0A80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0AC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0B00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0B40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0B80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0BC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0C00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0C40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0C80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0CC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0D00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0D40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0D80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0DC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0E00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0E40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0E80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0EC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0F00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0F40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0F80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0FC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1040 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1080 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 10C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1100 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1140 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1180 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 11C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1200 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1240 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1280 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 12C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1300 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1340 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1380 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 13C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1400 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1440 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1480 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 14C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1500 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1540 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1580 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 15C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1600 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1640 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1680 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 16C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1700 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1740 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1780 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 17C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1800 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1840 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1880 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 18C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1900 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1940 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1980 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 19C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1A00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1A40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1A80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1AC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1B00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1B40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1B80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1BC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1C00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1C40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1C80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1CC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1D00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1D40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1D80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1DC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1E00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1E40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1E80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1EC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1F00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1F40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1F80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1FC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2040 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2080 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 20C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2100 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2140 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2180 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 21C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2200 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2240 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2280 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 22C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2300 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2340 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2380 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 23C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2400 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2440 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2480 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 24C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2500 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2540 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2580 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 25C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2600 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2640 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2680 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 26C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2700 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2740 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2780 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 27C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2800 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2840 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2880 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 28C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2900 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2940 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2980 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 29C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2A00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2A40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2A80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2AC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2B00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2B40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2B80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2BC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2C00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2C40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2C80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2CC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2D00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2D40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2D80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2DC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2E00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2E40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2E80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2EC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2F00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2F40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2F80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2FC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3040 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3080 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 30C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3100 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3140 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3180 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 31C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3200 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3240 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3280 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 32C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3300 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3340 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3380 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 33C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3400 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3440 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3480 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 34C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3500 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3540 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3580 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 35C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3600 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3640 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3680 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 36C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3700 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3740 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3780 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 37C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3800 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3840 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3880 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 38C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3900 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3940 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3980 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 39C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3A00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3A40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3A80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3AC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3B00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3B40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3B80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3BC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3C00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3C40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3C80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3CC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3D00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3D40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3D80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3DC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3E00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3E40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3E80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3EC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3F00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3F40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3F80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3FC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +CRC32 = 0x72CDA035 diff --git a/tests/fixtures/kxpa100/out-of-range.KXPAConfig b/tests/fixtures/kxpa100/out-of-range.KXPAConfig new file mode 100644 index 0000000000..dc18b77fb8 --- /dev/null +++ b/tests/fixtures/kxpa100/out-of-range.KXPAConfig @@ -0,0 +1,295 @@ +*KXPA Configuration saved on 2026-10-14 at 02:07:26 +*Do not edit this file! +*Make configuration changes using the KXPA front panel and save the resulting configuration +KXPA Utility Revision = Elecraft KXPA Utility Revision 1.14.3.4 +KXPA Firmware Revision = 01.39 +KXPA Serial Number = 12345 +MEM2 00 00 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 20 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 40 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 60 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 80 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 A0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 C0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 E0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 00 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 20 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 40 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 60 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 80 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 A0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 C0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 E0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 00 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 20 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 40 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 60 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 80 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 A0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 C0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 E0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 00 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 20 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 40 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 60 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 80 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 A0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 C0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 E0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM3 0000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0040 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0080 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 00C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0100 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0140 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0180 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 01C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0200 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0240 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0280 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 02C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0300 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0340 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0380 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 03C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0400 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0440 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0480 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 04C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0500 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0540 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0580 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 05C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0600 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0640 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0680 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 06C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0700 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0740 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0780 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 07C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0800 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0840 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0880 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 08C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0900 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0940 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0980 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 09C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0A00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0A40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0A80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0AC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0B00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0B40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0B80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0BC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0C00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0C40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0C80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0CC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0D00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0D40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0D80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0DC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0E00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0E40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0E80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0EC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0F00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0F40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0F80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0FC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1040 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1080 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 10C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1100 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1140 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1180 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 11C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1200 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1240 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1280 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 12C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1300 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1340 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1380 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 13C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1400 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1440 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1480 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 14C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1500 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1540 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1580 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 15C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1600 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1640 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1680 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 16C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1700 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1740 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1780 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 17C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1800 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1840 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1880 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 18C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1900 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1940 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1980 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 19C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1A00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1A40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1A80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1AC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1B00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1B40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1B80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1BC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1C00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1C40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1C80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1CC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1D00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1D40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1D80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1DC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1E00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1E40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1E80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1EC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1F00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1F40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1F80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1FC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2040 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2080 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 20C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2100 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2140 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2180 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 21C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2200 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2240 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2280 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 22C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2300 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2340 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2380 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 23C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2400 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2440 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2480 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 24C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2500 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2540 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2580 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 25C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2600 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2640 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2680 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 26C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2700 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2740 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2780 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 27C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2800 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2840 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2880 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 28C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2900 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2940 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2980 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 29C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2A00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2A40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2A80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2AC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2B00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2B40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2B80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2BC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2C00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2C40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2C80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2CC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2D00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2D40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2D80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2DC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2E00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2E40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2E80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2EC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2F00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2F40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2F80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2FC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3040 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3080 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 30C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3100 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3140 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3180 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 31C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3200 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3240 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3280 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 32C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3300 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3340 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3380 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 33C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3400 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3440 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3480 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 34C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3500 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3540 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3580 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 35C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3600 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3640 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3680 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 36C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3700 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3740 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3780 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 37C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3800 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3840 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3880 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 38C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3900 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3940 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3980 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 39C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3A00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3A40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3A80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3AC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3B00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3B40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3B80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3BC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3C00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3C40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3C80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3CC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3D00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3D40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3D80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3DC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3E00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3E40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3E80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3EC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3F00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3F40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3F80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 4000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +CRC32 = 0xF2BA7CFA diff --git a/tests/fixtures/kxpa100/synthetic.KXPAConfig b/tests/fixtures/kxpa100/synthetic.KXPAConfig new file mode 100644 index 0000000000..6d262ec132 --- /dev/null +++ b/tests/fixtures/kxpa100/synthetic.KXPAConfig @@ -0,0 +1,295 @@ +*KXPA Configuration saved on 2026-10-14 at 02:07:26 +*Do not edit this file! +*Make configuration changes using the KXPA front panel and save the resulting configuration +KXPA Utility Revision = Elecraft KXPA Utility Revision 1.14.3.4 +KXPA Firmware Revision = 01.39 +KXPA Serial Number = 12345 +MEM2 00 00 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 20 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 40 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 60 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 80 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 A0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 C0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 00 E0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 00 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 20 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 40 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 60 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 80 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 A0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 C0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 01 E0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 00 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 20 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 40 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 60 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 80 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 A0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 C0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 02 E0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 00 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 20 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 40 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 60 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 80 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 A0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 C0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM2 03 E0 20 0000000000000000000000000000000000000000000000000000000000000000 +MEM3 0000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0040 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0080 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 00C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0100 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0140 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0180 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 01C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0200 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0240 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0280 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 02C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0300 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0340 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0380 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 03C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0400 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0440 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0480 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 04C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0500 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0540 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0580 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 05C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0600 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0640 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0680 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 06C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0700 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0740 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0780 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 07C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0800 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0840 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0880 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 08C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0900 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0940 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0980 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 09C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0A00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0A40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0A80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0AC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0B00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0B40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0B80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0BC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0C00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0C40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0C80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0CC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0D00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0D40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0D80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0DC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0E00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0E40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0E80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0EC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0F00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0F40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0F80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 0FC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1040 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1080 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 10C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1100 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1140 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1180 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 11C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1200 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1240 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1280 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 12C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1300 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1340 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1380 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 13C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1400 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1440 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1480 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 14C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1500 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1540 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1580 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 15C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1600 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1640 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1680 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 16C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1700 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1740 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1780 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 17C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1800 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1840 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1880 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 18C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1900 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1940 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1980 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 19C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1A00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1A40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1A80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1AC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1B00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1B40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1B80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1BC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1C00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1C40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1C80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1CC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1D00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1D40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1D80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1DC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1E00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1E40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1E80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1EC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1F00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1F40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1F80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 1FC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2040 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2080 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 20C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2100 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2140 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2180 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 21C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2200 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2240 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2280 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 22C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2300 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2340 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2380 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 23C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2400 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2440 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2480 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 24C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2500 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2540 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2580 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 25C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2600 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2640 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2680 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 26C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2700 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2740 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2780 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 27C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2800 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2840 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2880 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 28C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2900 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2940 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2980 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 29C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2A00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2A40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2A80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2AC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2B00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2B40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2B80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2BC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2C00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2C40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2C80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2CC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2D00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2D40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2D80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2DC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2E00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2E40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2E80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2EC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2F00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2F40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2F80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 2FC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3000 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3040 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3080 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 30C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3100 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3140 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3180 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 31C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3200 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3240 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3280 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 32C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3300 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3340 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3380 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 33C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3400 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3440 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3480 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 34C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3500 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3540 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3580 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 35C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3600 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3640 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3680 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 36C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3700 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3740 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3780 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 37C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3800 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3840 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3880 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 38C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3900 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3940 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3980 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 39C0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3A00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3A40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3A80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3AC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3B00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3B40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3B80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3BC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3C00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3C40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3C80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3CC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3D00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3D40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3D80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3DC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3E00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3E40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3E80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3EC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3F00 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3F40 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3F80 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +MEM3 3FC0 40 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +CRC32 = 0x08875A33 diff --git a/tests/kxpa100_advanced_widget_test.cpp b/tests/kxpa100_advanced_widget_test.cpp new file mode 100644 index 0000000000..4531150bbd --- /dev/null +++ b/tests/kxpa100_advanced_widget_test.cpp @@ -0,0 +1,64 @@ +#include "gui/Kxpa100AdvancedWidget.h" +#include "models/Kxpa100Model.h" +#include "TestSettingsProfile.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define CHECK(x) do { if (!(x)) { std::fprintf(stderr,"FAIL %d: %s\n",__LINE__,#x); return 1; } } while (false) +namespace AetherSDR { +class Kxpa100ModelTest { +public: + static void ready(Kxpa100Model& model) { + model.m_now=[] { return qint64(100); }; model.m_connected=model.m_detected=true; model.m_lastReply=100; + model.m_status.identity="KXPA100"; model.m_status.serial="12345"; model.m_status.firmware="01.39"; + model.m_status.tuning=false; model.m_status.inputWatts=0; model.m_status.forwardWatts=0; model.m_status.tunerPresent=true; + model.m_received["^TP"]=model.m_received["^PI"]=model.m_received["^PF"]=100; + } + static void removeTuner(Kxpa100Model& model) { model.m_status.tunerPresent=false; emit model.statusUpdated(model.m_status); } +}; +class Kxpa100AdvancedWidgetTest { +public: + static int run() { + Kxpa100Model model; Kxpa100ModelTest::ready(model); Kxpa100AdvancedWidget widget(&model); + widget.showStatus("Backup readback completed."); + auto* accessible=QAccessible::queryAccessibleInterface(widget.m_status); + CHECK(accessible && accessible->text(QAccessible::Name)==widget.m_status->text()); + auto* table=widget.findChild("kxConfiguration"); CHECK(table && table->rowCount()==12 && !table->isEnabled()); + Kxpa100::Configuration config; config.fill(3); emit model.configurationRead(config); + CHECK(table->isEnabled()); table->item(1,1)->setCheckState(Qt::Unchecked); + CHECK(widget.m_staged && (*widget.m_staged)[0]==2); + table->item(1,2)->setCheckState(Qt::Unchecked); CHECK((*widget.m_staged)[0]==2); + CHECK(table->item(1,2)->checkState()==Qt::Checked); // Never stage an unconnected band. + CHECK(widget.findChild("kxApplyConfiguration")->isEnabled()); + widget.findChild("kxDiscardConfiguration")->click(); CHECK(!widget.m_staged); + table->item(0,1)->setCheckState(Qt::Unchecked); CHECK(widget.m_staged); + for (int mask : *widget.m_staged) { CHECK(mask==2); } + CHECK(widget.loadBackup(QStringLiteral(KXPA100_FIXTURE_PATH))); + auto* restore=widget.findChild("kxRestoreBackup"); CHECK(!restore->isEnabled()); + widget.findChild("kxConfirmRestore")->setChecked(true); CHECK(restore->isEnabled()); + auto* erase=widget.findChild("kxEraseMemories"); CHECK(!erase->isEnabled()); + widget.findChild("kxConfirmErase")->setChecked(true); CHECK(erase->isEnabled()); + widget.findChild("kxEraseAntenna")->setCurrentIndex(1); CHECK(!erase->isEnabled()); + CHECK(widget.findChild("kxEraseAntenna")->count()==3); + Kxpa100ModelTest::removeTuner(model); + CHECK(!table->isEnabled() && !widget.findChild("kxApplyConfiguration")->isEnabled()); + CHECK(widget.findChild("kxSaveBackup")->isEnabled()); + model.disconnectDevice(); CHECK(!restore->isEnabled() && !table->isEnabled()); + auto* transient=new Kxpa100AdvancedWidget(nullptr); transient->show(); + transient->findChild("kxChooseBackup")->click(); QTest::qWait(10); + QPointer dialog=transient->findChild("kxMaintenanceFileDialog"); CHECK(dialog); + delete transient; CHECK(dialog && !dialog->parent() && !dialog->isVisible()); + QCoreApplication::sendPostedEvents(nullptr,QEvent::DeferredDelete); CHECK(dialog.isNull()); + return 0; + } +}; +} +int main(int argc,char** argv) { TestSettingsProfile profile("kxpa100-advanced"); QApplication app(argc,argv); return AetherSDR::Kxpa100AdvancedWidgetTest::run(); } diff --git a/tests/kxpa100_applet_test.cpp b/tests/kxpa100_applet_test.cpp new file mode 100644 index 0000000000..4da69aabbb --- /dev/null +++ b/tests/kxpa100_applet_test.cpp @@ -0,0 +1,95 @@ +#include "gui/Kxpa100Applet.h" +#include "gui/HGauge.h" +#include "TestSettingsProfile.h" +#include "core/ThemeManager.h" +#include "models/Kxpa100Model.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define CHECK(x) do { if (!(x)) { std::fprintf(stderr,"FAIL %d: %s\n",__LINE__,#x); return 1; } } while (false) +namespace AetherSDR { +class Kxpa100ModelTest { +public: + static void ready(Kxpa100Model& model) { + model.m_now=[] { return qint64(100); }; model.m_connected=model.m_detected=true; model.m_lastReply=100; + auto& s=model.m_status; s.identity="KXPA100"; s.serial="00031"; s.firmware="01.39"; + s.tunerPresent=true; s.tuning=false; s.operate=true; s.inputWatts=0; s.forwardWatts=0; + s.reflectedWatts=0; s.dissipatedWatts=0; s.temperatureC=31.5; s.supplyVolts=13.3; s.drainAmps=0; + s.mode=Kxpa100::Mode::Auto; s.antenna=2; s.band=5; s.bypassed=false; s.attenuator=0; s.faultCode='N'; + s.swr=1.3; s.bypassSwr=2.2; s.capacitorMask=128; s.inductorMask=2; s.side=Kxpa100::CapacitorSide::Transmitter; + for (const QString& key : {"^TP","^PI","^PF","^SW","^TU","^MD","^AN","^OP"}) { model.m_received[key]=100; } + } + static int run() { + Kxpa100Model model; ready(model); QList wire; + model.m_write=[&](const QByteArray& bytes) { wire.append(bytes); return true; }; + Kxpa100Applet widget; widget.setModel(&model); widget.resize(260,widget.sizeHint().height()); widget.show(); QTest::qWait(10); + CHECK(wire.isEmpty()); + auto* drive=dynamic_cast(widget.findChild("kxInputGauge")); + auto* driveLabel=widget.findChild("kxMeterLabel1"); + CHECK(drive && driveLabel && driveLabel->text() == "DRV 0.0W"); + model.m_status.inputWatts=4.5; model.m_status.forwardWatts=85; + emit model.statusUpdated(model.m_status); + CHECK(driveLabel->text() == "DRV 4.5W" && drive->value() == 4.5f); + CHECK(drive->toolTip().contains("input power") && drive->accessibleDescription().contains("measured by the KXPA100")); + drive->setValueImmediate(10); + CHECK(drive->filledFraction() == 1.0f); + ready(model); emit model.statusUpdated(model.m_status); + CHECK(wire.isEmpty()); + auto* mini=widget.findChild("kxMini"); auto* manual=widget.findChild("kxManualToggle"); + auto* settings=widget.findChild("kxSettings"); auto* swr=widget.findChild("kxSwr"); + CHECK(mini && manual && settings && swr); + for (auto* b : widget.findChildren()) { CHECK(b->height() <= 22); } + CHECK(widget.findChild("kxCBit7")->text() == "1360"); + CHECK(widget.findChild("kxCBit6")->text() == "660"); + CHECK(!widget.findChild("kxAntenna3")); + CHECK(!widget.findChild("kxFrequency")->text().contains("0 kHz")); + auto* scroll=widget.findChild("kxScroll"); CHECK(scroll); + CHECK(scroll->verticalScrollBar()->maximum() == 0); + widget.resize(248,widget.minimumSizeHint().height()); QTest::qWait(10); + CHECK(scroll->verticalScrollBar()->maximum() == 0); + const int full=widget.sizeHint().height(); mini->click(); QTest::qWait(10); + CHECK(!drive->isVisible() && !driveLabel->isVisible()); + CHECK(widget.sizeHint().height() <= full * 0.60 && !manual->isVisible() && !settings->isVisible()); + CHECK(widget.findChild("kxTune")->isVisible() && swr->isVisible()); + mini->click(); manual->click(); CHECK(manual->text().startsWith("▾")); manual->click(); CHECK(manual->text().startsWith("▸")); + QTest::mousePress(swr,Qt::LeftButton); CHECK(swr->text().startsWith("BYP")); + QTest::mouseRelease(swr,Qt::LeftButton); CHECK(swr->text().startsWith("SWR")); + CHECK(!swr->accessibleDescription().contains("Space") && !swr->toolTip().contains("Space")); + widget.setRadioContext("Test radio", 14074, true); + widget.setAssociationResult(false); + auto* result = widget.findChild("kxOperationResult"); + CHECK(result && result->text().contains("not saved") && result->accessibleName() == result->text()); + widget.setRadioContext("Test radio", 14074, true); + CHECK(result->text().contains("not saved")); + CHECK(widget.findChild("kxAssociateRadio")->text() == "Remove radio association"); + widget.setAssociationResult(true); + CHECK(result->text() == "Radio association updated."); + model.m_status.tunerPresent=false; emit model.statusUpdated(model.m_status); + auto* antenna=widget.findChild("kxAntenna2"); + CHECK(antenna->isVisible() && !antenna->isEnabled() && !antenna->accessibleDescription().isEmpty()); + CHECK(widget.findChild("kxOperate")->isEnabled()); + model.m_status.tunerPresent=true; model.m_status.attenuator=2; emit model.statusUpdated(model.m_status); + CHECK(!widget.findChild("kxAttenuatorStatus")->isEnabled()); + settings->click(); widget.resize(260,700); widget.findChild("kxDebug")->click(); + auto* output=widget.findChild("kxDebugOutput"); CHECK(output); + for (int i=0;i<150;++i) { model.report(QStringLiteral("Diagnostic %1").arg(i)); } + QTest::qWait(10); CHECK(output->verticalScrollBar()->value()==output->verticalScrollBar()->maximum()); + output->verticalScrollBar()->setValue(0); model.report("Stay at earlier line"); QTest::qWait(10); + CHECK(output->verticalScrollBar()->value()==0); + settings->click(); widget.findChild("kxHistory")->click(); widget.resize(700,700); QTest::qWait(10); + auto* graph=widget.findChild("kxHistoryGraph"); const int wide=graph->width(); + widget.resize(260,500); QTest::qWait(10); CHECK(graph->width() < wide && graph->accessibleDescription().contains("W")); + return 0; + } +}; +} +int main(int argc,char** argv) { + TestSettingsProfile profile("kxpa100-applet"); QApplication app(argc,argv); + return AetherSDR::Kxpa100ModelTest::run(); +} diff --git a/tests/kxpa100_maintenance_test.cpp b/tests/kxpa100_maintenance_test.cpp new file mode 100644 index 0000000000..60b1781d55 --- /dev/null +++ b/tests/kxpa100_maintenance_test.cpp @@ -0,0 +1,52 @@ +#include "core/Kxpa100Maintenance.h" +#include +#include +#include +using namespace AetherSDR::Kxpa100; +#define CHECK(x) do { if (!(x)) { std::fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #x); return 1; } } while (false) +int main() +{ + CHECK(buildEepromRead(false, 0, 64) == "^ERP000040C0;"); + CHECK(buildEepromRead(true, 16384, 1).isEmpty()); + CHECK(buildEepromWrite(false, 31, QByteArray(1, 'x')).isEmpty()); + CHECK(buildEepromWrite(false, 224, QByteArray(64, 'x')).isEmpty()); + CHECK(!buildEepromWrite(false, 224, QByteArray(32, 'x')).isEmpty()); + CHECK(buildEepromWrite(true, 0x3800, QByteArray(1, 'x')).isEmpty()); + CHECK(buildEepromWrite(true, 0x37ff, QByteArray(2, 'x')).isEmpty()); + CHECK(!buildEepromWrite(true, 0x37c0, QByteArray(64, 'x')).isEmpty()); + CHECK(!parseEepromReply("^ERP000201FE00")); + CHECK(parseEepromReply("^ERP00020100FD")->data == QByteArray(1, '\0')); + CHECK(parseEepromReply("^EWP002040A0")->writeAck); + CHECK(!parseEepromReply("^ERP00000000")); + QFile fixture(QStringLiteral(KXPA100_FIXTURE_PATH)); CHECK(fixture.open(QIODevice::ReadOnly)); + const QByteArray original = fixture.readAll(); QString error; + const auto backup = decodeBackup(original, &error); + CHECK(backup && backup->internal.size() == 1024 && backup->external.size() == 16384); + CHECK(backup->serial == "12345" && backup->firmware == "01.39"); + CHECK(restoreAllowed(*backup, "12345", "01.39").isEmpty()); + CHECK(!restoreAllowed(*backup, "00031", "01.39").isEmpty()); + CHECK(!restoreAllowed(*backup, "12345", "01.40").isEmpty()); + Backup newer = *backup; newer.firmware = "01.40"; + CHECK(!restoreAllowed(newer, "12345", "01.40").isEmpty()); + QByteArray bad = original; bad[500] = bad[500] == 'A' ? 'B' : 'A'; CHECK(!decodeBackup(bad)); + CHECK(!decodeBackup(original.left(original.size()/2)) && !decodeBackup(original + "EXTRA\n")); + for (const char* name : {"missing", "duplicate", "invalid-metadata", "out-of-range"}) { + QFile invalid(QStringLiteral(KXPA100_INVALID_FIXTURE_DIR) + '/' + QString::fromLatin1(name) + ".KXPAConfig"); + CHECK(invalid.open(QIODevice::ReadOnly)); CHECK(!decodeBackup(invalid.readAll())); + } + const auto roundtrip = decodeBackup(encodeBackup(*backup, QDateTime::fromSecsSinceEpoch(0, QTimeZone::UTC))); + CHECK(roundtrip && roundtrip->internal == backup->internal && roundtrip->external == backup->external); + QByteArray faults(2048, char(0xff)); + faults.replace(0, 8, QByteArray::fromHex("4301020304050607")); + faults.replace(255*8, 8, QByteArray::fromHex("5000000000002301")); + const auto records = decodeFaultHistory(0, faults); + CHECK(records.size() == 2 && records[0].elapsedSeconds == 197121 && records[0].frequencyKhz == 1284); + CHECK(records[0].value == 179.8 && records[0].unit == "A"); + CHECK(records[1].slot == 255 && records[1].value == 2.91 && records[1].unit == "W"); + for (const auto& item : QList>{{'N',291},{'A',29.1},{'S',29.1},{'C',29.1},{'D',2.91},{'P',2.91},{'R',2.91},{'H',0.291},{'L',0.291},{'T',29.1}}) { + QByteArray sample(2048, char(0xff)); QByteArray record = QByteArray::fromHex("0000000000002301"); record[0] = item.first; + sample.replace(0, 8, record); CHECK(decodeFaultHistory(0, sample).front().value == item.second); + } + CHECK(decodeFaultHistory(256, faults).isEmpty() && decodeFaultHistory(0, faults.left(100)).isEmpty()); + return 0; +} diff --git a/tests/kxpa100_model_test.cpp b/tests/kxpa100_model_test.cpp new file mode 100644 index 0000000000..5c23e49c3c --- /dev/null +++ b/tests/kxpa100_model_test.cpp @@ -0,0 +1,221 @@ +#include "models/Kxpa100Model.h" +#include "core/Kxpa100Maintenance.h" +#include +#include +#include +#ifdef HAVE_SERIALPORT +#include +#endif +#include +#include +using namespace AetherSDR::Kxpa100; +#define CHECK(x) do { if (!(x)) { std::fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #x); return 1; } } while (false) +namespace AetherSDR { +class Kxpa100ModelTest { +public: + struct Harness { + Kxpa100Model model; + qint64 now = 100; + QList wire; + Harness() { + model.m_now = [this] { return now; }; + model.m_write = [this](const QByteArray& bytes) { wire.append(bytes); return true; }; + model.m_connected = model.m_detected = true; + model.m_lastReply = now; model.m_nextPoll = 100000; + model.m_status.identity = "KXPA100"; model.m_status.serial = "00031"; model.m_status.firmware = "01.39"; + model.m_status.tuning = false; model.m_status.inputWatts = 0; model.m_status.forwardWatts = 0; + model.m_status.tunerPresent = true; model.m_status.mode = Mode::Auto; model.m_status.antenna = 1; + model.m_status.operate = true; model.m_status.faultCode = 'N'; + for (const QString& key : {"^I","^RV","^SN","^TP","^PI","^PF","^TU","^MD","^AN","^OP","^FL"}) { model.m_received[key] = now; } + } + void reply(const QByteArray& bytes) { model.tick(); model.receiveBytes(bytes); } + void preflight() { + reply("^IKXPA100;"); reply("^RV01.39;"); reply("^SN00031;"); + reply("^TP0;"); reply("^PI0000;"); reply("^PF0000;"); + } + }; + static int maintenance() { + // Bytes are injected at the model boundary; this test opens no socket or serial port. + for (int failure : {0,1,2}) { + Harness h; + QByteArray internal(1024, char(0x51)), external(16384, char(0x72)); + const QByteArray originalInternal = internal, originalExternal = external; + Backup backup{QByteArray(1024,char(0x19)),QByteArray(16384,char(0x28)),"01.39","00031"}; + int applied = 0, consumed = 0; + QSignalSpy done(&h.model,&Kxpa100Model::operationFinished), failed(&h.model,&Kxpa100Model::operationFailed); + CHECK(h.model.restoreBackup(backup,true)); + for (int step = 0; step < 5000 && h.model.operationBusy(); ++step) { + h.model.tick(); h.now += 50; + if (consumed == h.wire.size()) { continue; } + const QByteArray wire = h.wire[consumed++]; + if (wire.startsWith("^ER") || wire.startsWith("^EW")) { + const bool ext = wire[3] == 'S', write = wire[2] == 'W'; + const QByteArray bytes = QByteArray::fromHex(wire.mid(4).chopped(1)); + const int address = quint8(bytes[0])*256 + quint8(bytes[1]), count = quint8(bytes[2]); + QByteArray& memory = ext ? external : internal; + if (write) { + CHECK(address >= (ext ? 0 : 32) && address + count <= (ext ? 0x3800 : 1024)); + CHECK(address/256 == (address+count-1)/256); + memory.replace(address,count,bytes.mid(3,count)); ++applied; + if (failure == 1 && applied == 2) { h.now += 4000; continue; } + } + QByteArray reply = bytes.left(3); + if (!write) { reply += (failure == 2 && applied == 2) ? QByteArray(count,'!') : memory.mid(address,count); } + int sum = 0; for (char b : reply) { sum += quint8(b); } reply.append(char(-sum)); + h.model.receiveBytes(wire.left(4) + reply.toHex().toUpper() + ';'); + } else { + const QMap replies{{"^I;","^IKXPA100;"},{"^RV;","^RV01.39;"},{"^SN;","^SN00031;"}, + {"^TP;","^TP0;"},{"^PI;","^PI0000;"},{"^PF;","^PF0000;"}}; + CHECK(replies.contains(wire)); h.model.receiveBytes(replies.value(wire)); + } + } + CHECK(!h.model.operationBusy()); + CHECK(internal.left(32) == originalInternal.left(32) && external.mid(0x3800) == originalExternal.mid(0x3800)); + if (failure == 0) { + CHECK(done.size() == 1 && failed.isEmpty() && h.wire.last() == "^RS;"); + CHECK(internal.mid(32) == backup.internal.mid(32) && external.left(0x3800) == backup.external.left(0x3800)); + } else { + CHECK(done.isEmpty() && failed.size() == 1 && applied == 2 && !h.wire.contains("^RS;")); + } + } + return 0; + } + static int transportBoundaries() + { + Kxpa100Model model; + CHECK(model.m_socket->readBufferSize() > 0 && model.m_socket->readBufferSize() <= 4096); + QSignalSpy failed(&model, &Kxpa100Model::connectionFailed); + const QList invalidHosts{QString(), QString(254, QLatin1Char('a')), + QStringLiteral("bridge/path"), QStringLiteral("bridge host"), + QStringLiteral("bridge\nhost"), QStringLiteral("bridge") + QChar(0)}; + for (const QString& host : invalidHosts) { + model.connectToHost(host, 4001); + CHECK(!model.isConnected() && !model.m_connecting && !model.m_timer.isActive()); + CHECK(model.m_socket->state() == QAbstractSocket::UnconnectedState); + } + model.connectToHost(QStringLiteral("bridge"), 0); + CHECK(failed.size() == invalidHosts.size() + 1 && !model.m_connecting); +#ifdef HAVE_SERIALPORT + CHECK(model.m_serial && !model.m_serial->isOpen()); + model.connectSerial(QString(), 38400); + model.connectSerial(QStringLiteral("unused-port"), 12345); + CHECK(!model.m_serial->isOpen() && !model.isConnected()); +#endif + return 0; + } + static int swrPolling() + { + const QList> replies{{"^SW01.3;", 1.3}, {"^SW00.0;", 0.0}, + {"^SW99.9;", 99.9}, {"^SW013;", 1.3}}; + for (const auto& [reply, expected] : replies) { + Harness h; + QSignalSpy failed(&h.model, &Kxpa100Model::connectionFailed); + QSignalSpy disconnected(&h.model, &Kxpa100Model::disconnected); + QSignalSpy updated(&h.model, &Kxpa100Model::statusUpdated); + h.model.m_nextPoll = h.now; + h.model.tick(); + CHECK(h.wire == QList{"^SW;"} && h.model.m_active); + const int timeout = h.model.m_active->timeoutMs; + h.model.receiveBytes("^SW1.3;"); + CHECK(h.model.m_active && !h.model.status().swr && updated.isEmpty()); + h.model.receiveBytes(reply.left(5)); + CHECK(h.model.m_active && updated.isEmpty()); + h.model.receiveBytes(reply.mid(5)); + CHECK(!h.model.m_active && h.model.status().swr == expected); + CHECK(updated.size() == 1 && h.model.fieldAgeMs(QStringLiteral("^SW")) == 0); + h.now += timeout + 1; + h.model.tick(); + CHECK(h.model.isConnected() && h.model.isDetected() && h.model.isResponding()); + CHECK(failed.isEmpty() && disconnected.isEmpty()); + CHECK(h.wire.size() == 2 && h.wire.last() == "^TP;" && h.model.m_active); + h.model.receiveBytes("^TP0;"); + CHECK(!h.model.m_active && h.model.status().swr == expected); + } + return 0; + } + static int run() { + CHECK(transportBoundaries() == 0); + CHECK(swrPolling() == 0); + { + Harness h; h.model.transportOpened(); + CHECK(!h.model.isDetected() && h.wire.isEmpty()); + h.model.tick(); h.model.receiveBytes("^IKXP"); CHECK(!h.model.isDetected()); + h.model.receiveBytes("A100;"); h.reply("^RV01.39;"); CHECK(!h.model.isDetected()); + h.reply("^SN00031;"); CHECK(h.model.isDetected()); + h.model.disconnectDevice(); CHECK(!h.model.isDetected()); + CHECK(!h.wire.contains("^FT;^TP;") && !h.wire.contains("^OP1;^OP;")); + } + { + Harness h; QSignalSpy done(&h.model,&Kxpa100Model::operationFinished); + CHECK(h.model.setAntenna(2)); CHECK(h.model.status().antenna == 1); + h.preflight(); h.reply("^AN1;"); CHECK(h.wire.last() == "^AN2;^AN;" && h.model.operationBusy()); + h.now += 100; h.reply("^AN2;"); + CHECK(!h.model.operationBusy() && done.size() == 1 && h.model.status().antenna == 2); + CHECK(h.wire.count("^AN2;^AN;") == 1); + } + { + Harness h; CHECK(h.model.setAntenna(2)); h.preflight(); h.model.tick(); + h.now += 2500; h.model.receiveBytes("^AN2;"); + CHECK(!h.model.operationBusy() && !h.model.isConnected()); + CHECK(h.wire.count("^AN2;^AN;") == 1); + } + { + Harness h; CHECK(h.model.setAntenna(2)); h.reply("^IKXPA100;"); h.reply("^RV01.39;"); h.reply("^SN00032;"); + CHECK(!h.model.isConnected() && !h.model.operationBusy()); + CHECK(!h.wire.contains("^AN2;^AN;")); + } + { + Harness h; h.model.m_status.tunerPresent = false; + CHECK(!h.model.setAntenna(2) && !h.model.startTune() && h.wire.isEmpty()); + CHECK(h.model.setOperate(false)); // Optional tuner does not disable the amplifier. + } + { + Harness h; CHECK(h.model.setAntenna(2)); + h.reply("^IKXPA100;"); h.reply("^RV01.39;"); h.reply("^SN00031;"); + h.reply("^TP0;"); h.reply("^PI0010;"); h.reply("^PF0100;"); + CHECK(!h.model.operationBusy() && !h.wire.contains("^AN2;^AN;")); + } + { + Harness h; bool permit = true; CHECK(h.model.startTune([&] { return permit; })); + h.preflight(); permit = false; h.model.tick(); + CHECK(!h.model.operationBusy() && !h.wire.contains("^FT;^TP;")); + } + { + Harness h; CHECK(h.model.startTune()); h.preflight(); h.reply("^TP1;"); + CHECK(!h.model.operationBusy() && h.model.status().tuning == true); + CHECK(h.model.cancelTune()); h.reply("^FE;^TP0;"); + CHECK(!h.model.operationBusy() && h.model.status().tuning == false); + } + { + Harness h; h.now += 6000; + CHECK(!h.model.setAntenna(2) && !h.model.tunerEligible() && !h.model.amplifierMeterEligible()); + h.model.m_lastReply = h.now; // Unrelated replies cannot freshen critical fields. + CHECK(!h.model.tunerEligible() && !h.model.amplifierMeterEligible()); + } + { + Harness h; CHECK(!h.model.restoreBackup({}, true)); CHECK(!h.model.factoryReset(false)); + CHECK(!h.model.eraseMemories(15,0,false) && h.wire.isEmpty()); + CHECK(h.model.setMode(Mode::Manual)); h.model.cancelOperation(); + CHECK(!h.model.operationBusy()); h.model.tick(); CHECK(h.wire.isEmpty()); + } + { + Harness h; CHECK(h.model.readConfiguration()); h.reply("^AEA33333333333;"); + CHECK(h.model.configuration() && (*h.model.configuration())[0] == 3); + Configuration invalid = *h.model.configuration(); invalid[0] = 0; + CHECK(!h.model.applyConfiguration(invalid)); + } + { + Harness h; CHECK(h.model.readConfiguration()); h.reply("^AEA33333333333;"); + Configuration config = *h.model.configuration(); config[0] = 1; + CHECK(h.model.applyConfiguration(config)); h.preflight(); + QSignalSpy progress(&h.model,&Kxpa100Model::operationProgress); + QObject::connect(&h.model,&Kxpa100Model::configurationRead,&h.model,[&] { h.model.disconnectDevice(); }); + h.reply("^AEA13333333333;"); CHECK(!h.model.isConnected() && !h.model.operationBusy()); + CHECK(progress.isEmpty()); + } + CHECK(maintenance() == 0); + return 0; + } +}; +} +int main(int argc, char** argv) { QCoreApplication app(argc, argv); return AetherSDR::Kxpa100ModelTest::run(); } diff --git a/tests/kxpa100_protocol_test.cpp b/tests/kxpa100_protocol_test.cpp new file mode 100644 index 0000000000..0a057c198c --- /dev/null +++ b/tests/kxpa100_protocol_test.cpp @@ -0,0 +1,67 @@ +#include "core/Kxpa100Protocol.h" +#include +#include +using namespace AetherSDR::Kxpa100; +#define CHECK(x) do { if (!(x)) { std::fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #x); return 1; } } while (false) +int main() +{ + FrameParser parser; + QList frames; + parser.setCallback([&](const QByteArray& frame) { frames.append(frame); }); + parser.feed("\r\n^IKXP"); parser.feed("A100;^OP1;^TU1;^MDB;^BYB;^AN2;^AT2;^TP0;"); + CHECK(frames.size() == 8 && frames.front() == "^IKXPA100"); + Status s; + for (const QByteArray& frame : frames) { CHECK(applyStatus(frame, s)); } + CHECK(s.identity == "KXPA100" && s.operate == true && s.tunerPresent == true); + CHECK(s.mode == Mode::Bypass && s.bypassed == true && s.antenna == 2 && s.attenuator == 2 && s.tuning == false); + CHECK(applyStatus("^PI0012", s) && s.inputWatts == 1.2); + CHECK(applyStatus("^PF0500", s) && s.forwardWatts == 50.0); + CHECK(applyStatus("^PV0011", s) && s.reflectedWatts == 1.1); + CHECK(applyStatus("^PD0123", s) && s.dissipatedWatts == 12.3); + CHECK(applyStatus("^PC0023", s) && s.drainAmps == 2.3); + CHECK(applyStatus("^SV13287", s) && s.supplyVolts == 13.287); + CHECK(applyStatus("^TM0315", s) && s.temperatureC == 31.5); + CHECK(applyStatus("^SW010", s) && s.swr == 1.0); + CHECK(applyStatus("^SW013", s) && s.swr == 1.3); + CHECK(applyStatus("^SW01.0", s) && s.swr == 1.0); + CHECK(applyStatus("^SW01.3", s) && s.swr == 1.3); + CHECK(applyStatus("^SW00.0", s) && s.swr == 0.0); + CHECK(applyStatus("^SW00.1", s) && s.swr == 0.1); + CHECK(applyStatus("^SW99.9", s) && s.swr == 99.9); + CHECK(applyStatus("^SW999", s) && s.swr == 99.9); + for (const QByteArray& invalid : {"^SW009", "^SW1.3", "^SW001.3", "^SW01.30", "^SW01,3", + "^SW-1.3", "^SW+1.3", "^SW 1.3", "^SW01.-", "^SW01.a", "^SW01.3extra", "^SW1e3"}) { + CHECK(!applyStatus(invalid, s) && s.swr == 99.9); + CHECK(!matchesStatusQuery("^SW;", invalid)); + } + CHECK(matchesStatusQuery("^SW;", "^SW01.3") && matchesStatusQuery("^SW;", "^SW00.0")); + CHECK(matchesStatusQuery("^SW;", "^SW013")); + CHECK(applyStatus("^SB012", s) && s.bypassSwr == 1.2); + CHECK(applyStatus("^F14074", s) && s.frequencyKhz == 14074); + CHECK(applyStatus("^FLP01234", s) && s.faultCode == 'P' && s.faultDetail == 123.4); + CHECK(applyStatus("^FLN", s) && s.faultCode == 'N'); + CHECK(!applyStatus("^PF5", s) && s.forwardWatts == 50.0); + CHECK(!applyStatus("^PF-001", s) && !applyStatus("^AN3", s) && !applyStatus("^OP2", s)); + CHECK(!applyStatus("^SWnan", s) && !applyStatus("^RV01.39extra", s)); + CHECK(matchesStatusQuery("^OP;", "^OP1") && !matchesStatusQuery("^F;", "^FLN")); + frames.clear(); parser.feed(QByteArray(5000, 'A') + ";^OP0;"); + CHECK(frames.size() == 1 && frames[0] == "^OP0"); + CHECK(relayTotal(true, 255) == 2624 && relayTotal(false, 255) == 17370); + CHECK(relayMaskForValue(true, 32) == 3 && relayTotal(true, relayMaskForValue(true, 31)) == 32); + CHECK(buildFrequencyCommand("^SM", 14074) == "^SM14074;"); + CHECK(buildFrequencyCommand("^MT", 0).isEmpty() && buildFrequencyCommand("^FT", 14074).isEmpty()); + CHECK(buildErase(15, 0) == "^EM150;" && buildErase(5, 2) == "^EM052;"); + CHECK(buildErase(11, 2).isEmpty() && buildErase(5, 3).isEmpty()); + const auto config = parseConfiguration("^AEA33333333333"); + CHECK(config && (*config)[10] == 3 && !parseConfiguration("^AEA33333333330")); + CHECK(configurationSet(5, 2) == "^AE052;" && configurationSet(5, 0).isEmpty()); + CHECK(configurationSet(11, 1).isEmpty()); + const auto memory = decodeMemoryBin("^DM F03805 BN01 BIN040 FR03800-03819 AN1 BYN SIT CR0E LRD8 AN2 EMPTY"); + CHECK(memory && memory->matches.size() == 2 && memory->matches[0].capacitorMask == 14 && memory->matches[1].empty); + CHECK(!decodeMemoryBin("^DM F03805 BN01 BIN040 FR03810-03819 AN1 EMPTY AN2 EMPTY")); + CHECK(!decodeMemoryBin("^DM F03805 BN11 BIN040 FR03800-03819 AN1 EMPTY AN2 EMPTY")); + CHECK(!decodeMemoryBin("^DM F03805 BN01 BIN040 FR03800-03819 AN1 EMPTY AN1 EMPTY")); + frames.clear(); parser.feed("^DM F03805 BN01 BIN040 FR03800-03819\r\nAN1 BYN SIT CR0E LRD8\r\nAN2 EMPTY;"); + CHECK(frames.size() == 1 && decodeMemoryBin(frames[0])); + return 0; +} diff --git a/tests/kxpa100_tx_controls_test.cpp b/tests/kxpa100_tx_controls_test.cpp new file mode 100644 index 0000000000..8f6dd298fe --- /dev/null +++ b/tests/kxpa100_tx_controls_test.cpp @@ -0,0 +1,167 @@ +#include "TestSettingsProfile.h" +#include "core/AppSettings.h" +#include "core/RadioSettingsScope.h" +#include "core/SettingsDatabase.h" +#include "gui/TxApplet.h" +#include "gui/SharedTxMeterSelection.h" +#include "models/Kxpa100Model.h" +#include "models/Kxpa100RadioAssociation.h" +#include "models/RadioModel.h" +#include "models/SliceModel.h" +#include +#include +#include +#include +#include +#include +#include +#include +using namespace AetherSDR; +#define CHECK(x) do { if (!(x)) { std::fprintf(stderr,"FAIL %d: %s\n",__LINE__,#x); return 1; } } while (false) +namespace { +QStringList preferenceWarnings; +void capturePreferenceWarning(QtMsgType type, const QMessageLogContext& context, + const QString& message) +{ + if (type == QtWarningMsg && QString::fromLatin1(context.category) == "aether.kxpa100") { + preferenceWarnings.append(message); + } +} +} + +namespace AetherSDR { +class TxOperationIntegrationTestAccess { +public: + static void identity(RadioModel& r, const QString& serial = "HL2-TEST") { r.m_lastInfo.serial=serial; emit r.infoChanged(); } + static void family(RadioModel& r, const QString& family) { r.m_family=family; } +}; +class Kxpa100ModelTest { +public: + static void ready(Kxpa100Model& k, QList& wire) { + k.m_timer.stop(); k.m_now=[] { return qint64(100); }; k.m_lastReply=100; k.m_nextPoll=100000; + k.m_write=[&wire](const QByteArray& bytes) { wire.append(bytes); return true; }; + k.m_connected=k.m_detected=true; + auto& s=k.m_status; s.identity="KXPA100"; s.firmware="01.39"; s.serial="00031"; + s.tuning=false; s.inputWatts=0; s.forwardWatts=0; s.tunerPresent=true; + s.mode=Kxpa100::Mode::Auto; s.antenna=2; s.operate=true; s.faultCode='N'; + for (const QString& key : {"^I","^RV","^SN","^TP","^PI","^PF","^TU","^MD","^AN","^OP","^FL"}) { k.m_received[key]=100; } + } + static void frame(Kxpa100Model& k,const QByteArray& reply) { k.receiveBytes(reply); } + static void observe(Kxpa100Model& k,const QByteArray& query,const QByteArray& reply) { k.query(query); step(k,reply); } + static void step(Kxpa100Model& k,const QByteArray& reply) { k.tick(); k.receiveBytes(reply); } + static void sendNext(Kxpa100Model& k) { k.tick(); } + static void preflight(Kxpa100Model& k) { + for (const QByteArray& s : {QByteArray("^IKXPA100;"),QByteArray("^RV01.39;"),QByteArray("^SN00031;"),QByteArray("^TP0;"),QByteArray("^PI0000;"),QByteArray("^PF0000;")}) { step(k,s); } + } + static void stale(Kxpa100Model& k) { k.m_now=[] { return qint64(6100); }; emit k.availabilityChanged(); } + static void drop(Kxpa100Model& k) { k.transportClosed(); } + static void match(Kxpa100Model& k) { + for (const QByteArray& s : {QByteArray("^TP0;"),QByteArray("^MDA;"),QByteArray("^AN2;"),QByteArray("^BYN;"),QByteArray("^SIT;"),QByteArray("^CR00;"),QByteArray("^SW010;"),QByteArray("^SB010;"),QByteArray("^LR00;")}) { step(k,s); } + } +}; +} +class Backend final : public IRadioBackend { +public: + QStringList wire; + bool connected = false; + RadioCapabilities capabilities() const override { RadioCapabilities c; c.canTransmit=true; c.hasTuner=false; return c; } + bool isConnected() const override { return connected; } + void connectRadio(const RadioConnectRequest&) override {} + void disconnectRadio() override {} + void setSliceFrequency(int,double) override {} + void setSliceMode(int,const QString&) override {} + void setSliceFilter(int,int,int) override {} + void setSliceAgc(int,const QString&,int) override {} + void setPanCenter(const QString&,double,PanCenterIntent) override {} + void setKeying(bool,const TxCoordinator::Operation&,const TxCoordinator::Completion&) override {} + void setAtu(bool on,const TxCoordinator::Operation&,const TxCoordinator::Completion&) override { wire << (on?"atu:on":"atu:off"); } + void setTune(bool on,int,const TxCoordinator::Operation&,const TxCoordinator::Completion&) override { wire << (on?"tune:on":"tune:off"); } + void invokeExtension(const QString&,const QString&,quint64,const QVariant&) override {} +}; +int main(int argc,char** argv) { + TestSettingsProfile profile("kxpa100_tx_controls"); + QApplication app(argc,argv); + AppSettings::instance().load(); + RadioModel radio; auto backend=std::make_unique(); auto* b=backend.get(); + radio.setBackendForTest(std::move(backend),"test"); + CHECK(radio.automationApplySliceFixture(0,"A")); + b->connected = true; + SliceDelta slice; slice.txSlice=true; slice.mode="USB"; slice.frequency=14.1; slice.panId="0x40000000"; radio.slice(0)->applyChanges(slice); + radio.transmitModel().setTxModeGetter([] { return QStringLiteral("USB"); }); + radio.transmitModel().setHasTuner(false); + // Host-modulated radios expose the TX slice without Flex's transmit-freq status. + TransmitDelta delta; delta.transmitFreq=0; delta.tunePower=1; radio.transmitModel().applyChanges(delta); + Kxpa100Model k; QList wire; Kxpa100ModelTest::ready(k,wire); + Kxpa100RadioAssociation association(&radio, &k); + CHECK(!association.associated() && b->wire.isEmpty() && wire.isEmpty()); + TxOperationIntegrationTestAccess::identity(radio); + CHECK(Kxpa100RadioAssociation::associate(&radio, &k)); + CHECK(association.associated()); + CHECK(!radio.settingsScope().featureExact("kxpa100").contains("useTxControls")); + TxOperationIntegrationTestAccess::family(radio, "previous-family"); + TxOperationIntegrationTestAccess::identity(radio); + CHECK(!association.associated()); + TxOperationIntegrationTestAccess::family(radio, "test"); + emit radio.connectionStateChanged(true); + CHECK(association.associated()); + { + const RadioSettingsScope scope = radio.settingsScope(); + const QJsonObject original = scope.featureExact("kxpa100"); + SettingsDatabase locker; + CHECK(locker.open(AppSettings::instance().filePath()) && locker.begin()); + const QtMessageHandler previous = qInstallMessageHandler(capturePreferenceWarning); + const auto restoreHandler = qScopeGuard([previous] { qInstallMessageHandler(previous); }); + CHECK(!Kxpa100RadioAssociation::dissociate(&radio, &k)); + CHECK(association.associated() && scope.featureExact("kxpa100") == original); + CHECK(preferenceWarnings.size() == 1); + CHECK(preferenceWarnings.first().contains("association could not be saved")); + locker.rollback(); + locker.close(); + CHECK(Kxpa100RadioAssociation::dissociate(&radio, &k)); + CHECK(!association.associated()); + CHECK(Kxpa100RadioAssociation::associate(&radio, &k)); + CHECK(association.associated()); + } + // Existing stored overlay preferences cannot change the native radio controls. + { + QJsonObject legacy = radio.settingsScope().featureExact("kxpa100"); + legacy["useTxControls"] = true; + CHECK(radio.settingsScope().setFeature("kxpa100", 1, legacy)); + emit k.associationChanged(); + CHECK(association.associated()); + TxApplet applet; + applet.setRadioModel(&radio); + applet.setTransmitModel(&radio.transmitModel()); + QPushButton* atu = nullptr; + QPushButton* mem = nullptr; + for (auto* button : applet.findChildren()) { + if (button->accessibleName() == "ATU tune") { atu = button; } + if (button->accessibleName() == "ATU memories") { mem = button; } + } + CHECK(atu && mem && atu->text() == "ATU" && mem->text() == "MEM"); + CHECK(!atu->isEnabled() && !mem->isEnabled()); + for (auto* label : applet.findChildren()) { CHECK(label->text() != "KXPA100"); } + Kxpa100ModelTest::observe(k, "^TU;", "^TU0;"); + CHECK(association.associated() && k.amplifierMeterEligible() && !k.tunerEligible()); + CHECK(!atu->isEnabled() && atu->text() == "ATU"); + radio.transmitModel().setHasTuner(true); + CHECK(atu->isEnabled() && atu->text() == "ATU" && mem->text() == "MEM"); + } + const auto kxOnly = selectSharedTxMeters(5, false, false, false, true); + CHECK(kxOnly.source == SharedTxMeterSource::Kxpa100 && kxOnly.maxWatts == 100); + const auto pgxl = selectSharedTxMeters(100, true, true, true, true); + CHECK(pgxl.source == SharedTxMeterSource::Amplifier && pgxl.highPowerAmplifier); + const auto kpa = selectSharedTxMeters(100, false, true, false, true); + CHECK(kpa.source == SharedTxMeterSource::Amplifier && kpa.maxWatts == 500); + const auto blocked = selectSharedTxMeters(100, false, true, true, true); + CHECK(blocked.source == SharedTxMeterSource::RadioKeyBlocked && blocked.maxWatts == 100); + const auto stale = selectSharedTxMeters(5, false, false, false, false); + CHECK(stale.source == SharedTxMeterSource::Radio && stale.maxWatts == 5); + TxOperationIntegrationTestAccess::identity(radio, "ANOTHER-RADIO"); + CHECK(!association.associated()); + TxOperationIntegrationTestAccess::identity(radio); + CHECK(association.associated()); + CHECK(b->wire.isEmpty()); + for (const QByteArray& command : wire) { CHECK(command == "^TU;"); } + return 0; +} diff --git a/tests/tests.cmake b/tests/tests.cmake index bcbc4b02fa..e1d343dbe3 100644 --- a/tests/tests.cmake +++ b/tests/tests.cmake @@ -3708,6 +3708,7 @@ add_executable(firmware_close_dialog_test src/gui/RtlReceiverSettingsWidget.cpp src/gui/ControlAvailabilityRegistry.cpp src/gui/RadioSetupDialog_Peripherals.cpp + src/gui/Kxpa100AdvancedWidget.cpp src/gui/Kat500AdvancedWidget.cpp # Kat500Model links through aetherdesktop_support -> aethercore src/gui/Kpa500AdvancedWidget.cpp src/gui/PersistentDialog.cpp @@ -3735,6 +3736,7 @@ add_executable(flex_control_visibility_test src/gui/RtlReceiverSettingsWidget.cpp src/gui/ControlAvailabilityRegistry.cpp src/gui/RadioSetupDialog_Peripherals.cpp + src/gui/Kxpa100AdvancedWidget.cpp src/gui/Kat500AdvancedWidget.cpp # Kat500Model links through aetherdesktop_support -> aethercore src/gui/Kpa500AdvancedWidget.cpp src/gui/PersistentDialog.cpp @@ -3773,6 +3775,7 @@ add_executable(radio_setup_region_field_test src/gui/RtlReceiverSettingsWidget.cpp src/gui/ControlAvailabilityRegistry.cpp src/gui/RadioSetupDialog_Peripherals.cpp + src/gui/Kxpa100AdvancedWidget.cpp src/gui/Kat500AdvancedWidget.cpp # Kat500Model links through aetherdesktop_support -> aethercore src/gui/Kpa500AdvancedWidget.cpp src/gui/PersistentDialog.cpp @@ -3800,6 +3803,7 @@ add_executable(radio_setup_operations_test src/gui/RtlReceiverSettingsWidget.cpp src/gui/ControlAvailabilityRegistry.cpp src/gui/RadioSetupDialog_Peripherals.cpp + src/gui/Kxpa100AdvancedWidget.cpp src/gui/Kat500AdvancedWidget.cpp src/gui/Kpa500AdvancedWidget.cpp src/gui/PersistentDialog.cpp @@ -3832,6 +3836,7 @@ add_executable(radio_setup_label_theme_token_test src/gui/RtlReceiverSettingsWidget.cpp src/gui/ControlAvailabilityRegistry.cpp src/gui/RadioSetupDialog_Peripherals.cpp + src/gui/Kxpa100AdvancedWidget.cpp src/gui/Kat500AdvancedWidget.cpp # Kat500Model links through aetherdesktop_support -> aethercore src/gui/Kpa500AdvancedWidget.cpp src/gui/PersistentDialog.cpp @@ -3858,6 +3863,7 @@ add_executable(radio_setup_window_size_test src/gui/RtlReceiverSettingsWidget.cpp src/gui/ControlAvailabilityRegistry.cpp src/gui/RadioSetupDialog_Peripherals.cpp + src/gui/Kxpa100AdvancedWidget.cpp src/gui/Kat500AdvancedWidget.cpp # Kat500Model links through aetherdesktop_support -> aethercore src/gui/Kpa500AdvancedWidget.cpp src/gui/PersistentDialog.cpp @@ -3884,6 +3890,7 @@ add_executable(radio_setup_pair_grid_test src/gui/RtlReceiverSettingsWidget.cpp src/gui/ControlAvailabilityRegistry.cpp src/gui/RadioSetupDialog_Peripherals.cpp + src/gui/Kxpa100AdvancedWidget.cpp src/gui/Kat500AdvancedWidget.cpp # Kat500Model links through aetherdesktop_support -> aethercore src/gui/Kpa500AdvancedWidget.cpp src/gui/PersistentDialog.cpp @@ -3916,6 +3923,7 @@ add_executable(radio_setup_max_power_field_test src/gui/RtlReceiverSettingsWidget.cpp src/gui/ControlAvailabilityRegistry.cpp src/gui/RadioSetupDialog_Peripherals.cpp + src/gui/Kxpa100AdvancedWidget.cpp src/gui/Kat500AdvancedWidget.cpp # Kat500Model links through aetherdesktop_support -> aethercore src/gui/Kpa500AdvancedWidget.cpp src/gui/PersistentDialog.cpp @@ -3945,6 +3953,7 @@ add_executable(radio_setup_tx_timing_fields_test src/gui/RtlReceiverSettingsWidget.cpp src/gui/ControlAvailabilityRegistry.cpp src/gui/RadioSetupDialog_Peripherals.cpp + src/gui/Kxpa100AdvancedWidget.cpp src/gui/Kat500AdvancedWidget.cpp # Kat500Model links through aetherdesktop_support -> aethercore src/gui/Kpa500AdvancedWidget.cpp src/gui/PersistentDialog.cpp @@ -3975,6 +3984,7 @@ add_executable(radio_setup_recording_mode_dim_test src/gui/RtlReceiverSettingsWidget.cpp src/gui/ControlAvailabilityRegistry.cpp src/gui/RadioSetupDialog_Peripherals.cpp + src/gui/Kxpa100AdvancedWidget.cpp src/gui/Kat500AdvancedWidget.cpp # Kat500Model links through aetherdesktop_support -> aethercore src/gui/Kpa500AdvancedWidget.cpp src/gui/PersistentDialog.cpp @@ -4006,6 +4016,7 @@ add_executable(radio_setup_headphone_output_gate_test src/gui/RtlReceiverSettingsWidget.cpp src/gui/ControlAvailabilityRegistry.cpp src/gui/RadioSetupDialog_Peripherals.cpp + src/gui/Kxpa100AdvancedWidget.cpp src/gui/Kat500AdvancedWidget.cpp # Kat500Model links through aetherdesktop_support -> aethercore src/gui/Kpa500AdvancedWidget.cpp src/gui/PersistentDialog.cpp @@ -4035,6 +4046,7 @@ add_executable(radio_nickname_test src/gui/RadioSetupDialog_Peripherals.cpp src/gui/Kpa500AdvancedWidget.cpp src/gui/Kat500AdvancedWidget.cpp + src/gui/Kxpa100AdvancedWidget.cpp src/gui/PersistentDialog.cpp src/gui/FramelessResizer.cpp src/gui/FramelessWindowTitleBar.cpp @@ -4065,6 +4077,7 @@ add_executable(radio_setup_show_tx_waterfall_test src/gui/RtlReceiverSettingsWidget.cpp src/gui/ControlAvailabilityRegistry.cpp src/gui/RadioSetupDialog_Peripherals.cpp + src/gui/Kxpa100AdvancedWidget.cpp src/gui/Kat500AdvancedWidget.cpp # Kat500Model links through aetherdesktop_support -> aethercore src/gui/Kpa500AdvancedWidget.cpp src/gui/PersistentDialog.cpp @@ -4799,6 +4812,31 @@ target_include_directories(spe_protocol_test PRIVATE src) target_link_libraries(spe_protocol_test PRIVATE Qt6::Core) add_test(NAME spe_protocol_test COMMAND spe_protocol_test) +add_executable(kxpa100_protocol_test tests/kxpa100_protocol_test.cpp src/core/Kxpa100Protocol.cpp) +target_include_directories(kxpa100_protocol_test PRIVATE src) +target_link_libraries(kxpa100_protocol_test PRIVATE Qt6::Core) +add_test(NAME kxpa100_protocol_test COMMAND kxpa100_protocol_test) + +add_executable(kxpa100_maintenance_test tests/kxpa100_maintenance_test.cpp + src/core/Kxpa100Maintenance.cpp src/core/Kxpa100Protocol.cpp) +target_include_directories(kxpa100_maintenance_test PRIVATE src) +target_link_libraries(kxpa100_maintenance_test PRIVATE Qt6::Core) +target_compile_definitions(kxpa100_maintenance_test PRIVATE + KXPA100_FIXTURE_PATH="${CMAKE_SOURCE_DIR}/tests/fixtures/kxpa100/synthetic.KXPAConfig" + KXPA100_INVALID_FIXTURE_DIR="${CMAKE_SOURCE_DIR}/tests/fixtures/kxpa100") +add_test(NAME kxpa100_maintenance_test COMMAND kxpa100_maintenance_test) + +add_executable(kxpa100_model_test tests/kxpa100_model_test.cpp + src/models/Kxpa100Model.cpp src/models/Kxpa100Model_Maintenance.cpp src/models/Kxpa100Model.h + src/core/Kxpa100Protocol.cpp src/core/Kxpa100Maintenance.cpp) +target_include_directories(kxpa100_model_test PRIVATE src) +target_link_libraries(kxpa100_model_test PRIVATE Qt6::Core Qt6::Network Qt6::Test) +if(TARGET Qt6::SerialPort) + target_compile_definitions(kxpa100_model_test PRIVATE HAVE_SERIALPORT) + target_link_libraries(kxpa100_model_test PRIVATE Qt6::SerialPort) +endif() +add_test(NAME kxpa100_model_test COMMAND kxpa100_model_test) + add_executable(kat500_protocol_test tests/kat500_protocol_test.cpp src/core/Kat500Protocol.cpp) target_include_directories(kat500_protocol_test PRIVATE src) target_link_libraries(kat500_protocol_test PRIVATE Qt6::Core) @@ -4825,6 +4863,17 @@ if(TARGET Qt6::SerialPort) endif() add_test(NAME kat500_model_test COMMAND kat500_model_test) +add_executable(kxpa100_applet_test tests/kxpa100_applet_test.cpp + src/gui/Kxpa100Applet.cpp src/models/Kxpa100Model.cpp src/models/Kxpa100Model_Maintenance.cpp src/models/Kxpa100Model.h + src/core/Kxpa100Protocol.cpp src/core/Kxpa100Maintenance.cpp + src/gui/CanonSurface.cpp src/gui/CanonWindow.cpp src/gui/DragValuePopup.cpp + src/core/ThemeManager.cpp src/core/ThemeSeedGenerated.cpp + $ $ + $ ${THEME_TEST_RESOURCES}) +target_include_directories(kxpa100_applet_test PRIVATE src tests) +target_link_libraries(kxpa100_applet_test PRIVATE Qt6::Core Qt6::Network Qt6::Gui Qt6::Widgets Qt6::Test) +add_test(NAME kxpa100_applet_test COMMAND kxpa100_applet_test) +set_tests_properties(kxpa100_applet_test PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen") add_executable(kat500_applet_test tests/kat500_applet_test.cpp src/gui/Kat500Applet.cpp src/models/Kat500Model.cpp src/models/Kat500Model.h src/core/Kat500Protocol.cpp src/core/Kat500Maintenance.cpp @@ -4836,6 +4885,18 @@ target_include_directories(kat500_applet_test PRIVATE src tests) target_link_libraries(kat500_applet_test PRIVATE Qt6::Core Qt6::Network Qt6::Gui Qt6::Widgets Qt6::Test) add_test(NAME kat500_applet_test COMMAND kat500_applet_test) set_tests_properties(kat500_applet_test PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen") +add_executable(kxpa100_advanced_widget_test tests/kxpa100_advanced_widget_test.cpp + src/gui/Kxpa100AdvancedWidget.cpp src/models/Kxpa100Model.cpp src/models/Kxpa100Model_Maintenance.cpp src/models/Kxpa100Model.h + src/core/Kxpa100Protocol.cpp src/core/Kxpa100Maintenance.cpp + src/core/ThemeManager.cpp src/core/ThemeSeedGenerated.cpp + $ $ + $ ${THEME_TEST_RESOURCES}) +target_include_directories(kxpa100_advanced_widget_test PRIVATE src tests) +target_link_libraries(kxpa100_advanced_widget_test PRIVATE Qt6::Core Qt6::Network Qt6::Widgets Qt6::Test) +add_test(NAME kxpa100_advanced_widget_test COMMAND kxpa100_advanced_widget_test) +set_tests_properties(kxpa100_advanced_widget_test PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen") + +target_compile_definitions(kxpa100_advanced_widget_test PRIVATE KXPA100_FIXTURE_PATH="${CMAKE_SOURCE_DIR}/tests/fixtures/kxpa100/synthetic.KXPAConfig") add_executable(kat500_advanced_widget_test tests/kat500_advanced_widget_test.cpp src/gui/Kat500AdvancedWidget.cpp src/models/Kat500Model.cpp src/models/Kat500Model.h src/core/Kat500Protocol.cpp src/core/Kat500Maintenance.cpp @@ -5393,6 +5454,7 @@ add_executable(peripheral_auth_dialog_test src/gui/RtlReceiverSettingsWidget.cpp src/gui/ControlAvailabilityRegistry.cpp src/gui/RadioSetupDialog_Peripherals.cpp + src/gui/Kxpa100AdvancedWidget.cpp src/gui/Kat500AdvancedWidget.cpp # Kat500Model links through aetherdesktop_support -> aethercore src/gui/Kpa500AdvancedWidget.cpp src/gui/AntennaGeniusApplet.cpp @@ -8876,6 +8938,14 @@ set_tests_properties(gui_nested_lifetime_test PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen") # Injected serial transport and backend only: no sockets, serial ports or RF. +add_executable(kxpa100_tx_controls_test tests/kxpa100_tx_controls_test.cpp + src/gui/TxApplet.cpp src/gui/AtuPreTuneDialog.cpp src/gui/DragValuePopup.cpp + src/gui/FramelessResizer.cpp src/gui/FramelessWindowTitleBar.cpp) +target_include_directories(kxpa100_tx_controls_test PRIVATE src tests) +target_link_libraries(kxpa100_tx_controls_test PRIVATE aethercore Qt6::Widgets Qt6::Test) +add_test(NAME kxpa100_tx_controls_test COMMAND kxpa100_tx_controls_test) +set_tests_properties(kxpa100_tx_controls_test PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen") + add_executable(kat500_tx_controls_test tests/kat500_tx_controls_test.cpp src/gui/TxApplet.cpp src/gui/AtuPreTuneDialog.cpp src/gui/DragValuePopup.cpp src/gui/FramelessResizer.cpp src/gui/FramelessWindowTitleBar.cpp) @@ -9236,7 +9306,9 @@ set(AETHER_SETTINGS_CONSUMERS canon_window_test canon_surface_test kpa500_applet_test + kxpa100_applet_test kat500_applet_test + kxpa100_advanced_widget_test kat500_advanced_widget_test panadapter_header_test theme_seed_test