diff --git a/extra/nt-bridge/.gitignore b/extra/nt-bridge/.gitignore index 67e2f0b..4e10d56 100644 --- a/extra/nt-bridge/.gitignore +++ b/extra/nt-bridge/.gitignore @@ -1,2 +1,6 @@ cmake-build/** -cmake-install/** \ No newline at end of file +cmake-install/** +build/** +install/** +log/** +.vscode/** \ No newline at end of file diff --git a/extra/nt-bridge/CMakeLists.txt b/extra/nt-bridge/CMakeLists.txt index 609854d..594541d 100644 --- a/extra/nt-bridge/CMakeLists.txt +++ b/extra/nt-bridge/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.12) -project(nt-bridge) +project(nt_bridge) if(NOT MSVC) if(NOT CMAKE_BUILD_TYPE) @@ -8,6 +8,8 @@ if(NOT MSVC) endif() endif() +set(CMAKE_CXX_STANDARD 20) + message(STATUS "[NT BRIDGE]: Configuring protobuf...") execute_process( COMMAND cmake @@ -52,3 +54,29 @@ set(Protobuf_PROTOC_EXECUTABLE "${PROJECT_SOURCE_DIR}/protobuf/cmake-install/bin list(APPEND CMAKE_LIBRARY_PATH "${PROJECT_SOURCE_DIR}/protobuf/cmake-install/lib") add_subdirectory(allwpilib) set(BUILD_SHARED_LIBS ${__BUILD_SHARED_LIBS}) + + +find_package(ament_cmake REQUIRED) +find_package(rclcpp REQUIRED) +find_package(sensor_msgs REQUIRED) +find_package(geometry_msgs REQUIRED) +find_package(nav_msgs REQUIRED) + +add_executable(nt_bridge_node "src/main.cpp") +target_link_libraries(nt_bridge_node + wpiutil + wpinet + wpimath + ntcore +) + +ament_target_dependencies(nt_bridge_node + "rclcpp" + "nav_msgs" + "sensor_msgs" + "geometry_msgs" +) +install( + TARGETS nt_bridge_node + DESTINATION lib/${PROJECT_NAME}/) +ament_package() diff --git a/extra/nt-bridge/package.xml b/extra/nt-bridge/package.xml new file mode 100644 index 0000000..b26a507 --- /dev/null +++ b/extra/nt-bridge/package.xml @@ -0,0 +1,23 @@ + + + + nt_bridge + 0.1.0 + Networktable to ROS bridge for UESim + sigma + MIT + + rclcpp + sensor_msgs + geometry_msgs + nav_msgs + + ament_cmake + + + + + ament_cmake + + diff --git a/extra/nt-bridge/src/main.cpp b/extra/nt-bridge/src/main.cpp index e69de29..34a7379 100644 --- a/extra/nt-bridge/src/main.cpp +++ b/extra/nt-bridge/src/main.cpp @@ -0,0 +1,154 @@ +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + + +class NTBridgeNode : public rclcpp::Node { +public: + NTBridgeNode( + bool use_nt_client = false, + int nt_client_team = -1, + const char* nt_client_server = "localhost", + unsigned int nt_client_port = 0 + ) : Node("nt_bridge_node") { + nt::NetworkTableInstance inst = nt::NetworkTableInstance::GetDefault(); + + bool using_client = false; + if(use_nt_client) { + if(nt_client_team >= 0) { + inst.StartClient4("nt_bridge_node"); + inst.SetServerTeam( + nt_client_team, + nt_client_port + ); + using_client = true; + RCLCPP_INFO(this->get_logger(), "Started NT client using team number: %d", nt_client_team); + } + if(nt_client_server != nullptr) { + inst.StartClient4("nt_bridge_node"); + inst.SetServer( + nt_client_server, + nt_client_port + ); + using_client = true; + RCLCPP_INFO(this->get_logger(), "Started NT client using server: %s", nt_client_server); + } + } + if(!using_client) { + inst.StartServer(); + RCLCPP_INFO(this->get_logger(), "Started NT server."); + } + + this->nt_scan_sub = inst.GetRawTopic("uesim/scan").Subscribe( "PointXYZ_[]", {} ); + this->nt_pose_sub = inst.GetFloatArrayTopic("uesim/pose").Subscribe( {} ); + + inst.AddListener(this->nt_scan_sub, nt::EventFlags::kValueAll, std::bind(&NTBridgeNode::nt_scan_cb, this, std::placeholders::_1)); + inst.AddListener(this->nt_pose_sub, nt::EventFlags::kValueAll, std::bind(&NTBridgeNode::nt_pose_cb, this, std::placeholders::_1)); + + this->scan_pub = this->create_publisher("/uesim/scan", 1); + this->pose_pub = this->create_publisher("/uesim/pose", 1); + } + ~NTBridgeNode() = default; + +protected: + void nt_scan_cb(const nt::Event& event) { + const nt::Value& val = event.GetValueEventData()->value; + if( val.IsRaw() ) { + std::span data = val.GetRaw(); + const size_t + nelems = data.size() / 16, + nbytes = nelems * 16; + + sensor_msgs::msg::PointCloud2 cloud; + cloud.data.resize(nbytes); + memcpy(cloud.data.data(), data.data(), nbytes); + + cloud.width = nelems; + cloud.height = 1; + cloud.is_bigendian = false; + cloud.point_step = 16; + cloud.row_step = nbytes; + cloud.is_dense = true; + cloud.header.frame_id = "1"; + + cloud.fields.resize(3); + cloud.fields[0].name = "x"; + cloud.fields[0].offset = 0; + cloud.fields[0].datatype = sensor_msgs::msg::PointField::FLOAT32; + cloud.fields[0].count = 1; + cloud.fields[1].name = "y"; + cloud.fields[1].offset = 4; + cloud.fields[1].datatype = sensor_msgs::msg::PointField::FLOAT32; + cloud.fields[1].count = 1; + cloud.fields[2].name = "z"; + cloud.fields[2].offset = 8; + cloud.fields[2].datatype = sensor_msgs::msg::PointField::FLOAT32; + cloud.fields[2].count = 1; + + cloud.header.stamp.sec = static_cast( val.time() / 1000000L ); + cloud.header.stamp.nanosec = static_cast( val.time() % 1000000L ) * 1000U; + + this->scan_pub->publish(cloud); + + RCLCPP_INFO(this->get_logger(), "Published Scan!"); + } + } + void nt_pose_cb(const nt::Event& event) { + const nt::Value& val = event.GetValueEventData()->value; + if( val.IsFloatArray() ) { + std::span data = val.GetFloatArray(); + + if(data.size() >= 7) { + geometry_msgs::msg::PoseStamped pose; + + pose.pose.position.x = static_cast( data[0] ); + pose.pose.position.y = static_cast( data[1] ); + pose.pose.position.z = static_cast( data[2] ); + + pose.pose.orientation.w = static_cast( data[3] ); + pose.pose.orientation.x = static_cast( data[4] ); + pose.pose.orientation.y = static_cast( data[5] ); + pose.pose.orientation.z = static_cast( data[6] ); + + pose.header.stamp.sec = static_cast( val.time() / 1000000L ); + pose.header.stamp.nanosec = static_cast( val.time() % 1000000L ) * 1000U; + + this->pose_pub->publish(pose); + + RCLCPP_INFO(this->get_logger(), "Published Pose!"); + } + } + } + + +protected: + nt::RawSubscriber nt_scan_sub; + nt::FloatArraySubscriber nt_pose_sub; + + rclcpp::Publisher::SharedPtr scan_pub; + rclcpp::Publisher::SharedPtr pose_pub; + + // rclcpp::Subscription grid_sub; + +}; + + +int main(int argc, char** argv) { + + rclcpp::init(argc, argv); + rclcpp::spin(std::make_shared()); + rclcpp::shutdown(); + + return 0; + +} diff --git a/src/ldrp/perception.cpp b/src/ldrp/perception.cpp index 113937b..017190a 100644 --- a/src/ldrp/perception.cpp +++ b/src/ldrp/perception.cpp @@ -66,17 +66,49 @@ namespace util { return (static_cast(seconds) * 1000000L) + (static_cast(nanoseconds) / 1000L); } + + void _test(rclcpp::Node* node) { + pcl::PointCloud _c; + sensor_msgs::msg::PointCloud2 _c2; + pcl::toROSMsg(_c, _c2); + RCLCPP_INFO(node->get_logger(), + "\n-------------------------------- TESTING --------------------------------" + "\nPointXYZ \"frame_id\": %s" + "\nPointCloud2 total fields: %ld" + "\nPointCloud2 field #1: { name: %s, offset: %d, datatype: %d, count: %d }" + "\nPointCloud2 field #2: { name: %s, offset: %d, datatype: %d, count: %d }" + "\nPointCloud2 field #3: { name: %s, offset: %d, datatype: %d, count: %d }" + "\n-------------------------------------------------------------------------", + _c.header.frame_id.c_str(), + _c2.fields.size(), + _c2.fields[0].name.c_str(), + _c2.fields[0].offset, + _c2.fields[0].datatype, + _c2.fields[0].count, + _c2.fields[1].name.c_str(), + _c2.fields[1].offset, + _c2.fields[1].datatype, + _c2.fields[1].count, + _c2.fields[2].name.c_str(), + _c2.fields[2].offset, + _c2.fields[2].datatype, + _c2.fields[2].count + ); + } + }; PerceptionNode::PerceptionNode() : rclcpp::Node("perception_node") { RCLCPP_INFO(this->get_logger(), "Perception Node Initialization!"); // setup subs/pubs - this->scan_sub = this->create_subscription("/cloud_all_fields_fullframe", 1, + this->scan_sub = this->create_subscription("/uesim/scan", 1, std::bind(&PerceptionNode::scan_cb, this, std::placeholders::_1)); - this->pose_sub = this->create_subscription("/dlio/odom_node/pose", 1, + this->pose_sub = this->create_subscription("/uesim/pose", 1, std::bind(&PerceptionNode::pose_cb, this, std::placeholders::_1)); - this->grid_pub = this->create_publisher("/obstacle_grid", 1); + this->grid_pub = this->create_publisher("/ldrp/obstacle_grid", 1); + + // util::_test(this); //get parameters util::declare_param(this, "map_resolution_cm", this->_config.map_resolution_cm, this->_config.map_resolution_cm); @@ -132,57 +164,88 @@ PerceptionNode::~PerceptionNode() {} void PerceptionNode::scan_cb(const sensor_msgs::msg::PointCloud2::ConstSharedPtr& scan) { - + // this->sampler_mutex.lock(); RCLCPP_INFO(this->get_logger(), "Scan callback called!"); pcl::PointCloud::Ptr point_cloud = std::make_shared>(); pcl::fromROSMsg(*scan, *point_cloud); const int64_t ts = util::constructTimestampMicros(scan->header.stamp.sec, scan->header.stamp.nanosec); + this->sampler_mutex.lock(); this->scan_sampler.insert( ts, point_cloud ); this->scan_sampler.updateMin(ts - util::floatSecondsToIntMicros(this->_config.scan_matching_history_range_s)); + const size_t current_nsamples = this->scan_sampler.getSamples().size(); + this->sampler_mutex.unlock(); RCLCPP_INFO(this->get_logger(), - "\nScan sample timestamp (ms): %ld" - "\nTotal scan samples: %ld", + "\n\tScan sample timestamp (ms): %ld" + "\n\tTotal scan samples: %ld", ts, - this->scan_sampler.getSamples().size() + current_nsamples ); + // this->sampler_mutex.unlock(); } void PerceptionNode::pose_cb(const geometry_msgs::msg::PoseStamped::ConstSharedPtr& pose) { - RCLCPP_DEBUG(this->get_logger(), "Transform callback called!"); + // this->sampler_mutex.lock(); + RCLCPP_INFO(this->get_logger(), "Transform callback called!"); const int64_t trfm_target_ts = util::constructTimestampMicros(pose->header.stamp.sec, pose->header.stamp.nanosec); + this->sampler_mutex.lock(); const typename decltype(this->scan_sampler)::ElemT* sample = this->scan_sampler.sampleTimestamped(trfm_target_ts); + this->sampler_mutex.unlock(); // if scan doesn't exist - if(!sample) return; + if(!sample) { + RCLCPP_INFO(this->get_logger(), + "Invalid sample recieved!" + "\n\tTransform timestamp (ms): %ld", + trfm_target_ts + ); + // this->sampler_mutex.unlock(); + return; + } // transform age - scan age const int64_t ts_diff = trfm_target_ts - sample->first; // if scan came after the transform do not use it - if(ts_diff < 0) return; + // if(ts_diff < 0) { + // RCLCPP_INFO(this->get_logger(), + // "Invalid timestamp delta!" + // "\n\tTransform timestamp (ms): %ld" + // "\n\tSampled timestamp (ms): %ld" + // "\n\tTimestamp difference (ms): %ld", + // trfm_target_ts, + // sample->first, + // ts_diff + // ); + // this->sampler_mutex.unlock(); + // return; + // } // valid case to use transform with scan - else if(ts_diff < this->_config.scan_matching_history_range_s) { + if(abs(ts_diff) < util::floatSecondsToIntMicros(this->_config.scan_matching_history_range_s)) { // config for 'epsilon' pcl::PointCloud::Ptr scan = sample->second; + this->sampler_mutex.lock(); this->scan_sampler.updateMin(sample->first + 1); // don't use 'sample' after here since it is invalid + const size_t current_nsamples = this->scan_sampler.getSamples().size(); + this->sampler_mutex.unlock(); RCLCPP_INFO(this->get_logger(), - "\nTransform timestamp (ms): %ld" - "\nSampled timestamp (ms): %ld" - "\nTimestamp difference (ms): %ld" - "\nTotal scan samples: %ld", + "\n\tTransform timestamp (ms): %ld" + "\n\tSampled timestamp (ms): %ld" + "\n\tTimestamp difference (ms): %ld" + "\n\tTotal scan samples: %ld", trfm_target_ts, sample->first, ts_diff, - this->scan_sampler.getSamples().size() + current_nsamples ); + // this->sampler_mutex.unlock(); const Eigen::Quaternionf quat{ static_cast(pose->pose.orientation.w), @@ -204,6 +267,7 @@ void PerceptionNode::pose_cb(const geometry_msgs::msg::PoseStamped::ConstSharedP this->process_and_export(*scan, *reinterpret_cast(&pos)); } + // this->sampler_mutex.unlock(); } @@ -344,4 +408,17 @@ void PerceptionNode::process_and_export(const pcl::PointCloud& cl this->grid_pub->publish(out_grid); + RCLCPP_INFO(this->get_logger(), + "Grid Updated!" + "\n\tOrigin: (%f, %f)" + "\n\tDims: (%d, %d)" + "\n\tSize: (%f, %f)", + _origin.x(), + _origin.y(), + _grid_size.x(), + _grid_size.y(), + _grid_size.x() * this->accumulator.cellRes(), + _grid_size.y() * this->accumulator.cellRes() + ); + } diff --git a/src/ldrp/perception.hpp b/src/ldrp/perception.hpp index d995171..47e3c0c 100644 --- a/src/ldrp/perception.hpp +++ b/src/ldrp/perception.hpp @@ -29,6 +29,8 @@ #include "./grid.hpp" #include "./timestamp_sampler.hpp" +#include + #include #include #include @@ -62,12 +64,14 @@ class PerceptionNode : public rclcpp::Node { TimestampSampler::Ptr, int64_t> scan_sampler{}; QuantizedRatioGrid accumulator{}; + std::mutex sampler_mutex{}; + struct { // bool // scan_matching_skip_invalid = false; double - scan_matching_history_range_s = 0.25, + scan_matching_history_range_s = 1.0, map_resolution_cm = 5., pmf_max_range_cm = 250., max_z_thresh_cm = 100., diff --git a/src/ldrp/timestamp_sampler.hpp b/src/ldrp/timestamp_sampler.hpp index dc3d9c6..27b3a4e 100644 --- a/src/ldrp/timestamp_sampler.hpp +++ b/src/ldrp/timestamp_sampler.hpp @@ -35,25 +35,10 @@ class TimestampSampler { /** Add a new sample to the buffer */ inline void insert(TimeT time, const Type& sample) - { this->insert(time, sample); } + { this->_insert(time, Type{ sample }); } /** Add a new sample to the buffer */ - void insert(TimeT time, Type&& sample) { - if(this->samples.size() <= 0 || time > this->samples.back().first) { - this->samples.emplace_back(time, std::forward(sample)); - } else { - auto after = std::upper_bound(this->samples.begin(), this->samples.end(), time, &This_T::t_less__); - auto before = after - 1; - if(after == this->samples.begin()) { - this->samples.insert(after, std::pair{time, std::forward(sample)}); - } else { - if(before == this->samples.begin() || before->first < time) { - this->samples.insert(after, std::pair{time, std::forward(sample)}); - } else { - before->second = sample; - } - } - } - } + inline void insert(TimeT time, Type&& sample) + { this->_insert(time, sample); } /** Erase all the elements */ inline void clear() { this->samples.clear(); } @@ -82,6 +67,24 @@ class TimestampSampler { protected: + void _insert(TimeT time, Type&& sample) { + if(this->samples.size() <= 0 || time > this->samples.back().first) { + this->samples.emplace_back(time, std::forward(sample)); + } else { + auto after = std::upper_bound(this->samples.begin(), this->samples.end(), time, &This_T::t_less__); + auto before = after - 1; + if(after == this->samples.begin()) { + this->samples.insert(after, std::pair{time, std::forward(sample)}); + } else { + if(before == this->samples.begin() || before->first < time) { + this->samples.insert(after, std::pair{time, std::forward(sample)}); + } else { + before->second = sample; + } + } + } + } + void enforceBound() { auto last = this->samples.begin(); for(; last < this->samples.end() && last->first < this->absolute_bound; last++);