Repository navigation
Conversation
51e2e3f to
00bad7c
Compare
|
Q1 - not an oversight. The design assumes addressing. The MAVLink-M swarm dataflow concept specifies "Addressed TARGET_HANDOVER per target to the chosen shooter's sysid", and lists "addressed handoff" among the things the dialect already provides. It explicitly rules out the alternative: "What is explicitly not supported is a third, unsafe pattern - broadcasting one target to everyone and letting whoever arrives strike it. That produces convergence: multiple munitions wasted on one target, and multiple armed aircraft meeting at one point in space." store_id, effector_id and esad_id all carry "0 = all" semantics. No track_uid on ESAD_ARMING, ESAD_CONFIG, SENSOR_TASKING, TERMINAL_CONTROL, CAS_9LINE. Q2 - agree with the split; ESAD_ARMING especially. Q3 - yes, use the reserved names on MAVLINK_M_ACK. |
|
Thanks @ryanjAA, that answers all three. Q1: that settles it. Addressing was always the design, it just was not in a place a router can act on. Good point on store_id, effector_id and esad_id as well. Those pick the device inside an aircraft, these new fields pick the aircraft, and 0 still means all at both levels. Q2: good, the split stays as posted and TARGET_CUE stays broadcast. Q3: I will add it to this PR. Concretely, origin_sysid becomes target_system, since it already names the system the ack has to get back to, target_component goes next to it, and ack_sysid goes away because the frame header already says who is sending. That leaves MAVLINK_M_ACK looking like COMMAND_ACK in common. Tell me if ack_sysid is there for a reason I am missing, otherwise I will push that here. On track_uid, agreed, and I think it belongs in its own PR because it answers a different question: these fields say who has to act, track_uid says what the message is about. Your ESAD_ARMING against ESAD_STATE example is the clearest one. It hits SENSOR_TASKING too, where you cannot tell a sensor which track to watch, so a ground station ends up sending coordinates and the task cannot follow a moving target. Happy to write that one up if nobody else is on it. So: the ack change lands here, this PR covers who has to act, and what the message is about comes separately. If that works for you, an approval once the ack commit is up and this can stop being an RFC. |
|
MAVLINK_M_ACK carries target_system and target_component, and ack_sysid is gone because the frame header already says who is sending. It generates as {53004, 224, 69, 69, 3, 16, 17}, so a router can place it, and the payload stays at 69 bytes since two fields go in and two come out. Every other message in the dialect generates a byte-identical entry row. I left origin_sysid alone on TRACK_IDENTITY, TARGET_CUE, PARTICIPANT_POSITION and ENGAGEMENT_DIRECTIVE. Those say who a report is about, not where it goes, so they are not the same field wearing a different name. |
|
I reviewed c55620e. The generated ACK entry is: CRC_EXTRA moves from 47 to 224, the payload remains 69 bytes, and target_system/target_component land at offsets 16 and 17. No wire-format objection. Worth stating explicitly that the message ID and payload length are both unchanged. Offset 16 retains the same effective value, while offset 17 changes meaning from ack_sysid to target_component.CRC_EXTRA is therefore the only on-wire compatibility guard between the old and new definitions. That is correct and fail-closed, but it is not the usual case where a changed payload length makes the incompatibility obvious, and a reader will assume it is. There is also a stronger argument for keeping the target fields in the base definition than the one in the PR body. Extension fields are excluded from CRC_EXTRA. An old sender omits them, a new receiver decodes the omission as zero, and zero targets mean broadcast. Extension-placed addressing would therefore let mixed definitions stay CRC-compatible while silently degrading to the exact behavior this PR exists to prevent. Base fields force a CRC mismatch instead. That is the case to make. Four immediate description clarifications beloww. As currently framed, none changes the wire format. The SENSOR_TASKING question, plus the separate ENGAGEMENT_DIRECTIVE.origin_sysid question below, could reveal that a field change is warranted, which is why I would rather settle both before merge.
Please also define the direction discriminator, presumably status = UNSPECIFIED for a task request and a nonzero status for a response, and state which task fields are meaningful or ignored in a response. Otherwise a single frame can carry both a command and a status with no normative rule for interpreting it.
For target_system = 0, each receiving system evaluates that rule against its own local ESADs. That is the only enforceable reading, and it exposes an important consequence for the broadcast-policy follow-up: a broadcastARM can partially apply. One system may accept and arm while another rejects because its local selected ESAD set does not share the supplied hash. No individual target system can determine the network-wide outcome; the originator would need to aggregate per-system ACKs and/or subsequentESAD_STATE reports. The follow-up should define that response obligation explicitly. ESAD_CONFIG has the corresponding unresolved multi-device rule and should state how its one challenge hash is validated when esad_id = 0,store_id = 0, or both. On ENGAGEMENT_DIRECTIVE.origin_sysid: the explanation for retaining origin_sysid fits TRACK_IDENTITY, TARGET_CUE and PARTICIPANT_POSITION, where the field records provenance or the subject of a report.ENGAGEMENT_DIRECTIVE instead describes origin_sysid as the system issuing the directive, which may duplicate the frame source. I am not asking to remove it and I am not asking for ack_sysid back. Please just say which meaning is intended. If it is the logical or original issuing authority and may differ from the immediate MAVLink sender after gateway translation, that is useful and the field should say so. If it intentionally duplicates the immediate frame source so that detached audit records stay self-contained, document that rationale. Otherwise it is accidental redundancy and worth removing while the definition is still mutable. On broadcast validity generally: the hazard predates this PR, since these messages previously had no system-level destination at all. What this PR adds is the first explicit field against which the intended policy can be stated and enforced, so it is the right moment to open the question. Please open and link a follow-up issue defining broadcast validity per operation and direction rather than per message ID. Several of these combine fail-safe actions such as DISARM, ABORT, CHECK_FIRE and WAVE_OFF, where network-wide delivery may be intentional, with affirmative actions such as ARM, RESUME, RETARGET and CLEARED_HOT, where it is not. To make the intended policy explicit: the convergence rationale I quoted earlier applies to affirmative multi-recipient directives as well. Broadcast ARM is not intended to be valid. The follow-up should codify broadcast ARM as invalid while separately and explicitly defining which fail-safe broadcasts, such as DISARM, ABORT, CHECK_FIRE or WAVE_OFF, are permitted. On track_uid, agreed it belongs in a separate PR. ESAD_ARMING is the sharpest case because it has no track or engagement binding, whileESAD_STATE reports the track against which the store is armed. ThenSENSOR_TASKING, which cannot reference a persistent track: task_id identifies the transaction, not the subject, so following a moving target requires continually re-tasking with fresh coordinates or an out-of-band association. Then CAS_9LINE, which carries the 9-line geometry but no sequence and no persistent cross-message identity. ESAD_CONFIG andTERMINAL_CONTROL are weaker, and TERMINAL_CONTROL.sequence already gives an engagement binding. For #11: check_wire_compat.py currently builds its map as{m.id: (m.name, m.crc_extra)} and compares only those tuples. Please extend it to report full generated metadata for changed messages, that isCRC_EXTRA, min_length, max_length, target flags, target_system offset and target_component offset. The #11 description also still says comparison against #10 reports eleven changed messages; after c55620e it is twelve. No wire-format objection. Once the four descriptions and the ENGAGEMENT_DIRECTIVE.origin_sysid semantics are resolved, and the broadcast-policy issue is linked, I'd say it's ready to approve. |
README.md, IDMAPPING.md and the military.xml header document an ID allocation, and peers rely on a wire contract: a message ID keeps its meaning once assigned, and a peer drops any message whose CRC_EXTRA differs from its own. Nothing checks either, so a pull request can allocate outside 53000-53999, take an ID from the private 53900-53999 block, or change a message body without the wire effect showing in review. check_dialect_policy.py checks the documented allocation over both dialect files: military.xml takes messages and MAV_CMD entries from the shared 53000-53899 window, military_extensions.xml only from the private block, names follow MAVLink's case conventions, and the template reuses no shared ID or name. It needs only the Python standard library, so it runs on a plain checkout. Schema conformance, field types and clashes with common.xml stay with the schema check and mavgen, which read the whole include tree. check_wire_compat.py parses two versions of military.xml with pymavlink's generator, which computes CRC_EXTRA the way every consumer does, and lists removals, renames and CRC_EXTRA changes by message ID. By default it only reports, because the dialect is still changing and replacing a message body is a decision for review; WIRE_COMPAT_ENFORCE=1 makes it fail once IDs are frozen. CRC_EXTRA leaves out enum values and extension fields, so the report does not see changes to them. Validation: the policy check passes on main (26 messages and 4 MAV_CMD entries in the shared window, 2 template messages in the private block), and a tree with 11 planted violations, one per rule, reports all 11 and exits 1. The wire check reports 0 changes against itself, and against #10 at c55620e it lists the 12 messages that PR changes, FIRES at CRC_EXTRA 124; an independent diff of the field lists finds the same 12.
The wire report compares CRC_EXTRA, which covers the name, type and order of the base fields and nothing else. A changed enum value, a removed enum entry, or an extension field that is removed, retyped or moved changes what peers decode but leaves CRC_EXTRA alone, so the report shows no change for any of them. The wire-compat job runs mavlink/scripts/check_api_break.py from the mavlink checkout at the pinned MAVLINK_REF, the check upstream runs on its own definitions. Running it from the checkout, not a copy kept here, means a pin bump updates the checker with the definitions it is matched to; the composite action adds scripts/ to that sparse checkout. The checker takes the base from the pull request event and diffs every XML file the pull request changes, and its output goes to the log and the step summary. Like the CRC report it only reports, and one job-level WIRE_COMPAT_ENFORCE switch covers both checks. An exit without the checker's verdict line is the tool failing, not an API break, so the step fails on it whatever the switch says, and a check that never ran cannot pass. Validation, simulating the job's pull request environment with mavlink 87da370 and main 1a7a00d as the base: a pull request that changes no XML reports "No XML files changed." and passes. The military.xml of #10 at c55620e reports 59 changed and 2 removed fields in 12 messages, the same 12 the CRC report lists, as a warning, and fails with WIRE_COMPAT_ENFORCE=1. Giving one MAVLINK_M_TARGET_CLASS entry the value 40 in place of 4 raises the warning here while the CRC report shows 0 wire-breaking changes. A base commit missing from the history makes the checker raise, and the step fails with "exited 1 without a verdict". actionlint 1.7.12, with shellcheck, and zizmor 1.30.1, regular and auditor personas, report nothing.
IDMAPPING.md is the allocation record. README.md links it as the message-ID allocation, the site publishes it as the ID Allocation page, and the contributing guide asks for it to be updated in the same pull request as a new ID. Nothing compares it with military.xml, so a pull request can add, move or drop a message and leave the table and the published page wrong. On main the two agree: 26 messages, 4 MAV_CMD entries, and no ID in use inside a reserved block. The policy check reads the first table under each "## " heading of IDMAPPING.md. Every military.xml message must be in "Shared messages" and every MAV_CMD entry in "MAV_CMD entries", at the ID military.xml gives it (the New ID and New value columns), and every row must name something military.xml defines. No message or command may use an ID inside a "Reserved blocks" range that lies in the shared window 53000-53899; the private block belongs to the allocation rule. A name is the cell's leading UPPER_SNAKE word, so "TARGET (formerly TARGET_COORD)" is TARGET, and backticks are accepted. The history table and the old ID columns are not checked. A missing file, table or column is a finding in itself, so a restructured document fails the check rather than passing it unread. A finding about the XML points at the element, one about the document at its row. Validation: main's files pass. On copies of them, a message added at 53062 with no document change gives 2 findings (not in the table, inside 53062-53089); FIRES listed at 53025 gives 1, at its row; a stale row and a dropped MAV_CMD row give 1 each; a renamed "Shared messages" heading gives 1; the military.xml of #10 at c55620e passes. 21 policy tests pass on Python 3.10.12 and 3.14.5 with 100% line and branch coverage (200 statements, 98 branches), and turning off the reserved-block test, the ID comparison or the one table per section rule each fails the suite. actionlint 1.7.12, zizmor 1.30.1 and ruff 0.14.0 report nothing.
The wire report compares only each message's name and CRC_EXTRA, but a
router places a message from the row mavgen writes for it in the C
headers' MAVLINK_MESSAGE_CRCS table: {msgid, CRC_EXTRA, min_length,
max_length, flags, target_system offset, target_component offset}. A
reviewer of an addressing change needs the lengths, the target flags
and the offsets, and the report hides all of them. It also hides any
change that leaves CRC_EXTRA alone: extension fields, including a
target field placed among them, which moves the routing row while
older peers keep accepting the message.
wire_map reads the row straight off pymavlink's parse, the same
attributes mavgen_c.py prints into the table. Each wire-breaking line
(removed, renamed, or CRC_EXTRA changed) carries the old and the new
row. A message whose row changes with CRC_EXTRA unchanged is listed as
an extension-only INFO change with both rows, and its step summary
note says that a sender without the new extension fields sends zeros
there, which a router reads as broadcast. Additions carry their row.
WIRE_COMPAT_ENFORCE still fails on wire-breaking changes only.
Validation: against the military.xml of #10 at c55620e the report
lists 12 wire-breaking messages with their rows, among them
MAVLINK_M_ACK {53004, 47, 69, 69, 0, 0, 0} -> {53004, 224, 69, 69, 3,
16, 17}, FIRES {53020, 16, 62, 62, 0, 0, 0} -> {53020, 124, 64, 64, 3,
40, 41} and SENSOR_TASKING {53050, 244, 24, 24, 0, 0, 0} -> {53050,
107, 26, 26, 3, 22, 23}. For both files, all 26 rows match the
MAVLINK_MESSAGE_CRCS table mavgen generates. 13 wire report tests
pass, 42 script tests in all, on Python 3.10.12 and 3.14.5, with 100%
line and branch coverage of check_wire_compat.py (64 statements, 24
branches); breaking on any row change, dropping the flags, or
ignoring CRC-compatible changes each fail the suite. ruff 0.14.0,
actionlint 1.7.12 and zizmor 1.30.1 report nothing.
The wire report compares CRC_EXTRA, which covers the name, type and order of the base fields and nothing else. A changed enum value, a removed enum entry, or an extension field that is removed, retyped or moved changes what peers decode but leaves CRC_EXTRA alone, so the report shows no change for any of them. The wire-compat job runs mavlink/scripts/check_api_break.py from the mavlink checkout at the pinned MAVLINK_REF, the check upstream runs on its own definitions. Running it from the checkout, not a copy kept here, means a pin bump updates the checker with the definitions it is matched to; the composite action adds scripts/ to that sparse checkout. The checker takes the base from the pull request event and diffs every XML file the pull request changes, and its output goes to the log and the step summary. Like the CRC report it only reports, and one job-level WIRE_COMPAT_ENFORCE switch covers both checks. An exit without the checker's verdict line is the tool failing, not an API break, so the step fails on it whatever the switch says, and a check that never ran cannot pass. Validation, simulating the job's pull request environment with mavlink 87da370 and main 1a7a00d as the base: a pull request that changes no XML reports "No XML files changed." and passes. The military.xml of #10 at c55620e reports 59 changed and 2 removed fields in 12 messages, the same 12 the CRC report lists, as a warning, and fails with WIRE_COMPAT_ENFORCE=1. Giving one MAVLINK_M_TARGET_CLASS entry the value 40 in place of 4 raises the warning here while the CRC report shows 0 wire-breaking changes. A base commit missing from the history makes the checker raise, and the step fails with "exited 1 without a verdict". actionlint 1.7.12, with shellcheck, and zizmor 1.30.1, regular and auditor personas, report nothing.
IDMAPPING.md is the allocation record. README.md links it as the message-ID allocation, the site publishes it as the ID Allocation page, and the contributing guide asks for it to be updated in the same pull request as a new ID. Nothing compares it with military.xml, so a pull request can add, move or drop a message and leave the table and the published page wrong. On main the two agree: 26 messages, 4 MAV_CMD entries, and no ID in use inside a reserved block. The policy check reads the first table under each "## " heading of IDMAPPING.md. Every military.xml message must be in "Shared messages" and every MAV_CMD entry in "MAV_CMD entries", at the ID military.xml gives it (the New ID and New value columns), and every row must name something military.xml defines. No message or command may use an ID inside a "Reserved blocks" range that lies in the shared window 53000-53899; the private block belongs to the allocation rule. A name is the cell's leading UPPER_SNAKE word, so "TARGET (formerly TARGET_COORD)" is TARGET, and backticks are accepted. The history table and the old ID columns are not checked. A missing file, table or column is a finding in itself, so a restructured document fails the check rather than passing it unread. A finding about the XML points at the element, one about the document at its row. Validation: main's files pass. On copies of them, a message added at 53062 with no document change gives 2 findings (not in the table, inside 53062-53089); FIRES listed at 53025 gives 1, at its row; a stale row and a dropped MAV_CMD row give 1 each; a renamed "Shared messages" heading gives 1; the military.xml of #10 at c55620e passes. 21 policy tests pass on Python 3.10.12 and 3.14.5 with 100% line and branch coverage (200 statements, 98 branches), and turning off the reserved-block test, the ID comparison or the one table per section rule each fails the suite. actionlint 1.7.12, zizmor 1.30.1 and ruff 0.14.0 report nothing.
The wire report compares only each message's name and CRC_EXTRA, but a
router places a message from the row mavgen writes for it in the C
headers' MAVLINK_MESSAGE_CRCS table: {msgid, CRC_EXTRA, min_length,
max_length, flags, target_system offset, target_component offset}. A
reviewer of an addressing change needs the lengths, the target flags
and the offsets, and the report hides all of them. It also hides any
change that leaves CRC_EXTRA alone: extension fields, including a
target field placed among them, which moves the routing row while
older peers keep accepting the message.
wire_map reads the row straight off pymavlink's parse, the same
attributes mavgen_c.py prints into the table. Each wire-breaking line
(removed, renamed, or CRC_EXTRA changed) carries the old and the new
row. A message whose row changes with CRC_EXTRA unchanged is listed as
an extension-only INFO change with both rows, and its step summary
note says that a sender without the new extension fields sends zeros
there, which a router reads as broadcast. Additions carry their row.
WIRE_COMPAT_ENFORCE still fails on wire-breaking changes only.
Validation: against the military.xml of #10 at c55620e the report
lists 12 wire-breaking messages with their rows, among them
MAVLINK_M_ACK {53004, 47, 69, 69, 0, 0, 0} -> {53004, 224, 69, 69, 3,
16, 17}, FIRES {53020, 16, 62, 62, 0, 0, 0} -> {53020, 124, 64, 64, 3,
40, 41} and SENSOR_TASKING {53050, 244, 24, 24, 0, 0, 0} -> {53050,
107, 26, 26, 3, 22, 23}. For both files, all 26 rows match the
MAVLINK_MESSAGE_CRCS table mavgen generates. 13 wire report tests
pass, 42 script tests in all, on Python 3.10.12 and 3.14.5, with 100%
line and branch coverage of check_wire_compat.py (64 statements, 24
branches); breaking on any row change, dropping the flags, or
ignoring CRC-compatible changes each fail the suite. ruff 0.14.0,
actionlint 1.7.12 and zizmor 1.30.1 report nothing.
c55620e to
9d332b4
Compare
|
@ryanjAA thanks, it's all in 9d332b4. I checked each point against the devguide and against what mavgen generates before taking it, and they hold.
Broadcast validity is in this PR rather than a follow-up issue, per action and direction. 0 is valid only for actions that stop or hold: ABORT, CHECK_FIRE, CEASE_FIRE, DISARM, WAVE_OFF, a STANDBY sensor request, withheld or withdrawn consent, and the CHECK_FIRING, CEASE_LOADING and END_OF_MISSION calls. Anything that starts, arms, configures, grants, briefs or hands over names its recipient, and so does SELF_DESTRUCT because it can't be undone. A DISARM sent to every system still checks the hash per system, so it disarms only where the selected ESADs share it. I folded Your base-field argument is in the first commit and the description, together with its other side: old and new definitions drop each other's ABORTs too, so every system has to move together. For #11: the wire report prints each changed message's generated entry row, old and new, so against this PR it lists the twelve messages with rows like MAVLINK_M_ACK |
README.md, IDMAPPING.md and the military.xml header document an ID allocation, and peers rely on a wire contract: a message ID keeps its meaning once assigned, and a peer drops any message whose CRC_EXTRA differs from its own. Nothing checks either, so a pull request can allocate outside 53000-53999, take an ID from the private 53900-53999 block, or change a message body without the wire effect showing in review. check_dialect_policy.py checks the documented allocation over both dialect files: military.xml takes messages and MAV_CMD entries from the shared 53000-53899 window, military_extensions.xml only from the private block, names follow MAVLink's case conventions, and the template reuses no shared ID or name. It needs only the Python standard library, so it runs on a plain checkout. Schema conformance, field types and clashes with common.xml stay with the schema check and mavgen, which read the whole include tree. check_wire_compat.py parses two versions of military.xml with pymavlink's generator, which computes CRC_EXTRA the way every consumer does, and lists removals, renames and CRC_EXTRA changes by message ID. By default it only reports, because the dialect is still changing and replacing a message body is a decision for review; WIRE_COMPAT_ENFORCE=1 makes it fail once IDs are frozen. CRC_EXTRA leaves out enum values and extension fields, so the report does not see changes to them. Validation: the policy check passes on main (26 messages and 4 MAV_CMD entries in the shared window, 2 template messages in the private block), and a tree with 11 planted violations, one per rule, reports all 11 and exits 1. The wire check reports 0 changes against itself, and against #10 at c55620e it lists the 12 messages that PR changes, FIRES at CRC_EXTRA 124; an independent diff of the field lists finds the same 12. Assisted-by: Claude-Code:claude-opus-5-5 Signed-off-by: TSC21 <nmarques@riis.com>
The wire report compares CRC_EXTRA, which covers the name, type and order of the base fields and nothing else. A changed enum value, a removed enum entry, or an extension field that is removed, retyped or moved changes what peers decode but leaves CRC_EXTRA alone, so the report shows no change for any of them. The wire-compat job runs mavlink/scripts/check_api_break.py from the mavlink checkout at the pinned MAVLINK_REF, the check upstream runs on its own definitions. Running it from the checkout, not a copy kept here, means a pin bump updates the checker with the definitions it is matched to; the composite action adds scripts/ to that sparse checkout. The checker takes the base from the pull request event and diffs every XML file the pull request changes, and its output goes to the log and the step summary. Like the CRC report it only reports, and one job-level WIRE_COMPAT_ENFORCE switch covers both checks. An exit without the checker's verdict line is the tool failing, not an API break, so the step fails on it whatever the switch says, and a check that never ran cannot pass. Validation, simulating the job's pull request environment with mavlink 87da370 and main 1a7a00d as the base: a pull request that changes no XML reports "No XML files changed." and passes. The military.xml of #10 at c55620e reports 59 changed and 2 removed fields in 12 messages, the same 12 the CRC report lists, as a warning, and fails with WIRE_COMPAT_ENFORCE=1. Giving one MAVLINK_M_TARGET_CLASS entry the value 40 in place of 4 raises the warning here while the CRC report shows 0 wire-breaking changes. A base commit missing from the history makes the checker raise, and the step fails with "exited 1 without a verdict". actionlint 1.7.12, with shellcheck, and zizmor 1.30.1, regular and auditor personas, report nothing. Assisted-by: Claude-Code:claude-opus-5-5 Signed-off-by: TSC21 <nmarques@riis.com>
IDMAPPING.md is the allocation record. README.md links it as the message-ID allocation, the site publishes it as the ID Allocation page, and the contributing guide asks for it to be updated in the same pull request as a new ID. Nothing compares it with military.xml, so a pull request can add, move or drop a message and leave the table and the published page wrong. On main the two agree: 26 messages, 4 MAV_CMD entries, and no ID in use inside a reserved block. The policy check reads the first table under each "## " heading of IDMAPPING.md. Every military.xml message must be in "Shared messages" and every MAV_CMD entry in "MAV_CMD entries", at the ID military.xml gives it (the New ID and New value columns), and every row must name something military.xml defines. No message or command may use an ID inside a "Reserved blocks" range that lies in the shared window 53000-53899; the private block belongs to the allocation rule. A name is the cell's leading UPPER_SNAKE word, so "TARGET (formerly TARGET_COORD)" is TARGET, and backticks are accepted. The history table and the old ID columns are not checked. A missing file, table or column is a finding in itself, so a restructured document fails the check rather than passing it unread. A finding about the XML points at the element, one about the document at its row. Validation: main's files pass. On copies of them, a message added at 53062 with no document change gives 2 findings (not in the table, inside 53062-53089); FIRES listed at 53025 gives 1, at its row; a stale row and a dropped MAV_CMD row give 1 each; a renamed "Shared messages" heading gives 1; the military.xml of #10 at c55620e passes. 21 policy tests pass on Python 3.10.12 and 3.14.5 with 100% line and branch coverage (200 statements, 98 branches), and turning off the reserved-block test, the ID comparison or the one table per section rule each fails the suite. actionlint 1.7.12, zizmor 1.30.1 and ruff 0.14.0 report nothing. Assisted-by: Claude-Code:claude-opus-5-5 Signed-off-by: TSC21 <nmarques@riis.com>
The wire report compares only each message's name and CRC_EXTRA, but a
router places a message from the row mavgen writes for it in the C
headers' MAVLINK_MESSAGE_CRCS table: {msgid, CRC_EXTRA, min_length,
max_length, flags, target_system offset, target_component offset}. A
reviewer of an addressing change needs the lengths, the target flags
and the offsets, and the report hides all of them. It also hides any
change that leaves CRC_EXTRA alone: extension fields, including a
target field placed among them, which moves the routing row while
older peers keep accepting the message.
wire_map reads the row straight off pymavlink's parse, the same
attributes mavgen_c.py prints into the table. Each wire-breaking line
(removed, renamed, or CRC_EXTRA changed) carries the old and the new
row. A message whose row changes with CRC_EXTRA unchanged is listed as
an extension-only INFO change with both rows, and its step summary
note says that a sender without the new extension fields sends zeros
there, which a router reads as broadcast. Additions carry their row.
WIRE_COMPAT_ENFORCE still fails on wire-breaking changes only.
Validation: against the military.xml of #10 at c55620e the report
lists 12 wire-breaking messages with their rows, among them
MAVLINK_M_ACK {53004, 47, 69, 69, 0, 0, 0} -> {53004, 224, 69, 69, 3,
16, 17}, FIRES {53020, 16, 62, 62, 0, 0, 0} -> {53020, 124, 64, 64, 3,
40, 41} and SENSOR_TASKING {53050, 244, 24, 24, 0, 0, 0} -> {53050,
107, 26, 26, 3, 22, 23}. For both files, all 26 rows match the
MAVLINK_MESSAGE_CRCS table mavgen generates. 13 wire report tests
pass, 42 script tests in all, on Python 3.10.12 and 3.14.5, with 100%
line and branch coverage of check_wire_compat.py (64 statements, 24
branches); breaking on any row change, dropping the flags, or
ignoring CRC-compatible changes each fail the suite. ruff 0.14.0,
actionlint 1.7.12 and zizmor 1.30.1 report nothing.
Assisted-by: Claude-Code:claude-opus-5-5
Signed-off-by: TSC21 <nmarques@riis.com>
9d332b4 to
6987c44
Compare
README.md, IDMAPPING.md and the military.xml header document an ID allocation, and peers rely on a wire contract: a message ID keeps its meaning once assigned, and a peer drops any message whose CRC_EXTRA differs from its own. Nothing checks either, so a pull request can allocate outside 53000-53999, take an ID from the private 53900-53999 block, or change a message body without the wire effect showing in review. check_dialect_policy.py checks the documented allocation over both dialect files: military.xml takes messages and MAV_CMD entries from the shared 53000-53899 window, military_extensions.xml only from the private block, names follow MAVLink's case conventions, and the template reuses no shared ID or name. It needs only the Python standard library, so it runs on a plain checkout. Schema conformance, field types and clashes with common.xml stay with the schema check and mavgen, which read the whole include tree. check_wire_compat.py parses two versions of military.xml with pymavlink's generator, which computes CRC_EXTRA the way every consumer does, and lists removals, renames and CRC_EXTRA changes by message ID. By default it only reports, because the dialect is still changing and replacing a message body is a decision for review; WIRE_COMPAT_ENFORCE=1 makes it fail once IDs are frozen. CRC_EXTRA leaves out enum values and extension fields, so the report does not see changes to them. Validation: the policy check passes on main (26 messages and 4 MAV_CMD entries in the shared window, 2 template messages in the private block), and a tree with 11 planted violations, one per rule, reports all 11 and exits 1. The wire check reports 0 changes against itself, and against #10 at c55620e it lists the 12 messages that PR changes, FIRES at CRC_EXTRA 124; an independent diff of the field lists finds the same 12. Assisted-by: Claude-Code:claude-opus-5-5 Signed-off-by: TSC21 <nmarques@riis.com>
The wire report compares CRC_EXTRA, which covers the name, type and order of the base fields and nothing else. A changed enum value, a removed enum entry, or an extension field that is removed, retyped or moved changes what peers decode but leaves CRC_EXTRA alone, so the report shows no change for any of them. The wire-compat job runs mavlink/scripts/check_api_break.py from the mavlink checkout at the pinned MAVLINK_REF, the check upstream runs on its own definitions. Running it from the checkout, not a copy kept here, means a pin bump updates the checker with the definitions it is matched to; the composite action adds scripts/ to that sparse checkout. The checker takes the base from the pull request event and diffs every XML file the pull request changes, and its output goes to the log and the step summary. Like the CRC report it only reports, and one job-level WIRE_COMPAT_ENFORCE switch covers both checks. An exit without the checker's verdict line is the tool failing, not an API break, so the step fails on it whatever the switch says, and a check that never ran cannot pass. Validation, simulating the job's pull request environment with mavlink 87da370 and main 1a7a00d as the base: a pull request that changes no XML reports "No XML files changed." and passes. The military.xml of #10 at c55620e reports 59 changed and 2 removed fields in 12 messages, the same 12 the CRC report lists, as a warning, and fails with WIRE_COMPAT_ENFORCE=1. Giving one MAVLINK_M_TARGET_CLASS entry the value 40 in place of 4 raises the warning here while the CRC report shows 0 wire-breaking changes. A base commit missing from the history makes the checker raise, and the step fails with "exited 1 without a verdict". actionlint 1.7.12, with shellcheck, and zizmor 1.30.1, regular and auditor personas, report nothing. Assisted-by: Claude-Code:claude-opus-5-5 Signed-off-by: TSC21 <nmarques@riis.com>
IDMAPPING.md is the allocation record. README.md links it as the message-ID allocation, the site publishes it as the ID Allocation page, and the contributing guide asks for it to be updated in the same pull request as a new ID. Nothing compares it with military.xml, so a pull request can add, move or drop a message and leave the table and the published page wrong. On main the two agree: 26 messages, 4 MAV_CMD entries, and no ID in use inside a reserved block. The policy check reads the first table under each "## " heading of IDMAPPING.md. Every military.xml message must be in "Shared messages" and every MAV_CMD entry in "MAV_CMD entries", at the ID military.xml gives it (the New ID and New value columns), and every row must name something military.xml defines. No message or command may use an ID inside a "Reserved blocks" range that lies in the shared window 53000-53899; the private block belongs to the allocation rule. A name is the cell's leading UPPER_SNAKE word, so "TARGET (formerly TARGET_COORD)" is TARGET, and backticks are accepted. The history table and the old ID columns are not checked. A missing file, table or column is a finding in itself, so a restructured document fails the check rather than passing it unread. A finding about the XML points at the element, one about the document at its row. Validation: main's files pass. On copies of them, a message added at 53062 with no document change gives 2 findings (not in the table, inside 53062-53089); FIRES listed at 53025 gives 1, at its row; a stale row and a dropped MAV_CMD row give 1 each; a renamed "Shared messages" heading gives 1; the military.xml of #10 at c55620e passes. 21 policy tests pass on Python 3.10.12 and 3.14.5 with 100% line and branch coverage (200 statements, 98 branches), and turning off the reserved-block test, the ID comparison or the one table per section rule each fails the suite. actionlint 1.7.12, zizmor 1.30.1 and ruff 0.14.0 report nothing. Assisted-by: Claude-Code:claude-opus-5-5 Signed-off-by: TSC21 <nmarques@riis.com>
The wire report compares only each message's name and CRC_EXTRA, but a
router places a message from the row mavgen writes for it in the C
headers' MAVLINK_MESSAGE_CRCS table: {msgid, CRC_EXTRA, min_length,
max_length, flags, target_system offset, target_component offset}. A
reviewer of an addressing change needs the lengths, the target flags
and the offsets, and the report hides all of them. It also hides any
change that leaves CRC_EXTRA alone: extension fields, including a
target field placed among them, which moves the routing row while
older peers keep accepting the message.
wire_map reads the row straight off pymavlink's parse, the same
attributes mavgen_c.py prints into the table. Each wire-breaking line
(removed, renamed, or CRC_EXTRA changed) carries the old and the new
row. A message whose row changes with CRC_EXTRA unchanged is listed as
an extension-only INFO change with both rows, and its step summary
note says that a sender without the new extension fields sends zeros
there, which a router reads as broadcast. Additions carry their row.
WIRE_COMPAT_ENFORCE still fails on wire-breaking changes only.
Validation: against the military.xml of #10 at c55620e the report
lists 12 wire-breaking messages with their rows, among them
MAVLINK_M_ACK {53004, 47, 69, 69, 0, 0, 0} -> {53004, 224, 69, 69, 3,
16, 17}, FIRES {53020, 16, 62, 62, 0, 0, 0} -> {53020, 124, 64, 64, 3,
40, 41} and SENSOR_TASKING {53050, 244, 24, 24, 0, 0, 0} -> {53050,
107, 26, 26, 3, 22, 23}. For both files, all 26 rows match the
MAVLINK_MESSAGE_CRCS table mavgen generates. 13 wire report tests
pass, 42 script tests in all, on Python 3.10.12 and 3.14.5, with 100%
line and branch coverage of check_wire_compat.py (64 statements, 24
branches); breaking on any row change, dropping the flags, or
ignoring CRC-compatible changes each fail the suite. ruff 0.14.0,
actionlint 1.7.12 and zizmor 1.30.1 report nothing.
Assisted-by: Claude-Code:claude-opus-5-5
Signed-off-by: TSC21 <nmarques@riis.com>
Every message in the dialect is broadcast: none carries
target_system or target_component, so a message that orders one
system to do something cannot say which system it is for. A station
tasking a sensor on one aircraft, or ordering an effector to engage,
puts that message where every system reading the dialect sees it.
With two aircraft on a shared bearer, or one aircraft carrying two
payloads that both speak the dialect, nothing in the message
separates them. MAVLink's own rule is that a point-to-point message
carries target_system, and target_component when components matter.
The eleven messages that command a system, or that hand something
to a named one, carry the two fields: FIRES, SPLASH_CORRECTION,
TARGET_HANDOVER, ESAD_ARMING, ESAD_CONFIG, SENSOR_TASKING,
CAS_9LINE, TERMINAL_CONTROL, ENGAGEMENT_DIRECTIVE, CALL_FOR_FIRE and
LOITER_MUNITION_CONTROL. The messages that describe the world keep
broadcasting, because a contact is a contact and anything listening
should draw it. The fields select the intended recipient for routing
and processing. They do not authorize a request or oblige anyone to
carry it out, and target_component counts only within a nonzero
target_system, as MAVLink routing reads it.
The names are the ones MAVLink reserves. mavgen fills the target
offsets in the message-entry table only for fields called
target_system and target_component, and that table is what a router
reads to place a message: FIRES becomes {53020, 124, 64, 64, 3, 40,
41}, where 3 is the have-target flag and 40 and 41 are the byte
offsets. A field named anything else leaves that row at zero and the
message keeps flooding every endpoint.
The fields are base fields, not extensions. CRC_EXTRA leaves out
extension fields, so a sender built from the old definition would
omit them, the receiver would read zero, and zero means every
system: addressing would fall back, unnoticed, to the broadcast it
exists to replace. A base field changes CRC_EXTRA instead, so old
and new definitions drop each other's messages, an ABORT included,
and every system has to move to the new definition together.
Validation: the dialect validates against mavschema.xsd and
generates C and Python with strict units at mavlink 87da370 and
pymavlink 19880422, and the C headers compile at -Wall -Wextra
-Werror. The eleven messages change CRC_EXTRA, grow by two bytes and
carry the have-target flag; the other fifteen keep their rows.
Assisted-by: Claude-Code:claude-opus-5-5
Signed-off-by: TSC21 <nmarques@riis.com>
MAVLINK_M_ACK closes the loop on a message one system sent to
another, so it has exactly one correct destination: the sender of
the acknowledged message. A field of its own naming that sender
stays invisible to a router, which places messages from the
message-entry table mavgen generates and finds a zeroed target row
there, so an acknowledgment reaches every endpoint on the bearer.
The reserved names carry the destination, the way COMMAND_ACK
carries it for commands. target_system and target_component are the
source system and component IDs in the frame header of the
acknowledged message, never that message's own target fields, which
are 0 when it went to every system, and never 0 themselves. The
acknowledging system is the source of the frame, so the header names
it and the message body does not repeat it.
The dialect generates MAVLINK_M_ACK as {53004, 224, 69, 69, 3, 16,
17}: 3 is the have-target flag and 16 and 17 are the byte offsets of
the two fields, the bytes main's definition gives origin_sysid and
ack_sysid. The message ID and the 69-byte payload stay the same,
offset 16 carries the same system and offset 17 changes meaning, so
CRC_EXTRA, 47 on main and 224 here, is the only thing that tells the
two definitions apart on the wire. A peer that checks it drops the
other definition's acknowledgments.
Validation: the dialect validates against mavschema.xsd and
generates C and Python with strict units at mavlink 87da370 and
pymavlink 19880422; the other twenty-five messages keep their rows.
Assisted-by: Claude-Code:claude-opus-5-5
Signed-off-by: TSC21 <nmarques@riis.com>
Three directive messages name who has to act but not what about.
ESAD_STATE reports the track a store is armed against, yet
ESAD_ARMING cannot carry it, so that binding is set out of band and
assumed to hold. SENSOR_TASKING can point a sensor only at a
coordinate, which a moving target leaves behind, so following it
means re-tasking with fresh coordinates. CAS_9LINE carries the brief
but no sequence and no track, so TERMINAL_CONTROL.sequence has no
brief to refer to.
ESAD_ARMING, SENSOR_TASKING and CAS_9LINE gain a track_uid, the
TRACK_IDENTITY handle FIRES, TARGET_HANDOVER and the other directives
already carry; all-zero means no track. An ARM's track is the one
ESAD_STATE.track_uid reports back. A task with a track follows it,
and its point of interest becomes a starting cue; when the sensor
designates or marks, the track only cues it and it lases or marks
what its own tracker holds. CAS_9LINE also gains the engagement
sequence TERMINAL_CONTROL refers to. ESAD_CONFIG and TERMINAL_CONTROL
get no track: a configuration sets a device mode, not an engagement,
and TERMINAL_CONTROL binds to its brief through sequence.
The three messages change on the wire with this pull request's
addressing anyway, so the new base fields add no second break for
consumers.
Validation: the dialect validates against mavschema.xsd and
generates C and Python with strict units at mavlink 87da370 and
pymavlink 19880422, and the C headers compile at -Wall -Wextra
-Werror. Entry rows: ESAD_ARMING {53031, 3, 33, 33, 3, 12, 13},
SENSOR_TASKING {53050, 79, 42, 42, 3, 22, 23}, CAS_9LINE {53060,
151, 72, 72, 3, 50, 51}. The largest dialect payload stays 245
bytes, and against main the same 12 messages change, no others.
Assisted-by: Claude-Code:claude-opus-5-5
Signed-off-by: TSC21 <nmarques@riis.com>
SENSOR_TASKING is both a tasking request and the sensor's status
response, and it has one pair of target fields. Nothing says which
direction a frame is or whom each direction names, so one
implementation answers the requester while another echoes the
request's own target and sends the response back to the sensor.
Nothing says which fields a response carries either.
The status field tells the directions apart, the way TIMESYNC's
fields do for its request and response. A request carries UNSPECIFIED
and names the sensor or payload. A response carries ACK, REJECTED,
ACTIVE or COMPLETE and names the requester: the source system and
component IDs in the request's frame header. A response echoes
task_id and track_uid and reports the mode in effect in sensor_mode,
and receivers ignore its point-of-interest fields.
MAVLINK_M_TASKING_UNSPECIFIED says it marks a request.
Only descriptions change, so the wire format and CRC_EXTRA stay as
the addressing commits set them.
Validation: the dialect validates against mavschema.xsd and
generates C and Python with strict units at mavlink 87da370 and
pymavlink 19880422; SENSOR_TASKING keeps the entry row {53050, 79,
42, 42, 3, 22, 23}.
Assisted-by: Claude-Code:claude-opus-5-5
Signed-off-by: TSC21 <nmarques@riis.com>
ESAD_ARMING and ESAD_CONFIG carry a target_system, so one command can reach more than one system, but their multi-device rule reads as if one receiver saw every selected ESAD: with esad_id = 0 "each targeted ESAD must have advertised the same challenge hash; otherwise receivers reject the broadcast command". A receiver can judge only its own ESADs, so that rule has no reading across two systems, and ESAD_CONFIG never says how its one hash is checked against several ESADs. "Broadcast" also names two things: esad_id = 0 or store_id = 0 inside a system, and target_system = 0 across systems. Each receiving system applies an ESAD_ARMING with esad_id = 0, or an ESAD_CONFIG with esad_id = 0, store_id = 0 or both, only if every local ESAD the command selects advertises arming_challenge_hash, and rejects it otherwise. A command that reaches several systems can apply on some and be rejected on others, so the originator confirms each system from its ESAD_STATE. esad_id = 0 and store_id = 0 read "every local ESAD" and "every store on the receiving system", here and in the Store ID parameter of the four store commands, which leaves "broadcast" to mean target_system = 0. ESAD_STATE.esad_id says which ESADs must share the hash. Only descriptions change, so every entry row stays as the addressing commits set it. Validation: the dialect validates against mavschema.xsd and generates C and Python with strict units at mavlink 87da370 and pymavlink 19880422; the wire report against main lists the same 12 messages and rows, and "broadcast" appears nowhere in military.xml. Assisted-by: Claude-Code:claude-opus-5-5 Signed-off-by: TSC21 <nmarques@riis.com>
ENGAGEMENT_DIRECTIVE.origin_sysid reads "System ID issuing the
directive", which the frame header also carries, so it is either a
duplicate of the header or a different system, and the definition
does not say which. The acknowledgment drops its ack_sysid for being
that duplicate, while TRACK_IDENTITY, TARGET_CUE and
PARTICIPANT_POSITION keep their origin_sysid for provenance.
origin_sysid takes the provenance meaning here too: the system that
first issued the directive. A system that passes the directive on as
a message of its own, a fire direction center forwarding an
observer's CHECK FIRE for example, keeps this value, so it can differ
from the source system ID in the frame header. Routers never change
either, since forwarded frames are not modified. The field is not
authenticated, so it serves display and audit, never a decision to
act or not to act.
Only the description changes; the wire format stays as the
addressing commits set it.
Validation: the dialect validates against mavschema.xsd and
generates C and Python with strict units at mavlink 87da370 and
pymavlink 19880422, and ENGAGEMENT_DIRECTIVE keeps the entry row
{53023, 57, 46, 46, 3, 26, 27}.
Assisted-by: Claude-Code:claude-opus-5-5
Signed-off-by: TSC21 <nmarques@riis.com>
target_system = 0 sends a message to every system, and nothing in the dialect says when that is allowed. Some of the addressed messages carry fail-safe actions, where reaching everyone at once can be the point: ABORT and CHECK_FIRE, DISARM, CEASE_FIRE, WAVE_OFF. Others carry affirmative ones: a fire mission, a handover, ARM, RESUME, CLEARED_HOT, a 9-line, a grant of consent. Sent to everyone, those let several shooters converge on one target, or arm stores nobody meant to arm. So the rule is per action and direction, not per message ID. Each target_system description states the rule for its message. 0 is valid only for actions that stop or hold: the ABORT and CHECK_FIRE directives, DISARM, the ABORT and CEASE_FIRE clearances, the CHECK_FIRING and CEASE_LOADING fire-mission commands, WAVE_OFF, terminal ABORT, and withheld or withdrawn consent. From such a message a receiver acts only on the stop, never drops it for anything else the message carries, and ignores the rest, so a broadcast ABORT does not send every munition to one loiter point and a broadcast CHECK_FIRING does not hand its spotting to every cell. FIRES, TARGET_HANDOVER, ESAD_CONFIG, CAS_9LINE, CALL_FOR_FIRE and SENSOR_TASKING always name their recipient, SELF_DESTRUCT too because it cannot be undone, and END_OF_MISSION because it ends a mission rather than holding it. SENSOR_TASKING is never sent to every system, not even as a STANDBY, since caging every sensor also cages a designator guiding a weapon in flight. A 9-line goes to each aircraft of a flight, and the store commands that arm, test or configure are never sent to every system either. Receivers ignore a message sent with 0 that the rule does not allow. Enum values are named in full, so the generated reference links them. Only descriptions change, so every entry row stays as the addressing commits set it. Validation: the dialect validates against mavschema.xsd and generates C and Python with strict units at mavlink 87da370 and pymavlink 19880422; every enum entry the rules name exists in military.xml, and the wire report against main lists the same 12 messages and rows. Assisted-by: Claude-Code:claude-opus-5-5 Signed-off-by: TSC21 <nmarques@riis.com>
Addressing changes what reaches a recipient, and the dialect does not say how. A router forwards a message whose target_system is not 0 only on links where it has heard that system, and drops it elsewhere without telling the sender (mavlink-router endpoint.cpp:567-585), so an ABORT addressed to a munition under emission control dies at the first relay that has not heard it. Nothing says what target_system holds for a participant beyond a gateway, so an acknowledgment or a tasking response addressed to "the source of the request" stops at the gateway, or a station cannot tell systems behind it apart. Two networks joined with overlapping system IDs deliver one addressed FIRES to two effectors. Nothing says what "every system" means at a gateway boundary, and a router built from another definition drops the changed messages, stops included. A normative section in the dialect header states the rules once: addressed delivery needs the recipient heard on the path, so a stop for a silent recipient goes to every system and selects with its own fields; an affirmative directive names its component when there is a choice, a stop uses component 0; system IDs are network-local and unique across joined networks, with remapping at a gateway; identifiers are chosen by their originator and matched with its ID; a gateway re-originates, gives each far-side participant a proxy system ID with its own HEARTBEAT, returns responses, applies the receiver rules before translating and only reports what the far side reported; a stop to every system crosses a gateway as a stop, nothing else does; and routers and gateways move to a new definition with everything else. The per-message rules stay in the field descriptions, which the generated reference shows. Only the header comment changes, so the generated code and every entry row stay the same. Validation: the dialect validates against mavschema.xsd and generates C and Python with strict units at mavlink 87da370 and pymavlink 19880422; the wire report against main lists the same 12 messages and rows. The routing behavior cited is mavlink-router 2362c62 and the devguide routing rules at master 7412790c2. Assisted-by: Claude-Code:claude-opus-5-5 Signed-off-by: TSC21 <nmarques@riis.com>
MAVLINK_M_ACK names the message it answers by ack_msgid and
ack_instance, and that pair is not unique: an ABORT, a CHECK_FIRE and
a RESUME of mission 12 all come back as (53023, 12), CLEARED_HOT and
ABORT of one brief look the same, and LOITER_MUNITION_CONTROL and
TARGET_HANDOVER acknowledgments carry instance 0. A station cannot
tell which of its directives a system accepted. Nothing says which
messages are acknowledged at all, who answers a message sent to every
system, or what a receiver does with a repeat.
ack_time_usec carries the time_usec of the acknowledged message, the
field every message in the dialect starts with, so ack_msgid,
ack_instance and ack_time_usec name one message from one sender.
Every message that carries target_system is acknowledged, except
SENSOR_TASKING, whose status response answers it. A message sent to
every system is answered by every receiver, with the new
MAVLINK_M_ACK_NOT_APPLICABLE where nothing there is selected, so a
station can show one row per system after a broadcast stop. A repeat
with the same source, message ID and time_usec is a duplicate,
answered again and not acted on twice, which lets a sender retry and
a redundant path deliver twice. Receivers ignore an acknowledgment
sent to every system.
MAVLINK_M_ACK changes on the wire in this pull request anyway, so the
new base field adds no second break: the message generates as
{53004, 167, 77, 77, 3, 24, 25}, with target_system and
target_component at bytes 24 and 25. Implementations copy time_usec
into ack_time_usec and match pending directives on the three fields.
Validation: the dialect validates against mavschema.xsd and
generates C and Python with strict units at mavlink 87da370 and
pymavlink 19880422, and the C headers compile at -Wall -Wextra
-Werror. The wire report against main lists the same 12 messages;
only MAVLINK_M_ACK's row differs from the commit before.
Assisted-by: Claude-Code:claude-opus-5-5
Signed-off-by: TSC21 <nmarques@riis.com>
A stop sent to every system has no defined reach. Nothing says whether it matches on track or on sequence, sequence numbers repeat across issuers, and no stop can say "every engagement", so a network-wide CHECK FIRE can miss the mission it is meant for. The dialect has four ways to say stop and requires no system to honour any but its own, so a gateway that turns a CHECK FIRE into ENGAGEMENT_DIRECTIVE does not stop a munition that only reads LOITER_MUNITION_CONTROL. Stops travel to every link while starts follow a learned route, so a late RESUME can arrive after the CHECK_FIRE that should have held it, and nothing orders the two or bounds how old a start may be. "Should be honored on receipt" also leaves open whether message signing still applies. An ABORT or CHECK_FIRE applies to every engagement its sequence or nonzero track_uid matches, whoever issued it, so the stop errs toward stopping more; a RESUME or RETARGET must match every selector it carries. 0xFFFF in a stop's sequence means every engagement on the receiver, and FIRES and CAS_9LINE reserve it. ENGAGEMENT_DIRECTIVE ABORT and CHECK_FIRE become the common stop that every system able to engage, release or guide a weapon honours, a loitering munition mapping them to its own ABORT and WAVE_OFF. For one engagement a start no later than the last stop is ignored, a start older than the receiver's maximum age is answered MAVLINK_M_ACK_EXPIRED, and a stop is never rejected for age. Safety stops are acted on once the link's signing accepts them. FIELD CONVENTIONS notes that ABORT and DISARM keep the value 0, so a zero-filled message is a stop, which fails safe. Breaking: 0xFFFF in a sequence takes a meaning it did not have, and receivers must apply the matching and ordering rules. The messages involved change on the wire in this pull request anyway. Validation: the dialect validates against mavschema.xsd and generates C and Python with strict units at mavlink 87da370 and pymavlink 19880422; the wire report against main lists the same 12 messages and rows as the commit before. Assisted-by: Claude-Code:claude-opus-5-5 Signed-off-by: TSC21 <nmarques@riis.com>
TERMINAL_CONTROL and LOITER_MUNITION_CONTROL travel both ways, but only SENSOR_TASKING says how each direction is addressed. An aircraft's IN call or a munition's consent request has no stated recipient, and sent to every system it is ignored under the rule for 0. An aircraft report that echoes clearance ABORT to every system reads as an abort for all of them, the human-authority wording reads as covering ABORT and CEASE_FIRE too, and abort_authority_present reads as a gate on an ABORT. consent_state is not defined as a change, so a station that resends its state can grant consent again after a withdrawal. In TERMINAL_CONTROL the controller sends clearance to the attacking aircraft with aircraft_call NONE, and the aircraft sends aircraft_call to the controller, the source of the brief, with clearance NONE. CONTINUE and CLEARED_HOT stay human-authority instruments and count only from the controller that sent the brief; ABORT and CEASE_FIRE count on receipt from any sender, and abort_authority_present is a precondition for CLEARED_HOT only. In LOITER_MUNITION_CONTROL a consent request goes to the human authority, the source of the tasking for that track, a grant counts only from that system and a stop from anyone, and consent_state and terminate carry changes, with NONE leaving the munition as it is. Breaking: receivers must check the sender of a clearance or a grant and treat consent_state and terminate as changes; the two messages change on the wire in this pull request anyway. Validation: the dialect validates against mavschema.xsd and generates C and Python with strict units at mavlink 87da370 and pymavlink 19880422; the wire report against main lists the same 12 messages and rows as the commit before. Assisted-by: Claude-Code:claude-opus-5-5 Signed-off-by: TSC21 <nmarques@riis.com>
A sensor answers only its current requester, so a requester whose task another request replaces, or whose track the sensor loses, never hears that its task ended: its station keeps showing the task as active. task_id has no stated owner either, so two requesters can pick the same one for one sensor. MAVLINK_M_TASKING_STATUS gains MAVLINK_M_TASKING_INTERRUPTED. When a task ends for any reason other than its own requester's request, the sensor sends that requester a status response with it, echoing the task's task_id and track_uid. task_id is chosen by the requester and echoed unchanged, and a sensor tells tasks apart by the requester's system and component IDs together with task_id. Status responses go only to the requester. Adding an enum entry and description text does not change the wire format; SENSOR_TASKING keeps the entry row the addressing commits gave it. Validation: the dialect validates against mavschema.xsd and generates C and Python with strict units at mavlink 87da370 and pymavlink 19880422; the wire report against main lists the same 12 messages and rows. Assisted-by: Claude-Code:claude-opus-5-5 Signed-off-by: TSC21 <nmarques@riis.com>
ESAD_ARMING checks arming_challenge_hash for a DISARM as for an ARM,
and with esad_id = 0 the receiving system applies the command only if
every selected ESAD advertises the one hash it carries. One faulted
or reloaded ESAD therefore blocks disarming every other ESAD on the
system, and a DISARM sent to every system, which the rule for 0
allows, carries one hash and works on one system at most. The texts
also call the hash authentication: ESAD_CONFIG says an
unauthenticated sender cannot reconfigure the device, and the header
says the hash authenticates the MAVLink leg, while ESAD_STATE sends
the hash to every system, so any listener can copy it.
A DISARM applies to every selected local ESAD without checking the
hash, so no ESAD's state can hold it back; an ARM keeps the
all-or-nothing rule, so it never applies in part, and ESAD_CONFIG
keeps its check. The hash is described as what it is, a public value
that ties a command to the state the ESAD advertised, with the
sender authenticated by MAVLink message signing on the link, as for
any stop. A command that selects no local ESAD is rejected, and
ESAD_STATE clears its track_uid when the store is disarmed.
Breaking: a receiver accepts a DISARM whatever its hash, and rejects
a command that selects nothing. ESAD_ARMING changes on the wire in
this pull request anyway.
Validation: the dialect validates against mavschema.xsd and
generates C and Python with strict units at mavlink 87da370 and
pymavlink 19880422; ESAD_ARMING and ESAD_CONFIG keep the rows {53031,
3, 33, 33, 3, 12, 13} and {53032, 163, 17, 17, 3, 12, 13}.
Assisted-by: Claude-Code:claude-opus-5-5
Signed-off-by: TSC21 <nmarques@riis.com>
6987c44 to
1215ede
Compare
|
@ryanjAA one more round on top of your review. I ran an adversarial pass over this PR from four angles (MAVLink routing, tactical comms and gateways, weapons safety, operator UX), checked each finding against the devguide, mavlink-router and what mavgen generates, and the gaps it found are in 1215ede.
The base-field choice stays, and the description says what it costs: across a definition boundary an ABORT is dropped too, so every system moves together. |
IDMAPPING.md is the allocation record. README.md links it as the message-ID allocation, the site publishes it as the ID Allocation page, and the contributing guide asks for it to be updated in the same pull request as a new ID. Nothing compares it with military.xml, so a pull request can add, move or drop a message and leave the table and the published page wrong. On main the two agree: 26 messages, 4 MAV_CMD entries, and no ID in use inside a reserved block. The policy check reads the first table under each "## " heading of IDMAPPING.md. Every military.xml message must be in "Shared messages" and every MAV_CMD entry in "MAV_CMD entries", at the ID military.xml gives it (the New ID and New value columns), and every row must name something military.xml defines. No message or command may use an ID inside a "Reserved blocks" range that lies in the shared window 53000-53899; the private block belongs to the allocation rule. A name is the cell's leading UPPER_SNAKE word, so "TARGET (formerly TARGET_COORD)" is TARGET, and backticks are accepted. The history table and the old ID columns are not checked. A missing file, table or column is a finding in itself, so a restructured document fails the check rather than passing it unread. A finding about the XML points at the element, one about the document at its row. Validation: main's files pass. On copies of them, a message added at 53062 with no document change gives 2 findings (not in the table, inside 53062-53089); FIRES listed at 53025 gives 1, at its row; a stale row and a dropped MAV_CMD row give 1 each; a renamed "Shared messages" heading gives 1; the military.xml of #10 at c55620e passes. 21 policy tests pass on Python 3.10.12 and 3.14.5 with 100% line and branch coverage (200 statements, 98 branches), and turning off the reserved-block test, the ID comparison or the one table per section rule each fails the suite. actionlint 1.7.12, zizmor 1.30.1 and ruff 0.14.0 report nothing. Assisted-by: Claude-Code:claude-opus-5-5 Signed-off-by: TSC21 <nmarques@riis.com>
The wire report compares only each message's name and CRC_EXTRA, but a
router places a message from the row mavgen writes for it in the C
headers' MAVLINK_MESSAGE_CRCS table: {msgid, CRC_EXTRA, min_length,
max_length, flags, target_system offset, target_component offset}. A
reviewer of an addressing change needs the lengths, the target flags
and the offsets, and the report hides all of them. It also hides any
change that leaves CRC_EXTRA alone: extension fields, including a
target field placed among them, which moves the routing row while
older peers keep accepting the message.
wire_map reads the row straight off pymavlink's parse, the same
attributes mavgen_c.py prints into the table. Each wire-breaking line
(removed, renamed, or CRC_EXTRA changed) carries the old and the new
row. A message whose row changes with CRC_EXTRA unchanged is listed as
an extension-only INFO change with both rows, and its step summary
note says that a sender without the new extension fields sends zeros
there, which a router reads as broadcast. Additions carry their row.
WIRE_COMPAT_ENFORCE still fails on wire-breaking changes only.
Validation: against the military.xml of #10 at c55620e the report
lists 12 wire-breaking messages with their rows, among them
MAVLINK_M_ACK {53004, 47, 69, 69, 0, 0, 0} -> {53004, 224, 69, 69, 3,
16, 17}, FIRES {53020, 16, 62, 62, 0, 0, 0} -> {53020, 124, 64, 64, 3,
40, 41} and SENSOR_TASKING {53050, 244, 24, 24, 0, 0, 0} -> {53050,
107, 26, 26, 3, 22, 23}. For both files, all 26 rows match the
MAVLINK_MESSAGE_CRCS table mavgen generates. 13 wire report tests
pass, 42 script tests in all, on Python 3.10.12 and 3.14.5, with 100%
line and branch coverage of check_wire_compat.py (64 statements, 24
branches); breaking on any row change, dropping the flags, or
ignoring CRC-compatible changes each fail the suite. ruff 0.14.0,
actionlint 1.7.12 and zizmor 1.30.1 report nothing.
Assisted-by: Claude-Code:claude-opus-5-5
Signed-off-by: TSC21 <nmarques@riis.com>
Eleven of the dialect's messages order one system to act, but every message is broadcast, so none of them can say which system it's for. This PR gives them MAVLink's standard
target_systemandtarget_component, so a router delivers each one to its recipient, and says what that means for stops, gateways and acknowledgments. It settles the points raised in the RFC discussion and the gaps an adversarial review found after it, gateways first among them.This is a definitions change and adds no capability. It lets the directives the dialect already defines name their recipient, using the two field names core MAVLink reserves for that, and pins down the rules those definitions left open. The integration behind it is in QGC, which consumes the situational-awareness messages and originates sensor tasking and nothing else, so
SENSOR_TASKINGis the only one of these messages it exercises. The rest of the list is covered because a definitions change should treat every directive in the dialect the same way, not because I implement them or plan to.FIRES, SPLASH_CORRECTION, TARGET_HANDOVER, ESAD_ARMING, ESAD_CONFIG, SENSOR_TASKING, CAS_9LINE, TERMINAL_CONTROL, ENGAGEMENT_DIRECTIVE, CALL_FOR_FIRE and LOITER_MUNITION_CONTROL carry the two fields; the messages that describe the world stay broadcast. MAVLINK_M_ACK goes back to the sender of the message it acknowledges and echoes that message's
time_usec, so each directive gets its own answer, and every addressed directive is acknowledged. Sending to every system is valid only for actions that stop or hold, and a receiver acts only on the stop in such a message. A stop applies to every engagement it matches, with0xFFFFmeaning all of them, ENGAGEMENT_DIRECTIVE ABORT and CHECK_FIRE are the stop every system that can engage or guide a weapon honours, and a stop always wins over an earlier start. SENSOR_TASKING, TERMINAL_CONTROL and LOITER_MUNITION_CONTROL say how each direction is addressed, a sensor tells a requester when its task is interrupted, and a DISARM applies whatever the ESAD challenge holds. ESAD_ARMING, SENSOR_TASKING and CAS_9LINE gaintrack_uid, and CAS_9LINE gains thesequenceTERMINAL_CONTROL refers to.Addressing only works where routers can deliver. A router forwards an addressed message only on links where it has heard the recipient, so the dialect header states the rules for that: a stop for a recipient that may be silent, under emission control or behind a receive-only link, goes to every system and selects with its own fields. A gateway to another network or protocol (UCI, VMF, Link 16, or another MAVLink network) re-originates messages, presents each far-side participant as a proxy system ID with its own HEARTBEAT, returns acknowledgments and responses to it, and only reports what the far side reported. System IDs are unique across joined networks, and routers and gateways move to the new definition along with everything else.
This breaks the wire format for twelve messages, and the stop and DISARM rules change what receivers must do. Each of the twelve changes CRC_EXTRA; FIRES, for example, generates as
{53020, 124, 64, 64, 3, 40, 41}and MAVLINK_M_ACK as{53004, 167, 77, 77, 3, 24, 25}, with flag 3 telling a router where the target sits. The target fields are base fields on purpose. As extensions, a sender built from the old definition would leave them out and the receiver would read zero, which means every system. As base fields, old and new definitions drop each other's messages instead, an ABORT included, so every system, routers and gateways too, has to move to this definition together.@mrpollo, ryanjAA has reviewed the design and the wire format, and the review follow-ups are in this push. Could you review it and merge when you're happy? Merging republishes the C library with the twelve new CRC_EXTRA values, so anything built from the current headers has to regenerate at the same time.
I ran the checks from #11 locally with mavlink 87da370 and pymavlink 19880422 on each of the thirteen commits: the dialect validates, generates C and Python with strict units and compiles at
-Werror, the ID checks pass, the wire report against main lists exactly the twelve messages, and the docs site builds with no new dead links.Assisted-by: Claude-Code:claude-opus-5-5