Skip to content

CRSF: Allow future use of unused bits in the "Arming status byte" - #7022

Open
mha1 wants to merge 6 commits into
EdgeTX:mainfrom
mha1:PR_use_more_status_bits
Open

CRSF: Allow future use of unused bits in the "Arming status byte"#7022
mha1 wants to merge 6 commits into
EdgeTX:mainfrom
mha1:PR_use_more_status_bits

Conversation

@mha1

@mha1 mha1 commented Jan 22, 2026

Copy link
Copy Markdown
Contributor

This PR allows future use of the currently unused status bits in the byte we use to communicate the commanded arming status to ExpressLRS modules if we ever have a need for them. The changes are fully backward compatible with the existing V2.11 and V2.12 implementation for communicating the commanded arming status to ExpressLRS modules. It is also compatible with modules using ExpressLRS V3 firmware.

Summary of changes:

  • sends status byte with every CRSF 0x16 RC Channels packet. Note: the TBS CRSF V3 specification allows sending extra fields. CRSF V3 Frame Details: "size may be bigger than expected frame of given type. This should not be a reason to count the frame invalid. Frame receiver should just ignore extra fields.". ExpressLRS pre-V4 firmware ignores the field and will use CH5 Arming.
  • determines arming method and status using bit 0 and 1 of the status byte
image
  • makes status byte bits 2 to 7 available for future use

This complements the changes made to V4 in ExpressLRS/ExpressLRS#3470 and is fully backwards compatible to the existing implementation

Summary by CodeRabbit

  • Bug Fixes
    • Fixed Crossfire CHANNELS frame structure to use a consistent fixed-length payload with dedicated status byte
    • Improved arming status reporting in Crossfire protocol to properly reflect armed state based on configuration

Review Change Stack

@gagarinlg gagarinlg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

what is this part of the code for?

Comment thread radio/src/pulses/crossfire.cpp Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR updates CRSF channel frame generation so the status byte is always appended, enabling arming-mode signaling while leaving higher bits available for future status flags.

Changes:

  • Always sends a 25-byte CRSF channel frame payload including the extra status byte.
  • Encodes Switch-mode armed state in bit 0 and CH5 arming mode in bit 1.
  • Updates CRC calculation to include the always-present status byte.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread radio/src/pulses/crossfire.cpp
Comment thread radio/src/pulses/crossfire.cpp Outdated
@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c7cc18bc-7954-4fbf-a3b7-2ac54497ada0

📥 Commits

Reviewing files that changed from the base of the PR and between de5f4eb and debd129.

📒 Files selected for processing (1)
  • radio/src/pulses/crossfire.cpp

📝 Walkthrough

Walkthrough

createCrossfireChannelsFrame now builds CHANNELS frames with fixed 25-byte payload including a status byte. Frame header construction and channel-assembly points are adjusted; status byte encodes armed state (Switch mode) or CH5 mode flag, with CRC8 recomputed for the new length.

Changes

Crossfire Frame Status Byte Integration

Layer / File(s) Summary
Frame structure with status byte assembly
radio/src/pulses/crossfire.cpp
Frame header allocates fixed 25-byte payload; channel-ID assembly offset adjusted. Status byte assembled after channel packing: reflects commanded armed status in Switch mode, or constant CH5 indicator (0x02) otherwise. CRC8 length updated to match new frame format.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: allowing future use of unused bits in the arming status byte while maintaining backward compatibility.
Description check ✅ Passed The description covers the key aspects including backward compatibility, the status byte implementation, and a summary of changes with visual documentation.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

mha1 and others added 6 commits August 27, 2026 00:49
Replaces the "TODO check" placeholder with real coverage of the wire
format: the spec-defined 0x16 channel packing (verified against
independently computed expected bytes), and the ExpressLRS arming
status-byte extension for both CH5 and Switch arming modes (length,
status bits, CRC), matching the fixed 25-byte frame introduced in
this PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@pfeerick
pfeerick force-pushed the PR_use_more_status_bits branch from debd129 to 44a5b59 Compare August 27, 2026 00:56
@pfeerick

Copy link
Copy Markdown
Member

Is this truely compatible with ELRS V3? Or do you mean 3.6.3 and later - as this seems to cause an issue for at least 3.5.3 through 3.6.2? Since not everyone updates to the new version as soon as it comes out, we can not assume they will be running 3.6.3 or later... especially if this goes into 2.11.x, for example. If the following works, should we be looking at version gating it? Only the pulses/crossfire.cpp bit is relevant for you, the rest is updating the tests to match.

diff --git a/radio/src/pulses/crossfire.cpp b/radio/src/pulses/crossfire.cpp
index 1cd9259c40..e1cf9780e0 100644
--- a/radio/src/pulses/crossfire.cpp
+++ b/radio/src/pulses/crossfire.cpp
@@ -101,9 +101,21 @@ uint8_t createCrossfireChannelsFrame(uint8_t moduleIdx, uint8_t * frame, int16_t
   // - arming mode Switch or CH5 (bit 1)
   // - bits 2-7 spare
   //
+  // Gate on the module's own ELRS version - older than 4.0 (#3470)
+  // don't mask individual bits: they assign the whole byte to a bool
+  // (any nonzero -> armed). Sending 0x02 unconditionally in CH5 mode
+  // would make those modules report a spurious armed state.
+  //
+  bool fixedStatusByte = CRSF_ELRS_MIN_VER(moduleIdx, 4, 0);
+
+  ModuleData *md = &g_model.moduleData[moduleIdx];
+  uint8_t armingMode = md->crsf.crsfArmingMode; // 0 = Channel mode, 1 = Switch mode
+  uint8_t lenAdjust = (fixedStatusByte || armingMode == ARMING_MODE_SWITCH) ? 1 : 0;
+
   uint8_t * buf = frame;
   *buf++ = MODULE_ADDRESS;
-  *buf++ = 25;                  // 1(ID) + 22(channel data) + 1(extra status byte) + 1(CRC)
+  *buf++ = 24 + lenAdjust;      // 1(ID) + 22(channel data) + (+1 extra byte if present) + 1(CRC)
   uint8_t * crc_start = buf;
   *buf++ = CHANNELS_ID;
 
@@ -126,22 +138,25 @@ uint8_t createCrossfireChannelsFrame(uint8_t moduleIdx, uint8_t * frame, int16_t
   //
   // assemble status byte
   //
-  ModuleData *md = &g_model.moduleData[moduleIdx];
+  if (fixedStatusByte) {
+    if (armingMode == ARMING_MODE_SWITCH) {
+      swsrc_t sw = md->crsf.crsfArmingTrigger;
 
-  if (md->crsf.crsfArmingMode == ARMING_MODE_SWITCH) {
-    swsrc_t sw =  md->crsf.crsfArmingTrigger;
+      *buf = (sw != SWSRC_NONE) && getSwitch(sw, 0);  // commanded armed status in Switch mode
+    } else {
+      *buf = 0x02;                                    // flag arming mode CH5
+    }
+    buf++;
+  } else if (armingMode == ARMING_MODE_SWITCH) {
+    swsrc_t sw = md->crsf.crsfArmingTrigger;
 
-    *buf = (sw != SWSRC_NONE) && getSwitch(sw, 0);  // commanded armed status in Switch mode
-  } else {
-    *buf = 0x02;                                    // flag arming mode CH5
+    *buf++ = (sw != SWSRC_NONE) && getSwitch(sw, 0);  // commanded armed status in Switch mode
   }
 
-  buf++;
-  
   //
   // add crc
   //
-  *buf++ = crc8(crc_start, 24);
+  *buf++ = crc8(crc_start, 23 + lenAdjust);
 
   return buf - frame;
 }
diff --git a/radio/src/tests/crossfire.cpp b/radio/src/tests/crossfire.cpp
index b904a454f8..5b0d037037 100644
--- a/radio/src/tests/crossfire.cpp
+++ b/radio/src/tests/crossfire.cpp
@@ -44,6 +44,10 @@ TEST(Crossfire, createCrossfireChannelsFrame)
     pulsesStart[i] = -1024 + (2048 / MAX_TRAINER_CHANNELS) * i;
   }
 
+  // channel packing doesn't depend on arming mode or module version, but
+  // reset explicitly rather than relying on running before the tests below
+  crossfireModuleStatus[EXTERNAL_MODULE] = {0};
+
   createCrossfireChannelsFrame(EXTERNAL_MODULE, crossfire, pulsesStart);
 
   ASSERT_EQ(crossfire[0], MODULE_ADDRESS);

--- a/radio/src/tests/crossfire.cpp
+++ b/radio/src/tests/crossfire.cpp
@@ -58,9 +58,10 @@ TEST(Crossfire, createCrossfireChannelsFrame)
 
 // Status byte after the 0x16 payload is an ExpressLRS extension, not TBS CRSF
 // spec (semantics per ExpressLRS's TXModuleEndpoint.cpp / crsf_protocol.h).
-// Frame is always 25 bytes (1 ID + 22 channel data + 1 status + 1 CRC);
-// bit 0 = commanded armed status (Switch mode only), bit 1 = arming mode is CH5.
-TEST(Crossfire, ExpressLRSArmingExtension_CH5Mode)
+// The fixed 25-byte frame is only sent once the module confirms (via
+// CRSF_ELRS_MIN_VER) it's new enough to mask individual status bits (#3470);
+// older/unknown modules get the legacy variable-length frame they understand.
+TEST(Crossfire, ExpressLRSArmingExtension_CH5Mode_LegacyModule)
 {
   MODEL_RESET();
 
@@ -73,6 +74,37 @@ TEST(Crossfire, ExpressLRSArmingExtension_CH5Mode)
   }
 
   g_model.moduleData[EXTERNAL_MODULE].crsf.crsfArmingMode = ARMING_MODE_CH5;
+  crossfireModuleStatus[EXTERNAL_MODULE] = {0}; // no/unknown module version
+
+  uint8_t len = createCrossfireChannelsFrame(EXTERNAL_MODULE, crossfire, pulsesStart);
+
+  // no status byte at all for CH5 mode pre-#3470: legacy 24-byte payload
+  ASSERT_EQ(len, 26);
+  ASSERT_EQ(crossfire[0], MODULE_ADDRESS);
+  ASSERT_EQ(crossfire[1], 24);
+  ASSERT_EQ(crossfire[2], CHANNELS_ID);
+
+  uint8_t crc = crc8(&crossfire[2], 23);
+  ASSERT_EQ(crossfire[25], crc);
+}
+
+TEST(Crossfire, ExpressLRSArmingExtension_CH5Mode_ModernModule)
+{
+  MODEL_RESET();
+
+  int16_t pulsesStart[MAX_TRAINER_CHANNELS];
+  uint8_t crossfire[CROSSFIRE_FRAME_MAXLEN];
+
+  memset(crossfire, 0, sizeof(crossfire));
+  for (int i=0; i<MAX_TRAINER_CHANNELS; i++) {
+    pulsesStart[i] = -1024 + (2048 / MAX_TRAINER_CHANNELS) * i;
+  }
+
+  g_model.moduleData[EXTERNAL_MODULE].crsf.crsfArmingMode = ARMING_MODE_CH5;
+  crossfireModuleStatus[EXTERNAL_MODULE] = {0};
+  crossfireModuleStatus[EXTERNAL_MODULE].isELRS = true;
+  crossfireModuleStatus[EXTERNAL_MODULE].major = 4;
+  crossfireModuleStatus[EXTERNAL_MODULE].minor = 0;
 
   uint8_t len = createCrossfireChannelsFrame(EXTERNAL_MODULE, crossfire, pulsesStart);
 
@@ -101,6 +133,8 @@ TEST(Crossfire, ExpressLRSArmingExtension_SwitchMode)
   g_model.moduleData[EXTERNAL_MODULE].crsf.crsfArmingMode = ARMING_MODE_SWITCH;
   g_model.moduleData[EXTERNAL_MODULE].crsf.crsfArmingTrigger = SWSRC_NONE;
 
+  // Switch mode already implied a status byte pre-#3470 (bit 0 only), so it
+  // gets the fixed-length frame regardless of module version.
   uint8_t len = createCrossfireChannelsFrame(EXTERNAL_MODULE, crossfire, pulsesStart);
 
   ASSERT_EQ(len, 27);

@mha1

mha1 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Is this truely compatible with ELRS V3?

Yes, can you elaborate on the issue or concern?

V3 doesn't have any knowledge of the extra status byte and doesn't have code to act on it. It'll read the 25byte payload but ignore the last byte (= status byte)

image

Version gating in pulses/crossfire.cpp is not necessary.

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.

4 participants