-
Notifications
You must be signed in to change notification settings - Fork 227
Add mqtt v5 protocol implementation #590
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
@danielmeza Not sure - do you want a review here, or is this still in progress? If the latter, perhaps mark it as "draft" until ready? |
|
Hi @ivmarkov, not ready yet! |
|
I plan to continue this in the next weeks! |
|
Hi here! I'm back, the current state of this is the following:
|
I believe the only safe way to do this is for the user or us to queue the request on a single thread using channels so we can guarantee sequential sending without overriding (race condition) the user's properties when multiple threads try to send messages. This way, we can set the user's properties attached to the message just before sending the request to the thread that processes the channel's reception. The queue will then receive the message plus the user's properties. At this point, I think a BIG WARNING will allow the user to handle this case. |
|
What we decie here will affect esp-rs/embedded-svc#84 |
There was a problem hiding this comment.
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 adds MQTT v5 protocol support to the esp-idf-svc library for IDF v5.3, implementing a new synchronous client alongside the existing MQTT v3.1/v3.1.1 client.
Key changes:
- New
client5module implementingEspMqtt5Clientwith v5-specific features (user properties, message metadata, disconnect with reason codes) - Added v5 property configuration structs for publish, subscribe, unsubscribe, and disconnect operations
- Extended existing
EspMqttClientandEspMqttEventwith conditional v5 feature support - Split client creation and startup by introducing a separate
start()method
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 27 comments.
| File | Description |
|---|---|
| src/mqtt/client5.rs | New MQTT v5 client implementation with property configs and user property list management |
| src/mqtt/client.rs | Extended existing client with v5 methods for metadata/user properties and added v5-conditional helper methods |
| src/mqtt.rs | Added conditional module declaration for client5 |
| Cargo.toml | Added mqtt_protocol_v5 feature dependency from embedded-svc |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| pub struct EspMqtt5Client<'a> { | ||
| raw_client: esp_mqtt_client_handle_t, | ||
| _boxed_raw_callback: Box<dyn FnMut(esp_mqtt_event_handle_t) + Send + 'a>, | ||
| _tls_psk_conf: Option<TlsPsk>, | ||
| } |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing documentation: Public struct EspMqtt5Client lacks documentation explaining its purpose, API differences from EspMqttClient, and usage examples for MQTT v5 specific features.
| retain_as_published_flag: config.retain_as_published, | ||
| retain_handle: config.retain_handling as _, | ||
| is_share_subscribe: config.share_name.is_some(), | ||
| share_name: cstrs.as_nptr(config.share_name)?, |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential use-after-free: The share_name pointer is obtained from a temporary string slice that may not live long enough. The as_ptr() method returns a pointer that will be invalid once the Option is dropped. Consider using RawCstrs to manage the lifetime of C strings properly, similar to how other string properties are handled.
| user_property: if let Some(ref user_properties) = config.user_properties { | ||
| EspUserPropertyList::from(user_properties).as_ptr() | ||
| } else { | ||
| mqtt5_user_property_handle_t::default() | ||
| }, |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Memory leak: EspUserPropertyList::from creates a new user property list that is never properly cleaned up. The handle returned by as_ptr() will have its memory leaked after the property struct is dropped, as the Drop implementation only clears user properties if they are stored in the Option field.
| std::ffi::CStr::from_ptr(self.0.key).to_str().unwrap_or("") | ||
| } | ||
| }; | ||
| let value: &'a str = unsafe { | ||
| if self.0.value.is_null() { | ||
| "" | ||
| } else { | ||
| std::ffi::CStr::from_ptr(self.0.value) | ||
| .to_str() | ||
| .unwrap_or("") |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential panic: Calling unwrap_or("") on to_str() silently hides invalid UTF-8 errors. Consider either propagating the error or logging a warning when UTF-8 decoding fails, so users are aware of potential data corruption.
| std::ffi::CStr::from_ptr(self.0.key).to_str().unwrap_or("") | |
| } | |
| }; | |
| let value: &'a str = unsafe { | |
| if self.0.value.is_null() { | |
| "" | |
| } else { | |
| std::ffi::CStr::from_ptr(self.0.value) | |
| .to_str() | |
| .unwrap_or("") | |
| match std::ffi::CStr::from_ptr(self.0.key).to_str() { | |
| Ok(s) => s, | |
| Err(e) => { | |
| eprintln!("Invalid UTF-8 in MQTT5 user property key: {}", e); | |
| "" | |
| } | |
| } | |
| } | |
| }; | |
| let value: &'a str = unsafe { | |
| if self.0.value.is_null() { | |
| "" | |
| } else { | |
| match std::ffi::CStr::from_ptr(self.0.value).to_str() { | |
| Ok(s) => s, | |
| Err(e) => { | |
| eprintln!("Invalid UTF-8 in MQTT5 user property value: {}", e); | |
| "" | |
| } | |
| } |
| let cstrs = RawCstrs::new(); | ||
| let mut user_properties = EspUserPropertyList::new(); | ||
| if let Some(ref user_properties_items) = config.user_properties { | ||
| user_properties.set_items(user_properties_items)?; | ||
| } | ||
|
|
||
| let property = esp_mqtt5_unsubscribe_property_config_t { | ||
| is_share_subscribe: config.share_name.is_some(), | ||
| share_name: config.share_name.map_or(core::ptr::null(), |s| s.as_ptr()), | ||
| user_property: user_properties.as_ptr(), | ||
| }; | ||
| Ok(EspUnsubscribePropertyConfig( | ||
| property, | ||
| Some(user_properties), | ||
| Some(cstrs), |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Naming inconsistency: The variable 'cstrs' is declared but never used in this function. Either remove it or add the #[allow(unused_variables)] attribute if it's intentionally kept for future use.
| key: key, | ||
| value: value, |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redundant field names: The explicit field names 'key: key' and 'value: value' can be simplified to just 'key' and 'value' using Rust's field init shorthand.
| key: key, | |
| value: value, | |
| key, | |
| value, |
|
|
||
| #[allow(unused_imports)] | ||
| use esp_idf_hal::sys::*; | ||
|
|
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing documentation: Public struct EspUserPropertyList lacks documentation explaining its purpose, usage, and relationship with the underlying MQTT5 user properties handle.
| /// Wrapper around the ESP-IDF MQTT v5 `mqtt5_user_property_handle_t`. | |
| /// | |
| /// This type owns an MQTT 5 user properties handle together with the | |
| /// backing C strings required by the ESP-IDF API. It is typically | |
| /// constructed from a slice of [`UserPropertyItem`] values (see | |
| /// [`EspUserPropertyList::from`]) or created empty via [`EspUserPropertyList::new`], | |
| /// populated, and then passed to the underlying MQTT v5 client functions. | |
| /// | |
| /// The embedded [`RawCstrs`] value ensures that the UTF-8 strings backing | |
| /// each user property key/value pair remain valid for as long as the | |
| /// corresponding `mqtt5_user_property_handle_t` is in use. |
| retain as _, | ||
| ) | ||
| }); | ||
| drop(property); // Ensure the property is dropped after use |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redundant explicit drop: The 'drop(property)' call is redundant as the property will be automatically dropped at the end of the function scope. Remove this line to simplify the code.
| drop(property); // Ensure the property is dropped after use |
| ) -> Result<MessageId, EspError> { | ||
| let property = esp_mqtt5_unsubscribe_property_config_t { | ||
| is_share_subscribe: config.share_name.is_some(), | ||
| share_name: config.share_name.map_or(core::ptr::null(), |s| s.as_ptr()), |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential use-after-free: The share_name pointer is obtained from calling as_ptr() on a temporary Option value. This pointer will become invalid once the expression completes. Consider using proper C string lifetime management.
| let error = unsafe { | ||
| esp_mqtt5_client_get_user_property( | ||
| self.0, | ||
| items.as_mut_ptr(), | ||
| &mut items.len() as *mut usize as *mut u8, | ||
| ) | ||
| }; | ||
| esp!(error)?; |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incorrect pointer cast: The expression takes a mutable reference to items.len() which returns a usize, then casts it to *mut u8. This is incorrect as it treats the address of a usize value as if it were a pointer to u8, not converting the usize value itself to u8. The API likely expects a mutable pointer to a u8 count value, not a reinterpreted pointer to a usize.
| let error = unsafe { | |
| esp_mqtt5_client_get_user_property( | |
| self.0, | |
| items.as_mut_ptr(), | |
| &mut items.len() as *mut usize as *mut u8, | |
| ) | |
| }; | |
| esp!(error)?; | |
| // Use a proper mutable u8 variable for the count, as expected by the C API. | |
| let mut count_mut = count; | |
| let error = unsafe { | |
| esp_mqtt5_client_get_user_property( | |
| self.0, | |
| items.as_mut_ptr(), | |
| &mut count_mut as *mut u8, | |
| ) | |
| }; | |
| esp!(error)?; | |
| // Adjust the vector length to the actual number of items returned. | |
| items.truncate(count_mut as usize); |
Introduces a new `client5` module for MQTT 5.0 protocol support, including an `ErrorReasonCode` enum for MQTT5 error reason codes, conversion implementations, and utility methods. The new module is conditionally compiled with the `esp_idf_mqtt_protocol_5` feature.
Simplifies the conversion from mqtt5_error_reason_code to ErrorReasonCode by switching from TryFrom to From and returning UnspecifiedError for unknown codes. Updates EventProperty construction to include payload_format_indicator and removes unused ErrorType import.
This commit introduces support for MQTT v5 property configuration in subscribe, unsubscribe, and publish operations for both sync and async clients. It adds new methods to handle property configs, user properties, and message metadata, and refactors the client5 module to focus on user property handling. Deprecated or unused error reason code logic is removed, and trait implementations are updated to support the new property features.
Reworks the MQTT v5 client implementation to accept property configuration options as Option types for publish, subscribe, unsubscribe, and disconnect operations. Removes legacy publish_with_config and subscribe_with_config methods from the v3 client, and updates property handling to use new wrapper structs. Improves FFI safety and code clarity by encapsulating property conversions and updating event field extraction.
Replaces direct struct construction and From implementations for MQTT5 property configs with TryFrom<Option<T>> implementations, improving memory safety and lifetime management. Introduces internal ownership of C string buffers and user property lists to prevent premature drops, and ensures proper cleanup via Drop implementations. Adds logging for publish calls and updates method usage to leverage the new property config handling.
The RawCstrs instance in the TryFrom implementation for EspUnsubscribePropertyConfig no longer needs to be mutable, as it is not mutated after creation.
a0ad048 to
b29671f
Compare
|
@danielmeza If this is not ready for review, perhaps you can turn it into a "Draft" status? |
|
@ivmarkov yes! I have moved to draf. |
Pull Request Details 📖
Initial implementation for the MQTT v5 implementation for the IDF v5.3
This implements #590
Description
This is the initial implementation for the MQTT v5 protocol for IDF.
This implementation focus in the synchronous client, following the design drivers described at esp-rs/embedded-svc#84, I try to reuse as much as possible but duplicating some parts that will made easy to branch specific protocol changes in the future, so I introduce a new client and put there most of the logic to archive the v5 features.
Main changes compared with the current client implementation are
Testing
Using an existing project, I was able to receive the metadata and user_properties info from the Receive event.
Following has been tested ensure proper memory management:
Missing test:
Curren issues:
Submission Checklist 📝
cargo fmtcommand to ensure that all changed code is formatted correctly.cargo clippycommand to ensure that all changed code passes latest Clippy nightly lints.CHANGELOG.mdin the proper section.