Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR introduces a new CRSF (Crossfire) bridge component for the Copper-rs framework, enabling long-range radio communication for drone telemetry and remote control using ELRS (ExpressLRS) protocol.
Key Changes:
- New bridge component
cu-crsfwith support for RC channels and link quality statistics - Platform-agnostic design supporting both
stdandno_stdenvironments - Serial port abstraction with registry system for embedded targets
Reviewed Changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
Cargo.toml |
Adds new cu_crsf bridge component to workspace members |
components/bridges/cu_crsf/Cargo.toml |
Defines dependencies and features for CRSF bridge (std/no_std support) |
components/bridges/cu_crsf/build.rs |
Build script for setting up log index directory environment variable |
components/bridges/cu_crsf/src/messages.rs |
Wrapper types for CRSF message payloads with serialization support |
components/bridges/cu_crsf/src/lib.rs |
Main bridge implementation with serial communication and channel management |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| tx_channels: &[BridgeChannelConfig<<Self::Tx as BridgeChannelSet>::Id>], | ||
| rx_channels: &[BridgeChannelConfig<<Self::Rx as BridgeChannelSet>::Id>], | ||
| ) -> CuResult<Self> | ||
| where | ||
| Self: Sized, | ||
| { | ||
| let _ = tx_channels; | ||
| let _ = rx_channels; | ||
|
|
There was a problem hiding this comment.
The variables tx_channels and rx_channels parameters are explicitly ignored with let _ = .... If these parameters are not used in the current implementation, consider documenting why they're not needed or if there's planned future functionality. Alternatively, prefix the parameter names with underscore (_tx_channels, _rx_channels) which is the idiomatic way to indicate unused parameters in Rust.
| tx_channels: &[BridgeChannelConfig<<Self::Tx as BridgeChannelSet>::Id>], | |
| rx_channels: &[BridgeChannelConfig<<Self::Rx as BridgeChannelSet>::Id>], | |
| ) -> CuResult<Self> | |
| where | |
| Self: Sized, | |
| { | |
| let _ = tx_channels; | |
| let _ = rx_channels; | |
| _tx_channels: &[BridgeChannelConfig<<Self::Tx as BridgeChannelSet>::Id>], | |
| _rx_channels: &[BridgeChannelConfig<<Self::Rx as BridgeChannelSet>::Id>], | |
| ) -> CuResult<Self> | |
| where | |
| Self: Sized, | |
| { |
| impl LinkStatisticsPayload { | ||
| pub fn inner(&self) -> &LinkStatistics { | ||
| &self.0 | ||
| } |
There was a problem hiding this comment.
The inner_mut() method is provided but LinkStatisticsPayload doesn't implement it, while RcChannelsPayload does. This creates an inconsistent API between the two similar wrapper types. Consider adding an inner_mut() method to LinkStatisticsPayload for consistency.
| } | |
| } | |
| /// Returns mutable access to the inner CRSF structure. | |
| pub fn inner_mut(&mut self) -> &mut LinkStatistics { | |
| &mut self.0 | |
| } |
| Packet::RcChannels(channels) => { | ||
| self.last_rc = Some(channels); | ||
| for (i, value) in self.last_rc.iter().enumerate() { | ||
| debug!("RC Channel {}: {}", i, value.as_ref()); |
There was a problem hiding this comment.
The debug message references value.as_ref() but value is of type &u16, which doesn't have an as_ref() method that makes sense in this context. This should likely be just value or *value to dereference and print the u16 value.
| debug!("RC Channel {}: {}", i, value.as_ref()); | |
| debug!("RC Channel {}: {}", i, *value); |
| _ => { | ||
| error!("CRSF: Serial port read error"); |
There was a problem hiding this comment.
The error message silently swallows the actual error from the serial port read operation. Consider including the error details in the log message to aid debugging: error!("CRSF: Serial port read error: {:?}", err);
| _ => { | |
| error!("CRSF: Serial port read error"); | |
| Err(err) => { | |
| error!("CRSF: Serial port read error: {:?}", err); |
| impl<S, E> CuCrsfBridge<S, E> | ||
| where | ||
| S: SerialFactory<E>, | ||
| { |
There was a problem hiding this comment.
The register_serial public method lacks documentation. Users in no_std environments will need to understand how and when to call this method to register their serial ports.
| { | |
| { | |
| /// Registers a serial port for use with the bridge in `no_std` environments. | |
| /// | |
| /// # Arguments | |
| /// | |
| /// * `slot` - The slot index to associate with the serial port. Each slot corresponds to a bridge instance. | |
| /// * `serial_port` - The serial port implementation to register. Must implement `SerialFactory<E>`. | |
| /// | |
| /// # Usage | |
| /// | |
| /// Call this method before creating or using a bridge instance to ensure the serial port is available. | |
| /// This is required in `no_std` environments where dynamic allocation and registration are not handled automatically. | |
| /// | |
| /// # Errors | |
| /// | |
| /// Returns an error if the registration fails (e.g., if the slot is already occupied). |
| for (i, value) in self.last_rc.iter().enumerate() { | ||
| debug!("RC Channel {}: {}", i, value.as_ref()); |
There was a problem hiding this comment.
The iteration over self.last_rc on line 231 is incorrect. self.last_rc is an Option<RcChannels>, not an iterable collection. This code will not compile. You likely meant to iterate over self.last_rc.unwrap().0 or handle the Option properly with if let Some(channels) = &self.last_rc.
| for (i, value) in self.last_rc.iter().enumerate() { | |
| debug!("RC Channel {}: {}", i, value.as_ref()); | |
| if let Some(channels) = &self.last_rc { | |
| for (i, value) in channels.0.iter().enumerate() { | |
| debug!("RC Channel {}: {}", i, value.as_ref()); | |
| } |
| pub trait SerialFactory<E>: Write<Error = E> + Read<Error = E> + Send + 'static { | ||
| fn try_new(config: Option<&ComponentConfig>) -> CuResult<Self> | ||
| where | ||
| Self: Sized; | ||
| } |
There was a problem hiding this comment.
The SerialFactory trait lacks documentation. Public traits should have doc comments explaining their purpose and when/how they should be implemented.
| pub struct CrsfRx; | ||
|
|
||
| impl CrsfRx { | ||
| pub const LQ: BridgeChannel<RxId, LinkStatisticsPayload> = | ||
| BridgeChannel::new(RxId::LinkQuality, "lqrx"); | ||
| pub const RC_CHANNELS: BridgeChannel<RxId, RcChannelsPayload> = | ||
| BridgeChannel::new(RxId::RCChannels, "rcrx"); | ||
| } | ||
| impl BridgeChannelSet for CrsfRx { | ||
| type Id = RxId; | ||
|
|
||
| const STATIC_CHANNELS: &'static [&'static dyn BridgeChannelInfo<Self::Id>] = | ||
| &[&Self::LQ, &Self::RC_CHANNELS]; | ||
| } | ||
|
|
||
| #[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] | ||
| #[serde(rename_all = "snake_case")] | ||
| pub enum TxId { | ||
| LinkQuality, | ||
| RCChannels, // only support those for now, there are a bunch of others | ||
| } | ||
|
|
||
| pub struct CrsfTx; |
There was a problem hiding this comment.
The CrsfRx and CrsfTx structs lack documentation. These are public API types that users will reference when configuring channels, so they should have doc comments explaining what they represent.
| TxId::LinkQuality => { | ||
| let lsi = Self::payload_as_lq(payload).inner().clone(); | ||
| debug!( | ||
| "CRSF: Sent LinkStatistics: Downlink LQ:{}", | ||
| lsi.downlink_link_quality | ||
| ); | ||
| let ls = Packet::LinkStatistics(lsi); | ||
| let raw_packet = ls.into_raw(PacketAddress::Transmitter); | ||
| self.serial_port | ||
| .write_all(raw_packet.data()) | ||
| .map_err(|_| CuError::from("CRSF: Serial port write error"))?; | ||
| } | ||
| TxId::RCChannels => { | ||
| let rc = Self::payload_as_rc(payload).inner().clone(); | ||
| for (i, value) in rc.iter().enumerate() { | ||
| debug!("Sending RC Channel {}: {}", i, value); | ||
| } | ||
| let rc = Packet::RcChannels(rc); | ||
| let raw_packet = rc.into_raw(PacketAddress::Transmitter); | ||
| self.serial_port | ||
| .write_all(raw_packet.data()) | ||
| .map_err(|_| CuError::from("CRSF: Serial port write error"))?; | ||
| } | ||
| } |
There was a problem hiding this comment.
[nitpick] There's significant code duplication between the LinkQuality and RCChannels cases in both the send and receive methods. The pattern of extracting payload, converting to CRSF type, creating packet, and writing to serial is identical except for the types. Consider extracting this logic into a helper method that can be parameterized by type to reduce duplication and improve maintainability.
This is again useful in the drone space to send telemetry or remote control over the waves at long distance (up to the curvature of the earth in line of sight)
lots of bridges do have a natural ID -> Route map and suddenly this mandatory route parameted make them stutturing for example an AIO ESC have 4 motors, you cannot route anything but the API was forcing you to add a random string to it. This will be followed by some helper to generate more complex bridges like middleware ones where the user needs to generate it at the last minute with all the types they want to expose to each ID.
* Moved route on cubridges to optional lots of bridges do have a natural ID -> Route map and suddenly this mandatory route parameted make them stutturing for example an AIO ESC have 4 motors, you cannot route anything but the API was forcing you to add a random string to it. This will be followed by some helper to generate more complex bridges like middleware ones where the user needs to generate it at the last minute with all the types they want to expose to each ID. * removing some AI stupidity * Mopping up dumb errors from CI.
lots of bridges do have a natural ID -> Route map and suddenly this mandatory route parameted make them stutturing for example an AIO ESC have 4 motors, you cannot route anything but the API was forcing you to add a random string to it. This will be followed by some helper to generate more complex bridges like middleware ones where the user needs to generate it at the last minute with all the types they want to expose to each ID.
This is again useful in the drone space to send telemetry or remote control over the waves at long distance (up to the curvature of the earth in line of sight)