Refactored extras field parsing

This commit is contained in:
2026-03-18 18:22:53 +01:00
parent 6adf1281ef
commit e9e329ccc1
3 changed files with 166 additions and 95 deletions

View File

@@ -764,32 +764,60 @@ export class Frame implements IFrame {
const extras: Partial<Extras> = {}; const extras: Partial<Extras> = {};
const fields: Field[] = []; const fields: Field[] = [];
const beforeFields: Field[] = [];
let altitudeOffset: number | undefined = undefined;
let altitudeFields: Field[] = [];
let commentOffset: number = 0;
let commentBefore: string | undefined = undefined;
// Process successive 7-byte data extensions at the start of the comment. // Process successive 7-byte data extensions at the start of the comment.
let ext = comment.trimStart(); let ext = comment.trimStart();
while (ext.length >= 7) { while (ext.length >= 7) {
// We first process the altitude marker, because it may appear anywhere
// in the comment and we want to extract it and its value before
// processing other tokens that may be present.
//
// /A=NNNNNN -> altitude in feet (6 digits) // /A=NNNNNN -> altitude in feet (6 digits)
// /A=-NNNNN -> altitude in feet with leading minus for negative altitudes (5 digits) // /A=-NNNNN -> altitude in feet with leading minus for negative altitudes (5 digits)
const altMatch = ext.match(/\/A=(-\d{5}|\d{6})/); const altMatch = ext.match(/\/A=(-\d{5}|\d{6})/);
if (altMatch) { if (altitudeOffset === undefined && altMatch) {
const altitude = feetToMeters(parseInt(altMatch[1], 10)); // feet to meters const altitude = feetToMeters(parseInt(altMatch[1], 10)); // feet to meters
if (isNaN(altitude)) { if (isNaN(altitude)) {
break; // Invalid altitude format, stop parsing extras break; // Invalid altitude format, stop parsing extras
} }
extras.altitude = altitude; extras.altitude = altitude;
// Keep track of where the altitude token appears in the comment for structure purposes.
altitudeOffset = comment.indexOf(altMatch[0]);
if (withStructure) { if (withStructure) {
fields.push({ type: FieldType.STRING, name: "altitude marker", length: 2 }); altitudeFields = [
fields.push({ {
type: FieldType.STRING, type: FieldType.STRING,
name: "altitude value", name: "altitude marker",
length: altMatch[1].length, data: new TextEncoder().encode("/A=").buffer,
value: altitude.toFixed(3) + "m" length: 3
}); },
{
type: FieldType.STRING,
name: "altitude",
data: new TextEncoder().encode(altMatch[1]).buffer,
length: altMatch[1].length,
value: altitude.toFixed(3) + "m"
}
];
} }
// remove altitude token from comment and advance ext for further parsing if (altitudeOffset > 0) {
comment = comment.replace(altMatch[0], "").trimStart(); // Splice the comment into "before" and "after" around the altitude token.
commentBefore = comment.substring(0, altitudeOffset).trimEnd();
comment = comment.substring(altitudeOffset + altMatch[0].length).trimStart();
ext = commentBefore + comment; // Update ext to reflect the new comment with altitude token removed
continue;
}
// remove altitude token from ext and advance ext for further parsing
commentOffset += 7;
ext = ext.replace(altMatch[0], "").trimStart(); ext = ext.replace(altMatch[0], "").trimStart();
continue; continue;
@@ -801,17 +829,24 @@ export class Frame implements IFrame {
if (/^\d{4}$/.test(r)) { if (/^\d{4}$/.test(r)) {
extras.range = milesToMeters(parseInt(r, 10)) / 1000.0; // Convert to kilometers extras.range = milesToMeters(parseInt(r, 10)) / 1000.0; // Convert to kilometers
if (withStructure) { if (withStructure) {
fields.push({ type: FieldType.STRING, name: "RNG marker", length: 3, value: "RNG" }); (altitudeOffset !== undefined && commentOffset < altitudeOffset ? beforeFields : fields).push(
fields.push({ {
type: FieldType.STRING, type: FieldType.STRING,
name: "range (rrrr)", name: "range marker",
length: 4, value: "RNG",
value: extras.range.toString() + "km" length: 3
}); },
{
type: FieldType.STRING,
name: "range (rrrr)",
length: 4,
value: extras.range.toString() + "km"
}
);
} }
// remove range token from comment and advance ext for further parsing // remove range token from ext and advance ext for further parsing
comment = comment.substring(7).trimStart(); commentOffset += 7;
ext = ext.substring(7).trimStart(); ext = ext.substring(7).trimStart();
continue; continue;
@@ -852,40 +887,42 @@ export class Frame implements IFrame {
}; };
if (withStructure) { if (withStructure) {
fields.push({ type: FieldType.STRING, name: "PHG marker", length: 3, value: "PHG" }); (altitudeOffset !== undefined && commentOffset < altitudeOffset ? beforeFields : fields).push(
fields.push({ { type: FieldType.STRING, name: "PHG marker", length: 3, value: "PHG" },
type: FieldType.STRING, {
name: "power (p)", type: FieldType.STRING,
length: 1, name: "power (p)",
value: powerWatts !== undefined ? powerWatts.toString() + "W" : undefined length: 1,
}); value: powerWatts !== undefined ? powerWatts.toString() + "W" : undefined
fields.push({ },
type: FieldType.STRING, {
name: "height (h)", type: FieldType.STRING,
length: 1, name: "height (h)",
value: heightMeters !== undefined ? heightMeters.toString() + "m" : undefined length: 1,
}); value: heightMeters !== undefined ? heightMeters.toString() + "m" : undefined
fields.push({ },
type: FieldType.STRING, {
name: "gain (g)", type: FieldType.STRING,
length: 1, name: "gain (g)",
value: gainDbi !== undefined ? gainDbi.toString() + "dBi" : undefined length: 1,
}); value: gainDbi !== undefined ? gainDbi.toString() + "dBi" : undefined
fields.push({ },
type: FieldType.STRING, {
name: "directivity (d)", type: FieldType.STRING,
length: 1, name: "directivity (d)",
value: length: 1,
directivity !== undefined value:
? typeof directivity === "number" directivity !== undefined
? directivity.toString() + " ? typeof directivity === "number"
: directivity ? directivity.toString() + "°"
: undefined : directivity
}); : undefined
}
);
} }
// remove PHG token from comment and advance ext for further parsing // remove PHG token from ext and advance ext for further parsing
comment = comment.substring(7).trimStart(); commentOffset += 7;
ext = ext.substring(7).trimStart(); ext = ext.substring(7).trimStart();
continue; continue;
@@ -934,40 +971,42 @@ export class Frame implements IFrame {
}; };
if (withStructure) { if (withStructure) {
fields.push({ type: FieldType.STRING, name: "DFS marker", length: 3, value: "DFS" }); (altitudeOffset !== undefined && commentOffset < altitudeOffset ? beforeFields : fields).push(
fields.push({ { type: FieldType.STRING, name: "DFS marker", length: 3, value: "DFS" },
type: FieldType.STRING, {
name: "strength (s)", type: FieldType.STRING,
length: 1, name: "strength (s)",
value: strength !== undefined ? strength.toString() : undefined length: 1,
}); value: strength !== undefined ? strength.toString() : undefined
fields.push({ },
type: FieldType.STRING, {
name: "height (h)", type: FieldType.STRING,
length: 1, name: "height (h)",
value: heightMeters !== undefined ? heightMeters.toString() + "m" : undefined length: 1,
}); value: heightMeters !== undefined ? heightMeters.toString() + "m" : undefined
fields.push({ },
type: FieldType.STRING, {
name: "gain (g)", type: FieldType.STRING,
length: 1, name: "gain (g)",
value: gainDbi !== undefined ? gainDbi.toString() + "dBi" : undefined length: 1,
}); value: gainDbi !== undefined ? gainDbi.toString() + "dBi" : undefined
fields.push({ },
type: FieldType.STRING, {
name: "directivity (d)", type: FieldType.STRING,
length: 1, name: "directivity (d)",
value: length: 1,
directivity !== undefined value:
? typeof directivity === "number" directivity !== undefined
? directivity.toString() + " ? typeof directivity === "number"
: directivity ? directivity.toString() + "°"
: undefined : directivity
}); : undefined
}
);
} }
// remove DFS token from comment and advance ext for further parsing // remove DFS token from ext and advance ext for further parsing
comment = comment.substring(7).trimStart(); commentOffset += 7;
ext = ext.substring(7).trimStart(); ext = ext.substring(7).trimStart();
continue; continue;
@@ -981,9 +1020,11 @@ export class Frame implements IFrame {
extras.spd = knotsToKmh(parseInt(speedStr, 10)); extras.spd = knotsToKmh(parseInt(speedStr, 10));
if (withStructure) { if (withStructure) {
fields.push({ type: FieldType.STRING, name: "course", length: 3, value: extras.cse.toString() + "°" }); (altitudeOffset !== undefined && commentOffset < altitudeOffset ? beforeFields : fields).push(
fields.push({ type: FieldType.CHAR, name: "marker", length: 1, value: "/" }); { type: FieldType.STRING, name: "course", length: 3, value: extras.cse.toString() + "°" },
fields.push({ type: FieldType.STRING, name: "speed", length: 3, value: extras.spd.toString() + " km/h" }); { type: FieldType.CHAR, name: "marker", length: 1, value: "/" },
{ type: FieldType.STRING, name: "speed", length: 3, value: extras.spd.toString() + " km/h" }
);
} }
// remove course/speed token from comment and advance ext for further parsing // remove course/speed token from comment and advance ext for further parsing
@@ -1004,14 +1045,16 @@ export class Frame implements IFrame {
extras.dfs.strength = dfStrength; extras.dfs.strength = dfStrength;
if (withStructure) { if (withStructure) {
fields.push({ type: FieldType.STRING, name: "DF marker", length: 1, value: "/" }); (altitudeOffset !== undefined && commentOffset < altitudeOffset ? beforeFields : fields).push(
fields.push({ type: FieldType.STRING, name: "bearing", length: 3, value: dfBearing.toString() + "°" }); { type: FieldType.STRING, name: "DF marker", length: 1, value: "/" },
fields.push({ type: FieldType.CHAR, name: "separator", length: 1, value: "/" }); { type: FieldType.STRING, name: "bearing", length: 3, value: dfBearing.toString() + "°" },
fields.push({ type: FieldType.STRING, name: "strength", length: 3, value: dfStrength.toString() }); { type: FieldType.CHAR, name: "separator", length: 1, value: "/" },
{ type: FieldType.STRING, name: "strength", length: 3, value: dfStrength.toString() }
);
} }
// remove DF token from comment and advance ext for further parsing // remove DF token from ext and advance ext for further parsing
comment = comment.substring(8).trimStart(); commentOffset += 8;
ext = ext.substring(8).trimStart(); ext = ext.substring(8).trimStart();
continue; continue;
@@ -1024,8 +1067,36 @@ export class Frame implements IFrame {
break; break;
} }
// Export comment with extras fields removed, if any were parsed.
comment = comment.substring(commentOffset).trimStart();
extras.comment = comment; extras.comment = comment;
extras.fields = fields.length > 0 ? fields : undefined;
if (withStructure) {
const commentBeforeFields: Field[] = commentBefore
? [
{
type: FieldType.STRING,
name: "comment",
value: commentBefore,
length: commentBefore.length
}
]
: [];
const commentFields: Field[] = comment
? [
{
type: FieldType.STRING,
name: "comment",
value: comment,
length: comment.length
}
]
: [];
// Insert the altitude fields at the correct position in the comment section based on where the altitude token was located in the original comment. If there was no altitude token, put all fields at the start of the comment section.
extras.fields = [...commentBeforeFields, ...beforeFields, ...altitudeFields, ...fields, ...commentFields];
}
return extras as Extras; return extras as Extras;
} }

View File

@@ -110,7 +110,7 @@ describe("APRS extras test vectors", () => {
const commentSeg = structure.find((s) => /comment/i.test(String(s.name))) as Segment | undefined; const commentSeg = structure.find((s) => /comment/i.test(String(s.name))) as Segment | undefined;
expect(commentSeg).toBeDefined(); expect(commentSeg).toBeDefined();
const fieldsRNG = (commentSeg!.fields ?? []) as Field[]; const fieldsRNG = (commentSeg!.fields ?? []) as Field[];
const hasRNG = fieldsRNG.some((f) => f.name === "RNG marker"); const hasRNG = fieldsRNG.some((f) => f.name === "range marker");
expect(hasRNG).toBe(true); expect(hasRNG).toBe(true);
}); });
}); });

View File

@@ -950,7 +950,7 @@ describe("Packet dissection with sections", () => {
const commentSection = result.structure?.find((s) => s.name === "comment"); const commentSection = result.structure?.find((s) => s.name === "comment");
expect(commentSection).toBeDefined(); expect(commentSection).toBeDefined();
expect(commentSection?.data?.byteLength).toBe("Test message".length); expect(commentSection?.data?.byteLength).toBe("Test message".length);
expect(commentSection?.fields?.[0]?.name).toBe("text"); expect(commentSection?.fields?.[0]?.name).toBe("comment");
}); });
}); });