Fixed bug in extras segment parsing

This commit is contained in:
2026-03-18 19:01:16 +01:00
parent e9e329ccc1
commit 04166daeee
2 changed files with 95 additions and 43 deletions

View File

@@ -770,8 +770,12 @@ export class Frame implements IFrame {
let commentOffset: number = 0;
let commentBefore: string | undefined = undefined;
// eslint-disable-next-line no-useless-assignment
let match: RegExpMatchArray | null = null;
// Process successive 7-byte data extensions at the start of the comment.
let ext = comment.trimStart();
comment = comment.trimStart();
let ext = comment;
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
@@ -796,23 +800,25 @@ export class Frame implements IFrame {
type: FieldType.STRING,
name: "altitude marker",
data: new TextEncoder().encode("/A=").buffer,
value: "/A=",
length: 3
},
{
type: FieldType.STRING,
name: "altitude",
data: new TextEncoder().encode(altMatch[1]).buffer,
length: altMatch[1].length,
value: altitude.toFixed(3) + "m"
value: altitude.toFixed(3) + "m",
length: 6
}
];
}
if (altitudeOffset > 0) {
// 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
// Reset the comment with the altitude marker removed.
commentBefore = comment.substring(0, altitudeOffset);
comment = comment.substring(altitudeOffset + altMatch[0].length);
ext = commentBefore; // Continue processing extensions in the part of the comment before the altitude marker
commentOffset = 0; // Reset
continue;
}
@@ -824,42 +830,46 @@ export class Frame implements IFrame {
}
// RNGrrrr -> pre-calculated range in miles (4 digits)
if (ext.startsWith("RNG")) {
const r = ext.substring(3, 7);
if (/^\d{4}$/.test(r)) {
extras.range = milesToMeters(parseInt(r, 10)) / 1000.0; // Convert to kilometers
if (withStructure) {
(altitudeOffset !== undefined && commentOffset < altitudeOffset ? beforeFields : fields).push(
{
type: FieldType.STRING,
name: "range marker",
value: "RNG",
length: 3
},
{
type: FieldType.STRING,
name: "range (rrrr)",
length: 4,
value: extras.range.toString() + "km"
}
);
}
// remove range token from ext and advance ext for further parsing
commentOffset += 7;
ext = ext.substring(7).trimStart();
continue;
if ((match = ext.match(/^RNG(\d{4})/))) {
const r = match[1];
extras.range = milesToMeters(parseInt(r, 10)) / 1000.0; // Convert to kilometers
if (withStructure) {
(altitudeOffset !== undefined && commentOffset < altitudeOffset ? beforeFields : fields).push(
{
type: FieldType.STRING,
name: "range marker",
value: "RNG",
length: 3
},
{
type: FieldType.STRING,
name: "range (rrrr)",
length: 4,
value: extras.range.toString() + "km"
}
);
}
// remove range token from ext and advance ext for further parsing
if (commentBefore !== undefined && commentBefore.length > 0) {
commentBefore = commentBefore.substring(7);
ext = commentBefore;
} else {
commentOffset += 7;
ext = ext.substring(7);
}
continue;
}
// PHGphgd
if (!extras.phg && ext.startsWith("PHG")) {
//if (!extras.phg && ext.startsWith("PHG")) {
if (!extras.phg && (match = ext.match(/^PHG([0-9 ])([0-9 ])([0-9 ])([0-9 ])/))) {
// PHGphgd: p = power (0-9 or space), h = height (0-9 or space), g = gain (0-9 or space), d = directivity (0-9 or space)
const p = ext.charAt(3);
const h = ext.charAt(4);
const g = ext.charAt(5);
const d = ext.charAt(6);
const p = match[1];
const h = match[2];
const g = match[3];
const d = match[4];
const pNum = parseInt(p, 10);
const powerWatts = Number.isNaN(pNum) ? undefined : pNum * pNum;
const hIndex = h.charCodeAt(0) - 48;
@@ -1028,7 +1038,6 @@ export class Frame implements IFrame {
}
// remove course/speed token from comment and advance ext for further parsing
comment = comment.substring(7).trimStart();
ext = ext.substring(7).trimStart();
// If there is an 8-byte DF/NRQ following (leading '/'), parse that too
@@ -1068,8 +1077,13 @@ export class Frame implements IFrame {
}
// Export comment with extras fields removed, if any were parsed.
comment = comment.substring(commentOffset).trimStart();
extras.comment = comment;
if (commentOffset > 0 && commentBefore !== undefined && commentBefore.length > 0) {
extras.comment = commentBefore.substring(commentOffset) + comment;
} else if (commentBefore !== undefined && commentBefore.length > 0) {
extras.comment = commentBefore + comment;
} else {
extras.comment = comment;
}
if (withStructure) {
const commentBeforeFields: Field[] = commentBefore
@@ -1095,7 +1109,7 @@ export class Frame implements IFrame {
: [];
// 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];
extras.fields = [...beforeFields, ...commentBeforeFields, ...altitudeFields, ...fields, ...commentFields];
}
return extras as Extras;

View File

@@ -6,6 +6,35 @@ import type { PositionPayload } from "../src/frame.types";
import { feetToMeters, milesToMeters } from "../src/parser";
describe("APRS extras test vectors", () => {
it("parses altitude token marker mid-comment and emits structure", () => {
const raw = "N0CALL>APRS,WIDE1-1:!4500.00N/07000.00W#RNG0001ALT/A=001234 Your Comment Here";
const frame = Frame.fromString(raw);
const res = frame.decode(true) as { payload: PositionPayload | null; structure: Dissected };
const { payload, structure } = res;
// console.log(structure[structure.length - 1]); // Log the last segment for debugging
expect(payload).not.toBeNull();
// Altitude 001234 ft -> meters
expect(Math.round((payload!.position.altitude || 0) / 0.3048)).toBe(1234);
const commentSeg = structure.find((s) => /comment/i.test(String(s.name))) as Segment | undefined;
expect(commentSeg).toBeDefined();
const fieldsAlt = (commentSeg!.fields ?? []) as Field[];
const hasAlt = fieldsAlt.some((f) => f.name === "altitude");
expect(hasAlt).toBe(true);
const commentIndex = (commentSeg!.fields ?? []).findIndex((f) => f.name === "comment");
expect(commentIndex).toBe(2); // Range marker + range go before.
const altitudeIndex = (commentSeg!.fields ?? []).findIndex((f) => f.name === "altitude");
expect(altitudeIndex).toBeGreaterThan(0); // Altitude should come after comment in the structure
expect(altitudeIndex).toBeGreaterThan(commentIndex);
const secondCommentIndex = (commentSeg!.fields ?? []).findIndex((f, i) => f.name === "comment" && i > commentIndex);
expect(secondCommentIndex).toBeGreaterThan(altitudeIndex); // Any additional comment fields should come after altitude
});
it("parses PHG from position with messaging (spec vector 1)", () => {
const raw = "NOCALL>APZRAZ,qAS,PA2RDK-14:=5154.19N/00627.77E>PHG500073 de NOCALL";
const frame = Frame.fromString(raw);
@@ -98,7 +127,7 @@ describe("APRS extras test vectors", () => {
it("parses RNG token and emits structure", () => {
const raw =
"NOCALL-S>APDG01,TCPIP*,qAC,NOCALL-GS:;DN9PJF B *181227z5148.38ND00634.32EaRNG0001/A=000010 70cm Voice (D-Star) 439.50000MHz -7.6000MHz";
"N0CALL-S>APDG01,TCPIP*,qAC,N0CALL-GS:;N0CALL B *181721z5148.38ND00634.32EaRNG0001/A=000010 70cm Voice (D-Star) 439.50000MHz -7.6000MHz";
const frame = Frame.fromString(raw);
const res = frame.decode(true) as { payload: PositionPayload | null; structure: Dissected };
const { payload, structure } = res;
@@ -112,5 +141,14 @@ describe("APRS extras test vectors", () => {
const fieldsRNG = (commentSeg!.fields ?? []) as Field[];
const hasRNG = fieldsRNG.some((f) => f.name === "range marker");
expect(hasRNG).toBe(true);
const rangeIndex = (commentSeg!.fields ?? []).findIndex((f) => f.name === "range marker");
expect(rangeIndex).toBeGreaterThanOrEqual(0);
const altitudeIndex = (commentSeg!.fields ?? []).findIndex((f) => f.name === "altitude");
expect(altitudeIndex).toBeGreaterThanOrEqual(0);
expect(rangeIndex).toBeGreaterThanOrEqual(0);
expect(altitudeIndex).toBeGreaterThan(rangeIndex); // Altitude comes after range
const commentIndex = (commentSeg!.fields ?? []).findIndex((f) => f.name === "comment");
expect(commentIndex).toBeGreaterThan(altitudeIndex); // Comment comes after altitude
});
});