ArduPilot / node-mavlink

This project is providing native TypeScript bindings and tools for sending and receiving MavLink messages over a verity of medium
GNU Lesser General Public License v3.0
75 stars 25 forks source link

If you're looking for the officially supported bindings for JavaScript see the pymavlink project.

Node.js MavLink library

This package is the implementation of serialization and parsing of MavLink messages for v1 and v2 protocols.

It consists partially of code generated from the XML documents in the original MavLink repository and a few pieces that define the parser and serializer.

Getting started

It is extremely easy to get started using this library. First you need to install the required packages. For demonstration purposes we'll be using the serialport package to read the data from serial port.

Reading messages

$ npm install --save node-mavlink serialport

Once you've done it you can start using it. First you'll need a serial port that can parse messages one by one. Please note that since we're using ECMAScript modules the file name should end with .mjs extension (e.g. test.mjs)

import { SerialPort } from 'serialport'
import { MavLinkPacketSplitter, MavLinkPacketParser } from 'node-mavlink'

// substitute /dev/ttyACM0 with your serial port!
const port = new SerialPort({ path: '/dev/ttyACM0', baudRate: 115200 })

// constructing a reader that will emit each packet separately
const reader = port
  .pipe(new MavLinkPacketSplitter())
  .pipe(new MavLinkPacketParser())

reader.on('data', packet => {
  console.log(packet)
})

That's it! That is all it takes to read the raw data. But it doesn't end there - in fact this is only the beginning of what this library can do for you.

Each message consists of multiple fields that contain specific data. Parsing the data is also very easy.

import {
  MavLinkPacketRegistry,
  minimal, common, ardupilotmega
} from 'node-mavlink'

// create a registry of mappings between a message id and a data class
const REGISTRY: MavLinkPacketRegistry = {
  ...minimal.REGISTRY,
  ...common.REGISTRY,
  ...ardupilotmega.REGISTRY,
}

reader.on('data', packet => {
  const clazz = REGISTRY[packet.header.msgid]
  if (clazz) {
    const data = packet.protocol.data(packet.payload, clazz)
    console.log('Received packet:', data)
  }
})

Sending messages

Sending messages is also very easy. One example that is very useful is to send the REQUEST_PROTOCOL_VERSION to switch to protocol version 2.

import { MavLinkProtocolV2, send } from 'node-mavlink'

// Create an instance of of the `RequestProtocolVersionCommand`
// class that will be the vessel for containing the command data.
// Underneath the cover it uses CommandLong to convert the data.
//
// By convention the intermediate fields that are then serialized
// are named with `_` (underscore) prefix and should not be used
// directly. That doesn't mean you can't use them, but if there
// is an equivalent Command class it is just a lot easier and every
// parameter not only has a more descriptive names but also in-line
// documentation.
const command = new common.RequestProtocolVersionCommand()
command.confirmation = 1

port.on('open', async () => {
  // the port is open - we're ready to send data
  await send(port, command, new MavLinkProtocolV2())
})

Interacting with other communication mediums

The splitter and parser work with generic streams. Of course the obvious choice for many use cases will be a serial port but the support doesn't end there.

There are options for streams working over network (TCP or UDP), GSM network - pretty much anything that sends and receives data over Node.js Streams.

Here's an example for connecting to telemetry via TCP (for example using esp-link and a cheap ESP8266 module)

import { connect } from 'net'

// substitute 192.168.4.1 with the IP address of your module
const port = connect({ host: '192.168.4.1', port: 2323 })

port.on('connect', () => {
  console.log('Connected!')
  // here you can start sending commands
})

The rest is exactly the same. The TCP connection also is a stream so piping the data through the MavLinkPacketSplitter and MavLinkPacketParser works as expected.

A short note to my future self and others about baudrates

The default serial port speed for telemetry in Ardupilot is 57600 bauds. This means that in the user interface of the esp-link (accessible via a web page under the same IP address) you need to make sure the speed is properly set on the µC Console tab. Just select the proper baudrate from the dropdown at the top of the page and you'll be all set. No reboot of the module required!

Using MAVESP8266 in UDP mode

The official firmware for setting up a UDP telemetry using ESP8266 is MAVESP8266. This firmware exposes messages over UDP rather than TCP but has other advantages (see the documentation).

To setup a stream that reads from a UDP socket isn't as easy as with TCP sockets (which are in a sense streams on their own) but is not hard at all because the library exposes the MavEsp8266 class that encapsulates all of the hard work for you:

import { MavEsp8266, common } from 'node-mavlink'

async function main() {
  const port = new MavEsp8266()

  // start the communication
  await port.start()

  // log incoming packets
  port.on('data', packet => {
    console.log(packet.debug())
  })

  // You're now ready to send messages to the controller using the socket
  // let's request the list of parameters
  const message = new common.ParamRequestList()
  message.targetSystem = 1
  message.targetComponent = 1

  // The `send` method is another utility method, very handy to have it provided
  // by the library. It takes care of the sequence number and data serialization.
  await port.send(message)
}

main()

That's it! Easy as a lion :)

Signed packages

MavLink v2 introduces package signing. The way it currently works with Mission planner is you give it a pass phrase, Mission Planner encodes it using sha256 hashing algorithm and uses it as part of the signature calculation. Therefore if someone does not know the secret passphrase they won't be able to create packets that would seem to be coming from a source. It's a kind of security thing.

Reading signature

The node-mavlink library introduced signature parsing in version 0.0.1-beta.10. The way to verify if a package can be trusted is as follows:

import { MavLinkPacketSignature } from 'node-mavlink'

// calculate secret key (change 'qwerty' to your secret phrase)
const key = MavLinkPacketSignature.key('qwerty')

// log incoming messages
port.on('data', packet => {
  console.log(packet.debug())
  if (packet.signature) {
    if (packet.signature.matches(key)) {
      // signature valid
    } else {
      // signature not valid - possible fraud package detected
    }
  } else {
    // packet is not signed
  }
})

What you do with that information is up to you. You can continue to process that package or you can drop it. The library imposes no restriction on packets with invalid signatures.

Sending signed packages

First we need to learn how to create a secure key. As mentioned before the key in Mission Planner is created by calculating an SHA256 checksum over a secret phrase that you can specify and then taking the first 6 bytes of it.

To do the same using this library:

import { MavLinkPacketSignature } from 'node-mavlink'

const key = MavLinkPacketSignature.key('your very secret passphrase')

Now that we have the key ready we can send signed packages. Let's use the ParamRequestList as an example:

import { common, sendSigned } from 'node-mavlink'

async function requestParameterList() {
  const message = new common.ParamRequestList()
  message.targetSystem = 1
  message.targetComponent = 1

  await sendSigned(port, message, key)
}

If you're using the MavEsp8266 class for communicating over UDP it also exposes the sendSigned() method with the same signature.

Utility functions

The library exposes a few utility functions that make the life easier when writing application code

async waitFor(cb: Function, timeout: number, interval: number)

This function calls the cb callback periodically at the interval (default: 100ms) and if it returns a truthy value it will stop polling. If, however, the value is falsy for a longer period of time than the timeout (default: 10000ms) then it will throw a Timeout error.

async send(stream: Writable, msg: MavLinkData, protocol: MavLinkProtocol)

This function serializes the msg message using the provided protocol (default: MavLinkProtocolV1) and sends it to the stream. If the process is successful the method returns with the length of written data denoting that no error occurred. However, if the process was not successful it will error out with the underlying error object returned on by the stream.

async sendSigned(stream: Writable, msg: MavLinkData, key: Buffer, linkId: uint8_t, sysid: uint8_t, compid: uint8_t)

This is a similar function to send but does so using MavLink v2 protocol and signs the message.

The default values for some parameters are as follows:

async sleep(ms: number)

This is a very handy utility function that asynchronously pauses for a given time (ms).

Running sim_vehicle.py

The easiest way to start playing around with this package is to use sim_vehicle.py. You can use the default parameters for the MavEsp8266 if you'll make the simulator compatible with it:

$ Tools/autotest/sim_vehicle.py -v ArduCopter -f quad --console --map --out udpin:127.0.0.1:14555

That last parameter (--out udpin:127.0.0.1:14555) opens up for incoming messages in port 14555, which is the default send port for MavEsp8266 and its default firmware.

Registering custom messages

There are times when you want to have custom messages, for example when you're building a rocket and there is no target you can use out of the box. There are actually two scenarios:

  1. You have a few custom messages, but generally you're happy with the original set of messages
  2. You don't care about the original messages, maybe you do about the heartbeat, but nothing else

Registering a single command

There are 3 steps to register a custom command:

a) create a class that defines your custom command b) add it to your REGISTRY c) register your custom command's magic number

The first two steps are pretty self explanatory and there is a plethora of examples in the mavlink-mappings project - use those to learn how to create your own message definitions.

The last step is quite easy:

import { registerCustomMessageMagicNumber } from 'node-mavlink'

registerCustomMessageMagicNumber('999999', 42)

From now on the splitter will know how to properly calculate CRC for your packages and you're all good.

Replacing magic numbers registry all together

Well, if all you care about is the ping, why parse anything else, right? And if on top of the ping command you've got a number of custom messages - all the better to not parse even the messages!

import { MavLinkSplitter, MavLinkParser } from 'node-mavlink'

const MY_MAGIC_NUMBERS = {
  '0': 50, // Heartbeat
  // ...other magic number definitions go here
}

const source = ... // obtain source stream
const reader = source
  .pipe(new MavLinkPacketSplitter({}, { magicNumbers: MY_MAGIC_NUMBERS }))
  .pipe(new MavLinkPacketParser())

Closing thoughts

The original generated sources lack one very important aspect of a reusable library: documentation. Also, most of the time the names are more C-like than JavaScript-like.

When generating sources for data classes a number of things happen:

This leads to generated code that contains not only raw types but also documentation where it is mostly useful: right at your fingertips.

I hope you'll enjoy using this library! If you have any comments, find a bug or just generally want to share your thoughts you can reach me via email: padcom@gmail.com

Peace!