diff --git a/App.xaml.cs b/App.xaml.cs index e6776ae..55e44cc 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -121,6 +121,8 @@ private static void RegisterServices(IServiceCollection services, SettingsServic services.AddTransient(); services.AddSingleton(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddTransient(); // ViewModels diff --git a/Assets/radio_presets.json b/Assets/radio_presets.json index e88e72e..96c00c4 100644 --- a/Assets/radio_presets.json +++ b/Assets/radio_presets.json @@ -6,10 +6,10 @@ "Name": "Icom IC-7300", "Manufacturer": "Icom", "HamlibModelId": 3073, - "BaudRate": 19200, + "BaudRate": 115200, "PttMethod": "CAT", "Popularity": 100, - "Notes": "CI-V address default 0x94. USB cable to the radio's USB port; driver is Silicon Labs CP210x." + "Notes": "CI-V address default 0x94. USB cable to the radio's USB port; driver is Silicon Labs CP210x. CI-V USB baud rate is 115200 unless changed in the radio's menu (Set > Connectors > CI-V > CI-V USB Baud Rate); Echo Back is on by default." }, { "Name": "Icom IC-705", diff --git a/Assets/rig_families.json b/Assets/rig_families.json index eb957b5..c00374c 100644 --- a/Assets/rig_families.json +++ b/Assets/rig_families.json @@ -1,6 +1,6 @@ [ { - "_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." + "_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. For Icom, the CI-V addresses tried after the broadcast attempt come from rig_ids.json. 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", @@ -18,7 +18,7 @@ "Name": "Icom CI-V", "Probe": "IcomReadId", "FallbackProbe": "IcomReadFreq", - "Bauds": [ 19200, 9600, 115200, 4800 ], + "Bauds": [ 115200, 19200, 9600, 4800 ], "HamlibBackends": [ 3 ], "VendorHints": [ "Icom" ], "HandoffStopBits": 1, diff --git a/BrandingInfo.cs b/BrandingInfo.cs index 4fd6021..a03a970 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.8"; + public const string Version = "0.7.0"; public const string Tagline = "Know your rig is ready"; // ── Developer / publisher ──────────────────────────────────── @@ -48,6 +48,13 @@ public static class BrandingInfo // ── Hamlib / rigctl resources ───────────────────────────────── public const string HamlibDownloadUrl = "https://github.com/Hamlib/Hamlib/releases"; + + /// + /// winget command that installs WSJT-X, which bundles Hamlib. Shown to the + /// operator as a copyable line; RigCheck never runs it. Package ID + /// verified 2026-09-21 (installs WSJT-X 3.0.2). + /// + public const string WsjtxWingetCommand = "winget install JoeTaylor.WSJT-x"; public const string WsjtxDownloadUrl = "https://wsjt.sourceforge.io/wsjtx.html"; public const string HamlibDocsUrl = "https://hamlib.github.io"; diff --git a/CHANGELOG.md b/CHANGELOG.md index 25870c3..296a7c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,130 @@ 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.7.0] - 2026-09-21 + +**Milestone: environment checks and Find my radio, proven on a real Icom IC-7300.** + +Scan PC, Find my radio, and Run Tests all did their job on real hardware today. Everything the +bench found — CI-V broadcast never answers, WSJT-X names its Hamlib `rigctl-wsjtx.exe`, the +connection test hung in rigctl's interactive mode, Hamlib says nothing on failure without `-vv`, +`get_vfo` is not a fault on Icoms — is fixed in 0.6.10 through 0.6.17 below. This is the version +the first beta will be cut from. + +## [0.6.17] - 2026-09-21 + +### Fixed +- Pasting a full command line from the results (`rigctl-wsjtx -m 3073 -r COM3 -s 115200 f`) into + the raw console sent the connection options twice; the exe name and connection options are now + stripped so only the subcommand runs, and the box shows a placeholder saying so + +### Added +- Screenshots from the first IC-7300 run in the README + +## [0.6.16] - 2026-09-21 + +First all-green run on an IC-7300; one false pass corrected. + +### Fixed +- "Get VFO" reported a pass with a page of Hamlib trace as the VFO name. The IC-7300 (like most + Icoms) has no get_vfo in Hamlib; rigctl printed "Feature not available" but still exited 0. + Hamlib's own error line is now recognised whatever the exit code, and an unsupported query is a + warning that says so — the radio is fine, the command just does not exist for it +- Result messages show only the first line of rigctl's output, never a trace + +## [0.6.15] - 2026-09-21 + +Failures now say why. + +### Fixed +- Hamlib 4.7.1 prints nothing when rigctl fails at its default verbosity, so a port held by + WSJT-X, a missing rigctld, and a radio that is off all came out as "an unexpected error". + rigctl now runs with `-vv`, its banner line is stripped from the output, and its actual + messages ("serial port COM3 is already open", "does not exist", "failed to connect") drive the + diagnosis +- The "port in use" diagnosis names the program that is running right now (WSJT-X, Fldigi, + JS8Call, Flrig, rigctld, Winlink Express, VARA) — checked only when that failure occurs +- The suggested rigctld command uses the name that exists on the PC (`rigctld-wsjtx`) and omits + `-s` when the baud is Radio default +- The Quick preset picker shows the saved radio on startup instead of "Choose your radio…" + +## [0.6.14] - 2026-09-21 + +Run Tests works against a real radio. + +### Fixed +- The "Open connection" test ran rigctl with no command, which does not connect-and-exit — it + enters rigctl's interactive mode and waits for keyboard input until RigCheck's timeout, so + every run reported "the radio did not respond" and skipped the other six tests, even though + Hamlib could talk to the radio in 7 ms. The test now reads the frequency, which proves the + round trip; rigctl's input is also closed at launch so it can never wait on a keyboard again +- WSJT-X stores several settings as Qt variant blobs (`(...PTT_method_VOX...)`); the + configuration reader now decodes them instead of passing the blob through to the handoff + +## [0.6.13] - 2026-09-21 + +WSJT-X's Hamlib is found. + +### Fixed +- Hamlib was reported missing on a PC with WSJT-X installed: WSJT-X ships its copy as + `rigctl-wsjtx.exe` (and `rigctld-wsjtx.exe`), not `rigctl.exe`. Both names are now tried, the + install folder is also read from the Uninstall registry key, and either name is accepted on PATH +- Copyable commands start with the name that exists on the PC (`rigctl-wsjtx …` for WSJT-X users) +- Scan PC re-searches for Hamlib, so installing WSJT-X while RigCheck is open updates the banner + and enables Run Tests without a restart + +## [0.6.12] - 2026-09-21 + +### Added +- When Scan PC finds no Hamlib, the diagnosis shows the one-line winget command that installs + WSJT-X (`winget install JoeTaylor.WSJT-x`) as a copyable fix — RigCheck never runs it + +### Fixed +- Summary line reads "115200 baud, 8N1 (8 data bits, no parity, 1 stop)" instead of "1 stop bit(s)" +- Text copied from the results panel no longer carries a trailing space where a Copy button sat + +## [0.6.11] - 2026-09-21 + +### Added +- Find my radio ends with the sentence the operator came for — "Your radio: Icom IC-7300 on COM3 + — 115200 baud, 8 data bits, no parity, 1 stop bit" — in the results and in the status bar + +## [0.6.10] - 2026-09-21 + +First lessons from a real IC-7300. + +### Fixed +- Find my radio never got an answer from an Icom: the read-ID was sent to CI-V address 00 + (broadcast), which rigs act on but by design never answer. When only our own echo comes + back (Echo Back is on by default on the IC-7300) or nothing at all, RigCheck now addresses + each CI-V address in `rig_ids.json` in turn, the selected radio's first +- Our own frame echoed back is now reported as what it is — a CI-V radio listening at that + speed — instead of "nothing recognisable" +- Without Hamlib installed, Find my radio reported a found radio as unverified because the + rigctl tests could not run. A radio that named itself over the serial port now counts as + verified, the Connection panel is filled in, and the handoff is shown; the note says to + install WSJT-X or Hamlib for the full tests +- The status bar now explains that Run Tests needs Hamlib because the tests are Hamlib + commands, and that Find my radio and Scan PC work without it + +### Changed +- Icom baud order is now 115200 first (the IC-7300's USB default), then 19200, 9600, 4800; the + IC-7300 preset uses 115200 +- The reply window waits a little longer for silence so an echo and the answer behind it are + read as one exchange + +## [0.6.9] - 2026-09-21 + +Find my radio hands you the settings; Hamlib's dummy rig can no longer pass unnoticed. + +### Added +- Settings handoff after a verified find: the exact fields for WSJT-X / JS8Call (File › Settings › + Radio), Fldigi (Rig Control › Hamlib), and Winlink Express, each as one copyable line +- RigCheck reads what WSJT-X and JS8Call are configured to use (`WSJT-X.ini`, `JS8Call.ini`): + their port is probed first, their radio stands in when none is chosen, and after the sweep + each program gets a one-line verdict — already matches, or exactly which fields to change +- The raw console warns beside every command sent to Hamlib model 1, the dummy rig + ## [0.6.8] - 2026-09-21 Find my radio — the second half of the 0.7.0 milestone, first cut. diff --git a/Models/Models.cs b/Models/Models.cs index cc4ac42..0efa3f1 100644 --- a/Models/Models.cs +++ b/Models/Models.cs @@ -276,3 +276,21 @@ public record ProbeEvent( string Message, string? Bytes = null, // hex dump of what was sent or received DiscoveredRig? Rig = null); + +/// +/// What one of the operator's digital-mode programs is configured to use, +/// read from its own settings file. Values are the program's own strings +/// ("Eight", "Two", "Hardware"), not RigCheck's. +/// +public record ConfigClue( + string App, // "WSJT-X", "JS8Call" + string FilePath, + string RigName, // Hamlib display name, e.g. "Icom IC-7300" + int ModelId, // resolved Hamlib model, 0 if unknown + string Port, // "COM3", or "localhost:4532" for rigctl mode + int Baud, // 0 = not set + string DataBits, + string StopBits, + string Handshake, + string PttMethod, + bool UseRigctld); diff --git a/README.md b/README.md index 83fbbc0..f310a32 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ RigCheck is the second ShackDesk suite application. "Know your rig is ready."
-![RigCheck main window](docs/screenshots/Screenshot_RigCheck-0.6.4-Main.png) +![RigCheck — Find my radio found an IC-7300, verified it with Hamlib, and handed over the WSJT-X settings](docs/screenshots/Screenshot_RigCheck-0.6.16-FindMyRadio.png) Serial CAT communication diagnostics using Hamlib. Verify your radio connection before it matters. @@ -151,6 +151,9 @@ Target users: is never a valid selection, so a passing result always means a real radio answered ### PC Scan (no radio needed) + +![Scan PC results](docs/screenshots/Screenshot_RigCheck-0.6.16-ScanPC.png) + The **Scan PC** button checks the computer side before any radio is involved, so "Run Tests is greyed out — why?" has an answer: @@ -187,7 +190,7 @@ model to pick, **Find my radio** works it out: 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 +6. 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 @@ -199,6 +202,9 @@ ticked ports. The data files (`rig_families.json`, `rig_ids.json`, `port_skip_pa choose a built-in query by name and cannot contain command bytes. Design: `docs/discovery-flow.md`. ### Diagnostic Test Suite + +![Run Tests against an IC-7300](docs/screenshots/Screenshot_RigCheck-0.6.16-RunTests.png) + Run a sequence of standard Hamlib queries and display pass/fail results in plain English: diff --git a/Resources/Strings.de.resx b/Resources/Strings.de.resx index 5ab924f..fec602a 100644 --- a/Resources/Strings.de.resx +++ b/Resources/Strings.de.resx @@ -127,7 +127,7 @@ Details wurden in das Protokoll geschrieben. Falls dies wiederholt auftritt, mel Hamlib gefunden über {0} Hamlib nicht gefunden — WSJT-X installieren oder Hamlib herunterladen - Hamlib nicht gefunden. „Tests starten“ ist erst verfügbar, wenn Hamlib installiert ist. + Hamlib nicht gefunden — „Tests starten“ braucht es (die Tests sind Hamlib-Befehle). „Mein Funkgerät finden“ und „PC prüfen“ funktionieren auch ohne. Tests laufen… Alle {0} Tests bestanden. {0} Test(s) fehlgeschlagen — Details in den Ergebnissen. @@ -287,7 +287,7 @@ Fortfahren? {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 + Installieren Sie WSJT-X, Fldigi oder das eigenständige Hamlib für Windows — jedes enthält rigctl.exe. Der Befehl unten installiert WSJT-X mit winget 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} @@ -378,4 +378,34 @@ Fortfahren? {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“ + Modell 1 ist das Dummy-Gerät von Hamlib — jede Antwort unten ist simuliert, nicht von einem Funkgerät + Einzugebende Einstellungen — Zeile für Ihr Programm kopieren + WSJT-X und JS8Call: File › Settings › Radio + Rig: {0} · Serial Port: {1} · Baud Rate: {2} · Data Bits: Eight · Stop Bits: {3} · Handshake: None · PTT Method: {4} + Rig: Hamlib NET rigctl · Network Server: {0} + Fldigi: Configure › Rig Control › Hamlib + Rig: {0} · Device: {1} · Baud rate: {2} · Stopbits: {3} · PTT via Hamlib command + Rig: Hamlib NET rigctl · Device: {0} + Winlink Express: Session › Radio Setup (eigene Geräteliste — das nächstliegende Modell wählen) + Radio: {0} · Serial Port: {1} · Baud: {2} · PTT: {3} + {0} ist für {1} an {2} mit {3} Baud konfiguriert + {0} ist auf rigctl unter {1} konfiguriert + {0} hat diese Einstellungen bereits + {0} ist auf {1} an {2} mit {3} Baud eingestellt — ändern Sie {4} + Rig auf {0} + Serial Port auf {0} + Baud Rate auf {0} + {0} verwendet rigctl ({1}) — das funktioniert, solange rigctld läuft + {0} ist auf {1} an {2} mit {3} Baud eingestellt — so lassen oder auf Hamlib NET rigctl umstellen, um das Funkgerät über rigctld zu teilen + Hamlib ist nicht installiert, daher können die vollständigen rigctl-Tests nicht laufen — ein Funkgerät, das sich über die serielle Schnittstelle identifiziert hat, gilt als geprüft. Installieren Sie WSJT-X oder Hamlib, um die Tests auszuführen. + {0} an {1} durch direkte Abfrage geprüft + unser eigener Rahmen kam zurück — ein CI-V-Funkgerät mit eingeschaltetem Echo Back hört mit {0} Baud zu; jede bekannte CI-V-Adresse wird nun der Reihe nach abgefragt + Ihr Funkgerät: {0} an {1} — {2} Baud, 8N{3} (8 Datenbits, keine Parität, {3} Stopp) + Ihr Funkgerät ist über rigctld unter {0} erreichbar + Läuft gerade: {0} — mit großer Wahrscheinlichkeit das Programm, das die Schnittstelle belegt. Beenden Sie es (oder trennen Sie dort die Gerätesteuerung) und versuchen Sie es erneut + Dieses Funkgerät meldet sein aktives VFO nicht über Hamlib (normal für {0} und die meisten Icoms) — kein Problem + Dieses Funkgerät meldet die Signalstärke nicht über Hamlib ({0}) — kein Problem + Der Hamlib-Treiber für das {0} implementiert diese Abfrage nicht. + Das Funkgerät antwortet; nur dieser eine Befehl existiert für es nicht. WSJT-X und die anderen Programme brauchen ihn nicht. + nur der Unterbefehl, z. B. f m t l STRENGTH — eine eingefügte vollständige Befehlszeile wird für Sie gekürzt diff --git a/Resources/Strings.es.resx b/Resources/Strings.es.resx index 18514bb..87bfcd9 100644 --- a/Resources/Strings.es.resx +++ b/Resources/Strings.es.resx @@ -127,7 +127,7 @@ Los detalles se han guardado en el registro. Si esto sigue ocurriendo, infórmel Hamlib encontrado mediante {0} Hamlib no encontrado — instale WSJT-X o descargue Hamlib - Hamlib no encontrado. «Ejecutar pruebas» no estará disponible hasta que Hamlib esté instalado. + Hamlib no encontrado — «Ejecutar pruebas» lo necesita (las pruebas son comandos de Hamlib). «Buscar mi radio» y «Analizar PC» funcionan sin él. Ejecutando pruebas… Las {0} pruebas se superaron. {0} prueba(s) fallaron — vea los resultados para más detalles. @@ -287,7 +287,7 @@ Los detalles se han guardado en el registro. Si esto sigue ocurriendo, infórmel {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 + Instale WSJT-X, Fldigi o el Hamlib independiente para Windows — cada uno incluye rigctl.exe. El comando de abajo instala WSJT-X con winget 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} @@ -378,4 +378,34 @@ Los detalles se han guardado en el registro. Si esto sigue ocurriendo, infórmel {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 + El modelo 1 es la radio ficticia de Hamlib — todas las respuestas de abajo son simuladas, no de una radio + Ajustes que debe introducir — copie la línea de su programa + WSJT-X y JS8Call: File › Settings › Radio + Rig: {0} · Serial Port: {1} · Baud Rate: {2} · Data Bits: Eight · Stop Bits: {3} · Handshake: None · PTT Method: {4} + Rig: Hamlib NET rigctl · Network Server: {0} + Fldigi: Configure › Rig Control › Hamlib + Rig: {0} · Device: {1} · Baud rate: {2} · Stopbits: {3} · PTT via Hamlib command + Rig: Hamlib NET rigctl · Device: {0} + Winlink Express: Session › Radio Setup (tiene su propia lista de radios — elija el modelo más cercano) + Radio: {0} · Serial Port: {1} · Baud: {2} · PTT: {3} + {0} está configurado para {1} en {2} a {3} baudios + {0} está configurado para usar rigctl en {1} + {0} ya tiene estos ajustes + {0} está configurado con {1} en {2} a {3} baudios — cambie {4} + Rig a {0} + Serial Port a {0} + Baud Rate a {0} + {0} usa rigctl ({1}) — eso funciona siempre que rigctld esté en ejecución + {0} está configurado con {1} en {2} a {3} baudios — déjelo así, o cámbielo a Hamlib NET rigctl para compartir la radio a través de rigctld + Hamlib no está instalado, así que las pruebas completas de rigctl no pueden ejecutarse — una radio que se identificó por el puerto serie cuenta como verificada. Instale WSJT-X o Hamlib para ejecutar las pruebas. + {0} en {1} verificada por consulta directa + volvió nuestra propia trama — una radio CI-V con Echo Back activado está escuchando a {0} baudios; preguntando a cada dirección CI-V conocida por turno + Su radio: {0} en {1} — {2} baudios, 8N{3} (8 bits de datos, sin paridad, {3} de parada) + Su radio es accesible a través de rigctld en {0} + En ejecución ahora mismo: {0} — casi con seguridad el programa que retiene el puerto. Ciérrelo (o desconecte su control de radio) y vuelva a intentarlo + Esta radio no informa su VFO activo a través de Hamlib (normal para {0} y la mayoría de Icom) — no es un problema + Esta radio no informa la intensidad de señal a través de Hamlib ({0}) — no es un problema + El controlador de Hamlib para {0} no implementa esta consulta. + La radio responde; simplemente este comando no existe para ella. WSJT-X y los demás programas no lo necesitan. + solo el subcomando, p. ej. f m t l STRENGTH — una línea de comando completa pegada se recorta automáticamente diff --git a/Resources/Strings.fr.resx b/Resources/Strings.fr.resx index d718c9b..20fc912 100644 --- a/Resources/Strings.fr.resx +++ b/Resources/Strings.fr.resx @@ -127,7 +127,7 @@ Les détails ont été écrits dans le journal. Si cela se reproduit, merci de l Hamlib trouvé via {0} Hamlib introuvable — installez WSJT-X ou téléchargez Hamlib - Hamlib introuvable. « Lancer les tests » ne sera disponible qu'une fois Hamlib installé. + Hamlib introuvable — « Lancer les tests » en a besoin (les tests sont des commandes Hamlib). « Trouver ma radio » et « Analyser le PC » fonctionnent sans. Tests en cours… Les {0} tests ont réussi. {0} test(s) échoué(s) — voir les résultats pour les détails. @@ -287,7 +287,7 @@ Continuer ? {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 + Installez WSJT-X, Fldigi ou le Hamlib autonome pour Windows — chacun inclut rigctl.exe. La commande ci-dessous installe WSJT-X avec winget 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} @@ -378,4 +378,34 @@ Continuer ? {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 + Le modèle 1 est la radio factice de Hamlib — toutes les réponses ci-dessous sont simulées, pas issues d'une radio + Réglages à saisir — copiez la ligne de votre programme + WSJT-X et JS8Call : File › Settings › Radio + Rig: {0} · Serial Port: {1} · Baud Rate: {2} · Data Bits: Eight · Stop Bits: {3} · Handshake: None · PTT Method: {4} + Rig: Hamlib NET rigctl · Network Server: {0} + Fldigi : Configure › Rig Control › Hamlib + Rig: {0} · Device: {1} · Baud rate: {2} · Stopbits: {3} · PTT via Hamlib command + Rig: Hamlib NET rigctl · Device: {0} + Winlink Express : Session › Radio Setup (sa propre liste de radios — choisissez le modèle le plus proche) + Radio: {0} · Serial Port: {1} · Baud: {2} · PTT: {3} + {0} est configuré pour {1} sur {2} à {3} bauds + {0} est configuré pour utiliser rigctl à {1} + {0} a déjà ces réglages + {0} est réglé sur {1} sur {2} à {3} bauds — changez {4} + Rig en {0} + Serial Port en {0} + Baud Rate en {0} + {0} utilise rigctl ({1}) — cela fonctionne tant que rigctld tourne + {0} est réglé sur {1} sur {2} à {3} bauds — gardez cela, ou passez à Hamlib NET rigctl pour partager la radio via rigctld + Hamlib n'est pas installé, les tests rigctl complets ne peuvent donc pas s'exécuter — une radio qui s'est identifiée sur le port série compte comme vérifiée. Installez WSJT-X ou Hamlib pour lancer les tests. + {0} sur {1} vérifiée par requête directe + notre propre trame est revenue — une radio CI-V avec Echo Back activé écoute à {0} bauds ; interrogation de chaque adresse CI-V connue à tour de rôle + Votre radio : {0} sur {1} — {2} bauds, 8N{3} (8 bits de données, sans parité, {3} stop) + Votre radio est joignable via rigctld à {0} + En cours d'exécution : {0} — presque certainement le programme qui occupe le port. Fermez-le (ou déconnectez-y le contrôle de la radio) et réessayez + Cette radio n'indique pas son VFO actif via Hamlib (normal pour {0} et la plupart des Icom) — pas un problème + Cette radio n'indique pas la force du signal via Hamlib ({0}) — pas un problème + Le pilote Hamlib du {0} n'implémente pas cette requête. + La radio répond ; cette commande n'existe simplement pas pour elle. WSJT-X et les autres programmes n'en ont pas besoin. + sous-commande seule, p. ex. f m t l STRENGTH — une ligne de commande complète collée est raccourcie pour vous diff --git a/Resources/Strings.ja.resx b/Resources/Strings.ja.resx index c01581a..ed11cb2 100644 --- a/Resources/Strings.ja.resx +++ b/Resources/Strings.ja.resx @@ -127,7 +127,7 @@ Hamlib を {0} 経由で検出 Hamlib が見つかりません — WSJT-X をインストールするか Hamlib をダウンロードしてください - Hamlib が見つかりません。Hamlib をインストールするまで「テストを実行」は使用できません。 + Hamlib が見つかりません — 「テストを実行」には必要です(テストは Hamlib のコマンドです)。「無線機を探す」と「PC をスキャン」は Hamlib なしで動作します。 テストを実行中… {0} 件のテストがすべて合格しました。 {0} 件のテストが失敗しました — 詳細は結果を確認してください。 @@ -287,7 +287,7 @@ {0} {1} (ビルド {2}、{3}) 見つかりません Hamlib がインストールされていないか、RigCheck が探す場所にありません。 - WSJT-X、Fldigi、または Windows 用の単体 Hamlib をインストールしてください — いずれにも rigctl.exe が含まれています + WSJT-X、Fldigi、または Windows 用の単体 Hamlib をインストールしてください — いずれにも rigctl.exe が含まれています。下のコマンドは winget で WSJT-X をインストールします RigCheck は WSJT-X、Fldigi、Hamlib のインストールフォルダーと PATH を探します 使用中: {0}: {1} {0} 個見つかりました — 使用中: {1}: {2}。その他: {3} @@ -378,4 +378,34 @@ {1} の {0}: {2} 件のテストが失敗 「接続」パネルを {0}(モデル {1})、{2}、{3} ボーに設定しました — 「テストを実行」はこの設定を使います 「無線機を探す」の記録 + モデル 1 は Hamlib のダミーリグです — 以下の応答はすべてシミュレーションで、無線機からのものではありません + 入力する設定 — お使いのプログラムの行をコピーしてください + WSJT-X と JS8Call: File › Settings › Radio + Rig: {0} · Serial Port: {1} · Baud Rate: {2} · Data Bits: Eight · Stop Bits: {3} · Handshake: None · PTT Method: {4} + Rig: Hamlib NET rigctl · Network Server: {0} + Fldigi: Configure › Rig Control › Hamlib + Rig: {0} · Device: {1} · Baud rate: {2} · Stopbits: {3} · PTT via Hamlib command + Rig: Hamlib NET rigctl · Device: {0} + Winlink Express: Session › Radio Setup(独自の無線機一覧 — 最も近い機種を選択) + Radio: {0} · Serial Port: {1} · Baud: {2} · PTT: {3} + {0} は {2} の {1}({3} ボー)に設定されています + {0} は {1} の rigctl を使う設定です + {0} はすでにこの設定です + {0} は {2} の {1}({3} ボー)に設定されています — {4} に変更してください + Rig を {0} + Serial Port を {0} + Baud Rate を {0} + {0} は rigctl({1})を使用します — rigctld が動作していれば動きます + {0} は {2} の {1}({3} ボー)に設定されています — そのままにするか、rigctld 経由で無線機を共有するために Hamlib NET rigctl に切り替えてください + Hamlib がインストールされていないため、rigctl の完全なテストは実行できません — シリアルポートで自ら名乗った無線機は検証済みとみなします。テストを実行するには WSJT-X または Hamlib をインストールしてください。 + {1} の {0} は直接問い合わせで検証済み + 自分のフレームがそのまま返ってきました — Echo Back が有効な CI-V 無線機が {0} ボーで受信しています。既知の CI-V アドレスを順に問い合わせます + あなたの無線機: {1} の {0} — {2} ボー、8N{3}(データ 8 ビット、パリティなし、ストップ {3} ビット) + あなたの無線機は {0} の rigctld 経由で利用できます + 現在実行中: {0} — ほぼ確実にポートを占有しているプログラムです。終了する(またはそのリグ制御を切断する)してから、もう一度お試しください + この無線機は Hamlib 経由でアクティブ VFO を報告しません({0} や多くの Icom 機では正常)— 問題ありません + この無線機は Hamlib 経由で信号強度を報告しません({0})— 問題ありません + {0} 用の Hamlib ドライバーはこの問い合わせを実装していません。 + 無線機は応答しています。このコマンドがこの機種に存在しないだけです。WSJT-X などのプログラムには不要です。 + サブコマンドのみ(例: f m t l STRENGTH)— 完全なコマンド行を貼り付けても自動的に短縮されます diff --git a/Resources/Strings.resx b/Resources/Strings.resx index e286e78..440e1cd 100644 --- a/Resources/Strings.resx +++ b/Resources/Strings.resx @@ -429,7 +429,7 @@ Details have been written to the log. If this keeps happening, please report it Hamlib not found — install WSJT-X or download Hamlib - Hamlib not found. Run Tests will be unavailable until Hamlib is installed. + Hamlib not found — Run Tests needs it (the tests are Hamlib commands). Find my radio and Scan PC work without it. Running tests… @@ -932,7 +932,7 @@ Continue? Hamlib is not installed, or not where RigCheck looks. - Install WSJT-X, Fldigi, or the standalone Hamlib for Windows — each includes rigctl.exe + Install WSJT-X, Fldigi, or the standalone Hamlib for Windows — each includes rigctl.exe. The command below installs WSJT-X with winget RigCheck looks in the WSJT-X, Fldigi, and Hamlib install folders and on PATH @@ -1213,4 +1213,98 @@ Continue? FIND MY RADIO TRANSCRIPT + + Model 1 is Hamlib's dummy rig — every reply below is simulated, not from a radio + Shown in the raw console whenever a command is sent with Hamlib model 1. + + + Settings to enter — copy the line for your program + + + WSJT-X and JS8Call: File › Settings › Radio + + + Rig: {0} · Serial Port: {1} · Baud Rate: {2} · Data Bits: Eight · Stop Bits: {3} · Handshake: None · PTT Method: {4} + Field labels as they appear in WSJT-X. Translate only if WSJT-X is translated in your language. {0} rig name, {1} port, {2} baud, {3} stop bits, {4} PTT method. + + + Rig: Hamlib NET rigctl · Network Server: {0} + + + Fldigi: Configure › Rig Control › Hamlib + + + Rig: {0} · Device: {1} · Baud rate: {2} · Stopbits: {3} · PTT via Hamlib command + Field labels as they appear in Fldigi. {0} rig, {1} device, {2} baud, {3} stop bits. + + + Rig: Hamlib NET rigctl · Device: {0} + + + Winlink Express: Session › Radio Setup (its own radio list — pick the nearest model) + + + Radio: {0} · Serial Port: {1} · Baud: {2} · PTT: {3} + + + {0} is configured for {1} on {2} at {3} baud + + + {0} is configured to use rigctl at {1} + + + {0} already has these settings + + + {0} is set to {1} on {2} at {3} baud — change {4} + {0} program, {1} its configured rig, {2} its port, {3} its baud, {4} comma list built from Handoff_DiffRig/Port/Baud. + + + Rig to {0} + + + Serial Port to {0} + + + Baud Rate to {0} + + + {0} uses rigctl ({1}) — that works whenever rigctld is running + + + {0} is set to {1} on {2} at {3} baud — keep that, or switch it to Hamlib NET rigctl to share the radio through rigctld + + + Hamlib is not installed, so the full rigctl tests cannot run — a radio that named itself over the serial port counts as verified. Install WSJT-X or Hamlib to run the tests. + + + {0} on {1} verified by direct query + + + our own frame came back — a CI-V radio with Echo Back on is listening at {0} baud; asking each known CI-V address in turn + + + Your radio: {0} on {1} — {2} baud, 8N{3} (8 data bits, no parity, {3} stop) + + + Your radio is reachable through rigctld at {0} + + + Running right now: {0} — almost certainly the program holding the port. Close it (or disconnect its rig control) and try again + + + This radio does not report its active VFO through Hamlib (normal for {0} and most Icoms) — not a problem + + + This radio does not report signal strength through Hamlib ({0}) — not a problem + + + Hamlib's driver for the {0} does not implement this query. + + + The radio is answering; this one command simply does not exist for it. WSJT-X and the other programs do not need it. + + + subcommand only, e.g. f m t l STRENGTH — a pasted full command line is trimmed for you + diff --git a/Resources/Styles.xaml b/Resources/Styles.xaml index 843f7b9..89e838b 100644 --- a/Resources/Styles.xaml +++ b/Resources/Styles.xaml @@ -19,6 +19,7 @@ + diff --git a/RigCheck.csproj b/RigCheck.csproj index b3c5b3d..9a46bfa 100644 --- a/RigCheck.csproj +++ b/RigCheck.csproj @@ -35,9 +35,9 @@ RigCheck Assets\rigcheck.ico app.manifest - 0.6.8 - 0.6.8.0 - 0.6.8.0 + 0.7.0 + 0.7.0.0 + 0.7.0.0 My Computer Guru LLC Copyright © 2025-2026 My Computer Guru LLC RigCheck by ShackDesk diff --git a/Services/ConfigClueService.cs b/Services/ConfigClueService.cs new file mode 100644 index 0000000..1d6aca9 --- /dev/null +++ b/Services/ConfigClueService.cs @@ -0,0 +1,178 @@ +using RigCheck.Models; +using Serilog; +using System.IO; + +namespace RigCheck.Services; + +/// +/// Reads what the operator's own digital-mode programs are already +/// configured to use. This is the best clue Find my radio has: most of the +/// time the model is right and only the port or the speed is wrong, and +/// "WSJT-X says COM5 at 9600, but the radio answered on COM3 at 19200" is +/// exactly the sentence the operator needs. +/// +/// Read-only, file-based, no process interaction. Handles WSJT-X and +/// JS8Call (same INI layout). Fldigi stores baud as a list index and the +/// model as its own numbering, so it is left for a later pass. +/// +public class ConfigClueService +{ + private readonly RadioPresetsService _presets; + private readonly DiscoveryDataService _data; + + public ConfigClueService(RadioPresetsService presets, DiscoveryDataService data) + { + _presets = presets; + _data = data; + } + + /// Every configuration RigCheck could read, in the order it looked. + public IReadOnlyList ReadAll() + { + var local = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var clues = new List(); + + foreach (var (app, path) in new[] + { + ("WSJT-X", Path.Combine(local, "WSJT-X", "WSJT-X.ini")), + ("JS8Call", Path.Combine(local, "JS8Call", "JS8Call.ini")), + }) + { + var clue = ReadWsjtxStyle(app, path); + if (clue is not null) clues.Add(clue); + } + return clues; + } + + // ── WSJT-X / JS8Call ────────────────────────────────────────────────── + // [Configuration] + // Rig=Icom IC-7300 Hamlib's rig name, or "Hamlib NET rigctl" + // CATSerialPort=COM3 + // CATSerialRate=19200 + // CATDataBits=Default|Seven|Eight + // CATStopBits=Default|One|Two + // CATHandshake=Default|None|XonXoff|Hardware + // PTTMethod=CAT|DTR|RTS|VOX + // NetworkServer=localhost:4532 (rigctl mode) + + private ConfigClue? ReadWsjtxStyle(string app, string path) + { + try + { + if (!File.Exists(path)) return null; + + var cfg = ReadIniSection(path, "Configuration"); + if (cfg.Count == 0) return null; + + var rigName = cfg.GetValueOrDefault("Rig", string.Empty); + if (string.IsNullOrEmpty(rigName) || rigName == "None") return null; + + var useRigctld = rigName.Contains("NET rigctl", StringComparison.OrdinalIgnoreCase); + int.TryParse(cfg.GetValueOrDefault("CATSerialRate", "0"), out var baud); + + return new ConfigClue( + App: app, + FilePath: path, + RigName: rigName, + ModelId: useRigctld ? 2 : ResolveModel(rigName), + Port: useRigctld ? cfg.GetValueOrDefault("NetworkServer", "localhost:4532") + : cfg.GetValueOrDefault("CATSerialPort", string.Empty), + Baud: baud, + DataBits: cfg.GetValueOrDefault("CATDataBits", "Default"), + StopBits: cfg.GetValueOrDefault("CATStopBits", "Default"), + Handshake: cfg.GetValueOrDefault("CATHandshake", "Default"), + PttMethod: cfg.GetValueOrDefault("PTTMethod", "CAT"), + UseRigctld: useRigctld); + } + catch (Exception ex) + { + Log.Debug(ex, "Could not read {App} configuration at {Path}", app, path); + return null; + } + } + + // WSJT-X stores Hamlib's display name ("Icom IC-7300"). Presets and the + // identity table use the same convention, so a name match is usually + // exact; fall back to a loose contains-match on the model part. + private int ResolveModel(string rigName) + { + var preset = _presets.Presets.FirstOrDefault(p => p.Name.Equals(rigName, StringComparison.OrdinalIgnoreCase)) + ?? _presets.Presets.FirstOrDefault(p => rigName.Contains(ModelPart(p.Name), StringComparison.OrdinalIgnoreCase)); + if (preset is not null) return preset.HamlibModelId; + + var id = _data.Ids.FirstOrDefault(i => i.Name.Equals(rigName, StringComparison.OrdinalIgnoreCase)) + ?? _data.Ids.FirstOrDefault(i => rigName.Contains(ModelPart(i.Name), StringComparison.OrdinalIgnoreCase)); + return id?.HamlibModelId ?? 0; + } + + // "Icom IC-7300" → "IC-7300" + private static string ModelPart(string name) + { + var space = name.IndexOf(' '); + return space > 0 ? name[(space + 1)..] : name; + } + + private static Dictionary ReadIniSection(string path, string section) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + var inScope = false; + + foreach (var raw in File.ReadLines(path)) + { + var line = raw.Trim(); + if (line.Length == 0 || line[0] is ';' or '#') continue; + + if (line[0] == '[') + { + inScope = line.Equals($"[{section}]", StringComparison.OrdinalIgnoreCase); + continue; + } + if (!inScope) continue; + + var eq = line.IndexOf('='); + if (eq <= 0) continue; + result[line[..eq].Trim()] = DecodeQtValue(line[(eq + 1)..].Trim()); + } + return result; + } + + // WSJT-X writes enum settings as Qt variants, e.g. + // @Variant(\0\0\0\x7f\0\0\0\x1eTransceiverFactory::PTTMethod\0\0\0\0\xfPTT_method_VOX\0) + // @Variant(...TransceiverFactory::StopBits\0\0\0\0\x12\x64\x65\x66\x61ult_stop_bits\0) + // Unescape \0 and \xHH (Qt escapes any hex-looking character that + // follows an escape, so a greedy hex read is safe), split on NUL, and + // take the last segment minus its length-prefix byte. Then strip the + // type prefixes/suffixes so callers see "VOX", "two", "none", "default". + private static string DecodeQtValue(string raw) + { + if (!raw.StartsWith("@Variant(", StringComparison.Ordinal)) return raw; + + var inner = raw[9..].TrimEnd(')'); + var sb = new System.Text.StringBuilder(); + for (int i = 0; i < inner.Length; i++) + { + if (inner[i] == '\\' && i + 1 < inner.Length) + { + if (inner[i + 1] == '0') { sb.Append('\0'); i++; continue; } + if (inner[i + 1] == 'x') + { + int j = i + 2; var hex = string.Empty; + while (j < inner.Length && hex.Length < 4 && Uri.IsHexDigit(inner[j])) hex += inner[j++]; + if (hex.Length > 0) { sb.Append((char)Convert.ToInt32(hex, 16)); i = j - 1; continue; } + } + } + sb.Append(inner[i]); + } + + var segments = sb.ToString().Split('\0', StringSplitOptions.RemoveEmptyEntries); + var last = segments.Length > 0 ? segments[^1] : string.Empty; + last = new string(last.SkipWhile(c => !char.IsLetter(c) && c != '_').ToArray()); + + return last + .Replace("PTT_method_", string.Empty) + .Replace("handshake_", string.Empty) + .Replace("_stop_bits", string.Empty) + .Replace("_stop_bit", string.Empty) + .Replace("_data_bits", string.Empty); + } +} diff --git a/Services/DiagnosisEngine.cs b/Services/DiagnosisEngine.cs index a87de2a..2c08777 100644 --- a/Services/DiagnosisEngine.cs +++ b/Services/DiagnosisEngine.cs @@ -30,6 +30,7 @@ public DiagnosticResult Diagnose(RigctlResult result, ConnectionConfig cfg) RigctlError.NoResponse => NoResponse(cfg), RigctlError.WrongModel => WrongModel(cfg), RigctlError.RigctldNotRunning => RigctldNotRunning(cfg), + RigctlError.NotSupported => NotSupported(cfg), RigctlError.Cancelled => Cancelled(), _ => Unknown(result.ErrorMessage), }; @@ -48,17 +49,46 @@ public DiagnosticResult Diagnose(RigctlResult result, ConnectionConfig cfg) FixCommand: null, LearnMoreUrl: BrandingInfo.HamlibDownloadUrl); - private static DiagnosticResult PortInUse(string port) => new( - Summary: Strings.Format("Diag_PortInUse_Summary", port), - Checks: + // The programs that hold a CAT port open, by process name. Checked only + // here, after the operator's own Run Tests has failed with "in use" — + // never on a timer or at startup. + private static readonly (string Process, string Name)[] PortHolders = + [ + ("wsjtx", "WSJT-X"), ("jt9", "WSJT-X"), ("fldigi", "Fldigi"), ("flrig", "Flrig"), + ("js8call", "JS8Call"), ("rigctld", "rigctld"), ("rigctld-wsjtx", "rigctld"), + ("RMS Express", "Winlink Express"), ("VARA", "VARA"), + ]; + + private static DiagnosticResult PortInUse(string port) + { + var running = PortHolders + .Where(h => + { + var found = System.Diagnostics.Process.GetProcessesByName(h.Process); + foreach (var p in found) p.Dispose(); + return found.Length > 0; + }) + .Select(h => h.Name) + .Distinct() + .ToList(); + + var checks = new List(); + if (running.Count > 0) + checks.Add(Strings.Format("Diag_PortInUse_Running", string.Join(", ", running))); + checks.AddRange( [ Strings.Get("Diag_PortInUse_1"), Strings.Get("Diag_PortInUse_2"), Strings.Get("Diag_PortInUse_3"), Strings.Get("Diag_PortInUse_4"), - ], - FixCommand: null, - LearnMoreUrl: null); + ]); + + return new( + Summary: Strings.Format("Diag_PortInUse_Summary", port), + Checks: checks.ToArray(), + FixCommand: null, + LearnMoreUrl: BrandingInfo.HelpUrl + "-port-in-use"); + } private static DiagnosticResult PortNotFound(string port) => new( Summary: Strings.Format("Diag_PortNotFound_Summary", port), @@ -120,7 +150,16 @@ public DiagnosticResult Diagnose(RigctlResult result, ConnectionConfig cfg) Strings.Format("Diag_Rigctld_3", cfg.RigctldPort), Strings.Get("Diag_Rigctld_4"), ], - FixCommand: $"rigctld -m {cfg.ModelId} -r {cfg.ComPort} -s {cfg.BaudRate} -t {cfg.RigctldPort}", + // rigctld sits beside rigctl under the same naming (rigctld-wsjtx for WSJT-X's copy) + FixCommand: $"{RigctlCommandBuilder.ExeName.Replace("rigctl", "rigctld")} -m {cfg.ModelId} -r {cfg.ComPort}" + + (cfg.BaudRate > 0 ? $" -s {cfg.BaudRate}" : string.Empty) + + $" -t {cfg.RigctldPort}", + LearnMoreUrl: BrandingInfo.HelpUrl + "-firewall"); + + private static DiagnosticResult NotSupported(ConnectionConfig cfg) => new( + Summary: Strings.Format("Diag_NotSupported_Summary", cfg.RadioModelName), + Checks: [Strings.Get("Diag_NotSupported_1")], + FixCommand: null, LearnMoreUrl: null); private static DiagnosticResult Cancelled() => new( diff --git a/Services/DiscoveryEngine.cs b/Services/DiscoveryEngine.cs index f2ee9f3..c1600be 100644 --- a/Services/DiscoveryEngine.cs +++ b/Services/DiscoveryEngine.cs @@ -11,7 +11,11 @@ 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 + bool CheckNetwork = true, // rigctld and Flrig ports + IReadOnlyList Clues = null!) // what WSJT-X etc. are configured for +{ + public IReadOnlyList Clues { get; init; } = Clues ?? []; +} /// /// Find my radio, stages 0–2: inventory the ticked ports, rank protocol @@ -25,7 +29,8 @@ public record DiscoveryRequest( 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 const int QuietMs = 60; // reply is complete after this much silence (echo and answer arrive as two frames) + private const int AddressedTimeoutMs = 150; // CI-V answers within a few ms; the address sweep must stay quick private readonly DiscoveryDataService _data; private readonly RadioPresetsService _presets; @@ -121,17 +126,25 @@ private static IEnumerable CheckNetworkTransports() // ── 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. + // panel, then the family WSJT-X / JS8Call are configured for, 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 and any configured + // baud moved to the front for their families — the common failure is + // "right radio, wrong port or speed", so the configured values are the + // best first guess and the configured port is probed first by the caller. private List<(RigFamily Family, int Baud)> RankCandidates(ComPortInfo port, DiscoveryRequest request) { var preferred = _data.FamilyForModel(request.PreferredModelId); var hinted = _data.FamilyForVendor(port.RadioFamily); + var clueFamilies = request.Clues + .Select(c => (Family: _data.FamilyForModel(c.ModelId), c.Baud)) + .Where(x => x.Family is not null) + .ToList(); var families = _data.Families - .OrderByDescending(f => f == preferred ? 2 : 0) + .OrderByDescending(f => f == preferred ? 3 : 0) + .ThenByDescending(f => clueFamilies.Any(c => c.Family == f) ? 2 : 0) .ThenByDescending(f => f == hinted ? 1 : 0) .ThenByDescending(f => f.Weight) .ToList(); @@ -139,10 +152,11 @@ private static IEnumerable CheckNetworkTransports() 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) + var first = new List(); + if (family == preferred && request.PreferredBaud > 0) first.Add(request.PreferredBaud); + first.AddRange(clueFamilies.Where(c => c.Family == family && c.Baud > 0).Select(c => c.Baud)); + + foreach (var baud in first.Distinct().Concat(family.Bauds.Where(b => !first.Contains(b)))) list.Add((family, baud)); } return list; @@ -159,9 +173,27 @@ private static IEnumerable CheckNetworkTransports() ProbeOperations.TryParse(family.Probe, out var op); var (_, parsed) = await ExchangeAsync(probe, port.PortName, op, events, ct); + // CI-V: the first frame goes to the broadcast address, which rigs act + // on but never answer. If only our own echo came back (Echo Back is + // on and the speed is right) — or nothing at all, since Echo Back can + // be off — address each known rig in turn. Addresses come from + // rig_ids.json; the frame layout is fixed here. + if (op == ProbeOperation.IcomReadId && parsed.Kind is not (ProbeReplyKind.Identity or ProbeReplyKind.Frequency)) + { + if (parsed.Kind == ProbeReplyKind.Echo) + events.Add(new(ProbeEventKind.Note, port.PortName, Strings.Format("Disc_Echo", baud))); + + foreach (var address in CivAddresses(family, request)) + { + ct.ThrowIfCancellationRequested(); + (_, parsed) = await ExchangeAsync(probe, port.PortName, op, events, ct, address, AddressedTimeoutMs); + if (parsed.Kind is ProbeReplyKind.Identity or ProbeReplyKind.Frequency) break; + } + } + // 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) + if (parsed.Kind is (ProbeReplyKind.None or ProbeReplyKind.Acknowledged or ProbeReplyKind.Echo) && family.FallbackProbe is not null && ProbeOperations.TryParse(family.FallbackProbe, out var fallback)) { @@ -179,13 +211,32 @@ private static IEnumerable CheckNetworkTransports() return (rig, events); } + // Every CI-V address rig_ids.json knows for this family, the address of + // the radio chosen in the Connection panel first. + private IEnumerable CivAddresses(RigFamily family, DiscoveryRequest request) + { + var entries = _data.Ids.Where(i => i.Family.Equals(family.Id, StringComparison.OrdinalIgnoreCase)).ToList(); + var ordered = entries + .OrderByDescending(i => i.HamlibModelId == request.PreferredModelId) + .ThenByDescending(i => request.Clues.Any(c => c.ModelId == i.HamlibModelId)) + .Select(i => i.Reply); + + var seen = new HashSet(); + foreach (var reply in ordered) + { + if (byte.TryParse(reply, System.Globalization.NumberStyles.HexNumber, null, out var address) && seen.Add(address)) + yield return address; + } + } + private static async Task<(byte[] Reply, ProbeReply Parsed)> ExchangeAsync( - SerialProbe probe, string portName, ProbeOperation op, List events, CancellationToken ct) + SerialProbe probe, string portName, ProbeOperation op, List events, CancellationToken ct, + byte civAddress = 0x00, int timeoutMs = ReplyTimeoutMs) { - var request = ProbeOperations.Bytes(op); + var request = ProbeOperations.Bytes(op, civAddress); events.Add(new(ProbeEventKind.Sent, portName, op.ToString(), ProbeOperations.Dump(request))); - var reply = await probe.ExchangeAsync(request, ReplyTimeoutMs, QuietMs, ct); + var reply = await probe.ExchangeAsync(request, timeoutMs, QuietMs, ct); if (reply.Length > 0) events.Add(new(ProbeEventKind.Received, portName, string.Empty, ProbeOperations.Dump(reply))); else diff --git a/Services/EnvironmentCheckService.cs b/Services/EnvironmentCheckService.cs index 64da3e9..a6e1f12 100644 --- a/Services/EnvironmentCheckService.cs +++ b/Services/EnvironmentCheckService.cs @@ -107,11 +107,14 @@ private static TestResult CheckWindows() private TestResult CheckHamlib() { + // Re-run the search: the operator may have installed WSJT-X since + // RigCheck started, and the cached answer must not stay stale. + _locator.Find(); 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)); + Diag("Env_HamlibNone", 2, fixCommand: BrandingInfo.WsjtxWingetCommand, learnMore: BrandingInfo.HamlibDownloadUrl)); } var (path, via) = copies[0]; diff --git a/Services/HamlibLocatorService.cs b/Services/HamlibLocatorService.cs index fa8797c..3575eac 100644 --- a/Services/HamlibLocatorService.cs +++ b/Services/HamlibLocatorService.cs @@ -49,7 +49,8 @@ public void Find() { _cachedPath = path; FoundVia = via; - Log.Information("rigctl.exe found via {Via}: {Path}", via, path); + RigctlCommandBuilder.ExeName = Path.GetFileNameWithoutExtension(path); + Log.Information("rigctl found via {Via}: {Path}", via, path); return; } } @@ -74,20 +75,28 @@ public void Find() public IEnumerable<(string Path, string Via)> CandidatePaths() { // ── 1. WSJT-X bundled Hamlib ───────────────────────────────────── - // WSJT-X ships rigctl.exe in its own bin directory. - var wsjtxDirs = new[] + // WSJT-X ships Hamlib in its bin folder under its OWN names: + // rigctl-wsjtx.exe and rigctld-wsjtx.exe (confirmed on 3.0.2, + // installed by winget to C:\WSJT\wsjtx). Older builds used plain + // rigctl.exe, so both are tried. The install folder also comes from + // the Uninstall key, which covers a non-default install path. + var wsjtxDirs = new List { @"C:\WSJT\wsjtx\bin", @"C:\Program Files\WSJT-X\bin", @"C:\Program Files (x86)\WSJT-X\bin", }; - // Also check registry for WSJT-X install location var wsjtxReg = RegistryInstallPath(@"SOFTWARE\WSJT-X", "InstallDir"); if (wsjtxReg is not null) - wsjtxDirs = [..wsjtxDirs, Path.Combine(wsjtxReg, "bin")]; + wsjtxDirs.Add(Path.Combine(wsjtxReg, "bin")); + foreach (var dir in UninstallKeyInstallDirs("wsjtx", "WSJT-X")) + wsjtxDirs.Add(Path.Combine(dir, "bin")); - foreach (var d in wsjtxDirs) - yield return (Path.Combine(d, "rigctl.exe"), "WSJT-X"); + foreach (var d in wsjtxDirs.Distinct(StringComparer.OrdinalIgnoreCase)) + { + yield return (Path.Combine(d, "rigctl-wsjtx.exe"), "WSJT-X"); + yield return (Path.Combine(d, "rigctl.exe"), "WSJT-X"); + } // ── 2. Fldigi bundled Hamlib ────────────────────────────────────── var fldigiDirs = new[] @@ -115,9 +124,14 @@ public void Find() yield return (Path.Combine(d, "rigctl.exe"), "Hamlib standalone"); // ── 4. PATH ─────────────────────────────────────────────────────── - var fromPath = FindInPath("rigctl.exe"); - if (fromPath is not null) - yield return (fromPath, "PATH"); + // Either name: a WSJT-X user who added its bin folder to PATH has + // rigctl-wsjtx, not rigctl. + foreach (var exe in new[] { "rigctl.exe", "rigctl-wsjtx.exe" }) + { + var fromPath = FindInPath(exe); + if (fromPath is not null) + yield return (fromPath, "PATH"); + } } // ── Helpers ────────────────────────────────────────────────────────── @@ -134,6 +148,53 @@ public void Find() return null; } + /// + /// Install folders of programs whose Uninstall entry matches any of the + /// given name fragments: InstallLocation when set, else the folder of + /// the uninstaller (NSIS installers such as WSJT-X's set only that). + /// + private static IEnumerable UninstallKeyInstallDirs(params string[] nameFragments) + { + var found = new List(); + 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) + { + try + { + using var key = root.OpenSubKey(sub); + if (key is null) continue; + foreach (var name in key.GetSubKeyNames()) + { + using var app = key.OpenSubKey(name); + var display = app?.GetValue("DisplayName") as string ?? string.Empty; + if (!nameFragments.Any(f => name.Contains(f, StringComparison.OrdinalIgnoreCase) + || display.Contains(f, StringComparison.OrdinalIgnoreCase))) + continue; + + var location = app?.GetValue("InstallLocation") as string; + if (string.IsNullOrWhiteSpace(location)) + { + var uninstaller = (app?.GetValue("UninstallString") as string ?? string.Empty).Trim('"'); + location = Path.GetDirectoryName(uninstaller.Split(".exe", StringSplitOptions.None)[0] + ".exe"); + } + if (!string.IsNullOrWhiteSpace(location)) + found.Add(location.TrimEnd('\\')); + } + } + catch (Exception ex) + { + Log.Debug(ex, "Uninstall key scan failed under {Root}\\{Sub}", root.Name, sub); + } + } + return found; + } + private static string? RegistryInstallPath(string subKey, string valueName) { try diff --git a/Services/HamlibRunnerService.cs b/Services/HamlibRunnerService.cs index d2c4074..5b94dae 100644 --- a/Services/HamlibRunnerService.cs +++ b/Services/HamlibRunnerService.cs @@ -48,10 +48,11 @@ public async Task RunAsync(RigctlCommand command, var startInfo = new ProcessStartInfo { FileName = rigctlPath, - Arguments = string.Join(" ", command.Args.Select(QuoteIfNeeded)), + Arguments = string.Join(" ", Verbosity.Concat(command.Args).Select(QuoteIfNeeded)), UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, + RedirectStandardInput = true, // closed right after start: rigctl must never wait for a keyboard CreateNoWindow = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8, @@ -75,6 +76,7 @@ public async Task RunAsync(RigctlCommand command, }; process.Start(); + process.StandardInput.Close(); process.BeginOutputReadLine(); process.BeginErrorReadLine(); @@ -91,19 +93,25 @@ await process "rigctl did not respond within the timeout period."); } - var stdout = stdoutBuilder.ToString().Trim(); + var stdout = StripBanner(stdoutBuilder.ToString()).Trim(); var stderr = stderrBuilder.ToString().Trim(); var exitCode = process.ExitCode; Log.Debug("rigctl exit={Exit} stdout={Out} stderr={Err}", exitCode, stdout, stderr); - if (exitCode == 0) + // rigctl exits 0 even when the command itself failed (seen with + // "v" on an IC-7300: "Feature not available", exit 0, plus a + // trace dump). Treat Hamlib's own error text as failure and + // keep only the message, never the trace. + var commandError = HamlibErrorLine(stdout) ?? HamlibErrorLine(stderr); + if (exitCode == 0 && commandError is null) return RigctlResult.Success(command, stdout); - // Exit code non-zero — classify the error - var error = ClassifyError(exitCode, stderr, stdout); - return RigctlResult.Failure(command, error, stderr.Length > 0 ? stderr : stdout); + var error = commandError is not null && commandError.Contains("Feature not available", StringComparison.OrdinalIgnoreCase) + ? RigctlError.NotSupported + : ClassifyError(exitCode, stderr, stdout); + return RigctlResult.Failure(command, error, commandError ?? (stderr.Length > 0 ? stderr : stdout)); } catch (OperationCanceledException) { @@ -116,6 +124,37 @@ await process } } + // ── Verbosity ───────────────────────────────────────────────────────── + // At its default verbosity rigctl (Hamlib 4.7.1) says NOTHING when it + // fails — exit code 2 and empty output — so a port held by WSJT-X, a + // missing rigctld, and a radio that is switched off all looked the same. + // "-vv" makes it name the cause on stderr: + // serial_open: serial port COM3 is already open + // serial_open: serial port COM9 does not exist + // network_open: failed to connect to localhost:4532 + // and adds one banner line to stdout on success, which is stripped so + // the command's own output stays parseable: + // Opened rig model 3073, 'IC-7300' + + private static readonly string[] Verbosity = ["-vv"]; + + /// + /// Hamlib reports a failed command as "get_vfo: error = Feature not + /// available" or a bare "Feature not available" line, often buried in a + /// trace dump. Returns that one line, or null when there is none. + /// + private static string? HamlibErrorLine(string output) => + output.Split('\n') + .Select(l => l.Trim()) + .FirstOrDefault(l => l.Contains(": error = ", StringComparison.Ordinal) + || l.StartsWith("Feature not available", StringComparison.OrdinalIgnoreCase) + || l.StartsWith("Communication timed out", StringComparison.OrdinalIgnoreCase) + || l.StartsWith("IO error", StringComparison.OrdinalIgnoreCase)); + + private static string StripBanner(string stdout) => + string.Join('\n', stdout.Split('\n') + .Where(l => !l.StartsWith("Opened rig model", StringComparison.Ordinal))); + // ── Error classification ────────────────────────────────────────────── private static RigctlError ClassifyError(int exitCode, string stderr, string stdout) @@ -123,20 +162,21 @@ private static RigctlError ClassifyError(int exitCode, string stderr, string std var combined = (stderr + stdout).ToLowerInvariant(); if (combined.Contains("port in use") || combined.Contains("access denied") - || combined.Contains("sharing violation")) + || combined.Contains("sharing violation") || combined.Contains("already open")) return RigctlError.PortInUse; if (combined.Contains("timeout") || combined.Contains("timed out")) return RigctlError.Timeout; if (combined.Contains("no such device") || combined.Contains("cannot open") - || combined.Contains("file not found")) + || combined.Contains("file not found") || combined.Contains("does not exist")) return RigctlError.PortNotFound; if (combined.Contains("invalid model") || combined.Contains("unknown rig")) return RigctlError.WrongModel; - if (combined.Contains("connection refused") || combined.Contains("rigctld")) + if (combined.Contains("connection refused") || combined.Contains("failed to connect") + || combined.Contains("rigctld")) return RigctlError.RigctldNotRunning; if (combined.Contains("no response") || combined.Contains("io_err")) @@ -178,6 +218,8 @@ public enum RigctlError NoResponse, WrongModel, RigctldNotRunning, + /// The radio's Hamlib backend does not implement this query (get_vfo on many Icoms). Not a fault. + NotSupported, Cancelled, Unknown, } diff --git a/Services/HandoffBuilder.cs b/Services/HandoffBuilder.cs new file mode 100644 index 0000000..a9dc780 --- /dev/null +++ b/Services/HandoffBuilder.cs @@ -0,0 +1,82 @@ +using RigCheck.Localization; +using RigCheck.Models; + +namespace RigCheck.Services; + +/// +/// Stage 4 of Find my radio — the deliverable. Turns a verified rig into +/// the exact fields to enter in WSJT-X / JS8Call, Fldigi, and Winlink +/// Express, and compares them with what those programs are configured to +/// use today so the operator sees precisely what to change. +/// +/// Field names are the programs' own labels. They are resources so a +/// translator can match a localized WSJT-X, but they are not RigCheck's +/// words to change. +/// +public class HandoffBuilder +{ + private readonly DiscoveryDataService _data; + + public HandoffBuilder(DiscoveryDataService data) => _data = data; + + public IReadOnlyList Build(DiscoveredRig rig, IReadOnlyList clues) + { + var lines = new List(); + var family = _data.Family(rig.FamilyId); + var stopBits = family?.HandoffStopBits ?? rig.HandoffStopBits; + var ptt = clues.FirstOrDefault(c => !string.IsNullOrEmpty(c.PttMethod))?.PttMethod ?? "CAT"; + + lines.Add(new(TranscriptKind.Found, Strings.Get("Handoff_Heading"))); + + if (rig.UseRigctld) + { + lines.Add(new(TranscriptKind.Note, Strings.Get("Handoff_WsjtxPath"))); + lines.Add(new(TranscriptKind.Command, Strings.Format("Handoff_WsjtxNet", rig.Port))); + lines.Add(new(TranscriptKind.Note, Strings.Get("Handoff_FldigiPath"))); + lines.Add(new(TranscriptKind.Command, Strings.Format("Handoff_FldigiNet", rig.Port))); + } + else + { + lines.Add(new(TranscriptKind.Note, Strings.Get("Handoff_WsjtxPath"))); + lines.Add(new(TranscriptKind.Command, Strings.Format("Handoff_Wsjtx", rig.ModelName, rig.Port, rig.Baud, stopBits, ptt))); + lines.Add(new(TranscriptKind.Note, Strings.Get("Handoff_FldigiPath"))); + lines.Add(new(TranscriptKind.Command, Strings.Format("Handoff_Fldigi", rig.ModelName, rig.Port, rig.Baud, stopBits))); + lines.Add(new(TranscriptKind.Note, Strings.Get("Handoff_WinlinkPath"))); + lines.Add(new(TranscriptKind.Command, Strings.Format("Handoff_Winlink", rig.ModelName, rig.Port, rig.Baud, ptt))); + } + + // ── Configured vs. what actually works ──────────────────────────── + foreach (var clue in clues) + lines.Add(new(TranscriptKind.Note, Compare(clue, rig))); + + return lines; + } + + /// One sentence per program: matches, works via rigctl, or what to change. + private static string Compare(ConfigClue clue, DiscoveredRig rig) + { + if (clue.UseRigctld) + return Strings.Format("Handoff_DiffRigctl", clue.App, clue.RigName); + + if (rig.UseRigctld) + return Strings.Format("Handoff_DiffSerialVsRigctld", clue.App, clue.RigName, clue.Port, clue.Baud); + + var changes = new List(); + if (clue.ModelId != rig.HamlibModelId) + changes.Add(Strings.Format("Handoff_DiffRig", rig.ModelName)); + if (!clue.Port.Equals(rig.Port, StringComparison.OrdinalIgnoreCase)) + changes.Add(Strings.Format("Handoff_DiffPort", rig.Port)); + if (clue.Baud != rig.Baud) + changes.Add(Strings.Format("Handoff_DiffBaud", rig.Baud)); + + return changes.Count == 0 + ? Strings.Format("Handoff_DiffMatch", clue.App) + : Strings.Format("Handoff_DiffChange", clue.App, clue.RigName, clue.Port, clue.Baud, string.Join(", ", changes)); + } + + /// The opening line for each program whose settings were read, shown before the sweep. + public static string Describe(ConfigClue clue) => + clue.UseRigctld + ? Strings.Format("Handoff_ClueRigctl", clue.App, clue.Port) + : Strings.Format("Handoff_Clue", clue.App, clue.RigName, clue.Port, clue.Baud); +} diff --git a/Services/ProbeOperations.cs b/Services/ProbeOperations.cs index ed75624..d011a0e 100644 --- a/Services/ProbeOperations.cs +++ b/Services/ProbeOperations.cs @@ -29,6 +29,8 @@ public enum ProbeReplyKind None, /// Bytes came back but nothing recognisable — often the wrong baud rate. Garbage, + /// Only our own frame came back: a CI-V rig with Echo Back on is listening at this speed but was not addressed. + Echo, /// 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. @@ -48,12 +50,17 @@ public static class ProbeOperations 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 + /// + /// The exact bytes sent for an operation. For CI-V, + /// is the rig address the frame is sent to. 00 is broadcast: rigs act on + /// it but by design never answer, so identification has to address the + /// rig directly — the addresses come from rig_ids.json. + /// + public static byte[] Bytes(ProbeOperation op, byte civAddress = CivBroadcast) => 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.IcomReadId => [0xFE, 0xFE, civAddress, CivController, 0x19, 0x00, 0xFD], + ProbeOperation.IcomReadFreq => [0xFE, 0xFE, civAddress, CivController, 0x03, 0xFD], ProbeOperation.YaesuLegacyReadFreq => [0x00, 0x00, 0x00, 0x00, 0x03], _ => throw new ArgumentOutOfRangeException(nameof(op)), }; @@ -119,10 +126,13 @@ private static ProbeReply ParseCiv(byte[] reply, byte expectCmd) 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); + // Any well-formed frame from a non-controller address means CI-V at + // this baud. Only our own frame back means Echo Back is on and the rig + // is listening at this speed — it just was not addressed. + var frames = CivFrames(reply).Where(f => f.Length >= 5).ToList(); + if (frames.Any(f => f[3] != CivController)) return new(ProbeReplyKind.Acknowledged); + if (frames.Count > 0) return new(ProbeReplyKind.Echo); + return new(ProbeReplyKind.Garbage); } private static IEnumerable CivFrames(byte[] buf) diff --git a/Services/RigctlCommandBuilder.cs b/Services/RigctlCommandBuilder.cs index 633f4ad..9e09502 100644 --- a/Services/RigctlCommandBuilder.cs +++ b/Services/RigctlCommandBuilder.cs @@ -17,6 +17,14 @@ namespace RigCheck.Services; /// public class RigctlCommandBuilder { + /// + /// The name the copyable commands start with. "rigctl" for standalone + /// Hamlib; WSJT-X ships its copy as "rigctl-wsjtx", and a command the + /// operator pastes must use the name that actually exists on their PC. + /// Set by HamlibLocatorService when it finds the exe. + /// + public static string ExeName { get; set; } = "rigctl"; + // ── Connection argument builders ───────────────────────────────────── /// @@ -49,11 +57,21 @@ public static RigctlCommand NetworkArgs(ConnectionConfig cfg) => // ── Test commands ──────────────────────────────────────────────────── - /// Test 1: Open connection — just connect and disconnect (no subcommand). + /// + /// Test 1: Open connection. Asks for the frequency: rigctl with no + /// subcommand does not "just connect", it enters interactive mode and + /// waits on stdin forever — which read as a radio timeout on a working + /// IC-7300. Hamlib's open already exchanges with the rig; the reply to + /// "f" proves the round trip. + /// public static RigctlCommand TestConnection(ConnectionConfig cfg) { var conn = ConnArgs(cfg); - return conn with { DisplayCommand = $"rigctl {conn.ConnectionLabel}" }; + return conn with + { + Args = [..conn.Args, "f"], + DisplayCommand = $"{ExeName} {conn.ConnectionLabel} f", + }; } /// Test 2: Get frequency — 'f' subcommand. @@ -63,7 +81,7 @@ public static RigctlCommand GetFrequency(ConnectionConfig cfg) return conn with { Args = [..conn.Args, "f"], - DisplayCommand = $"rigctl {conn.ConnectionLabel} f" + DisplayCommand = $"{ExeName} {conn.ConnectionLabel} f" }; } @@ -74,7 +92,7 @@ public static RigctlCommand GetMode(ConnectionConfig cfg) return conn with { Args = [..conn.Args, "m"], - DisplayCommand = $"rigctl {conn.ConnectionLabel} m" + DisplayCommand = $"{ExeName} {conn.ConnectionLabel} m" }; } @@ -85,7 +103,7 @@ public static RigctlCommand GetPtt(ConnectionConfig cfg) return conn with { Args = [..conn.Args, "t"], - DisplayCommand = $"rigctl {conn.ConnectionLabel} t" + DisplayCommand = $"{ExeName} {conn.ConnectionLabel} t" }; } @@ -96,7 +114,7 @@ public static RigctlCommand GetSmeter(ConnectionConfig cfg) return conn with { Args = [..conn.Args, "l", "STRENGTH"], - DisplayCommand = $"rigctl {conn.ConnectionLabel} l STRENGTH" + DisplayCommand = $"{ExeName} {conn.ConnectionLabel} l STRENGTH" }; } @@ -107,7 +125,7 @@ public static RigctlCommand GetVfo(ConnectionConfig cfg) return conn with { Args = [..conn.Args, "v"], - DisplayCommand = $"rigctl {conn.ConnectionLabel} v" + DisplayCommand = $"{ExeName} {conn.ConnectionLabel} v" }; } @@ -118,7 +136,7 @@ public static RigctlCommand SetFrequency(ConnectionConfig cfg, long frequencyHz) return conn with { Args = [..conn.Args, "F", frequencyHz.ToString()], - DisplayCommand = $"rigctl {conn.ConnectionLabel} F {frequencyHz}" + DisplayCommand = $"{ExeName} {conn.ConnectionLabel} F {frequencyHz}" }; } @@ -130,16 +148,38 @@ public static RigctlCommand SetFrequency(ConnectionConfig cfg, long frequencyHz) /// public static RigctlCommand RawCommand(ConnectionConfig cfg, string rawInput) { - var tokens = rawInput.Trim().Split(' ', - StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var tokens = StripConnectionPrefix(rawInput.Trim().Split(' ', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); var conn = ConnArgs(cfg); return conn with { Args = [..conn.Args, ..tokens], - DisplayCommand = $"rigctl {conn.ConnectionLabel} {rawInput.Trim()}" + DisplayCommand = $"{ExeName} {conn.ConnectionLabel} {string.Join(' ', tokens)}" }; } + // Operators paste the whole line the results panel shows them — + // "rigctl-wsjtx -m 3073 -r COM3 -s 115200 f" — into a console that only + // wants "f". Drop the exe name and the connection options so the panel's + // settings apply once, not twice. + private static readonly HashSet ConnectionOptions = + ["-m", "-r", "-s", "-t", "-C", "-p", "-P", "-c", "--model", "--rig-file", "--serial-speed"]; + + private static string[] StripConnectionPrefix(string[] tokens) + { + if (tokens.Length == 0 || !tokens[0].StartsWith("rigctl", StringComparison.OrdinalIgnoreCase)) + return tokens; + + var rest = new List(); + for (int i = 1; i < tokens.Length; i++) + { + if (ConnectionOptions.Contains(tokens[i]) && i + 1 < tokens.Length) { i++; continue; } + if (tokens[i].StartsWith("-v", StringComparison.Ordinal)) continue; + rest.Add(tokens[i]); + } + return rest.ToArray(); + } + // ── Private helpers ────────────────────────────────────────────────── private static RigctlCommand ConnArgs(ConnectionConfig cfg) => diff --git a/Services/TestRunnerService.cs b/Services/TestRunnerService.cs index a70e03d..ac9aef3 100644 --- a/Services/TestRunnerService.cs +++ b/Services/TestRunnerService.cs @@ -196,7 +196,13 @@ private async Task TestGetSmeterAsync(ConnectionConfig cfg, Cancella if (result.IsSuccess) return TestResult.Warning( TestId.GetSmeter, - Strings.Format("Msg_SmeterUnparsed", result.RawOutput), + Strings.Format("Msg_SmeterUnparsed", FirstLine(result.RawOutput)), + cmd.DisplayCommand); + + if (result.Error == RigctlError.NotSupported) + return TestResult.Warning( + TestId.GetSmeter, + Strings.Format("Msg_SmeterNotSupported", cfg.RadioModelName), cmd.DisplayCommand); return TestResult.Fail( @@ -215,7 +221,17 @@ private async Task TestGetVfoAsync(ConnectionConfig cfg, Cancellatio if (result.IsSuccess) return TestResult.Pass( TestId.GetVfo, - Strings.Format("Msg_Vfo", result.RawOutput.Trim()), + Strings.Format("Msg_Vfo", FirstLine(result.RawOutput)), + cmd.DisplayCommand); + + // Many Icom backends (IC-7300 included) have no get_vfo: Hamlib says + // "Feature not available". The radio is fine; the query just does + // not exist for it. A warning, so the run can still be all green + // in spirit, with the reason spelled out. + if (result.Error == RigctlError.NotSupported) + return TestResult.Warning( + TestId.GetVfo, + Strings.Format("Msg_VfoNotSupported", cfg.RadioModelName), cmd.DisplayCommand); return TestResult.Fail( @@ -305,6 +321,10 @@ private static bool TryParseSmeter(string raw, out string sLabel, out string dbm return true; } + /// The first non-empty line of rigctl output — the value, never a trace. + private static string FirstLine(string output) => + output.Split('\n', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()?.Trim() ?? string.Empty; + private static IEnumerable RemainingTests() => [ TestId.GetFrequency, TestId.GetMode, TestId.GetPtt, diff --git a/TRANSLATING.md b/TRANSLATING.md index d53c433..cc76748 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% (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 | +| English (source) | `Strings.resx` | 100% (373 of 373) | Source | +| German (`de`) | `Strings.de.resx` | 100% (373 of 373) | Machine translated — review wanted | +| Spanish (`es`) | `Strings.es.resx` | 100% (373 of 373) | Machine translated — review wanted | +| French (`fr`) | `Strings.fr.resx` | 100% (373 of 373) | Machine translated — review wanted | +| Japanese (`ja`) | `Strings.ja.resx` | 100% (373 of 373) | Machine translated — review wanted | --- diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index 43907ea..c618602 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -19,6 +19,8 @@ public partial class MainViewModel : ObservableObject private readonly TestRunnerService _testRunner; private readonly EnvironmentCheckService _envCheck; private readonly DiscoveryEngine _discovery; + private readonly ConfigClueService _clues; + private readonly HandoffBuilder _handoff; private readonly LogExportService _logExport; private readonly HamlibLocatorService _hamlib; private readonly SettingsService _settings; @@ -66,6 +68,8 @@ public MainViewModel( TestRunnerService testRunner, EnvironmentCheckService envCheck, DiscoveryEngine discovery, + ConfigClueService clues, + HandoffBuilder handoff, LogExportService logExport, HamlibLocatorService hamlib, SettingsService settings, @@ -77,6 +81,8 @@ public MainViewModel( _testRunner = testRunner; _envCheck = envCheck; _discovery = discovery; + _clues = clues; + _handoff = handoff; _logExport = logExport; _hamlib = hamlib; _settings = settings; @@ -175,6 +181,13 @@ private async Task ScanEnvironmentAsync() using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); var suite = await _envCheck.RunAllAsync(Connection.BuildConfig(), progress, cts.Token); + // The scan re-searched for Hamlib; reflect the answer in the + // banner and the Run Tests gate without a restart. + CheckHamlib(); + OnPropertyChanged(nameof(IsHamlibAvailable)); + OnPropertyChanged(nameof(IsHamlibMissing)); + RunTestsCommand.NotifyCanExecuteChanged(); + Results.SetSuiteResult(suite); TaskCompleted?.Invoke(suite.AllPassed); StatusMessage = Strings.Format("Status_ScanDone", suite.WarningCount + suite.FailCount); @@ -216,7 +229,21 @@ private async Task FindRadioAsync(IReadOnlyList ports) try { using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3)); - var request = new DiscoveryRequest(ports, Connection.ModelId, Connection.BaudRate); + // What the operator's own programs are set to is the best first + // guess: their port is probed first, and their radio stands in + // for the Connection panel when nothing is chosen there. + var clues = _clues.ReadAll(); + foreach (var clue in clues) + Results.AddTranscript(TranscriptKind.Note, HandoffBuilder.Describe(clue)); + + var orderedPorts = ports + .OrderByDescending(p => clues.Any(c => c.Port.Equals(p.PortName, StringComparison.OrdinalIgnoreCase))) + .ToList(); + var preferredModel = Connection.ModelId > 1 + ? Connection.ModelId + : clues.FirstOrDefault(c => c.ModelId > 1)?.ModelId ?? 0; + + var request = new DiscoveryRequest(orderedPorts, preferredModel, Connection.BaudRate, Clues: clues); Results.AddTranscript(TranscriptKind.Note, Strings.Format("Disc_Start", ports.Count)); @@ -236,8 +263,26 @@ private async Task FindRadioAsync(IReadOnlyList ports) } // ── Stage 3: verify every find with the real test suite ─────── + // Without Hamlib there is no rigctl to verify with, but the probe + // already had a real exchange with the radio — a rig that named + // itself, or answered with a frequency inside a ham band, is + // verified by that exchange. Say so, rather than fail a step + // that cannot run. + var hamlibMissing = !_hamlib.IsAvailable; + if (hamlibMissing && found.Count > 0) + Results.AddTranscript(TranscriptKind.Note, Strings.Get("Disc_NoHamlibVerify")); + foreach (var rig in found) { + if (hamlibMissing) + { + if (rig.UseRigctld || rig.Score < 0.8) continue; + verified.Add((rig, new TestSuiteResult([], Connection.BuildConfig()))); + Results.AddTranscript(TranscriptKind.Found, + Strings.Format("Disc_VerifiedByProbe", rig.ModelName, rig.Port)); + continue; + } + Results.AddTranscript(TranscriptKind.Command, Strings.Format("Disc_Verifying", rig.ModelName, rig.Port, rig.Baud)); @@ -264,10 +309,23 @@ private async Task FindRadioAsync(IReadOnlyList ports) var best = verified.OrderByDescending(v => v.Rig.Score).First(); Connection.ApplyDiscovered(best.Rig); Results.SetSuiteResult(best.Suite); - Results.AddTranscript(TranscriptKind.Found, + + // The one sentence the operator came for. The probe itself + // used 8N1; the stop bits shown are what the radio's family + // expects from the digital-mode programs. + var summary = best.Rig.UseRigctld + ? Strings.Format("Disc_SummaryRigctld", best.Rig.Port) + : Strings.Format("Disc_Summary", best.Rig.ModelName, best.Rig.Port, best.Rig.Baud, best.Rig.HandoffStopBits); + Results.AddTranscript(TranscriptKind.Found, summary); + Results.AddTranscript(TranscriptKind.Note, Strings.Format("Disc_Applied", best.Rig.ModelName, best.Rig.HamlibModelId, best.Rig.Port, best.Rig.Baud)); + + // Stage 4: the settings to type into each program, and how + // they differ from what those programs use today. + foreach (var line in _handoff.Build(best.Rig, clues)) + Results.AddTranscript(line.Kind, line.Text); StatusMessage = verified.Count == 1 - ? Strings.Format("Status_DiscoveredOne", best.Rig.ModelName, best.Rig.Port) + ? summary : Strings.Format("Status_DiscoveredMany", verified.Count, best.Rig.ModelName, best.Rig.Port); } else diff --git a/ViewModels/ViewModels.cs b/ViewModels/ViewModels.cs index b44b91a..ec6f9c7 100644 --- a/ViewModels/ViewModels.cs +++ b/ViewModels/ViewModels.cs @@ -220,6 +220,11 @@ public void LoadFrom(RigCheckSettings s) var saved = AvailablePorts.FirstOrDefault(p => p.PortName == s.ComPort); if (saved is not null) SelectedPort = saved; + + // Show the matching preset in the picker without re-applying its + // defaults — the saved baud and serial settings must win. + _selectedPreset = AvailablePresets.FirstOrDefault(p => p.HamlibModelId == s.RadioModelId); + OnPropertyChanged(nameof(SelectedPreset)); } public void SaveTo(RigCheckSettings s) @@ -402,6 +407,13 @@ private async Task SendCommandAsync(ConnectionConfig cfg) var cmd = RigctlCommandBuilder.RawCommand(cfg, input); AddEntry(ConsoleEntryKind.Command, cmd.DisplayCommand); + // The test suite refuses model 1, but the console lets an operator + // type anything. Model 1 is Hamlib's dummy rig: every reply is + // simulated, so say so beside each command rather than let a + // convincing-looking answer stand. + if (cfg.ModelId == 1 && !cfg.UseRigctld) + AddEntry(ConsoleEntryKind.Error, Strings.Get("Console_DummyRig")); + IsRunning = true; try { diff --git a/Views/Converters.cs b/Views/Converters.cs index ee36589..0c9b174 100644 --- a/Views/Converters.cs +++ b/Views/Converters.cs @@ -61,3 +61,12 @@ public object Convert(object value, Type t, object p, CultureInfo c) public object ConvertBack(object value, Type t, object p, CultureInfo c) => throw new NotSupportedException(); } + +/// Visible while a string is empty — used for placeholder text over an empty TextBox. +public sealed class EmptyToVisibleConverter : IValueConverter +{ + public object Convert(object value, Type t, object p, CultureInfo c) => + string.IsNullOrEmpty(value as string) ? Visibility.Visible : Visibility.Collapsed; + public object ConvertBack(object value, Type t, object p, CultureInfo c) => + throw new NotSupportedException(); +} diff --git a/Views/MainWindow.xaml b/Views/MainWindow.xaml index 5b43c74..2d267a3 100644 --- a/Views/MainWindow.xaml +++ b/Views/MainWindow.xaml @@ -296,12 +296,21 @@