UNPKG

isatdatapro-microservices

Version:

A library for creating microservices to access Inmarsat's IsatData Pro satellite IoT system

892 lines (856 loc) 24.8 kB
/** * Core Modem Codec module * @module messageCodecs/coreModem */ 'use strict'; const codecServiceId = 0; const logger = require('../logging').loggerProxy(__filename); const { Mobile, MessageReturn, MessageForward } = require('../database/models'); const { Payload, Field } = require('../database/models/MessagePayloadJson'); const { parseFieldValue } = require('./commonMessageFormat'); const event = require('../eventHandler'); /** * Rounds a number to a certain decimal precision * @param {number} num A decimal number * @param {number} places The number of decimal places to round to * @returns {number} rounded */ function roundTo(num, places) { if (typeof (places) !== 'number') places = 0; return +(Math.round(num + 'e+' + places) + 'e-' + places); } /** * Returns a datestamp from a day and minute, assuming the current year * @param {number} year Full year UTC * @param {number} month From 0..11 UTC * @param {number} dayOfMonth Day of month from 1..31 UTC * @param {number} minuteOfDay Minute of day from 0..1439 UTC * @returns {Date} */ function timestampFromMinuteDay(year, month, dayOfMonth, minuteOfDay) { const hour = minuteOfDay / 60; const minute = minuteOfDay % 60; const tsDate = new Date(year, month, dayOfMonth, hour, minute); return tsDate; } /** * Sets up mobile metadata template for update * @param {Message} message A Return or Forward message * @returns {Mobile} A Mobile object */ function populateMobile(message) { let mobile = new Mobile(); mobile.mobileId = message.mobileId; mobile.mailboxId = message.mailboxId; if (message.receiveTimeUtc) { mobile.lastMessageReceivedTimeUtc = message.receiveTimeUtc; mobile.satelliteRegion = message.satelliteRegion; } else { mobile.mobileWakeupPeriod = message.mobileWakeupPeriod; } return mobile; } /** * Logs a warning for parsing unknown field names * @param {string} fieldName The field name * @param {number} messageId The message ID */ function handleUnknownField(fieldName, messageId) { logger.warn(`Unknown field ${fieldName} in message ${messageId}`); } /** * Parses Inmarsat-defined standard modem Mobile-Originated messages * and emits various events * @public * @param {MessageReturn} message The message with metadata * @returns {Object} mobile metadata */ function parseCoreModem(message) { if (!message.payloadJson || message.codecServiceId !== 0) { throw new Error(`Attempt to parse message ${message.messageId}` + ` as core modem`); } const messageType = message.payloadJson.codecMessageId; switch (messageType) { case 97: case 1: case 0: return parseModemRegistration(message); case 2: parseModemProtocolError(message); break; case 70: return parseModemSleepSchedule(message); case 72: return parseModemLocation(message); case 98: return parseModemLastRxInfo(message); //break; case 99: return parseModemRxMetrics(message); //break; case 100: return parseModemTxMetrics(message); //break; case 112: return parseModemPingReply(message); //break; case 113: return parseNetworkPingRequest(message); //break; case 115: return parseModemBroadcastIds(message); default: logger.warn(`No parsing logic defined for SIN 0 MIN ${messageType}`); } } /** * Parses the Registration message or Configuration response * @private * @param {MessagePayload} message JSON Payload structure * @param {MessageMetadata} meta Metadata including mobileId, timestamp, [topic] * @returns {Object} { mobile, event } */ function parseModemRegistration(message) { let mobile = populateMobile(message); mobile.lastRegistrationTimeUtc = message.receiveTimeUtc; let tmp = {}; message.payloadJson.fields.forEach((field) => { switch (field.name) { case 'hardwareMajorVersion': tmp.hwMajor = parseFieldValue(field); break; case 'hardwareMinorVersion': tmp.hwMinor = parseFieldValue(field); break; case 'softwareMajorVersion': tmp.fwMajor = parseFieldValue(field); break; case 'softwareMinorVersion': tmp.fwMinor = parseFieldValue(field); break; case 'product': tmp.productId = parseFieldValue(field); break; case 'wakeupPeriod': mobile.mobileWakeupPeriod = parseFieldValue(field); break; case 'lastResetReason': mobile.lastResetReason = parseFieldValue(field); break; case 'virtualCarrier': tmp.vcId = parseFieldValue(field); break; case 'beam': tmp.beamId = parseFieldValue(field); break; case 'vain': tmp.vain = parseFieldValue(field); break; case 'operatorTxState': mobile.operatorTxState = parseFieldValue(field); break; case 'userTxState': mobile.userTxState = parseFieldValue(field); break; case 'broadcastIDCount': mobile.broadcastIdCount = parseFieldValue(field); break; default: handleUnknownField(field.name, message.messageId); } }); mobile.version.hardware = `${tmp.hwMajor}.${tmp.hwMinor}`; mobile.version.firmware = `${tmp.fwMajor}.${tmp.fwMinor}`; mobile.version.productId = `${tmp.productId}`; switch (message.codecMessageId) { case 0: event.modemRegistration(mobile); break; case 1: event.modemBeamSwitch(mobile); break; default: event.modemConfigReply(mobile); } event.modemRegistration(mobile); return mobile; } /** * Parses the modem error message * @private * @param {MessageReturn} message The return message */ function parseModemProtocolError(message) { let mobile = populateMobile(message); let error = { mobileId: message.mobileId, messageId: message.messageId, timestamp: message.receiveTimeUtc, messageReference: null, errorCode: null, errorInfo: null, }; message.payloadJson.fields.forEach(field => { switch (field.name) { case 'messageReference': error.messageReference = parseFieldValue(field); break; case 'errorCode': error.errorCode = parseFieldValue(field); break; case 'errorInfo': error.errorInfo = parseFieldValue(field); break; default: logger.warn(`Unknown field ${field.name} in ${message.name}`); } }); event.modemProtocolError(error); return error; } /** * Returns the wakeup interval in seconds * @private * @param {number | string} wakeupCode * @returns {number} interval in seconds */ function getWakeupSeconds(wakeupCode) { let interval = 5; switch (wakeupCode) { case 0: case 'None': break; case 1: case 'Seconds30': interval = 30; break; case 2: case 'Seconds60': interval = 60; break; case 3: case 'Minutes3': interval = 3 * 60; break; case 4: case 'Minutes10': interval = 10 * 60; break; case 5: case 'Minutes30': interval = 30 * 60; break; case 6: case 'Minutes2': interval = 2 * 60; break; case 7: case 'Minutes5': interval = 5 * 60; break; case 8: case 'Minutes15': interval = 15 * 60; break; case 9: case 'Minutes20': interval = 20 * 60; break; default: console.warn(`unrecognized wakeupPeriod: ${wakeupCode}`); } return interval; } /** * Parses wakeup interval change notification * @private * @param {MessageReturn} message JSON Payload structure * @returns {object} { mobile, event } */ function parseModemSleepSchedule(message) { let mobile = populateMobile(message); let eventDetail = {}; message.payloadJson.fields.forEach(field => { switch (field.name) { case 'wakeupPeriod': mobile.mobileWakeupPeriod = parseFieldValue(field); let wakeupSeconds = getWakeupSeconds(mobile.mobileWakeupPeriod); eventDetail.mobileWakeupPeriod = `${wakeupSeconds} seconds`; break; case 'mobileInitiated': eventDetail.localInitiated = parseFieldValue(field); break; case 'messageReference': eventDetail.messageReference = parseFieldValue(field); break; default: handleUnknownField(field.name, message.messageId); } }); event.modemWakeupPeriodChange(eventDetail); return mobile; } /** * Parses location and timestamp data to update the IdpMobiles * collection with device metadata * @private * @param {MessageReturn} message The location message * @returns {Mobile} The Mobile metadata */ function parseModemLocation(message) { let mobile = populateMobile(message); let tmp = {}; message.payloadJson.fields.forEach(field => { switch (field.name) { case 'latitude': case 'longitude': mobile.location[field.name] = roundTo(parseFieldValue(field) / 60000, 6); break; case 'altitude': case 'speed': case 'heading': case 'fixStatus': mobile.location[field.name] = parseFieldValue(field); break; case 'dayOfMonth': case 'minuteOfDay': tmp[field.name] = parseFieldValue(field); break; default: handleUnknownField(field.name, message.messageId); } }); const rxTime = new Date(message.receiveTimeUtc); const year = rxTime.getUTCFullYear(); const month = rxTime.getUTCMonth(); //months from 0-11 mobile.location.timestamp = timestampFromMinuteDay( year, month, tmp.dayOfMonth, tmp.minuteOfDay); return mobile; } /** * Parses response to query for last receive information * @private * @param {MessageReturn} message The lastRxInfo message */ function parseModemLastRxInfo(message) { let mobile = populateMobile(message); let eventDetail = {}; message.payloadJson.fields.forEach(field => { switch (field.name) { case 'sipValid': case 'subframe': case 'packets': case 'packetsOK': case 'frequencyOffset': case 'timingOffset': case 'packetCNO': case 'uwCNO': case 'uwRSSI': case 'uwSymbols': case 'uwErrors': case 'packetSymbols': case 'packetErrors': eventDetail[field.name] = parseFieldValue(field); break; default: handleUnknownField(field.name, message.messageId); } }); event.modemLastRxInfoResponse(eventDetail); return mobile; } const METRICS_PERIODS = { 'SinceReset': 0, 'LastPartialMinute': 1, 'LastFullMinute': 2, 'LastPartialHour': 3, 'LastFullHour': 4, 'LastPartialDay': 5, 'LastFullDay': 6, }; /** * Returns a string value of the metrics period, since it may not be an integer * (e.g. 'partial minute' is non-specific) * @param {(string|number)} periodCode The period over which metrics were * calculated by the modem * @returns {string} the enumerated period * @throws {Error} if periodCode invalid */ function getMetricsPeriod(periodCode) { let period; switch (periodCode) { case 0: case 'SinceReset': period = 'SinceReset'; break; case 1: case 'LastPartialMinute': period = 'LastPartialMinute'; break; case 2: case 'LastFullMinute': period = 'LastFullMinute'; break; case 3: case 'LastPartialHour': period = 'LastPartialHour'; break; case 4: case 'LastFullHour': period = 'LastFullHour'; break; case 5: case 'LastPartialDay': period = 'LastPartialDay'; break; case 6: case 'LastFullDay': period = 'LastFullDay'; break; case 15: case 14: case 13: case 12: case 11: case 10: case 9: case 8: case 7: default: throw new Error(`Unsupported Metrics period ${periodCode}`); } return period; } /** * Parses response to get receive metrics * @private * @param {MessageReturn} message The message * @param {MessageMetadata} meta Metadata including mobileId, timestamp, [topic] */ function parseModemRxMetrics(message) { let mobile = populateMobile(message); let eventDetail = {}; message.payloadJson.fields.forEach(field => { switch (field.name) { case 'period': eventDetail.period = getMetricsPeriod(parseFieldValue(field)); break; case 'AvgCN0': eventDetail.avgSnr = parseFieldValue(field); break; case 'SamplesCN0': eventDetail.samplesSnr = parseFieldValue(field); break; case 'ChannelErrorRate': eventDetail.channelErrorRate = parseFieldValue(field); break; case 'numSegments': case 'numSegmentsOk': case 'uwErrorRate': eventDetail[field.name] = parseFieldValue(field); break; default: handleUnknownField(field.name, message.messageId); } }); event.modemRxMetricsReply(eventDetail); return mobile; } /** * Returns metadata by parsing an encoded metric * @private * @param {Object} txMetrics * @returns {{segmentsTotal, segmentsOk, segmentsFailed}} */ function processTxMetric(txMetrics) { let meta = {}; txMetrics.forEach(metric => { switch (metric.name) { case 'PacketsTotal': meta.segmentsTotal = metric.value; break; case 'PacketsSuccess': meta.segmentsOk = metric.value; break; case 'PacketsFailed': meta.segmentsFailed = metric.value; break; } }); return meta; } /** * Parses response to get transmit metrics * @private * @param {MessageReturn} message The return message */ function parseModemTxMetrics(message) { let mobile = populateMobile(message); let eventDetails = {}; let tmp = {}; let packetTypeMask = 0; message.payloadJson.fields.forEach(field => { switch (field.name) { case 'period': eventDetails.period = getMetricsPeriod(parseFieldValue(field)); break; case 'packetTypeMask': packetTypeMask = parseFieldValue(field); tmp.bitmask = []; for (let b = 0; b < 8; b++) { tmp.bitmask[b] = (packetTypeMask >> b) & 1; } break; case 'txMetrics': tmp.txMetrics = parseFieldValue(field); break; default: handleUnknownField(field.name, message.messageId); } }); eventDetails.txMetrics = []; if (packetTypeMask === 0) { let metric = { type: 'none' }; eventDetails.txMetrics.push(metric); } else { for (let i = 0; i < tmp.bitmask.length; i++) { if (tmp.bitmask[i] === 1) { let metric = {}; switch (i) { case 0: metric.type = 'ack'; break; case 1: metric.type = '0.5s subframe 0.33 rate'; break; case 2: metric.type = '0.5s subframe 0.5 rate'; break; case 3: metric.type = '0.5s subframe 0.75 rate'; break; case 5: metric.type = '1s subframe 0.33 rate'; break; case 6: metric.type = '1s subframe 0.5 rate'; break; default: metric.type = 'undefined'; } Object.assign(metric, processTxMetric(tmp.txMetrics[i])) eventDetails.txMetrics.push(metric); } } } event.modemTxMetricsReply(eventDetails); return mobile; } /** * Returns the converted pingTime field value from timestamp * @param {string} timestamp datestamp * @returns {number} */ function pingTime(timestamp) { let d; if (typeof (timestamp) === 'undefined') { d = new Date(); } else { d = new Date(timestamp); } const pingTime = ((d.getUTCHours() * 3600 + d.getUTCMinutes() * 60 + d.getUTCSeconds()) % 65535); return pingTime; } /** * Parses a ping response to update the IdpMobiles collection metadata * @private * @param {MessageReturn} message The return message */ function parseModemPingReply(message) { let mobile = populateMobile(message); let latency = {}; let requestTime, responseTime; let receiveTime = pingTime(message.receiveTimeUtc); message.payloadJson.fields.forEach(field => { switch (field.name) { case 'requestTime': requestTime = parseFieldValue(field); break; case 'responseTime': responseTime = parseFieldValue(field); break; default: handleUnknownField(field.name, message.messageId); } }); if (responseTime < requestTime) { responseTime += 65535; if (responseTime > 86399) { responseTime -= 86400 } } latency.forward = responseTime - requestTime; if (receiveTime < responseTime) { receiveTime += 65535; if (receiveTime > 86399) { receiveTime -= 86400 } } latency.return = receiveTime - responseTime; latency.roundTrip = latency.forward + latency.return; let eventDetails = { requestTime: requestTime, responseTime: responseTime, receiveTime: receiveTime, latency: latency, }; event.modemPingReply(eventDetails); return mobile; } /** * Parses request from modem for network ping response * (note: response is automatically generated by the network) * @private * @param {MessageReturn} message The return message */ function parseNetworkPingRequest(message) { let mobile = populateMobile(message); let eventDetail = {}; let requestTime; const receiveTime = pingTime(message.receiveTimeUtc); message.payloadJson.fields.forEach(field => { switch (field.name) { case 'requestTime': requestTime = parseFieldValue(field); eventDetail.latency = receiveTime - requestTime; break; default: handleUnknownField(field.name, message.messageId); } }); event.networkPingRequest(eventDetail); return mobile; } /** * Parses request from modem for network ping response * (note: response is automatically generated by the network) * @private * @param {MessageReturn} message The return message * @returns {Mobile} Mobile metadata */ function parseModemBroadcastIds(message) { try { let mobile = populateMobile(message); mobile.broadcastIds = []; message.payloadJson.fields.forEach(field => { switch (field.name) { case 'broadcastIDs': //TODO: determine if zero values should populate the array parseFieldValue(field).forEach(entry => { if (entry[0].value !== 0) { mobile.broadcastIds.push(entry[0].value); } }); break; default: handleUnknownField(field.name, message.messageId); } }); mobile.broadcastIdCount = mobile.broadcastIds.length; event.broadcastIdResponse(mobile.mobileId, mobile.broadcastIds); return mobile; } catch (e) { logger.error(e.stack); } } //: ***** Mobile-Terminated (aka Forward) Message Parsers ********************* /** * Encodes the modem reset message based on the reset type * @param {(string|number)} [resetType='ModemFlush'] * @returns {MessageForward} Message and raw payload number array * @throws {Error} on invalid resetType */ function commandReset(resetType) { const resetTypes = { 'ModemPreserve': 0, 'ModemFlush': 1, 'Terminal': 2, 'TerminalModemFlush': 3, }; if (typeof (resetType) === 'undefined') { resetType = 'ModemFlush'; } else if (typeof(resetType) === 'number') { for (let t in resetTypes) { if (resetTypes[t] === resetType) { resetType = t; break; } } } else if (typeof(resetType) === 'string') { resetType = resetType[0].toUpperCase() + resetType.split(1); } if (!(resetType in resetTypes)) { throw new Error(`Invalid resetType ${resetType}`) } let payloadJson = new Payload('reset', 0, 68, true); payloadJson.addField(new Field('resetType', 'enum', resetType)); // payloadRaw = [0, 68, resetType]; return payloadJson; } /** * Returns a setWakeupPeriod message * @param {(string|number)} mobileWakeupPeriod A valid wakeupPeriod * @returns {MessageForward} The forward message * @throws {Error} if mobileWakeupPeriod is invalid */ function commandSetMobileWakeupPeriod(mobileWakeupPeriod) { const wakeupPeriods = { 'None': 0, 'Seconds30': 1, 'Seconds60': 2, 'Minutes3': 3, 'Minutes10': 4, 'Minutes30': 5, 'Minutes2': 6, 'Minutes5': 7, 'Minutes15': 8, 'Minutes20': 9, }; function valueError() { throw new Error(`Invalid mobileWakeupPeriod ${mobileWakeupPeriod}`); } if (typeof(mobileWakeupPeriod) === 'string') { mobileWakeupPeriod = mobileWakeupPeriod[0].toUpperCase() + mobileWakeupPeriod.slice(1); if (!(mobileWakeupPeriod in wakeupPeriods)) { valueError(); } } else if (typeof(mobileWakeupPeriod) === 'number') { let isValid = false; for (let prop in wakeupPeriods) { if (wakeupPeriods.hasOwnProperty(prop) && wakeupPeriods[prop] === mobileWakeupPeriod) { isValid = true; break; } } if (!isValid) { valueError(); } } else { valueError(); } let payloadJson = new Payload('setSleepSchedule', 0, 70, true); payloadJson.addField(new Field('wakeupPeriod', 'enum', mobileWakeupPeriod)); return payloadJson; } /** * Mutes or unmutes the modem transmitter * @param {boolean} muteFlag Set or clear transmit mute * @returns {MessageForward} The forward message */ function commandMute(muteFlag) { logger.warn('Feature not implemented'); let payloadJson = new Payload('setTxMute', 0, 71, true); payloadJson.addField(new Field('reserved', 'unsignedint', 0)); payloadJson.addField(new Field('txMute', 'boolean', muteFlag)); return payloadJson; } /** * Returns payload for location request * @returns {Object} payload */ function commandGetLocation() { let payloadJson = new Payload('getPosition', 0, 72, true); // message.payloadRaw = [0, 72]; return payloadJson; } /** * Gets modem configuration * @returns {MessageForward} The message */ function commandGetModemConfiguration() { let payloadJson = new Payload('getConfiguration', 0, 97, true); // message.payloadRaw = [0, 97]; return payloadJson; } /** * Gets modem last receive information * @returns {MessageForward} The message */ function commandGetLastRxInfo() { console.warn('Feature not tested'); let payloadJson = new Payload('getLastRxInfo', 0, 98, true); // message.payloadRaw = [0, 98]; return payloadJson; } /** * Returns a JSON payload to query modem Transmit metrics * @param {string} [metricsPeriod='LastPartialMinute'] * @returns {MessageForward} The forward message */ function commandGetRxMetrics(metricsPeriod) { console.warn('Feature not tested'); try { metricsPeriod = getMetricsPeriod(metricsPeriod); } catch (e) { metricsPeriod = 'LastPartialMinute'; } let payloadJson = new Payload('getRxMetrics', 0, 99, true); payloadJson.addField(new Field('reserved', 'boolean', true)); payloadJson.addField(new Field('period', 'enum', metricsPeriod)); // message.payloadRaw = [0, 99, 2]; return payloadJson; } /** * Returns a JSON payload to query modem Transmit metrics * @param {string} [metricsPeriod='LastPartialMinute'] * @returns {MessageForward} The forward message */ function commandGetTxMetrics(metricsPeriod) { console.warn('Feature not tested'); try { metricsPeriod = getMetricsPeriod(metricsPeriod); } catch (e) { metricsPeriod = 'LastPartialMinute'; } let payloadJson = new Payload('getRxMetrics', 0, 100, true); payloadJson.addField(new Field('Reserved', 'boolean', true)); payloadJson.addField(new Field('MetricsPeriod', 'enum', metricsPeriod)); // message.payloadRaw = [0, 100, 2]; return payloadJson; } /** * Returns payload for a modem ping request * @returns {MessageForward} The forward message */ function commandModemPing() { let payloadJson = new Payload('pingModem', 0, 112, true); payloadJson.addField(new Field('requestTime', 'unsignedint', pingTime())); // TODO: calculate requestTime for rawPayload // message.payloadRaw = [0, 112]; return payloadJson; } /** * Gets provisioned Broadcast IDs * @returns {MessageForward} The forward message */ function commandGetBroadcastIds() { let payloadJson = new Payload('requestBroadcastIds', 0, 115, true); // message.payloadRaw = [0, 115]; return payloadJson; } module.exports = { codecServiceId, //parseCoreModem, parse: parseCoreModem, commandMessages: { reset: commandReset, setWakeupPeriod: commandSetMobileWakeupPeriod, setTxMute: commandMute, getLocation: commandGetLocation, getConfiguration: commandGetModemConfiguration, ping: commandModemPing, getBroadcastIds: commandGetBroadcastIds, } };