Skip to content

Commit

Permalink
Meteorological and hydrographic data from AIS VDM binary message 8 (#245
Browse files Browse the repository at this point in the history
)
  • Loading branch information
KEGustafsson committed Feb 17, 2024
1 parent 141bcbe commit 629db22
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 12 deletions.
161 changes: 150 additions & 11 deletions hooks/VDM.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ const debug = require('debug')('signalk-parser-nmea0183/VDM')
const utils = require('@signalk/nmea0183-utilities')
const Decoder = require('ggencoder').AisDecode
const schema = require('@signalk/signalk-schema')
const knotsToMs = (v) =>
parseFloat(utils.transform(v, 'knots', 'ms').toFixed(2))
const degToRad = (v) => utils.transform(v, 'deg', 'rad')
const cToK = (v) => parseFloat(utils.transform(v, 'c', 'k').toFixed(2))
const nmToM = (v) => parseFloat(utils.transform(v, 'nm', 'm').toFixed(2))

const stateMapping = {
0: 'motoring',
Expand Down Expand Up @@ -67,6 +72,50 @@ const specialManeuverMapping = {
3: 'reserved',
}

const beaufortScale = {
0: 'calm, 0-0.2 m/s',
1: 'light air, 0.3-1.5 m/s',
2: 'light breeze, 1.6-3.3 m/s',
3: 'gentle breeze, 3.4-5.4 m/s',
4: 'moderate breeze, 5.5-7.9 m/s',
5: 'fresh breeze, 8-10.7 m/s',
6: 'strong breeze, 10.8-13.8 m/s',
7: 'high wind, 13.9-17.1 m/s',
8: 'gale, 17.2-20.7 m/s',
9: 'strong gale, 20.8-24.4 m/s',
10: 'storm, 24.5-28.4 m/s',
11: 'violent storm, 28.5-32.6 m/s',
12: 'hurricane-force, ≥ 32.7 m/s',
13: 'not available',
14: 'reserved',
15: 'reserved',
}

const statusTable = {
0: 'steady',
1: 'decreasing',
2: 'increasing',
3: 'not available',
}

const precipitationType = {
0: 'reserved',
1: 'rain',
2: 'thunderstorm',
3: 'freezing rain',
4: 'mixed/ice',
5: 'snow',
6: 'reserved',
7: 'not available',
}

const iceTable = {
0: 'no',
1: 'yes',
2: 'reserved',
3: 'not available',
}

module.exports = function (input, session) {
const { id, sentence, parts, tags } = input
const data = new Decoder(sentence, session)
Expand Down Expand Up @@ -115,16 +164,6 @@ module.exports = function (input, session) {
})
}

if (data.lon && data.lat) {
values.push({
path: 'navigation.position',
value: {
longitude: data.lon,
latitude: data.lat,
},
})
}

if (data.length) {
values.push({
path: 'design.length',
Expand Down Expand Up @@ -262,18 +301,118 @@ module.exports = function (input, session) {
}

if (typeof data.fid !== 'undefined') {
if (data.fid == 31 || data.fid == 11 || data.fid == 33) {
contextPrefix = 'meteo.'
}
values.push({
path: 'sensors.ais.functionalId',
value: data.fid,
})
}

if (data.lon && data.lat) {
values.push({
path: 'navigation.position',
value: {
longitude: data.lon,
latitude: data.lat,
},
})
}

;[
['avgwindspd', 'wind.averageSpeed', knotsToMs],
['windgust', 'wind.gust', knotsToMs],
['winddir', 'wind.directionTrue', degToRad],
['windgustdir', 'wind.gustDirectionTrue', degToRad],
['airtemp', 'outside.temperature', cToK],
['relhumid', 'outside.relativeHumidity', (v) => v],
['dewpoint', 'outside.dewPointTemperature', cToK],
['airpress', 'outside.pressure', (v) => v * 100],
['waterlevel', 'water.level', (v) => v],
['signwavewhgt', 'water.waves.significantHeight', (v) => v],
['waveperiod', 'water.waves.period', (v) => v],
['wavedir', 'water.waves.directionTrue', degToRad],
['swellhgt', 'water.swell.height', (v) => v],
['swellperiod', 'water.swell.period', (v) => v],
['swelldir', 'water.swell.directionTrue', degToRad],
['watertemp', 'water.temperature', cToK],
['salinity', 'water.salinity', (v) => v],
['surfcurrspd', 'water.current.drift', knotsToMs],
['surfcurrdir', 'water.current.set', degToRad],
].forEach(([propName, path, f]) => {
if (data[propName] !== undefined) {
contextPrefix = 'meteo.'
values.push({
path: `environment.` + path,
value: f(data[propName]),
})
}
})
;[
['ice', 'water.ice', iceTable],
['precipitation', 'outside.precipitation', precipitationType],
['seastate', 'water.seaState', beaufortScale],
['waterlevelten', 'water.levelTendency', statusTable],
['airpressten', 'outside.pressureTendency', statusTable],
].forEach(([propName, path, f]) => {
if (data[propName] !== undefined) {
contextPrefix = 'meteo.'
values.push(
{
path: `environment.` + path,
value: f[data[propName]],
},
{
path: `environment.` + path + `Value`,
value: data[propName],
}
)
}
})

if (data.horvisib !== undefined && data.horvisibrange !== undefined) {
contextPrefix = 'meteo.'
values.push(
{
path: 'environment.outside.horizontalVisibility',
value: utils.transform(data.horvisib, 'nm', 'm'),
},
{
path: 'environment.outside.horizontalVisibility.overRange',
value: data.horvisibrange,
}
)
}

if (
data.utcday !== undefined &&
data.utchour !== undefined &&
data.utcminute !== undefined
) {
contextPrefix = 'meteo.'
const y = new Date().getUTCFullYear()
const m = new Date().getUTCMonth() + 1
const d = data.utcday
const h = data.utchour
const min = data.utcminute
const date = `${y}-${m.toString().padStart(2, '0')}-${d
.toString()
.padStart(2, '0')}T${h.toString().padStart(2, '0')}:${min
.toString()
.padStart(2, '0')}:00.000Z`
values.push({
path: 'environment.date',
value: date,
})
}

if (values.length === 0) {
return null
}

const delta = {
context: contextPrefix + `urn:mrn:imo:mmsi:${data.mmsi}`,
context: contextPrefix + `urn:mrn:imo:mmsi:${data.mmsikey || data.mmsi}`,
updates: [
{
source: tags.source,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dependencies": {
"@signalk/nmea0183-utilities": "^0.8.0",
"@signalk/signalk-schema": "^1.7.1",
"ggencoder": "^1.0.4",
"ggencoder": "^1.0.8",
"moment-timezone": "^0.5.21",
"split": "^1.0.1"
},
Expand Down
48 changes: 48 additions & 0 deletions test/VDM.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,52 @@ describe('VDM', function () {
.filter((pathValue) => pathValue.path === '')[3]
.value.registrations.imo.should.equal('IMO 1010258')
})

it('meteo single sentence converts ok', () => {
const delta = new Parser().parse(
'!AIVDM,1,1,,A,8@2R5Ph0GhOCT1a2VvkrgwvlFR06EuOwgqrqwnSwe7wvlOwwsAwwnSGmwvwt,0*40'
)
delta.context.should.equal('meteo.urn:mrn:imo:mmsi:002655619:366097')
const currentYear = new Date().getFullYear();
const output = [
['environment.water.level', -0.17],
['environment.water.levelTendency', 'steady'],
['environment.water.levelTendencyValue', 0],
['environment.date', currentYear + '-02-22T15:42:00.000Z']
]
output.forEach(([path, value]) =>
delta.updates[0].values
.find((pathValue) => pathValue.path === path)
.value.should.equal(value)
)
})

it('meteo dual sentence converts ok', () => {
const meteoSentences = [
'!AIVDM,2,1,4,A,8@2R5Ph0GhENJAb8wnScjAJ:AB06EuOwgwl?wnSwe7wvlOwwsAwwnSGm,0*15',
'!AIVDM,2,2,4,A,wvwt,0*10',
]
const parser = new Parser()
let delta = parser.parse(meteoSentences[0])
should.equal(delta, null)
delta = parser.parse(meteoSentences[1])
delta.context.should.equal('meteo.urn:mrn:imo:mmsi:002655619:967728')
delta.updates[0].values[3].value.longitude.should.equal(11.7283)
delta.updates[0].values[3].value.latitude.should.equal(57.9669)
const currentYear = new Date().getFullYear()
const output = [
['sensors.ais.designatedAreaCode', 1],
['sensors.ais.functionalId', 31],
['environment.wind.averageSpeed', 9.26],
['environment.wind.gust', 11.32],
['environment.wind.directionTrue', 4.817108736604238],
['environment.wind.gustDirectionTrue', 4.817108736604238],
['environment.date', currentYear + '-02-20T14:47:00.000Z'],
]
output.forEach(([path, value]) =>
delta.updates[0].values
.find((pathValue) => pathValue.path === path)
.value.should.equal(value)
)
})
})

0 comments on commit 629db22

Please sign in to comment.