diff --git a/App.xaml.cs b/App.xaml.cs index b07a4d5..e6776ae 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -118,6 +118,9 @@ private static void RegisterServices(IServiceCollection services, SettingsServic services.AddSingleton(); services.AddSingleton(); services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); + services.AddTransient(); services.AddTransient(); // ViewModels diff --git a/Assets/port_skip_patterns.json b/Assets/port_skip_patterns.json new file mode 100644 index 0000000..8505bfb --- /dev/null +++ b/Assets/port_skip_patterns.json @@ -0,0 +1,16 @@ +[ + { + "_comment": "RigCheck by ShackDesk — COM ports whose name contains any of these (case-insensitive) are classified as that DeviceClass and never opened by Find my radio. Stray bytes to a rotator, amplifier, or antenna controller are a real-world hazard, and GPS pucks and Bluetooth links are never a radio CAT port. The operator can still tick such a port by hand. The same classes will drive future device-specific discovery (rotators, GPS, loggers) with their own protocol families. Add entries by pull request — see CONTRIBUTING.md." + }, + { "Pattern": "rotor", "DeviceClass": "rotator" }, + { "Pattern": "rotator", "DeviceClass": "rotator" }, + { "Pattern": "GS-232", "DeviceClass": "rotator" }, + { "Pattern": "amplifier", "DeviceClass": "amplifier" }, + { "Pattern": "SPE Expert", "DeviceClass": "amplifier" }, + { "Pattern": "SteppIR", "DeviceClass": "antenna" }, + { "Pattern": "GPS", "DeviceClass": "gps" }, + { "Pattern": "u-blox", "DeviceClass": "gps" }, + { "Pattern": "NMEA", "DeviceClass": "gps" }, + { "Pattern": "Garmin", "DeviceClass": "gps" }, + { "Pattern": "Bluetooth", "DeviceClass": "bluetooth" } +] diff --git a/Assets/rig_families.json b/Assets/rig_families.json new file mode 100644 index 0000000..eb957b5 --- /dev/null +++ b/Assets/rig_families.json @@ -0,0 +1,50 @@ +[ + { + "_comment": "RigCheck by ShackDesk — protocol families for Find my radio. Each family names ONE probe operation from RigCheck's fixed, compiled, query-only set (KenwoodId, IcomReadId, IcomReadFreq, YaesuLegacyReadFreq) and the baud rates to sweep, most likely first. This file can never contain command bytes; it only chooses which built-in query to send. Weight orders families when nothing else hints; HamlibBackend is the thousands digit of the Hamlib model number (1 Yaesu, 2 Kenwood/Elecraft, 3 Icom). HandoffStopBits is what the operator should enter in WSJT-X — the probe itself always uses 8N1. Validate at jsonlint.com before submitting." + }, + { + "Id": "Kenwood", + "Name": "Kenwood / Elecraft", + "Probe": "KenwoodId", + "Bauds": [ 115200, 38400, 9600, 4800 ], + "HamlibBackends": [ 2 ], + "VendorHints": [ "Kenwood", "Elecraft" ], + "HandoffStopBits": 1, + "DefaultModelId": 2037, + "Weight": 0.8 + }, + { + "Id": "Icom", + "Name": "Icom CI-V", + "Probe": "IcomReadId", + "FallbackProbe": "IcomReadFreq", + "Bauds": [ 19200, 9600, 115200, 4800 ], + "HamlibBackends": [ 3 ], + "VendorHints": [ "Icom" ], + "HandoffStopBits": 1, + "DefaultModelId": 3073, + "Weight": 1.0 + }, + { + "Id": "YaesuCat", + "Name": "Yaesu CAT", + "Probe": "KenwoodId", + "Bauds": [ 38400, 4800, 9600, 19200 ], + "HamlibBackends": [ 1 ], + "VendorHints": [ "Yaesu" ], + "HandoffStopBits": 2, + "DefaultModelId": 1035, + "Weight": 0.9 + }, + { + "Id": "YaesuLegacy", + "Name": "Yaesu FT-817 / 857 / 897 (5-byte)", + "Probe": "YaesuLegacyReadFreq", + "Bauds": [ 4800, 9600, 38400 ], + "HamlibBackends": [ 1 ], + "VendorHints": [ "Yaesu" ], + "HandoffStopBits": 2, + "DefaultModelId": 1020, + "Weight": 0.4 + } +] diff --git a/Assets/rig_ids.json b/Assets/rig_ids.json new file mode 100644 index 0000000..be058fd --- /dev/null +++ b/Assets/rig_ids.json @@ -0,0 +1,43 @@ +[ + { + "_comment": "RigCheck by ShackDesk — how a radio names itself, mapped to its Hamlib model number. Kenwood, Elecraft, and Yaesu CAT rigs answer 'ID;' with 'IDnnn;'. Icom rigs answer a CI-V read-ID with their own CI-V address (two hex digits); Reply holds that address. Where several rigs share one reply (Elecraft K3 / KX3 / KX2 all answer ID017;), the first entry is the default and the others are listed so a selected preset can override. Model numbers follow Hamlib's riglist.h: thousands = backend (1 Yaesu, 2 Kenwood/Elecraft, 3 Icom). Validate at jsonlint.com; corrections and additions welcome — see CONTRIBUTING.md." + }, + + { "Family": "Kenwood", "Reply": "ID019;", "Name": "Kenwood TS-2000", "HamlibModelId": 2014 }, + { "Family": "Kenwood", "Reply": "ID020;", "Name": "Kenwood TS-480", "HamlibModelId": 2028 }, + { "Family": "Kenwood", "Reply": "ID021;", "Name": "Kenwood TS-590S", "HamlibModelId": 2031 }, + { "Family": "Kenwood", "Reply": "ID023;", "Name": "Kenwood TS-590SG", "HamlibModelId": 2037 }, + { "Family": "Kenwood", "Reply": "ID024;", "Name": "Kenwood TS-890S", "HamlibModelId": 2041 }, + + { "Family": "Kenwood", "Reply": "ID017;", "Name": "Elecraft K3", "HamlibModelId": 2029 }, + { "Family": "Kenwood", "Reply": "ID017;", "Name": "Elecraft KX3", "HamlibModelId": 2045 }, + { "Family": "Kenwood", "Reply": "ID017;", "Name": "Elecraft KX2", "HamlibModelId": 2044 }, + + { "Family": "YaesuCat", "Reply": "ID0251;", "Name": "Yaesu FT-2000", "HamlibModelId": 1029 }, + { "Family": "YaesuCat", "Reply": "ID0241;", "Name": "Yaesu FT-450", "HamlibModelId": 1033 }, + { "Family": "YaesuCat", "Reply": "ID0310;", "Name": "Yaesu FT-950", "HamlibModelId": 1034 }, + { "Family": "YaesuCat", "Reply": "ID0362;", "Name": "Yaesu FTDX5000", "HamlibModelId": 1032 }, + { "Family": "YaesuCat", "Reply": "ID0462;", "Name": "Yaesu FTDX3000", "HamlibModelId": 1037 }, + { "Family": "YaesuCat", "Reply": "ID0583;", "Name": "Yaesu FTDX1200", "HamlibModelId": 1038 }, + { "Family": "YaesuCat", "Reply": "ID0570;", "Name": "Yaesu FT-991", "HamlibModelId": 1035 }, + { "Family": "YaesuCat", "Reply": "ID0670;", "Name": "Yaesu FT-991A", "HamlibModelId": 1035 }, + { "Family": "YaesuCat", "Reply": "ID0650;", "Name": "Yaesu FT-891", "HamlibModelId": 1036 }, + { "Family": "YaesuCat", "Reply": "ID0681;", "Name": "Yaesu FTDX101D", "HamlibModelId": 1040 }, + { "Family": "YaesuCat", "Reply": "ID0682;", "Name": "Yaesu FTDX101MP", "HamlibModelId": 1041 }, + { "Family": "YaesuCat", "Reply": "ID0761;", "Name": "Yaesu FTDX10", "HamlibModelId": 1042 }, + + { "Family": "Icom", "Reply": "58", "Name": "Icom IC-706MkIIG", "HamlibModelId": 3020 }, + { "Family": "Icom", "Reply": "5E", "Name": "Icom IC-718", "HamlibModelId": 3021 }, + { "Family": "Icom", "Reply": "6A", "Name": "Icom IC-7800", "HamlibModelId": 3037 }, + { "Family": "Icom", "Reply": "74", "Name": "Icom IC-7700", "HamlibModelId": 3062 }, + { "Family": "Icom", "Reply": "76", "Name": "Icom IC-7200", "HamlibModelId": 3061 }, + { "Family": "Icom", "Reply": "7A", "Name": "Icom IC-7600", "HamlibModelId": 3049 }, + { "Family": "Icom", "Reply": "7C", "Name": "Icom IC-9100", "HamlibModelId": 3068 }, + { "Family": "Icom", "Reply": "80", "Name": "Icom IC-7410", "HamlibModelId": 3067 }, + { "Family": "Icom", "Reply": "88", "Name": "Icom IC-7100", "HamlibModelId": 3070 }, + { "Family": "Icom", "Reply": "8E", "Name": "Icom IC-7851", "HamlibModelId": 3075 }, + { "Family": "Icom", "Reply": "94", "Name": "Icom IC-7300", "HamlibModelId": 3073 }, + { "Family": "Icom", "Reply": "98", "Name": "Icom IC-7610", "HamlibModelId": 3078 }, + { "Family": "Icom", "Reply": "A2", "Name": "Icom IC-9700", "HamlibModelId": 3081 }, + { "Family": "Icom", "Reply": "A4", "Name": "Icom IC-705", "HamlibModelId": 3085 } +] diff --git a/BrandingInfo.cs b/BrandingInfo.cs index 2690bde..4fd6021 100644 --- a/BrandingInfo.cs +++ b/BrandingInfo.cs @@ -12,7 +12,7 @@ public static class BrandingInfo public const string AppName = "RigCheck"; public const string SuiteName = "ShackDesk"; public const string FullName = "RigCheck by ShackDesk"; - public const string Version = "0.6.6"; + public const string Version = "0.6.8"; public const string Tagline = "Know your rig is ready"; // ── Developer / publisher ──────────────────────────────────── diff --git a/CHANGELOG.md b/CHANGELOG.md index 3484d23..25870c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,47 @@ All notable changes to RigCheck are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version numbers: patch (x.x.1) for landed features and fixes, minor (x.1.0) for milestones. +## [0.6.8] - 2026-09-21 + +Find my radio — the second half of the 0.7.0 milestone, first cut. + +### Added +- **Find my radio** button: tick the COM ports it may open, and it sweeps each with read-only + identify and read-frequency queries (Kenwood/Elecraft/Yaesu `ID;`, Icom CI-V, Yaesu 5-byte) + across the likely baud rates, stops on the first radio that answers, verifies it with the + normal Hamlib test suite, and fills in the Connection panel. rigctld already listening is + reported as a working connection; Flrig is noted +- Every byte sent and received is shown in the results panel with a Copy button, and goes into + Copy Results and Export Log under a "Find my radio transcript" heading, so the exchange can be + replayed in a terminal program +- Data files anyone can extend by pull request: `rig_families.json` (which built-in query and + which baud rates per protocol family), `rig_ids.json` (how each radio names itself → Hamlib + model), `port_skip_patterns.json` (ports classed as rotator, amplifier, GPS, or Bluetooth start + unticked). Data files choose a query by name; they can never contain command bytes +- Design document: `docs/discovery-flow.md` (with the diagram) and `docs/discovery-flow.drawio` +- Telemetry event `discovery` (ports swept, families/models/bauds that answered, verified count) + +### Safety +- RTS and DTR are never asserted during discovery — on many interfaces they are the PTT line +- The sweep only runs from the button, only on ticked ports, and sends read commands only + +## [0.6.7] - 2026-09-21 + +First half of the 0.7.0 milestone: the PC-side checks. + +### Added +- **Scan PC** button: ten read-only checks that need no radio — Windows version, every Hamlib + copy found and which one is used, `rigctl --version`, rigctl on PATH (with the `setx` fix + shown), the selected radio model with a clear warning if it is Hamlib's dummy rig, serial + driver problem codes, whether anything listens on the rigctld and Flrig ports, rigctld startup + entries, Windows Firewall rules for rigctld, and installed radio software +- Scan results use the same results panel, Copy Results, and Export Log as the connection tests +- Completion notification also fires when a scan finishes + +### Changed +- The results panel shows pending placeholders only for the checks about to run, instead of + every test RigCheck knows about + ## [0.6.6] - 2026-09-21 Results you can select and copy; a heads-up when a run finishes. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 89a9f14..690d315 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,10 +16,35 @@ Thank you for your interest in contributing to RigCheck by ShackDesk. | ------ | ----- | | Bug report | Open an issue with your exported RigCheck log attached | | Feature request | Open an issue describing the problem you are trying to solve | -| Radio or cable database entry | Open an issue or submit a PR editing the data files under `Assets/` | +| Radio, cable, or protocol data | Edit a JSON file under `Assets/` and submit a PR — see [Data files](#data-files); no compiler needed | | Translation | See [TRANSLATING.md](TRANSLATING.md) — no programming needed | | Code | Fork, branch, and submit a pull request (see below) | +## Data files + +Most of what RigCheck knows about radios lives in JSON, not code, so anyone can +add a rig by pull request. Validate at jsonlint.com before submitting; each file +starts with a `_comment` entry that explains its fields. + +| File | What it holds | +| ------ | ----- | +| `Assets/radio_presets.json` | Quick-start presets: Hamlib model, default serial settings, popularity (sort order) | +| `Assets/usb_devices.json` | USB VID/PID → serial chip, cable hint, radio family, suggested presets | +| `Assets/rig_families.json` | Protocol families for Find my radio: which built-in query to send, baud rates to sweep (most likely first), default model | +| `Assets/rig_ids.json` | How a radio names itself (`ID023;`, CI-V address `94`) → Hamlib model number | +| `Assets/port_skip_patterns.json` | Port-name patterns classed as rotator, amplifier, antenna, GPS, or Bluetooth — never opened by Find my radio | + +Hamlib model numbers follow `riglist.h` in the Hamlib source: the thousands +digit is the backend (1 Yaesu, 2 Kenwood and Elecraft, 3 Icom). Please say +where a value came from (the radio's manual, `rigctl -l`, a real exchange) in +the PR. + +**One rule that will not bend:** data files select a probe operation by name +from the fixed set compiled into RigCheck. A PR that adds command bytes, a new +field carrying bytes, or a "custom command" mechanism to any data file will be +declined. A data file that could make RigCheck send arbitrary bytes at a +transmitter is a safety problem, not a feature. + ## CLA All contributors must agree to the [Contributor License Agreement](CLA.md) before a pull request can be merged. By submitting a PR, you agree to its terms. diff --git a/Models/Models.cs b/Models/Models.cs index e76fa36..cc4ac42 100644 --- a/Models/Models.cs +++ b/Models/Models.cs @@ -29,6 +29,7 @@ public record ConnectionConfig public enum TestId { + // Connection tests — need a radio OpenConnection, GetFrequency, GetMode, @@ -36,6 +37,18 @@ public enum TestId GetSmeter, GetVfo, SetFrequency, + + // Environment checks — user-initiated Scan, no radio needed + EnvWindows, + EnvHamlib, + EnvRigctlVersion, + EnvRigctlPath, + EnvRadioModel, + EnvSerialDrivers, + EnvRigctld, + EnvAutostart, + EnvFirewall, + EnvRadioApps, } public enum TestStatus @@ -181,3 +194,85 @@ public record DiagnosticResult( public bool HasHelpTopics => HelpTopics.Length > 0; public bool IsOk => string.IsNullOrEmpty(Summary); } + +// ── Results transcript ──────────────────────────────────────────────────── +// Free-form lines that sit between test results in the results panel: the +// commands and raw serial exchanges of a discovery run, so a technical user +// can replicate them with a terminal program. + +public enum TranscriptKind +{ + /// A rigctl / Hamlib command line as it would be typed. + Command, + /// Bytes written to the serial port (hex, with ASCII where printable). + SerialTx, + /// Bytes read back from the serial port. + SerialRx, + /// A reply from rigctl or the radio, already decoded. + Response, + /// Progress line while probing ("Trying COM3 at 9600…"). + Trying, + /// Something found — a rig, a running rigctld. + Found, + /// Anything else worth showing in muted text. + Note, +} + +public record TranscriptLine(TranscriptKind Kind, string Text); + +// ── Discovery (Find my radio) ───────────────────────────────────────────── +// Declarative rig knowledge loaded from rig_families.json, rig_ids.json, +// and port_skip_patterns.json. The data can only name a probe operation +// from RigCheck's fixed set; it never carries command bytes. + +/// A protocol family and how to probe for it. +public record RigFamily +{ + public string Id { get; init; } = string.Empty; + public string Name { get; init; } = string.Empty; + public string Probe { get; init; } = string.Empty; // ProbeOperation name + public string? FallbackProbe { get; init; } // tried when Probe gets no reply + public int[] Bauds { get; init; } = []; + public int[] HamlibBackends { get; init; } = []; // thousands digit of model numbers + public string[] VendorHints { get; init; } = []; // matched against cable RadioFamily + public int HandoffStopBits { get; init; } = 1; + public int DefaultModelId { get; init; } // when the rig confirms the family but not itself + public double Weight { get; init; } = 0.5; +} + +/// How a rig names itself, mapped to a Hamlib model. +public record RigIdEntry +{ + public string Family { get; init; } = string.Empty; + public string Reply { get; init; } = string.Empty; // "ID023;" or CI-V address "94" + public string Name { get; init; } = string.Empty; + public int HamlibModelId { get; init; } +} + +/// A port-name pattern discovery must never open. +public record PortSkipPattern +{ + public string Pattern { get; init; } = string.Empty; + public string DeviceClass { get; init; } = string.Empty; // rotator, amplifier, antenna, gps, bluetooth +} + +/// A working connection discovery found and verified. +public record DiscoveredRig( + string Port, // "COM3", or "localhost:4532" for rigctld + int Baud, // 0 for network transports + string FamilyId, + int HamlibModelId, + string ModelName, + double Score, // 1.0 = rig named itself, 0.8 = frequency reply in a ham band + int HandoffStopBits, + bool UseRigctld = false); + +public enum ProbeEventKind { Trying, Sent, Received, Response, Skipped, InUse, Found, PortDone, Note } + +/// One step of a discovery run, emitted as it happens. +public record ProbeEvent( + ProbeEventKind Kind, + string Port, + string Message, + string? Bytes = null, // hex dump of what was sent or received + DiscoveredRig? Rig = null); diff --git a/README.md b/README.md index 6278712..83fbbc0 100644 --- a/README.md +++ b/README.md @@ -139,20 +139,64 @@ Target users: ## Core Features ### Connection Configuration -- Radio model selector (searchable dropdown - populated from Hamlib rig database) -- COM port selector (populated from system, - inherits detection logic from PortPane if installed) -- Baud rate selector: 4800/9600/19200/38400/ - 57600/115200 -- Data bits, parity, stop bits (default 8N1) -- Flow control: None/Hardware/Software -- PTT method: CAT/RTS/DTR/VOX/None -- Network mode: rigctld host:port (default - localhost:4532) -- Connection type toggle: Direct serial vs - rigctld network -- Detect and test GPS (USB serial receivers, puck) +- Quick-start preset picker for popular radios (`Assets/radio_presets.json`), which fills + the Hamlib model and serial defaults; every field can be overridden afterwards +- COM port list from Plug and Play, with the USB chip identified by VID/PID and a cable hint + when it is a known radio interface (`Assets/usb_devices.json`) +- Baud rate: Radio default, or 1200 through 115200 +- Data bits, parity, stop bits, flow control, and PTT method, each defaulting to + "Radio default" so Hamlib's own model database decides +- Direct serial or rigctld network mode (host:port, default localhost:4532) +- Nothing is chosen for you: no port is auto-selected and the Hamlib dummy rig (model 1) + is never a valid selection, so a passing result always means a real radio answered + +### PC Scan (no radio needed) +The **Scan PC** button checks the computer side before any radio is +involved, so "Run Tests is greyed out — why?" has an answer: + +- Windows version and architecture +- Every copy of Hamlib found (WSJT-X, Fldigi, standalone, PATH) and which one RigCheck uses +- `rigctl --version` actually runs +- Whether `rigctl` is on PATH — if not, the commands RigCheck shows will not work in a plain + command window, and the exact `setx` line to fix it is shown +- The selected radio model, called out unmistakably when it is Hamlib's dummy rig (model 1) +- Serial port drivers: ports present, plus any Plug and Play device with a problem code + (28 = no driver, 10 = cannot start, 22 = disabled) +- Whether anything is listening on the rigctld port (and Flrig's), and whether that matches + the selected connection mode +- rigctld startup entries and Windows Firewall rules for it — a Block rule from a dismissed + prompt is a classic +- Installed radio software (WSJT-X, Fldigi, JS8Call, Flrig, Winlink Express) + +Everything is read-only. RigCheck never changes PATH, firewall rules, drivers, or startup +entries — it shows the command or the setting and leaves the change to you. The scan runs only +when you click it; nothing enumerates processes or ports at startup. + +### Find my radio +When the operator does not know the port, the speed, or even which Hamlib +model to pick, **Find my radio** works it out: + +1. The operator ticks the COM ports RigCheck may open (ports that look like a rotator, + amplifier, GPS, or Bluetooth link start unticked). +2. Each port is swept with read-only queries — `ID;` for Kenwood, Elecraft, and Yaesu CAT + rigs, CI-V read-ID and read-frequency for Icom, the five-byte read-frequency for the + FT-817 family — at the baud rates that family is likely to use, most likely first. The + radio already chosen in the Connection panel and the cable's USB chip move their family + to the front of the queue. +3. A rig that names itself is looked up in `Assets/rig_ids.json`. One that only reports a + frequency inside an amateur band is taken as its family's most likely model. +4. Every find is verified with the same Hamlib test suite as **Run Tests**, and the best + verified one is written into the Connection panel. +5. A rigctld already listening on 4532 is reported as a working connection; Flrig on 12345 + is noted. + +Everything sent and received is shown in the results panel, with a Copy button per line, and +goes into the exported log so the exchange can be replayed with a terminal program. + +Safety rules that do not have a setting: RTS and DTR are never asserted (on many interfaces they +are PTT); only read commands are ever sent; the sweep runs only from the button and only on +ticked ports. The data files (`rig_families.json`, `rig_ids.json`, `port_skip_patterns.json`) +choose a built-in query by name and cannot contain command bytes. Design: `docs/discovery-flow.md`. ### Diagnostic Test Suite Run a sequence of standard Hamlib queries and diff --git a/Resources/Strings.de.resx b/Resources/Strings.de.resx index 76e7d36..5ab924f 100644 --- a/Resources/Strings.de.resx +++ b/Resources/Strings.de.resx @@ -273,4 +273,109 @@ Fortfahren? Wenn ein Durchlauf beendet ist Taskleistenschaltfläche blinken lassen, wenn das Fenster im Hintergrund ist Einen Ton abspielen + Windows-Version + Hamlib (rigctl.exe) + rigctl läuft + rigctl im PATH + Ausgewähltes Funkgerät + Treiber der seriellen Schnittstellen + rigctld-Server + rigctld beim Systemstart + Windows-Firewall + Installierte Funksoftware + Prüfung konnte nicht ausgeführt werden: {0} + {0} {1} (Build {2}, {3}) + Nicht gefunden + Hamlib ist nicht installiert oder nicht dort, wo RigCheck sucht. + Installieren Sie WSJT-X, Fldigi oder das eigenständige Hamlib für Windows — jedes enthält rigctl.exe + RigCheck sucht in den Installationsordnern von WSJT-X, Fldigi und Hamlib sowie im PATH + Verwendet {0}: {1} + {0} Kopien gefunden — verwendet {1}: {2}. Außerdem: {3} + Übersprungen — Hamlib nicht gefunden + rigctl wurde nicht ausgeführt: {0} + rigctl.exe wurde gefunden, konnte aber nicht gestartet werden. + Möglicherweise hat der Virenschutz die Datei in Quarantäne verschoben — prüfen Sie den Verlauf der Sicherheitssoftware + Installieren Sie WSJT-X oder Hamlib neu, um die Datei wiederherzustellen + Ja — {0} + Nein — die von RigCheck angezeigten Befehle laufen in einem normalen Eingabeaufforderungsfenster nicht + Der Ordner mit rigctl.exe ist nicht im PATH. + Die hier gezeigten Befehle beginnen nur mit „rigctl“; Windows muss wissen, wo diese Datei liegt + Führen Sie den folgenden Befehl in einer Administrator-Eingabeaufforderung aus und öffnen Sie danach ein neues Fenster + {0} (Hamlib-Modell {1}) + Kein Funkgerät gewählt — wählen Sie eines unter Verbindung + Hamlib-Dummy-Gerät (Modell 1) — eine Simulation, nicht Ihr Funkgerät + Modell 1 ist das eingebaute Testgerät von Hamlib. Jeder Befehl gelingt, ohne dass ein Funkgerät angeschlossen ist. + Wählen Sie Ihr tatsächliches Funkgerät aus der Liste, bevor Sie einem Ergebnis vertrauen + {0} Schnittstelle(n), keine Treiberprobleme: {1} + Windows sieht keine COM-Schnittstellen + Es ist keine serielle Schnittstelle vorhanden. + Stecken Sie das USB-Kabel ein und prüfen Sie, ob es ein Datenkabel ist und kein reines Ladekabel + Manche Funkgeräte brauchen einen Treiber des Herstellers (Silicon Labs CP210x für die meisten USB-Anschlüsse von Icom, Yaesu und Kenwood) + {0} (Problemcode {1}) + Ein serielles Gerät hat ein Treiberproblem. + Code 28: kein Treiber installiert — installieren Sie den USB-Treiber des Herstellers + Code 10: das Gerät kann nicht gestartet werden — versuchen Sie einen anderen USB-Anschluss oder ein anderes Kabel + Code 22: das Gerät ist im Geräte-Manager deaktiviert + Lauscht auf Port {0} ({1}) + Nichts lauscht auf Port {0} — für direkte serielle Verbindung in Ordnung + Nichts lauscht auf Port {0}, aber der rigctld-Modus ist ausgewählt + rigctld läuft nicht. + Starten Sie rigctld selbst — WSJT-X und Fldigi starten es nicht für Sie + Oder wechseln Sie unter Verbindung zur direkten seriellen Verbindung + Flrig lauscht ebenfalls auf Port {0} + unbekanntes Programm + Nicht eingerichtet — normal, die meisten Funkamateure starten es von Hand + Eingerichtet: {0} + Übersprungen — Firewall-Regeln konnten nicht gelesen werden + Noch keine Regel für rigctld — Windows fragt beim ersten Lauschen; wählen Sie Zulassen + Zulassen-Regel für rigctld vorhanden + Für rigctld existiert eine Blockieren-Regel + Die Windows-Firewall blockiert rigctld. + Das kommt meist davon, dass beim ersten Start von rigctld in der Firewall-Abfrage auf Abbrechen geklickt wurde + Windows-Sicherheit › Firewall & Netzwerkschutz › Eine App durch die Firewall zulassen — rigctld für private Netzwerke aktivieren + Weder WSJT-X, Fldigi, JS8Call, Flrig noch Winlink Express gefunden + Dieser PC wird geprüft… + Prüfung abgeschlossen — {0} Punkt(e) brauchen Aufmerksamkeit + PC prüfen + Hamlib, Treiber, rigctld und die Firewall auf diesem PC prüfen — kein Funkgerät nötig. Läuft nur, wenn Sie darauf klicken. + Mein Funkgerät finden + Durchsucht die angehakten COM-Schnittstellen mit reinen Leseabfragen, bis ein Funkgerät antwortet, prüft es und füllt die Verbindungseinstellungen aus. Tastet nie den Sender. + Mein Funkgerät finden + Haken Sie die Schnittstellen an, die RigCheck öffnen darf. Es sendet nur Lesebefehle (Identifizieren, Frequenz lesen) mit einigen Geschwindigkeiten und hört pro Schnittstelle beim ersten antwortenden Funkgerät auf. + Keine COM-Schnittstellen gefunden. Stecken Sie das USB- oder serielle Kabel des Funkgeräts ein und versuchen Sie es erneut. + RTS und DTR werden nie gesetzt — bei vielen Interfaces sind sie die PTT-Leitung. Schnittstellen, die nach Rotor, Endstufe, GPS oder Bluetooth aussehen, sind zunächst nicht angehakt. + Sieht aus wie {0} — nicht angehakt; nur anhaken, wenn Sie sicher sind + Starten + eine Rotorsteuerung + eine Endstufe + eine Antennensteuerung + ein GPS-Empfänger + eine Bluetooth-Verbindung + Suche nach Ihrem Funkgerät… + Gefunden und geprüft: {0} an {1} — Verbindungseinstellungen ausgefüllt + {0} funktionierende Verbindungen gefunden — verwendet {1} an {2} + Ein Funkgerät hat geantwortet, aber die Hamlib-Tests sind nicht bestanden — siehe Ergebnisse + An den angehakten Schnittstellen hat kein Funkgerät geantwortet + Suche abgebrochen — sie hat zu lange gedauert + Durchsuche {0} Schnittstelle(n) + {0}: übersprungen — sieht aus wie {1} + {0}: von einem anderen Programm belegt (WSJT-X, Fldigi oder rigctld hat sie bereits — das ist der funktionierende Weg) + {0}: konnte nicht geöffnet werden — {1} + {0} mit {1} Baud — {2} + keine Antwort + es kamen Bytes zurück, aber nichts Erkennbares — vermutlich die falsche Geschwindigkeit + ein {0}-Gerät hat mit {1} Baud geantwortet, sich aber nicht identifiziert + Frequenzantwort: {0} MHz{1} + (innerhalb eines Amateurfunkbands) + {0} wird von {1} geteilt — verwendet {2} + {0} steht für {1} noch nicht in der Liste von RigCheck — verwendet die Familienvorgabe; bitte melden + {0} (Hamlib-Modell {1}) an {2} mit {3} Baud gefunden + rigctld läuft bereits auf Port {0} — das ist eine funktionierende Verbindung + Flrig läuft auf Port 12345 (Hamlib-Modell 4 verbindet darüber) + {0}: kein Funkgerät hat geantwortet + Prüfe {0} an {1} mit {2} Baud über rigctl + {0} an {1} hat die Hamlib-Tests bestanden + {0} an {1}: {2} Test(s) fehlgeschlagen + Verbindungseinstellungen auf {0} (Modell {1}), {2}, {3} Baud gesetzt — Tests ausführen verwendet diese jetzt + PROTOKOLL „MEIN FUNKGERÄT FINDEN“ diff --git a/Resources/Strings.es.resx b/Resources/Strings.es.resx index 9eb703a..18514bb 100644 --- a/Resources/Strings.es.resx +++ b/Resources/Strings.es.resx @@ -273,4 +273,109 @@ Los detalles se han guardado en el registro. Si esto sigue ocurriendo, infórmel Cuando termina una ejecución Hacer parpadear el botón de la barra de tareas si la ventana está en segundo plano Reproducir un sonido + Versión de Windows + Hamlib (rigctl.exe) + rigctl se ejecuta + rigctl en PATH + Radio seleccionada + Controladores de puerto serie + Servidor rigctld + rigctld al iniciar + Firewall de Windows + Software de radio instalado + No se pudo ejecutar la comprobación: {0} + {0} {1} (compilación {2}, {3}) + No encontrado + Hamlib no está instalado, o no está donde RigCheck lo busca. + Instale WSJT-X, Fldigi o el Hamlib independiente para Windows — cada uno incluye rigctl.exe + RigCheck busca en las carpetas de instalación de WSJT-X, Fldigi y Hamlib y en PATH + Usando {0}: {1} + {0} copias encontradas — usando {1}: {2}. También: {3} + Omitido — Hamlib no encontrado + rigctl no se ejecutó: {0} + Se encontró rigctl.exe pero no se pudo iniciar. + El antivirus puede haberlo puesto en cuarentena — revise el historial del software de seguridad + Reinstale WSJT-X o Hamlib para restaurarlo + Sí — {0} + No — los comandos que muestra RigCheck no funcionarán en una ventana de comandos normal + La carpeta que contiene rigctl.exe no está en PATH. + Los comandos mostrados aquí empiezan solo con "rigctl"; Windows necesita saber dónde está + Ejecute el comando siguiente en un símbolo del sistema como administrador y luego abra una ventana nueva + {0} (modelo Hamlib {1}) + No se ha elegido radio — elija una en Conexión + Radio ficticia de Hamlib (modelo 1) — una simulación, no su radio + El modelo 1 es la radio de prueba integrada de Hamlib. Todos los comandos tienen éxito sin ninguna radio conectada. + Elija su radio real de la lista antes de confiar en cualquier resultado + {0} puerto(s), sin problemas de controlador: {1} + Windows no ve ningún puerto COM + No hay ningún puerto serie presente. + Conecte el cable USB y compruebe que sea un cable de datos, no solo de carga + Algunas radios necesitan un controlador del fabricante (Silicon Labs CP210x para la mayoría de puertos USB de Icom, Yaesu y Kenwood) + {0} (código de problema {1}) + Un dispositivo serie tiene un problema de controlador. + Código 28: no hay controlador instalado — instale el controlador USB del fabricante + Código 10: el dispositivo no puede iniciarse — pruebe otro puerto USB u otro cable + Código 22: el dispositivo está deshabilitado en el Administrador de dispositivos + Escuchando en el puerto {0} ({1}) + Nada escucha en el puerto {0} — correcto para conexión serie directa + Nada escucha en el puerto {0}, pero está seleccionado el modo rigctld + rigctld no se está ejecutando. + Inicie rigctld usted mismo — WSJT-X y Fldigi no lo inician por usted + O cambie a serie directa en Conexión + Flrig también está escuchando en el puerto {0} + programa desconocido + No configurado — normal, la mayoría de operadores lo inician a mano + Configurado: {0} + Omitido — no se pudieron leer las reglas del firewall + Aún no hay regla para rigctld — Windows preguntará la primera vez que escuche; elija Permitir + Regla de permiso presente para rigctld + Existe una regla de bloqueo para rigctld + El Firewall de Windows está bloqueando rigctld. + Esto suele venir de pulsar Cancelar en el aviso del firewall la primera vez que se ejecutó rigctld + Seguridad de Windows › Firewall y protección de red › Permitir una aplicación a través del firewall — marque rigctld para redes privadas + No se encontró WSJT-X, Fldigi, JS8Call, Flrig ni Winlink Express + Analizando este PC… + Análisis completado — {0} elemento(s) requieren atención + Analizar PC + Comprueba Hamlib, controladores, rigctld y el firewall en este PC — no se necesita radio. Solo se ejecuta al hacer clic. + Buscar mi radio + Recorre los puertos COM marcados con consultas de solo lectura hasta que una radio responda, la verifica y rellena el panel de Conexión. Nunca activa el transmisor. + Buscar mi radio + Marque los puertos que RigCheck puede abrir. Solo envía comandos de lectura (identificar, leer frecuencia) a unas pocas velocidades y se detiene en la primera radio que responde en cada puerto. + No se encontraron puertos COM. Conecte el cable USB o serie de la radio y vuelva a intentarlo. + RTS y DTR nunca se activan — en muchas interfaces son la línea de PTT. Los puertos que parecen un rotor, un amplificador, un GPS o un enlace Bluetooth empiezan desmarcados. + Parece {0} — desmarcado; márquelo solo si está seguro + Iniciar + un controlador de rotor + un amplificador + un controlador de antena + un receptor GPS + un enlace Bluetooth + Buscando su radio… + Encontrada y verificada: {0} en {1} — panel de Conexión rellenado + {0} conexiones funcionales encontradas — usando {1} en {2} + Una radio respondió, pero las pruebas de Hamlib no pasaron — vea los resultados + Ninguna radio respondió en los puertos marcados + Buscar mi radio se detuvo — tardó demasiado + Recorriendo {0} puerto(s) + {0}: omitido — parece {1} + {0}: en uso por otro programa (WSJT-X, Fldigi o rigctld ya lo tiene — esa es la vía que funciona) + {0}: no se pudo abrir — {1} + {0} a {1} baudios — {2} + sin respuesta + llegaron bytes pero nada reconocible — probablemente la velocidad equivocada + una radio {0} respondió a {1} baudios pero no se identificó + respuesta de frecuencia: {0} MHz{1} + (dentro de una banda de aficionados) + {0} es compartido por {1} — usando {2} + {0} aún no está en la lista de RigCheck para {1} — usando el valor por defecto de la familia; por favor infórmelo + Encontrada {0} (modelo Hamlib {1}) en {2} a {3} baudios + rigctld ya se está ejecutando en el puerto {0} — esa es una conexión que funciona + Flrig se está ejecutando en el puerto 12345 (use el modelo Hamlib 4 para conectar a través de él) + {0}: ninguna radio respondió + Verificando {0} en {1} a {2} baudios con rigctl + {0} en {1} pasó las pruebas de Hamlib + {0} en {1}: {2} prueba(s) fallaron + Panel de Conexión establecido en {0} (modelo {1}), {2}, {3} baudios — Ejecutar pruebas usa estos valores ahora + TRANSCRIPCIÓN DE BUSCAR MI RADIO diff --git a/Resources/Strings.fr.resx b/Resources/Strings.fr.resx index 16d0035..d718c9b 100644 --- a/Resources/Strings.fr.resx +++ b/Resources/Strings.fr.resx @@ -273,4 +273,109 @@ Continuer ? Quand une exécution se termine Faire clignoter le bouton de la barre des tâches si la fenêtre est en arrière-plan Jouer un son + Version de Windows + Hamlib (rigctl.exe) + rigctl s'exécute + rigctl dans le PATH + Radio sélectionnée + Pilotes de port série + Serveur rigctld + rigctld au démarrage + Pare-feu Windows + Logiciels radio installés + La vérification n'a pas pu s'exécuter : {0} + {0} {1} (build {2}, {3}) + Introuvable + Hamlib n'est pas installé, ou pas là où RigCheck le cherche. + Installez WSJT-X, Fldigi ou le Hamlib autonome pour Windows — chacun inclut rigctl.exe + RigCheck cherche dans les dossiers d'installation de WSJT-X, Fldigi et Hamlib ainsi que dans le PATH + Utilise {0} : {1} + {0} copies trouvées — utilise {1} : {2}. Également : {3} + Ignoré — Hamlib introuvable + rigctl ne s'est pas exécuté : {0} + rigctl.exe a été trouvé mais n'a pas pu démarrer. + L'antivirus l'a peut-être mis en quarantaine — consultez l'historique du logiciel de sécurité + Réinstallez WSJT-X ou Hamlib pour le restaurer + Oui — {0} + Non — les commandes affichées par RigCheck ne fonctionneront pas dans une fenêtre de commandes ordinaire + Le dossier contenant rigctl.exe n'est pas dans le PATH. + Les commandes affichées ici commencent simplement par « rigctl » ; Windows doit savoir où il se trouve + Exécutez la commande ci-dessous dans une invite de commandes administrateur, puis ouvrez une nouvelle fenêtre + {0} (modèle Hamlib {1}) + Aucune radio choisie — choisissez-en une sous Connexion + Radio factice Hamlib (modèle 1) — une simulation, pas votre radio + Le modèle 1 est la radio de test intégrée à Hamlib. Chaque commande réussit sans aucune radio branchée. + Choisissez votre vraie radio dans la liste avant de vous fier à un résultat + {0} port(s), aucun problème de pilote : {1} + Windows ne voit aucun port COM + Aucun port série n'est présent. + Branchez le câble USB et vérifiez qu'il s'agit d'un câble de données, pas d'un câble de charge seule + Certaines radios ont besoin d'un pilote du fabricant (Silicon Labs CP210x pour la plupart des ports USB Icom, Yaesu et Kenwood) + {0} (code de problème {1}) + Un périphérique série a un problème de pilote. + Code 28 : aucun pilote installé — installez le pilote USB du fabricant + Code 10 : le périphérique ne peut pas démarrer — essayez un autre port USB ou un autre câble + Code 22 : le périphérique est désactivé dans le Gestionnaire de périphériques + À l'écoute sur le port {0} ({1}) + Rien n'écoute sur le port {0} — normal pour une liaison série directe + Rien n'écoute sur le port {0}, mais le mode rigctld est sélectionné + rigctld n'est pas en cours d'exécution. + Démarrez rigctld vous-même — WSJT-X et Fldigi ne le démarrent pas pour vous + Ou passez en liaison série directe sous Connexion + Flrig écoute aussi sur le port {0} + programme inconnu + Non configuré — normal, la plupart des opérateurs le lancent à la main + Configuré : {0} + Ignoré — les règles du pare-feu n'ont pas pu être lues + Pas encore de règle pour rigctld — Windows demandera la première fois qu'il écoutera ; choisissez Autoriser + Règle d'autorisation présente pour rigctld + Une règle de blocage existe pour rigctld + Le Pare-feu Windows bloque rigctld. + Cela vient généralement d'un clic sur Annuler dans l'invite du pare-feu la première fois que rigctld a été lancé + Sécurité Windows › Pare-feu et protection du réseau › Autoriser une application via le pare-feu — cochez rigctld pour les réseaux privés + Aucun de WSJT-X, Fldigi, JS8Call, Flrig ou Winlink Express trouvé + Analyse de ce PC… + Analyse terminée — {0} élément(s) demandent votre attention + Analyser le PC + Vérifie Hamlib, les pilotes, rigctld et le pare-feu sur ce PC — aucune radio nécessaire. Ne s'exécute que lorsque vous cliquez. + Trouver ma radio + Balaie les ports COM cochés avec des requêtes en lecture seule jusqu'à ce qu'une radio réponde, la vérifie et remplit le panneau Connexion. N'active jamais l'émetteur. + Trouver ma radio + Cochez les ports que RigCheck peut ouvrir. Il n'envoie que des commandes de lecture (identification, lecture de fréquence) à quelques vitesses et s'arrête à la première radio qui répond sur chaque port. + Aucun port COM trouvé. Branchez le câble USB ou série de la radio, puis réessayez. + RTS et DTR ne sont jamais activés — sur beaucoup d'interfaces, c'est la ligne PTT. Les ports qui ressemblent à un rotor, un amplificateur, un GPS ou une liaison Bluetooth sont décochés au départ. + Ressemble à {0} — décoché ; ne le cochez que si vous êtes sûr + Démarrer + un contrôleur de rotor + un amplificateur + un contrôleur d'antenne + un récepteur GPS + une liaison Bluetooth + Recherche de votre radio… + Trouvée et vérifiée : {0} sur {1} — panneau Connexion rempli + {0} connexions fonctionnelles trouvées — utilisation de {1} sur {2} + Une radio a répondu, mais les tests Hamlib n'ont pas réussi — voir les résultats + Aucune radio n'a répondu sur les ports cochés + Trouver ma radio s'est arrêté — cela a pris trop de temps + Balayage de {0} port(s) + {0} : ignoré — ressemble à {1} + {0} : utilisé par un autre programme (WSJT-X, Fldigi ou rigctld l'a déjà — c'est la voie qui fonctionne) + {0} : impossible à ouvrir — {1} + {0} à {1} bauds — {2} + pas de réponse + des octets sont revenus mais rien de reconnaissable — probablement la mauvaise vitesse + une radio {0} a répondu à {1} bauds mais ne s'est pas identifiée + réponse de fréquence : {0} MHz{1} + (dans une bande amateur) + {0} est partagé par {1} — utilisation de {2} + {0} n'est pas encore dans la liste de RigCheck pour {1} — utilisation de la valeur par défaut de la famille ; merci de le signaler + {0} (modèle Hamlib {1}) trouvée sur {2} à {3} bauds + rigctld tourne déjà sur le port {0} — c'est une connexion qui fonctionne + Flrig tourne sur le port 12345 (utilisez le modèle Hamlib 4 pour passer par lui) + {0} : aucune radio n'a répondu + Vérification de {0} sur {1} à {2} bauds avec rigctl + {0} sur {1} a réussi les tests Hamlib + {0} sur {1} : {2} test(s) échoué(s) + Panneau Connexion réglé sur {0} (modèle {1}), {2}, {3} bauds — Lancer les tests utilise ces réglages maintenant + TRANSCRIPTION DE TROUVER MA RADIO diff --git a/Resources/Strings.ja.resx b/Resources/Strings.ja.resx index d9deeca..c01581a 100644 --- a/Resources/Strings.ja.resx +++ b/Resources/Strings.ja.resx @@ -273,4 +273,109 @@ 実行が終了したとき ウィンドウが背面にある場合、タスクバーのボタンを点滅させる サウンドを再生する + Windows のバージョン + Hamlib (rigctl.exe) + rigctl の実行 + PATH 上の rigctl + 選択した無線機 + シリアルポートのドライバー + rigctld サーバー + 起動時の rigctld + Windows ファイアウォール + インストール済みの無線ソフト + チェックを実行できませんでした: {0} + {0} {1} (ビルド {2}、{3}) + 見つかりません + Hamlib がインストールされていないか、RigCheck が探す場所にありません。 + WSJT-X、Fldigi、または Windows 用の単体 Hamlib をインストールしてください — いずれにも rigctl.exe が含まれています + RigCheck は WSJT-X、Fldigi、Hamlib のインストールフォルダーと PATH を探します + 使用中: {0}: {1} + {0} 個見つかりました — 使用中: {1}: {2}。その他: {3} + スキップ — Hamlib が見つかりません + rigctl を実行できませんでした: {0} + rigctl.exe は見つかりましたが、起動できませんでした。 + ウイルス対策ソフトが隔離した可能性があります — セキュリティソフトの履歴を確認してください + WSJT-X または Hamlib を再インストールして復元してください + はい — {0} + いいえ — RigCheck が表示するコマンドは通常のコマンドウィンドウでは動作しません + rigctl.exe のあるフォルダーが PATH にありません。 + ここに表示されるコマンドは「rigctl」で始まるだけなので、Windows はその場所を知る必要があります + 下のコマンドを管理者のコマンドプロンプトで実行し、その後で新しいウィンドウを開いてください + {0} (Hamlib モデル {1}) + 無線機が選択されていません — 「接続」で選択してください + Hamlib のダミーリグ (モデル 1) — シミュレーションであり、あなたの無線機ではありません + モデル 1 は Hamlib 内蔵のテスト用リグです。無線機を接続していなくてもすべてのコマンドが成功します。 + 結果を信頼する前に、一覧から実際の無線機を選択してください + {0} 個のポート、ドライバーの問題なし: {1} + Windows に COM ポートが見えません + シリアルポートがありません。 + USB ケーブルを接続し、充電専用ではなくデータケーブルであることを確認してください + 一部の無線機にはメーカー提供のドライバーが必要です (Icom、Yaesu、Kenwood のほとんどの USB ポートには Silicon Labs CP210x) + {0} (問題コード {1}) + シリアルデバイスにドライバーの問題があります。 + コード 28: ドライバーが未インストール — メーカーの USB ドライバーをインストールしてください + コード 10: デバイスを開始できません — 別の USB ポートまたはケーブルを試してください + コード 22: デバイスマネージャーでデバイスが無効になっています + ポート {0} で待ち受け中 ({1}) + ポート {0} で待ち受けているものはありません — シリアル直結なら問題ありません + ポート {0} で待ち受けているものはありませんが、rigctld モードが選択されています + rigctld が実行されていません。 + rigctld は自分で起動してください — WSJT-X や Fldigi は起動してくれません + または「接続」でシリアル直結に切り替えてください + Flrig もポート {0} で待ち受けています + 不明なプログラム + 未設定 — 通常です。ほとんどの局は手動で起動します + 設定済み: {0} + スキップ — ファイアウォールの規則を読み取れませんでした + rigctld の規則はまだありません — 初めて待ち受けるときに Windows が確認するので「許可」を選んでください + rigctld の許可規則があります + rigctld のブロック規則が存在します + Windows ファイアウォールが rigctld をブロックしています。 + rigctld を初めて実行したときにファイアウォールの確認で「キャンセル」を押したことが原因であることがほとんどです + Windows セキュリティ › ファイアウォールとネットワーク保護 › ファイアウォールによるアプリケーションの許可 — プライベートネットワークで rigctld にチェックを入れてください + WSJT-X、Fldigi、JS8Call、Flrig、Winlink Express のいずれも見つかりません + この PC をスキャン中… + スキャン完了 — 要確認の項目: {0} 件 + PC をスキャン + この PC の Hamlib、ドライバー、rigctld、ファイアウォールを確認します — 無線機は不要です。クリックしたときだけ実行されます。 + 無線機を探す + チェックした COM ポートを読み取り専用の問い合わせで走査し、無線機が応答したら検証して「接続」パネルに設定を入れます。送信機を送信状態にすることはありません。 + 無線機を探す + RigCheck が開いてよいポートにチェックを入れてください。送るのは読み取りコマンド(識別、周波数の読み取り)だけで、数種類の速度を試し、各ポートで最初に応答した無線機で止まります。 + COM ポートが見つかりません。無線機の USB または シリアルケーブルを接続して、もう一度お試しください。 + RTS と DTR は決して有効にしません — 多くのインターフェースではこれが PTT の線だからです。ローテーター、アンプ、GPS、Bluetooth に見えるポートは最初はチェックが外れています。 + {0}のようです — チェック解除中。確実な場合だけチェックしてください + 開始 + ローテーターコントローラー + アンプ + アンテナコントローラー + GPS 受信機 + Bluetooth 接続 + 無線機を探しています… + 発見・検証済み: {1} の {0} — 「接続」パネルに設定しました + 動作する接続が {0} 件見つかりました — {2} の {1} を使用します + 無線機は応答しましたが、Hamlib のテストに合格しませんでした — 結果をご覧ください + チェックしたポートでは無線機が応答しませんでした + 無線機の検索を中止しました — 時間がかかりすぎました + {0} 個のポートを走査中 + {0}: スキップ — {1}のようです + {0}: 他のプログラムが使用中です(WSJT-X、Fldigi、rigctld がすでに使っています — それが動作している経路です) + {0}: 開けませんでした — {1} + {0}、{1} ボー — {2} + 応答なし + バイトは返ってきましたが認識できません — おそらく速度が違います + {0} の無線機が {1} ボーで応答しましたが、機種名は返しませんでした + 周波数の応答: {0} MHz{1} + (アマチュアバンド内) + {0} は {1} で共通です — {2} を使用します + {0} は {1} の RigCheck の一覧にまだありません — ファミリーの既定値を使用します。報告をお願いします + {2} の {3} ボーで {0}(Hamlib モデル {1})を発見 + rigctld がすでにポート {0} で動作しています — これは動作する接続です + Flrig がポート 12345 で動作しています(経由するには Hamlib モデル 4 を使用) + {0}: 無線機の応答なし + rigctl で {1} の {0}({2} ボー)を検証中 + {1} の {0} は Hamlib のテストに合格しました + {1} の {0}: {2} 件のテストが失敗 + 「接続」パネルを {0}(モデル {1})、{2}、{3} ボーに設定しました — 「テストを実行」はこの設定を使います + 「無線機を探す」の記録 diff --git a/Resources/Strings.resx b/Resources/Strings.resx index 2380703..e286e78 100644 --- a/Resources/Strings.resx +++ b/Resources/Strings.resx @@ -888,4 +888,329 @@ Continue? Play a sound + + Windows version + + + Hamlib (rigctl.exe) + + + rigctl runs + + + rigctl on PATH + + + Selected radio + + + Serial port drivers + + + rigctld server + + + rigctld at startup + + + Windows Firewall + + + Radio software installed + + + Check could not run: {0} + + + {0} {1} (build {2}, {3}) + Placeholders: {0} product name, {1} release such as 24H2, {2} build number, {3} architecture. + + + Not found + + + Hamlib is not installed, or not where RigCheck looks. + + + Install WSJT-X, Fldigi, or the standalone Hamlib for Windows — each includes rigctl.exe + + + RigCheck looks in the WSJT-X, Fldigi, and Hamlib install folders and on PATH + + + Using {0}: {1} + + + {0} copies found — using {1}: {2}. Also: {3} + Placeholders: {0} count, {1} where the used copy came from, {2} its path, {3} list of the other copies. + + + Skipped — Hamlib not found + + + rigctl did not run: {0} + + + rigctl.exe was found but could not be started. + + + Antivirus may have quarantined it — check the security software's history + + + Reinstall WSJT-X or Hamlib to restore it + + + Yes — {0} + + + No — the commands RigCheck shows will not run in a plain command window + + + The folder containing rigctl.exe is not on PATH. + + + The commands shown here start with just "rigctl"; Windows needs to know where that is + + + Run the command below in an Administrator command prompt, then open a new window + + + {0} (Hamlib model {1}) + + + No radio chosen — pick one under Connection + + + Hamlib dummy rig (model 1) — a simulation, not your radio + + + Model 1 is Hamlib's built-in test rig. Every command succeeds without any radio attached. + + + Choose your real radio from the list before trusting any result + + + {0} port(s), no driver problems: {1} + + + Windows sees no COM ports + + + No serial port is present. + + + Plug in the USB cable and check it is a data cable, not a charge-only one + + + Some radios need a driver from the maker (Silicon Labs CP210x for most Icom, Yaesu, and Kenwood USB ports) + + + {0} (problem code {1}) + + + A serial device has a driver problem. + + + Code 28: no driver installed — install the maker's USB driver + + + Code 10: the device cannot start — try another USB port or cable + + + Code 22: the device is disabled in Device Manager + + + Listening on port {0} ({1}) + + + Nothing listening on port {0} — fine for direct serial + Shown when nothing listens on the rigctld port and direct serial is selected — this is the normal case. + + + Nothing listening on port {0}, but rigctld mode is selected + + + rigctld is not running. + + + Start rigctld yourself — WSJT-X and Fldigi do not start it for you + + + Or switch to direct serial under Connection + + + Flrig is also listening on port {0} + + + unknown program + + + Not configured — normal, most operators start it by hand + + + Configured: {0} + + + Skipped — firewall rules could not be read + + + No rule for rigctld yet — Windows will ask the first time it listens; choose Allow + + + Allow rule present for rigctld + + + A Block rule exists for rigctld + + + Windows Firewall is blocking rigctld. + + + This usually comes from clicking Cancel on the firewall prompt the first time rigctld ran + + + Windows Security › Firewall & network protection › Allow an app through firewall — tick rigctld for Private networks + Path through the Windows Security app; use the exact menu names Windows shows in your language. + + + None of WSJT-X, Fldigi, JS8Call, Flrig, or Winlink Express found + + + Scanning this PC… + + + Scan complete — {0} item(s) need attention + + + Scan PC + Button. Runs the PC-side checks (no radio). Keep short. + + + Check Hamlib, drivers, rigctld, and the firewall on this PC — no radio needed. Runs only when you click it. + + + Find my radio + Button. Starts the port sweep. Keep short. + + + Sweep the COM ports you tick with read-only queries until a radio answers, then verify it and fill in the Connection panel. Never keys the transmitter. + + + Find my radio + + + Tick the ports RigCheck may open. It sends only read commands (identify, read frequency) at a few speeds and stops on the first radio that answers on each port. + + + No COM ports were found. Plug in the radio's USB or serial cable, then try again. + + + RTS and DTR are never raised — on many interfaces they are the PTT line. Ports that look like a rotator, amplifier, GPS, or Bluetooth link start unticked. + + + Looks like a {0} — unticked; tick it only if you are sure + Placeholder {0} is a device class such as "rotator controller" (DeviceClass_* keys). + + + Start + + + rotator controller + + + amplifier + + + antenna controller + + + GPS receiver + + + Bluetooth link + + + Looking for your radio… + + + Found and verified: {0} on {1} — Connection panel filled in + + + {0} working connections found — using {1} on {2} + + + A radio answered, but the Hamlib tests did not pass — see the results + + + No radio answered on the ticked ports + + + Find my radio stopped — it took too long + + + Sweeping {0} port(s) + + + {0}: skipped — looks like a {1} + + + {0}: in use by another program (WSJT-X, Fldigi, or rigctld already has it — that is the working path) + + + {0}: could not open — {1} + + + {0} at {1} baud — {2} + Placeholders: {0} port name, {1} baud rate, {2} protocol family name. + + + no reply + + + bytes came back but nothing recognisable — probably the wrong speed + + + a {0} rig answered at {1} baud but did not name itself + + + frequency reply: {0} MHz{1} + + + (inside an amateur band) + Suffix appended to the frequency reply line, e.g. "14.074 MHz (inside an amateur band)". + + + {0} is shared by {1} — using {2} + + + {0} is not in RigCheck's list yet for {1} — using the family default; please report it + + + Found {0} (Hamlib model {1}) on {2} at {3} baud + Placeholders: {0} radio name, {1} Hamlib model number, {2} port, {3} baud. + + + rigctld is already running on port {0} — that is a working connection + + + Flrig is running on port 12345 (use Hamlib model 4 to connect through it) + + + {0}: no radio answered + + + Verifying {0} on {1} at {2} baud with rigctl + + + {0} on {1} passed the Hamlib tests + + + {0} on {1}: {2} test(s) failed + + + Connection panel set to {0} (model {1}), {2}, {3} baud — Run Tests uses these now + + + FIND MY RADIO TRANSCRIPT + diff --git a/RigCheck.csproj b/RigCheck.csproj index 238111d..b3c5b3d 100644 --- a/RigCheck.csproj +++ b/RigCheck.csproj @@ -35,9 +35,9 @@ RigCheck Assets\rigcheck.ico app.manifest - 0.6.6 - 0.6.6.0 - 0.6.6.0 + 0.6.8 + 0.6.8.0 + 0.6.8.0 My Computer Guru LLC Copyright © 2025-2026 My Computer Guru LLC RigCheck by ShackDesk diff --git a/Services/DiscoveryDataService.cs b/Services/DiscoveryDataService.cs new file mode 100644 index 0000000..6ffb0ce --- /dev/null +++ b/Services/DiscoveryDataService.cs @@ -0,0 +1,71 @@ +using Newtonsoft.Json; +using RigCheck.Models; +using Serilog; + +namespace RigCheck.Services; + +/// +/// Loads the declarative rig knowledge Find my radio runs on: +/// rig_families.json, rig_ids.json, and port_skip_patterns.json, each via +/// DataFileLocator so an external copy beside the exe overrides the +/// embedded one. Entries with a _comment key are documentation and are +/// dropped on load. +/// +public class DiscoveryDataService +{ + public IReadOnlyList Families { get; } + public IReadOnlyList Ids { get; } + public IReadOnlyList SkipPatterns { get; } + + public DiscoveryDataService() + { + Families = Load("rig_families.json", f => !string.IsNullOrEmpty(f.Id) && ProbeOperations.TryParse(f.Probe, out _)); + Ids = Load("rig_ids.json", i => !string.IsNullOrEmpty(i.Reply) && i.HamlibModelId > 1); + SkipPatterns = Load("port_skip_patterns.json", p => !string.IsNullOrEmpty(p.Pattern)); + } + + public RigFamily? Family(string id) => + Families.FirstOrDefault(f => f.Id.Equals(id, StringComparison.OrdinalIgnoreCase)); + + /// The family a Hamlib model number belongs to, by its backend (thousands) digit. + public RigFamily? FamilyForModel(int hamlibModelId) => + hamlibModelId <= 1 ? null + : Families.FirstOrDefault(f => f.HamlibBackends.Contains(hamlibModelId / 1000)); + + /// The family a cable's RadioFamily hint (from usb_devices.json) points at. + public RigFamily? FamilyForVendor(string? vendor) => + string.IsNullOrEmpty(vendor) ? null + : Families.FirstOrDefault(f => f.VendorHints.Any(v => v.Equals(vendor, StringComparison.OrdinalIgnoreCase))); + + /// + /// All rigs that answer with this identity in this family. Several is + /// legitimate (Elecraft K3/KX3/KX2 share ID017;); the first is the default. + /// + public IReadOnlyList Lookup(string familyId, string reply) => + Ids.Where(i => i.Family.Equals(familyId, StringComparison.OrdinalIgnoreCase) + && i.Reply.Equals(reply, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + /// The device class a port name matches, or null when it looks like fair game. + public string? SkipClass(string friendlyName) => + SkipPatterns.FirstOrDefault(p => friendlyName.Contains(p.Pattern, StringComparison.OrdinalIgnoreCase))?.DeviceClass; + + // ── Loading ─────────────────────────────────────────────────────────── + + private static List Load(string fileName, Func keep) + { + try + { + var json = DataFileLocator.ReadAllText(fileName); + if (json is null) return []; + var items = JsonConvert.DeserializeObject>(json)?.Where(keep).ToList() ?? []; + Log.Information("Loaded {Count} entries from {File}", items.Count, fileName); + return items; + } + catch (Exception ex) + { + Log.Warning(ex, "Failed to load {File}", fileName); + return []; + } + } +} diff --git a/Services/DiscoveryEngine.cs b/Services/DiscoveryEngine.cs new file mode 100644 index 0000000..f2ee9f3 --- /dev/null +++ b/Services/DiscoveryEngine.cs @@ -0,0 +1,261 @@ +using RigCheck.Localization; +using RigCheck.Models; +using Serilog; +using System.Net.NetworkInformation; +using System.Runtime.CompilerServices; + +namespace RigCheck.Services; + +/// What the operator asked Find my radio to look at. +public record DiscoveryRequest( + IReadOnlyList Ports, // already ticked by the operator + int PreferredModelId = 0, // from the Connection panel, 0 = none + int PreferredBaud = 0, // 0 = radio default / unknown + bool CheckNetwork = true); // rigctld and Flrig ports + +/// +/// Find my radio, stages 0–2: inventory the ticked ports, rank protocol +/// families and baud rates per port, and probe each with query-only +/// exchanges until a rig answers. Emits every step as it happens so the +/// results panel can show the sweep live and the log can replay it. +/// +/// See docs/discovery-flow.md for the design. Verification (stage 3) is +/// the ordinary test suite run by the caller against each DiscoveredRig. +/// +public class DiscoveryEngine +{ + private const int ReplyTimeoutMs = 300; // how long to wait for the first byte + private const int QuietMs = 40; // reply is complete after this much silence + + private readonly DiscoveryDataService _data; + private readonly RadioPresetsService _presets; + + public DiscoveryEngine(DiscoveryDataService data, RadioPresetsService presets) + { + _data = data; + _presets = presets; + } + + public async IAsyncEnumerable RunAsync( + DiscoveryRequest request, + [EnumeratorCancellation] CancellationToken ct = default) + { + // ── Network transports first: instant, and "rigctld is already + // running" is the answer for a lot of WSJT-X setups. + if (request.CheckNetwork) + { + foreach (var ev in CheckNetworkTransports()) + yield return ev; + } + + // ── Serial ports, one at a time, handle held open across the sweep. + foreach (var port in request.Ports) + { + ct.ThrowIfCancellationRequested(); + + var skipClass = _data.SkipClass(port.FriendlyName); + if (skipClass is not null) + { + yield return new(ProbeEventKind.Skipped, port.PortName, + Strings.Format("Disc_SkippedClass", port.PortName, skipClass)); + continue; + } + + var candidates = RankCandidates(port, request); + if (candidates.Count == 0) continue; + + using var probe = SerialProbe.TryOpen(port.PortName, candidates[0].Baud, out var failure); + if (probe is null) + { + var kind = failure == "in use" ? ProbeEventKind.InUse : ProbeEventKind.Skipped; + var text = failure == "in use" + ? Strings.Format("Disc_InUse", port.PortName) + : Strings.Format("Disc_OpenFailed", port.PortName, failure ?? string.Empty); + yield return new(kind, port.PortName, text); + continue; + } + + DiscoveredRig? found = null; + foreach (var (family, baud) in candidates) + { + ct.ThrowIfCancellationRequested(); + await probe.SetBaudAsync(baud, ct); + + yield return new(ProbeEventKind.Trying, port.PortName, + Strings.Format("Disc_Trying", port.PortName, baud, family.Name)); + + var (rig, events) = await ProbeFamilyAsync(probe, port, family, baud, request, ct); + foreach (var ev in events) yield return ev; + + if (rig is not null) + { + found = rig; + yield return new(ProbeEventKind.Found, port.PortName, + Strings.Format("Disc_Found", rig.ModelName, rig.HamlibModelId, rig.Port, rig.Baud), Rig: rig); + break; // one rig per port; on to the next port + } + } + + if (found is null) + yield return new(ProbeEventKind.PortDone, port.PortName, Strings.Format("Disc_NoAnswer", port.PortName)); + } + } + + // ── Stage 0: network ────────────────────────────────────────────────── + + private static IEnumerable CheckNetworkTransports() + { + var listeners = IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners(); + + if (listeners.Any(l => l.Port == BrandingInfo.DefaultRigctldPort)) + { + var rig = new DiscoveredRig($"localhost:{BrandingInfo.DefaultRigctldPort}", 0, "rigctld", + 2, "rigctld (NET rigctl)", 1.0, 1, UseRigctld: true); + yield return new(ProbeEventKind.Found, rig.Port, + Strings.Format("Disc_FoundRigctld", BrandingInfo.DefaultRigctldPort), Rig: rig); + } + + if (listeners.Any(l => l.Port == 12345)) + yield return new(ProbeEventKind.Note, "localhost:12345", Strings.Get("Disc_FlrigSeen")); + } + + // ── Stage 1: ranking ────────────────────────────────────────────────── + // Family order: the family of the radio already chosen in the Connection + // panel, then the family the cable's VID/PID hints at, then by weight. + // Baud order is the family's list, with the operator's chosen baud + // moved to the front for that family. + + private List<(RigFamily Family, int Baud)> RankCandidates(ComPortInfo port, DiscoveryRequest request) + { + var preferred = _data.FamilyForModel(request.PreferredModelId); + var hinted = _data.FamilyForVendor(port.RadioFamily); + + var families = _data.Families + .OrderByDescending(f => f == preferred ? 2 : 0) + .ThenByDescending(f => f == hinted ? 1 : 0) + .ThenByDescending(f => f.Weight) + .ToList(); + + var list = new List<(RigFamily, int)>(); + foreach (var family in families) + { + IEnumerable bauds = family.Bauds; + if (family == preferred && request.PreferredBaud > 0) + bauds = new[] { request.PreferredBaud }.Concat(family.Bauds.Where(b => b != request.PreferredBaud)); + foreach (var baud in bauds) + list.Add((family, baud)); + } + return list; + } + + // ── Stage 2: one family at one baud ─────────────────────────────────── + + private async Task<(DiscoveredRig? Rig, List Events)> ProbeFamilyAsync( + SerialProbe probe, ComPortInfo port, RigFamily family, int baud, + DiscoveryRequest request, CancellationToken ct) + { + var events = new List(); + + ProbeOperations.TryParse(family.Probe, out var op); + var (_, parsed) = await ExchangeAsync(probe, port.PortName, op, events, ct); + + // Icom rigs that predate the read-ID command answer NG or nothing; + // a plain read-frequency still proves the family and the baud. + if (parsed.Kind is (ProbeReplyKind.None or ProbeReplyKind.Acknowledged) + && family.FallbackProbe is not null + && ProbeOperations.TryParse(family.FallbackProbe, out var fallback)) + { + (_, parsed) = await ExchangeAsync(probe, port.PortName, fallback, events, ct); + } + + var rig = parsed.Kind switch + { + ProbeReplyKind.Identity => ResolveByIdentity(port, family, baud, parsed.Id!, request, events), + ProbeReplyKind.Frequency => ResolveByFrequency(port, family, baud, parsed.FrequencyHz, request, events), + ProbeReplyKind.Acknowledged => Note(events, port.PortName, Strings.Format("Disc_Acknowledged", family.Name, baud)), + ProbeReplyKind.Garbage => Note(events, port.PortName, Strings.Get("Disc_Garbage")), + _ => null, + }; + return (rig, events); + } + + private static async Task<(byte[] Reply, ProbeReply Parsed)> ExchangeAsync( + SerialProbe probe, string portName, ProbeOperation op, List events, CancellationToken ct) + { + var request = ProbeOperations.Bytes(op); + events.Add(new(ProbeEventKind.Sent, portName, op.ToString(), ProbeOperations.Dump(request))); + + var reply = await probe.ExchangeAsync(request, ReplyTimeoutMs, QuietMs, ct); + if (reply.Length > 0) + events.Add(new(ProbeEventKind.Received, portName, string.Empty, ProbeOperations.Dump(reply))); + else + events.Add(new(ProbeEventKind.Note, portName, Strings.Get("Disc_NoReply"))); + + Log.Debug("Probe {Port} {Op}: sent {Sent} got {Got}", portName, op, + ProbeOperations.Dump(request), ProbeOperations.Dump(reply)); + return (reply, ProbeOperations.Parse(op, reply)); + } + + private static DiscoveredRig? Note(List events, string port, string text) + { + events.Add(new(ProbeEventKind.Note, port, text)); + return null; + } + + // ── Model resolution ────────────────────────────────────────────────── + + // The rig named itself. Several rigs may share an identity (Elecraft); + // the radio chosen in the Connection panel breaks the tie, else the + // first entry in rig_ids.json. An identity not in the table still + // proves the family: fall back to the family's default model and say so. + private DiscoveredRig ResolveByIdentity(ComPortInfo port, RigFamily family, int baud, string id, + DiscoveryRequest request, List events) + { + var matches = _data.Lookup(family.Id, id); + if (matches.Count > 0) + { + var pick = matches.FirstOrDefault(m => m.HamlibModelId == request.PreferredModelId) ?? matches[0]; + if (matches.Count > 1) + events.Add(new(ProbeEventKind.Note, port.PortName, + Strings.Format("Disc_SharedId", id, string.Join(", ", matches.Select(m => m.Name)), pick.Name))); + return new(port.PortName, baud, family.Id, pick.HamlibModelId, pick.Name, 1.0, family.HandoffStopBits); + } + + events.Add(new(ProbeEventKind.Note, port.PortName, Strings.Format("Disc_UnknownId", id, family.Name))); + var (modelId, name) = DefaultModel(family, request); + return new(port.PortName, baud, family.Id, modelId, name, 0.7, family.HandoffStopBits); + } + + // A frequency came back. Inside a ham band is strong evidence; merely + // plausible is weaker but still worth handing to rigctl to confirm. + private DiscoveredRig? ResolveByFrequency(ComPortInfo port, RigFamily family, int baud, long hz, + DiscoveryRequest request, List events) + { + var score = ProbeOperations.IsInHamBand(hz) ? 0.8 + : ProbeOperations.IsPlausibleFrequency(hz) ? 0.6 + : 0.0; + + events.Add(new(ProbeEventKind.Response, port.PortName, + Strings.Format("Disc_FrequencyReply", (hz / 1_000_000.0).ToString("0.000"), score >= 0.8 ? " " + Strings.Get("Disc_InBand") : string.Empty))); + + if (score == 0.0) return null; + + var (modelId, name) = DefaultModel(family, request); + return new(port.PortName, baud, family.Id, modelId, name, score, family.HandoffStopBits); + } + + // The family is known but the rig did not name itself: use the radio + // chosen in the Connection panel when it belongs to this family, else + // the family's default from rig_families.json. + private (int ModelId, string Name) DefaultModel(RigFamily family, DiscoveryRequest request) + { + var modelId = _data.FamilyForModel(request.PreferredModelId) == family + ? request.PreferredModelId + : family.DefaultModelId; + + var name = _presets.Presets.FirstOrDefault(p => p.HamlibModelId == modelId)?.Name + ?? _data.Ids.FirstOrDefault(i => i.HamlibModelId == modelId)?.Name + ?? $"{family.Name} ({modelId})"; + return (modelId, name); + } +} diff --git a/Services/EnvironmentCheckService.cs b/Services/EnvironmentCheckService.cs new file mode 100644 index 0000000..64da3e9 --- /dev/null +++ b/Services/EnvironmentCheckService.cs @@ -0,0 +1,375 @@ +using Microsoft.Win32; +using RigCheck.Localization; +using RigCheck.Models; +using Serilog; +using System.Diagnostics; +using System.IO; +using System.Management; +using System.Net.NetworkInformation; +using System.Runtime.InteropServices; + +namespace RigCheck.Services; + +/// +/// Pre-flight checks on the PC itself — no radio needed. Answers the +/// questions a helper would ask first: is Hamlib installed and which copy, +/// does rigctl run, are the USB-serial drivers healthy, is rigctld already +/// running, has Windows Firewall blocked it. +/// +/// Everything here is read-only. RigCheck never modifies PATH, firewall +/// rules, startup entries, or drivers; where a fix is needed it shows the +/// command or the setting to change and lets the operator do it. +/// +/// Runs only when the operator clicks Scan. Enumerating processes and +/// listening ports unprompted is a pattern security software watches for, +/// so none of this happens at startup. +/// +public class EnvironmentCheckService +{ + private readonly HamlibLocatorService _locator; + private readonly HamlibRunnerService _runner; + private readonly ComPortService _ports; + + public EnvironmentCheckService(HamlibLocatorService locator, + HamlibRunnerService runner, + ComPortService ports) + { + _locator = locator; + _runner = runner; + _ports = ports; + } + + /// The checks, in run order — used for the pending placeholders. + public static readonly IReadOnlyList Checks = + [ + TestId.EnvWindows, TestId.EnvHamlib, TestId.EnvRigctlVersion, TestId.EnvRigctlPath, + TestId.EnvRadioModel, TestId.EnvSerialDrivers, TestId.EnvRigctld, TestId.EnvAutostart, + TestId.EnvFirewall, TestId.EnvRadioApps, + ]; + + public async Task RunAllAsync( + ConnectionConfig cfg, + IProgress? progress = null, + CancellationToken ct = default) + { + var results = new List(); + + // Each check is isolated: one throwing must not hide the others. + async Task Run(TestId id, Func> check) + { + if (ct.IsCancellationRequested) return; + TestResult result; + try + { + result = await check(); + } + catch (Exception ex) + { + Log.Warning(ex, "Environment check {Check} failed", id); + result = TestResult.Warning(id, Strings.Format("Env_CheckFailed", ex.Message), string.Empty); + } + results.Add(result); + progress?.Report(result); + } + + await Run(TestId.EnvWindows, () => Task.FromResult(CheckWindows())); + await Run(TestId.EnvHamlib, () => Task.FromResult(CheckHamlib())); + await Run(TestId.EnvRigctlVersion, () => CheckRigctlVersionAsync(ct)); + await Run(TestId.EnvRigctlPath, () => Task.FromResult(CheckRigctlOnPath())); + await Run(TestId.EnvRadioModel, () => Task.FromResult(CheckRadioModel(cfg))); + await Run(TestId.EnvSerialDrivers, () => Task.Run(CheckSerialDrivers, ct)); + await Run(TestId.EnvRigctld, () => Task.Run(() => CheckRigctld(cfg), ct)); + await Run(TestId.EnvAutostart, () => Task.FromResult(CheckAutostart())); + await Run(TestId.EnvFirewall, () => Task.FromResult(CheckFirewall())); + await Run(TestId.EnvRadioApps, () => Task.FromResult(CheckRadioApps())); + + return new TestSuiteResult(results, cfg); + } + + // ── Windows ─────────────────────────────────────────────────────────── + + private static TestResult CheckWindows() + { + using var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion"); + var product = key?.GetValue("ProductName") as string ?? "Windows"; + var release = key?.GetValue("DisplayVersion") as string ?? string.Empty; + var build = Environment.OSVersion.Version.Build; + + // The registry still says "Windows 10" on Windows 11; the build number tells the truth. + if (build >= 22000) product = product.Replace("Windows 10", "Windows 11"); + + var arch = RuntimeInformation.OSArchitecture.ToString(); + return TestResult.Pass(TestId.EnvWindows, + Strings.Format("Env_Windows", product, release, build, arch), string.Empty); + } + + // ── Hamlib ──────────────────────────────────────────────────────────── + + private TestResult CheckHamlib() + { + var copies = _locator.FindAll(); + if (copies.Count == 0) + { + return TestResult.Fail(TestId.EnvHamlib, Strings.Get("Env_HamlibNone"), string.Empty, + Diag("Env_HamlibNone", 2, learnMore: BrandingInfo.HamlibDownloadUrl)); + } + + var (path, via) = copies[0]; + var message = copies.Count == 1 + ? Strings.Format("Env_HamlibOne", via, path) + : Strings.Format("Env_HamlibMany", copies.Count, via, path, + string.Join("; ", copies.Skip(1).Select(c => $"{c.Via}: {c.Path}"))); + + return TestResult.Pass(TestId.EnvHamlib, message, string.Empty); + } + + private async Task CheckRigctlVersionAsync(CancellationToken ct) + { + if (!_locator.IsAvailable) + return TestResult.Skipped(TestId.EnvRigctlVersion, Strings.Get("Env_SkippedNoHamlib")); + + var cmd = new RigctlCommand(["--version"], DisplayCommand: "rigctl --version"); + var result = await _runner.RunAsync(cmd, ct); + + if (!result.IsSuccess) + { + return TestResult.Fail(TestId.EnvRigctlVersion, + Strings.Format("Env_RigctlVersionFail", result.ErrorMessage), cmd.DisplayCommand, + Diag("Env_RigctlVersionFail", 2)); + } + + // First line is "rigctl Hamlib 4.6.2 ..." — enough on its own. + var firstLine = result.RawOutput.Split('\n', StringSplitOptions.RemoveEmptyEntries) + .FirstOrDefault()?.Trim() ?? result.RawOutput.Trim(); + return TestResult.Pass(TestId.EnvRigctlVersion, firstLine, cmd.DisplayCommand); + } + + // The commands RigCheck shows say "rigctl ..." — they only work in the + // operator's own command window if Hamlib's bin folder is on PATH. + private TestResult CheckRigctlOnPath() + { + if (!_locator.IsAvailable) + return TestResult.Skipped(TestId.EnvRigctlPath, Strings.Get("Env_SkippedNoHamlib")); + + var onPath = _locator.FindAll().FirstOrDefault(c => c.Via == "PATH"); + if (onPath.Path is not null) + return TestResult.Pass(TestId.EnvRigctlPath, Strings.Format("Env_RigctlPathYes", onPath.Path), string.Empty); + + var binDir = Path.GetDirectoryName(_locator.RigctlPath!) ?? string.Empty; + return TestResult.Warning(TestId.EnvRigctlPath, Strings.Get("Env_RigctlPathNo"), string.Empty) with + { + Diagnosis = Diag("Env_RigctlPathNo", 2, fixCommand: HamlibLocatorService.PathAddCommand(binDir)), + }; + } + + // ── Radio model ─────────────────────────────────────────────────────── + // Model 1 is Hamlib's dummy rig: every command succeeds without touching + // hardware. The exported log must make it unmistakable which model was + // in use, so this check always runs. + + private static TestResult CheckRadioModel(ConnectionConfig cfg) => cfg.ModelId switch + { + 0 => TestResult.Warning(TestId.EnvRadioModel, Strings.Get("Env_RadioModelNone"), string.Empty), + 1 => TestResult.Warning(TestId.EnvRadioModel, Strings.Get("Env_RadioModelDummy"), string.Empty) with + { + Diagnosis = Diag("Env_RadioModelDummy", 1, learnMore: BrandingInfo.HelpUrl + "-dummy-rig"), + }, + _ => TestResult.Pass(TestId.EnvRadioModel, + Strings.Format("Env_RadioModel", cfg.RadioModelName, cfg.ModelId), string.Empty), + }; + + // ── Serial drivers ──────────────────────────────────────────────────── + // A cable whose driver failed does not get a COM number, so it never + // shows in the port list. Ask Plug and Play for every device with a + // problem code and keep the ones that look like serial adapters. + + private TestResult CheckSerialDrivers() + { + var ports = _ports.GetAvailablePorts(); + var problems = new List(); + + using var searcher = new ManagementObjectSearcher( + "SELECT Name, PNPClass, ConfigManagerErrorCode FROM Win32_PnPEntity WHERE ConfigManagerErrorCode <> 0"); + foreach (ManagementObject dev in searcher.Get()) + { + var name = dev["Name"]?.ToString() ?? string.Empty; + var cls = dev["PNPClass"]?.ToString() ?? string.Empty; + var code = Convert.ToInt32(dev["ConfigManagerErrorCode"] ?? 0); + if (cls is "Ports" || LooksLikeSerialAdapter(name)) + problems.Add(Strings.Format("Env_SerialProblem", name, code)); + } + + if (problems.Count > 0) + { + return TestResult.Warning(TestId.EnvSerialDrivers, string.Join("; ", problems), string.Empty) with + { + Diagnosis = Diag("Env_SerialProblem", 3), + }; + } + + if (ports.Count == 0) + { + return TestResult.Warning(TestId.EnvSerialDrivers, Strings.Get("Env_SerialNone"), string.Empty) with + { + Diagnosis = Diag("Env_SerialNone", 2), + }; + } + + var list = string.Join(", ", ports.Select(p => p.PortName)); + return TestResult.Pass(TestId.EnvSerialDrivers, Strings.Format("Env_SerialOk", ports.Count, list), string.Empty); + } + + private static bool LooksLikeSerialAdapter(string name) + { + var n = name.ToLowerInvariant(); + return n.Contains("serial") || n.Contains("uart") || n.Contains("cp210") || + n.Contains("ftdi") || n.Contains("ft232") || n.Contains("prolific") || + n.Contains("ch340") || n.Contains("ch341") || n.Contains("usb-to-serial") || + n.Contains("unknown device"); + } + + // ── rigctld ─────────────────────────────────────────────────────────── + + private static TestResult CheckRigctld(ConnectionConfig cfg) + { + var listeners = IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners(); + var port = cfg.UseRigctld ? cfg.RigctldPort : BrandingInfo.DefaultRigctldPort; + var listening = listeners.Any(l => l.Port == port); + var flrig = listeners.Any(l => l.Port == 12345); + + var extra = flrig ? " " + Strings.Format("Env_FlrigListening", 12345) : string.Empty; + + if (listening) + { + // Name the owner if it is obviously rigctld; anything else stays "unknown". + var rigctld = Process.GetProcessesByName("rigctld"); + var owner = rigctld.Length > 0 ? "rigctld.exe" : Strings.Get("Env_ProcessUnknown"); + foreach (var p in rigctld) p.Dispose(); + return TestResult.Pass(TestId.EnvRigctld, + Strings.Format("Env_RigctldListening", port, owner) + extra, string.Empty); + } + + if (cfg.UseRigctld) + { + return TestResult.Warning(TestId.EnvRigctld, Strings.Format("Env_RigctldNeeded", port) + extra, string.Empty) with + { + Diagnosis = Diag("Env_RigctldNeeded", 2), + }; + } + + return TestResult.Pass(TestId.EnvRigctld, Strings.Format("Env_RigctldNone", port) + extra, string.Empty); + } + + // Startup folders and Run keys only. Scheduled tasks would need + // schtasks output parsing; not worth it for something this rare. + private static TestResult CheckAutostart() + { + var found = new List(); + + foreach (var folder in new[] { Environment.SpecialFolder.Startup, Environment.SpecialFolder.CommonStartup }) + { + var dir = Environment.GetFolderPath(folder); + if (!Directory.Exists(dir)) continue; + found.AddRange(Directory.EnumerateFiles(dir) + .Where(f => Path.GetFileName(f).Contains("rigctld", StringComparison.OrdinalIgnoreCase))); + } + + foreach (var root in new[] { Registry.CurrentUser, Registry.LocalMachine }) + { + using var run = root.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run"); + if (run is null) continue; + foreach (var name in run.GetValueNames()) + { + var value = run.GetValue(name)?.ToString() ?? string.Empty; + if (value.Contains("rigctld", StringComparison.OrdinalIgnoreCase)) + found.Add($"{root.Name}\\...\\Run\\{name} = {value}"); + } + } + + return found.Count == 0 + ? TestResult.Pass(TestId.EnvAutostart, Strings.Get("Env_AutostartNone"), string.Empty) + : TestResult.Pass(TestId.EnvAutostart, Strings.Format("Env_AutostartFound", string.Join("; ", found)), string.Empty); + } + + // Firewall rules live in the registry as "v2.x|Action=Block|Active=TRUE| + // Dir=In|App=C:\...\rigctld.exe|Name=...|". Reading them needs no + // elevation and no netsh. A Block rule is the classic result of + // dismissing the firewall prompt the first time rigctld listened. + private static TestResult CheckFirewall() + { + using var key = Registry.LocalMachine.OpenSubKey( + @"SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\FirewallRules"); + if (key is null) + return TestResult.Skipped(TestId.EnvFirewall, Strings.Get("Env_FirewallUnreadable")); + + bool allow = false, block = false; + foreach (var name in key.GetValueNames()) + { + var rule = key.GetValue(name)?.ToString() ?? string.Empty; + if (!rule.Contains("rigctld", StringComparison.OrdinalIgnoreCase)) continue; + if (!rule.Contains("|Active=TRUE|", StringComparison.OrdinalIgnoreCase)) continue; + if (rule.Contains("|Action=Block|", StringComparison.OrdinalIgnoreCase)) block = true; + if (rule.Contains("|Action=Allow|", StringComparison.OrdinalIgnoreCase)) allow = true; + } + + if (block) + { + return TestResult.Warning(TestId.EnvFirewall, Strings.Get("Env_FirewallBlock"), string.Empty) with + { + Diagnosis = Diag("Env_FirewallBlock", 2), + }; + } + + return TestResult.Pass(TestId.EnvFirewall, + Strings.Get(allow ? "Env_FirewallAllow" : "Env_FirewallNone"), string.Empty); + } + + // ── Installed radio software ────────────────────────────────────────── + + private static readonly string[] KnownApps = + ["WSJT-X", "Fldigi", "JS8Call", "Winlink Express", "RMS Express", "Flrig", "Hamlib"]; + + private static TestResult CheckRadioApps() + { + var found = new SortedSet(StringComparer.OrdinalIgnoreCase); + + var roots = new (RegistryKey Root, string Sub)[] + { + (Registry.LocalMachine, @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"), + (Registry.LocalMachine, @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"), + (Registry.CurrentUser, @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"), + }; + + foreach (var (root, sub) in roots) + { + using var key = root.OpenSubKey(sub); + if (key is null) continue; + foreach (var subName in key.GetSubKeyNames()) + { + using var app = key.OpenSubKey(subName); + var display = app?.GetValue("DisplayName") as string; + if (string.IsNullOrEmpty(display)) continue; + if (!KnownApps.Any(k => display.Contains(k, StringComparison.OrdinalIgnoreCase))) continue; + var version = app?.GetValue("DisplayVersion") as string; + found.Add(string.IsNullOrEmpty(version) ? display : $"{display} {version}"); + } + } + + return found.Count == 0 + ? TestResult.Pass(TestId.EnvRadioApps, Strings.Get("Env_AppsNone"), string.Empty) + : TestResult.Pass(TestId.EnvRadioApps, string.Join(", ", found), string.Empty); + } + + // ── Helpers ─────────────────────────────────────────────────────────── + + /// + /// Build a diagnosis from resource keys "{key}_Summary" and "{key}_1..n", + /// the same layout DiagnosisEngine uses. + /// + private static DiagnosticResult Diag(string key, int checks, string? fixCommand = null, string? learnMore = null) => + new( + Summary: Strings.Get($"{key}_Summary"), + Checks: Enumerable.Range(1, checks).Select(i => Strings.Get($"{key}_{i}")).ToArray(), + FixCommand: fixCommand, + LearnMoreUrl: learnMore); +} diff --git a/Services/HamlibLocatorService.cs b/Services/HamlibLocatorService.cs index 8f007f7..fa8797c 100644 --- a/Services/HamlibLocatorService.cs +++ b/Services/HamlibLocatorService.cs @@ -57,6 +57,17 @@ public void Find() Log.Warning("rigctl.exe not found. Hamlib may not be installed."); } + /// + /// Every rigctl.exe that actually exists, in priority order, first one + /// being the copy RigCheck uses. Several is normal — WSJT-X and Fldigi + /// each bundle their own — and the environment scan lists them all. + /// + public IReadOnlyList<(string Path, string Via)> FindAll() => + CandidatePaths() + .Where(c => File.Exists(c.Path)) + .DistinctBy(c => c.Path, StringComparer.OrdinalIgnoreCase) + .ToList(); + /// /// Returns all candidate (path, source-label) pairs in priority order. /// diff --git a/Services/LogExportService.cs b/Services/LogExportService.cs index e9427e9..d5e25e6 100644 --- a/Services/LogExportService.cs +++ b/Services/LogExportService.cs @@ -20,11 +20,12 @@ public class LogExportService /// Write a test suite result to a file chosen by the user. /// Returns the path written, or null on failure. /// - public async Task ExportAsync(TestSuiteResult suite, string outputPath) + public async Task ExportAsync(TestSuiteResult suite, string outputPath, + IReadOnlyList? transcript = null) { try { - var text = BuildReport(suite); + var text = BuildReport(suite, transcript); await File.WriteAllTextAsync(outputPath, text, Encoding.UTF8); Log.Information("Log exported to {Path}", outputPath); return outputPath; @@ -37,9 +38,11 @@ public class LogExportService } /// - /// Build the report as a string (also used for clipboard copy). + /// Build the report as a string (also used for clipboard copy). The + /// transcript, when given, is the Find my radio sweep — every byte sent + /// and received — so a helper can replay it with a terminal program. /// - public string BuildReport(TestSuiteResult suite) + public string BuildReport(TestSuiteResult suite, IReadOnlyList? transcript = null) { var sb = new StringBuilder(); var cfg = suite.Config; @@ -85,6 +88,30 @@ public string BuildReport(TestSuiteResult suite) sb.AppendLine(Row(Strings.Get("Export_Radio"), Strings.Format("Export_HamlibId", cfg.RadioModelName, cfg.ModelId))); sb.AppendLine(); + // ── Discovery transcript ────────────────────────────────────────── + // Prefixes mirror the results panel: "$" a command, ">" bytes sent, + // "<" bytes received, "*" something found. + if (transcript is { Count: > 0 }) + { + sb.AppendLine("----------------------------------------------------------"); + sb.AppendLine(Strings.Get("Export_Transcript")); + sb.AppendLine("----------------------------------------------------------"); + sb.AppendLine(); + foreach (var line in transcript) + { + var prefix = line.Kind switch + { + TranscriptKind.Command => "$ ", + TranscriptKind.SerialTx => "> ", + TranscriptKind.SerialRx => "< ", + TranscriptKind.Found => "* ", + _ => " ", + }; + sb.AppendLine($" {prefix}{line.Text}"); + } + sb.AppendLine(); + } + // ── Test results ────────────────────────────────────────────────── sb.AppendLine("----------------------------------------------------------"); sb.AppendLine(Strings.Get("Export_TestResults")); diff --git a/Services/ProbeOperations.cs b/Services/ProbeOperations.cs new file mode 100644 index 0000000..ed75624 --- /dev/null +++ b/Services/ProbeOperations.cs @@ -0,0 +1,200 @@ +using System.Text; + +namespace RigCheck.Services; + +// ── Probe operations ────────────────────────────────────────────────────── +// The complete, fixed set of things Find my radio can send to a serial +// port. Every one is a read: identify yourself, or what frequency are you +// on. rig_families.json picks an operation BY NAME; it can never supply +// bytes of its own, so a tampered or auto-updated data file cannot make +// RigCheck send anything else at a transmitter. +// +// Adding an operation means adding it here, in code, with a review. + +public enum ProbeOperation +{ + /// "ID;" — Kenwood, Elecraft, and Yaesu CAT rigs answer "IDnnn;". + KenwoodId, + /// CI-V read transceiver ID (cmd 19 00) to the broadcast address; the rig answers with its own address. + IcomReadId, + /// CI-V read operating frequency (cmd 03). Fallback for rigs that do not answer 19 00. + IcomReadFreq, + /// Yaesu FT-817/857/897 five-byte read frequency (opcode 03). + YaesuLegacyReadFreq, +} + +public enum ProbeReplyKind +{ + /// Nothing came back in time. + None, + /// Bytes came back but nothing recognisable — often the wrong baud rate. + Garbage, + /// The rig answered in the family's protocol but did not name itself (e.g. Kenwood "?;", CI-V NG). + Acknowledged, + /// The rig named itself; Id holds the identity string as it appears in rig_ids.json. + Identity, + /// The rig reported its frequency; FrequencyHz is set. + Frequency, +} + +public record ProbeReply(ProbeReplyKind Kind, string? Id = null, long FrequencyHz = 0); + +public static class ProbeOperations +{ + // CI-V controller address rigctl and most software use for the PC. + private const byte CivController = 0xE0; + private const byte CivBroadcast = 0x00; + + public static bool TryParse(string name, out ProbeOperation op) => + Enum.TryParse(name, ignoreCase: true, out op); + + /// The exact bytes sent for an operation. + public static byte[] Bytes(ProbeOperation op) => op switch + { + ProbeOperation.KenwoodId => "ID;"u8.ToArray(), + ProbeOperation.IcomReadId => [0xFE, 0xFE, CivBroadcast, CivController, 0x19, 0x00, 0xFD], + ProbeOperation.IcomReadFreq => [0xFE, 0xFE, CivBroadcast, CivController, 0x03, 0xFD], + ProbeOperation.YaesuLegacyReadFreq => [0x00, 0x00, 0x00, 0x00, 0x03], + _ => throw new ArgumentOutOfRangeException(nameof(op)), + }; + + /// Interpret whatever came back after sending an operation. + public static ProbeReply Parse(ProbeOperation op, byte[] reply) + { + if (reply.Length == 0) return new(ProbeReplyKind.None); + + return op switch + { + ProbeOperation.KenwoodId => ParseKenwood(reply), + ProbeOperation.IcomReadId => ParseCiv(reply, expectCmd: 0x19), + ProbeOperation.IcomReadFreq => ParseCiv(reply, expectCmd: 0x03), + ProbeOperation.YaesuLegacyReadFreq => ParseYaesuLegacy(reply), + _ => new(ProbeReplyKind.Garbage), + }; + } + + // ── Kenwood-style ASCII ("ID023;") ──────────────────────────────────── + + private static ProbeReply ParseKenwood(byte[] reply) + { + var text = Encoding.ASCII.GetString(reply); + + // "IDnnn;" anywhere in the buffer — some rigs prefix an echo or a stray ";". + var at = text.IndexOf("ID", StringComparison.Ordinal); + if (at >= 0) + { + var end = text.IndexOf(';', at); + if (end > at + 2 && text.AsSpan(at + 2, end - at - 2).ToString().All(char.IsDigit)) + return new(ProbeReplyKind.Identity, text.Substring(at, end - at + 1)); + } + + // "?;" is Kenwood for "I heard you but did not understand" — right baud, right family. + if (text.Contains("?;", StringComparison.Ordinal)) + return new(ProbeReplyKind.Acknowledged); + + return new(ProbeReplyKind.Garbage); + } + + // ── Icom CI-V frames ────────────────────────────────────────────────── + // Frame: FE FE [] [data…] FD + // The rig echoes our own frame first when CI-V transceive is on, so + // frames addressed FROM the controller are skipped. + + private static ProbeReply ParseCiv(byte[] reply, byte expectCmd) + { + foreach (var frame in CivFrames(reply)) + { + if (frame.Length < 5) continue; + var to = frame[2]; var src = frame[3]; var cmd = frame[4]; + if (src == CivController) continue; // our own echo + if (to != CivController && to != CivBroadcast) continue; + + if (cmd == 0xFA) return new(ProbeReplyKind.Acknowledged); // NG — understood, refused + if (cmd != expectCmd) continue; + + if (expectCmd == 0x19 && frame.Length >= 7) + return new(ProbeReplyKind.Identity, frame[6].ToString("X2")); + + if (expectCmd == 0x03 && frame.Length >= 10) + return new(ProbeReplyKind.Frequency, FrequencyHz: BcdLittleEndian(frame, 5, 5)); + } + + // Any well-formed frame from a non-controller address means CI-V at this baud. + return CivFrames(reply).Any(f => f.Length >= 5 && f[3] != CivController) + ? new(ProbeReplyKind.Acknowledged) + : new(ProbeReplyKind.Garbage); + } + + private static IEnumerable CivFrames(byte[] buf) + { + int i = 0; + while (i < buf.Length - 1) + { + if (buf[i] != 0xFE || buf[i + 1] != 0xFE) { i++; continue; } + var end = Array.IndexOf(buf, (byte)0xFD, i + 2); + if (end < 0) yield break; + yield return buf[i..(end + 1)]; + i = end + 1; + } + } + + // CI-V frequency: 5 BCD bytes, least significant first (10 Hz digit pairs up). + private static long BcdLittleEndian(byte[] buf, int start, int count) + { + long value = 0; + for (int i = start + count - 1; i >= start; i--) + value = value * 100 + ((buf[i] >> 4) * 10) + (buf[i] & 0x0F); + return value; + } + + // ── Yaesu five-byte (FT-817 family) ────────────────────────────────── + // Reply to read-frequency: 4 BCD bytes, most significant first, in + // 10 Hz units, then one mode byte. + + private static ProbeReply ParseYaesuLegacy(byte[] reply) + { + if (reply.Length < 5) return new(ProbeReplyKind.Garbage); + + long value = 0; + for (int i = 0; i < 4; i++) + { + var hi = reply[i] >> 4; var lo = reply[i] & 0x0F; + if (hi > 9 || lo > 9) return new(ProbeReplyKind.Garbage); + value = value * 100 + hi * 10 + lo; + } + return new(ProbeReplyKind.Frequency, FrequencyHz: value * 10); + } + + // ── Sanity check ────────────────────────────────────────────────────── + + /// + /// True when a frequency sits inside an amateur band. A rig parked on a + /// ham frequency is strong evidence that the reply is real and not a + /// coincidence of bytes at the wrong baud rate. + /// + public static bool IsInHamBand(long hz) + { + var mhz = hz / 1_000_000.0; + return HamBands.Any(b => mhz >= b.Low && mhz <= b.High); + } + + /// Anything a real receiver could be tuned to — weaker evidence than a ham band. + public static bool IsPlausibleFrequency(long hz) => hz is >= 100_000 and <= 3_000_000_000; + + private static readonly (double Low, double High)[] HamBands = + [ + (1.8, 2.0), (3.5, 4.0), (5.25, 5.45), (7.0, 7.3), (10.1, 10.15), (14.0, 14.35), + (18.068, 18.168), (21.0, 21.45), (24.89, 24.99), (28.0, 29.7), (50.0, 54.0), + (70.0, 70.5), (144.0, 148.0), (222.0, 225.0), (420.0, 450.0), (902.0, 928.0), + (1240.0, 1300.0), (2300.0, 2450.0), + ]; + + /// Hex dump with printable ASCII beside it, for the transcript. + public static string Dump(byte[] bytes) + { + if (bytes.Length == 0) return string.Empty; + var hex = string.Join(" ", bytes.Select(b => b.ToString("X2"))); + var ascii = new string(bytes.Select(b => b is >= 0x20 and < 0x7F ? (char)b : '.').ToArray()); + return ascii.Any(c => c != '.') ? $"{hex} \"{ascii}\"" : hex; + } +} diff --git a/Services/SerialProbe.cs b/Services/SerialProbe.cs new file mode 100644 index 0000000..53227fd --- /dev/null +++ b/Services/SerialProbe.cs @@ -0,0 +1,116 @@ +using Serilog; +using System.IO; +using System.IO.Ports; + +namespace RigCheck.Services; + +/// +/// One open serial port during discovery. Keeps the handle across the +/// baud sweep (opening a USB-serial port is slow and some adapters reset +/// DTR/RTS on open) and does a single write-then-collect exchange. +/// +/// RTS and DTR are never asserted. On a great many interfaces RTS or DTR +/// IS the PTT line — asserting either would key the transmitter. That rule +/// does not have a setting. +/// +public sealed class SerialProbe : IDisposable +{ + private readonly SerialPort _port; + + public string PortName => _port.PortName; + + private SerialProbe(SerialPort port) => _port = port; + + /// + /// Open the port at the first baud rate. Returns null when the port is + /// owned by another program (the common, informative case) or missing. + /// + public static SerialProbe? TryOpen(string portName, int baud, out string? failure) + { + failure = null; + var port = new SerialPort(portName, baud, Parity.None, 8, StopBits.One) + { + Handshake = Handshake.None, + DtrEnable = false, + RtsEnable = false, + ReadTimeout = 50, + WriteTimeout = 300, + }; + + try + { + port.Open(); + port.DiscardInBuffer(); + port.DiscardOutBuffer(); + return new SerialProbe(port); + } + catch (UnauthorizedAccessException) + { + failure = "in use"; + port.Dispose(); + return null; + } + catch (Exception ex) when (ex is IOException or ArgumentException or InvalidOperationException) + { + failure = ex.Message; + Log.Debug(ex, "Could not open {Port}", portName); + port.Dispose(); + return null; + } + } + + /// Change speed without closing; settle briefly so the adapter is ready. + public async Task SetBaudAsync(int baud, CancellationToken ct) + { + if (_port.BaudRate == baud) return; + _port.BaudRate = baud; + _port.DiscardInBuffer(); + await Task.Delay(30, ct); + } + + /// + /// Write the bytes, then collect whatever arrives until the line has + /// been quiet for or + /// has passed. Runs on a worker thread; SerialPort reads are blocking. + /// + public Task ExchangeAsync(byte[] request, int timeoutMs, int quietMs, CancellationToken ct) => + Task.Run(() => + { + _port.DiscardInBuffer(); + _port.Write(request, 0, request.Length); + + var collected = new MemoryStream(); + var buffer = new byte[256]; + var deadline = Environment.TickCount64 + timeoutMs; + var lastByte = Environment.TickCount64; + var gotAny = false; + + while (Environment.TickCount64 < deadline && !ct.IsCancellationRequested) + { + try + { + var n = _port.Read(buffer, 0, buffer.Length); // blocks up to ReadTimeout (50 ms) + if (n > 0) + { + collected.Write(buffer, 0, n); + lastByte = Environment.TickCount64; + gotAny = true; + } + } + catch (TimeoutException) + { + // Nothing this tick. If we already have data and the line + // has gone quiet, the reply is complete. + if (gotAny && Environment.TickCount64 - lastByte >= quietMs) break; + } + } + + return collected.ToArray(); + }, ct); + + public void Dispose() + { + try { if (_port.IsOpen) _port.Close(); } catch { /* closing a yanked USB port throws; nothing to do */ } + _port.Dispose(); + } +} diff --git a/Services/TelemetryService.cs b/Services/TelemetryService.cs index 7b2461a..a1643d0 100644 --- a/Services/TelemetryService.cs +++ b/Services/TelemetryService.cs @@ -80,6 +80,28 @@ public Task ReportTestRunAsync(TestSuiteResult suite, ComPortInfo? port) => }), }); + /// + /// Outcome of a Find my radio run: how many ports were swept and what + /// answered. Model, family, baud, and score per find — this is the data + /// that improves the family weights and baud orders in rig_families.json. + /// No port names or bytes. + /// + public Task ReportDiscoveryAsync(int portsSwept, IReadOnlyList found, IReadOnlyList verified) => + ReportAsync("discovery", new Dictionary + { + ["ports_swept"] = portsSwept, + ["found"] = found.Count, + ["verified"] = verified.Count, + ["rigs"] = found.Select(r => new + { + family = r.FamilyId, + model_id = r.HamlibModelId, + baud = r.Baud, + score = r.Score, + verified = verified.Contains(r), + }), + }); + /// Exception type and message only — never the stack trace, which can contain file paths. public Task ReportCrashAsync(Exception ex) => ReportAsync("crash", new Dictionary diff --git a/Services/TestRunnerService.cs b/Services/TestRunnerService.cs index 20f8073..a70e03d 100644 --- a/Services/TestRunnerService.cs +++ b/Services/TestRunnerService.cs @@ -33,6 +33,13 @@ public TestRunnerService( /// Progress is reported via the callback /// so the UI can update results in real time as each test completes. /// + /// The connection tests, in run order — used for the pending placeholders. + public static readonly IReadOnlyList SuiteTests = + [ + TestId.OpenConnection, TestId.GetFrequency, TestId.GetMode, TestId.GetPtt, + TestId.GetSmeter, TestId.GetVfo, TestId.SetFrequency, + ]; + public async Task RunAllAsync( ConnectionConfig cfg, bool includeSetFreqTest, diff --git a/TRANSLATING.md b/TRANSLATING.md index d75f36d..d53c433 100644 --- a/TRANSLATING.md +++ b/TRANSLATING.md @@ -16,11 +16,11 @@ starting from nothing, and it is the most useful thing you can do. | Language | File | Coverage | Status | | --- | --- | --- | --- | -| English (source) | `Strings.resx` | 100% (238 of 238) | Source | -| German (`de`) | `Strings.de.resx` | 100% (238 of 238) | Machine translated — review wanted | -| Spanish (`es`) | `Strings.es.resx` | 100% (238 of 238) | Machine translated — review wanted | -| French (`fr`) | `Strings.fr.resx` | 100% (238 of 238) | Machine translated — review wanted | -| Japanese (`ja`) | `Strings.ja.resx` | 100% (238 of 238) | Machine translated — review wanted | +| English (source) | `Strings.resx` | 100% (343 of 343) | Source | +| German (`de`) | `Strings.de.resx` | 100% (343 of 343) | Machine translated — review wanted | +| Spanish (`es`) | `Strings.es.resx` | 100% (343 of 343) | Machine translated — review wanted | +| French (`fr`) | `Strings.fr.resx` | 100% (343 of 343) | Machine translated — review wanted | +| Japanese (`ja`) | `Strings.ja.resx` | 100% (343 of 343) | Machine translated — review wanted | --- @@ -93,4 +93,4 @@ appears on the repository's contributors page. ## Questions -Ask in [Discussions](https://github.com/Computer-Tsu/ShackDesk-RigCheck/discussions). +Ask in the [Translations](https://github.com/Computer-Tsu/ShackDesk-RigCheck/discussions/categories/translations) category of Discussions. diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index 646b988..43907ea 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -17,6 +17,8 @@ namespace RigCheck.ViewModels; public partial class MainViewModel : ObservableObject { private readonly TestRunnerService _testRunner; + private readonly EnvironmentCheckService _envCheck; + private readonly DiscoveryEngine _discovery; private readonly LogExportService _logExport; private readonly HamlibLocatorService _hamlib; private readonly SettingsService _settings; @@ -28,7 +30,9 @@ public partial class MainViewModel : ObservableObject // ── Observable state ───────────────────────────────────────────────── - [ObservableProperty] private bool _isRunning; + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(RunTestsCommand), nameof(ScanEnvironmentCommand), nameof(FindRadioCommand))] + private bool _isRunning; [ObservableProperty] private bool _rawConsoleVisible; [ObservableProperty] private bool _chromeVisible; [ObservableProperty] private bool _alwaysOnTop; @@ -60,6 +64,8 @@ public MainViewModel( TestResultsViewModel results, RawConsoleViewModel rawConsole, TestRunnerService testRunner, + EnvironmentCheckService envCheck, + DiscoveryEngine discovery, LogExportService logExport, HamlibLocatorService hamlib, SettingsService settings, @@ -69,6 +75,8 @@ public MainViewModel( Results = results; RawConsole = rawConsole; _testRunner = testRunner; + _envCheck = envCheck; + _discovery = discovery; _logExport = logExport; _hamlib = hamlib; _settings = settings; @@ -98,7 +106,7 @@ private async Task RunTestsAsync() { IsRunning = true; StatusMessage = Strings.Get("Status_Running"); - Results.Clear(); + Results.Clear(TestRunnerService.SuiteTests); CopyResultsCommand.NotifyCanExecuteChanged(); var cfg = Connection.BuildConfig(); @@ -148,6 +156,148 @@ private async Task RunTestsAsync() private bool CanRunTests() => !IsRunning && _hamlib.IsAvailable && Connection.IsReady; + // Environment scan: PC-side checks only, no radio needed, so it is not + // gated on readiness — it is the thing to run when Run Tests is greyed + // out and the operator wants to know why. Always operator-initiated. + [RelayCommand(CanExecute = nameof(CanScan))] + private async Task ScanEnvironmentAsync() + { + IsRunning = true; + StatusMessage = Strings.Get("Status_Scanning"); + Results.Clear(EnvironmentCheckService.Checks); + CopyResultsCommand.NotifyCanExecuteChanged(); + + try + { + var progress = new Progress(result => + Application.Current.Dispatcher.Invoke(() => Results.AddResult(result))); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var suite = await _envCheck.RunAllAsync(Connection.BuildConfig(), progress, cts.Token); + + Results.SetSuiteResult(suite); + TaskCompleted?.Invoke(suite.AllPassed); + StatusMessage = Strings.Format("Status_ScanDone", suite.WarningCount + suite.FailCount); + + Log.Information("Environment scan complete: {Pass} ok, {Fail} fail, {Warn} warn", + suite.PassCount, suite.FailCount, suite.WarningCount); + } + catch (Exception ex) + { + StatusMessage = Strings.Get("Status_RunFailed"); + Log.Error(ex, "Environment scan threw an exception"); + } + finally + { + IsRunning = false; + CopyResultsCommand.NotifyCanExecuteChanged(); + } + } + + private bool CanScan() => !IsRunning; + + // ── Find my radio ───────────────────────────────────────────────────── + // The window collects the ticked ports in a dialog and calls this. The + // engine's events become transcript lines as they arrive; each rig it + // finds is then verified with the ordinary test suite, and the best + // verified one is written into the Connection panel. + + [RelayCommand(CanExecute = nameof(CanScan))] + private async Task FindRadioAsync(IReadOnlyList ports) + { + IsRunning = true; + StatusMessage = Strings.Get("Status_Discovering"); + Results.Clear([]); + CopyResultsCommand.NotifyCanExecuteChanged(); + + var found = new List(); + var verified = new List<(DiscoveredRig Rig, TestSuiteResult Suite)>(); + + try + { + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3)); + var request = new DiscoveryRequest(ports, Connection.ModelId, Connection.BaudRate); + + Results.AddTranscript(TranscriptKind.Note, Strings.Format("Disc_Start", ports.Count)); + + await foreach (var ev in _discovery.RunAsync(request, cts.Token)) + { + Results.AddTranscript(ev.Kind switch + { + ProbeEventKind.Sent => TranscriptKind.SerialTx, + ProbeEventKind.Received => TranscriptKind.SerialRx, + ProbeEventKind.Trying => TranscriptKind.Trying, + ProbeEventKind.Found => TranscriptKind.Found, + ProbeEventKind.Response => TranscriptKind.Response, + _ => TranscriptKind.Note, + }, ev.Bytes ?? ev.Message); + + if (ev.Rig is not null) found.Add(ev.Rig); + } + + // ── Stage 3: verify every find with the real test suite ─────── + foreach (var rig in found) + { + Results.AddTranscript(TranscriptKind.Command, + Strings.Format("Disc_Verifying", rig.ModelName, rig.Port, rig.Baud)); + + var cfg = rig.UseRigctld + ? new ConnectionConfig { UseRigctld = true, RigctldHost = rig.Port.Split(':')[0], + RigctldPort = int.TryParse(rig.Port.Split(':').Last(), out var p) ? p : BrandingInfo.DefaultRigctldPort, + ModelId = rig.HamlibModelId, RadioModelName = rig.ModelName } + : new ConnectionConfig { ModelId = rig.HamlibModelId, RadioModelName = rig.ModelName, + ComPort = rig.Port, BaudRate = rig.Baud }; + + var progress = new Progress(r => + Application.Current.Dispatcher.Invoke(() => Results.AddResult(r))); + var suite = await _testRunner.RunAllAsync(cfg, false, progress, cts.Token); + if (suite.AllPassed) verified.Add((rig, suite)); + + Results.AddTranscript(suite.AllPassed ? TranscriptKind.Found : TranscriptKind.Note, + suite.AllPassed ? Strings.Format("Disc_Verified", rig.ModelName, rig.Port) + : Strings.Format("Disc_VerifyFailed", rig.ModelName, rig.Port, suite.FailCount)); + } + + // ── Stage 4 (first cut): hand the best one to the Connection panel + if (verified.Count > 0) + { + var best = verified.OrderByDescending(v => v.Rig.Score).First(); + Connection.ApplyDiscovered(best.Rig); + Results.SetSuiteResult(best.Suite); + Results.AddTranscript(TranscriptKind.Found, + Strings.Format("Disc_Applied", best.Rig.ModelName, best.Rig.HamlibModelId, best.Rig.Port, best.Rig.Baud)); + StatusMessage = verified.Count == 1 + ? Strings.Format("Status_DiscoveredOne", best.Rig.ModelName, best.Rig.Port) + : Strings.Format("Status_DiscoveredMany", verified.Count, best.Rig.ModelName, best.Rig.Port); + } + else + { + Results.SetSuiteResult(new TestSuiteResult([], Connection.BuildConfig())); + StatusMessage = found.Count > 0 ? Strings.Get("Status_DiscoveredUnverified") : Strings.Get("Status_DiscoveredNone"); + Results.AddTranscript(TranscriptKind.Note, StatusMessage); + } + + TaskCompleted?.Invoke(verified.Count > 0); + Log.Information("Discovery complete: {Found} found, {Verified} verified", found.Count, verified.Count); + _ = _telemetry.ReportDiscoveryAsync(ports.Count, found, verified.Select(v => v.Rig).ToList()); + } + catch (OperationCanceledException) + { + StatusMessage = Strings.Get("Status_DiscoveryTimeout"); + Results.AddTranscript(TranscriptKind.Note, StatusMessage); + } + catch (Exception ex) + { + StatusMessage = Strings.Get("Status_RunFailed"); + Log.Error(ex, "Discovery threw an exception"); + } + finally + { + IsRunning = false; + CopyResultsCommand.NotifyCanExecuteChanged(); + } + } + [RelayCommand] private async Task ExportLogAsync() { @@ -168,7 +318,7 @@ private async Task ExportLogAsync() if (dlg.ShowDialog() != true) return; - var path = await _logExport.ExportAsync(Results.SuiteResult, dlg.FileName); + var path = await _logExport.ExportAsync(Results.SuiteResult, dlg.FileName, Results.Transcript); StatusMessage = path is not null ? Strings.Format("Status_LogSaved", path) @@ -180,7 +330,7 @@ private async Task ExportLogAsync() private void CopyResults() { if (Results.SuiteResult is null) return; - Clipboard.SetText(_logExport.BuildReport(Results.SuiteResult)); + Clipboard.SetText(_logExport.BuildReport(Results.SuiteResult, Results.Transcript)); StatusMessage = Strings.Get("Status_Copied"); } diff --git a/ViewModels/ViewModels.cs b/ViewModels/ViewModels.cs index 18ff0b4..b44b91a 100644 --- a/ViewModels/ViewModels.cs +++ b/ViewModels/ViewModels.cs @@ -160,6 +160,34 @@ private void ApplyPreset(RadioPreset preset) PttMethod = preset.PttMethod; } + /// + /// Fill the panel from a rig Find my radio verified, so Run Tests and + /// the exported log use exactly what worked. Serial parameters other + /// than baud stay "Radio default" — the probe used 8N1 and Hamlib knows + /// the rest; the stop bits the operator should enter in WSJT-X are in + /// the handoff text, not forced here. + /// + public void ApplyDiscovered(DiscoveredRig rig) + { + if (rig.UseRigctld) + { + UseRigctld = true; + var parts = rig.Port.Split(':'); + RigctldHost = parts[0]; + if (parts.Length > 1 && int.TryParse(parts[1], out var p)) RigctldPort = p; + return; + } + + UseRigctld = false; + // Preset first: selecting one applies its defaults, and the values + // that were actually verified must win over them. + SelectedPreset = AvailablePresets.FirstOrDefault(x => x.HamlibModelId == rig.HamlibModelId); + ModelId = rig.HamlibModelId; + ModelName = rig.ModelName; + BaudRate = rig.Baud; + SelectedPort = AvailablePorts.FirstOrDefault(x => x.PortName == rig.Port); + } + public ConnectionConfig BuildConfig() => new() { ModelId = ModelId, @@ -240,45 +268,66 @@ public record Choice(string Value, string Label) /// public partial class TestResultsViewModel : ObservableObject { - public ObservableCollection Items { get; } = []; + /// + /// What the results panel shows, in order: TestResultItemViewModel for + /// test results and TranscriptLine for the free-form lines of a + /// discovery run. Mixed on purpose so a run reads as it happened. + /// + public ObservableCollection Items { get; } = []; + + /// The transcript lines only, for the exported log. + public IReadOnlyList Transcript => Items.OfType().ToList(); [ObservableProperty] private string _summaryText = string.Empty; [ObservableProperty] private bool _hasResults; public TestSuiteResult? SuiteResult { get; private set; } - public void Clear() + /// + /// Start a new run: one pending placeholder per test so the operator + /// sees what is coming. The list differs between the connection tests + /// and the environment scan. + /// + public void Clear(IEnumerable plannedTests) { Items.Clear(); SummaryText = string.Empty; HasResults = false; SuiteResult = null; - // Add pending placeholders for all tests - foreach (TestId id in Enum.GetValues()) + foreach (var id in plannedTests) Items.Add(new TestResultItemViewModel(TestResult.Pending(id))); } public void AddResult(TestResult result) { - // Replace the pending placeholder for this test - var existing = Items.FirstOrDefault(i => i.TestId == result.Id); - if (existing is not null) - { - var idx = Items.IndexOf(existing); - Items[idx] = new TestResultItemViewModel(result); - } + // Replace this test's pending placeholder if there is one; otherwise + // append. Discovery verifies several rigs in one run, so a result + // for a test that already has a real result is a new row, not a + // replacement. + var pending = Items.OfType() + .FirstOrDefault(i => i.TestId == result.Id && i.Status == TestStatus.Pending); + if (pending is not null) + Items[Items.IndexOf(pending)] = new TestResultItemViewModel(result); else - { Items.Add(new TestResultItemViewModel(result)); - } HasResults = true; } + /// Append a transcript line (discovery progress, bytes sent and received). + public void AddTranscript(TranscriptKind kind, string text) + { + Items.Add(new TranscriptLine(kind, text)); + HasResults = true; + } + public void SetSuiteResult(TestSuiteResult suite) { SuiteResult = suite; + // A discovery run that verified nothing still has a transcript worth + // copying, so it gets an empty suite — but no "all 0 passed" banner. + if (suite.Results.Count == 0) { SummaryText = string.Empty; return; } SummaryText = suite.AllPassed ? Strings.Format("Summary_AllPassed", suite.PassCount) : Strings.Format("Summary_Mixed", suite.FailCount, suite.PassCount, suite.WarningCount); diff --git a/Views/DiscoveryDialog.xaml b/Views/DiscoveryDialog.xaml new file mode 100644 index 0000000..99bf63c --- /dev/null +++ b/Views/DiscoveryDialog.xaml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +