Add decoders for Hydronav, LABOv3 navigation headers

This commit is contained in:
D. Berge
2020-08-31 13:40:56 +02:00
parent c4c09f0a9d
commit e4803da149
5 changed files with 464 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
class NavHeaderError extends Error {
constructor (message, payload) {
super (message);
this.name = "NavHeaderError";
this.payload = payload;
}
}
module.exports = NavHeaderError;

View File

@@ -0,0 +1,225 @@
const NavHeaderError = require('./error');
/**
* Detect if a Hydronav header is present in `buffer`.
*/
function detect (buffer) {
const s = buffer.indexOf("@@@UHEAD");
return s != -1 ? s : false;
};
function parse (buffer) {
const s = buffer.indexOf("@@@UHEAD");
if (s == -1) {
return; // undefined
}
const parser = [
// @@@UHEAD marker
(buf, ctx) => {
if (buf.subarray(s, s+8).toString('ascii') != "@@@UHEAD") {
console.log(s, buf.subarray(s, s+8).toString('ascii'));
throw new NavHeaderError("Expected @@@UHEAD marker not found at position "+s, buf);
}
},
// Timestamp. We also need to pick the date from further down the message
(buf, ctx) => {
const t = buf.subarray(s+8, s+8+15).toString('ascii');
const d = buf.subarray(s+105, s+105+10).toString('ascii');
ctx.tstamp = new Date(d.replace(/(.{2})\/(.{2})\/(.{4})/, "$3-$2-$1") + t.replace(/(.{8}).(.*)/, "T$1.$2Z"));
if (isNaN(ctx.tstamp)) {
throw new NavHeaderError(`Invalid timestamp from '${d}' + '${t}'`, buf);
}
},
// GPS clock, whatever that means
(buf, ctx) => {
ctx.gpsClock = Number(String.fromCharCode(buf[s+24]));
},
// Vessel name
(buf, ctx) => {
ctx.vesselName = buf.subarray(s+26, s+26+8).toString('ascii');
},
// Station number (aka shotpoint)
// NOTE this waypoint will not be valid if mode is offline
(buf, ctx) => {
ctx.shotPoint = Number(buf.subarray(s+35, s+35+7).toString('ascii'))
},
// FSID
// TODO Find out what this is!
(buf, ctx) => {
ctx.fsid = Number(buf.subarray(s+43, s+43+10).toString('ascii'))
},
// Northing
// NOTE We have no clue which CRS
(buf, ctx) => {
ctx.northing = Number(buf.subarray(s+54, s+54+11).toString('ascii'))
const hemisphere = String.fromCharCode(buf[s+65]);
switch (hemisphere) {
case "N":
case "n":
break;
case "S":
case "s":
ctx.northing *= -1;
break;
default:
throw new NavHeaderError("Unexpected hemisphere marker for northings: "+hemisphere, buf);
}
},
// Easting
// NOTE We have no clue which CRS
(buf, ctx) => {
ctx.easting = Number(buf.subarray(s+67, s+67+11).toString('ascii'))
const hemisphere = String.fromCharCode(buf[s+78]);
switch (hemisphere) {
case "E":
case "e":
break;
case "W":
case "w":
ctx.easting *= -1;
break;
default:
throw new NavHeaderError("Unexpected hemisphere marker for eastings: "+hemisphere, buf);
}
},
// Speed
// (we convert to metres per second)
(buf, ctx) => {
ctx.speed = Number(buf.subarray(s+80, s+80+6).toString('ascii'));
const units = buf.subarray(s+86, s+86+2).toString('ascii');
switch (units.toUpperCase()) {
case "KT":
ctx.speed *= (1.852/3.6);
break;
default:
throw new NavHeaderError("Unexpected speed units: "+units, buf);
}
},
// Bearing
// (in degrees)
(buf, ctx) => {
ctx.bearing = Number(buf.subarray(s+89, s+89+6).toString('ascii'));
const units = buf.subarray(s+95, s+95+2).toString('ascii');
switch (units.toUpperCase()) {
case "DG":
break;
default:
throw new NavHeaderError("Unexpected angular units: "+units, buf);
}
},
// Water depth
// (in metres)
(buf, ctx) => {
ctx.waterDepth = Number(buf.subarray(s+98, s+98+5).toString('ascii'));
const units = String.fromCharCode(buf[s+103]);
switch (units.toUpperCase()) {
case "M":
break;
default:
throw new NavHeaderError("Unexpected depth units: "+units, buf);
}
},
// Date
// Already taken care of along with the time part
// Distance offline
// (meaning crossline?)
(buf, ctx) => {
ctx.crossline = Number(buf.subarray(s+116, s+116+5).toString('ascii'));
const units = String.fromCharCode(buf[s+121]);
switch (units.toUpperCase()) {
case "M":
break;
default:
throw new NavHeaderError("Unexpected crossline units: "+units, buf);
}
},
// Distance inline
(buf, ctx) => {
ctx.inline = Number(buf.subarray(s+123, s+123+8).toString('ascii'));
const units = String.fromCharCode(buf[s+131]);
switch (units.toUpperCase()) {
case "M":
break;
default:
throw new NavHeaderError("Unexpected inline units: "+units, buf);
}
},
// Line status
(buf, ctx) => {
const status = String.fromCharCode(buf[s+133]);
switch (status) {
case "0":
ctx.lineStatus = "offline";
break;
case "1":
ctx.lineStatus = "approach";
break;
case "2":
ctx.lineStatus = "online";
break;
case "3":
ctx.lineStatus = "runout";
break;
default:
throw new NavHeaderError("Unrecognised line status: "+status, buf);
}
},
// GPS height
(buf, ctx) => {
ctx.gpsHeight = Number(buf.subarray(s+135, s+135+5).toString('ascii'));
const units = String.fromCharCode(buf[s+140]);
switch (units.toUpperCase()) {
case "M":
break;
default:
throw new NavHeaderError("Unexpected GPS height units: "+units, buf);
}
},
// Line name
(buf, ctx) => {
ctx.lineName = buf.subarray(s+0x8e, s+0x8e+16).toString('ascii');
},
// End of Hydronav data
(buf, ctx) => {
if (String.fromCharCode(buf[s+0x9e]) != "*") {
throw new NavHeaderError("Missing end of HYDRONAV data marker '*'", buf);
}
}
];
const hydronav = {};
hydronav._type = "hydronav";
hydronav._received = new Date();
for (const fn of parser) {
fn(buffer, hydronav);
}
return hydronav;
}
module.exports = {
name: "Hydronav",
detect,
parse
};

View File

@@ -0,0 +1,5 @@
module.exports = {
hydronav: require('./hydronav'),
labo: require('./labo'),
smartsource: require('./smartsource')
};

View File

@@ -0,0 +1,210 @@
const NavHeaderError = require('./error');
/**
* Detect if a LABO header is present in `buffer`.
*/
function detect (buffer) {
const s = buffer.indexOf("$1");
return (s != -1 &&
buffer.subarray(s+6, s+10).toString('ascii') == "0003") ? s : false;
};
function parse (buffer) {
const s = buffer.indexOf("$1");
if (s == -1) {
return; // undefined
}
let offset = 0;
function ascii (length) {
const start = offset;
const end = offset+length;
offset += length;
// const v = buffer.subarray(start, end).toString('ascii');
// console.log("v = ", v);
// return v;
return buffer.subarray(start, end).toString('ascii');
}
const parser = [
// LABO marker
(buf, ctx) => {
if (! /\$[12]/.test(ascii(2))) {
throw new NavHeaderError("Expected LABO marker not found at position "+s, buf);
}
},
// Message length
// NOTE isn't this fixed?
(buf, ctx) => {
ctx.messageLength = Number(ascii(4));
},
// Revision
(buf, ctx) => {
const rev = Number(ascii(4));
if (rev != 3) {
throw new NavHeaderError("Unsupported LABO header version: "+rev, buf);
}
},
// Line status
(buf, ctx) => {
const status = Number(ascii(2));
switch (status) {
case 1:
ctx.lineStatus = "offline";
break;
case 2:
ctx.lineStatus = "approach";
break;
case 3:
ctx.lineStatus = "online";
break;
case 4:
ctx.lineStatus = "runout";
break;
default:
throw new NavHeaderError("Unrecognised line status: "+status, buf);
}
},
// Timestamp
// We process a number of fields in one go
(buf, ctx) => {
const t = ascii(6);
const ms = ascii(7);
const d = ascii(8);
const tref = ascii(3);
if (tref != "UTC" && tref != "GPS") {
throw new NavHeaderError("Unrecognised time reference: "+tref, buf);
}
const tstr =
d.replace(/^(.{4})(.{2})(.{2})$/, "$1-$2-$3") + "T" +
t.replace(/^(.{2})(.{2})(.{2})$/, "$1:$2:$3") +
ms + "Z";
const tstamp = new Date(tstr);
if (isNaN(tstamp)) {
throw new NavHeaderError("Invalid timestamp: "+tstr, buf);
}
ctx.tstamp = tstamp;
},
// Shotpoint
(buf, ctx) => {
ctx.shotPoint = Number(ascii(6));
},
// Line name
(buf, ctx) => {
ctx.lineName = ascii(16);
},
// Master latitude
(buf, ctx) => {
ctx.latitudeMaster = Number(ascii(11));
},
// Master longitude
(buf, ctx) => {
ctx.longitudeMaster = Number(ascii(11));
},
// Master depth
// in metres
(buf, ctx) => {
ctx.waterDepth = Number(ascii(6));
},
// Source latitude
(buf, ctx) => {
ctx.latitude = Number(ascii(11));
},
// Source longitude
(buf, ctx) => {
ctx.longitude = Number(ascii(11));
},
// Source gyro
// NOTE is this the same as Hydronav bearing?
(buf, ctx) => {
ctx.heading = Number(ascii(5));
},
// Master CMG
(buf, ctx) => {
ctx.cmg = Number(ascii(5));
},
// Master speed
// FIXME Probably knots?
// We convert to m/s
(buf, ctx) => {
ctx.speed = Number(ascii(4)) * 1.852 / 3.6;
},
// Vessel ID
(buf, ctx) => {
ctx.vesselId = ascii(3);
},
// Master easting
(buf, ctx) => {
ctx.eastingMaster = Number(ascii(11));
},
// Master northing
(buf, ctx) => {
ctx.northingMaster = Number(ascii(11));
},
// Source delta easting
// NOTE This is the vessel's easting in Triggerfish
(buf, ctx) => {
ctx.deltaEasting = Number(ascii(7));
},
// Source delta northing
// NOTE This is the vessel's northing in Triggerfish
(buf, ctx) => {
ctx.deltaNorthing = Number(ascii(7));
},
// Line bearing
(buf, ctx) => {
ctx.lineBearing = Number(5);
}
// Julian day
// NOTE We don't care about this, ignored
// -rstdiff option
// NOTE We don't even know what this is, ignored
];
const labo = {};
labo._type = "labov3";
labo._received = new Date();
for (const fn of parser) {
fn(buffer, labo);
}
return labo;
}
module.exports = {
name: "Labo v3",
detect,
parse
};

View File

@@ -0,0 +1,14 @@
function detect (buffer) {
return false;
}
function parse (buffer) {
return; // undefined
}
module.exports = {
name: "SmartSource",
detect,
parse
};