Skip to content

implement 32 bit system IDs - #1229

Open
tridge wants to merge 11 commits into
ArduPilot:masterfrom
tridge:pr-sysid-32
Open

implement 32 bit system IDs#1229
tridge wants to merge 11 commits into
ArduPilot:masterfrom
tridge:pr-sysid-32

Conversation

@tridge

@tridge tridge commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

This is a WIP effort to implement 32 bit system IDs in MAVLink2. It is based on mavlink/rfcs#20 with a change to use 32 bit instead of 16 bit wide system ID.
Only C and python generation so far, plus addition of CS and JavaScript dropping of unknown incompatible flags
I am also planning on resolving the 32 bit uint32_t issue with ArduPilot parameters as part of this effort which will make using IPv4 addresses more practical

buf[ofs++] = (target_sysid >> 16) & 0xFF;
buf[ofs++] = (target_sysid >> 24) & 0xFF;
buf[ofs++] = target_compid;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does MAVLINK_IFLAG_TARGETTED require MAVLINK_IFLAG_SYSID32?

Suggested change
}
if (incompat_flags & MAVLINK_IFLAG_TARGETTED) {
buf[ofs++] = target_sysid & 0xFF;
if (incompat_flags & MAVLINK_IFLAG_SYSID32)
buf[ofs++] = (target_sysid >> 8) & 0xFF;
buf[ofs++] = (target_sysid >> 16) & 0xFF;
buf[ofs++] = (target_sysid >> 24) & 0xFF;
}
buf[ofs++] = target_compid;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. It almost looks like the two flags are independent which has the benefit of not always burning the 3 bytes in the header.

Comment on lines +335 to +339
if (target_sysid > 255) {
msg->incompat_flags |= MAVLINK_IFLAG_TARGETTED;
msg->target_sysid = target_sysid;
msg->target_compid = target_compid;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (target_sysid > 255) {
msg->incompat_flags |= MAVLINK_IFLAG_TARGETTED;
msg->target_sysid = target_sysid;
msg->target_compid = target_compid;
}
if (target_sysid > 0) {
msg->incompat_flags |= MAVLINK_IFLAG_TARGETTED;
msg->target_sysid = target_sysid;
msg->target_compid = target_compid;
if (target_sysid > 255) {
msg->incompat_flags |= MAVLINK_IFLAG_SYSID32;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the confusion is in the name MAVLINK_IFLAG_TARGETTED. It should be called MAVLINK_IFLAG_TARGETTED_SYSID32. And then the original implementation is correct and your suggestion is wrong. A target_sysid > 0 is perfectly valid with the existing mavlink implementation and doesn't need a incompat flag.

buf[0] = magic;
buf[1] = len;
if (magic == MAVLINK_STX_MAVLINK1) {
if (sysid > 255 || target_sysid > 255) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Elsewhere, equivalent checks also check if msgid > 255.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it doesn't make sense to send a corrupt message over Mavlink1.

/*
compat_flags bits
*/
#define MAVLINK_CFLAG_SYSID32 0x01 // system is capable of understanding 32 bit system ID

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be better as a new flag in MAV_PROTOCOL_CAPABILITY_MAVLINK2?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAV_PROTOCOL_CAPABILITY flags are a level higher. They document what Mavlink services/features are supported but they don't describe the protocol bytes changed here. And MAV_PROTOCOL_CAPABILITY flags are only received, once mavlink implementations are actually working/talking.

@tridge tridge removed the WIP label Jul 28, 2026

@julianoes julianoes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Should sysid32 be behind a #define, so that we can keep updating implementations that don't support sysid32, or should this be rolled out when updating universally and potentially break builds? Nevermind, seeing all the functions such as _pack(...) I suppose it would add a lot of duplication and be very messy.
  2. Is there any benefit of allowing 32 bit compids as well? I think I like the idea of the symmetry but I'm not sure it would be the right thing to do.
  3. Can we do a clean format PR before this. I see spaces and tabs added in this PR and I just find that a bit yucky.

I think I'm actually somewhat convinced that this could work.

buf[0] = magic;
buf[1] = len;
if (magic == MAVLINK_STX_MAVLINK1) {
if (sysid > 255 || target_sysid > 255) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it doesn't make sense to send a corrupt message over Mavlink1.

buf[5] = msgid & 0xFF;
return MAVLINK_CORE_HEADER_MAVLINK1_LEN+1;
}
uint8_t ofs = 2;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could do uint8_t ofs = 0 at the very top and use it throughout.

buf[ofs++] = (target_sysid >> 16) & 0xFF;
buf[ofs++] = (target_sysid >> 24) & 0xFF;
buf[ofs++] = target_compid;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. It almost looks like the two flags are independent which has the benefit of not always burning the 3 bytes in the header.

/*
compat_flags bits
*/
#define MAVLINK_CFLAG_SYSID32 0x01 // system is capable of understanding 32 bit system ID

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAV_PROTOCOL_CAPABILITY flags are a level higher. They document what Mavlink services/features are supported but they don't describe the protocol bytes changed here. And MAV_PROTOCOL_CAPABILITY flags are only received, once mavlink implementations are actually working/talking.

#define MAVLINK_IFLAG_MASK 0x01 // mask of all understood bits
#define MAVLINK_IFLAG_SIGNED 0x01 // this message has a MAVLink2 signature attached
#define MAVLINK_IFLAG_SYSID32 0x02 // this message uses a 32 bit system ID
#define MAVLINK_IFLAG_TARGETTED 0x04 // this message has target sysid/compid in an extended header

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd make it clear that this is related to sysid 32. Just "targetted" could also mean compid, etc.

Suggested change
#define MAVLINK_IFLAG_TARGETTED 0x04 // this message has target sysid/compid in an extended header
#define MAVLINK_IFLAG_TARGETTED_SYSID32 0x04 // this message has target sysid/compid in an extended header

Comment on lines +335 to +339
if (target_sysid > 255) {
msg->incompat_flags |= MAVLINK_IFLAG_TARGETTED;
msg->target_sysid = target_sysid;
msg->target_compid = target_compid;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the confusion is in the name MAVLINK_IFLAG_TARGETTED. It should be called MAVLINK_IFLAG_TARGETTED_SYSID32. And then the original implementation is correct and your suggestion is wrong. A target_sysid > 0 is perfectly valid with the existing mavlink implementation and doesn't need a incompat flag.

MAVLINK_PARSE_STATE_SIGNATURE_WAIT,
MAVLINK_PARSE_STATE_SIGNATURE_WAIT_BAD_CRC
MAVLINK_PARSE_STATE_SIGNATURE_WAIT_BAD_CRC,
// new states appended to keep existing state numbering stable

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that have an actual benefit? I suppose if implementations check against magic numbers?

@julianoes

julianoes commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

I've spent some tokens on an Opus 5 review. See details below.

@tridge let me know if you want to tackle these or whether you want to defer them to future PRs.

Edit: and I'm not trying to slow you down or block you, just want to make sure we don't end up breaking too much. I'm eager to add this to PX4 and MAVSDK.

Details

Review: pr-sysid-32 vs master

Branch: pr-sysid-32 (10 commits, ~1400 lines across 15 files)
Repo: pymavlink
Scope: MAVLink 2.1 32-bit system IDs (MAVLINK_IFLAG_SYSID32) and extended target headers (MAVLINK_IFLAG_TARGETTED)

Method: read of the full diff plus surrounding code. No builds or tests were run.

Commits under review

d8cd90c3 generator: C: clear target IDs when parsing a MAVLink1 frame
0ede65f6 generator: mavgen_javascript: fix link_id extraction from signed messages
3b3381b8 generator: mavgen_javascript: 32 bit sysid and extended target support
43c6e224 generator: mavgen_lua: handle the 32 bit sysid mavlink_message_t layout
bff89dc8 generator: reject unknown incompat_flags in CS and JavaScript parsers
b6aa784c tests: add 32 bit sysid tests
53d39dd7 generator: mavgen_python: 32 bit sysid and extended target support
32c6d19f generator: C: test 32 bit sysid and extended targets
c88459af generator: mavgen_c: generate 32 bit sysid/target aware APIs
7c34da73 generator: C: add 32 bit system ID and extended target support

Overall

The core wire work is solid and internally consistent:

  • _mav_header_pack() is a good centralisation of what was previously four copies of hand-written header serialisation.
  • The CRC and signing offset arithmetic is correct everywhere I checked — header_len now consistently includes the magic byte, and header_len-1 is passed into crc_calculate.
  • The parser state additions are non-overlapping and appended, so existing state numbering stays stable.
  • The stale-target clearing in both GOT_LENGTH and the MAVLink1 GOT_STX path is the right fix.
  • The C to Python golden-frame cross-check in tests/test_sysid32.py is exactly the right way to pin this down.
  • Lua struct offsets (295/299) check out against a MAVPACKED mavlink_message_t.

Findings follow in rough severity order.


1. C _decode() turns a 32-bit target into a broadcast, and diverges from Python/JS

generator/mavgen_c.py:759

f.decode_call = '_MAV_RETURN_uint8_t(msg, %u)' % f.wire_offset

For a TARGETTED frame, mavlink_msg_command_long_decode() yields pkt.target_system == 0. In MAVLink, 0 means broadcast. So the near-universal receiver idiom

if (packet.target_system == 0 || packet.target_system == mavlink_system.sysid) {
    handle();
}

accepts a message addressed to some other 32-bit system. The testsuite even locks this in:

assert(pkt.target_system == (targetted?0:target));

Python (mavgen_python.py) and JS both do the opposite — they overlay the full 32-bit value onto the decoded field, so msg.target_system is correct there. Same feature, three languages, two different semantics for the same frame.

The constraint is understood: mavlink_X_t.target_system is uint8_t and the #else branch memcpy's the struct straight off the wire, so it cannot be widened. But 0 is the worst possible sentinel. Options worth weighing:

  • Use 0xFF (or another reserved value) so the frame fails an equality check rather than passing a broadcast check.
  • Emit a companion target_system32 field in the struct.
  • At minimum, make this a documented, loud caveat rather than a comment in the generator.

2. MAVLINK_CFLAG_SYSID32 is set unconditionally on every MAVLink2 frame

generator/C/include_v2.0/mavlink_helpers.h:341, generator/mavgen_python.py:363, generator/mavgen_javascript.py:378

Every outgoing MAVLink2 packet from every pymavlink user now has compat_flags == 0x01 instead of 0x00. That changes the bytes and CRC of literally every frame — the JS reference-buffer churn in this diff is a preview of what downstream golden tests, tlog comparisons and interop fixtures will hit.

Two concerns beyond the churn:

  • It is an unconditional capability claim for a bit assignment that, as far as I can tell from the branch, is not finalised upstream. If the bit moves, every shipped build is lying.
  • Combined with finding 1, a plain recompile makes a C application advertise "I understand 32-bit system IDs" while its _decode()-based routing demonstrably does not.

Suggest making this opt-in — a MAVLINK_STATUS_FLAG_OUT_SYSID32 or a compile-time define — rather than always-on.

3. C# MAVLinkMessage.processBuffer() is unguarded

generator/CS/MAVLinkMessage.cs:157

The commit "reject unknown incompat_flags in CS and JavaScript parsers" only covers MavlinkParse.ReadPacket(Stream). processBuffer reads srcSystem from buffer[5], msgid from buffer[7..9], and CRC from MAVLINK_CORE_HEADER_LEN + payloadlength + 1/+2 — all fixed offsets.

It is reachable from the public new MAVLinkMessage(byte[]), which is how packet-oriented transports (UDP datagrams, log replay) feed it. An extended-header frame there yields a garbage msgid and a misplaced CRC read rather than a clean rejection. generator/CS/MAVLinkMessage.cs:76 (ByteArrayToStructure(..., MAVLINK_NUM_HEADER_BYTES, payloadlength)) has the same fixed-offset assumption.

The discard-length arithmetic in MavlinkParse.cs itself is correct — worked through: lengthtoread + extlen - (headerlengthstx - 2) equals payload + extlen + 2 + sig.

4. Lua layout detection breaks if MAVLINK_MAX_PAYLOAD_LEN is reduced

generator/mavgen_lua.py:89

return #message >= 299

The old Lua code only used offsets counted from the start of the struct (at most 12 + payload_length), so it was immune to MAVLINK_MAX_PAYLOAD_LEN. The new code reads target_sysid/target_compid at absolute offsets 295-299, and detects the layout by total size — both of which shift if a build lowers MAVLINK_MAX_PAYLOAD_LEN (a supported #ifndef knob).

On such a build, sysid32_layout() returns false and the script silently decodes with the old offsets: wrong sysid, wrong msgid, no error. Worth at least a comment stating the assumption, or deriving the offset from #message rather than hardcoding 295.

5. Generator coverage gaps

Java (generator/mavgen_java.py:420), WLua (generator/mavgen_wlua.py:676if version == 0xfd and incompatibility_flag == 0x01), Swift, spin2, C++11 and TypeScript are untouched.

Java and the Wireshark dissector will misparse extended-header frames at fixed offsets rather than reject them. Not necessarily blocking, but the Wireshark dissector in particular is the tool people reach for when debugging exactly this feature. Worth an explicit "out of scope, follow-up" note in the PR.


Smaller items

  • Orphaned doxygengenerator/C/include_v2.0/mavlink_helpers.h:290: moving _mav_parse_error() up left the @brief Finalize a MAVLink message with channel assignment / @param system_id / @param length block attached to _mav_parse_error.
  • pack_status non-CRC_EXTRA branchgenerator/mavgen_c.py:277: the #else arm still calls plain mavlink_finalize_message_buffer without ${target_suffix}, so with MAVLINK_CRC_EXTRA=0 a >255 target is silently dropped (payload byte already zeroed, no extended header). That branch was already arity-broken on master, but it is now also silently lossy.
  • Leaked loop variablegenerator/mavgen_c.py:693: sysid32_capable is assigned inside the first for m in xml.message loop and read in the second. Works by scope leakage; should be hoisted above both.
  • Stale commentgenerator/mavgen_javascript.py:750: "the frame is rejected in decode()" is no longer true now that MAVLINK_IFLAG_MASK = 0x07. It is accurate in mavgen_javascript_stable.py, where it was presumably copied from.
  • MAVLink1 JS dialects get MAVLINK_IFLAG_MASK = 0x07, claiming to understand flags the v1 decoder cannot see. Harmless (v1 forces incompat_flags = 0) but misleading.
  • Inconsistent state naminggenerator/C/include_v2.0/mavlink_types.h: GOT_SYSID1..3 mean "1..3 bytes consumed", but GOT_TARGET_SYSID0..3 mean "1..4 bytes consumed". Same concept, off by one apart.
  • Test readabilitytests/test_sysid32.py:95: assert payload[30] == 0 if len(payload) > 30 else True parses as intended but reads as a precedence bug. if len(payload) > 30: assert payload[30] == 0 is clearer.
  • ABI note for the PR description: mavlink_message_t grows 291 to 299 bytes, mavlink_system_t.sysid and __mavlink_signing_stream.sysid widen to uint32_t, MAVLINK_MAX_PACKET_LEN grows by 8, and every _pack/_encode/get_target_system signature changes type. Anything sharing these headers needs a lockstep rebuild — and any downstream buffer sized by a hardcoded constant instead of MAVLINK_MAX_PACKET_LEN will now overflow.

Incidental fix worth calling out

generator/mavgen_python.py:1197 changes m._payload = msgbuf[6:...] to msgbuf[headerlen:...]. On master that was wrong for MAVLink2 (4 header bytes leaked into _payload). Genuine fix, but it is unrelated to sysid32 and buried in a large commit — it deserves its own commit and a changelog line, since anything reading msg._payload on v2 gets different bytes now.

Checked and found correct

  • CRC offsets in mavlink_finalize_message_buffer_target, _mav_finalize_message_chan_send_target, mavlink_msg_to_send_buffer and _mavlink_resend_uart.
  • MAVLINK_START/END_UART_SEND lengths.
  • mavlink_signature_check re-serialising the header, and never hitting the header_len == 0 path since MAVLink1 frames are never signed.
  • MAVLink1 guards refusing to truncate rather than aliasing a sysid.
  • Python mav20_sysid32_unpacker layout (13 bytes, < so unaligned).
  • JS header_len propagation into the length validation and payloadBuf slice.
  • JS _link_id fix (msgbuf[-13] to msgbuf.slice(-13)[0]) — that was a real pre-existing bug.
  • C# discard arithmetic.
  • Lua CRC reconstruction byte order.
  • dialects/*.py are gitignored, so no checked-in generated output goes stale.

tridge added 11 commits August 29, 2026 15:50
Implements the MAVLink2.1 extended headers from mavlink/rfcs#20,
amended to a 32 bit system ID so IPv4 addresses can be used directly
as system IDs:

 - MAVLINK_IFLAG_SYSID32: set only when the sender sysid is over 255,
   the header sysid field widens in place from 1 to 4 bytes
 - MAVLINK_IFLAG_TARGETTED: set only when the target sysid is over
   255, target_sysid[4] and target_compid[1] are appended to the
   header and the payload target byte is zeroed
 - MAVLINK_CFLAG_SYSID32 is always set in compat_flags to advertise
   capability. Old parsers ignore compat_flags, so frames with small
   IDs remain compatible with existing MAVLink2.0 parsers

The wire header now varies in length (10 to 18 bytes), so header
serialisation is centralised in mavlink_header_to_send_buffer() and
used by finalize, mavlink_msg_to_send_buffer, _mavlink_resend_uart and
signature check. The signature sha256 previously hashed the message
struct as if it were the wire header; it now hashes the re-serialised
header, which is also required by the wider struct sysid field.

New finalize/send entry points carry an explicit target so the
convenience send path (which never builds a mavlink_message_t) can
emit the extended header. MAVLink1 output with a 32 bit ID is an
error, never a truncation, as a masked sysid would alias another
system and a zeroed target byte would become a broadcast.

mavlink_msg_get_target_sysid()/mavlink_msg_get_target_compid() give
routers a single call for the effective target of any message. On
receive TARGETTED is honoured for any message, including ones with no
payload target fields, for forward compatibility with full-RFC
senders.
Tag target_system/target_component fields in mavparse (keyed on the
routing semantics, so MANUAL_CONTROL.target is included) and use the
tags in the C generator:

 - pack/send/encode system_id and target_system arguments widen to
   uint32_t. The payload struct stays wire-layout so its target byte
   is written as zero when the target is over 255 and the target is
   passed through to the _target finalize/send entry points
 - per-message get_target_system()/get_target_component() return the
   extended header target when MAVLINK_IFLAG_TARGETTED is set
 - decode() reads the raw payload byte for target fields so the wire
   struct is bit-exact in both the aligned and byte-swap builds;
   code needing the full 32 bit target must use the getters

MAVLink1 generation is unchanged.
Round trips for all four sysid/target width combinations, golden
legacy-compat check (frames with small IDs differ from MAVLink2.0
output only in compat_flags and CRC, and compat_flags 0 frames still
parse), convenience-send and resend_uart byte-exact checks, signing
over the variable-length header including corruption cases, MAVLink1
truncation guards and parser recovery from a truncated extended
header.
Mirrors the C implementation: SYSID32 set only when srcSystem is over
255, TARGETTED only when a message's target_system field is over 255
(the payload byte is then zeroed), MAVLINK_CFLAG_SYSID32 always
advertised, variable header length handled in framing and decode, and
MAVLink1 packing raises MAVError for 32 bit IDs rather than
truncating.

On decode the extended header target is overlaid onto the decoded
message fields, so existing code reading msg.target_system keeps
working for targets over 255. New get_target_system() and
get_target_component() return the effective target of any message.

This also fixes get_payload() for MAVLink2: the payload slice
previously started at offset 6 (the MAVLink1 header length) and so
included 4 header bytes.
Round trips for all sysid/target width combinations, signing over
extended headers including corruption cases, MAVLink1 guards, legacy
compat framing, and golden frames produced by the C implementation to
pin C/Python cross-language wire compatibility.
The CS and JavaScript runtimes read a fixed MAVLink2 header length and
would consume the wrong byte count on MAVLink2.1 extended-header
frames, mis-framing the stream instead of cleanly dropping them. Drop
frames with incompatible flags the parser does not understand, and
consume the extended header bytes so the stream stays in sync. The
Java parser already rejects unknown incompat flags.
The Lua module parses the in-memory mavlink_message_t structure, not
the wire format. The sysid field widened to 32 bits, moving the
compid, msgid and payload offsets, and the CRC check must reconstruct
the wire header (which depends on the SYSID32/TARGETTED flags) rather
than assuming the structure matches the wire bytes. The extended
header target is exposed as target_sysid/target_compid.

Users often copy current scripts onto older firmware, so the layout
is detected at runtime from the structure size: 291 bytes for the
older 1 byte sysid layout, 299 bytes for the 32 bit sysid layout.
decode_header additionally returns the payload offset as a second
return value so callers do not need layout knowledge of their own.
Full MAVLink2.1 support for the NextGen JavaScript generator,
mirroring the C and Python implementations: SYSID32 set only when the
source system is over 255, TARGETTED only when a message's
target_system field is over 255 (the payload byte is then zeroed),
MAVLINK_CFLAG_SYSID32 always advertised, variable header length in
framing and decode, extended header targets overlaid onto decoded
fields, and MAVLink1 packing throws for 32 bit IDs.

The static test fixtures gain the compat_flags advertisement and
make_tests.py expects it for MAVLink2 frames, keeping the byte-level
cross-check against the C implementation exact. Adds round trip tests
for 32 bit source and target IDs.
…ages

Negative array indexing returns undefined in JavaScript, so _link_id
was never set from the signature block. Use slice instead, matching
check_signature().
the MAVLink1 path skips the GOT_LENGTH state, which is where
target_sysid and target_compid are cleared, so a re-used receive buffer
kept the target from a previous MAVLink2 frame. Forwarding that
MAVLink1 frame then silently failed, as _mav_header_pack() treats a
target over 255 as unrepresentable in MAVLink1 and returns 0. This
broke forwarding on a mixed-version link.

The existing test could not catch this as it resets the parser buffer
for every frame; the new test re-uses one parser across both frames.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants