MAVLink v1 and v2 framing, signing, named message fields and enum values, and the mission, command, and offboard protocols. One capability of pamoja, one memory-safe Rust core with bindings for TypeScript, Python, and C#.
npm install @pamoja/mavlink
This pulls in @pamoja/native, the compiled engine. npm install pamoja is the whole framework in one package.
The test that runs in CI, spliced here as it ran.
From bindings/node/guides/mavlink.ts:
import {
type MavlinkFrame,
type MavlinkHeader,
MavlinkMessage,
MavlinkParser,
enumEntry,
enumNames,
enumValue,
fromObject,
message,
schemaFor,
} from '@pamoja/mavlink'
const VEHICLE = 1
const AUTOPILOT = 1
const STATION = 255
const PLANNER = 190
// An enum field travels as a number, and the dialect names each number. Printing the name
// keeps a reader from looking up what 2 or 81 means.
const name = (enumeration: string, value: number): string =>
enumEntry(enumeration, value) ?? String(value)
const header = (systemId: number, componentId: number, sequence: number): MavlinkHeader => ({
systemId,
componentId,
sequence,
})
// Every node broadcasts a heartbeat to say what it is and that it is alive. The frame wraps
// the payload in a header and a checksum seeded with the message's own value.
const heartbeatShape = schemaFor('HEARTBEAT')
const announce = fromObject(heartbeatShape, {
type: enumValue('MAV_TYPE_GCS'),
autopilot: enumValue('MAV_AUTOPILOT_INVALID'),
system_status: enumValue('MAV_STATE_ACTIVE'),
mavlink_version: 3,
})
const sent = announce.toFrame(header(STATION, PLANNER, 0))
console.log(`sent ${heartbeatShape.name} in ${sent.bytes.length} bytes`)
// The vehicle answers with its own heartbeat, which reaches the station behind some noise
// and a copy with its last byte flipped in flight.
const vehicle = fromObject(heartbeatShape, {
type: enumValue('MAV_TYPE_QUADROTOR'),
autopilot: enumValue('MAV_AUTOPILOT_ARDUPILOTMEGA'),
base_mode:
enumValue('MAV_MODE_FLAG_CUSTOM_MODE_ENABLED') |
enumValue('MAV_MODE_FLAG_STABILIZE_ENABLED') |
enumValue('MAV_MODE_FLAG_MANUAL_INPUT_ENABLED'),
system_status: enumValue('MAV_STATE_STANDBY'),
mavlink_version: 3,
})
const good = vehicle.toFrame(header(VEHICLE, AUTOPILOT, 0))
const garbled = Buffer.from(good.bytes)
garbled[garbled.length - 1] ^= 0xff
const delivered = Buffer.concat([Buffer.from('???'), garbled, good.bytes])
// The parser skips whatever does not start a frame and drops a frame whose checksum fails,
// so only the good copy comes out.
const frames = new MavlinkParser().push(delivered)
console.log(
`parsed ${frames.length} frame out of ${delivered.length} bytes,` +
' past the noise and the garbled copy',
)
const heard = MavlinkMessage.decode(heartbeatShape, frames[0]!.payload)
console.log(
`heard ${name('MAV_TYPE', heard.get('type'))} on` +
` ${name('MAV_AUTOPILOT', heard.get('autopilot'))},` +
` in ${name('MAV_STATE', heard.get('system_status'))}`,
)
// The base mode is a bitmask, so it names a set of flags rather than one value.
const baseMode = heard.get('base_mode')
console.log(`flags ${enumNames('MAV_MODE_FLAG', baseMode).join(' | ')}`)
if ((baseMode & enumValue('MAV_MODE_FLAG_SAFETY_ARMED')) === 0) {
console.log('disarmed MAV_MODE_FLAG_SAFETY_ARMED is not among them')
}
| Language | Package | Reference |
|---|---|---|
| Rust | pamoja-mavlink |
reference, docs.rs, install |
| TypeScript | @pamoja/mavlink |
reference, install |
| Python | pamoja-mavlink |
reference, install |
| C# | Pamoja.Mavlink |
reference, install |
@pamoja/mavlink reference, every class, function, and type this package exports.MIT
Ergonomic facade over the generated MAVLink binding.
MAVLink is the language drones speak: PX4 and ArduPilot autopilots and MAVSDK ground stations all exchange MAVLink frames, so talking to a vehicle means putting exactly the right bytes on the wire and trusting the bytes that come back. This is that byte layer: v1 and v2 frames, the CRC-16/MCRF4XX checksum every frame carries, the per-message
CRC_EXTRAseed that catches a frame whose shape does not match, and MAVLink 2 signing.Nothing here is limited to the messages this build happens to know. The common dialect's seeds are built in, and Dialect carries any others, derived from a message definition the way the specification does.
Above the bytes sits the shape: MessageSchema names a message's fields, so a MavlinkMessage is filled in and read back by name rather than by byte offset, and MessageSchemaBuilder describes a message this build has never heard of.
Above the messages sit the exchanges: MissionSender and MissionReceiver carry a plan between a station and a vehicle, CommandProtocol matches a command to its acknowledgment and counts retries, and offboard builds setpoints. Each takes a frame off the link and hands back the frame to send, with no IO or timers of its own.