Fixed bug in extras segment parsing
This commit is contained in:
98
src/frame.ts
98
src/frame.ts
@@ -770,8 +770,12 @@ export class Frame implements IFrame {
|
|||||||
let commentOffset: number = 0;
|
let commentOffset: number = 0;
|
||||||
let commentBefore: string | undefined = undefined;
|
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.
|
// 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) {
|
while (ext.length >= 7) {
|
||||||
// We first process the altitude marker, because it may appear anywhere
|
// We first process the altitude marker, because it may appear anywhere
|
||||||
// in the comment and we want to extract it and its value before
|
// 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,
|
type: FieldType.STRING,
|
||||||
name: "altitude marker",
|
name: "altitude marker",
|
||||||
data: new TextEncoder().encode("/A=").buffer,
|
data: new TextEncoder().encode("/A=").buffer,
|
||||||
|
value: "/A=",
|
||||||
length: 3
|
length: 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.STRING,
|
type: FieldType.STRING,
|
||||||
name: "altitude",
|
name: "altitude",
|
||||||
data: new TextEncoder().encode(altMatch[1]).buffer,
|
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) {
|
if (altitudeOffset > 0) {
|
||||||
// Splice the comment into "before" and "after" around the altitude token.
|
// Reset the comment with the altitude marker removed.
|
||||||
commentBefore = comment.substring(0, altitudeOffset).trimEnd();
|
commentBefore = comment.substring(0, altitudeOffset);
|
||||||
comment = comment.substring(altitudeOffset + altMatch[0].length).trimStart();
|
comment = comment.substring(altitudeOffset + altMatch[0].length);
|
||||||
ext = commentBefore + comment; // Update ext to reflect the new comment with altitude token removed
|
ext = commentBefore; // Continue processing extensions in the part of the comment before the altitude marker
|
||||||
|
commentOffset = 0; // Reset
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -824,42 +830,46 @@ export class Frame implements IFrame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RNGrrrr -> pre-calculated range in miles (4 digits)
|
// RNGrrrr -> pre-calculated range in miles (4 digits)
|
||||||
if (ext.startsWith("RNG")) {
|
if ((match = ext.match(/^RNG(\d{4})/))) {
|
||||||
const r = ext.substring(3, 7);
|
const r = match[1];
|
||||||
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) {
|
(altitudeOffset !== undefined && commentOffset < altitudeOffset ? beforeFields : fields).push(
|
||||||
(altitudeOffset !== undefined && commentOffset < altitudeOffset ? beforeFields : fields).push(
|
{
|
||||||
{
|
type: FieldType.STRING,
|
||||||
type: FieldType.STRING,
|
name: "range marker",
|
||||||
name: "range marker",
|
value: "RNG",
|
||||||
value: "RNG",
|
length: 3
|
||||||
length: 3
|
},
|
||||||
},
|
{
|
||||||
{
|
type: FieldType.STRING,
|
||||||
type: FieldType.STRING,
|
name: "range (rrrr)",
|
||||||
name: "range (rrrr)",
|
length: 4,
|
||||||
length: 4,
|
value: extras.range.toString() + "km"
|
||||||
value: extras.range.toString() + "km"
|
}
|
||||||
}
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove range token from ext and advance ext for further parsing
|
|
||||||
commentOffset += 7;
|
|
||||||
ext = ext.substring(7).trimStart();
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// 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)
|
// 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 p = match[1];
|
||||||
const h = ext.charAt(4);
|
const h = match[2];
|
||||||
const g = ext.charAt(5);
|
const g = match[3];
|
||||||
const d = ext.charAt(6);
|
const d = match[4];
|
||||||
const pNum = parseInt(p, 10);
|
const pNum = parseInt(p, 10);
|
||||||
const powerWatts = Number.isNaN(pNum) ? undefined : pNum * pNum;
|
const powerWatts = Number.isNaN(pNum) ? undefined : pNum * pNum;
|
||||||
const hIndex = h.charCodeAt(0) - 48;
|
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
|
// remove course/speed token from comment and advance ext for further parsing
|
||||||
comment = comment.substring(7).trimStart();
|
|
||||||
ext = ext.substring(7).trimStart();
|
ext = ext.substring(7).trimStart();
|
||||||
|
|
||||||
// If there is an 8-byte DF/NRQ following (leading '/'), parse that too
|
// 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.
|
// Export comment with extras fields removed, if any were parsed.
|
||||||
comment = comment.substring(commentOffset).trimStart();
|
if (commentOffset > 0 && commentBefore !== undefined && commentBefore.length > 0) {
|
||||||
extras.comment = comment;
|
extras.comment = commentBefore.substring(commentOffset) + comment;
|
||||||
|
} else if (commentBefore !== undefined && commentBefore.length > 0) {
|
||||||
|
extras.comment = commentBefore + comment;
|
||||||
|
} else {
|
||||||
|
extras.comment = comment;
|
||||||
|
}
|
||||||
|
|
||||||
if (withStructure) {
|
if (withStructure) {
|
||||||
const commentBeforeFields: Field[] = commentBefore
|
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.
|
// 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;
|
return extras as Extras;
|
||||||
|
|||||||
@@ -6,6 +6,35 @@ import type { PositionPayload } from "../src/frame.types";
|
|||||||
import { feetToMeters, milesToMeters } from "../src/parser";
|
import { feetToMeters, milesToMeters } from "../src/parser";
|
||||||
|
|
||||||
describe("APRS extras test vectors", () => {
|
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)", () => {
|
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 raw = "NOCALL>APZRAZ,qAS,PA2RDK-14:=5154.19N/00627.77E>PHG500073 de NOCALL";
|
||||||
const frame = Frame.fromString(raw);
|
const frame = Frame.fromString(raw);
|
||||||
@@ -98,7 +127,7 @@ describe("APRS extras test vectors", () => {
|
|||||||
|
|
||||||
it("parses RNG token and emits structure", () => {
|
it("parses RNG token and emits structure", () => {
|
||||||
const raw =
|
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 frame = Frame.fromString(raw);
|
||||||
const res = frame.decode(true) as { payload: PositionPayload | null; structure: Dissected };
|
const res = frame.decode(true) as { payload: PositionPayload | null; structure: Dissected };
|
||||||
const { payload, structure } = res;
|
const { payload, structure } = res;
|
||||||
@@ -112,5 +141,14 @@ describe("APRS extras test vectors", () => {
|
|||||||
const fieldsRNG = (commentSeg!.fields ?? []) as Field[];
|
const fieldsRNG = (commentSeg!.fields ?? []) as Field[];
|
||||||
const hasRNG = fieldsRNG.some((f) => f.name === "range marker");
|
const hasRNG = fieldsRNG.some((f) => f.name === "range marker");
|
||||||
expect(hasRNG).toBe(true);
|
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
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user