FT891.Simulator
2.1.0
dotnet add package FT891.Simulator --version 2.1.0
NuGet\Install-Package FT891.Simulator -Version 2.1.0
<PackageReference Include="FT891.Simulator" Version="2.1.0" />
<PackageVersion Include="FT891.Simulator" Version="2.1.0" />
<PackageReference Include="FT891.Simulator" />
paket add FT891.Simulator --version 2.1.0
#r "nuget: FT891.Simulator, 2.1.0"
#:package FT891.Simulator@2.1.0
#addin nuget:?package=FT891.Simulator&version=2.1.0
#tool nuget:?package=FT891.Simulator&version=2.1.0
FT891.Simulator
A virtual Yaesu FT-891 exposed over TCP. It behaves like the real radio's
serial port: it reads ;-terminated CAT frames, validates each one, keeps a full
RadioState, updates that state on a valid set, replies with a
correctly formatted, byte-exact frame on a valid read, and silently drops
anything malformed — logging every frame (accepted or rejected) with the reason.
Use it to develop and test a CAT client with no hardware attached.
Target framework: net48 (console app) · References FT891.Core
Running it
# default port 4000
dotnet run --project FT891.Simulator
# custom port
dotnet run --project FT891.Simulator -- 4005
…or run the built executable directly:
FT891.Simulator\bin\Debug\net48\FT891.Simulator.exe 4000
On start it prints:
[SIM] FT-891 virtual radio listening on 127.0.0.1:4000
FT-891 CAT simulator running. Connect a client (default baud-equivalent TCP).
Press Ctrl+C or Enter to stop.
Connect with any TCP client — most naturally the TcpCatTransport from
FT891.Core:
using var radio = new FT891Cat(new TcpCatTransport("127.0.0.1", 4000));
radio.Connect();
The virtual radio's state
RadioState is a plain object holding the entire radio:
frequency (VFO-A/B), mode, memory channel, power & TX power, split, all audio
levels, RF attenuator / preamp / tuner, the full DSP chain (AGC, NR, NB, notch,
IF-shift, bandwidth, contour), CW settings (+ five keyer memories), VOX, speech
processor, clarifier, CTCSS/repeater, scan, and the display/UI toggles.
Defaults are chosen so a read issued before any set still returns a well-formed, parseable frame — e.g. VFO-A powers up at 14.250 MHz, VFO-B at 7.100 MHz, mode USB, TX power 50 W.
How a frame is processed
bytes in ──► assemble until ';' ──► validate ──► classify ──► act ──► log
Validation (in order)
- Terminator / length — at least 3 bytes and ends with
;, else rejected. - 2-char alpha prefix — both leading characters must be letters, else rejected.
- Known command — the prefix must exist in
CatSpec, else rejected (this is what makes the simulator drop unknown commands like a real radio).
READ vs SET, decided by byte count
| Condition | Treated as |
|---|---|
Prefix is read-only telemetry (MR IF OI ID SM RM BY RS TS MS UL NA) |
READ |
AC with length 6 (tuner set is one byte shorter than its reply) |
SET |
length == the CatSpec reply length |
SET |
| length < the reply length (the short query form) | READ |
| length > the reply length | rejected (wrong byte count) |
Action
- Valid SET →
ApplySetparses the payload intoRadioState. No reply. - Valid READ →
BuildResponseformats the answer out ofRadioStateand sends back exactly the number of bytesCatSpecpromises. - Invalid → dropped, no reply. A parse error during handling is caught and logged rather than crashing the server.
Example log
[RX] FA014250000; -> SET FA accepted
[RX] FA; -> READ -> FA014250000;
[RX] MD0123; -> REJECTED: byte count 7 exceeds 5 for 'MD'
[RX] ZQ; -> REJECTED: unknown command 'ZQ'
Embedding it (used by the tests)
SimulatorServer is a self-contained, in-process server — the integration tests
spin one up on an ephemeral port and drive a real FT891Cat against it.
// port 0 → the OS picks a free port; read it back from .Port
var server = new SimulatorServer(port: 0, log: _ => { }); // quiet
server.Start();
var radio = new FT891Cat(new TcpCatTransport("127.0.0.1", server.Port));
radio.Connect();
// ... drive the radio ...
server.Stop(); // also via Dispose()
| Member | Purpose |
|---|---|
SimulatorServer(int port = 4000, RadioState state = null, Action<string> log = null) |
Construct; inject state and/or a log sink. |
Start() |
Bind, begin accepting clients (each handled concurrently). |
Stop() / Dispose() |
Cancel and stop the listener. |
Port |
The actually-bound port (meaningful after Start, esp. with port 0). |
RequestedPort |
The port asked for. |
State |
The live RadioState. |
A virtual band, and a CW beacon
By default the S-meter reads a static value. Set SimulatorServer.SignalSource
(a Func<long,int> mapping the tuned VFO-A frequency to an S-meter level 0–255)
and the meter follows it instead — so a client can tune around and actually find
signals. BY (busy) then derives from BusyThreshold. It's opt-in: leave it null
and nothing changes.
server.SignalSource = hz => (Math.Abs(hz - 14_060_000) < 500) ? 200 : 10; // a signal on 14.060
The Morse namespace builds on this to transmit CW:
| Type | What it does |
|---|---|
MorseCode |
International Morse tables; sanitise text, encode to a keyed timeline, decode patterns. |
MorseDecoder |
Real-time decoder — feed it key on/off samples (e.g. from the S-meter), get text. |
MorseAbbreviations |
Expand Q-codes / prosigns / operating shorthand into plain English. |
CwBeacon |
Keys a block of text into Morse on a loop; plug its SignalAt into SignalSource and the S-meter on that frequency rises and falls with the keying. |
var beacon = new CwBeacon(14_058_000, "CQ DE M0XYZ ES 73 K", wpm: 16);
server.SignalSource = hz => beacon.SignalAt(hz, Environment.TickCount);
// A client tuned to 14.058 now reads decodable CW on its S-meter.
FT891.Demo's Morse screen is the receiver for exactly this.
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
- FT891.Core (>= 2.1.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.