FT891.Core 2.1.0

dotnet add package FT891.Core --version 2.1.0
                    
NuGet\Install-Package FT891.Core -Version 2.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FT891.Core" Version="2.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FT891.Core" Version="2.1.0" />
                    
Directory.Packages.props
<PackageReference Include="FT891.Core" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FT891.Core --version 2.1.0
                    
#r "nuget: FT891.Core, 2.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FT891.Core@2.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FT891.Core&version=2.1.0
                    
Install as a Cake Addin
#tool nuget:?package=FT891.Core&version=2.1.0
                    
Install as a Cake Tool

FT891.Core

The library at the heart of the solution: a complete, async CAT client for the Yaesu FT-891, the protocol engine that drives it, the command table it relies on, and the transport abstraction that lets it talk to either real hardware or the simulator.

Target framework: net48 · Language: C# latest · No runtime dependencies beyond System.IO.Ports (for the serial transport). Every public member is XML-documented and the doc file ships in the NuGet package, so consumers get full IntelliSense.


Folder layout

FT891.Core/
├── FT891Exception.cs         the single exception type every failure surfaces as
├── FT891Ranges.cs            the radio's value ranges (IntRange) — the limits in one place
├── FrequencyFormat.cs        hz.ToFormattedString() "14.250.000", MHz/kHz strings, TryParseFrequency
├── MeterScale.cs             raw 0–255 meter values → S-units / watts / SWR
├── RadioMonitor.cs           background polling + change events for UI apps
├── Enums/
│   └── Enums.cs              OperatingMode, AgcMode, MeterType, TunerState,
│                             ScanMode, BreakInMode, BandSelect
├── Models/
│   ├── RadioInfo.cs          record: frequency, mode, TX, S-meter, split, channel
│   └── MeterReading.cs       record: meter type + value
├── Protocol/
│   ├── CatSpec.cs            command → reply-length table (per Yaesu 1909-C)
│   ├── CatSerialEngine.cs    the single read-to-';' TransceiveAsync
│   ├── FT891Cat.cs           ICatInterface implementation (one-liners)
│   ├── FT891Cat.Diagnostics.cs  RunDiagnosticAsync — PASS/FAIL sweep of every read
│   ├── ICatTransport.cs      the byte-pipe abstraction
│   ├── SerialPortTransport.cs   real radio (RS-232 / USB, 8-N-2)
│   ├── TcpCatTransport.cs    virtual serial port over TCP
│   ├── KeyingPort.cs         PTT/CW keying via the second COM port's RTS/DTR lines
│   └── Num.cs                Clamp helper (Math.Clamp is absent on net48)
├── Interface/
│   └── ICatInterface.cs      the full public surface (the canonical XML docs)
└── Properties/
    └── IsExternalInit.cs     polyfill so records/init compile on net48

How it fits together

1. CatSpec — the response-length table

A single dictionary maps each 2-letter command to the total number of bytes its reply contains, including the prefix and the trailing ;:

{ "FA", 12 },   // "FA014250000;"  → 2 + 9 + 1
{ "MD",  5 },   // "MD0" + 1 hex mode digit + ";"
{ "IF", 28 },   // full status frame
...

This is the only place that encodes per-command knowledge. The lengths come from the official Yaesu FT-891 CAT Operation Reference Book (1909-C), cross-checked against Hamlib's rigs/yaesu implementation. The engine reads each reply up to its ; terminator, so the table serves two purposes: presence of a key marks a command as one that replies (absent = set-only), and the value documents the frame size for error messages. KM is variable-length — its entry is the maximum.

2. CatSerialEngine.TransceiveAsync — the only I/O

public async Task<string> TransceiveAsync(string command, int expectedBytes = -1,
                                          CancellationToken cancellationToken = default)
  • Ensures the command ends with ;.
  • If expectedBytes is -1, looks it up in CatSpec from the 2-char prefix.
  • expectedBytes == 0 → a set-only command; write and return immediately.
  • Otherwise it reads exactly expectedBytes bytes in a loop (handling partial TCP/serial reads) — never scanning for a delimiter.
  • The blocking write/read runs off the caller's thread (Task.Run), so a serial timeout never freezes a UI thread despite the synchronous transports.
  • A SemaphoreSlim serialises access so concurrent callers can't interleave a write/read pair.
  • TimeoutRetryCount (default 0) re-issues the command on a timeout before giving up — only timeouts retry.

3. ICatTransport — serial or TCP

public interface ICatTransport : IDisposable
{
    bool IsOpen { get; }
    void Open();  void Close();  void DiscardInBuffer();
    void Write(string data);
    int  Read(byte[] buffer, int offset, int count);
}
  • SerialPortTransport — wraps System.IO.Ports.SerialPort configured the way the FT-891 expects: 38400 baud, 8 data bits, no parity, two stop bits, ASCII encoding, 2000 ms read/write timeout.
  • TcpCatTransport — treats a TCP socket as a virtual serial port. A socket read-timeout is surfaced as a timeout, so "the radio said nothing" always looks the same regardless of transport.

4. FT891Cat — one-liners over the engine

Every method is a single expression that builds a frame (for a set) or builds a frame, awaits the reply, and slices out the value (for a read):

public Task SetVfoAFrequencyAsync(long hz) => Set($"FA{hz:D9};");
public Task<long> GetVfoAFrequencyAsync()  => Get("FA;", r => long.Parse(r.Substring(2, 9)));

The private Get helper is the one place reply-parse failures are caught and re-thrown as a descriptive FT891Exception (see below).

Three constructors let you choose the transport:

new FT891Cat("COM3");                                  // real serial port
new FT891Cat(new TcpCatTransport("127.0.0.1", 4000));  // simulator / network
new FT891Cat(existingCatSerialEngine);                 // bring your own engine

API surface (ICatInterface)

Group Representative members
Connection Connect, Disconnect, IsConnected, Dispose, InterCommandDelayMs, TimeoutRetryCount
Calibration / diagnostics InitializeLibraryAsync (measure the radio, set the pacing), RunDiagnosticAsync (PASS/FAIL sweep of every read command)
VFO & frequency Get/SetVfoAFrequencyAsync, Get/SetVfoBFrequencyAsync, CopyVfoAToVfoBAsync, CopyVfoBToVfoAAsync, SwapVfosAsync, BandUp/DownAsync, SelectBandAsync, FrequencyUp/DownAsync, ZeroInAsync
Mode Get/SetModeAsync
Memory Get/SetMemoryChannelAsync, CopyVfoAToMemoryAsync, CopyMemoryToVfoAAsync, ChannelUpDownAsync, WriteMemoryAsync, ReadMemoryAsync, Store/RecallQuickMemoryBankAsync
TX / PTT / split IsTransmittingAsync, SetMoxAsync, Get/SetSplitAsync
Power Get/SetPowerAsync, Get/SetTxPowerAsync
Audio levels AF, RF, Mic, Squelch, Monitor — Get/Set…Async
RF / antenna Get/SetRfAttenuatorAsync, Get/SetPreampAsync, Get/SetTunerStateAsync
DSP / filters AGC, Noise Reduction (+level), Noise Blanker (+level), Auto Notch, Manual Notch, IF Shift, Bandwidth, Contour
CW Get/SetKeySpeedAsync, Get/SetKeyPitchAsync, Get/SetBreakInModeAsync, Get/SetSemiBreakInDelayAsync, Get/SetCwSpotAsync, Get/SetKeyerAsync, SendCwAsync, Get/SetKeyerMemoryAsync
VOX Get/SetVoxAsync, Get/SetVoxGainAsync, Get/SetVoxDelayAsync
Speech processor Get/SetSpeechProcessorAsync, Get/SetSpeechProcessorLevelAsync
Clarifier Get/SetClarifierAsync, ClarifierUp/DownAsync, ClarifierClearAsync
CTCSS / repeater Get/SetCtcssAsync, Get/SetCtcssDcsNumberAsync, Get/SetOffsetAsync
Scan Get/SetScanModeAsync
Meters / status GetSMeterAsync, ReadMeterAsync, GetBusyAsync, GetRadioInfoAsync, GetRadioIdAsync, GetOppositeVfoInfoAsync
Meter scaling MeterScale.ToSMeterDb / FormatSMeter / ToWatts / ToSwr — approximate conversions of the raw 0–255 values
Frequency formatting hz.ToFormattedString()"14.250.000", ToMegahertzString / ToKilohertzString, FrequencyFormat.TryParseFrequency — all culture-invariant
Ranges FT891Ranges.TxPowerWatts / KeySpeedWpm / … (IntRange with Clamp/Contains) — the radio's limits, public, in one place
Wire tracing LastCommand, LastResponse, LastResponseBytes(), FrameSent / FrameReceived events — the exact frames on the wire
Human summaries RadioInfo.ToFormattedString()"14.250.000 USB ch 025 TX", MeterReading.ToFormattedString()"S9+20" / "47 W" / "2.3:1"
Live monitoring RadioMonitorStart/Stop/StopAsync, FrequencyChanged/ModeChanged/TransmitChanged/SplitChanged/MemoryChannelChanged/SMeterChanged/StateChanged/MonitorError, PollIntervalMs/IncludeSMeter/UseSynchronizationContext
Keying (hardware) KeyingPortPtt (RTS) and Key (DTR) on the FT-891's second, non-CAT COM port; drops both lines on close
Display / UI Dimmer, Lock, Fast-step, Auto-information — Get/Set…Async
Message memory PlaybackAsync, LoadMessageAsync
Escape hatch SendRawCommandAsync(command, expectedBytes = -1)

Values are clamped to the radio's valid ranges before transmission (e.g. TX power 5–100 W, key speed 4–60 WPM). The ranges are public — FT891Ranges exposes every limit as an IntRange with Clamp/Contains, so your UI can pre-validate instead of being silently adjusted.


Error handling

Every runtime failure that escapes the library is a single exception type: FT891Exception. You only ever need one catch:

try
{
    await radio.SetVfoAFrequencyAsync(14_074_000);
    long hz = await radio.GetVfoAFrequencyAsync();
}
catch (FT891Exception ex)
{
    Console.WriteLine(ex.Message);
    // "Radio did not respond to 'FA;' (0/12 bytes received). Check the
    //  radio is on and the cable/port settings are correct."
    // "Malformed response to 'FA;': could not parse "FA?ERROR0000;"."
    // "Could not open the radio connection: Access to the port 'COM3' is denied."
}
  • The message says what the library was doing — the CAT command involved and, for parse failures, the raw bytes received.
  • The original error (TimeoutException, IOException, FormatException, …) is preserved in InnerException for logging/diagnosis.
  • The only exceptions not wrapped are argument-validation errors (ArgumentNullException etc.) — those indicate a bug in the calling code, not a runtime condition.
  • Disconnect() and Dispose() never throw.
  • RunDiagnosticAsync() never throws on command failures — it reports per-command PASS / FAIL — FT891Exception: message entries instead. (Cancellation is the one exception: it stops the run and propagates.)
  • Cancellation: every async method takes an optional CancellationToken. Cancellation throws OperationCanceledException (not FT891Exception). Because each underlying read blocks for up to the transport timeout, cancellation can take up to one timeout to take effect.
  • Retry: set TimeoutRetryCount (default 0) to automatically re-send a command that timed out, up to N extra attempts. Only timeouts retry — parse failures, not-connected, and transport faults fail immediately. When retries were used, the exception message notes the attempt count (e.g. "…after 3 attempts").

Upgrading from 1.x: code that caught TimeoutException, InvalidOperationException or FormatException from library calls should now catch FT891Exception instead.


Running modern C# on .NET Framework 4.8

The original source used several APIs/syntax that don't exist on net48. They are handled so the code stays clean and idiomatic:

Modern feature net48 accommodation
record types + init setters Properties/IsExternalInit.cs polyfill + <LangVersion>latest</LangVersion>
Math.Clamp Num.Clamp(value, min, max) helper
string.EndsWith(char) EndsWith(";", StringComparison.Ordinal)
range slicing s[..2] s.Substring(0, 2) (no System.Index/Range on net48)
System.IO.Ports NuGet package System.IO.Ports 10.0.8
net48 reference assemblies Microsoft.NETFramework.ReferenceAssemblies (cross-environment build)

Protocol provenance (2.1.0)

Before 2.1.0 the frame layouts were only self-consistent — the parser, the length table, the simulator and the tests all agreed with each other but had never been checked against the radio, and ~17 commands diverged (most famously IF, which is a 28-byte channel-first frame with no TX or split flags, not a 34-byte frequency-first one). Every frame is now aligned to the official FT-891 CAT Operation Reference Book (1909-C), cross-checked against Hamlib (rigs/yaesu/ft891.c, newcat.c), and the engine reads to the ; terminator so a wrong length can never stall a read again.


READMEs written by Claude Code Opus 4.8m Context - From CodeBase - TSGBMPPT

Product Compatible and additional computed target framework versions.
.NET Framework net48 is compatible.  net481 was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on FT891.Core:

Package Downloads
FT891.Simulator

A virtual Yaesu FT-891 transceiver over TCP for developing and testing CAT clients without hardware. Embed SimulatorServer in-process (as FT891.Tests does) or run it standalone.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.1.0 29 6/7/2026
2.0.0 59 6/6/2026
1.2.1 64 6/5/2026 1.2.1 is deprecated because it is no longer maintained.
1.2.0 67 6/5/2026 1.2.0 is deprecated because it is no longer maintained.