FT891.Core
2.1.0
dotnet add package FT891.Core --version 2.1.0
NuGet\Install-Package FT891.Core -Version 2.1.0
<PackageReference Include="FT891.Core" Version="2.1.0" />
<PackageVersion Include="FT891.Core" Version="2.1.0" />
<PackageReference Include="FT891.Core" />
paket add FT891.Core --version 2.1.0
#r "nuget: FT891.Core, 2.1.0"
#:package FT891.Core@2.1.0
#addin nuget:?package=FT891.Core&version=2.1.0
#tool nuget:?package=FT891.Core&version=2.1.0
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
expectedBytesis-1, looks it up inCatSpecfrom the 2-char prefix. expectedBytes == 0→ a set-only command; write and return immediately.- Otherwise it reads exactly
expectedBytesbytes 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
SemaphoreSlimserialises 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— wrapsSystem.IO.Ports.SerialPortconfigured 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 | RadioMonitor — Start/Stop/StopAsync, FrequencyChanged/ModeChanged/TransmitChanged/SplitChanged/MemoryChannelChanged/SMeterChanged/StateChanged/MonitorError, PollIntervalMs/IncludeSMeter/UseSynchronizationContext |
| Keying (hardware) | KeyingPort — Ptt (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 inInnerExceptionfor logging/diagnosis. - The only exceptions not wrapped are argument-validation errors
(
ArgumentNullExceptionetc.) — those indicate a bug in the calling code, not a runtime condition. Disconnect()andDispose()never throw.RunDiagnosticAsync()never throws on command failures — it reports per-commandPASS/FAIL — FT891Exception: messageentries instead. (Cancellation is the one exception: it stops the run and propagates.)- Cancellation: every async method takes an optional
CancellationToken. Cancellation throwsOperationCanceledException(notFT891Exception). 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,InvalidOperationExceptionorFormatExceptionfrom library calls should now catchFT891Exceptioninstead.
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 | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET Framework | net48 is compatible. net481 was computed. |
-
.NETFramework 4.8
- System.IO.Ports (>= 10.0.8)
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.