Skip to content

Elrs 4.0 support - #20

Merged
SaintSampo merged 19 commits into
mainfrom
ELRS-4.0-support
Jul 21, 2026
Merged

Elrs 4.0 support#20
SaintSampo merged 19 commits into
mainfrom
ELRS-4.0-support

Conversation

@SaintSampo

@SaintSampo SaintSampo commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Adds ExpressLRS 4.0 support while keeping full compatibility with 3.x, and
reworks packet dispatch to follow the CRSF specification.

Added

Frame types decoded

Frame ID Notes
GPS_TIME 0x03 Handset clock sync, ELRS 4.1+
AIRSPEED 0x0A
HEARTBEAT 0x0B CRSF router device discovery
RPM 0x0C Variable length, 24 bit signed values
TEMP 0x0D Variable length
CELLS 0x0E Variable length, millivolt cell voltages
DEVICE_PING / DEVICE_INFO 0x28 / 0x29 Device discovery
ELRS_STATUS 0x2E Packet counts, warning flags and message
COMMAND 0x32 Model select
HANDSET 0x3A Timing sync; named RADIO_ID in 3.x

Protocol mechanics handled along the way:

  • Variable length sensors. RPM, TEMP and CELLS derive their value counts
    from the frame length. RPM values are sign extended from 24 bits.
  • Channels status byte. ELRS 4.0 handsets on EdgeTX 2.11+ append an
    optional arming status byte, detected by frame length.
  • Extended header frames. Types 0x28 to 0x96 are routed by the
    destination and origin bytes they carry, via the new crsf_ext_header_t.
  • Command frame double CRC. COMMAND frames carry an extra CRC over the
    payload using polynomial 0xBA before the normal 0xD5 frame CRC.

Examples

Example Purpose
elrs4SelfTest 26 functional checks with no radio needed: cross wire two UARTs and exercise every parser and sender
elrs4ReceiverTest Bench test a receiver: channels, link statistics and all telemetry types
handsetEmulator Drive a TX module the way a handset does, including model ID, arm state and frame pacing

Changed

  • Packet dispatch is now specification compliant. Byte 0 of a CRSF frame
    is a sync byte, not a destination address. Dispatch is purely type based,
    and extended header frames are routed by their own destination and origin
    bytes. begin() takes an optional device address, defaulting to
    CRSF_ADDRESS_FLIGHT_CONTROLLER.

    Migration: existing sketches compile and run unchanged. Frames that the
    old address based dispatch silently dropped are now parsed, which is
    observable for sketches running several instances to bridge links.

  • BaroAltitude sends vertical speed in the same packet. The example
    previously truncated the frame and sent a separate Vario packet to work
    around an EdgeTX limitation that no longer exists. sendVario() remains for
    vertical speed that does not come from a barometer.

  • forwardChannelsToFC uses the new writeChannels() helper.

  • Documentation. The README is now about features, requirements, examples
    and compatibility, with ELRS 3.x and 4.x support marked per feature. The
    wire format moved to CRSF_PROTOCOL.md, corrected to document byte 0 as a
    sync byte and to explain extended header routing.

Fixed

  • Attitude values are signed. crsf_sensor_attitude_t used uint16_t
    where the specification is signed, so negative pitch, roll and yaw were
    misread. The telemetry example also cast negative floats to uint16_t,
    which is undefined behaviour.
  • GPS heading scale. The example multiplied by 1000, but the field is
    degrees times 100. Readings were ten times too large and overflowed the
    16 bit field above 65.5 degrees. This was the "EdgeTX overflow issue" noted
    in the old README TODO; the bug was ours.
  • Battery capacity used only 16 of its 24 bits, capping capacity at
    65535 mAh. Now 16777215 mAh.

Compatibility

Works with both ELRS 3.x and 4.x from the same sketch. Frames that do not
exist on an older link never arrive, and their getters stay zeroed.

Features needing a specific firmware:

Feature Requires
Airspeed, RPM, temperature, cells ELRS 3.5.5
Millivolt battery voltage from the receiver ELRS 4.0
Arming status byte ELRS 4.0 and EdgeTX 2.11
ELRS status frames, heartbeat, device discovery ELRS 4.0
GPS time forwarded to the handset ELRS 4.1

Validation

Self test 26 of 26. Verified on ELRS 3.x hardware (channels, link statistics,
arm state, and newer telemetry correctly absent) and on ELRS 4.x hardware
across all examples. handsetEmulator was confirmed end to end, with emulated
channel data arriving through the RF link on a second board.

SaintSampo and others added 19 commits July 14, 2026 14:48
Cast pitch, roll, and yaw to int16_t instead of uint16_t in the attitude telemetry example to properly support negative angle values.
Adds the newer CRSF telemetry frame types used by the ELRS 3.4+/4.x
ecosystem, with getters for each sensor. RPM, temp, and cells frames are
variable length, so their value counts are derived from the frame length.
Telemetry parsing is shared between the flight controller and backpack
directions, so the backpack path now receives all telemetry types instead
of only attitude.
ELRS 4.0 handsets running EdgeTX 2.11+ may append a status byte after the
packed channels in RC_CHANNELS_PACKED frames, carrying the commanded arm
state for Arm using Switch mode. Detect it by frame length, expose the raw
byte, and add isArmed() mirroring the ELRS arming logic (status bit in
switch mode, channel 5 position otherwise). Frames without the status byte
behave exactly as before.
ELRS TX modules send this extended-header frame to the handset with good/
bad packet counts, warning flags, and a warning message string. Decode it
into crsf_elrs_status_t, exposed via getElrsStatus(). The message is
variable length on the wire and is copied with a bounded length and
guaranteed null termination.
Demonstrates sending CRSF_FRAMETYPE_GPS_TIME so a connected handset can
set its clock (used by ELRS 4.1+ with EdgeTX 2.11+; older versions ignore
the packet).
writeChannels(addr, channels) sends a standard 22-byte packed channels
frame and works with any CRSF device. The three-argument overload appends
the ELRS 4.0 channels status byte (CRSF_CHANNELS_STATUS_* bits) so a
sketch acting as a handset can command arm state in Arm using Switch
mode. ELRS 3.x TX modules do not understand the longer frame, so the
status byte overload must only be used with 4.0+ modules.

The forwardChannelsToFC example now uses the plain helper; frames sent
toward a flight controller never carry the status byte.
Byte 0 of a CRSF frame is a sync byte, not a destination address, so
dispatch is now purely type-based: channels and telemetry frames are
decoded regardless of the leading byte, and extended header frames
(0x28-0x96) are routed by the destination/origin bytes they carry. Adds
crsf_ext_header_t and a device address to begin() (default flight
controller) for extended frames addressed to a specific device.

Behavior change from 1.x: frames that the old address-based dispatch
dropped (e.g. telemetry with an unexpected leading byte) are now parsed.
Wire compatible with both ELRS 3.x and 4.x.
Opt-in support for appearing as a device on an ELRS 4.0 CRSF network:
sendHeartbeat() announces this device's address for router discovery, and
setDeviceName() enables answering DEVICE_PING (0x28) with a DEVICE_INFO
(0x29) response addressed to the requester. Adds writeExtPacket() for
building extended header frames with this device's address as origin.

All of it is inert unless the sketch opts in, and the frames themselves
predate ELRS 4.0, so 3.x links are unaffected.
TX modules send this extended-header frame to tell the handset the
requested channels packet interval and a phase offset correction (both in
0.1us units). Decoded into crsf_handset_timing_t via getHandsetTiming()
so handset-emulation sketches can pace their channels frames. The frame
was named RADIO_ID in older firmwares with the same wire format, so this
works with both ELRS 3.x and 4.x modules.
The spec-compliant dispatch rework changes observable parsing behavior
for multi-instance bridge sketches (frames previously dropped by
address-based dispatch are now parsed), so this line is a major version.
elrs4SelfTest: loopback functional test needing no radio hardware - two
cross-wired UARTs on one board run a handset instance against a TX module
instance, exercising channels round-trip, the arm status byte and isArmed
logic, variable-length sensor decoding (cells/rpm/temp incl. 24-bit sign
extension), GPS time, ELRS_STATUS, handset timing sync, device discovery
ping/response, and heartbeat, printing PASS/FAIL per check.

handsetEmulator: drives a real TX module - paces channels frames from the
module's timing sync, optional 4.0 arm status byte, prints ELRS status.

elrs4ReceiverTest: FC-side bench test for a 4.x receiver - prints channels,
link stats, and the new sensors (millivolt cells, temps, rpm, airspeed,
GPS time), sends heartbeats and answers discovery pings so the sketch
appears in the ExpressLRS Lua.
Heading is degrees * 100 on the wire (EdgeTX parses it with 2 decimal
places), but the example multiplied by 1000. That made every reading ten
times too large and overflowed the uint16 field above 65.5 degrees, which
is the 'overflow issue in EdgeTX' the README noted: the bug was ours, not
EdgeTX's. 0-360 degrees now maps to 0-36000 and fits comfortably.
The battery example packed capacity with htobe16 shifted left by 8, which
is correct big endian but caps capacity at 65535mAh. EdgeTX reads the
field as three big endian bytes, so add an htobe24 macro (matching the one
in ELRS 4.x) and use it: capacity now goes up to 16777215mAh.

Verified the byte order lands as 12 D6 87 for 1234567mAh, which is what
EdgeTX's getCrossfireTelemetryValue<3> decodes back to 1234567.
EdgeTX picks what a BaroAltitude packet contains from its declared length:
a 2 byte payload is altitude only, 5 bytes adds TBS vertical speed, and 6
or more adds ELRS style int16 vertical speed. The example was truncating
the payload to altitude only and sending a separate Vario packet to work
around what looked like an EdgeTX parsing problem.

Sending the full 4 byte payload works, so altitude and vertical speed now
travel in one packet. The standalone Vario send is kept as sendVario() for
vertical speed that does not come from a barometer.

Also drops the callbacks TODO (not planned; the getter API stays) and
notes the 3.x/4.x compatibility story in the README intro.
The README was mostly a CRSF wire format dump, which buried what the
library actually does. It now covers features, hardware requirements and
wiring, installation, a quick start, a full API reference, a guide to the
examples, compatibility gotchas, and references.

The protocol specification moves to CRSF_PROTOCOL.md, reorganised into
tables and updated to match what this library now implements: byte 0
documented as a sync byte rather than a destination address, extended
header frames and their routing explained, and the frame types added
since ELRS 3.x filled in.

Also documents the model match trap, since a receiver that binds but
sends nothing looks exactly like a wiring fault.
Convert Features and Examples sections to use comparison tables showing ELRS 3.x vs 4.x compatibility. Remove Wiring and detailed API sections. This makes it immediately clear which features and examples work on each firmware version, improving usability for users working with different ELRS versions.
Implements the CRSF model select command (0x32) to allow emulating handset behavior when connecting to transmitter modules. This addresses the common issue of mismatched model IDs causing receivers to remain silent. Includes:

- New sendModelId() method to transmit model ID to TX module
- CRC8 polynomial calculation support for command frame payloads
- COMMAND frame protocol constants and support
- Updated examples to initialize model ID at startup
- Enhanced documentation explaining silent receiver issues and fixes
- Library description improvements
@SaintSampo
SaintSampo merged commit 7d29950 into main Jul 21, 2026
2 checks passed
@SaintSampo
SaintSampo deleted the ELRS-4.0-support branch July 21, 2026 05:28
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.

1 participant