Packet.Aprs 0.41.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Packet.Aprs --version 0.41.0
                    
NuGet\Install-Package Packet.Aprs -Version 0.41.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="Packet.Aprs" Version="0.41.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Packet.Aprs" Version="0.41.0" />
                    
Directory.Packages.props
<PackageReference Include="Packet.Aprs" />
                    
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 Packet.Aprs --version 0.41.0
                    
#r "nuget: Packet.Aprs, 0.41.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 Packet.Aprs@0.41.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=Packet.Aprs&version=0.41.0
                    
Install as a Cake Addin
#tool nuget:?package=Packet.Aprs&version=0.41.0
                    
Install as a Cake Tool

Packet.Aprs

APRS encoder and decoder: every APRS 1.2 data type, both directions, from AX.25 frames or TNC2 / APRS-IS text.

Packet.Aprs encodes and decodes Automatic Packet Reporting System packets per APRS12c, the merged APRS 1.0 + 1.1 + 1.2 reference, with Understanding APRS Packets and the APRS device identification database. It is spec-strict, with a named, individually switchable flag for each defect real traffic is full of. Part of Packet.NET, a .NET amateur-radio / AX.25 packet stack; it sits above Packet.Ax25 and decodes what an AX.25 UI frame carries.

Checked against real traffic: three million full-feed APRS-IS packets decode without an exception, every spec-clean one re-encodes to identical data, and field values agree with Ham::APRS::FAP (the aprs.fi parser) except where FAP is wrong. See docs/aprs-validation.md.

Install

dotnet add package Packet.Aprs

The shape of it

Everything goes through AprsPacket: a header (Source, Destination, Path), the raw Information bytes, the decoded Data, and any Diagnostics.

using Packet.Aprs;

AprsPacket packet = AprsPacket.Decode("N0CALL-9>APZ001,WIDE1-1,qAR,M0LTE-10:!5130.00N/00007.00W>088/036/A=001234Hello");

if (packet.Data is AprsPositionReport p)
{
    Console.WriteLine($"{p.Position.Latitude}, {p.Position.Longitude}"); // 51.5, -0.11666666666666667
    Console.WriteLine(p.Symbol.Description);                             // Car
    Console.WriteLine($"{p.CourseDegrees} deg at {p.SpeedKnots} kn");    // 88 deg at 36 kn
    Console.WriteLine($"{p.AltitudeFeet} ft, \"{p.Comment}\"");          // 1234 ft, "Hello"
}

Data is a closed hierarchy you pattern-match on: AprsPositionReport, AprsMicEReport, AprsObjectReport, AprsTextMessage, AprsWeatherReport, AprsTelemetryReport and so on, with AprsUnrecognizedData for anything that isn't APRS or can't be read. Positions, objects, items and Mic-E reports share a base, AprsPositionedData, so one case handles every kind of thing that has a place on a map.

Decoding never throws because of the information field: Data is always set, and Diagnostics says what was wrong. Only an unusable header (no SOURCE>DEST:) throws AprsFormatException; AprsPacket.TryDecode returns false instead. Encoding is the opposite: it is strict, and throws ArgumentException naming the offending property rather than write something the spec forbids.

Worked examples

1. Put every station from an APRS-IS feed on a map

APRS-IS servers send one packet per line, in the same TNC2 text form a TNC's monitor prints, plus comment lines starting with #. Connecting to a server is up to you (this package has no APRS-IS client); each line goes straight into TryDecode.

foreach (string line in lines)
{
    if (line.StartsWith('#') || !AprsPacket.TryDecode(line, out AprsPacket? packet))
    {
        continue; // server comments, and the rare line with no usable header
    }

    // An IGate's third-party packet carries the real one inside it.
    AprsPacket station = packet.Data is AprsThirdPartyTraffic relayed ? relayed.Packet : packet;

    switch (station.Data)
    {
        case AprsObjectReport { IsAlive: false } killed:
            Remove(killed.Name);
            break;
        case AprsObjectReport o:
            Plot(o.Name, o.Position, o.Symbol);
            break;
        case AprsItemReport i:
            Plot(i.Name, i.Position, i.Symbol);
            break;
        case AprsPositionedData p: // position reports and Mic-E
            Plot(station.Source.Value, p.Position, p.Symbol);
            break;
    }
}

Objects and items are things a station reports on behalf of something else (a repeater, a hurricane, a net's meeting point), so they are plotted under their own name rather than the sender's.

2. Work out who is moving, and on what radio

Mic-E is the compact format mobile radios send. Half of the position is packed into the destination address (TRQP7T here), which is why decoding always takes the whole packet rather than just the information field.

AprsPacket packet = AprsPacket.Decode("N1JCM-9>TRQP7T,WA1PLE-4*:`c'wl|+>/`\"4-}_%");
var mobile = (AprsMicEReport)packet.Data;

Console.WriteLine($"{mobile.Position.Latitude}, {mobile.Position.Longitude}");      // 42.179, -71.1985
Console.WriteLine($"{mobile.SpeedKnots} kn heading {mobile.CourseDegrees} deg");  // 9 kn heading 215 deg
Console.WriteLine($"{mobile.AltitudeFeet:F0} ft");                                // 72 ft
Console.WriteLine(mobile.Message);                                                // OffDuty
Console.WriteLine(mobile.Device);                                                 // Yaesu FTM-400DR
Console.WriteLine(mobile.MessagingCapable);                                       // True
Console.WriteLine(packet.Path[0]);                                                // WA1PLE-4*

The radio model comes from the type code and suffix around the status text, looked up in the embedded device database. AprsDeviceIdentification.Identify(packet) does the same for any packet, using the destination address (the "tocall") when it isn't Mic-E.

3. Read a weather station

Weather rides on a position report with the weather symbol. Values keep the units APRS sends, with the unit in the property name, so converting is explicit.

AprsPacket packet = AprsPacket.Decode("W1TG2>APU25N:@091842z4256.20N/07049.42W_310/004g015t081r000p033P002h54b10001/ - Hampton, NH Wx");
var report = (AprsPositionReport)packet.Data;
AprsWeather wx = report.Weather!;

double celsius = (wx.TemperatureFahrenheit!.Value - 32) / 1.8;
Console.WriteLine($"{celsius:F1} C, {wx.HumidityPercent}% humidity, {wx.PressureMillibars} hPa"); // 27.2 C, 54% humidity, 1000.1 hPa
Console.WriteLine($"wind {wx.WindDirectionDegrees} deg at {wx.WindSpeedMph} mph, gusting {wx.WindGustMph}"); // wind 310 deg at 4 mph, gusting 15
Console.WriteLine($"{wx.RainSinceMidnightInches} in of rain since midnight");                // 0.02 in of rain since midnight

// APRS timestamps are partial (this one is day 09, 18:42 UTC); resolve against when you heard it.
DateTime? observed = report.Timestamp?.Resolve(receivedAt);      // 2026-09-09 18:42 UTC
Console.WriteLine(AprsDeviceIdentification.Identify(packet)?.Model); // UI-View32

The spec gives a weather report no comment field, but almost every station sends one. By default the text is kept in report.Comment and the packet carries a warning saying so:

Console.WriteLine(packet.Diagnostics[0].Code); // WeatherComment

4. Send a message and know when it arrived

var message = new AprsTextMessage { Addressee = "N0CALL-7", Text = "Meet at the club at 8?", MessageId = "42" };
AprsPacket outgoing = AprsPacket.Create("M0LTE-9", "APZ001", message, "WIDE1-1,WIDE2-1");
Console.WriteLine(outgoing); // M0LTE-9>APZ001,WIDE1-1,WIDE2-1::N0CALL-7 :Meet at the club at 8?{42

// Later, from the feed:
AprsPacket incoming = AprsPacket.Decode("N0CALL-7>APDR16,WIDE1-1,qAR,N0CALL-10::M0LTE-9  :ack42");
if (incoming.Data is AprsMessageAck ack && ack.Addressee == "M0LTE-9" && ack.AcknowledgedId == message.MessageId)
{
    // Delivered: stop retrying.
}

Retrying and remembering what's outstanding is up to you; the codec keeps no state. Addressee padding, the { before the ID and reply-acks (ReplyAck) are handled for you. AprsBulletin, AprsMessageReject and the rest of the message family work the same way.

5. Announce a repeater

An object puts something that isn't your station on the map. APRS 1.2 adds a voice frequency, tone and offset that radios can tune to directly.

var repeater = new AprsObjectReport
{
    Name = "MYRPTR",
    IsAlive = true,
    Timestamp = AprsTimestamp.DayHoursMinutes(25, 18, 30), // or AprsTimestamp.FromDateTime(DateTime.UtcNow)
    Position = new AprsPosition(51.4543, -0.9781),
    Symbol = AprsSymbol.Parse("/r"),
    Frequency = new AprsVoiceFrequency { FrequencyMHz = 145.725m, ToneType = AprsToneType.Tone, ToneValue = 118, OffsetKHz = -600 },
    Comment = "Reading repeater",
};

Console.WriteLine(AprsPacket.Create("M0LTE", "APZ001", repeater, "WIDE2-1"));
// M0LTE>APZ001,WIDE2-1:;MYRPTR   *251830z5127.26N/00058.69Wr145.725MHz T118 -060 Reading repeater

// To take it off everyone's map, send it again marked dead:
AprsPacket kill = AprsPacket.Create("M0LTE", "APZ001", repeater with { IsAlive = false }, "WIDE2-1");

Positions go out to the hundredth of a minute the format allows (about 18 m), so what decodes back is the rounded position. Set IsCompressed = true for the shorter base-91 form, which is also more precise.

6. Turn telemetry into real values

A telemetry station sends five raw analog values and eight bits, and separately (as messages to itself) what each channel is called, its unit, and the equation that turns the raw number into a value.

string[] lines =
[
    "M0LTE-11>APZ001::M0LTE-11 :PARM.Battery,Temp,Humidity,Light,Pressure,Door,Mains,Fan",
    "M0LTE-11>APZ001::M0LTE-11 :UNIT.V,degC,%,lux,hPa,open,on,on",
    "M0LTE-11>APZ001::M0LTE-11 :EQNS.0,0.075,0,0,0.5,-40,0,0.4,0,0,4,0,0,1,900",
    "M0LTE-11>APZ001:T#123,172,123,150,050,113,10100000",
];

AprsTelemetryParameterNames? names = null;
AprsTelemetryUnits? units = null;
AprsTelemetryCoefficients? equations = null;
foreach (string line in lines)
{
    switch (AprsPacket.Decode(line).Data)
    {
        case AprsTelemetryParameterNames n: names = n; break;
        case AprsTelemetryUnits u: units = u; break;
        case AprsTelemetryCoefficients e: equations = e; break;
        case AprsTelemetryReport t when names is not null && units is not null && equations is not null:
            for (int channel = 1; channel <= t.Analog.Count; channel++)
            {
                if (t.Analog[channel - 1] is decimal raw)
                {
                    Console.WriteLine($"{names.Names[channel - 1]}: {equations.Scale(channel, raw):0.##} {units.Units[channel - 1]}");
                }
            }

            for (int bit = 0; bit < 8 && 5 + bit < names.Names.Count; bit++)
            {
                bool set = ((t.Digital >> bit) & 1) == 1;
                Console.WriteLine($"{names.Names[5 + bit]}: {(set ? units.Units[5 + bit] : "-")}");
            }

            break;
    }
}
// Battery: 12.9 V
// Temp: 21.5 degC
// Humidity: 60 %
// Light: 200 lux
// Pressure: 1013 hPa
// Door: open
// Mains: -
// Fan: on

In a real receiver, keep the definitions per station (the addressee is the station they describe).

7. Check your own beacon against the spec

Strict rejects anything the spec doesn't allow and says why, which makes it a handy linter for a beacon text or a new tracker's output.

const string beacon = "N1EOE>APN391,N1NCI-3*,WIDE2-1:!4216.95n/07243.20w#phg6230/ Easthampton MA";

AprsPacket strict = AprsPacket.Decode(beacon, AprsParseOptions.Strict);
foreach (AprsDiagnostic d in strict.Diagnostics)
{
    Console.WriteLine($"{d.Severity} {d.Code}: {d.Message}");
}
// Error LowercaseHemisphere: hemisphere 'n' must be upper case (UAP 5.9)

AprsPacket lenient = AprsPacket.Decode(beacon); // decodes, with two LowercaseHemisphere warnings

8. On the air with Packet.Ax25 or a KISS TNC

On RF the packet is an AX.25 UI frame. ToAx25Frame() writes it in KISS form (no flags, no FCS), which is what a KISS TNC takes and what Ax25Frame.ToBytes() produces, so frames cross between the two libraries as bytes:

AprsPacket received = AprsPacket.DecodeAx25(frame.ToBytes());        // an Ax25Frame from Packet.Ax25
Ax25Frame.TryParse(packet.ToAx25Frame(), out Ax25Frame? toSend);     // one to hand to a transport

await kiss.SendAsync(0, KissCommand.Data, packet.ToAx25Frame(), ct); // or straight to a KISS TNC

DecodeAx25 checks for a UI frame with PID 0xF0 itself. APRS-IS carries names that aren't AX.25 callsigns (qAR, TCPIP, WHO-IS), so the header uses AprsAddress, which keeps any address as written; convert to and from Packet.NET's strict Callsign when you need one:

AprsAddress me = AprsAddress.FromCallsign(new Callsign("M0LTE", 9));
if (received.Source.TryGetCallsign(out Callsign source)) { /* an AX.25 station, not an APRS-IS name */ }

ToAx25Frame throws if an address isn't valid AX.25.

Strict or lenient

AprsPacket.Decode(line);                              // AprsParseOptions.Lenient: accept and warn
AprsPacket.Decode(line, AprsParseOptions.Strict);     // only packets that follow the spec
AprsPacket.Decode(line, AprsParseOptions.Lenient with { AllowWeatherComment = false });

Each tolerance is a named flag, so you can turn off exactly the ones you don't want. Every flag, the spec rule it relaxes, and how common it is on the network are listed in docs/strict-vs-pragmatic-audit.md. About 88% of APRS-IS traffic passes Strict.

Good to know

  • Units are in the names. SpeedKnots, AltitudeFeet, TemperatureFahrenheit, WindSpeedMph, PressureMillibars: the units APRS uses on air, so nothing is converted behind your back.
  • To pass a packet on unchanged, send Information. It holds the bytes exactly as received. Re-encoding Data gives the canonical form, which can differ from what the sender wrote (filler bytes, field order, padding) while meaning the same thing.
  • Timestamps are partial. APRS timestamps have no year and some have no date; AprsTimestamp.Resolve(reference) picks the matching time nearest the moment you received the packet.
  • Text is UTF-8. Invalid UTF-8 falls back to Latin-1 byte by byte, with a NonUtf8Text warning.
  • Mic-E needs the destination. Decode the whole packet; the information field alone isn't enough.

What's covered

Data type Types
Positions (! = / @), uncompressed and compressed, with timestamps, ambiguity, !DAO! AprsPositionReport
Course/speed, PHG (+PHGR), RNG, DFS, DF bearing/NRQ, area objects, storm data, signposts properties on AprsPositionedData
Weather: complete, positionless, raw station formats AprsWeather, AprsWeatherReport, AprsRawWeatherReport
Mic-E, including device type codes, altitude, grid locator, legacy DTIs AprsMicEReport
Objects and items, including area and frequency objects AprsObjectReport, AprsItemReport
Messages, acks, rejects, reply-acks, bulletins, announcements, NWS AprsTextMessage, AprsMessageAck, ...
Telemetry reports, base-91 comment telemetry, PARM/UNIT/EQNS/BITS AprsTelemetryReport, AprsCommentTelemetry, ...
Status (incl. grid locator, meteor scatter beam/ERP), queries, capabilities AprsStatusReport, AprsGeneralQuery, AprsDirectedQuery, ...
Voice frequency / tone / offset (APRS 1.2) AprsVoiceFrequency
Third-party traffic, user-defined, NMEA, Maidenhead beacons, test data, Agrelo DF ...
Symbols, device identification, APRS-IS q-constructs AprsSymbolTable, AprsDeviceIdentification, AprsQConstruct

Out of scope for now: an APRS-IS client, messaging state (retries, ack tracking), digipeater and IGate logic.

See also


AGPL-3.0-licensed. Part of the Packet.NET stack. The embedded device database is from aprsorg/aprs-deviceid, CC BY-SA 2.0; see THIRD-PARTY-NOTICES.md in the package.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.43.0 0 9/27/2026
0.42.0 27 9/26/2026
0.41.0 38 9/26/2026
0.40.0 91 9/16/2026
0.39.0 92 9/15/2026
0.38.0 89 9/15/2026
0.37.0 102 9/10/2026
0.36.0 90 9/7/2026
0.35.0 100 9/6/2026
0.34.0 97 9/5/2026
0.33.0 92 9/3/2026
0.32.0 99 9/3/2026
0.31.0 95 9/2/2026
0.30.0 108 8/23/2026
0.29.0 116 8/22/2026
0.28.0 105 8/22/2026
0.27.0 107 8/17/2026
0.26.0 114 8/17/2026
0.25.0 122 8/3/2026
0.24.0 125 7/20/2026
Loading failed