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;
}
/**
* 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);
}
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)
{
}
}
private static final CallsignSSID dprstocall = CallsignSSID.parse("APDPRS");
private final static ThreadLocal< CCITTCRC > 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 DPRSpat = Pattern.compile("(?:(?:(?[0-9A-Z-]{1,9}+,DSTAR[*]:.+))$");
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 lineMatcher = DPRSpat.matcher(lineCharBuf);
private final Matcher gpsaMatcher = GPS_Apat.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 MsgLinePat = Pattern.compile("^(([ 0-9A-Z]{8}+),((?>[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("^[ 0-9A-Z]{8}+,[ ]{20}$");
private final Matcher D74Matcher = D74Pat.matcher(lineCharBuf);
private final static String[] D74Message = new String[]{"\\K", ""}; // getMessage() calls getSymbol() to get here
/**
* 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.group(1).length() == 7)
return CallsignSSID.create(dstarStationMatcher.group(1), dstarStationMatcher.group(2).trim(), dstarStationMatcher.group(1) + dstarStationMatcher.group(2).trim());
return CallsignSSID.create(dstarStationMatcher.group(1), dstarStationMatcher.group(2).trim(), "");
}
return CallsignSSID.emptyCall;
}
/**
* 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
String matchedString;
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;
lineCharBuf.clear(); // Set position=0 and limit=maxBufSize
int tpos = 0;
if (prevlinein != null)
{
// Only here if prevlinein.length == maxBufSize && linein.length < maxBufSize && linein.length > 0
MiscTools.getASCIIChars(prevlinein, linein.length, lineCharBuf.array(), 0, maxBufSize - linein.length);
tpos = maxBufSize - linein.length;
}
MiscTools.getASCIIChars(linein, 0, lineCharBuf.array(), tpos, linein.length);
if (prevlinein == null)
lineCharBuf.limit(linein.length); // Sets limit for all other functions (default is maxBufSize)
if (lineMatcher.reset().find())
{
lineCharBuf.position(lineMatcher.start());
if (lineCharBuf.charAt(1) == 'C') // GPS-A ($CRC...)
{
// Check for missing CR
for (tpos = 0; gpsaMatcher.region(tpos + 1, lineCharBuf.length()).find(); tpos = gpsaMatcher.start()) {}
if (tpos > 0)
lineCharBuf.position(lineCharBuf.position() + tpos); // Resets position to start of last found sequence
}
matchedString = lineCharBuf.toString(); // Fast array copy
fulllinein = MiscTools.getASCIIBytes(matchedString); // Fast depricated String.getBytes()
break;
}
if (lineCharBuf.length() >= 29 && readingGPS != null)
{
lineCharBuf.position(lineCharBuf.limit() - 29);
// Only time we will parse a message line
if (msgLineMatcher.reset().matches())
{
fulllinein = linein; // Not used in message parsing so this is fast and no heap used
matchedString = lineCharBuf.toString(); // Fast array copy, also not used in message parsing but used to get there
break;
}
if (parseD74 && D74Matcher.reset().matches())
{
fulllinein = null; // Used to indicate D74 match
matchedString = lineCharBuf.toString(); // Fast array copy, also not used in message parsing but used to get there
break;
}
// Garbled line but we don't reset reading GPS
}
}
}
catch (IndexOutOfBoundsException se)
{
// Added because find() might throw IndexOutOfBoundsException
classlogger.log(Level.FINE, "Regex find exception", se);
continue;
}
catch (Exception e)
{
classlogger.log(Level.WARNING, "Read Error", e);
return null;
}
if (matchedString.startsWith("$GP"))
{
// Handle GPS string
if (fulllinein.length < 8)
continue;
// A bad line beginning with $GP does not reset GPS lookup
PositInfo tinfo = PositInfo.calcGPS(matchedString, 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;
}
continue; // Go no further with GPS strings
}
// Modified to work from only one $ instead of two
if (matchedString.startsWith("$CRC"))
{
// D-PRS or GPS-A line
readingGPS = null; // Reset GPS reading logic if in place
final CCITTCRC getcrc = dstarcrc.get();
getcrc.reset();
getcrc.update(fulllinein, 9, fulllinein.length - 9);
getcrc.update('\r');
if ((int) getcrc.getValue() != getCRC(matchedString.substring(4, 8)))
continue;
try
{
ClientRcv.ClientPacket retpack = parent.parseTNC2Header(fulllinein, 9);
if (retpack == null)
continue;
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)
continue;
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
continue;
}
lastPacket = retpack;
}
return retpack;
}
catch (Exception e)
{
}
continue;
}
if (readingGPS != null)
{
// Must be exact length, formatted properly, valid APRS callsign-SSID and have valid prior GPS lat/lon
CallsignSSID dcall = convertDSTARStation();
if (!dcall.isValidCallSSID)
{
readingGPS = null; // GPS must be from immediately preceding sequence
continue;
}
String message[];
if (fulllinein == null)
// D74
message = D74Message;
else
message = getMessage();
// Will only get here if not $GP or $CRC and line is 29 characters and a comma is in position 8
if (message == null)
{
// Checksum failed
readingGPS = null; // GPS cannot be from prior sequence(s)
continue;
}
// 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));
readingGPS = null;
updateRcvdTotals(0, false, true);
continue;
}
// We don't send a posit if it isn't valid
if (!readingGPS.GeoLocation.isValid())
{
readingGPS = null;
continue;
}
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;
}
}
}
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 callsign id,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(5), 16)
== XORChecksum.getValue(msgLineMatcher.group(1)))
return new String[]
{
getSymbol(msgLineMatcher.group(3)), msgLineMatcher.group(4)
};
}
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;
}
}