Finnish APRS Parser (Fabulous APRS Parser) - Go edition
This is a Go port of the Ham::APRS::FAP Perl module, a fairly complete APRS (Automatic Packet Reporting System) parser. It parses normal, Mic-E and compressed location packets, NMEA location packets, objects, items, messages, telemetry and most weather packets. It is implemented in pure Go with no external dependencies (stdlib only).
The original Perl module is stable and fast enough to parse the APRS-IS stream in real time, and is used to power the http://aprs.fi/ web site.
But nobody actually tested or used this Go port yet. I have no clue whether it works.
The Go port parses packets 19 times faster than the original Perl version. For a constant incoming stream of packets, it uses about 5% of the CPU used by the Perl parser. This was tested by parsing the same 24-hour log of APRS-IS packets using both parsers.
On an old Intel Xeon L5520 CPU @ 2.27GHz (8 MB cache), it does about 230k packets/second, which is sufficient for my current needs.
This Go module was created using the Claude Code AI agent, by letting it inspect the old Perl code. The original Perl module's test packets and expected output values have been retained in the Go test suite, which should provide a good level of confidence that the port produces correct results.
- Position (uncompressed, compressed, Mic-E)
- Objects and items
- Messages, acks, and rejects
- Weather reports
- Telemetry
- Status reports
- Station capabilities
- NMEA (GPRMC, GPGGA, GPGLL)
- DX spots
- Special objects (area, signpost, etc)
- Network tunneling / third party packets
- Direction finding
- Station capability queries
- User defined data formats
This module is based (on those parts that are implemented) on APRS specification 1.0.1.
go get github.com/hessu/go-aprs-fap
package main
import (
"fmt"
"github.com/hessu/go-aprs-fap"
)
func main() {
packet := "N0CALL>APRS,WIDE1-1,WIDE2-1,qAo,IGATE:!6128.23N/02353.52E-PHG2360/Testing"
p, err := fap.Parse(packet)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Source: %s\n", p.SrcCallsign)
fmt.Printf("Type: %s\n", p.Type)
fmt.Printf("Lat: %.4f\n", *p.Latitude)
fmt.Printf("Lon: %.4f\n", *p.Longitude)
}Parse errors are returned as *fap.ParseError values, which carry a
machine-readable Code (e.g. "pos_short") and a human-readable Msg.
Sentinel error variables (e.g. fap.ErrPosShort) are provided for use
with errors.Is():
import "errors"
p, err := fap.Parse(raw)
if errors.Is(err, fap.ErrLocInvalid) {
// invalid location
} else if err != nil {
// some other parse failure
}To access the error code and message directly, use errors.As():
var parseErr *fap.ParseError
if errors.As(err, &parseErr) {
fmt.Printf("code=%s msg=%s\n", parseErr.Code, parseErr.Msg)
}Some issues are non-fatal: parsing succeeds but the packet has problems
worth noting (e.g. an invalid timestamp). These are collected in the
Warnings slice on the returned Packet:
p, err := fap.Parse(raw)
if err != nil {
// hard failure
}
for _, w := range p.Warnings {
fmt.Printf("warning: code=%s msg=%s\n", w.Code, w.Msg)
}Warnings use the same ParseError type as errors, so you can match
specific codes with errors.Is():
for i := range p.Warnings {
if errors.Is(&p.Warnings[i], fap.ErrTimestampInvalid) {
// timestamp could not be parsed, but the rest of the packet is fine
}
}The package includes an APRS-IS TCP client for connecting to APRS-IS servers. It is a Go port of the Ham::APRS::IS Perl module.
Do provide the name of your application in place of "myapp", and version number in place of "0.1" in the following example.
package main
import (
"fmt"
"time"
"github.com/hessu/go-aprs-fap"
)
func main() {
c, err := fap.Dial("rotate.aprs2.net:14580", "N0CALL", "-1", "myapp", "0.1", "r/60.18/24.94/100")
if err != nil {
fmt.Printf("Connect error: %v\n", err)
return
}
defer c.Close()
for {
line, err := c.ReadPacket(30 * time.Second)
if err != nil {
fmt.Printf("Read error: %v\n", err)
return
}
p, err := fap.Parse(line)
if err != nil {
fmt.Printf("Parse error: %v\n", err)
continue
}
fmt.Printf("%s> type=%s\n", p.SrcCallsign, p.Type)
}
}fap.Dial(addr, callsign, passcode, appName, appVer, filter...)— connect, authenticate, and return a*Connfap.Connect(addr, fap.ConnOptions{...})— likeDial, with dial/login timeouts, TLS, a custom dialer, aNoLoginmode for driving the handshake by hand, a verbatimLoginLine, and anOnCommenthook for#linesConn.Login()— send the login line and return the parsed*fap.LogResp(callsign, verified, server); a rejected login is not an errorConn.Banner()— the server's first#greeting lineConn.SendRaw(b),Conn.CloseWrite(),Conn.NetConn()— raw writes, TCP half-close, and the underlying connectionConn.ReadLine(timeout)— read one line (strips CR/LF)Conn.ReadPacket(timeout)— read one non-comment line (skips#keepalives)Conn.SendLine(line)— send a line (appends CR/LF)Conn.Close()— close the connectionfap.AprsPasscode(callsign)— compute the APRS-IS passcode for a callsign
EncodePosition creates an uncompressed APRS position body string.
Coordinates are in decimal degrees, speed in km/h, course in degrees,
altitude in meters. Symbol is a 2-character string (table + code).
lat := 60.4525
lon := 24.9842
speed := 45.0
course := 180.0
alt := 120.0
body, err := fap.EncodePosition(lat, lon, &speed, &course, &alt, "/>", nil)
// body is "!6027.15N/02459.05E/>180/024/A=000394"Optional parameters are passed via EncodePositionOpts:
body, err := fap.EncodePosition(lat, lon, nil, nil, nil, "/-", &fap.EncodePositionOpts{
Ambiguity: 2, // 0-4, reduces coordinate precision
Comment: "Test",
})Include an HMS UTC timestamp with the Timestamp option. This changes the
data type identifier from ! to / and prepends an HHMMSSh timestamp:
body, err := fap.EncodePosition(lat, lon, nil, nil, nil, "/-", &fap.EncodePositionOpts{
Timestamp: time.Date(2024, 3, 15, 12, 30, 45, 0, time.UTC),
})
// body is "/123045h6027.15N/02459.05E-"Enable DAO for extra precision using the !DAO! extension:
body, err := fap.EncodePosition(lat, lon, nil, nil, nil, "/-", &fap.EncodePositionOpts{
DAO: true,
})EncodeMessage encodes a Message struct into an APRS message body string
that can be parsed back by the packet parser.
msg := &fap.Message{
Destination: "N0CALL",
Text: "Hello world",
ID: "42",
}
body, err := fap.EncodeMessage(msg)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
// body is ":N0CALL :Hello world{42"It supports all message variants: regular messages, acks, rejects, and reply-acks. Validation is performed on all fields:
| Sentinel | Condition |
|---|---|
ErrMsgNoDst |
Destination is empty |
ErrMsgDstTooLong |
Destination exceeds 9 characters |
ErrMsgIDInvalid |
Message ID is not 1-5 alphanumeric characters |
ErrMsgReplyAck |
Reply-ack ID + '}' + ack ID exceeds 5 characters |
ErrMsgAckRej |
Both ack and reject set, or content with reject |
ErrMsgCRLF |
Any field contains CR or LF |
// Send an ack
ack := &fap.Message{Destination: "N0CALL", AckID: "42"}
body, _ := fap.EncodeMessage(ack)
// Send a reject
rej := &fap.Message{Destination: "N0CALL", RejID: "42"}
body, _ = fap.EncodeMessage(rej)
// Reply-ack (embedded ack in message)
msg := &fap.Message{Destination: "N0CALL", Text: "Hi", ID: "1", AckID: "ab"}
body, err := fap.EncodeMessage(msg)
if errors.Is(err, fap.ErrMsgReplyAck) {
// ack doesn't fit, send it separately
}- Ham::APRS::FAP - the original Perl module
- libfap - C library port of Ham::APRS::FAP
- python-libfap - Python bindings for libfap
Copyright (C) 2005-2026 Tapio Sokura, OH2KKU
Copyright (C) 2007-2026 Heikki Hannikainen, OH7LZB
This library is free software; you can redistribute it and/or modify it under the same terms Perl itself.