diff --git a/CHANGELOG.md b/CHANGELOG.md index 55398419..547687ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,13 +23,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +### Fixed + +- Keeping a ros1::ServiceServer alive no longer keeps the underlying node alive past the last ros1::NodeHandle being dropped. +- Dropping the last ros1::NodeHandle results in the node cleaning up any advertises, subscriptions, and services with the ROS master. +- Generated code now includes various lint attributes to suppress warnings. +- TCPROS header parsing now ignores (the undocumented fields) response_type and request_type and doesn't produce warnings on them. + +### Changed + +- Internal integral type Time changed from u32 to i32 representation to better align with ROS1 +- Conversions between ROS Time and Duration to std::time::Time and std::time::Duration switched to TryFrom as they can be fallible. + +## 0.11.1 + +### Added + +### Fixed + +- ROS1 Native Publishers no longer occasionally truncate very large messages when configured with latching + +### Changed + +- Passing of large messages containing uint8[] arrays is now substantially faster +- Generated code now relies on serde_bytes to enable faster handling of uint8[] arrays +- Switched to a fork of serde_rosmsg to enable faster handling of uint8[] arrays + +## 0.11.0 + +### Added + - ROS1 Native Publishers now support latching behavior - The XML RPC client for interacting directly with the rosmaster server has been exposed as a public API - Experimental: Initial support for writing generic clients that can be compile time specialized for rosbridge or ros1 +- Can subscribe to any topic and get raw bytes instead of a deserialized message of known type +- Can publish to any topic and send raw bytes instead of a deserialized message ### Fixed - ROS1 Native Publishers correctly call unadvertise when dropped +- ROS1 Native Publishers no longer occasionally truncate very large messages (>5MB) ### Changed @@ -45,6 +78,7 @@ This is to bring it in line with the ROS1 API. ### Added ### Fixed + - Bug with message_definitions provided by Publisher in the connection header not being the fully expanded definition. - Bug with ROS1 native subscribers not being able to receive messages larger than 4096 bytes. @@ -55,6 +89,7 @@ This is to bring it in line with the ROS1 API. ### Added ### Fixed + - Bug with ros1 native publishers not parsing connection headers correctly ### Changed @@ -62,6 +97,7 @@ This is to bring it in line with the ROS1 API. ## 0.10.0 - July 5th, 2024 ### Added + - ROS1 native service servers and service clients are now supported (experimental feature) ### Fixed @@ -89,45 +125,45 @@ crates that were previously adding dependencies on serde, serde-big-array, and s ### Changed - - The function interface for top level generation functions in `roslibrust_codegen` have been changed to include the list of dependent +- The function interface for top level generation functions in `roslibrust_codegen` have been changed to include the list of dependent filesystem paths that should trigger re-running code generation. Note: new files added to the search paths will not be automatically detected. - [Breaking Change] Codegen now generates fixed sized arrays as arrays [T; N] instead of Vec - - Removed `find_and_generate_ros_messages_relative_to_manifest_dir!` this proc_macro was changing the current working directory of the compilation job resulting in a variety of strange compilation behaviors. Build.rs scripts are recommended for use cases requiring fine grained control of message generation. - - The function interface for top level generation functions in `roslibrust_codegen` have been changed to include the list of dependent filesystem paths that should trigger re-running code generation. Note: new files added to the search paths will not be automatically detected. - - Refactor the `ros1::node` module into separate smaller pieces. This should be invisible externally (and no changes to examples were required). +- Removed `find_and_generate_ros_messages_relative_to_manifest_dir!` this proc_macro was changing the current working directory of the compilation job resulting in a variety of strange compilation behaviors. Build.rs scripts are recommended for use cases requiring fine grained control of message generation. +- The function interface for top level generation functions in `roslibrust_codegen` have been changed to include the list of dependent filesystem paths that should trigger re-running code generation. Note: new files added to the search paths will not be automatically detected. +- Refactor the `ros1::node` module into separate smaller pieces. This should be invisible externally (and no changes to examples were required). ## 0.8.0 - October 4th, 2023 ### Added - - Experimental support for ROS1 native communication behind the `ros1` feature flag - - Generation of C++ source added via `roslibrust_genmsg` along with arbitrary languages via passed in templates - - Generation of Rust source for actions - - Example for custom generic message usage with rosbridge - - Example for async native ROS1 listener - - Example for async native ROS1 publisher +- Experimental support for ROS1 native communication behind the `ros1` feature flag +- Generation of C++ source added via `roslibrust_genmsg` along with arbitrary languages via passed in templates +- Generation of Rust source for actions +- Example for custom generic message usage with rosbridge +- Example for async native ROS1 listener +- Example for async native ROS1 publisher ### Fixed - - Incorrect handling of ROS1 message string constants +- Incorrect handling of ROS1 message string constants ### Changed - - `crawl` function in `roslibrust_codegen` updated to a more flexible API - - Overhaul of error handling in roslibrust_codegen to bubble errors up, and remove use of panic! and unwrap(). Significantly better error messages should be produced from proc_macros and build.rs files. Direct usages of the API will need to be updated to handle the returned error type. - - RosMessageType trait now has associated constants for MD5SUM and DEFINITION to enable ROS1 native support. These constants are optional at this time with a default value of "" provided. +- `crawl` function in `roslibrust_codegen` updated to a more flexible API +- Overhaul of error handling in roslibrust_codegen to bubble errors up, and remove use of panic! and unwrap(). Significantly better error messages should be produced from proc_macros and build.rs files. Direct usages of the API will need to be updated to handle the returned error type. +- RosMessageType trait now has associated constants for MD5SUM and DEFINITION to enable ROS1 native support. These constants are optional at this time with a default value of "" provided. ## 0.7.0 - March 13, 2022 ### Added - - Support for default field values in ROS2 messages - - Added public APIs for getting message data from search and for generating Rust code given message data in roslibrust_codegen - - More useful logs available when running codegen - - Refactor some of the public APIs and types in roslibrust_codegen (concept of `ParsedMessageFile` vs `MessageFile`) - - Added a method `get_md5sum` to `MessageFile` - - Additional code generation API and macro which excludes `ROS_PACKAGE_PATH` +- Support for default field values in ROS2 messages +- Added public APIs for getting message data from search and for generating Rust code given message data in roslibrust_codegen +- More useful logs available when running codegen +- Refactor some of the public APIs and types in roslibrust_codegen (concept of `ParsedMessageFile` vs `MessageFile`) +- Added a method `get_md5sum` to `MessageFile` +- Additional code generation API and macro which excludes `ROS_PACKAGE_PATH` ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 7cd6986e..6d8217b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,19 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -35,6 +48,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "0.6.15" @@ -90,6 +109,23 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.3.0" @@ -156,6 +192,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytemuck" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" + [[package]] name = "byteorder" version = "1.5.0" @@ -174,6 +216,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1582e1c9e755dd6ad6b224dcffb135d199399a4568d454bd89fe515ca8425695" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.1.7" @@ -186,6 +234,45 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "bitflags 1.3.2", + "clap_lex 0.2.4", + "indexmap 1.9.3", + "textwrap", +] + [[package]] name = "clap" version = "4.5.13" @@ -204,7 +291,7 @@ checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" dependencies = [ "anstream", "anstyle", - "clap_lex", + "clap_lex 0.7.2", "strsim", ] @@ -220,6 +307,15 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "clap_lex" version = "0.7.2" @@ -278,6 +374,15 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "cpp_demangle" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96e58d342ad113c2b878f16d5d034c03be492ae460cdbc02b7f0f2284d310c7d" +dependencies = [ + "cfg-if", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -287,6 +392,63 @@ dependencies = [ "libc", ] +[[package]] +name = "criterion" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +dependencies = [ + "anes", + "atty", + "cast", + "ciborium", + "clap 3.2.25", + "criterion-plot", + "futures", + "itertools 0.10.5", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "tokio", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.11" @@ -302,6 +464,12 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -319,7 +487,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -335,6 +503,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + [[package]] name = "deranged" version = "0.3.11" @@ -432,11 +609,12 @@ dependencies = [ [[package]] name = "error-chain" -version = "0.10.0" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9435d864e017c3c6afeac1654189b06cdb491cf2ff73dbf0d73b0f292f42ff8" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" dependencies = [ "backtrace", + "version_check", ] [[package]] @@ -461,6 +639,18 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +[[package]] +name = "findshlibs" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64" +dependencies = [ + "cc", + "lazy_static", + "libc", + "winapi", +] + [[package]] name = "fnv" version = "1.0.7" @@ -629,13 +819,29 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 2.3.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -648,6 +854,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.3.9" @@ -741,6 +956,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.3.0" @@ -748,7 +973,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", +] + +[[package]] +name = "inferno" +version = "0.11.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "232929e1d75fe899576a3d5c7416ad0d88dbfbb3c3d6aa00873a7408a50ddb88" +dependencies = [ + "ahash", + "indexmap 2.3.0", + "is-terminal", + "itoa", + "log", + "num-format", + "once_cell", + "quick-xml 0.26.0", + "rgb", + "str_stack", ] [[package]] @@ -763,7 +1006,7 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", "windows-sys 0.52.0", ] @@ -783,6 +1026,15 @@ dependencies = [ "nom", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.12.1" @@ -862,6 +1114,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + [[package]] name = "mime" version = "0.3.17" @@ -898,7 +1159,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", "wasi", "windows-sys 0.52.0", @@ -921,6 +1182,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -947,6 +1219,25 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec", + "itoa", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_threads" version = "0.1.7" @@ -971,6 +1262,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "oorandom" +version = "11.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" + [[package]] name = "openssl" version = "0.10.66" @@ -1015,12 +1312,28 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + [[package]] name = "parking_lot_core" version = "0.9.10" @@ -1058,12 +1371,62 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "pprof" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196ded5d4be535690899a4631cc9f18cdc41b7ebf24a79400f46f48e49a11059" +dependencies = [ + "backtrace", + "cfg-if", + "criterion", + "findshlibs", + "inferno", + "libc", + "log", + "nix", + "once_cell", + "parking_lot", + "smallvec", + "symbolic-demangle", + "tempfile", + "thiserror", +] + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -1082,6 +1445,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.31.0" @@ -1130,6 +1502,26 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.3" @@ -1223,9 +1615,18 @@ dependencies = [ "winreg", ] +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +dependencies = [ + "bytemuck", +] + [[package]] name = "roslibrust" -version = "0.10.2" +version = "0.11.1" dependencies = [ "abort-on-drop", "anyhow", @@ -1245,10 +1646,10 @@ dependencies = [ "reqwest", "roslibrust_codegen", "roslibrust_codegen_macro", + "roslibrust_serde_rosmsg", "serde", "serde-big-array", "serde_json", - "serde_rosmsg", "serde_xmlrpc", "simple_logger", "smart-default 0.6.0", @@ -1261,7 +1662,7 @@ dependencies = [ [[package]] name = "roslibrust_codegen" -version = "0.10.0" +version = "0.11.1" dependencies = [ "env_logger 0.10.2", "lazy_static", @@ -1271,6 +1672,7 @@ dependencies = [ "quote", "serde", "serde-big-array", + "serde_bytes", "serde_json", "simple-error", "smart-default 0.7.1", @@ -1283,7 +1685,7 @@ dependencies = [ [[package]] name = "roslibrust_codegen_macro" -version = "0.10.0" +version = "0.11.1" dependencies = [ "proc-macro2", "quote", @@ -1295,10 +1697,10 @@ dependencies = [ name = "roslibrust_genmsg" version = "0.9.0" dependencies = [ - "clap", + "clap 4.5.13", "const_format", "env_logger 0.11.5", - "itertools", + "itertools 0.12.1", "lazy_static", "log", "minijinja", @@ -1307,15 +1709,31 @@ dependencies = [ "serde_json", ] +[[package]] +name = "roslibrust_serde_rosmsg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee7ea0fc21625dd94a69e126b9f2c1eab4b60dd80a6abf3e0a3e284f9d648586" +dependencies = [ + "byteorder", + "error-chain", + "serde", + "serde_derive", +] + [[package]] name = "roslibrust_test" version = "0.1.0" dependencies = [ + "criterion", "diffy", "env_logger 0.10.2", "lazy_static", + "log", + "pprof", "roslibrust", "roslibrust_codegen", + "tokio", ] [[package]] @@ -1426,6 +1844,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_bytes" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.204" @@ -1449,18 +1876,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_rosmsg" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e55d20c8bff70e82b5948a187c77d93c34fa538ae3d9999597fbb6245993680" -dependencies = [ - "byteorder", - "error-chain", - "serde", - "serde_derive", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1482,7 +1897,7 @@ dependencies = [ "anyhow", "base64 0.22.1", "iso8601", - "quick-xml", + "quick-xml 0.31.0", "serde", "serde-transcode", "thiserror", @@ -1582,12 +1997,47 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "str_stack" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "symbolic-common" +version = "10.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b55cdc318ede251d0957f07afe5fed912119b8c1bc5a7804151826db999e737" +dependencies = [ + "debugid", + "memmap2", + "stable_deref_trait", + "uuid", +] + +[[package]] +name = "symbolic-demangle" +version = "10.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79be897be8a483a81fff6a3a4e195b4ac838ef73ca42d348b3f722da9902e489" +dependencies = [ + "cpp_demangle", + "rustc-demangle", + "symbolic-common", +] + [[package]] name = "syn" version = "1.0.109" @@ -1681,6 +2131,12 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" + [[package]] name = "thiserror" version = "1.0.63" @@ -1744,6 +2200,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -1769,6 +2235,7 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", diff --git a/README.md b/README.md index 08b81d8b..d4eeca52 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,15 @@ This package aims to provide a convenient "async first" library for interacting Currently this packaged provides support for both ROS1 native communication (TCPROS) and rosbridge's protocol which provides support for both ROS1 and ROS2 albeit with some overhead. -Information about the protocol can be found [here](https://github.com/RobotWebTools/rosbridge_suite). +Information about the rosbridge protocol can be found [here](https://github.com/RobotWebTools/rosbridge_suite). Note on documentation: All information about the crate itself (examples, documentation, tutorials, etc.) lives in the source code and can be viewed on [docs.rs](https://docs.rs/roslibrust). This readme is for "Meta" information about developing for the crate. -Fully Supported via rosbridge: Noetic, Galactic, Humble, Iron, +Fully Supported via rosbridge: Noetic, Galactic, Humble, Iron. + +Fully Supported via ROS1 native: Noetic ## Code Generation of ROS Messages diff --git a/docker/galactic/Dockerfile b/docker/galactic/Dockerfile index 7efd9e2a..f9ebe632 100644 --- a/docker/galactic/Dockerfile +++ b/docker/galactic/Dockerfile @@ -7,7 +7,7 @@ RUN apt update && apt install -y git RUN apt update && apt install -y ros-galactic-rosbridge-suite # Curl required to install rust, build-essential required to build quote & proc-macro2 -RUN apt update && apt install -y --fix-missing curl build-essential +RUN apt update && apt install -y --fix-missing curl build-essential libssl-dev pkg-config # Install latest stable rust RUN curl https://sh.rustup.rs -sSf | sh -s -- -y diff --git a/docker/humble/Dockerfile b/docker/humble/Dockerfile index 586e7551..80e88166 100644 --- a/docker/humble/Dockerfile +++ b/docker/humble/Dockerfile @@ -7,7 +7,7 @@ RUN apt update && apt install -y git RUN apt update && apt install -y ros-humble-rosbridge-suite # Curl required to install rust, build-essential required to build quote & proc-macro2 -RUN apt update && apt install -y --fix-missing curl build-essential +RUN apt update && apt install -y --fix-missing curl build-essential libssl-dev pkg-config # Install latest stable rust RUN curl https://sh.rustup.rs -sSf | sh -s -- -y diff --git a/docker/noetic_compose.yaml b/docker/noetic_compose.yaml index 42e6732d..cde87686 100644 --- a/docker/noetic_compose.yaml +++ b/docker/noetic_compose.yaml @@ -4,7 +4,7 @@ services: # network_mode host required for ros1 testing network_mode: host # ports: - # - "9090:9090" + # - "9090:9090" # Pass through the ros master port for native ros1 testing # - "11311:11311" command: bash -c "source /opt/ros/noetic/setup.bash; roslaunch rosbridge_server rosbridge_websocket.launch & disown; rosrun rosapi rosapi_node" diff --git a/roslibrust/Cargo.toml b/roslibrust/Cargo.toml index 645bf452..b8810433 100644 --- a/roslibrust/Cargo.toml +++ b/roslibrust/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "roslibrust" -version = "0.10.2" +version = "0.11.1" authors = ["carter ", "ssnover "] edition = "2021" license = "MIT" @@ -34,11 +34,11 @@ tokio = { version = "1.20", features = [ ] } tokio-tungstenite = { version = "0.17" } uuid = { version = "1.1", features = ["v4"] } -roslibrust_codegen_macro = { path = "../roslibrust_codegen_macro", version = "0.10.0" } -roslibrust_codegen = { path = "../roslibrust_codegen", version = "0.10.0" } +roslibrust_codegen_macro = { path = "../roslibrust_codegen_macro", version = "0.11.1" } +roslibrust_codegen = { path = "../roslibrust_codegen", version = "0.11.1" } reqwest = { version = "0.11", optional = true } # Only used with native ros1 serde_xmlrpc = { version = "0.2", optional = true } # Only used with native ros1 -serde_rosmsg = { version = "0.2", optional = true } # Only used with native ros1 +roslibrust_serde_rosmsg = { version = "0.3", optional = true } # Only used with native ros1 hyper = { version = "0.14", features = [ "server", ], optional = true } # Only used with native ros1 @@ -74,7 +74,7 @@ ros1 = [ "dep:hyper", "dep:gethostname", "dep:regex", - "dep:serde_rosmsg", + "dep:roslibrust_serde_rosmsg", ] diff --git a/roslibrust/examples/ros1_publish_any.rs b/roslibrust/examples/ros1_publish_any.rs new file mode 100644 index 00000000..cc5db23a --- /dev/null +++ b/roslibrust/examples/ros1_publish_any.rs @@ -0,0 +1,45 @@ +roslibrust_codegen_macro::find_and_generate_ros_messages!("assets/ros1_common_interfaces"); + +/// This example demonstrates ths usage of the .advertise_any() function +/// +/// The intent of the API is to support use cases like play back data from a bag file. +/// Most users are encourage to not use this API and instead rely on generated message types. +/// See ros1_talker.rs for a "normal" example. + +#[cfg(feature = "ros1")] +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + // Note: this example needs a ros master running to work + let node = + roslibrust::ros1::NodeHandle::new("http://localhost:11311", "/ros1_publish_any").await?; + + // Definition from: https://docs.ros.org/en/noetic/api/std_msgs/html/msg/String.html + let msg_definition = r#"string data"#; + + let publisher = node + .advertise_any("/chatter", "std_msgs/String", &msg_definition, 100, false) + .await?; + + // Data taken from example in: + // https://wiki.ros.org/ROS/Connection%20Header + // Note: publish expects the body length field to be present, as well as length of each field + // - First four bytes are Body length = 9 bytes + // - Next four bytes are length of data field = 5 bytes + // - Lass five bytes are the data field as ascii "hello" + // Note: Byte order! + let data: Vec = vec![ + 0x09, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x68, 0x65, 0x6c, 0x6c, 0x6f, + ]; + + // This will publish "hello" in a loop at 1Hz + // `rostopic echo /chatter` will show the message being published + loop { + publisher.publish(&data).await?; + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } +} + +#[cfg(not(feature = "ros1"))] +fn main() { + eprintln!("This example does nothing without compiling with the feature 'ros1'"); +} diff --git a/roslibrust/src/ros1/master_client.rs b/roslibrust/src/ros1/master_client.rs index c665b573..56028b1e 100644 --- a/roslibrust/src/ros1/master_client.rs +++ b/roslibrust/src/ros1/master_client.rs @@ -20,6 +20,7 @@ pub enum RosMasterError { /// A client that exposes the API hosted by the [rosmaster](http://wiki.ros.org/ROS/Master_API) // TODO consider exposing this type publicly +#[derive(Clone)] // Note is clone to support an odd case in Node::drop pub struct MasterClient { client: reqwest::Client, // Address at which the rosmaster should be found diff --git a/roslibrust/src/ros1/mod.rs b/roslibrust/src/ros1/mod.rs index e65b60d2..e82b4e18 100644 --- a/roslibrust/src/ros1/mod.rs +++ b/roslibrust/src/ros1/mod.rs @@ -12,6 +12,7 @@ pub use node::*; mod publisher; pub use publisher::Publisher; +pub use publisher::PublisherAny; mod service_client; pub use service_client::ServiceClient; mod subscriber; diff --git a/roslibrust/src/ros1/node/actor.rs b/roslibrust/src/ros1/node/actor.rs index 5d83fc9d..9d2ce618 100644 --- a/roslibrust/src/ros1/node/actor.rs +++ b/roslibrust/src/ros1/node/actor.rs @@ -39,6 +39,9 @@ pub enum NodeMsg { topic: String, publishers: Vec, }, + // This function exists because "shutdown" is one of the XmlRpc Client APIs that is + // technically part of the ROS ecosystem (never really seen it used) + // This results in the node's task ending and the node being dropped. Shutdown, RegisterPublisher { reply: oneshot::Sender>, String>>, @@ -87,13 +90,18 @@ pub enum NodeMsg { }, } +/// Represents a communication handle to an underlying node server +/// The node server handles all communication with ROS Master and keeps +/// track of subscriptions, publishers, etc. +/// Things that need to interact with the node server do so through a command channel +/// Some handles are "root" handles that when dropped also drop the node server. #[derive(Clone)] pub(crate) struct NodeServerHandle { pub(crate) node_server_sender: mpsc::UnboundedSender, // If this handle should keep the underlying node task alive it will hold an // Arc to the underlying node task. This is an option because internal handles // within the node shouldn't keep it alive (e.g. what we hand to xml server) - _node_task: Option>>, + pub(crate) _node_task: Option>>, } impl NodeServerHandle { @@ -146,7 +154,6 @@ impl NodeServerHandle { /// Informs the underlying node server to shutdown /// This will stop all ROS functionality and poison all NodeHandles connected /// to the underlying node server. - // TODO this function should probably be pub(crate) and not pub? pub(crate) fn shutdown(&self) -> Result<(), NodeError> { self.node_server_sender.send(NodeMsg::Shutdown)?; Ok(()) @@ -176,6 +183,49 @@ impl NodeServerHandle { })?) } + /// Registers a publisher with the underlying node server + /// Returns a channel that the raw bytes of a publish can be shoved into to queue the publish + pub(crate) async fn register_publisher_any( + &self, + topic: &str, + topic_type: &str, + msg_definition: &str, + queue_size: usize, + latching: bool, + ) -> Result>, NodeError> { + let (sender, receiver) = oneshot::channel(); + + let md5sum; + let md5sum_res = + roslibrust_codegen::message_definition_to_md5sum(topic_type, msg_definition); + match md5sum_res { + // TODO(lucasw) make a new error type for this? + Err(err) => { + log::error!("{:?}", err); + return Err(NodeError::IoError(io::Error::from( + io::ErrorKind::ConnectionAborted, + ))); + } + Ok(md5sum_rv) => { + md5sum = md5sum_rv; + } + } + + self.node_server_sender.send(NodeMsg::RegisterPublisher { + reply: sender, + topic: topic.to_owned(), + topic_type: topic_type.to_owned(), + queue_size, + msg_definition: msg_definition.to_owned(), + md5sum, + latching, + })?; + let received = receiver.await?; + Ok(received.map_err(|_err| { + NodeError::IoError(io::Error::from(io::ErrorKind::ConnectionAborted)) + })?) + } + pub(crate) async fn unregister_publisher(&self, topic: &str) -> Result<(), NodeError> { let (sender, receiver) = oneshot::channel(); self.node_server_sender.send(NodeMsg::UnregisterPublisher { @@ -237,10 +287,10 @@ impl NodeServerHandle { // This gives a generic closure that operates on byte arrays that we can then store and use freely let server_typeless = move |message: Vec| -> Result, Box> { - let request = serde_rosmsg::from_slice::(&message) + let request = roslibrust_serde_rosmsg::from_slice::(&message) .map_err(|err| RosLibRustError::SerializationError(err.to_string()))?; let response = server(request)?; - Ok(serde_rosmsg::to_vec(&response) + Ok(roslibrust_serde_rosmsg::to_vec(&response) .map_err(|err| RosLibRustError::SerializationError(err.to_string()))?) }; let server_typeless = Box::new(server_typeless); @@ -330,6 +380,8 @@ impl NodeServerHandle { } } +// TODO we sometimes refer to this entity as "Node" and sometimes as "NodeServer" +// we should standardize terminology. /// Represents a single "real" node, typically only one of these is expected per process /// but nothing should specifically prevent that. /// This is sometimes referred to as the NodeServer in the documentation, many NodeHandles can point to one NodeServer @@ -409,6 +461,8 @@ impl Node { node.handle_msg(node_msg).await; } None => { + // This isn't an really expected case? + log::warn!("Node command channel closed, shutting down"); break; } } @@ -799,4 +853,59 @@ impl Node { ))); } } + + // Clears any extant node connections with the ros master + // This is not expected to be called anywhere other than the drop impl + fn shutdown(&mut self) { + // Based on this answer: 3b https://stackoverflow.com/questions/71541765/rust-async-drop + // Make copies of what we need to shut down + let client = self.client.clone(); + let subscriptions = std::mem::take(&mut self.subscriptions); + let publishers = std::mem::take(&mut self.publishers); + let service_servers = std::mem::take(&mut self.service_servers); + let host_addr = self.host_addr; + + // Move copies into a future that will do the clean-ups + let future = async move { + debug!("Start shutdown node"); + // Note: we're ignoring all failures here and doing best effort cleanup + // Many of these log messages will be incorrect until we get our cleanup logic dialed in. + for (topic, _subscriptions) in &subscriptions { + debug!("Node shutdown is cleaning up subscription: {topic}"); + let _ = client.unregister_subscriber(topic).await.map_err(|e| { + error!("Failed to unregister subscriber for topic: {topic} while shutting down node"); + e + }); + debug!("CHECK"); + } + + for (topic, _publication) in &publishers { + debug!("Node shutdown is cleaning up publishing: {topic}"); + let _ = client.unregister_publisher(topic).await.map_err(|e| { + error!("Failed to unregister publisher for topic: {topic} while shutting down node."); + e + }); + } + + for (topic, service_link) in &service_servers { + debug!("Node shutdown is cleaning up service: {topic}"); + let uri = format!("rosrpc://{}:{}", host_addr, service_link.port()); + let _ = client.unregister_service(topic, uri).await.map_err(|e| { + error!("Failed to unregister server server for topic: {topic} while shutting down node."); + e + }); + } + }; + // Spawn shutdown operation in a separate task + tokio::spawn(future); + } +} + +// It is important to clean-up any stray topic / service connections when we shut down +// Goal of this implementation is that the node appears fully dead to ROS after this and +// `rosnode list` / `rosnode info` don't show any remaining connections. +impl Drop for Node { + fn drop(&mut self) { + self.shutdown(); + } } diff --git a/roslibrust/src/ros1/node/handle.rs b/roslibrust/src/ros1/node/handle.rs index 33d02efb..3d2224b3 100644 --- a/roslibrust/src/ros1/node/handle.rs +++ b/roslibrust/src/ros1/node/handle.rs @@ -1,14 +1,15 @@ use super::actor::{Node, NodeServerHandle}; use crate::{ ros1::{ - names::Name, publisher::Publisher, service_client::ServiceClient, subscriber::Subscriber, - NodeError, ServiceServer, + names::Name, publisher::Publisher, publisher::PublisherAny, service_client::ServiceClient, + subscriber::Subscriber, subscriber::SubscriberAny, NodeError, ServiceServer, }, ServiceFn, }; -/// Represents a handle to an underlying [Node]. NodeHandle's can be freely cloned, moved, copied, etc. +/// Represents a handle to an underlying Node. NodeHandle's can be freely cloned, moved, copied, etc. /// This class provides the user facing API for interacting with ROS. +/// The last node handle dropped shuts down the node. #[derive(Clone)] pub struct NodeHandle { inner: NodeServerHandle, @@ -41,6 +42,18 @@ impl NodeHandle { Ok(nh) } + /// This creates a clone() of NodeHandle that doesn't keep the underlying node alive + /// This should be used for things like ServiceServer which wants to be able to talk to the node + /// but doesn't need to keep the node alive. + pub(crate) fn weak_clone(&self) -> NodeHandle { + NodeHandle { + inner: NodeServerHandle { + node_server_sender: self.inner.node_server_sender.clone(), + _node_task: None, + }, + } + } + /// This function may be removed... /// All node handles connect to a backend node server that actually handles the communication with ROS /// If this function returns false, the backend node server has shut down and this handle is invalid. @@ -55,12 +68,34 @@ impl NodeHandle { self.inner.get_client_uri().await } + /// Create a new publisher any arbitrary message type. + /// + /// This function is intended to be used when a message definition was not available at compile time, + /// such as when playing back data from a bag file. + /// This function requires the text of the expanded message definition as would be produced by `gendeps --cat`. + /// See for what this should like. + /// Messages autogenerated with roslibrust_codegen will include this information in their DEFINITION constant. + pub async fn advertise_any( + &self, + topic_name: &str, + topic_type: &str, + msg_definition: &str, + queue_size: usize, + latching: bool, + ) -> Result { + let sender = self + .inner + .register_publisher_any(topic_name, topic_type, msg_definition, queue_size, latching) + .await?; + Ok(PublisherAny::new(topic_name, sender)) + } + /// Create a new publisher for the given type. /// /// This function can be called multiple times to create multiple publishers for the same topic, /// however the FIRST call will establish the queue size and latching behavior for the topic. /// Subsequent calls will simply be given additional handles to the underlying publication. - /// This behavior was chosen to mirror ROS1's API, however it is reccomended to .clone() the returend publisher + /// This behavior was chosen to mirror ROS1's API, however it is recommended to .clone() the returned publisher /// instead of calling this function multiple times. pub async fn advertise( &self, @@ -75,6 +110,18 @@ impl NodeHandle { Ok(Publisher::new(topic_name, sender)) } + pub async fn subscribe_any( + &self, + topic_name: &str, + queue_size: usize, + ) -> Result { + let receiver = self + .inner + .register_subscriber::(topic_name, queue_size) + .await?; + Ok(SubscriberAny::new(receiver)) + } + pub async fn subscribe( &self, topic_name: &str, @@ -113,7 +160,8 @@ impl NodeHandle { .inner .register_service_server::(&service_name, server) .await?; - Ok(ServiceServer::new(service_name, self.clone())) + // Super important. Don't clone self or we create a STRONG NodeHandle that keeps the node alive + Ok(ServiceServer::new(service_name, self.weak_clone())) } // TODO Major: This should probably be moved to NodeServerHandle? diff --git a/roslibrust/src/ros1/publisher.rs b/roslibrust/src/ros1/publisher.rs index aef331de..0718aa98 100644 --- a/roslibrust/src/ros1/publisher.rs +++ b/roslibrust/src/ros1/publisher.rs @@ -16,6 +16,7 @@ use tokio::{ sync::{mpsc, RwLock}, }; +/// The regular Publisher representation returned by calling advertise on a [crate::ros1::NodeHandle]. pub struct Publisher { topic_name: String, sender: mpsc::Sender>, @@ -34,7 +35,7 @@ impl Publisher { /// Queues a message to be send on the related topic. /// Returns when the data has been queued not when data is actually sent. pub async fn publish(&self, data: &T) -> Result<(), PublisherError> { - let data = serde_rosmsg::to_vec(&data)?; + let data = roslibrust_serde_rosmsg::to_vec(&data)?; // TODO this is a pretty dumb... // because of the internal channel used for re-direction this future doesn't // actually complete when the data is sent, but merely when it is queued to be sent @@ -49,6 +50,45 @@ impl Publisher { } } +/// A specialty publisher used when message type is not known at compile time. +/// +/// Relies on user to provide serialized data. Typically used with playback from bag files. +pub struct PublisherAny { + topic_name: String, + sender: mpsc::Sender>, + phantom: PhantomData>, +} + +impl PublisherAny { + pub(crate) fn new(topic_name: &str, sender: mpsc::Sender>) -> Self { + Self { + topic_name: topic_name.to_owned(), + sender, + phantom: PhantomData, + } + } + + /// Queues a message to be send on the related topic. + /// Returns when the data has been queued not when data is actually sent. + /// + /// This expects the data to be the raw bytes of the message body as they would appear going over the wire. + /// See ros1_publish_any.rs example for more details. + /// Body length should be included as first four bytes. + pub async fn publish(&self, data: &Vec) -> Result<(), PublisherError> { + // TODO this is a pretty dumb... + // because of the internal channel used for re-direction this future doesn't + // actually complete when the data is sent, but merely when it is queued to be sent + // This function could probably be non-async + // Or we should do some significant re-work to have it only yield when the data is sent. + self.sender + .send(data.to_vec()) + .await + .map_err(|_| PublisherError::StreamClosed)?; + debug!("Publishing data on topic {}", self.topic_name); + Ok(()) + } +} + pub(crate) struct Publication { topic_type: String, listener_port: u16, @@ -169,10 +209,12 @@ impl Publication { loop { match rx.recv().await { Some(msg_to_publish) => { + trace!("Publish task got message to publish for topic: {topic}"); let mut streams = subscriber_streams.write().await; let mut streams_to_remove = vec![]; + // TODO: we're awaiting in a for loop... Could parallelize here for (stream_idx, stream) in streams.iter_mut().enumerate() { - if let Err(err) = stream.write(&msg_to_publish[..]).await { + if let Err(err) = stream.write_all(&msg_to_publish[..]).await { // TODO: A single failure between nodes that cross host boundaries is probably normal, should make this more robust perhaps debug!("Failed to send data to subscriber: {err}, removing"); streams_to_remove.push(stream_idx); @@ -198,7 +240,9 @@ impl Publication { // Tell the node server to dispose of this publication and unadvertise it // Note: we need to do this in a spawned task or a drop-loop race condition will occur // Dropping publication results in this task being dropped, which can end up canceling the future that is doing the dropping - // if we simpply .await here + // if we simply .await here + // TODO: This allows publisher to clean themselves up iff node remains running after publisher is dropped... + // NodeHandle clean-up is not resulting in a good state clean-up currently.. let nh_copy = node_handle.clone(); let topic = topic.clone(); tokio::spawn(async move { @@ -225,7 +269,6 @@ impl Publication { loop { if let Ok((mut stream, peer_addr)) = tcp_listener.accept().await { info!("Received connection from subscriber at {peer_addr} for topic {topic_name}"); - // Read the connection header: let connection_header = match tcpros::receive_header(&mut stream).await { Ok(header) => header, @@ -250,6 +293,8 @@ impl Publication { if let Some(connection_md5sum) = connection_header.md5sum { if connection_md5sum != "*" { if let Some(local_md5sum) = &responding_conn_header.md5sum { + // TODO(lucasw) is it ok to match any with "*"? + // if local_md5sum != "*" && connection_md5sum != *local_md5sum { if connection_md5sum != *local_md5sum { warn!( "Got subscribe request for {}, but md5sums do not match. Expected {:?}, received {:?}", @@ -272,7 +317,7 @@ impl Publication { .to_bytes(false) .expect("Couldn't serialize connection header"); stream - .write(&response_header_bytes[..]) + .write_all(&response_header_bytes[..]) .await .expect("Unable to respond on tcpstream"); @@ -282,7 +327,7 @@ impl Publication { debug!( "Publication configured to be latching and has last_message, sending" ); - let res = stream.write(last_message).await; + let res = stream.write_all(last_message).await; match res { Ok(_) => {} Err(e) => { @@ -320,8 +365,8 @@ pub enum PublisherError { StreamClosed, } -impl From for PublisherError { - fn from(value: serde_rosmsg::Error) -> Self { +impl From for PublisherError { + fn from(value: roslibrust_serde_rosmsg::Error) -> Self { Self::SerializingError(value.to_string()) } } diff --git a/roslibrust/src/ros1/service_client.rs b/roslibrust/src/ros1/service_client.rs index e8501c63..a5220549 100644 --- a/roslibrust/src/ros1/service_client.rs +++ b/roslibrust/src/ros1/service_client.rs @@ -54,7 +54,7 @@ impl ServiceClient { } pub async fn call(&self, request: &T::Request) -> RosLibRustResult { - let request_payload = serde_rosmsg::to_vec(request) + let request_payload = roslibrust_serde_rosmsg::to_vec(request) .map_err(|err| RosLibRustError::SerializationError(err.to_string()))?; let (response_tx, response_rx) = oneshot::channel(); @@ -69,7 +69,7 @@ impl ServiceClient { self.service_name, result_payload ); - let response: T::Response = serde_rosmsg::from_slice(&result_payload) + let response: T::Response = roslibrust_serde_rosmsg::from_slice(&result_payload) .map_err(|err| RosLibRustError::SerializationError(err.to_string()))?; return Ok(response); } @@ -202,13 +202,14 @@ impl ServiceClientLink { } else { // Parse an error message as the body let error_body = tcpros::receive_body(stream).await?; - let err_msg: String = serde_rosmsg::from_slice(&error_body).map_err(|err| { - log::error!("Failed to parse service call error message: {err}"); - std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Failed to parse service call error message", - ) - })?; + let err_msg: String = + roslibrust_serde_rosmsg::from_slice(&error_body).map_err(|err| { + log::error!("Failed to parse service call error message: {err}"); + std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Failed to parse service call error message", + ) + })?; // TODO probably specific error type for this Err(std::io::Error::new( std::io::ErrorKind::Other, diff --git a/roslibrust/src/ros1/service_server.rs b/roslibrust/src/ros1/service_server.rs index 7748a2d7..5abc3def 100644 --- a/roslibrust/src/ros1/service_server.rs +++ b/roslibrust/src/ros1/service_server.rs @@ -220,7 +220,7 @@ impl ServiceServerLink { warn!("Error from user service method for {service_name}: {e:?}"); let error_string = format!("{:?}", e); - let error_bytes = serde_rosmsg::to_vec(&error_string).unwrap(); + let error_bytes = roslibrust_serde_rosmsg::to_vec(&error_string).unwrap(); let full_response = [vec![0u8], error_bytes].concat(); stream.write_all(&full_response).await.unwrap(); diff --git a/roslibrust/src/ros1/subscriber.rs b/roslibrust/src/ros1/subscriber.rs index a19058df..621c16e6 100644 --- a/roslibrust/src/ros1/subscriber.rs +++ b/roslibrust/src/ros1/subscriber.rs @@ -1,6 +1,7 @@ use crate::ros1::{names::Name, tcpros::ConnectionHeader}; use abort_on_drop::ChildTask; -use roslibrust_codegen::RosMessageType; +use log::*; +use roslibrust_codegen::{RosMessageType, ShapeShifter}; use std::{marker::PhantomData, sync::Arc}; use tokio::{ io::AsyncWriteExt, @@ -27,18 +28,58 @@ impl Subscriber { } pub async fn next(&mut self) -> Option> { + trace!("Subscriber of type {:?} awaiting recv()", T::ROS_TYPE_NAME); let data = match self.receiver.recv().await { - Ok(v) => v, + Ok(v) => { + trace!("Subscriber of type {:?} received data", T::ROS_TYPE_NAME); + v + } Err(RecvError::Closed) => return None, Err(RecvError::Lagged(n)) => return Some(Err(SubscriberError::Lagged(n))), }; - match serde_rosmsg::from_slice::(&data[..]) { - Ok(p) => Some(Ok(p)), + trace!( + "Subscriber of type {:?} deserializing data", + T::ROS_TYPE_NAME + ); + let tick = tokio::time::Instant::now(); + match roslibrust_serde_rosmsg::from_slice::(&data[..]) { + Ok(p) => { + let duration = tick.elapsed(); + trace!( + "Subscriber of type {:?} deserialized data in {duration:?}", + T::ROS_TYPE_NAME + ); + Some(Ok(p)) + } Err(e) => Some(Err(e.into())), } } } +pub struct SubscriberAny { + receiver: broadcast::Receiver>, + _phantom: PhantomData, +} + +impl SubscriberAny { + pub(crate) fn new(receiver: broadcast::Receiver>) -> Self { + Self { + receiver, + _phantom: PhantomData, + } + } + + // pub async fn next(&mut self) -> Option> { + pub async fn next(&mut self) -> Option, SubscriberError>> { + let data = match self.receiver.recv().await { + Ok(v) => v, + Err(RecvError::Closed) => return None, + Err(RecvError::Lagged(n)) => return Some(Err(SubscriberError::Lagged(n))), + }; + Some(Ok(data)) + } +} + pub struct Subscription { subscription_tasks: Vec>, _msg_receiver: broadcast::Receiver>, @@ -105,7 +146,7 @@ impl Subscription { let sender = self.msg_sender.clone(); let publisher_list = self.known_publishers.clone(); let publisher_uri = publisher_uri.to_owned(); - + trace!("Creating new subscription connection for {publisher_uri} on {topic_name}"); let handle = tokio::spawn(async move { if let Ok(mut stream) = establish_publisher_connection( &node_name, @@ -118,8 +159,18 @@ impl Subscription { publisher_list.write().await.push(publisher_uri.to_owned()); // Repeatedly read from the stream until its dry loop { + trace!( + "Subscription to {} receiving from {} is awaiting next body", + topic_name, + publisher_uri + ); match tcpros::receive_body(&mut stream).await { Ok(body) => { + trace!( + "Subscription to {} receiving from {} received body", + topic_name, + publisher_uri + ); let send_result = sender.send(body); if let Err(err) = send_result { log::error!("Unable to send message data due to dropped channel, closing connection: {err}"); @@ -154,7 +205,10 @@ async fn establish_publisher_connection( stream.write_all(&conn_header_bytes[..]).await?; if let Ok(responded_header) = tcpros::receive_header(&mut stream).await { - if conn_header.md5sum == responded_header.md5sum { + if conn_header.md5sum == Some("*".to_string()) + || responded_header.md5sum == Some("*".to_string()) + || conn_header.md5sum == responded_header.md5sum + { log::debug!( "Established connection with publisher for {:?}", conn_header.topic @@ -241,8 +295,8 @@ pub enum SubscriberError { Lagged(u64), } -impl From for SubscriberError { - fn from(value: serde_rosmsg::Error) -> Self { +impl From for SubscriberError { + fn from(value: roslibrust_serde_rosmsg::Error) -> Self { Self::DeserializeError(value.to_string()) } } diff --git a/roslibrust/src/ros1/tcpros.rs b/roslibrust/src/ros1/tcpros.rs index 5041749f..e78d8978 100644 --- a/roslibrust/src/ros1/tcpros.rs +++ b/roslibrust/src/ros1/tcpros.rs @@ -86,6 +86,10 @@ impl ConnectionHeader { // for the purpose of discovering the service type // If you do `rosservice call /my_service` and hit TAB you'll see this field in the connection header // we can ignore it + } else if field.starts_with("response_type=") || field.starts_with("request_type=") { + // More undocumented fields! + // Discovered in testing that some roscpp service servers will set these on service responses + // We can ignore em } else if field.starts_with("error=") { log::error!("Error reported in TCPROS connection header: {field}, full header: {header_data:#?}"); } else { @@ -252,13 +256,15 @@ pub async fn receive_body(stream: &mut TcpStream) -> Result, std::io::Er let mut body_len_bytes = [0u8; 4]; stream.read_exact(&mut body_len_bytes).await?; let body_len = u32::from_le_bytes(body_len_bytes); + trace!("Read length from stream: {}", body_len); // Allocate buffer space for length and body let mut body = vec![0u8; body_len as usize + 4]; // Copy the length into the first four bytes body[..4].copy_from_slice(&body_len.to_le_bytes()); - // Read the body into the buffer + // Read the body into the buffer after the header stream.read_exact(&mut body[4..]).await?; + trace!("Read body of size: {}", body.len()); // Return body Ok(body) diff --git a/roslibrust/tests/ros1_native_integration_tests.rs b/roslibrust/tests/ros1_native_integration_tests.rs index 78f70c6d..2e2facfb 100644 --- a/roslibrust/tests/ros1_native_integration_tests.rs +++ b/roslibrust/tests/ros1_native_integration_tests.rs @@ -12,6 +12,65 @@ mod tests { "assets/ros1_common_interfaces" ); + #[test_log::test(tokio::test)] + async fn test_publish_any() { + // publish a single message in raw bytes and test the received message is as expected + let nh = NodeHandle::new("http://localhost:11311", "test_publish_any") + .await + .unwrap(); + + let publisher = nh + .advertise_any( + "/test_publish_any", + "std_msgs/String", + "string data\n", + 1, + true, + ) + .await + .unwrap(); + + let mut subscriber = nh + .subscribe::("/test_publish_any", 1) + .await + .unwrap(); + + let msg_raw: Vec = vec![8, 0, 0, 0, 4, 0, 0, 0, 116, 101, 115, 116].to_vec(); + publisher.publish(&msg_raw).await.unwrap(); + + let res = + tokio::time::timeout(tokio::time::Duration::from_millis(250), subscriber.next()).await; + let msg = res.unwrap().unwrap().unwrap(); + assert_eq!(msg.data, "test"); + } + + #[test_log::test(tokio::test)] + async fn test_subscribe_any() { + // get a single message in raw bytes and test the bytes are as expected + let nh = NodeHandle::new("http://localhost:11311", "test_subscribe_any") + .await + .unwrap(); + + let publisher = nh + .advertise::("/test_subscribe_any", 1, true) + .await + .unwrap(); + + let mut subscriber = nh.subscribe_any("/test_subscribe_any", 1).await.unwrap(); + + publisher + .publish(&std_msgs::String { + data: "test".to_owned(), + }) + .await + .unwrap(); + + let res = + tokio::time::timeout(tokio::time::Duration::from_millis(250), subscriber.next()).await; + let res = res.unwrap().unwrap().unwrap(); + assert!(res == vec![8, 0, 0, 0, 4, 0, 0, 0, 116, 101, 115, 116]); + } + #[test_log::test(tokio::test)] async fn test_latching() { let nh = NodeHandle::new("http://localhost:11311", "test_latching") @@ -415,7 +474,7 @@ mod tests { // Dropping watchdog at end of function cancels watchdog // This test can hang which gives crappy debug output - let watchdog: abort_on_drop::ChildTask<()> = tokio::spawn(async move { + let _watchdog: abort_on_drop::ChildTask<()> = tokio::spawn(async move { tokio::time::sleep(std::time::Duration::from_secs(2)).await; error!("Test watchdog tripped..."); std::process::exit(-1); @@ -482,4 +541,64 @@ mod tests { let msg = sub.next().await.unwrap().unwrap(); assert_eq!(msg.data, "hello world from rosbridge"); } + + /// Test that we correctly purge references to publishers, subscribers and services servers when a node shuts down + #[test_log::test(tokio::test)] + async fn node_cleanup() { + // Create our node + // this nh controls the lifetimes + let nh = NodeHandle::new("http://localhost:11311", "/test_node_cleanup") + .await + .unwrap(); + + // Create pub, sub, and service server to prove all get cleaned up + let _publisher = nh + .advertise::("/test_cleanup_pub", 1, false) + .await + .unwrap(); + + let _subscriber = nh + .subscribe::("/test_cleanup_sub", 1) + .await + .unwrap(); + + let _service_server = nh + .advertise_service::("/test_cleanup_srv", |_req| { + Ok(Default::default()) + }) + .await + .unwrap(); + + let master_client = roslibrust::ros1::MasterClient::new( + "http://localhost:11311", + "NAN", + "/test_node_cleanup_checker", + ) + .await + .unwrap(); + + let data = master_client.get_system_state().await.unwrap(); + info!("Got data before drop: {data:?}"); + + // Check that our three connections are reported by the ros master before starting + assert!(data.is_publishing("/test_cleanup_pub", "/test_node_cleanup")); + assert!(data.is_subscribed("/test_cleanup_sub", "/test_node_cleanup")); + assert!(data.is_service_provider("/test_cleanup_srv", "/test_node_cleanup")); + + // Drop our node handle + std::mem::drop(nh); + + // Confirm here that Node actually got shut down + debug!("Drop has happened"); + // Delay to allow destructor to complete + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + debug!("sleep is over"); + let data = master_client.get_system_state().await.unwrap(); + info!("Got data after drop: {data:?}"); + + // Check that our three connections are no longer reported by the ros master after dropping + assert!(!data.is_publishing("/test_cleanup_pub", "/test_node_cleanup")); + assert!(!data.is_subscribed("/test_cleanup_sub", "/test_node_cleanup")); + assert!(!data.is_service_provider("/test_cleanup_srv", "/test_node_cleanup")); + } } diff --git a/roslibrust_codegen/Cargo.toml b/roslibrust_codegen/Cargo.toml index ffb2d465..52364561 100644 --- a/roslibrust_codegen/Cargo.toml +++ b/roslibrust_codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "roslibrust_codegen" -version = "0.10.0" +version = "0.11.1" edition = "2021" authors = ["carter ", "ssnover "] license = "MIT" @@ -30,6 +30,7 @@ xml-rs = "0.8" # So that the generate code can find them, and users don't have to added dependencies themselves smart-default = "0.7" serde-big-array = "0.5" +serde_bytes = "0.11" [dev-dependencies] env_logger = "0.10" diff --git a/roslibrust_codegen/src/gen.rs b/roslibrust_codegen/src/gen.rs index ffc369db..1f4de44f 100644 --- a/roslibrust_codegen/src/gen.rs +++ b/roslibrust_codegen/src/gen.rs @@ -38,6 +38,7 @@ pub fn generate_service(service: ServiceFile) -> Result { #request_msg #response_msg + #[allow(dead_code)] pub struct #struct_name { } @@ -108,6 +109,7 @@ pub fn generate_struct(msg: MessageFile) -> Result { // Only if we have constants append the impl if !constants.is_empty() { base.extend(quote! { + #[allow(unused)] impl #struct_name { #(#constants )* } @@ -133,8 +135,11 @@ fn generate_field_definition( .ok_or(Error::new(format!("No Rust type for {}", field.field_type)))? .to_owned(), }; + // Wrap type in appropriate Vec or array wrapper based on array information let rust_field_type = match field.field_type.array_info { - Some(None) => format!("::std::vec::Vec<{rust_field_type}>"), + Some(None) => { + format!("::std::vec::Vec<{rust_field_type}>") + } Some(Some(fixed_length)) => format!("[{rust_field_type}; {fixed_length}]"), None => rust_field_type, }; @@ -184,6 +189,15 @@ fn generate_field_definition( // Larger than 32. const MAX_FIXED_ARRAY_LEN: usize = 32; let serde_line = match field.field_type.array_info { + Some(None) => { + // Special case for Vec, which massively benefit from optimizations in serde_bytes + // This makes deserializing an Image ~97% faster + if field.field_type.field_type == "uint8" { + quote! { #[serde(with = "::roslibrust_codegen::serde_bytes")] } + } else { + quote! {} + } + } Some(Some(fixed_array_len)) if fixed_array_len > MAX_FIXED_ARRAY_LEN => { quote! { #[serde(with = "::roslibrust_codegen::BigArray")] } } diff --git a/roslibrust_codegen/src/integral_types.rs b/roslibrust_codegen/src/integral_types.rs index 230fe8ea..9a984005 100644 --- a/roslibrust_codegen/src/integral_types.rs +++ b/roslibrust_codegen/src/integral_types.rs @@ -1,8 +1,17 @@ +use simple_error::{bail, SimpleError}; + use crate::RosMessageType; /// Matches the integral ros1 type time, with extensions for ease of use /// NOTE: in ROS1 "Time" is not a message in and of itself and std_msgs/Time should be used. /// However, in ROS2 "Time" is a message and part of builtin_interfaces/Time. +// Okay some complexities lurk here that I really don't like +// In ROS1 time is i32 secs and i32 nsecs +// In ROS2 time is i32 secs and u32 nsecs +// How many nsecs are there in a sec? +1e9 which will fit inside of either. +// But ROS really doesn't declare what is valid for nsecs larger than 1e9, how should that be handled? +// How should negative nsecs work anyway? +// https://docs.ros2.org/foxy/api/builtin_interfaces/msg/Time.html #[derive(:: serde :: Deserialize, :: serde :: Serialize, Debug, Default, Clone, PartialEq)] pub struct Time { // Note: rosbridge appears to accept secs and nsecs in for time without issue? @@ -10,22 +19,55 @@ pub struct Time { // This alias is required for ros2 where field has been renamed #[serde(alias = "sec")] - pub secs: u32, + pub secs: i32, // This alias is required for ros2 where field has been renamed #[serde(alias = "nanosec")] - pub nsecs: u32, + pub nsecs: i32, } -impl From for Time { - fn from(val: std::time::SystemTime) -> Self { - let delta = val - .duration_since(std::time::UNIX_EPOCH) - .expect("Failed to convert system time into unix epoch"); - let downcast_secs = u32::try_from(delta.as_secs()).expect("Failed to convert system time to ROS representation, seconds term overflows u32 likely"); - Time { +/// Provide a standard conversion between ROS time and std::time::SystemTime +impl TryFrom for Time { + type Error = SimpleError; + fn try_from(val: std::time::SystemTime) -> Result { + let delta = match val.duration_since(std::time::UNIX_EPOCH) { + Ok(delta) => delta, + Err(e) => bail!("Failed to convert system time into unix epoch: {}", e), + }; + // TODO our current method doesn't try to handel negative times + // It is unclear from ROS documentation how these would be generated or how they should be handled + // For now adopting a strict conversion policy of only converting when it makes clear logical sense + let downcast_secs = match i32::try_from(delta.as_secs()) { + Ok(val) => val, + Err(e) => bail!("Failed to convert seconds to i32: {e:?}"), + }; + let downcast_nanos = match i32::try_from(delta.subsec_nanos()) { + Ok(val) => val, + Err(e) => bail!("Failed to convert nanoseconds to i32: {e:?}"), + }; + Ok(Time { secs: downcast_secs, - nsecs: delta.subsec_nanos(), - } + nsecs: downcast_nanos, + }) + } +} + +/// Provide a standard conversion between ROS time and std::time::SystemTime +impl TryFrom