diff --git a/CRSF_PROTOCOL.md b/CRSF_PROTOCOL.md new file mode 100644 index 0000000..2d6f575 --- /dev/null +++ b/CRSF_PROTOCOL.md @@ -0,0 +1,351 @@ +# CRSF protocol specification + +The CRSF (Crossfire) protocol is not documented or maintained by any single +entity. This specification has been assembled from the ExpressLRS, EdgeTX and +OpenTX codebases, cross-checked against the TBS specification. Where firmwares +disagree, the behaviour described here is the one this library implements. + +See [References](#references) at the bottom for the upstream sources. + +## Packet format + +``` +[sync] [len] [type] [payload] [crc8] +``` + +| Field | Size | Description | +| --- | --- | --- | +| sync | 1 | Sync byte, see below | +| len | 1 | Number of bytes that follow, i.e. type + payload + crc | +| type | 1 | Frame type, see [Frame types](#frame-types) | +| payload | len - 2 | Frame specific, see [Payloads](#payloads) | +| crc8 | 1 | CRC over type and payload | + +Total packet length is `len + 2`, or payload length + 4. + +### Sync byte + +The first byte is a **sync marker, not routing information**. On a serial link +it is always `0xC8`, which collides with `CRSF_ADDRESS_FLIGHT_CONTROLLER` +because that address doubles as the sync value. On handset links you will also +see `0xEE` and `0xEA`. + +Do not infer the destination of a frame from this byte. Standard frames are +identified purely by their type, and frames that genuinely need addressing use +[extended header frames](#extended-header-frames). + +### CRC + +CRC8 with polynomial `0xD5`, initial value 0, covering all bytes from the type +byte through the end of the payload. It does not include the sync or length +bytes. + +### Addresses + +| Address | Value | Device | +| --- | --- | --- | +| `CRSF_ADDRESS_BROADCAST` | 0x00 | All devices | +| `CRSF_ADDRESS_USB` | 0x10 | USB | +| `CRSF_ADDRESS_BLUETOOTH_WIFI` | 0x12 | Bluetooth or WiFi link | +| `CRSF_ADDRESS_TBS_CORE_PNP_PRO` | 0x80 | TBS Core PNP Pro | +| `CRSF_ADDRESS_CURRENT_SENSOR` | 0xC0 | Current sensor | +| `CRSF_ADDRESS_GPS` | 0xC2 | GPS | +| `CRSF_ADDRESS_TBS_BLACKBOX` | 0xC4 | TBS Blackbox | +| `CRSF_ADDRESS_FLIGHT_CONTROLLER` | 0xC8 | Flight controller | +| `CRSF_ADDRESS_RACE_TAG` | 0xCC | Race tag | +| `CRSF_ADDRESS_RADIO_TRANSMITTER` | 0xEA | Handset | +| `CRSF_ADDRESS_CRSF_RECEIVER` | 0xEC | Receiver | +| `CRSF_ADDRESS_CRSF_TRANSMITTER` | 0xEE | Transmitter module | + +### Extended header frames + +Frame types in the range **0x28 to 0x96** carry two extra bytes at the start of +the payload: + +``` +[sync] [len] [type] [dest] [origin] [payload] [crc8] +``` + +`dest` is the address the frame is for and `origin` is the sender. A device +should act on a frame when `dest` is its own address or the broadcast address +`0x00`. This is how device discovery, parameter access and ELRS status frames +are routed between the handset, transmitter module, receiver and flight +controller. + +## Frame types + +| Type | ID | Direction | Notes | +| --- | --- | --- | --- | +| `GPS` | 0x02 | telemetry | | +| `GPS_TIME` | 0x03 | telemetry | Handset clock sync, ELRS 4.1+ | +| `VARIO` | 0x07 | telemetry | | +| `BATTERY_SENSOR` | 0x08 | telemetry | | +| `BARO_ALTITUDE` | 0x09 | telemetry | Optionally includes vertical speed | +| `AIRSPEED` | 0x0A | telemetry | | +| `HEARTBEAT` | 0x0B | any | Device discovery, ELRS 4.0+ | +| `RPM` | 0x0C | telemetry | Variable length | +| `TEMP` | 0x0D | telemetry | Variable length | +| `CELLS` | 0x0E | telemetry | Variable length | +| `VIDEO_TRANSMITTER` | 0x0F | to VTX | | +| `OPENTX_SYNC` | 0x10 | to handset | Legacy, superseded by `HANDSET` | +| `LINK_STATISTICS` | 0x14 | to FC | | +| `RC_CHANNELS_PACKED` | 0x16 | to FC / to TX | | +| `LINK_RX_ID` | 0x1C | telemetry | | +| `LINK_TX_ID` | 0x1D | telemetry | | +| `ATTITUDE` | 0x1E | telemetry | | +| `FLIGHT_MODE` | 0x21 | telemetry | | +| `DEVICE_PING` | 0x28 | extended | | +| `DEVICE_INFO` | 0x29 | extended | | +| `PARAMETER_SETTINGS_ENTRY` | 0x2B | extended | | +| `PARAMETER_READ` | 0x2C | extended | | +| `PARAMETER_WRITE` | 0x2D | extended | | +| `ELRS_STATUS` | 0x2E | extended | ELRS specific | +| `COMMAND` | 0x32 | extended | | +| `HANDSET` | 0x3A | extended | Named `RADIO_ID` in older firmwares | +| `KISS_REQ` | 0x78 | extended | | +| `KISS_RESP` | 0x79 | extended | | +| `MSP_REQ` | 0x7A | extended | | +| `MSP_RESP` | 0x7B | extended | | +| `MSP_WRITE` | 0x7C | extended | | +| `ARDUPILOT_RESP` | 0x80 | extended | | + +All multi-byte values are big endian unless stated otherwise. + +## Payloads + +### GPS (0x02) + +| Field | Type | Units | +| --- | --- | --- | +| latitude | int32 | degrees / 10,000,000 | +| longitude | int32 | degrees / 10,000,000 | +| groundspeed | uint16 | km/h / 10 | +| heading | uint16 | degrees / 100 | +| altitude | uint16 | metres + 1000 | +| satellites | uint8 | count | + +Heading is degrees times 100, so a full 0-360 degrees maps to 0-36000. Sending +a larger scale factor overflows the field. + +### GPS_TIME (0x03) + +Synchronises the handset clock. Sent by a flight controller such as Betaflight +2026.06+, and requires ELRS 4.1+ to pass through to the handset. + +| Field | Type | Units | +| --- | --- | --- | +| year | int16 | | +| month | uint8 | 1-12 | +| day | uint8 | 1-31 | +| hour | uint8 | 0-23 | +| minute | uint8 | 0-59 | +| second | uint8 | 0-59 | +| millisecond | uint16 | 0-999 | + +### VARIO (0x07) + +| Field | Type | Units | +| --- | --- | --- | +| verticalspd | int16 | cm/s | + +### BATTERY_SENSOR (0x08) + +| Field | Type | Units | +| --- | --- | --- | +| voltage | uint16 | volts * 10 | +| current | uint16 | amps * 10 | +| capacity | uint24 | mAh | +| remaining | uint8 | percent | + +Capacity is a 24 bit field, so the maximum is 16,777,215 mAh. + +### BARO_ALTITUDE (0x09) + +| Field | Type | Units | +| --- | --- | --- | +| altitude | uint16 | decimetres + 10000, or metres if the high bit is set | +| verticalspd | int16 | cm/s, optional | + +The receiving side decides what the frame contains from its declared length: +a 2 byte payload is altitude only, 3 bytes adds a TBS style single byte +vertical speed, and 4 bytes adds the ELRS style int16 vertical speed above. + +### AIRSPEED (0x0A) + +| Field | Type | Units | +| --- | --- | --- | +| speed | uint16 | km/h * 10 | + +### HEARTBEAT (0x0B) + +Announces a device so the CRSF router can discover it. + +| Field | Type | Units | +| --- | --- | --- | +| origin | int16 | address of the sending device | + +### RPM (0x0C) + +Variable length: the number of values is derived from the frame length. + +| Field | Type | Units | +| --- | --- | --- | +| source_id | uint8 | 0 = motor 1, 1 = motor 2, etc. | +| rpm | int24 x 1-19 | RPM, negative means reverse | + +### TEMP (0x0D) + +Variable length. + +| Field | Type | Units | +| --- | --- | --- | +| source_id | uint8 | 0 = FC including ESCs, 1 = ambient, etc. | +| temperature | int16 x 1-20 | tenths of a degree Celsius | + +### CELLS (0x0E) + +Variable length. ELRS 4.0+ receivers with battery voltage sensing send this +with `source_id` 128 to report millivolt precision voltage. + +| Field | Type | Units | +| --- | --- | --- | +| source_id | uint8 | 0 = battery 1, 1 = battery 2, etc. | +| cell | uint16 x 1-29 | millivolts | + +### VIDEO_TRANSMITTER (0x0F) + +| Field | Type | +| --- | --- | +| origin | uint8 | +| status | uint8 | +| band_channel | uint8 | +| user_frequency | uint16 | +| pitmode_and_power | uint8 | + +### LINK_STATISTICS (0x14) + +| Field | Type | Units | +| --- | --- | --- | +| uplink_RSSI_1 | uint8 | dBm * -1 | +| uplink_RSSI_2 | uint8 | dBm * -1 | +| uplink_Link_quality | uint8 | percent | +| uplink_SNR | int8 | dB | +| active_antenna | uint8 | 0 or 1 | +| rf_Mode | uint8 | packet rate index | +| uplink_TX_Power | uint8 | power index | +| downlink_RSSI | uint8 | dBm * -1 | +| downlink_Link_quality | uint8 | percent | +| downlink_SNR | int8 | dB | + +### RC_CHANNELS_PACKED (0x16) + +Sixteen channels packed into 11 bits each, 22 bytes total. Values are 172 to +1811 for -100% to +100%, with 992 as centre. With extended limits enabled the +usable range widens to 0 to 1984. + +ELRS 4.0+ handsets running EdgeTX 2.11+ may append **one extra status byte** +after the channel data, making the payload 23 bytes: + +| Bit | Name | Meaning | +| --- | --- | --- | +| 0 | `CRSF_CHANNELS_STATUS_ARMED` | Commanded arm state in "Arm using Switch" mode | +| 1 | `CRSF_CHANNELS_STATUS_ARMING_MODE_CH5` | Arm from the channel 5 value instead of bit 0 | + +Receivers that predate this simply see a longer frame than they expect, so only +send the status byte to an ELRS 4.0+ transmitter module. + +### LINK_RX_ID (0x1C) + +| Field | Type | Units | +| --- | --- | --- | +| rxRssiPercent | uint8 | percent | +| rxRfPower | uint8 | power index | + +### LINK_TX_ID (0x1D) + +| Field | Type | Units | +| --- | --- | --- | +| txRssiPercent | uint8 | percent | +| txRfPower | uint8 | power index | +| txFps | uint8 | frames per second / 10 | + +### ATTITUDE (0x1E) + +| Field | Type | Units | +| --- | --- | --- | +| pitch | int16 | radians * 10000 | +| roll | int16 | radians * 10000 | +| yaw | int16 | radians * 10000 | + +These are signed: negative angles are normal and must not be treated as +unsigned. + +### FLIGHT_MODE (0x21) + +| Field | Type | +| --- | --- | +| mode | null terminated string | + +### DEVICE_PING (0x28) + +Extended header frame with no payload. Device discovery request, usually sent +to the broadcast address. Every device answers with `DEVICE_INFO`. + +Only ELRS 4.0+ receivers answer pings over the flight controller serial port. + +### DEVICE_INFO (0x29) + +Extended header frame. The response to a ping. + +| Field | Type | +| --- | --- | +| name | null terminated string | +| serialNo | uint32 | +| hardwareVer | uint32 | +| softwareVer | uint32 | +| fieldCnt | uint8, number of configuration parameters | +| parameterVersion | uint8 | + +### ELRS_STATUS (0x2E) + +Extended header frame sent by an ELRS transmitter module to the handset. + +| Field | Type | +| --- | --- | +| pktsBad | uint8 | +| pktsGood | uint16 | +| flags | uint8, see below | +| msg | null terminated warning string | + +| Bit | Meaning | +| --- | --- | +| 0 | Connected | +| 2 | Model mismatch warning | +| 3 | Armed warning | +| 5 | Error: change blocked while connected | +| 6 | Error: baud rate too low | + +### HANDSET (0x3A) + +Extended header frame, named `RADIO_ID` in older firmwares. The first payload +byte is a subcommand. Subcommand `0x10` is timing sync, sent by a transmitter +module to tell the handset how fast to send channel frames: + +| Field | Type | Units | +| --- | --- | --- | +| subCommand | uint8 | 0x10 for timing sync | +| rate | uint32 | requested packet interval, 0.1 us units | +| offset | int32 | phase correction, 0.1 us units | + +### Undocumented frames + +The payloads of `PARAMETER_SETTINGS_ENTRY` (0x2B), `PARAMETER_READ` (0x2C), +`PARAMETER_WRITE` (0x2D), `COMMAND` (0x32), the KISS frames (0x78, 0x79), the +MSP frames (0x7A to 0x7C) and `ARDUPILOT_RESP` (0x80) are not documented here. +This library does not decode them. + +## References + +- [ExpressLRS `crsf_protocol.h`](https://github.com/ExpressLRS/ExpressLRS/blob/master/src/include/crsf_protocol.h) - authoritative for ELRS frame definitions +- [EdgeTX `crossfire.cpp`](https://github.com/EdgeTX/edgetx/blob/main/radio/src/telemetry/crossfire.cpp) - authoritative for how telemetry is decoded and displayed +- [TBS CRSF specification](https://github.com/tbs-fpv/tbs-crsf-spec/blob/main/crsf.md) - the vendor specification diff --git a/README.md b/README.md index f7d0956..1f7c9d5 100644 --- a/README.md +++ b/README.md @@ -1,206 +1,129 @@ -# AlfredoCRSF - CSRF serial protocol Arduino library - -This library is based on CapnBry's CRSF code, it has been modified to match the format of standard Arduino Library. Keywords and example files included. It has also now been extended to support more telemetry packet types. Check out the example files to learn more. - -This library was designed for ELRS but should be compatible with any CRSF receiver. - -TODO: -* For now callbacks have been removed. May add them back or replace with a flag system to alert when packets come in. -* Improve battery telemetry example by using all 24 capacity bits. (currently just 16 bits are used) -* Lib supports BaroAltitude packets but EdgeTX seems to not be able to parse them if Altitude is included. -* GPS heading seems to have some overflow issues in EdgeTX. - -# Hardware requirements - -This library is designed for ESP32. CRSF works best when you can access Serial Hardware peripherals that can achieve high baudrates (up to 420000). At least two serial peripherals are preferred, it is best to leave an MCUs default serial peripherals (the Serial object) for printing and debugging, and a second high speed peripheral for CRSF. - -This library should work on other MCUS like ATmega32U4/RP2040/STM32 but these are untested, attempt at your own risk. Avoid weak MCUs like atmega328p. - -# CRSF protocol specification - -The CRSF protocol is not documented or maintained by one single entity. The following specification has been cobbled together from the ELRS, EdgeTX and OpenTX project codebases. - -## Packet Format -`[dest] [len] [type] [payload] [crc8]` - -### DEST - Destination address or "sync" byte -* CRSF_ADDRESS_CRSF_TRANSMITTER = (0xEE) //Going to the transmitter module -* CRSF_ADDRESS_RADIO_TRANSMITTER = (0xEA) //Going to the handset -* CRSF_ADDRESS_FLIGHT_CONTROLLER = (0xC8) //Going to the flight controller -* CRSF_ADDRESS_CRSF_RECEIVER = (0xEC) //Going to the receiver (from FC) - -### LEN - Length of bytes that follow -Overall packet length is PayloadLength+4 (dest, len, type, crc), or LEN+2 (dest, len). - -### TYPE - CRSF_FRAMETYPE -* CRSF_FRAMETYPE_GPS = 0x02, -* CRSF_FRAMETYPE_GPS_TIME = 0x03, -* CRSF_FRAMETYPE_VARIO = 0x07, -* CRSF_FRAMETYPE_BATTERY_SENSOR = 0x08, -* CRSF_FRAMETYPE_BARO_ALTITUDE = 0x09, -* CRSF_FRAMETYPE_AIRSPEED = 0x0A, -* CRSF_FRAMETYPE_HEARTBEAT = 0x0B, -* CRSF_FRAMETYPE_RPM = 0x0C, -* CRSF_FRAMETYPE_TEMP = 0x0D, -* CRSF_FRAMETYPE_CELLS = 0x0E, -* CRSF_FRAMETYPE_LINK_STATISTICS = 0x14, -* CRSF_FRAMETYPE_OPENTX_SYNC = 0x10, -* CRSF_FRAMETYPE_RADIO_ID = 0x3A, -* CRSF_FRAMETYPE_RC_CHANNELS_PACKED = 0x16, -* CRSF_FRAMETYPE_LINK_RX_ID = 0x1C, -* CRSF_FRAMETYPE_LINK_TX_ID = 0x1D, -* CRSF_FRAMETYPE_ATTITUDE = 0x1E, -* CRSF_FRAMETYPE_FLIGHT_MODE = 0x21, -// Extended Header Frames, range: 0x28 to 0x96 -* CRSF_FRAMETYPE_DEVICE_PING = 0x28, -* CRSF_FRAMETYPE_DEVICE_INFO = 0x29, -* CRSF_FRAMETYPE_PARAMETER_SETTINGS_ENTRY = 0x2B, -* CRSF_FRAMETYPE_PARAMETER_READ = 0x2C, -* CRSF_FRAMETYPE_PARAMETER_WRITE = 0x2D, -* CRSF_FRAMETYPE_ELRS_STATUS = 0x2E, -* CRSF_FRAMETYPE_COMMAND = 0x32, -// KISS frames -* CRSF_FRAMETYPE_KISS_REQ = 0x78, -* CRSF_FRAMETYPE_KISS_RESP = 0x79, -// MSP commands -* CRSF_FRAMETYPE_MSP_REQ = 0x7A, -* CRSF_FRAMETYPE_MSP_RESP = 0x7B, -* CRSF_FRAMETYPE_MSP_WRITE = 0x7C, -// Ardupilot frames -* CRSF_FRAMETYPE_ARDUPILOT_RESP = 0x80, - -### CRC - CRC8 using poly 0xD5 -Includes all bytes from type (buffer[2]) to end of payload. - -## Payload of each frametype -### CRSF_FRAMETYPE_GPS = 0x02 -* int32_t latitude; // degree / 10,000,000 big endian -* int32_t longitude; // degree / 10,000,000 big endian -* uint16_t groundspeed; // km/h / 10 big endian -* uint16_t heading; // GPS heading, degree/100 big endian -* uint16_t altitude; // meters, +1000m big endian -* uint8_t satellites; // satellites -### CRSF_FRAMETYPE_GPS_TIME = 0x03 -Used to synchronize the handset clock (sent by e.g. Betaflight 2026.06+, requires ELRS 4.1+ to pass through). -* int16_t year; // BigEndian -* uint8_t month; -* uint8_t day; -* uint8_t hour; -* uint8_t minute; -* uint8_t second; -* uint16_t millisecond; // BigEndian -### CRSF_FRAMETYPE_VARIO = 0x07 -* int16_t verticalspd; // Vertical speed in cm/s, BigEndian -### CRSF_FRAMETYPE_BATTERY_SENSOR = 0x08 -* unsigned voltage : 16; // V * 10 big endian -* unsigned current : 16; // A * 10 big endian -* unsigned capacity : 24; // mah big endian -* unsigned remaining : 8; // % -### CRSF_FRAMETYPE_BARO_ALTITUDE = 0x09 -* uint16_t altitude; // Altitude in decimeters + 10000dm, or Altitude in meters if high bit is set, BigEndian -* int16_t verticalspd; // Vertical speed in cm/s, BigEndian -### CRSF_FRAMETYPE_AIRSPEED = 0x0A -* uint16_t speed; // Airspeed in 0.1 * km/h (hectometers/h), BigEndian -### CRSF_FRAMETYPE_HEARTBEAT = 0x0B -* int16_t Origin Device address; // BigEndian (used for device discovery by the ELRS 4.0 CRSF router) -### CRSF_FRAMETYPE_RPM = 0x0C -Variable length, count of values determined by frame length. -* uint8_t source_id; // e.g. 0 = Motor 1, 1 = Motor 2, etc. -* int24_t rpm[1-19]; // Signed 24-bit RPM values BigEndian, negative = reverse -### CRSF_FRAMETYPE_TEMP = 0x0D -Variable length, count of values determined by frame length. -* uint8_t source_id; // e.g. 0 = FC including all ESCs, 1 = Ambient, etc. -* int16_t temperature[1-20]; // Deci-degrees Celsius BigEndian (250 = 25.0C) -### CRSF_FRAMETYPE_CELLS = 0x0E -Variable length, count of values determined by frame length. ELRS 4.0+ receivers with VBAT sensing send this with source_id 128 for millivolt-precision voltage. -* uint8_t source_id; // e.g. 0 = battery 1, 1 = battery 2, etc. -* uint16_t cell[1-29]; // Cell voltage in millivolts BigEndian (3850 = 3.850V) -### CRSF_FRAMETYPE_VIDEO_TRANSMITTER = 0x0F -* uint8_t Origin address; -* uint8_t Status; -* uint8_t Band_Channel; -* uint16_t User_Frequency; -* uint8_t PitMode_and_Power; -### CRSF_FRAMETYPE_LINK_STATISTICS = 0x14 -* uint8_t uplink_RSSI_1; -* uint8_t uplink_RSSI_2; -* uint8_t uplink_Link_quality; -* int8_t uplink_SNR; -* uint8_t active_antenna; -* uint8_t rf_Mode; -* uint8_t uplink_TX_Power; -* uint8_t downlink_RSSI; -* uint8_t downlink_Link_quality; -* int8_t downlink_SNR; -### CRSF_FRAMETYPE_OPENTX_SYNC = 0x10 -* ???? -### CRSF_FRAMETYPE_RADIO_ID = 0x3A -* uint16_t radioAddress; //should be 0xEA00? -* uint8_t timingCorrectionFrame; //should be 0x10? -* uint32_t update_interval; //what is this? -* int32_t offset; //what is this? -### CRSF_FRAMETYPE_RC_CHANNELS_PACKED = 0x16 -* unsigned ch0 : 11; -* unsigned ch1 : 11; -* unsigned ch2 : 11; -* unsigned ch3 : 11; -* unsigned ch4 : 11; -* unsigned ch5 : 11; -* unsigned ch6 : 11; -* unsigned ch7 : 11; -* unsigned ch8 : 11; -* unsigned ch9 : 11; -* unsigned ch10 : 11; -* unsigned ch11 : 11; -* unsigned ch12 : 11; -* unsigned ch13 : 11; -* unsigned ch14 : 11; -* unsigned ch15 : 11; - -ELRS 4.0+ handsets (EdgeTX 2.11+) may append one status byte after the packed channels: -* bit 0: CRSF_CHANNELS_STATUS_ARMED - commanded armed status in Arm using Switch mode -* bit 1: CRSF_CHANNELS_STATUS_ARMING_MODE_CH5 - arm via CH5 instead of the armed bit -### CRSF_FRAMETYPE_LINK_RX_ID = 0x1C -* uint8_t rxRssiPercent; -* uint8_t rxRfPower; //should be signed int? -### CRSF_FRAMETYPE_LINK_TX_ID = 0x1D -* uint8_t txRssiPercent; -* uint8_t txRfPower; //should be signed int? -* uint8_t txFps; -### CRSF_FRAMETYPE_ATTITUDE = 0x1E -* int16_t pitch; // pitch in radians * 10000, BigEndian -* int16_t roll; // roll in radians * 10000, BigEndian -* int16_t yaw; // yaw in radians * 10000, BigEndian -### CRSF_FRAMETYPE_FLIGHT_MODE = 0x21 -* char[]; //Flight mode ( Null-terminated string ) -// Extended Header Frames, range: 0x28 to 0x96 -### CRSF_FRAMETYPE_DEVICE_PING = 0x28, -* ???? -### CRSF_FRAMETYPE_DEVICE_INFO = 0x29, -* ???? -### CRSF_FRAMETYPE_PARAMETER_SETTINGS_ENTRY = 0x2B, -* ???? -### CRSF_FRAMETYPE_PARAMETER_READ = 0x2C, -* ???? -### CRSF_FRAMETYPE_PARAMETER_WRITE = 0x2D, -* ???? -### CRSF_FRAMETYPE_ELRS_STATUS = 0x2E, -Extended header frame (payload preceded by destination and origin address bytes). Sent by an ELRS TX module to the handset. -* uint8_t pktsBad; -* uint16_t pktsGood; // BigEndian -* uint8_t flags; // bit 0: connected, bit 2: model mismatch warning, bit 3: armed warning, bit 5: error - change blocked while connected, bit 6: error - baud rate too low -* char msg[]; // Warning message (null-terminated string) -### CRSF_FRAMETYPE_COMMAND = 0x32, -* ???? -// KISS frames -### CRSF_FRAMETYPE_KISS_REQ = 0x78, -* ???? -### CRSF_FRAMETYPE_KISS_RESP = 0x79, -* ???? -// MSP commands -### CRSF_FRAMETYPE_MSP_REQ = 0x7A, -* ???? -### CRSF_FRAMETYPE_MSP_RESP = 0x7B, -* ???? -### CRSF_FRAMETYPE_MSP_WRITE = 0x7C, -* ???? +# AlfredoCRSF + +An Arduino library for the CRSF (Crossfire) serial protocol. Talk to an +ExpressLRS or TBS Crossfire receiver from a microcontroller: read stick and +switch positions, monitor link quality, and send telemetry back to the handset. + +Originally based on CapnBry's CRSF code, restructured as a standard Arduino +library and extended with support for many more packet types. + +## Features + +The same sketch runs on both ELRS generations. Features that need a newer +firmware are simply inert on an older link, never broken: frames that do not +exist on 3.x never arrive, and their getters stay zeroed. + +| Feature | ELRS 3.x | ELRS 4.x | +| --- | :---: | :---: | +| Receive RC channels | ✅ | ✅ | +| Link statistics (RSSI, LQ, SNR, TX power) | ✅ | ✅ | +| Link state tracking with failsafe timeout | ✅ | ✅ | +| Send telemetry: battery, GPS, vario, barometric altitude, attitude | ✅ | ✅ | +| Arm state from the channel 5 position | ✅ | ✅ | +| Handset timing sync (frame rate and phase) | ✅ | ✅ | +| Airspeed, RPM, temperature and cell voltage telemetry | ✅ | ✅ | +| Millivolt battery voltage reported by the receiver | ❌ | ✅ | +| Arm state from the channels status byte | ❌ | ✅ | +| ELRS status: packet counts, warning flags, messages | ❌ | ✅ | +| CRSF router participation: heartbeat and device discovery | ❌ | ✅ | +| GPS time forwarded to the handset clock | ❌ | ✅ | + + +There are no packet callbacks. Call `update()` in your loop and read the +latest values from the getters whenever you need them. + +## Hardware requirements + +Designed for the ESP32. CRSF runs at up to 420000 baud, so it needs a hardware +serial peripheral; software serial will not keep up. + +Two serial peripherals are strongly preferred: leave the default `Serial` for +printing and debugging, and use a second high speed peripheral for CRSF. + +Other MCUs such as the ATmega32U4, RP2040 and STM32 should work but are +untested. Avoid weak MCUs like the ATmega328P. + +## Installation + +Search for "AlfredoCRSF" in the Arduino IDE Library Manager, or clone this +repository into your Arduino `libraries` folder. + +## Quick start + +```cpp +#include +#include + +#define PIN_RX 18 +#define PIN_TX 17 + +HardwareSerial crsfSerial(1); +AlfredoCRSF crsf; + +void setup() +{ + Serial.begin(115200); + crsfSerial.begin(CRSF_BAUDRATE, SERIAL_8N1, PIN_RX, PIN_TX); + crsf.begin(crsfSerial); +} + +void loop() +{ + crsf.update(); // must be called regularly + + if (crsf.isLinkUp()) + { + Serial.print("throttle: "); + Serial.println(crsf.getChannel(3)); // channels are 1 based, value in us + } +} +``` + +## Examples + +| Example | ELRS 3.x | ELRS 4.x | What it does | +| --- | :---: | :---: | --- | +| `printAllChannels` | ✅ | ✅ | Print all 16 channels. Start here | +| `linkStatusLed` | ✅ | ✅ | Drive an LED from link state | +| `sendTelemetryBattery` | ✅ | ✅ | Measure a voltage divider and report battery telemetry | +| `sendTelemetryGpsBaroVarioAttitude` | ✅ | ✅ | Send GPS, altitude, vario and attitude. The GPS time packet needs 4.1+ | +| `forwardChannelsToFC` | ✅ | ✅ | Two receivers with failover, forwarding channels onward | +| `forwardPacketsFromHandset` | ✅ | ✅ | Read packets on the handset side of the link | +| `elrs4SelfTest` | ❌ | ✅ | Functional self test needing no radio at all: cross wire two UARTs and check every parser and sender | +| `elrs4ReceiverTest` | ❌ | ✅ | Bench test a receiver. On 3.x the newer telemetry sections stay silent, which is the compatibility check | +| `handsetEmulator` | ✅ | ✅ | Drive a TX module the way a handset does. Set `ARM_WITH_STATUS_BYTE` to 0 for 3.x | + +## Compatibility notes + +- **A receiver that binds but sends nothing** is the most common problem, and + it looks exactly like a wiring fault. Three things cause it: + - **Serial output not enabled.** On receivers with configurable IO, such as + the ER series, the pins have to be assigned a serial protocol in the ELRS + configurator before anything comes out of them. + - **Model match.** A mismatched model ID makes the receiver suppress its + entire serial output while still showing as connected. When driving a + transmitter module yourself, send the model ID with `sendModelId()` the way + a handset does, or turn model match off. + - **No radio link.** ELRS 3.x receivers stay silent until they connect to a + transmitter, so bench tests need the transmitter powered and bound. +- **The arming status byte** on channel frames is ELRS 4.0 with EdgeTX 2.11 or + newer. Sending it to a 3.x transmitter module produces a frame it does not + understand. +- **GPS time** forwarding to the handset needs ELRS 4.1+. + +## Protocol specification + +The wire format, frame types and payload layouts are documented in +[CRSF_PROTOCOL.md](CRSF_PROTOCOL.md). + +## References + +- [ExpressLRS](https://github.com/ExpressLRS/ExpressLRS) and its + [documentation](https://www.expresslrs.org/) +- [EdgeTX](https://github.com/EdgeTX/edgetx) +- [TBS CRSF specification](https://github.com/tbs-fpv/tbs-crsf-spec/blob/main/crsf.md) +- [CapnBry's CRSF work](https://github.com/CapnBry/CRServoF), the origin of this library + +## License + +GPL-3.0. See [LICENSE](LICENSE). diff --git a/examples/elrs4ReceiverTest/elrs4ReceiverTest.ino b/examples/elrs4ReceiverTest/elrs4ReceiverTest.ino new file mode 100644 index 0000000..f2bd12e --- /dev/null +++ b/examples/elrs4ReceiverTest/elrs4ReceiverTest.ino @@ -0,0 +1,134 @@ +// Bench test for an ELRS 4.x receiver connected as a flight controller would +// be. Prints channels, link stats, and all the newer telemetry sensors the +// receiver can produce, and joins the CRSF network as a discoverable device: +// with a 4.0 TX/RX pair this sketch should show up in the ExpressLRS Lua +// under Other Devices, and a VBAT-sensing receiver should produce a CELLS +// frame (source id 128) with millivolt precision. + +#include +#include + +#define PIN_RX 4 +#define PIN_TX 5 + +HardwareSerial crsfSerial(1); +AlfredoCRSF crsf; + +uint32_t lastHeartbeatMs = 0; +uint32_t lastPrintMs = 0; + +void setup() +{ + Serial.begin(115200); + Serial.println("ELRS 4.x receiver test"); + + crsfSerial.begin(CRSF_BAUDRATE, SERIAL_8N1, PIN_RX, PIN_TX); + if (!crsfSerial) while (1) Serial.println("Invalid crsfSerial configuration"); + + crsf.begin(crsfSerial); // defaults to CRSF_ADDRESS_FLIGHT_CONTROLLER + crsf.setDeviceName("AlfredoCRSF"); // answer device discovery pings +} + +void loop() +{ + crsf.update(); + + // Announce ourselves to the CRSF router once per second + if (millis() - lastHeartbeatMs > 1000) + { + lastHeartbeatMs = millis(); + crsf.sendHeartbeat(); + } + + if (millis() - lastPrintMs > 1000) + { + lastPrintMs = millis(); + printEverything(); + } +} + +void printEverything() +{ + Serial.println("----------------------------------------"); + Serial.print("link: "); + Serial.print(crsf.isLinkUp() ? "UP" : "DOWN"); + const crsfLinkStatistics_t *link = crsf.getLinkStatistics(); + Serial.print(" LQ: "); + Serial.print(link->uplink_Link_quality); + Serial.print(" RSSI: -"); + Serial.print(link->active_antenna == 0 ? link->uplink_RSSI_1 : link->uplink_RSSI_2); + Serial.print("dBm armed: "); + Serial.println(crsf.isArmed() ? "yes" : "no"); + + Serial.print("channels 1-8:"); + for (int i = 1; i <= 8; i++) + { + Serial.print(" "); + Serial.print(crsf.getChannel(i)); + } + Serial.println(""); + + // ELRS 4.0: VBAT receivers send millivolt-precision voltage as CELLS + const crsf_sensor_cells_t *cells = crsf.getCellsSensor(); + if (cells->cell_count > 0) + { + Serial.print("cells (source "); + Serial.print(cells->source_id); + Serial.print("):"); + for (int i = 0; i < cells->cell_count; i++) + { + Serial.print(" "); + Serial.print(cells->cell[i]); + Serial.print("mV"); + } + Serial.println(""); + } + + const crsf_sensor_temp_t *temp = crsf.getTempSensor(); + if (temp->temp_count > 0) + { + Serial.print("temps (deci-C):"); + for (int i = 0; i < temp->temp_count; i++) + { + Serial.print(" "); + Serial.print(temp->temperature[i]); + } + Serial.println(""); + } + + const crsf_sensor_rpm_t *rpm = crsf.getRpmSensor(); + if (rpm->rpm_count > 0) + { + Serial.print("rpm:"); + for (int i = 0; i < rpm->rpm_count; i++) + { + Serial.print(" "); + Serial.print(rpm->rpm[i]); + } + Serial.println(""); + } + + if (crsf.getAirspeedSensor()->speed != 0) + { + Serial.print("airspeed: "); + Serial.print(crsf.getAirspeedSensor()->speed / 10.0); + Serial.println(" km/h"); + } + + const crsf_sensor_gps_time_t *gpsTime = crsf.getGpsTimeSensor(); + if (gpsTime->year != 0) + { + Serial.print("gps time: "); + Serial.print(gpsTime->year); + Serial.print("-"); + Serial.print(gpsTime->month); + Serial.print("-"); + Serial.print(gpsTime->day); + Serial.print(" "); + Serial.print(gpsTime->hour); + Serial.print(":"); + Serial.print(gpsTime->minute); + Serial.print(":"); + Serial.println(gpsTime->second); + } +} diff --git a/examples/elrs4SelfTest/elrs4SelfTest.ino b/examples/elrs4SelfTest/elrs4SelfTest.ino new file mode 100644 index 0000000..207828f --- /dev/null +++ b/examples/elrs4SelfTest/elrs4SelfTest.ino @@ -0,0 +1,218 @@ +// AlfredoCRSF 2.0 / ELRS 4.0 feature self-test. No radio hardware needed: +// two UARTs on one ESP32 are cross-wired and two AlfredoCRSF instances talk +// to each other, one playing the handset and one playing a TX module. +// +// Wiring (two jumpers): +// PIN_TX_HANDSET (19) -> PIN_RX_MODULE (25) +// PIN_TX_MODULE (26) -> PIN_RX_HANDSET (18) +// +// Open the serial monitor at 115200; each check prints PASS or FAIL. + +#include +#include + +#define PIN_RX_HANDSET 18 +#define PIN_TX_HANDSET 19 +#define PIN_RX_MODULE 25 +#define PIN_TX_MODULE 26 + +HardwareSerial serialHandset(1); +HardwareSerial serialModule(2); +AlfredoCRSF crsfHandset; +AlfredoCRSF crsfModule; + +int passCount = 0; +int failCount = 0; + +void setup() +{ + Serial.begin(115200); + delay(1000); + Serial.println("AlfredoCRSF ELRS 4.0 self-test"); + + serialHandset.begin(CRSF_BAUDRATE, SERIAL_8N1, PIN_RX_HANDSET, PIN_TX_HANDSET); + serialModule.begin(CRSF_BAUDRATE, SERIAL_8N1, PIN_RX_MODULE, PIN_TX_MODULE); + + crsfHandset.begin(serialHandset, CRSF_ADDRESS_RADIO_TRANSMITTER); + crsfModule.begin(serialModule, CRSF_ADDRESS_CRSF_TRANSMITTER); + crsfModule.setDeviceName("SelfTestModule"); + + runTests(); + + Serial.println(""); + Serial.print("Result: "); + Serial.print(passCount); + Serial.print(" passed, "); + Serial.print(failCount); + Serial.println(" failed"); +} + +void loop() +{ +} + +void runTests() +{ + crsf_channels_t ch = { 0 }; + ch.ch0 = CRSF_CHANNEL_VALUE_1000; // getChannel(1) -> 1000 + ch.ch1 = CRSF_CHANNEL_VALUE_MID; // getChannel(2) -> 1500 + ch.ch2 = CRSF_CHANNEL_VALUE_2000; // getChannel(3) -> 2000 + ch.ch4 = CRSF_CHANNEL_VALUE_1000; // CH5 (arm channel) low + + // --- Plain channels frame (ELRS 3.x compatible, no status byte) --- + crsfHandset.writeChannels(CRSF_ADDRESS_CRSF_TRANSMITTER, &ch); + pump(20); + check("channels: ch1=1000", crsfModule.getChannel(1) == 1000); + check("channels: ch2=1500", crsfModule.getChannel(2) == 1500); + check("channels: ch3=2000", crsfModule.getChannel(3) == 2000); + check("channels: link up", crsfModule.isLinkUp()); + check("channels: no status byte", !crsfModule.hasChannelsStatus()); + check("channels: not armed (CH5 low)", !crsfModule.isArmed()); + + // --- ELRS 4.0 status byte, Arm using Switch mode --- + crsfHandset.writeChannels(CRSF_ADDRESS_CRSF_TRANSMITTER, &ch, CRSF_CHANNELS_STATUS_ARMED); + pump(20); + check("status byte: detected", crsfModule.hasChannelsStatus()); + check("status byte: armed via switch", crsfModule.isArmed()); + + // --- Status byte with the CH5-mode bit: CH5 value wins over the armed bit --- + crsfHandset.writeChannels(CRSF_ADDRESS_CRSF_TRANSMITTER, &ch, + CRSF_CHANNELS_STATUS_ARMED | CRSF_CHANNELS_STATUS_ARMING_MODE_CH5); + pump(20); + check("CH5 mode: not armed while CH5 low", !crsfModule.isArmed()); + ch.ch4 = CRSF_CHANNEL_VALUE_2000; + crsfHandset.writeChannels(CRSF_ADDRESS_CRSF_TRANSMITTER, &ch, + CRSF_CHANNELS_STATUS_ARMING_MODE_CH5); + pump(20); + check("CH5 mode: armed while CH5 high", crsfModule.isArmed()); + + // --- Back to a plain frame: status byte state must clear --- + crsfHandset.writeChannels(CRSF_ADDRESS_CRSF_TRANSMITTER, &ch); + pump(20); + check("status byte: cleared by plain frame", !crsfModule.hasChannelsStatus()); + + // --- GPS time telemetry --- + crsf_sensor_gps_time_t gpsTime = { 0 }; + gpsTime.year = htobe16(2026); + gpsTime.month = 7; + gpsTime.day = 14; + gpsTime.hour = 12; + gpsTime.minute = 34; + gpsTime.second = 56; + gpsTime.millisecond = htobe16(789); + crsfHandset.writePacket(CRSF_SYNC_BYTE, CRSF_FRAMETYPE_GPS_TIME, &gpsTime, sizeof(gpsTime)); + pump(20); + check("gps time: year", crsfModule.getGpsTimeSensor()->year == 2026); + check("gps time: millisecond", crsfModule.getGpsTimeSensor()->millisecond == 789); + + // --- Cells telemetry (variable length, millivolt cell voltages) --- + uint8_t cellsPayload[] = { 128, 0x0F, 0x0A, 0x0F, 0x14 }; // source 128, 3850mV, 3860mV + crsfHandset.writePacket(CRSF_SYNC_BYTE, CRSF_FRAMETYPE_CELLS, cellsPayload, sizeof(cellsPayload)); + pump(20); + check("cells: source id", crsfModule.getCellsSensor()->source_id == 128); + check("cells: count", crsfModule.getCellsSensor()->cell_count == 2); + check("cells: values", crsfModule.getCellsSensor()->cell[0] == 3850 && + crsfModule.getCellsSensor()->cell[1] == 3860); + + // --- RPM telemetry (24-bit signed values, test sign extension) --- + uint8_t rpmPayload[] = { 3, 0xFF, 0xFE, 0x0C }; // source 3, one value: -500 + crsfHandset.writePacket(CRSF_SYNC_BYTE, CRSF_FRAMETYPE_RPM, rpmPayload, sizeof(rpmPayload)); + pump(20); + check("rpm: count", crsfModule.getRpmSensor()->rpm_count == 1); + check("rpm: negative value", crsfModule.getRpmSensor()->rpm[0] == -500); + + // --- Temperature telemetry --- + uint8_t tempPayload[] = { 1, 0x00, 0xFA, 0xFF, 0xCE }; // source 1, 25.0C, -5.0C + crsfHandset.writePacket(CRSF_SYNC_BYTE, CRSF_FRAMETYPE_TEMP, tempPayload, sizeof(tempPayload)); + pump(20); + check("temp: values", crsfModule.getTempSensor()->temperature[0] == 250 && + crsfModule.getTempSensor()->temperature[1] == -50); + + // --- ELRS_STATUS from the module to the handset (extended header frame) --- + uint8_t statusPayload[] = { 1, 0x01, 0x02, CRSF_ELRS_FLAG_CONNECTED, 'O', 'K', '\0' }; + crsfModule.writeExtPacket(CRSF_FRAMETYPE_ELRS_STATUS, CRSF_ADDRESS_RADIO_TRANSMITTER, + statusPayload, sizeof(statusPayload)); + pump(20); + check("elrs status: packet counts", crsfHandset.getElrsStatus()->pktsBad == 1 && + crsfHandset.getElrsStatus()->pktsGood == 258); + check("elrs status: flags", crsfHandset.getElrsStatus()->flags == CRSF_ELRS_FLAG_CONNECTED); + check("elrs status: message", strcmp(crsfHandset.getElrsStatus()->msg, "OK") == 0); + + // --- HANDSET timing sync from the module (extended header frame) --- + uint8_t timingPayload[] = { CRSF_HANDSET_SUBCMD_TIMING, + 0x00, 0x00, 0x9C, 0x40, // rate: 40000 (4ms in 0.1us units) + 0xFF, 0xFF, 0xFF, 0x9C }; // offset: -100 + crsfModule.writeExtPacket(CRSF_FRAMETYPE_HANDSET, CRSF_ADDRESS_RADIO_TRANSMITTER, + timingPayload, sizeof(timingPayload)); + pump(20); + check("handset timing: rate", crsfHandset.getHandsetTiming()->rate == 40000); + check("handset timing: offset", crsfHandset.getHandsetTiming()->offset == -100); + + // --- Device discovery: ping the module, expect a DEVICE_INFO response. + // The response is checked as raw bytes since the handset side has no + // DEVICE_INFO parser (yet) --- + drain(serialHandset); + uint8_t none = 0; + crsfHandset.writeExtPacket(CRSF_FRAMETYPE_DEVICE_PING, CRSF_ADDRESS_BROADCAST, &none, 0); + pumpOnly(crsfModule, 20); + check("device ping: DEVICE_INFO response", sawFrameType(serialHandset, CRSF_FRAMETYPE_DEVICE_INFO)); + + // --- Heartbeat (raw check on the wire) --- + drain(serialHandset); + crsfModule.sendHeartbeat(); + delay(20); + check("heartbeat: frame on the wire", sawFrameType(serialHandset, CRSF_FRAMETYPE_HEARTBEAT)); +} + +// Run both parsers for a while so frames propagate +void pump(uint32_t ms) +{ + uint32_t start = millis(); + while (millis() - start < ms) + { + crsfHandset.update(); + crsfModule.update(); + delay(1); + } +} + +// Run only one parser (so the other side's RX bytes stay in the buffer for raw checks) +void pumpOnly(AlfredoCRSF &crsf, uint32_t ms) +{ + uint32_t start = millis(); + while (millis() - start < ms) + { + crsf.update(); + delay(1); + } +} + +void drain(Stream &port) +{ + while (port.available()) + port.read(); +} + +// Scan raw bytes on a port for a frame of the given type (sync byte two +// bytes before the type byte) +bool sawFrameType(Stream &port, uint8_t type) +{ + uint8_t buf[128]; + size_t n = 0; + while (port.available() && n < sizeof(buf)) + buf[n++] = port.read(); + for (size_t i = 2; i < n; i++) + { + if (buf[i] == type && buf[i - 2] == CRSF_SYNC_BYTE) + return true; + } + return false; +} + +void check(const char *name, bool ok) +{ + Serial.print(ok ? "PASS: " : "FAIL: "); + Serial.println(name); + if (ok) passCount++; + else failCount++; +} diff --git a/examples/forwardChannelsToFC/forwardChannelsToFC.ino b/examples/forwardChannelsToFC/forwardChannelsToFC.ino index 845177a..43aac0f 100644 --- a/examples/forwardChannelsToFC/forwardChannelsToFC.ino +++ b/examples/forwardChannelsToFC/forwardChannelsToFC.ino @@ -72,8 +72,7 @@ int getLinkQuality(AlfredoCRSF& crsf) { // Method to send channels based on CRSF instance void sendChannels(AlfredoCRSF& crsf) { - const crsf_channels_t* channels_ptr = crsf.getChannelsPacked(); - crsfOut.writePacket(CRSF_SYNC_BYTE, CRSF_FRAMETYPE_RC_CHANNELS_PACKED, channels_ptr, sizeof(*channels_ptr)); + crsfOut.writeChannels(CRSF_SYNC_BYTE, crsf.getChannelsPacked()); } // Fallback method to send default channel values @@ -96,5 +95,5 @@ void sendFallbackChannels() { crsfChannels.ch14 = CRSF_CHANNEL_VALUE_1000; crsfChannels.ch15 = CRSF_CHANNEL_VALUE_1000; - crsfOut.writePacket(CRSF_SYNC_BYTE, CRSF_FRAMETYPE_RC_CHANNELS_PACKED, &crsfChannels, sizeof(crsfChannels)); + crsfOut.writeChannels(CRSF_SYNC_BYTE, &crsfChannels); } diff --git a/examples/handsetEmulator/handsetEmulator.ino b/examples/handsetEmulator/handsetEmulator.ino new file mode 100644 index 0000000..f67a047 --- /dev/null +++ b/examples/handsetEmulator/handsetEmulator.ino @@ -0,0 +1,147 @@ +// Drives a real ELRS TX module the way a handset would: sends paced channels +// frames, follows the module's requested frame rate, and prints the module's +// ELRS status. Demonstrates the ELRS 4.0 "Arm using Switch" status byte. +// +// Wiring: ELRS TX modules use a single half-duplex data line (the S.Port-style +// pin in the module bay), non-inverted for ELRS. For a bench setup connect the +// module's data pin to PIN_RX directly and to PIN_TX through a ~1k resistor. +// Most modules auto-detect the handset baud rate; 400000 is the common default. +// +// Set ARM_WITH_STATUS_BYTE to 1 only with an ELRS 4.0+ module configured for +// "Arm using Switch" - 3.x modules do not understand the longer channels frame. + +#include +#include + +#define PIN_RX 18 +#define PIN_TX 19 +#define HANDSET_BAUD 400000 + +#define ARM_WITH_STATUS_BYTE 1 + +// Model ID sent to the module at startup, the way a handset does. +// +// 0xFF means "no model match" and is the safe default: ELRS inverts the model +// ID and masks it to 6 bits before mixing it into the link, so 0xFF cancels +// out and any receiver accepts it. Use 0 to 63 only if you actually run model +// match, and then it has to agree with what the receiver was bound to, or the +// receiver stays connected while going completely silent on its serial port. +#define MODEL_ID 0xFF + +HardwareSerial crsfSerial(1); +AlfredoCRSF crsf; + +uint32_t lastChannelsMicros = 0; +uint32_t lastPrintMs = 0; +uint32_t lastArmToggleMs = 0; +bool armed = false; + +void setup() +{ + Serial.begin(115200); + Serial.println("ELRS handset emulator"); + + crsfSerial.begin(HANDSET_BAUD, SERIAL_8N1, PIN_RX, PIN_TX); + if (!crsfSerial) while (1) Serial.println("Invalid crsfSerial configuration"); + + // We are the handset, so extended frames addressed to the radio are for us + crsf.begin(crsfSerial, CRSF_ADDRESS_RADIO_TRANSMITTER); + + // Handsets announce the selected model when they connect. Give the module a + // moment to be ready, then tell it. If you power the module up after this + // sketch, reset the board so it hears the model ID. + delay(500); + crsf.sendModelId(MODEL_ID); + Serial.print("Sent model ID "); + Serial.println(MODEL_ID); +} + +void loop() +{ + crsf.update(); + + // Toggle the demo arm state every 5 seconds + if (millis() - lastArmToggleMs > 5000) + { + lastArmToggleMs = millis(); + armed = !armed; + Serial.print("Commanded arm state: "); + Serial.println(armed ? "ARMED" : "disarmed"); + } + + // Pace channels frames at the rate the module asks for via its timing sync + // frames (0.1us units); default to 4ms until one has been received + uint32_t intervalUs = 4000; + if (crsf.getHandsetTiming()->rate != 0) + intervalUs = crsf.getHandsetTiming()->rate / 10; + if (micros() - lastChannelsMicros >= intervalUs) + { + lastChannelsMicros = micros(); + sendChannels(); + } + + // Once per second, print what the module reports + if (millis() - lastPrintMs > 1000) + { + lastPrintMs = millis(); + printModuleStatus(); + } +} + +void sendChannels() +{ + // Every channel needs a value inside the valid CRSF range of 172 to 1811. + crsf_channels_t ch = { 0 }; + ch.ch0 = CRSF_CHANNEL_VALUE_MID; // aileron center + ch.ch1 = CRSF_CHANNEL_VALUE_MID; // elevator center + ch.ch2 = CRSF_CHANNEL_VALUE_1000; // throttle low + ch.ch3 = CRSF_CHANNEL_VALUE_MID; // rudder center + ch.ch4 = armed ? CRSF_CHANNEL_VALUE_2000 : CRSF_CHANNEL_VALUE_1000; // CH5/AUX1 + ch.ch5 = CRSF_CHANNEL_VALUE_1000; + ch.ch6 = CRSF_CHANNEL_VALUE_1000; + ch.ch7 = CRSF_CHANNEL_VALUE_1000; + ch.ch8 = CRSF_CHANNEL_VALUE_1000; + ch.ch9 = CRSF_CHANNEL_VALUE_1000; + ch.ch10 = CRSF_CHANNEL_VALUE_1000; + ch.ch11 = CRSF_CHANNEL_VALUE_1000; + ch.ch12 = CRSF_CHANNEL_VALUE_1000; + ch.ch13 = CRSF_CHANNEL_VALUE_1000; + ch.ch14 = CRSF_CHANNEL_VALUE_1000; + ch.ch15 = CRSF_CHANNEL_VALUE_1000; + +#if ARM_WITH_STATUS_BYTE + // ELRS 4.0 Arm using Switch: arm state travels in the status byte + crsf.writeChannels(CRSF_ADDRESS_CRSF_TRANSMITTER, &ch, + armed ? CRSF_CHANNELS_STATUS_ARMED : 0); +#else + // Classic (ELRS 3.x compatible): arm state is just the CH5 value + crsf.writeChannels(CRSF_ADDRESS_CRSF_TRANSMITTER, &ch); +#endif +} + +void printModuleStatus() +{ + const crsf_elrs_status_t *status = crsf.getElrsStatus(); + Serial.print("pktsGood: "); + Serial.print(status->pktsGood); + Serial.print(" pktsBad: "); + Serial.print(status->pktsBad); + Serial.print(" connected: "); + Serial.print((status->flags & CRSF_ELRS_FLAG_CONNECTED) ? "yes" : "no"); + Serial.print(" armed flag: "); + Serial.print((status->flags & CRSF_ELRS_FLAG_ARMED) ? "yes" : "no"); + if (status->msg[0]) + { + Serial.print(" msg: "); + Serial.print(status->msg); + } + Serial.print(" frame interval: "); + Serial.print(crsf.getHandsetTiming()->rate / 10); + Serial.println("us"); + + const crsfLinkStatistics_t *link = crsf.getLinkStatistics(); + Serial.print(" downlink LQ: "); + Serial.print(link->downlink_Link_quality); + Serial.print(" uplink LQ: "); + Serial.println(link->uplink_Link_quality); +} diff --git a/examples/sendTelemetryBattery/sendTelemetryBattery.ino b/examples/sendTelemetryBattery/sendTelemetryBattery.ino index 3a258b4..fc1027c 100644 --- a/examples/sendTelemetryBattery/sendTelemetryBattery.ino +++ b/examples/sendTelemetryBattery/sendTelemetryBattery.ino @@ -50,7 +50,7 @@ static void sendRxBattery(float voltage, float current, float capacity, float re // Values are MSB first (BigEndian) crsfBatt.voltage = htobe16((uint16_t)(voltage * 10.0)); //Volts crsfBatt.current = htobe16((uint16_t)(current * 10.0)); //Amps - crsfBatt.capacity = htobe16((uint16_t)(capacity)) << 8; //mAh (with this implemetation max capacity is 65535mAh) + crsfBatt.capacity = htobe24((uint32_t)(capacity)); //mAh (24 bit field, max 16777215mAh) crsfBatt.remaining = (uint8_t)(remaining); //percent crsf.queuePacket(CRSF_SYNC_BYTE, CRSF_FRAMETYPE_BATTERY_SENSOR, &crsfBatt, sizeof(crsfBatt)); } \ No newline at end of file diff --git a/examples/sendTelemetryGpsBaroVarioAttitude/sendTelemetryGpsBaroVarioAttitude.ino b/examples/sendTelemetryGpsBaroVarioAttitude/sendTelemetryGpsBaroVarioAttitude.ino index 9c7faed..d234cfe 100644 --- a/examples/sendTelemetryGpsBaroVarioAttitude/sendTelemetryGpsBaroVarioAttitude.ino +++ b/examples/sendTelemetryGpsBaroVarioAttitude/sendTelemetryGpsBaroVarioAttitude.ino @@ -38,7 +38,7 @@ void sendGps(float latitude, float longitude, float groundspeed, float heading, crsfGps.latitude = htobe32((int32_t)(latitude*10000000.0)); crsfGps.longitude = htobe32((int32_t)(longitude*10000000.0)); crsfGps.groundspeed = htobe16((uint16_t)(groundspeed*10.0)); - crsfGps.heading = htobe16((int16_t)(heading*1000.0)); //TODO: heading seems to not display in EdgeTX correctly, some kind of overflow error + crsfGps.heading = htobe16((uint16_t)(heading*100.0)); //degrees * 100, so 0-360 degrees fits in 0-36000 crsfGps.altitude = htobe16((uint16_t)(altitude + 1000.0)); crsfGps.satellites = (uint8_t)(satellites); crsf.queuePacket(CRSF_SYNC_BYTE, CRSF_FRAMETYPE_GPS, &crsfGps, sizeof(crsfGps)); @@ -61,21 +61,28 @@ void sendGpsTime(int16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t crsf.queuePacket(CRSF_SYNC_BYTE, CRSF_FRAMETYPE_GPS_TIME, &crsfGpsTime, sizeof(crsfGpsTime)); } +// Sends altitude and vertical speed together in one BaroAltitude packet. +// EdgeTX decides what the packet contains from its length: a 2 byte payload +// is altitude only, and a 4 byte payload adds ELRS style vertical speed. +// Very old EdgeTX versions only understand the altitude, in which case send +// vertical speed separately with sendVario() below. void sendBaroAltitude(float altitude, float verticalspd) { crsf_sensor_baro_altitude_t crsfBaroAltitude = { 0 }; // Values are MSB first (BigEndian) - crsfBaroAltitude.altitude = htobe16((uint16_t)(altitude*10.0 + 10000.0)); - //crsfBaroAltitude.verticalspd = htobe16((int16_t)(verticalspd*100.0)); //TODO: fix verticalspd in BaroAlt packets - crsf.queuePacket(CRSF_SYNC_BYTE, CRSF_FRAMETYPE_BARO_ALTITUDE, &crsfBaroAltitude, sizeof(crsfBaroAltitude) - 2); - - //Supposedly vertical speed can be sent in a BaroAltitude packet, but I cant get this to work. - //For now I have to send a second vario packet to get vertical speed telemetry to my TX. + crsfBaroAltitude.altitude = htobe16((uint16_t)(altitude*10.0 + 10000.0)); //decimeters + 10000dm + crsfBaroAltitude.verticalspd = htobe16((int16_t)(verticalspd*100.0)); //cm/s + crsf.queuePacket(CRSF_SYNC_BYTE, CRSF_FRAMETYPE_BARO_ALTITUDE, &crsfBaroAltitude, sizeof(crsfBaroAltitude)); +} + +// Vertical speed on its own, for when it does not come from a barometer +void sendVario(float verticalspd) +{ crsf_sensor_vario_t crsfVario = { 0 }; // Values are MSB first (BigEndian) - crsfVario.verticalspd = htobe16((int16_t)(verticalspd*100.0)); + crsfVario.verticalspd = htobe16((int16_t)(verticalspd*100.0)); //cm/s crsf.queuePacket(CRSF_SYNC_BYTE, CRSF_FRAMETYPE_VARIO, &crsfVario, sizeof(crsfVario)); } diff --git a/library.properties b/library.properties index 0713f59..e470544 100644 --- a/library.properties +++ b/library.properties @@ -1,8 +1,8 @@ name=AlfredoCRSF -version=1.0.1 +version=2.0.0 author=Jacob Williams maintainer=Jacob Williams (jrw4561@gmail.com) -sentence=CSRF serial protocol Arduino library -paragraph=Use this library to communicate over serial with an ELRS receiver. Get channel data and send telemetry. +sentence=CRSF serial protocol Arduino library +paragraph=Communicate over serial with an ExpressLRS or TBS Crossfire receiver. Read RC channels and link statistics, and send telemetry back to the handset. Works with both ELRS 3.x and 4.x, including the 4.0 telemetry sensors, arming status byte and CRSF router frames. category=Device Control url=https://github.com/AlfredoSystems/AlfredoCRSF \ No newline at end of file diff --git a/src/AlfredoCRSF.cpp b/src/AlfredoCRSF.cpp index d8b53c7..823b526 100644 --- a/src/AlfredoCRSF.cpp +++ b/src/AlfredoCRSF.cpp @@ -1,6 +1,7 @@ #include AlfredoCRSF::AlfredoCRSF() : + _deviceAddr(CRSF_ADDRESS_FLIGHT_CONTROLLER), _deviceName(NULL), _crc(0xd5), _lastReceive(0), _lastChannelsPacket(0), _linkIsUp(false), _hasChannelsStatus(false), _channelsStatus(0) @@ -8,9 +9,10 @@ AlfredoCRSF::AlfredoCRSF() : } -void AlfredoCRSF::begin(Stream &port) +void AlfredoCRSF::begin(Stream &port, uint8_t deviceAddr) { this->_port = &port; + this->_deviceAddr = deviceAddr; } // Call from main loop to update @@ -62,7 +64,7 @@ void AlfredoCRSF::handleByteReceived() uint8_t crc = _crc.calc(&_rxBuf[2], len - 1); if (crc == inCrc) { - processPacketIn(len); + processPacketIn(); shiftRxBuffer(len + 2); reprocess = true; } @@ -92,70 +94,86 @@ void AlfredoCRSF::checkLinkDown() } } -void AlfredoCRSF::processPacketIn(uint8_t len) +// Byte 0 of a CRSF frame is a sync byte, not routing information: standard +// frames (type below 0x28) have meaning purely by their type, and extended +// frames (0x28-0x96) carry their routing in destination/origin header bytes +void AlfredoCRSF::processPacketIn() { const crsf_header_t *hdr = (crsf_header_t *)_rxBuf; - if (hdr->device_addr == CRSF_ADDRESS_FLIGHT_CONTROLLER) //Rx to FC + if (CRSF_IS_EXT_FRAMETYPE(hdr->type)) { - if (!processTelemetryPacketIn(hdr) && hdr->type == CRSF_FRAMETYPE_RC_CHANNELS_PACKED) - { - packetChannelsPacked(hdr); - } + processExtendedPacketIn(hdr); } - else if (hdr->device_addr == CRSF_ADDRESS_CRSF_TRANSMITTER) //Headset to TX + else if (hdr->type == CRSF_FRAMETYPE_RC_CHANNELS_PACKED) { - if (hdr->type == CRSF_FRAMETYPE_RC_CHANNELS_PACKED) - { - packetChannelsPacked(hdr); - } + packetChannelsPacked(hdr); } - else if (hdr->device_addr == CRSF_ADDRESS_RADIO_TRANSMITTER) //Telemetry to TX (Backpack) + else { processTelemetryPacketIn(hdr); } } -// Handle telemetry frame types common to the FC and backpack directions. -// Returns true if the frame type was recognized and handled. -bool AlfredoCRSF::processTelemetryPacketIn(const crsf_header_t *hdr) +void AlfredoCRSF::processTelemetryPacketIn(const crsf_header_t *hdr) { switch (hdr->type) { case CRSF_FRAMETYPE_GPS: packetGps(hdr); - return true; + break; case CRSF_FRAMETYPE_GPS_TIME: packetGpsTime(hdr); - return true; + break; case CRSF_FRAMETYPE_LINK_STATISTICS: packetLinkStatistics(hdr); - return true; + break; case CRSF_FRAMETYPE_BARO_ALTITUDE: packetBaroAltitude(hdr); - return true; + break; case CRSF_FRAMETYPE_VARIO: packetVario(hdr); - return true; + break; case CRSF_FRAMETYPE_ATTITUDE: packetAttitude(hdr); - return true; + break; case CRSF_FRAMETYPE_AIRSPEED: packetAirspeed(hdr); - return true; + break; case CRSF_FRAMETYPE_RPM: packetRpm(hdr); - return true; + break; case CRSF_FRAMETYPE_TEMP: packetTemp(hdr); - return true; + break; case CRSF_FRAMETYPE_CELLS: packetCells(hdr); - return true; + break; + } +} + +// Extended header frames. Status-carrying frames are decoded regardless of +// their destination; frames that require a response are only acted on when +// addressed to this device (or broadcast) +void AlfredoCRSF::processExtendedPacketIn(const crsf_header_t *hdr) +{ + if (hdr->frame_size < CRSF_FRAME_LENGTH_EXT_TYPE_CRC) + return; + switch (hdr->type) + { case CRSF_FRAMETYPE_ELRS_STATUS: packetElrsStatus(hdr); - return true; + break; + case CRSF_FRAMETYPE_HANDSET: + packetHandsetTiming(hdr); + break; + case CRSF_FRAMETYPE_DEVICE_PING: + { + const crsf_ext_header_t *ext = (const crsf_ext_header_t *)hdr; + if (_deviceName && (ext->dest_addr == _deviceAddr || ext->dest_addr == CRSF_ADDRESS_BROADCAST)) + sendDeviceInfo(ext->orig_addr); + break; + } } - return false; } // Shift the bytes in the RxBuf down by cnt bytes @@ -345,6 +363,22 @@ void AlfredoCRSF::packetElrsStatus(const crsf_header_t *p) _elrsStatus.msg[msgLen] = '\0'; } +// HANDSET is an extended header frame; the payload is a subcommand byte +// followed by subcommand-specific data +void AlfredoCRSF::packetHandsetTiming(const crsf_header_t *p) +{ + uint8_t payloadLen = p->frame_size - CRSF_FRAME_LENGTH_EXT_TYPE_CRC; + if (payloadLen < 9) + return; + const uint8_t *payload = &p->data[2]; // skip extended dest/origin + if (payload[0] != CRSF_HANDSET_SUBCMD_TIMING) + return; + _handsetTiming.rate = ((uint32_t)payload[1] << 24) | ((uint32_t)payload[2] << 16) | + ((uint32_t)payload[3] << 8) | payload[4]; + _handsetTiming.offset = (int32_t)(((uint32_t)payload[5] << 24) | ((uint32_t)payload[6] << 16) | + ((uint32_t)payload[7] << 8) | payload[8]); +} + void AlfredoCRSF::packetCells(const crsf_header_t *p) { uint8_t payloadLen = p->frame_size - CRSF_FRAME_LENGTH_TYPE_CRC; @@ -398,3 +432,78 @@ void AlfredoCRSF::writePacket(uint8_t addr, uint8_t type, const void *payload, u buf[len+3] = _crc.calc(&buf[2], len + 1); write(buf, len + 4); } + +void AlfredoCRSF::writeChannels(uint8_t addr, const crsf_channels_t *channels) +{ + writePacket(addr, CRSF_FRAMETYPE_RC_CHANNELS_PACKED, channels, sizeof(crsf_channels_t)); +} + +void AlfredoCRSF::writeChannels(uint8_t addr, const crsf_channels_t *channels, uint8_t status) +{ + // ELRS 4.0 extended channels frame: the packed channels followed by one + // status byte (CRSF_CHANNELS_STATUS_* bits) + uint8_t payload[sizeof(crsf_channels_t) + 1]; + memcpy(payload, channels, sizeof(crsf_channels_t)); + payload[sizeof(crsf_channels_t)] = status; + writePacket(addr, CRSF_FRAMETYPE_RC_CHANNELS_PACKED, payload, sizeof(payload)); +} + +void AlfredoCRSF::writeExtPacket(uint8_t type, uint8_t destAddr, const void *payload, uint8_t len) +{ + if (len > CRSF_MAX_PACKET_LEN - 2) + return; + uint8_t buf[CRSF_MAX_PACKET_LEN]; + buf[0] = destAddr; + buf[1] = _deviceAddr; + memcpy(&buf[2], payload, len); + writePacket(CRSF_SYNC_BYTE, type, buf, len + 2); +} + +// Model select is a COMMAND frame, which is an extended header frame with an +// extra payload CRC before the frame CRC: +// [sync][len][type][dest][origin][command][subcommand][model id][crcBA][crc] +void AlfredoCRSF::sendModelId(uint8_t modelId) +{ + uint8_t buf[10]; + buf[0] = CRSF_SYNC_BYTE; + buf[1] = 8; // type, dest, origin, command, subcommand, model id, both CRCs + buf[2] = CRSF_FRAMETYPE_COMMAND; + buf[3] = CRSF_ADDRESS_CRSF_TRANSMITTER; // to the transmitter module + buf[4] = _deviceAddr; // from us, acting as the handset + buf[5] = CRSF_COMMAND_SUBCMD_RX; + buf[6] = CRSF_COMMAND_MODEL_SELECT_ID; + buf[7] = modelId; + // Command frames carry an extra CRC over the payload before the frame CRC + buf[8] = Crc8::calcPoly(&buf[2], 6, CRSF_COMMAND_CRC_POLY); + buf[9] = _crc.calc(&buf[2], 7); + write(buf, sizeof(buf)); +} + +void AlfredoCRSF::sendHeartbeat() +{ + // Payload is the origin device address as a big endian int16 + uint8_t payload[2] = { 0, _deviceAddr }; + writePacket(CRSF_SYNC_BYTE, CRSF_FRAMETYPE_HEARTBEAT, payload, sizeof(payload)); +} + +void AlfredoCRSF::setDeviceName(const char *name) +{ + _deviceName = name; +} + +// DEVICE_INFO payload: null-terminated device name, then serial number, +// hardware and software version (uint32 big endian), field count and +// parameter version. We report no configuration fields. +void AlfredoCRSF::sendDeviceInfo(uint8_t destAddr) +{ + uint8_t payload[CRSF_DEVICE_NAME_MAX + 1 + 14]; + uint8_t nameLen = 0; + while (_deviceName[nameLen] && nameLen < CRSF_DEVICE_NAME_MAX) + { + payload[nameLen] = _deviceName[nameLen]; + nameLen++; + } + payload[nameLen++] = '\0'; + memset(&payload[nameLen], 0, 14); + writeExtPacket(CRSF_FRAMETYPE_DEVICE_INFO, destAddr, payload, nameLen + 14); +} diff --git a/src/AlfredoCRSF.h b/src/AlfredoCRSF.h index 06da56c..badf9e7 100644 --- a/src/AlfredoCRSF.h +++ b/src/AlfredoCRSF.h @@ -14,13 +14,46 @@ class AlfredoCRSF static const unsigned int CRSF_FAILSAFE_STAGE1_MS = 300; AlfredoCRSF(); - void begin(Stream& port); + // deviceAddr is this device's own CRSF address, used for extended header + // frames that are addressed to a specific device (e.g. device discovery + // pings). Pass CRSF_ADDRESS_RADIO_TRANSMITTER when acting as a handset. + void begin(Stream& port, uint8_t deviceAddr = CRSF_ADDRESS_FLIGHT_CONTROLLER); void update(); void write(uint8_t b); void write(const uint8_t *buf, size_t len); void queuePacket(uint8_t addr, uint8_t type, const void *payload, uint8_t len); void writePacket(uint8_t addr, uint8_t type, const void *payload, uint8_t len); + // Send a packed channels frame. addr is the leading byte: use CRSF_SYNC_BYTE + // when sending to a flight controller, CRSF_ADDRESS_CRSF_TRANSMITTER when + // sending to a TX module as a handset would. + void writeChannels(uint8_t addr, const crsf_channels_t *channels); + // ELRS 4.0+ TX modules only: also appends the channels status byte + // (CRSF_CHANNELS_STATUS_* bits) carrying the commanded arm state for Arm + // using Switch mode. ELRS 3.x modules do not understand the longer frame, + // so only use this against a 4.0+ module with Switch arming selected. + void writeChannels(uint8_t addr, const crsf_channels_t *channels, uint8_t status); + + // Send an extended header frame (type 0x28-0x96) from this device's + // address to destAddr. payload/len exclude the dest/origin bytes. + void writeExtPacket(uint8_t type, uint8_t destAddr, const void *payload, uint8_t len); + + // Tell a TX module which model ID is selected, the way a handset does when + // it connects. Only the low 6 bits are significant. Pass 0xFF, the value a + // handset uses for "no model match", unless you actually run model match: + // a mismatched ID leaves the receiver connected but completely silent on + // its serial port. + void sendModelId(uint8_t modelId); + + // Announce this device to the CRSF router for device discovery. + // Call periodically (e.g. once per second); optional. + void sendHeartbeat(); + + // Respond to CRSF device discovery pings with the given device name, so + // this device shows up to the router and configuration tools. The string + // is not copied and must remain valid. Pass NULL to disable (default). + void setDeviceName(const char *name); + // Return current channel value (1-based) in us int getChannel(unsigned int ch) const { return _channels[ch - 1]; } const crsf_channels_t *getChannelsPacked() const { return &_channelsPacked;} @@ -35,6 +68,8 @@ class AlfredoCRSF const crsf_sensor_temp_t *getTempSensor() const { return &_tempSensor; } const crsf_sensor_cells_t *getCellsSensor() const { return &_cellsSensor; } const crsf_elrs_status_t *getElrsStatus() const { return &_elrsStatus; } + // TX module's requested channels frame rate/phase (for handset emulation) + const crsf_handset_timing_t *getHandsetTiming() const { return &_handsetTiming; } bool isLinkUp() const { return _linkIsUp; } // ELRS 4.0+ (EdgeTX 2.11+) appends an optional status byte to channels @@ -47,6 +82,8 @@ class AlfredoCRSF private: Stream* _port; + uint8_t _deviceAddr; + const char *_deviceName; uint8_t _rxBuf[CRSF_MAX_PACKET_LEN+3]; uint8_t _rxBufPos; Crc8 _crc; @@ -62,6 +99,7 @@ class AlfredoCRSF crsf_sensor_temp_t _tempSensor; crsf_sensor_cells_t _cellsSensor; crsf_elrs_status_t _elrsStatus; + crsf_handset_timing_t _handsetTiming; uint32_t _baud; uint32_t _lastReceive; uint32_t _lastChannelsPacket; @@ -73,12 +111,13 @@ class AlfredoCRSF void handleSerialIn(); void handleByteReceived(); void shiftRxBuffer(uint8_t cnt); - void processPacketIn(uint8_t len); + void processPacketIn(); void checkPacketTimeout(); void checkLinkDown(); // Packet RX Handlers - bool processTelemetryPacketIn(const crsf_header_t *p); + void processTelemetryPacketIn(const crsf_header_t *p); + void processExtendedPacketIn(const crsf_header_t *p); void packetChannelsPacked(const crsf_header_t *p); void packetLinkStatistics(const crsf_header_t *p); void packetGps(const crsf_header_t *p); @@ -91,4 +130,7 @@ class AlfredoCRSF void packetTemp(const crsf_header_t *p); void packetCells(const crsf_header_t *p); void packetElrsStatus(const crsf_header_t *p); + void packetHandsetTiming(const crsf_header_t *p); + + void sendDeviceInfo(uint8_t destAddr); }; diff --git a/src/crc8.cpp b/src/crc8.cpp index 2612b8d..2ab7fd2 100644 --- a/src/crc8.cpp +++ b/src/crc8.cpp @@ -27,3 +27,17 @@ uint8_t Crc8::calc(uint8_t *data, uint8_t len) } return crc; } + +uint8_t Crc8::calcPoly(const uint8_t *data, uint8_t len, uint8_t poly) +{ + uint8_t crc = 0; + while (len--) + { + crc ^= *data++; + for (int shift = 0; shift < 8; ++shift) + { + crc = (crc << 1) ^ ((crc & 0x80) ? poly : 0); + } + } + return crc; +} diff --git a/src/crc8.h b/src/crc8.h index f8231a0..5b3f98e 100644 --- a/src/crc8.h +++ b/src/crc8.h @@ -8,6 +8,11 @@ class Crc8 Crc8(uint8_t poly); uint8_t calc(uint8_t *data, uint8_t len); + // One shot CRC over an arbitrary polynomial, computed a bit at a time. + // Slower than calc() but needs no lookup table, so it suits polynomials + // that are only used occasionally. + static uint8_t calcPoly(const uint8_t *data, uint8_t len, uint8_t poly); + protected: uint8_t _lut[256]; void init(uint8_t poly); diff --git a/src/crsf_protocol.h b/src/crsf_protocol.h index d2ed63c..97d43e1 100644 --- a/src/crsf_protocol.h +++ b/src/crsf_protocol.h @@ -27,6 +27,17 @@ #define CRSF_MAX_TEMP_VALUES 20 #define CRSF_MAX_CELL_VALUES 29 #define CRSF_ELRS_STATUS_MSG_LEN 56 +#define CRSF_DEVICE_NAME_MAX 32 + +// Subcommand in the first payload byte of a HANDSET (0x3A) frame +#define CRSF_HANDSET_SUBCMD_TIMING 0x10 + +// COMMAND (0x32) frames: a command byte, then a subcommand, then its data. +// They also carry an extra CRC over the payload using this polynomial, +// placed before the normal frame CRC. +#define CRSF_COMMAND_SUBCMD_RX 0x10 // commands aimed at the receiver +#define CRSF_COMMAND_MODEL_SELECT_ID 0x05 // select model/receiver ID +#define CRSF_COMMAND_CRC_POLY 0xBA // Flag bits in the ELRS_STATUS flags field #define CRSF_ELRS_FLAG_CONNECTED 0x01 // status: TX connected to an RX @@ -63,20 +74,20 @@ typedef enum //CRSF_FRAMETYPE_VIDEO_TRANSMITTER = 0x0F, //no need to support? (rev07) CRSF_FRAMETYPE_LINK_STATISTICS = 0x14, // CRSF_FRAMETYPE_OPENTX_SYNC = 0x10, //not in edgeTX - // CRSF_FRAMETYPE_RADIO_ID = 0x3A, //no need to support? CRSF_FRAMETYPE_RC_CHANNELS_PACKED = 0x16, // CRSF_FRAMETYPE_LINK_RX_ID = 0x1C, //no need to support? // CRSF_FRAMETYPE_LINK_TX_ID = 0x1D, //no need to support? CRSF_FRAMETYPE_ATTITUDE = 0x1E, // CRSF_FRAMETYPE_FLIGHT_MODE = 0x21, //no need to support? // Extended Header Frames, range: 0x28 to 0x96 - // CRSF_FRAMETYPE_DEVICE_PING = 0x28, //no "flight controller" needs to know about this - // CRSF_FRAMETYPE_DEVICE_INFO = 0x29, //no "flight controller" needs to know about this + CRSF_FRAMETYPE_DEVICE_PING = 0x28, //device discovery request (extended header frame) + CRSF_FRAMETYPE_DEVICE_INFO = 0x29, //device discovery response (extended header frame) // CRSF_FRAMETYPE_PARAMETER_SETTINGS_ENTRY = 0x2B, //no "flight controller" needs to know about this // CRSF_FRAMETYPE_PARAMETER_READ = 0x2C, //no "flight controller" needs to know about this // CRSF_FRAMETYPE_PARAMETER_WRITE = 0x2D, //no "flight controller" needs to know about this CRSF_FRAMETYPE_ELRS_STATUS = 0x2E, //ELRS good/bad packet count and status flags (extended header frame) - // CRSF_FRAMETYPE_COMMAND = 0x32, //no "flight controller" needs to know about this + CRSF_FRAMETYPE_COMMAND = 0x32, //commands e.g. model select, bind (extended header frame with an extra payload CRC) + CRSF_FRAMETYPE_HANDSET = 0x3A, //handset subcommands e.g. timing sync (extended header frame; named RADIO_ID in older firmwares) // KISS frames // CRSF_FRAMETYPE_KISS_REQ = 0x78, //not in edgeTX // CRSF_FRAMETYPE_KISS_RESP = 0x79, //not in edgeTX @@ -108,12 +119,28 @@ typedef enum typedef struct crsf_header_s { - uint8_t device_addr; // from crsf_addr_e + uint8_t device_addr; // sync byte; 0xC8 on serial links (0xEE/0xEA on handset links). Not routing information uint8_t frame_size; // counts size after this byte, so it must be the payload size + 2 (type and crc) uint8_t type; // from crsf_frame_type_e uint8_t data[0]; } PACKED crsf_header_t; +// Extended header frames (type in the range 0x28 to 0x96) carry routing +// information: a destination and origin address before the payload +typedef struct crsf_ext_header_s +{ + uint8_t device_addr; // sync byte + uint8_t frame_size; // counts size after this byte, so it must be the payload size + 4 (type, dest, orig and crc) + uint8_t type; // from crsf_frame_type_e + uint8_t dest_addr; // from crsf_addr_e + uint8_t orig_addr; // from crsf_addr_e + uint8_t payload[0]; +} PACKED crsf_ext_header_t; + +#define CRSF_FRAMETYPE_EXT_FIRST 0x28 +#define CRSF_FRAMETYPE_EXT_LAST 0x96 +#define CRSF_IS_EXT_FRAMETYPE(t) ((t) >= CRSF_FRAMETYPE_EXT_FIRST && (t) <= CRSF_FRAMETYPE_EXT_LAST) + typedef struct crsf_channels_s { uint16_t ch0 : 11; @@ -224,6 +251,14 @@ typedef struct crsf_sensor_baro_altitude_s } PACKED crsf_sensor_baro_altitude_t; +// Decoded form of the HANDSET (0x3A) timing subcommand, sent by a TX module +// to tell the handset the desired channels frame rate and phase +typedef struct crsf_handset_timing_s +{ + uint32_t rate; // requested channels packet interval, 0.1us units + int32_t offset; // timing offset correction, 0.1us units +} crsf_handset_timing_t; + // Decoded form of the ELRS_STATUS frame (extended header, TX module to // handset). On the wire the payload is pktsBad, pktsGood (big endian), // flags, then a variable-length null-terminated message string. @@ -251,10 +286,13 @@ typedef struct crsf_sensor_attitude_s #define be16toh(x) (x) #define be32toh(x) (x) #define htobe16(x) (x) +#define htobe24(x) (x) #define htobe32(x) (x) #else // __ORDER_LITTLE_ENDIAN__ #define be16toh(x) __builtin_bswap16(x) #define be32toh(x) __builtin_bswap32(x) #define htobe16(x) __builtin_bswap16(x) +// For the 24 bit fields used by some sensors, e.g. battery capacity +#define htobe24(x) (__builtin_bswap32((uint32_t)(x)) >> 8) #define htobe32(x) __builtin_bswap32(x) #endif // __BYTE_ORDER__