This is the javAPRSSrvr D-PRS parsing code. While dependent on other classes, the basic parsing structure is here. CCITTTCRC class is at end.
This is updated with up-to-date regex patterns and the ability to parse GPS from Kenwood TH-D74 radios.
DPRSIntf Class
package net.ae5pl.aprsigate;
import java.io.*;
import java.nio.*;
import java.time.*;
import java.util.regex.*;
import java.util.logging.*;
import java.util.*;
import net.ae5pl.aprs.*;
import net.ae5pl.aprssrvr.*;
import net.ae5pl.serintf.*;
import net.ae5pl.util.*;
import static java.lang.Math.*;
import static net.ae5pl.util.MiscTools.*;
/**
* D-PRS Interface for D-STAR DV serial feeds.
*
* @author Pete Loveall AE5PL
*/
public final class DPRSIntf extends APRSIGate.PacketIntf
{
private final static Logger classlogger = Logger.getLogger(DPRSIntf.class.getName());
private final static int maxBufSize = 512;
private final LineInputStream sis;
private final SerialIntf.SerialInputStream.SerialOutputStream sos;
private final long dupeDelay;
private final boolean parseD74;
/**
* Support for APRSIGate.PacketIntf ServiceLoader
*/
public DPRSIntf()
{
sis = null;
sos = null;
dupeDelay = 0L;
parseD74 = false;
emptyPacket = null;
}
/**
* Support for D-STAR serial feeds (D-PRS) Uses dupeDelay=10 seconds from
* APRSIGate portProps
*
* @param ig This is the APRSIGate container class as required by Java
*/
private DPRSIntf(APRSIGate ig)
{
super(ig);
SerialIntf serintf = Objects.requireNonNull(SerialIntf.getInterface(ig.portProps.getProperty("IntfName")), "No serial interface found for " + ig.portProps.getProperty("IntfName"));
SerialIntf.SerialInputStream tsis = serintf.getInputStream();
this.sis = new LineInputStream(tsis, maxBufSize);
this.sos = tsis.getOutputStream();
dupeDelay = ig.portProps.getProperty("dupeDelay", 10L);
parseD74 = ig.portProps.getProperty("parseD74", false);
emptyPacket = ig.getRawClientPacket(MiscTools.emptybyteArray);
}
private static final byte[] GPSACRC = getASCIIBytes("$$CRC0000,");
private static final CallsignSSID[] dstarpath =
{
CallsignSSID.parse("DSTAR")
};
@Override
public void SendPacket(APRSPacket outpacket)
{
APRSPacket tpack = outpacket.newPath(0, dstarpath);
byte[] newline = Arrays.copyOf(GPSACRC, GPSACRC.length + tpack.getTNC2Bytes().length + 1);
System.arraycopy(tpack.getTNC2Bytes(), 0, newline, GPSACRC.length, tpack.getTNC2Bytes().length);
newline[newline.length - 1] = '\r';
final CCITTCRC sendcrc = dstarcrc.get();
sendcrc.reset();
sendcrc.update(newline, GPSACRC.length, newline.length - GPSACRC.length);
getCRCChars(newline, 5, sendcrc.getValue());
try
{
sos.write(newline);
updateXmtdTotals(newline.length, true, true);
}
catch (IOException e)
{
}
}
protected static final CallsignSSID dprstocall = CallsignSSID.parse("APDPRS");
private final static ThreadLocal dstarcrc = ThreadLocal.withInitial(CCITTCRC::new);
// lineCharBuf and lineMatcher are done here as GetPacket is only called from APRSIGate.run
private final CharBuffer lineCharBuf = CharBuffer.allocate(maxBufSize);
private final static Pattern GPS_Apat = Pattern.compile("(?>[$]CRC[0-9A-F]{4}+,[0-9A-Z-]{3,9}+>[0-9A-Z-]{1,9}+,DSTAR[*]:).+$");
private final Matcher gpsaMatcher = GPS_Apat.matcher(lineCharBuf);
private final static Pattern GPSpat = Pattern.compile("(?>[$]GP(?>RMC|GGA)(?:,[-.0-9A-Z]{0,10}+){10,16}+[*][0-9A-F]{2}+)$");
private final Matcher gpsMatcher = GPSpat.matcher(lineCharBuf);
/**
* MsgLinePat groups:
* group(1) = message line sans checksum, group(2) = callsign ID, group 3 = symbol, group 4 message sans symbol & checksum, group 5 checksum
*/
private final static Pattern GenMsgPat = Pattern.compile("^(?>[ 0-9A-Z]{8}+,)\\p{Print}{20}+$");
private final Matcher genMsgMatcher = GenMsgPat.matcher(lineCharBuf);
private final static Pattern MsgLinePat = Pattern.compile("^(((?>[ABDHJLMNOPQS][0-9A-Z][ 0-9A-Z]))\\ (\\p{Print}{0,13}))[*]([0-9A-F]{2}+)\\ *$");
private final Matcher msgLineMatcher = MsgLinePat.matcher(lineCharBuf);
private final static Pattern dstarStationPat = Pattern.compile("^([0-9A-Z]{3,7}+)[ ]{0,4}([ 0-9A-Z])$");
private final Matcher dstarStationMatcher = dstarStationPat.matcher(lineCharBuf);
private final static Pattern D74Pat = Pattern.compile("^[ ]{20}+$");
private final Matcher D74Matcher = D74Pat.matcher(lineCharBuf);
private final static String[] D74Message = new String[]{"\\K", ""}; // getMessage() calls getSymbol() to get the same
/**
* This converts DSTAR station callsign ID combos to APRS TNC2 format
* including Icom's mangling of 7 character callsigns with ID.
*
* @return APRS CallsignSSID (validated if 6 or less characters for
* callsign) or emptyCall if invalid station callsign or ID==0
*/
private CallsignSSID convertDSTARStation()
{
// reset() done in region()
if (dstarStationMatcher.region(0, 8).matches())
{
// This handles Icom's mangling of D-PRS conversion for 7 character callsigns.
if (dstarStationMatcher.end(1) == 7)
return CallsignSSID.create(dstarStationMatcher.group(1), dstarStationMatcher.group(2).trim(), dstarStationMatcher.group().trim());
return CallsignSSID.create(dstarStationMatcher.group(1), dstarStationMatcher.group(2).trim(), "");
}
return CallsignSSID.emptyCall;
}
/**
* Processes a callsign,message line
*
* @param readingGPS PositInfo from prior GPS sentences. Reset to null after method returns
* @param curtime Time recorded for packet
* @param isD74 True if matched empty message and parseD74 set
* @return Packet or null if either not valid or to fast
*/
private ClientRcv.ClientPacket processCallsignMessage(PositInfo readingGPS, Instant curtime, boolean isD74)
{
try
{
// Must be exact length, formatted properly, valid APRS callsign-SSID and have valid prior GPS lat/lon
CallsignSSID dcall = convertDSTARStation();
if (!dcall.isValidCallSSID)
return null;
// Get symbol & message
String message[] = isD74 ? D74Message : getMessage();
// Test for valid checksum
if (message == null)
return null;
// Don't do multiples per transmission
if (lastPacket != null
&& lastPacket.OrgCall.equals(dcall)
&& curtime.minusSeconds(dupeDelay).isBefore(lastPacket.timeCreated))
{
// This is a sliding window
lastPacket = parent.new ClientPacket(lastPacket.OrgCall, lastPacket.DestCall, 0, readingGPS, lastPacket.path.toArray(CallsignSSID.emptyCallArray));
updateRcvdTotals(0, false, true);
return null;
}
// We don't send a posit if it isn't valid
if (!readingGPS.GeoLocation.isValid())
return null;
readingGPS.symbol[0] = message[0].charAt(0);
readingGPS.symbol[1] = message[0].charAt(1);
if (!message[1].isEmpty())
readingGPS.comment = message[1];
readingGPS.canMessage = false;
if (readingGPS.direction > 360 || readingGPS.direction < 0)
readingGPS.direction = 0;
else if (readingGPS.direction > 0)
readingGPS.dataextension = new PositInfo.dirspdInfo(readingGPS.direction, (int) round(readingGPS.GeoLocation.M));
lastPacket = parent.new ClientPacket(dcall, dprstocall, 0, readingGPS, dstarpath);
updateRcvdTotals(0, false, true);
return lastPacket;
}
catch (Exception e)
{
classlogger.log(Level.FINE, "Regex Exception: " + lineCharBuf.toString(), e);
}
return null;
}
/**
* Same as ClientRcv.ClientPacket.isPosit but includes Objects and Items
*
* @param retpack DPRS/GPS-A Packet
* @return true if packet contains a valid posit
*/
private boolean isDPRSPosit(ClientRcv.ClientPacket retpack)
{
final PacketInfo pi = retpack.getPacketInfo();
return pi != null && pi.getPositInfo() != null
&& !(pi instanceof ThirdPartyInfo);
// Removed as CRC should prevent this && pi.getPositInfo().GeoLocation.isValid();
}
private ClientRcv.ClientPacket lastPacket = null; // Used to restrict packets to one per transmission
@Override
public ClientRcv.ClientPacket GetPacket()
{
Instant lastLine = (lastPacket == null) ? null : lastPacket.timeCreated;
Instant curtime;
PositInfo readingGPS = null; // Used for GPS mode; null = not parsing GPS
byte[] linein;
byte[] prevlinein;
byte[] fulllinein; // Parsed line per DPRSPat
for (;;)
{
// Look for $GP line, DSTAR Call Line, or $$CRC line
try
{
for (;;)
{
prevlinein = null;
for (;;)
{
linein = sis.readLine(false);
if (linein == null)
throw new EOFException();
if (linein.length == 0)
{
// Ignore empty lines
if (prevlinein == null)
continue;
linein = prevlinein;
prevlinein = null;
break;
}
updateRcvdTotals(linein.length, false, false);
if (linein.length == maxBufSize)
prevlinein = linein;
else
break;
}
updateRcvdTotals(0, true, false);
curtime = Instant.now();
if (lastLine == null || curtime.minusSeconds(dupeDelay).isAfter(lastLine))
{
// Reset packet/GPS counters because dupeDelay seconds since last line have elapsed
readingGPS = null;
lastPacket = null;
}
lastLine = curtime;
if (prevlinein != null)
{
// Only here if prevlinein.length == maxBufSize && linein.length < maxBufSize && linein.length > 0
fulllinein = prevlinein;
System.arraycopy(fulllinein, linein.length, fulllinein, 0, maxBufSize - linein.length);
System.arraycopy(linein, 0, fulllinein, maxBufSize - linein.length, linein.length);
}
else
fulllinein = linein;
// lineCharBuf used for Regex and tests, getASCIIChars keeps arrays in sync positionally
lineCharBuf.clear(); // Set position=0 and limit=maxBufSize
MiscTools.getASCIIChars(fulllinein, 0, lineCharBuf.array(), 0, fulllinein.length);
lineCharBuf.limit(fulllinein.length); // Sets limit for all other functions (default is maxBufSize)
// Check for callsign,message line first if parsing GPS sentences
if (readingGPS != null && fulllinein.length >= 29 && fulllinein[fulllinein.length - 21] == ','
&& genMsgMatcher.region(lineCharBuf.limit() - 29, lineCharBuf.limit()).matches())
{
lineCharBuf.position(lineCharBuf.limit() - 29);
// Only time we will parse a message line
if (msgLineMatcher.region(9, 29).matches())
{
ClientRcv.ClientPacket tpack = processCallsignMessage(readingGPS, curtime, false);
if (tpack != null)
return tpack;
}
else if (parseD74 && D74Matcher.region(9, 29).matches())
{
// This also clears any callsign,messages with no message
ClientRcv.ClientPacket tpack = processCallsignMessage(readingGPS, curtime, true);
if (tpack != null)
return tpack;
}
readingGPS = null;
continue;
}
if (gpsaMatcher.reset().find())
{
lineCharBuf.position(gpsaMatcher.start());
for (; gpsaMatcher.region(1, lineCharBuf.length()).find();)
lineCharBuf.position(lineCharBuf.position() + gpsaMatcher.start());
ClientRcv.ClientPacket cp = processGPSA(Arrays.copyOfRange(fulllinein, lineCharBuf.position(), fulllinein.length));
if (cp != null)
{
if (cp.OrgCall.isValidCallSSID)
return cp;
readingGPS = null; // Passed CRC
continue;
}
}
if (gpsMatcher.reset().find())
{
// Can occur after an invalid CRC but only once at the end
lineCharBuf.position(lineCharBuf.position() + gpsMatcher.start());
if (lineCharBuf.length() < 8)
continue;
// A bad line beginning with $GP does not reset GPS lookup
PositInfo tinfo = PositInfo.calcGPS(lineCharBuf.toString(), true);
if (tinfo != null)
{
if (readingGPS == null)
readingGPS = tinfo;
else
{
readingGPS.GeoLocation.setLat(tinfo.GeoLocation.getLat());
readingGPS.GeoLocation.setLong(tinfo.GeoLocation.getLong());
if (tinfo.direction != 0)
{
readingGPS.GeoLocation.M = tinfo.GeoLocation.M;
readingGPS.direction = tinfo.direction;
}
if (!Double.isNaN(tinfo.GeoLocation.Z))
readingGPS.GeoLocation.Z = tinfo.GeoLocation.Z;
}
}
}
}
}
catch (IndexOutOfBoundsException se)
{
// Added because find() might throw IndexOutOfBoundsException
classlogger.log(Level.FINE, "Regex exception: " + lineCharBuf.toString(), se);
}
catch (Exception e)
{
classlogger.log(Level.WARNING, "Read Error", e);
return null;
}
}
}
private final ClientRcv.ClientPacket emptyPacket;
/**
* Processes $CRC...
*
* @param fulllinein byte array starting at the $CRC
* @return Packet if to be passed on, emptyPacket if valid but not to be passed, or null for invalid
*/
private ClientRcv.ClientPacket processGPSA(byte[] fulllinein)
{
try
{
int rcvdcrc = getCRC(lineCharBuf.subSequence(4, 8).toString());
if (rcvdcrc == -1)
return null; // Bad received CRC
final CCITTCRC getcrc = dstarcrc.get();
getcrc.reset();
getcrc.update(fulllinein, 9, fulllinein.length - 9);
getcrc.update('\r');
if ((int) getcrc.getValue() != rcvdcrc)
return null; // CRC does not match received data
ClientRcv.ClientPacket retpack = parent.parseTNC2Header(fulllinein, 9);
if (retpack == null)
return null; // Even though CRC ok, not a packet.
updateRcvdTotals(0, false, true);
// This adjusts for Icom's mangling of DPRS conversion of 7 character callsigns with ID
if (retpack.OrgCall.callsign.length() == 8 && retpack.OrgCall.callsign.equals(retpack.OrgCall.callSSID))
retpack = retpack.changeOrgCall(CallsignSSID.create(retpack.OrgCall.callsign.substring(0, 7), retpack.OrgCall.callSSID.substring(7), retpack.OrgCall.callSSID));
if (!retpack.OrgCall.isValidCallSSID)
return emptyPacket; // bad packet
retpack.parseAPRS(); // Do here so we know if it is a posit
if (isDPRSPosit(retpack))
{
// Don't do multiple GPS-A posits within dupeDelay seconds (moving window)
if (lastPacket != null
&& retpack.OrgCall.equals(lastPacket.OrgCall)
&& retpack.timeCreated.minusSeconds(dupeDelay).isBefore(lastPacket.timeCreated))
{
lastPacket = retpack; // Make moving window to keep from streaming
return emptyPacket; // too soon
}
lastPacket = retpack;
}
return retpack;
}
catch (Exception e)
{
classlogger.log(Level.INFO, "Error parsing: " + lineCharBuf.toString(), e);
}
return null;
}
private final static byte[] hexchars = new byte[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
/**
* Places 4 CRC hex characters at startpos
*
* @param buffer byte/character buffer
* @param startpos position in buffer to put 4 characters
* @param crc CRC to convert to hex characters
*/
private static void getCRCChars(byte[] buffer, int startpos, long crc)
{
int tcrc = (int) crc;
for (int i = startpos + 3; i >= startpos; --i)
{
buffer[i] = hexchars[tcrc & 0x0f];
tcrc >>>= 4;
}
}
/**
* Read 4 hex bytes/characters at startpos and convert to int
*
* @param buffer buffer containing hex CRC
* @return integer CRC, -1 for invalid
*/
private static int getCRC(String buffer)
{
try
{
return Integer.parseUnsignedInt(buffer, 16);
}
catch (NumberFormatException e)
{
}
return -1;
}
/**
* Parses "TOCALL" symbol per APRS.
*
* @param message MsgLinePat.group(2) (must be from regex to validate input)
* @return parsed symbol set/overlay and symbol
*/
private static String getSymbol(String message)
{
int offset = -1;
switch (message.charAt(0))
{
case 'B':
case 'O':
offset = -33;
break;
case 'P':
case 'A':
offset = 0;
break;
case 'M':
case 'N':
offset = -24;
break;
case 'H':
case 'D':
offset = 8;
break;
case 'L':
case 'S':
offset = 32;
break;
case 'J':
case 'Q':
offset = 74;
break;
}
boolean altIcons = false;
// x is valid, lets get y
switch (message.charAt(0))
{
case 'O':
case 'A':
case 'N':
case 'D':
case 'S':
case 'Q':
altIcons = true;
break;
}
char symbol = (char) (message.charAt(1) + offset);
char overlay = '/';
if (altIcons)
if (message.charAt(2) == ' ')
overlay = '\\';
else
overlay = message.charAt(2);
return String.valueOf(new char[]
{
overlay, symbol
});
}
/**
* Parses Callsign,Message line and checks checksum
*
* @return symbol,trimmed message sans symbol and checksum; or
* null if invalid
*/
private String[] getMessage()
{
// msgLineMatcher has already been matched
try
{
if (Integer.parseUnsignedInt(msgLineMatcher.group(4), 16)
== XORChecksum.getValue(lineCharBuf.subSequence(msgLineMatcher.start(1), msgLineMatcher.end(1))))
return new String[]
{
getSymbol(msgLineMatcher.group(2)), msgLineMatcher.group(3).trim()
};
}
catch (NumberFormatException e)
{
}
return null;
}
@Override
protected APRSIGate.PacketIntf getPacketInterface(APRSIGate newparent)
{
if ("DPRS".equalsIgnoreCase(newparent.portProps.getProperty("PacketInterface")) || loadHelper(newparent.portProps.getProperty("PacketInterface")))
return new DPRSIntf(newparent);
return null;
}
}
CCITTCRC Class
package net.ae5pl.util;
/**
* AX.25/D-STAR CRC-16-CCITT (sometimes called CRC-CCITT-FALSE) from X.25 spec.
* base=0xffff, polynomial=0x8408(Reversed 0x1021), XORed with 0xffff for value.
*
* @author Pete Loveall AE5PL
*/
public final class CCITTCRC implements java.util.zip.Checksum
{
private int crc = 0x0ffff;
@Override
public void update(int b)
{
boolean xorflag;
int ch = b & 0x0ff;
for (int i = 0; i < 8; ++i)
{
xorflag = ((crc ^ ch) & 0x01) == 1;
crc >>>= 1;
if (xorflag)
crc ^= 0x8408;
ch >>>= 1;
}
}
@Override
public void update(byte[] b, int off, int len)
{
final int offlen = off + len;
if (len < 1 || off < 0 || b.length < offlen)
throw new ArrayIndexOutOfBoundsException("offset " + off + " or length " + len + " invalid");
boolean xorflag;
int ch;
for (int i = off; i < offlen; ++i)
{
ch = b[i] & 0x0ff;
for (int j = 0; j < 8; ++j)
{
xorflag = ((crc ^ ch) & 0x01) == 1;
crc >>>= 1;
if (xorflag)
crc ^= 0x8408;
ch >>>= 1;
}
}
}
/**
* CCITT 16 bit CRC
*
* @return internal crc XOR 0x0ffff
*/
@Override
public long getValue()
{
return crc ^ 0x0ffff;
}
/**
* Resets internal crc to 0x0ffff
*/
@Override
public void reset()
{
crc = 0x0ffff;
}
}