Initial import of FaRui Autoware Dual-Orin Stack
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
build
|
||||
install
|
||||
log
|
||||
|
||||
.cache
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
COLCON_IGNORE
|
||||
@@ -0,0 +1,48 @@
|
||||
# 海康威视工业相机ROS2驱动
|
||||
|
||||
支持列表:
|
||||
|
||||
* GIGE相机(测试相机:MV-CS016-10GC)
|
||||
|
||||
* USB相机(未测试)
|
||||
|
||||
---
|
||||
|
||||
依赖项安装:
|
||||
|
||||
1. 在官网下载并安装MVS驱动包。
|
||||
|
||||
2. 运行MVS可视化工具,配置相机参数。设置相机名称(user_name, device_id),本项目将根据相机名称寻找对应设备。
|
||||
|
||||
---
|
||||
|
||||
独立节点(standalone)运行方式:
|
||||
|
||||
```bash
|
||||
ros2 launch hikvision_ros2_driver standalone.launch.yaml camera_name:=<camera_name>
|
||||
```
|
||||
|
||||
进程内通信(component)运行方式:
|
||||
|
||||
```bash
|
||||
ros2 launch hikvision_ros2_driver component.launch.yaml camera_name:=<camera_name> container_name:=<container_name>
|
||||
```
|
||||
|
||||
* 需要将camera_name替换成对应相机的名称。
|
||||
|
||||
* 对于进程内通信,需要提前启动`component_container`,并将container_name设置为对应container的名称。
|
||||
|
||||
---
|
||||
|
||||
发布话题:
|
||||
|
||||
* raw/image [sensor_msgs/msg/Image]: 相机原始数据,格式可以在MVS工具中修改,默认情况下应该是bayer格式。
|
||||
* info [hikvision_interface/msg/HikImageInfo]: 图像元数据,包括时间戳、曝光长度、增益大小、白平衡参数等。
|
||||
|
||||
---
|
||||
|
||||
补充:
|
||||
|
||||
* 不建议对bayer格式的原始数据使用jpeg压缩。即当相机原始数据格式为bayer时,不建议订阅`raw/image/compressed`话题。
|
||||
|
||||
* rosbag录制时建议开启zstd压缩以节省空间。(ros2 bag record -s mcap --storage-preset-profile zstd_fast)
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,27 @@
|
||||
cmake_minimum_required(VERSION 3.8)
|
||||
project(hikvision_interface)
|
||||
|
||||
# find dependencies
|
||||
find_package(ament_cmake REQUIRED)
|
||||
find_package(std_msgs REQUIRED)
|
||||
find_package(rosidl_default_generators REQUIRED)
|
||||
|
||||
rosidl_generate_interfaces(${PROJECT_NAME}
|
||||
"msg/HikImageInfo.msg"
|
||||
DEPENDENCIES std_msgs
|
||||
)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
find_package(ament_lint_auto REQUIRED)
|
||||
# the following line skips the linter which checks for copyrights
|
||||
# comment the line when a copyright and license is added to all source files
|
||||
set(ament_cmake_copyright_FOUND TRUE)
|
||||
# the following line skips cpplint (only works in a git repo)
|
||||
# comment the line when this package is in a git repo and when
|
||||
# a copyright and license is added to all source files
|
||||
set(ament_cmake_cpplint_FOUND TRUE)
|
||||
ament_lint_auto_find_test_dependencies()
|
||||
endif()
|
||||
|
||||
ament_export_dependencies(std_msgs rosidl_default_runtime)
|
||||
ament_package()
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
# header
|
||||
std_msgs/Header header
|
||||
# sensor stamp
|
||||
builtin_interfaces/Time dev_stamp
|
||||
uint32 frame_num
|
||||
# acqusistion
|
||||
float32 gain
|
||||
float32 exposure
|
||||
# white balance
|
||||
uint32 red
|
||||
uint32 green
|
||||
uint32 blue
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
||||
<package format="3">
|
||||
<name>hikvision_interface</name>
|
||||
<version>0.0.0</version>
|
||||
<description>ros2 interface for hikvision camera</description>
|
||||
<maintainer email="41963211+xinyang-go@users.noreply.github.com">loc</maintainer>
|
||||
<license>Apache-2.0</license>
|
||||
|
||||
<buildtool_depend>ament_cmake</buildtool_depend>
|
||||
|
||||
<build_depend>std_msgs</build_depend>
|
||||
<exec_depend>std_msgs</exec_depend>
|
||||
<buildtool_depend>rosidl_default_generators</buildtool_depend>
|
||||
<exec_depend>rosidl_default_runtime</exec_depend>
|
||||
<member_of_group>rosidl_interface_packages</member_of_group>
|
||||
|
||||
<test_depend>ament_lint_auto</test_depend>
|
||||
<test_depend>ament_lint_common</test_depend>
|
||||
|
||||
<export>
|
||||
<build_type>ament_cmake</build_type>
|
||||
</export>
|
||||
</package>
|
||||
@@ -0,0 +1,3 @@
|
||||
BasedOnStyle: Google
|
||||
IndentWidth: 4
|
||||
ColumnLimit: 120
|
||||
@@ -0,0 +1,50 @@
|
||||
cmake_minimum_required(VERSION 3.8)
|
||||
project(hikvision_ros2_driver)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_EXTENSIONS ON)
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE Release)
|
||||
endif()
|
||||
|
||||
add_compile_options(-Wall)
|
||||
|
||||
# find ros dependencies
|
||||
find_package(ament_cmake_auto REQUIRED)
|
||||
ament_auto_find_build_dependencies()
|
||||
|
||||
# find hikvision sdk
|
||||
list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
|
||||
find_package(MVS REQUIRED)
|
||||
|
||||
# build components
|
||||
ament_auto_add_library(${PROJECT_NAME} SHARED
|
||||
src/hikvision_ros2_driver.cpp
|
||||
)
|
||||
target_include_directories(${PROJECT_NAME} PUBLIC include)
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE ${MVS_INCLUDE_DIR})
|
||||
ament_target_dependencies(${PROJECT_NAME} driver_timestamp_sync)
|
||||
target_link_libraries(${PROJECT_NAME} ${MVS_LIBRARIES})
|
||||
|
||||
# register components
|
||||
rclcpp_components_register_node(${PROJECT_NAME}
|
||||
PLUGIN "hikvision_ros2_driver::HikvisionDriver"
|
||||
EXECUTABLE ${PROJECT_NAME}_node
|
||||
)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
find_package(ament_lint_auto REQUIRED)
|
||||
# the following line skips the linter which checks for copyrights
|
||||
# comment the line when a copyright and license is added to all source files
|
||||
set(ament_cmake_copyright_FOUND TRUE)
|
||||
# the following line skips cpplint (only works in a git repo)
|
||||
# comment the line when this package is in a git repo and when
|
||||
# a copyright and license is added to all source files
|
||||
set(ament_cmake_cpplint_FOUND TRUE)
|
||||
ament_lint_auto_find_test_dependencies()
|
||||
endif()
|
||||
|
||||
ament_auto_package(INSTALL_TO_SHARE
|
||||
launch
|
||||
)
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
if(DEFINED ENV{MVCAM_SDK_PATH})
|
||||
set(MVCAM_SDK_PATH $ENV{MVCAM_SDK_PATH} CACHE PATH "hikvision sdk path")
|
||||
else()
|
||||
set(MVCAM_SDK_PATH /opt/MVS CACHE PATH "hikvision sdk path")
|
||||
endif()
|
||||
|
||||
# find header
|
||||
find_path(MVS_INCLUDE_DIR
|
||||
NAMES MvCameraControl.h
|
||||
PATHS ${MVCAM_SDK_PATH}/include
|
||||
)
|
||||
|
||||
# find libraries
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64")
|
||||
find_library(MVS_LIBRARIES
|
||||
NAMES MvCameraControl
|
||||
PATHS ${MVCAM_SDK_PATH}/lib/64
|
||||
)
|
||||
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64")
|
||||
find_library(MVS_LIBRARIES
|
||||
NAMES MvCameraControl
|
||||
PATHS ${MVCAM_SDK_PATH}/lib/aarch64
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
message(VERBOSE "MVCAM_SDK_PATH: ${MVCAM_SDK_PATH}")
|
||||
message(VERBOSE "MVS_INCLUDE_DIR: ${MVS_INCLUDE_DIR}")
|
||||
message(VERBOSE "MVS_LIBRARIES: ${MVS_LIBRARIES}")
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(MVS
|
||||
FOUND_VAR MVS_FOUND
|
||||
REQUIRED_VARS
|
||||
MVS_INCLUDE_DIR
|
||||
MVS_LIBRARIES
|
||||
)
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
#ifndef HIKVISION_ROS2_DRIVER_HPP
|
||||
#define HIKVISION_ROS2_DRIVER_HPP
|
||||
|
||||
#include <rclcpp/rclcpp.hpp>
|
||||
|
||||
namespace hikvision_ros2_driver {
|
||||
|
||||
class HikvisionDriver : public rclcpp::Node {
|
||||
public:
|
||||
HikvisionDriver(const rclcpp::NodeOptions &options);
|
||||
~HikvisionDriver();
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> pImpl;
|
||||
};
|
||||
|
||||
} // namespace hikvision_ros2_driver
|
||||
|
||||
#endif // HIKVISION_ROS2_DRIVER_HPP
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
launch:
|
||||
- arg:
|
||||
name: camera_name
|
||||
- arg:
|
||||
name: container_name
|
||||
- arg:
|
||||
name: sync_mode
|
||||
default: freerun
|
||||
- arg:
|
||||
name: trigger_activation
|
||||
default: rising_edge
|
||||
- arg:
|
||||
name: trigger_delay_us
|
||||
default: '0.0'
|
||||
- arg:
|
||||
name: device_timestamp_tick_hz
|
||||
default: '0.0'
|
||||
- arg:
|
||||
name: use_timestamp_sync
|
||||
default: 'true'
|
||||
- arg:
|
||||
name: time_offset_sec
|
||||
default: '0.0'
|
||||
- arg:
|
||||
name: bias_alpha
|
||||
default: '0.01'
|
||||
- arg:
|
||||
name: jump_warn_threshold_ms
|
||||
default: '1000'
|
||||
- arg:
|
||||
name: clamp_backward_time
|
||||
default: 'true'
|
||||
- arg:
|
||||
name: timestamp_diagnostics
|
||||
default: 'false'
|
||||
- arg:
|
||||
name: timestamp_diagnostics_throttle_ms
|
||||
default: '2000'
|
||||
|
||||
- load_composable_node:
|
||||
target: $(var container_name)
|
||||
composable_node:
|
||||
- pkg: hikvision_ros2_driver
|
||||
plugin: hikvision_ros2_driver::HikvisionDriver
|
||||
name: hikvision_ros2_driver_node
|
||||
namespace: /driver/hikvision/$(var camera_name)
|
||||
param:
|
||||
- name: camera_name
|
||||
value: $(var camera_name)
|
||||
- name: sync_mode
|
||||
value: $(var sync_mode)
|
||||
- name: trigger_activation
|
||||
value: $(var trigger_activation)
|
||||
- name: trigger_delay_us
|
||||
value: $(var trigger_delay_us)
|
||||
- name: device_timestamp_tick_hz
|
||||
value: $(var device_timestamp_tick_hz)
|
||||
- name: use_timestamp_sync
|
||||
value: $(var use_timestamp_sync)
|
||||
- name: time_offset_sec
|
||||
value: $(var time_offset_sec)
|
||||
- name: bias_alpha
|
||||
value: $(var bias_alpha)
|
||||
- name: jump_warn_threshold_ms
|
||||
value: $(var jump_warn_threshold_ms)
|
||||
- name: clamp_backward_time
|
||||
value: $(var clamp_backward_time)
|
||||
- name: timestamp_diagnostics
|
||||
value: $(var timestamp_diagnostics)
|
||||
- name: timestamp_diagnostics_throttle_ms
|
||||
value: $(var timestamp_diagnostics_throttle_ms)
|
||||
extra_arg:
|
||||
- name: use_intra_process_comms
|
||||
value: 'true'
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
launch:
|
||||
- arg:
|
||||
name: camera_name
|
||||
- arg:
|
||||
name: sync_mode
|
||||
default: freerun
|
||||
- arg:
|
||||
name: trigger_activation
|
||||
default: rising_edge
|
||||
- arg:
|
||||
name: trigger_delay_us
|
||||
default: '0.0'
|
||||
- arg:
|
||||
name: device_timestamp_tick_hz
|
||||
default: '0.0'
|
||||
- arg:
|
||||
name: use_timestamp_sync
|
||||
default: 'true'
|
||||
- arg:
|
||||
name: time_offset_sec
|
||||
default: '0.0'
|
||||
- arg:
|
||||
name: bias_alpha
|
||||
default: '0.01'
|
||||
- arg:
|
||||
name: jump_warn_threshold_ms
|
||||
default: '1000'
|
||||
- arg:
|
||||
name: clamp_backward_time
|
||||
default: 'true'
|
||||
- arg:
|
||||
name: timestamp_diagnostics
|
||||
default: 'false'
|
||||
- arg:
|
||||
name: timestamp_diagnostics_throttle_ms
|
||||
default: '2000'
|
||||
|
||||
- node:
|
||||
pkg: hikvision_ros2_driver
|
||||
exec: hikvision_ros2_driver_node
|
||||
namespace: /driver/hikvision/$(var camera_name)
|
||||
param:
|
||||
- name: camera_name
|
||||
value: $(var camera_name)
|
||||
- name: sync_mode
|
||||
value: $(var sync_mode)
|
||||
- name: trigger_activation
|
||||
value: $(var trigger_activation)
|
||||
- name: trigger_delay_us
|
||||
value: $(var trigger_delay_us)
|
||||
- name: device_timestamp_tick_hz
|
||||
value: $(var device_timestamp_tick_hz)
|
||||
- name: use_timestamp_sync
|
||||
value: $(var use_timestamp_sync)
|
||||
- name: time_offset_sec
|
||||
value: $(var time_offset_sec)
|
||||
- name: bias_alpha
|
||||
value: $(var bias_alpha)
|
||||
- name: jump_warn_threshold_ms
|
||||
value: $(var jump_warn_threshold_ms)
|
||||
- name: clamp_backward_time
|
||||
value: $(var clamp_backward_time)
|
||||
- name: timestamp_diagnostics
|
||||
value: $(var timestamp_diagnostics)
|
||||
- name: timestamp_diagnostics_throttle_ms
|
||||
value: $(var timestamp_diagnostics_throttle_ms)
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
||||
<package format="3">
|
||||
<name>hikvision_ros2_driver</name>
|
||||
<version>0.0.0</version>
|
||||
<description>ros2 driver for hikvision camera</description>
|
||||
<maintainer email="41963211+xinyang-go@users.noreply.github.com">xinyang</maintainer>
|
||||
<license>Apache-2.0</license>
|
||||
|
||||
<buildtool_depend>ament_cmake_auto</buildtool_depend>
|
||||
|
||||
<depend>rclcpp</depend>
|
||||
<depend>rclcpp_components</depend>
|
||||
<depend>sensor_msgs</depend>
|
||||
<depend>image_transport</depend>
|
||||
<depend>image_transport_plugins</depend>
|
||||
<depend>hikvision_interface</depend>
|
||||
<depend>driver_timestamp_sync</depend>
|
||||
|
||||
<test_depend>ament_lint_auto</test_depend>
|
||||
<test_depend>ament_lint_common</test_depend>
|
||||
|
||||
<export>
|
||||
<build_type>ament_cmake</build_type>
|
||||
</export>
|
||||
</package>
|
||||
+505
@@ -0,0 +1,505 @@
|
||||
#include "hikvision_ros2_driver/hikvision_ros2_driver.hpp"
|
||||
|
||||
// ros
|
||||
#include <hikvision_interface/msg/hik_image_info.hpp>
|
||||
#include <image_transport/image_transport.hpp>
|
||||
#include <sensor_msgs/image_encodings.hpp>
|
||||
#include <builtin_interfaces/msg/time.hpp>
|
||||
#include <driver_timestamp_sync/timestamp_sync.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
// hikvision sdk
|
||||
#include <MvCameraControl.h>
|
||||
|
||||
#define MV_CHECK_THROW(logger, func, ...) \
|
||||
do { \
|
||||
int nRet = func(__VA_ARGS__); \
|
||||
if (MV_OK != nRet) { \
|
||||
RCLCPP_ERROR(logger, "hikvision sdk error: " #func " = %d", nRet); \
|
||||
throw std::runtime_error("hikvision sdk call failed: " #func); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
using hikvision_interface::msg::HikImageInfo;
|
||||
|
||||
namespace hikvision_ros2_driver {
|
||||
namespace {
|
||||
|
||||
builtin_interfaces::msg::Time ToBuiltinTime(const rclcpp::Time & stamp)
|
||||
{
|
||||
builtin_interfaces::msg::Time builtin;
|
||||
const auto total_ns = stamp.nanoseconds();
|
||||
builtin.sec = static_cast<int32_t>(total_ns / 1000000000ll);
|
||||
builtin.nanosec = static_cast<uint32_t>(total_ns % 1000000000ll);
|
||||
return builtin;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct HikvisionDriver::Impl {
|
||||
std::unique_ptr<rclcpp::Logger> logger;
|
||||
|
||||
void *handle{nullptr};
|
||||
std::string camera_name;
|
||||
std::string sync_mode{"freerun"};
|
||||
std::string trigger_activation{"rising_edge"};
|
||||
double trigger_delay_us{0.0};
|
||||
double device_timestamp_tick_hz{0.0};
|
||||
bool use_timestamp_sync{true};
|
||||
bool timestamp_diagnostics{false};
|
||||
int timestamp_diagnostics_throttle_ms{2000};
|
||||
driver_timestamp_sync::TimestampSync timestamp_sync;
|
||||
image_transport::Publisher img_pub;
|
||||
std::shared_ptr<rclcpp::Publisher<HikImageInfo>> p_info_pub;
|
||||
|
||||
void configure_trigger(HikvisionDriver * node);
|
||||
double resolve_device_timestamp_tick_hz(HikvisionDriver * node, MV_CC_DEVICE_INFO * device_info);
|
||||
std::optional<rclcpp::Time> convert_device_stamp_to_ros_time(uint64_t raw_dev_tick) const;
|
||||
uint32_t device_tick_ms(uint64_t raw_dev_tick) const;
|
||||
rclcpp::Time compute_header_stamp(HikvisionDriver * node, const MV_FRAME_OUT_INFO_EX & frame_info,
|
||||
const rclcpp::Time & host_rx_time, uint32_t converted_device_tick_ms, uint64_t raw_dev_tick);
|
||||
static void image_callback_ex(unsigned char *pData, MV_FRAME_OUT_INFO_EX *pFrameInfo, void *pUser);
|
||||
};
|
||||
|
||||
void HikvisionDriver::Impl::configure_trigger(HikvisionDriver * node)
|
||||
{
|
||||
auto logger = node->get_logger();
|
||||
|
||||
if (sync_mode == "freerun") {
|
||||
MV_CHECK_THROW(logger, MV_CC_SetEnumValue, handle, "TriggerMode", MV_TRIGGER_MODE_OFF);
|
||||
RCLCPP_INFO(logger, "hikvision sync_mode=freerun");
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned int trigger_source = 0;
|
||||
if (sync_mode == "external_line0") {
|
||||
trigger_source = MV_TRIGGER_SOURCE_LINE0;
|
||||
} else if (sync_mode == "external_line2") {
|
||||
trigger_source = MV_TRIGGER_SOURCE_LINE2;
|
||||
} else {
|
||||
throw std::runtime_error("unsupported sync_mode: " + sync_mode);
|
||||
}
|
||||
|
||||
MV_CHECK_THROW(logger, MV_CC_SetEnumValueByString, handle, "TriggerSelector", "FrameStart");
|
||||
MV_CHECK_THROW(logger, MV_CC_SetEnumValue, handle, "TriggerMode", MV_TRIGGER_MODE_ON);
|
||||
MV_CHECK_THROW(logger, MV_CC_SetEnumValue, handle, "TriggerSource", trigger_source);
|
||||
|
||||
if (trigger_activation == "rising_edge") {
|
||||
MV_CHECK_THROW(logger, MV_CC_SetEnumValueByString, handle, "TriggerActivation", "RisingEdge");
|
||||
} else if (trigger_activation == "falling_edge") {
|
||||
MV_CHECK_THROW(logger, MV_CC_SetEnumValueByString, handle, "TriggerActivation", "FallingEdge");
|
||||
} else {
|
||||
throw std::runtime_error("unsupported trigger_activation: " + trigger_activation);
|
||||
}
|
||||
|
||||
if (trigger_delay_us > 0.0) {
|
||||
MV_CHECK_THROW(logger, MV_CC_SetFloatValue, handle, "TriggerDelay", static_cast<float>(trigger_delay_us));
|
||||
}
|
||||
|
||||
RCLCPP_INFO(
|
||||
logger,
|
||||
"hikvision sync_mode=%s trigger_activation=%s trigger_delay_us=%.3f",
|
||||
sync_mode.c_str(),
|
||||
trigger_activation.c_str(),
|
||||
trigger_delay_us);
|
||||
}
|
||||
|
||||
double HikvisionDriver::Impl::resolve_device_timestamp_tick_hz(
|
||||
HikvisionDriver * node,
|
||||
MV_CC_DEVICE_INFO * device_info)
|
||||
{
|
||||
auto logger = node->get_logger();
|
||||
MVCC_INTVALUE int_value;
|
||||
memset(&int_value, 0, sizeof(int_value));
|
||||
|
||||
if (device_info->nTLayerType == MV_GIGE_DEVICE &&
|
||||
MV_CC_GetIntValue(handle, "GevTimestampTickFrequency", &int_value) == MV_OK &&
|
||||
int_value.nCurValue > 0)
|
||||
{
|
||||
const auto resolved = static_cast<double>(int_value.nCurValue);
|
||||
RCLCPP_INFO(logger, "resolved GevTimestampTickFrequency=%.3f Hz", resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
if (device_timestamp_tick_hz > 0.0) {
|
||||
RCLCPP_WARN(
|
||||
logger,
|
||||
"falling back to configured device_timestamp_tick_hz=%.3f Hz",
|
||||
device_timestamp_tick_hz);
|
||||
return device_timestamp_tick_hz;
|
||||
}
|
||||
|
||||
throw std::runtime_error("failed to resolve device timestamp tick frequency");
|
||||
}
|
||||
|
||||
std::optional<rclcpp::Time> HikvisionDriver::Impl::convert_device_stamp_to_ros_time(uint64_t raw_dev_tick) const
|
||||
{
|
||||
if (device_timestamp_tick_hz <= 0.0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const long double stamp_ns =
|
||||
static_cast<long double>(raw_dev_tick) * 1000000000.0L / static_cast<long double>(device_timestamp_tick_hz);
|
||||
return rclcpp::Time(static_cast<int64_t>(std::llround(stamp_ns)), RCL_ROS_TIME);
|
||||
}
|
||||
|
||||
uint32_t HikvisionDriver::Impl::device_tick_ms(uint64_t raw_dev_tick) const
|
||||
{
|
||||
if (device_timestamp_tick_hz <= 0.0) {
|
||||
throw std::runtime_error("device timestamp tick frequency must be positive");
|
||||
}
|
||||
|
||||
const long double tick_ms =
|
||||
static_cast<long double>(raw_dev_tick) * 1000.0L / static_cast<long double>(device_timestamp_tick_hz);
|
||||
return static_cast<uint32_t>(std::llround(tick_ms));
|
||||
}
|
||||
|
||||
rclcpp::Time HikvisionDriver::Impl::compute_header_stamp(
|
||||
HikvisionDriver * node,
|
||||
const MV_FRAME_OUT_INFO_EX & frame_info,
|
||||
const rclcpp::Time & host_rx_time,
|
||||
uint32_t converted_device_tick_ms,
|
||||
uint64_t raw_dev_tick)
|
||||
{
|
||||
if (!use_timestamp_sync) {
|
||||
return host_rx_time;
|
||||
}
|
||||
|
||||
auto sync_result = timestamp_sync.reconstruct(converted_device_tick_ms, host_rx_time);
|
||||
|
||||
if (sync_result.large_jump_detected) {
|
||||
RCLCPP_WARN_THROTTLE(
|
||||
node->get_logger(), *node->get_clock(), 2000,
|
||||
"hikvision timestamp jump detected: frame=%u raw_tick=%llu delta_ms=%u unwrapped_ms=%llu",
|
||||
frame_info.nFrameNum,
|
||||
static_cast<unsigned long long>(raw_dev_tick),
|
||||
sync_result.delta_tick_ms,
|
||||
static_cast<unsigned long long>(sync_result.unwrapped_device_tick_ms));
|
||||
}
|
||||
|
||||
if (sync_result.backward_clamped) {
|
||||
RCLCPP_WARN_THROTTLE(
|
||||
node->get_logger(), *node->get_clock(), 2000,
|
||||
"hikvision backward timestamp clamped: frame=%u stamp_ns=%lld",
|
||||
frame_info.nFrameNum,
|
||||
static_cast<long long>(sync_result.stamp.nanoseconds()));
|
||||
}
|
||||
|
||||
if (timestamp_diagnostics) {
|
||||
RCLCPP_INFO_THROTTLE(
|
||||
node->get_logger(), *node->get_clock(), timestamp_diagnostics_throttle_ms,
|
||||
"hikvision timestamp: frame=%u raw_tick=%llu tick_ms=%u host_rx_ns=%lld stamp_ns=%lld bias_ns=%lld init=%s",
|
||||
frame_info.nFrameNum,
|
||||
static_cast<unsigned long long>(raw_dev_tick),
|
||||
converted_device_tick_ms,
|
||||
static_cast<long long>(host_rx_time.nanoseconds()),
|
||||
static_cast<long long>(sync_result.stamp.nanoseconds()),
|
||||
static_cast<long long>(sync_result.filtered_bias_ns),
|
||||
sync_result.initialized_this_call ? "true" : "false");
|
||||
}
|
||||
|
||||
return sync_result.stamp;
|
||||
}
|
||||
|
||||
void HikvisionDriver::Impl::image_callback_ex(
|
||||
unsigned char *pData,
|
||||
MV_FRAME_OUT_INFO_EX *pFrameInfo,
|
||||
void *pUser) {
|
||||
auto node = reinterpret_cast<HikvisionDriver *>(pUser);
|
||||
|
||||
const auto host_rx_time = node->get_clock()->now();
|
||||
const uint64_t raw_dev_tick =
|
||||
(static_cast<uint64_t>(pFrameInfo->nDevTimeStampHigh) << 32ull) |
|
||||
static_cast<uint64_t>(pFrameInfo->nDevTimeStampLow);
|
||||
const uint32_t converted_device_tick_ms = node->pImpl->device_tick_ms(raw_dev_tick);
|
||||
const auto header_stamp =
|
||||
node->pImpl->compute_header_stamp(node, *pFrameInfo, host_rx_time, converted_device_tick_ms, raw_dev_tick);
|
||||
|
||||
auto p_img_msg = std::make_unique<sensor_msgs::msg::Image>();
|
||||
p_img_msg->header.frame_id = node->pImpl->camera_name;
|
||||
p_img_msg->header.stamp = ToBuiltinTime(header_stamp);
|
||||
p_img_msg->is_bigendian = false;
|
||||
p_img_msg->width = pFrameInfo->nWidth;
|
||||
p_img_msg->height = pFrameInfo->nHeight;
|
||||
|
||||
RCLCPP_INFO_THROTTLE(
|
||||
node->get_logger(),
|
||||
*node->get_clock(),
|
||||
2000,
|
||||
"pixel format=%d (0x%08X), width=%u, height=%u, frame_len=%u",
|
||||
static_cast<int>(pFrameInfo->enPixelType),
|
||||
static_cast<unsigned int>(pFrameInfo->enPixelType),
|
||||
pFrameInfo->nWidth,
|
||||
pFrameInfo->nHeight,
|
||||
pFrameInfo->nFrameLen);
|
||||
|
||||
auto direct_copy = [&](const std::string &encoding, size_t bytes_per_pixel) -> bool {
|
||||
const size_t expected_size =
|
||||
static_cast<size_t>(pFrameInfo->nWidth) *
|
||||
static_cast<size_t>(pFrameInfo->nHeight) *
|
||||
bytes_per_pixel;
|
||||
|
||||
if (pFrameInfo->nFrameLen < expected_size) {
|
||||
RCLCPP_ERROR(
|
||||
node->get_logger(),
|
||||
"frame too small: nFrameLen=%u < expected=%zu",
|
||||
pFrameInfo->nFrameLen,
|
||||
expected_size);
|
||||
return false;
|
||||
}
|
||||
|
||||
p_img_msg->encoding = encoding;
|
||||
p_img_msg->step = static_cast<sensor_msgs::msg::Image::_step_type>(
|
||||
pFrameInfo->nWidth * bytes_per_pixel);
|
||||
p_img_msg->data.resize(expected_size);
|
||||
std::copy_n(pData, expected_size, p_img_msg->data.data());
|
||||
return true;
|
||||
};
|
||||
|
||||
auto convert_to_bgr8 = [&]() -> bool {
|
||||
const size_t dst_size =
|
||||
static_cast<size_t>(pFrameInfo->nWidth) *
|
||||
static_cast<size_t>(pFrameInfo->nHeight) * 3u;
|
||||
|
||||
std::vector<uint8_t> bgr_buf(dst_size);
|
||||
|
||||
MV_CC_PIXEL_CONVERT_PARAM stConvertParam;
|
||||
memset(&stConvertParam, 0, sizeof(stConvertParam));
|
||||
stConvertParam.nWidth = pFrameInfo->nWidth;
|
||||
stConvertParam.nHeight = pFrameInfo->nHeight;
|
||||
stConvertParam.pSrcData = pData;
|
||||
stConvertParam.nSrcDataLen = pFrameInfo->nFrameLen;
|
||||
stConvertParam.enSrcPixelType = pFrameInfo->enPixelType;
|
||||
stConvertParam.enDstPixelType = PixelType_Gvsp_BGR8_Packed;
|
||||
stConvertParam.pDstBuffer = bgr_buf.data();
|
||||
stConvertParam.nDstBufferSize = static_cast<unsigned int>(bgr_buf.size());
|
||||
|
||||
const int nRet = MV_CC_ConvertPixelType(node->pImpl->handle, &stConvertParam);
|
||||
if (nRet != MV_OK) {
|
||||
RCLCPP_ERROR(
|
||||
node->get_logger(),
|
||||
"MV_CC_ConvertPixelType failed: %d, src pixel=%d (0x%08X)",
|
||||
nRet,
|
||||
static_cast<int>(pFrameInfo->enPixelType),
|
||||
static_cast<unsigned int>(pFrameInfo->enPixelType));
|
||||
return false;
|
||||
}
|
||||
|
||||
p_img_msg->encoding = sensor_msgs::image_encodings::BGR8;
|
||||
p_img_msg->step =
|
||||
static_cast<sensor_msgs::msg::Image::_step_type>(pFrameInfo->nWidth * 3u);
|
||||
p_img_msg->data = std::move(bgr_buf);
|
||||
return true;
|
||||
};
|
||||
|
||||
bool ok = false;
|
||||
switch (pFrameInfo->enPixelType) {
|
||||
case PixelType_Gvsp_Mono8:
|
||||
ok = direct_copy(sensor_msgs::image_encodings::MONO8, 1u);
|
||||
break;
|
||||
|
||||
case PixelType_Gvsp_RGB8_Packed:
|
||||
ok = direct_copy(sensor_msgs::image_encodings::RGB8, 3u);
|
||||
break;
|
||||
|
||||
case PixelType_Gvsp_BGR8_Packed:
|
||||
ok = direct_copy(sensor_msgs::image_encodings::BGR8, 3u);
|
||||
break;
|
||||
|
||||
// 8-bit Bayer:直接转 BGR8,避免下游再 debayer
|
||||
case PixelType_Gvsp_BayerRG8:
|
||||
case PixelType_Gvsp_BayerBG8:
|
||||
case PixelType_Gvsp_BayerGR8:
|
||||
case PixelType_Gvsp_BayerGB8:
|
||||
ok = convert_to_bgr8();
|
||||
break;
|
||||
|
||||
// 10-bit packed Bayer:也统一转 BGR8
|
||||
case PixelType_Gvsp_BayerRG10_Packed:
|
||||
case PixelType_Gvsp_BayerBG10_Packed:
|
||||
case PixelType_Gvsp_BayerGR10_Packed:
|
||||
case PixelType_Gvsp_BayerGB10_Packed:
|
||||
ok = convert_to_bgr8();
|
||||
break;
|
||||
|
||||
default:
|
||||
RCLCPP_ERROR_ONCE(
|
||||
node->get_logger(),
|
||||
"unsupported pixel format: %d (0x%08X)",
|
||||
static_cast<int>(pFrameInfo->enPixelType),
|
||||
static_cast<unsigned int>(pFrameInfo->enPixelType));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto p_info_msg = std::make_unique<hikvision_interface::msg::HikImageInfo>();
|
||||
p_info_msg->header.frame_id = node->pImpl->camera_name;
|
||||
p_info_msg->header.stamp = ToBuiltinTime(header_stamp);
|
||||
if (const auto device_stamp = node->pImpl->convert_device_stamp_to_ros_time(raw_dev_tick)) {
|
||||
p_info_msg->dev_stamp = ToBuiltinTime(*device_stamp);
|
||||
}
|
||||
p_info_msg->frame_num = pFrameInfo->nFrameNum;
|
||||
p_info_msg->gain = pFrameInfo->fGain;
|
||||
p_info_msg->exposure = pFrameInfo->fExposureTime;
|
||||
p_info_msg->red = pFrameInfo->nRed;
|
||||
p_info_msg->green = pFrameInfo->nGreen;
|
||||
p_info_msg->blue = pFrameInfo->nBlue;
|
||||
|
||||
node->pImpl->img_pub.publish(std::move(p_img_msg));
|
||||
node->pImpl->p_info_pub->publish(std::move(p_info_msg));
|
||||
}
|
||||
|
||||
HikvisionDriver::HikvisionDriver(const rclcpp::NodeOptions &options)
|
||||
: rclcpp::Node("hikvision_ros2_driver_node", options), pImpl(std::make_unique<Impl>()) {
|
||||
auto logger = get_logger();
|
||||
pImpl->logger = std::make_unique<rclcpp::Logger>(logger);
|
||||
|
||||
declare_parameter<std::string>("camera_name", "");
|
||||
declare_parameter<std::string>("sync_mode", "freerun");
|
||||
declare_parameter<std::string>("trigger_activation", "rising_edge");
|
||||
declare_parameter<double>("trigger_delay_us", 0.0);
|
||||
declare_parameter<double>("device_timestamp_tick_hz", 0.0);
|
||||
declare_parameter<bool>("use_timestamp_sync", true);
|
||||
declare_parameter<double>("time_offset_sec", 0.0);
|
||||
declare_parameter<double>("bias_alpha", 0.01);
|
||||
declare_parameter<int>("jump_warn_threshold_ms", 1000);
|
||||
declare_parameter<bool>("clamp_backward_time", true);
|
||||
declare_parameter<bool>("timestamp_diagnostics", false);
|
||||
declare_parameter<int>("timestamp_diagnostics_throttle_ms", 2000);
|
||||
|
||||
pImpl->camera_name = get_parameter("camera_name").as_string();
|
||||
pImpl->sync_mode = get_parameter("sync_mode").as_string();
|
||||
pImpl->trigger_activation = get_parameter("trigger_activation").as_string();
|
||||
pImpl->trigger_delay_us = get_parameter("trigger_delay_us").as_double();
|
||||
pImpl->device_timestamp_tick_hz = get_parameter("device_timestamp_tick_hz").as_double();
|
||||
pImpl->use_timestamp_sync = get_parameter("use_timestamp_sync").as_bool();
|
||||
pImpl->timestamp_diagnostics = get_parameter("timestamp_diagnostics").as_bool();
|
||||
pImpl->timestamp_diagnostics_throttle_ms = get_parameter("timestamp_diagnostics_throttle_ms").as_int();
|
||||
|
||||
driver_timestamp_sync::TimestampSyncConfig config;
|
||||
config.time_offset_sec = get_parameter("time_offset_sec").as_double();
|
||||
config.bias_alpha = get_parameter("bias_alpha").as_double();
|
||||
config.jump_warn_threshold_ms = static_cast<uint32_t>(get_parameter("jump_warn_threshold_ms").as_int());
|
||||
config.clamp_backward_time = get_parameter("clamp_backward_time").as_bool();
|
||||
pImpl->timestamp_sync.set_config(config);
|
||||
|
||||
if (pImpl->camera_name.empty()) {
|
||||
throw std::runtime_error("camera_name parameter must not be empty");
|
||||
}
|
||||
|
||||
RCLCPP_INFO(logger, "trying to open camera: '%s'", pImpl->camera_name.c_str());
|
||||
RCLCPP_INFO(
|
||||
logger,
|
||||
"timestamp_sync: enabled=%s offset=%.6f alpha=%.3f jump_warn_ms=%u clamp_backward=%s diagnostics=%s throttle_ms=%d",
|
||||
pImpl->use_timestamp_sync ? "true" : "false",
|
||||
config.time_offset_sec,
|
||||
config.bias_alpha,
|
||||
config.jump_warn_threshold_ms,
|
||||
config.clamp_backward_time ? "true" : "false",
|
||||
pImpl->timestamp_diagnostics ? "true" : "false",
|
||||
pImpl->timestamp_diagnostics_throttle_ms);
|
||||
|
||||
rclcpp::QoS qos(1);
|
||||
pImpl->img_pub = image_transport::create_publisher(this, "raw/image", qos.get_rmw_qos_profile());
|
||||
pImpl->p_info_pub = create_publisher<HikImageInfo>("info", qos);
|
||||
|
||||
MV_CC_DEVICE_INFO_LIST stDeviceList;
|
||||
memset(&stDeviceList, 0, sizeof(MV_CC_DEVICE_INFO_LIST));
|
||||
MV_CHECK_THROW(logger, MV_CC_EnumDevices, MV_GIGE_DEVICE | MV_USB_DEVICE, &stDeviceList);
|
||||
|
||||
MV_CC_DEVICE_INFO * matched_device = nullptr;
|
||||
for (uint32_t nDeviceId = 0; nDeviceId < stDeviceList.nDeviceNum; nDeviceId++) {
|
||||
auto *pDeviceInfo = stDeviceList.pDeviceInfo[nDeviceId];
|
||||
std::string user_defined_name;
|
||||
if (pDeviceInfo->nTLayerType == MV_GIGE_DEVICE) {
|
||||
user_defined_name = reinterpret_cast<const char *>(pDeviceInfo->SpecialInfo.stGigEInfo.chUserDefinedName);
|
||||
if (user_defined_name == pImpl->camera_name) {
|
||||
int nIp1 = ((pDeviceInfo->SpecialInfo.stGigEInfo.nCurrentIp & 0xff000000) >> 24);
|
||||
int nIp2 = ((pDeviceInfo->SpecialInfo.stGigEInfo.nCurrentIp & 0x00ff0000) >> 16);
|
||||
int nIp3 = ((pDeviceInfo->SpecialInfo.stGigEInfo.nCurrentIp & 0x0000ff00) >> 8);
|
||||
int nIp4 = (pDeviceInfo->SpecialInfo.stGigEInfo.nCurrentIp & 0x000000ff);
|
||||
RCLCPP_INFO(
|
||||
logger,
|
||||
"[%s]: GIGE, %s, %d.%d.%d.%d",
|
||||
user_defined_name.c_str(),
|
||||
pDeviceInfo->SpecialInfo.stGigEInfo.chModelName,
|
||||
nIp1,
|
||||
nIp2,
|
||||
nIp3,
|
||||
nIp4);
|
||||
matched_device = pDeviceInfo;
|
||||
break;
|
||||
}
|
||||
} else if (pDeviceInfo->nTLayerType == MV_USB_DEVICE) {
|
||||
user_defined_name = reinterpret_cast<const char *>(pDeviceInfo->SpecialInfo.stUsb3VInfo.chUserDefinedName);
|
||||
if (user_defined_name == pImpl->camera_name) {
|
||||
RCLCPP_INFO(
|
||||
logger,
|
||||
"[%s]: USB, %s",
|
||||
user_defined_name.c_str(),
|
||||
pDeviceInfo->SpecialInfo.stUsb3VInfo.chModelName);
|
||||
matched_device = pDeviceInfo;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
RCLCPP_WARN(logger, "type(%d) not support", pDeviceInfo->nTLayerType);
|
||||
}
|
||||
}
|
||||
|
||||
if (matched_device == nullptr) {
|
||||
throw std::runtime_error("camera not found: " + pImpl->camera_name);
|
||||
}
|
||||
|
||||
MV_CHECK_THROW(logger, MV_CC_CreateHandle, &pImpl->handle, matched_device);
|
||||
try {
|
||||
MV_CHECK_THROW(logger, MV_CC_OpenDevice, pImpl->handle);
|
||||
pImpl->device_timestamp_tick_hz = pImpl->resolve_device_timestamp_tick_hz(this, matched_device);
|
||||
pImpl->configure_trigger(this);
|
||||
MV_CHECK_THROW(logger, MV_CC_RegisterImageCallBackEx, pImpl->handle, &HikvisionDriver::Impl::image_callback_ex,
|
||||
this);
|
||||
MV_CHECK_THROW(logger, MV_CC_StartGrabbing, pImpl->handle);
|
||||
} catch (...) {
|
||||
if (pImpl->handle != nullptr) {
|
||||
MV_CC_CloseDevice(pImpl->handle);
|
||||
MV_CC_DestroyHandle(pImpl->handle);
|
||||
pImpl->handle = nullptr;
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
HikvisionDriver::~HikvisionDriver() {
|
||||
if (pImpl->handle == nullptr) return;
|
||||
auto logger = get_logger();
|
||||
|
||||
int nRet = MV_CC_StopGrabbing(pImpl->handle);
|
||||
if (MV_OK != nRet) {
|
||||
RCLCPP_ERROR(logger, "hikvision sdk error: MV_CC_StopGrabbing = %d", nRet);
|
||||
}
|
||||
nRet = MV_CC_CloseDevice(pImpl->handle);
|
||||
if (MV_OK != nRet) {
|
||||
RCLCPP_ERROR(logger, "hikvision sdk error: MV_CC_CloseDevice = %d", nRet);
|
||||
}
|
||||
nRet = MV_CC_DestroyHandle(pImpl->handle);
|
||||
if (MV_OK != nRet) {
|
||||
RCLCPP_ERROR(logger, "hikvision sdk error: MV_CC_DestroyHandle = %d", nRet);
|
||||
}
|
||||
pImpl->handle = nullptr;
|
||||
}
|
||||
|
||||
} // namespace hikvision_ros2_driver
|
||||
|
||||
#include "rclcpp_components/register_node_macro.hpp"
|
||||
RCLCPP_COMPONENTS_REGISTER_NODE(hikvision_ros2_driver::HikvisionDriver);
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,118 @@
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Changelog for package nmea_navsat_driver
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
2.0.0 (2022-06-25)
|
||||
------------------
|
||||
|
||||
* Replace dependency on transforms3d pip package to tf_transformations. (`#147 <https://github.com/evenator/nmea_navsat_driver/issues/147>`_)
|
||||
* Added log for successful connection. (`#149 <https://github.com/evenator/nmea_navsat_driver/issues/149>`_)
|
||||
* Removed printing every time the checksum is checked. (`#148 <https://github.com/evenator/nmea_navsat_driver/issues/148>`_)
|
||||
* nmea_socket_driver fixes for ROS2 (`#127 <https://github.com/evenator/nmea_navsat_driver/issues/127>`_)
|
||||
* Fix white space for PEP8 compliance
|
||||
* Fix socket driver for ROS2 Foxy
|
||||
* Add config file for socket driver
|
||||
* Decode bytes as ASCII
|
||||
* Fix bug where ROS node is not assigned correctly (`#114 <https://github.com/evenator/nmea_navsat_driver/issues/114>`_)
|
||||
Fixes `#71 <https://github.com/evenator/nmea_navsat_driver/issues/71>`_
|
||||
* Update launch file for ROS2 Foxy (`#110 <https://github.com/evenator/nmea_navsat_driver/issues/110>`_)
|
||||
* Remove get_default_launch_description, which no longer exists.
|
||||
* Update the parameters of the Node initializer to the Foxy API.
|
||||
* Update pyserial dependency to make it findable with rosdep (`#109 <https://github.com/evenator/nmea_navsat_driver/issues/109>`_)
|
||||
This allows rosdep to find the correct serial package and install it.
|
||||
* ROS2: Eloquent changes for python packages (`#101 <https://github.com/evenator/nmea_navsat_driver/issues/101>`_)
|
||||
Fix some warnings when installing packages that first appear in ROS2 Eloquent:
|
||||
- A missing the resource folder
|
||||
- Not explicitly installing package.xml in the share folder.
|
||||
* ROS2 updates for Dashing (`#80 <https://github.com/evenator/nmea_navsat_driver/issues/80>`_)
|
||||
Change subscriptions and parameters to conform to API changes in ROS 2 Dashing.
|
||||
* Remove scripts directory in favor of nodes subpackage (`#77 <https://github.com/evenator/nmea_navsat_driver/issues/77>`_)
|
||||
Since these modules will no longer be called as scripts in ROS 2,
|
||||
move them to a nodes subpackage, and remove the option to call them
|
||||
as executables.
|
||||
Addresses `#75 <https://github.com/evenator/nmea_navsat_driver/issues/75>`_ for ROS 2.
|
||||
* Fix nmea_topic_serial_reader name (`#74 <https://github.com/evenator/nmea_navsat_driver/issues/74>`_)
|
||||
- Fix `#72 <https://github.com/evenator/nmea_navsat_driver/issues/72>`_ (no module name scripts.nmea_topic_serial_driver)
|
||||
- Fix exception handling in nmea_topic_serial_reader ('rclpy' has no attribute 'ROSInterruptException')
|
||||
* Clean up launch file and make it runnable with ROS2 launch (`#73 <https://github.com/evenator/nmea_navsat_driver/issues/73>`_)
|
||||
Fixes `#70 <https://github.com/evenator/nmea_navsat_driver/issues/70>`_
|
||||
- Rename config file and put it into a config directory.
|
||||
- Make setup.py install the launch and config files.
|
||||
- Rename the launch file with the .launch.py suffix used by OSRF
|
||||
packages.
|
||||
- Refactor the launch file so that it works with `ros2 launch`.
|
||||
* Fix PEP8 Violations and update setup.cfg file for pycodestyle. (`#69 <https://github.com/evenator/nmea_navsat_driver/issues/69>`_)
|
||||
* Port to ROS 2 (`#64 <https://github.com/evenator/nmea_navsat_driver/issues/64>`_)
|
||||
Initial work to port nmea_navsat_driver to ROS2 by @klintan.
|
||||
* Add nmea_serial_driver launch file (`#60 <https://github.com/evenator/nmea_navsat_driver/issues/60>`_)
|
||||
Add example nmea_serial_driver launch file.
|
||||
* Remove automatic prefixing of forward slash to frame_id. (`#33 <https://github.com/evenator/nmea_navsat_driver/issues/33>`_/`#57 <https://github.com/evenator/nmea_navsat_driver/issues/57>`_)
|
||||
To be consistent with the current default behavior, the default frame_id has been set to /gps with the prepended forward slash.
|
||||
* Add support for IMU aided GPS systems (`#30 <https://github.com/evenator/nmea_navsat_driver/issues/30>`_/`#58 <https://github.com/evenator/nmea_navsat_driver/issues/58>`_)
|
||||
* Add support for IMU aided GPS systems like the Applanix POS/MV, whose NMEA strings typically begin '$IN'. (e.g. $INGGA).
|
||||
* Add support for VTG messages, which contain Course Over Ground and Speed Made Good. These are useful when not using RMC messages and you don't have a heading sensor.
|
||||
* Add support for publishing heading from GPHDT as a QuaternionStamped message on the topic /heading (`#25 <https://github.com/evenator/nmea_navsat_driver/issues/25>`_)
|
||||
* Improve Covariance Estimation (`#46 <https://github.com/evenator/nmea_navsat_driver/issues/46>`_)
|
||||
Use GST covariance where available, otherwise uses default covariances estimated from fix type.
|
||||
The previous implementation set covariance to HDOP^2. Instead, it should multiply that by the measurement variance. HDOP should be greater than 1.0.
|
||||
* Add Socket Driver (`#32 <https://github.com/evenator/nmea_navsat_driver/issues/32>`_)
|
||||
Add a NMEA socket driver node, which is like the existing serial driver node, but instead of attaching to a TTY handle from a serial port, it listens to a UDP port for NMEA sentences.
|
||||
* Add code to handle serial exception to allow node to exit cleanly (`#52 <https://github.com/evenator/nmea_navsat_driver/issues/52>`_)
|
||||
- Catch Serial exceptions and exit cleanly, instead of printing Python stack trace.
|
||||
- Catch Serial exception when opening the serial port, log a FATAL message, and exit instead of printing Python stack trace.
|
||||
* Remove MSL compensation (`#36 <https://github.com/evenator/nmea_navsat_driver/issues/36>`_)
|
||||
Fix for `#29 <https://github.com/evenator/nmea_navsat_driver/issues/29>`_ Altitude vs Elipsoid Height.
|
||||
* Add GLONASS support
|
||||
GLONASS capable devices send different NMEA sentences, which are
|
||||
basically identical to the GPS sentences, but with other prefixes.
|
||||
* Updated driver to accept status of 9 which some novatel receivers report for a WAAS (SBAS) fix.
|
||||
See http://www.novatel.com/support/known-solutions/which-novatel-position-types-correspond-to-the-gga-quality-indicator/
|
||||
|
||||
0.5.0 (2015-04-23)
|
||||
------------------
|
||||
* Release to Jade.
|
||||
|
||||
0.4.2 (2015-04-23)
|
||||
------------------
|
||||
* Fix remaining parse problem with NovAtel receivers (empty field specified for num_satellite).
|
||||
|
||||
0.4.1 (2014-08-03)
|
||||
------------------
|
||||
* Add debug logging output to the parser (PR #8, Mike Purvis)
|
||||
* Add queue size argument to publishers to fix warning on Indigo (PR #9, Mike Purvis)
|
||||
* Add support for roslint and some related cleanup (PR #10, Mike Purvis)
|
||||
|
||||
0.4.0 (2014-05-04)
|
||||
-------------------
|
||||
* Initial release for Indigo
|
||||
* Fix #5: Empty fields spam rosout with warnings. Driver now outputs sensor_msgs/NavSatFix messages that may contain NaNs in position and covariance when receiving invalid fixes from the device.
|
||||
|
||||
0.3.3 (2013-10-08)
|
||||
-------------------
|
||||
* Allow the driver to output velocity information anytime an RMC message is received
|
||||
|
||||
0.3.2 (2013-07-21)
|
||||
-------------------
|
||||
* Moved to nmea_navsat_driver package
|
||||
* Removed .py extensions from new-in-Hydro scripts
|
||||
* Now uses nmea_msgs/Sentence instead of custom sentence type
|
||||
* nmea_topic_driver reads the `frame_id` parameter from the sentence, not from the parameter server
|
||||
|
||||
0.3.1 (2013-05-07)
|
||||
-------------------
|
||||
* Removed incorrect find_package dependencies
|
||||
|
||||
0.3.0 (2013-05-05)
|
||||
-------------------
|
||||
* Initial release for Hydro
|
||||
* Converted to Catkin
|
||||
* nmea_gps_driver.py is now deprecated and will be removed in I-Turtle. Replacement node is nmea_serial_driver.py .
|
||||
* Refactored code into NMEA parser, common ROS driver and separate nodes for reading directly from serial or from topic.
|
||||
* Bugs fixed:
|
||||
- nmea_gps_driver crashes when a sentence doesn't have a checksum * character ( http://kforge.ros.org/gpsdrivers/trac/ticket/4 )
|
||||
- Add ability for nmea_gps_driver to support reading from string topic ( https://github.com/ros-drivers/nmea_gps_driver/issues/1 ). Use the nmea_topic_driver.py node to get this support.
|
||||
|
||||
0.2.0 (2012-03-15)
|
||||
------------------
|
||||
* Initial version (released into Fuerte)
|
||||
* Supports GGA or RMC+GSA sentences to generate sensor_msgs/NavSatFix messages
|
||||
@@ -0,0 +1,11 @@
|
||||
nmea_navsat_driver
|
||||
===============
|
||||
|
||||
ROS driver to parse NMEA strings and publish standard ROS NavSat message types. Does not require the GPSD daemon to be running.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
This package has no released Code API.
|
||||
|
||||
The ROS API documentation and other information can be found at http://ros.org/wiki/nmea_navsat_driver
|
||||
@@ -0,0 +1,8 @@
|
||||
nmea_navsat_driver:
|
||||
ros__parameters:
|
||||
port: "/dev/wheeltec_gnss"
|
||||
baud: 115200
|
||||
frame_id: "navsat_link"
|
||||
time_ref_source: "gps"
|
||||
useRMC: False
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# Copyright 2018 Open Source Robotics Foundation, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
""" A simple launch file for the nmea_serial_driver node. """
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
from launch import LaunchDescription, LaunchIntrospector, LaunchService
|
||||
from launch_ros import actions
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
"""Generate a launch description for a single serial driver."""
|
||||
config_file = os.path.join(get_package_share_directory("nmea_navsat_driver"), "config", "nmea_serial_driver.yaml")
|
||||
driver_node = actions.Node(
|
||||
package='nmea_navsat_driver',
|
||||
executable='nmea_serial_driver',
|
||||
output='screen',
|
||||
parameters=[config_file])
|
||||
|
||||
return LaunchDescription([driver_node])
|
||||
|
||||
|
||||
def main(argv):
|
||||
ld = generate_launch_description()
|
||||
|
||||
print('Starting introspection of launch description...')
|
||||
print('')
|
||||
|
||||
print(LaunchIntrospector().format_launch_description(ld))
|
||||
|
||||
print('')
|
||||
print('Starting launch of launch description...')
|
||||
print('')
|
||||
|
||||
ls = LaunchService()
|
||||
ls.include_launch_description(ld)
|
||||
return ls.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
||||
<package format="2">
|
||||
<name>nmea_navsat_driver</name>
|
||||
<version>2.0.0</version>
|
||||
<description>
|
||||
Package to parse NMEA strings and publish a very simple GPS message. Does not
|
||||
require or use the GPSD deamon.
|
||||
</description>
|
||||
|
||||
<maintainer email="evenator@gmail.com">Ed Venator</maintainer>
|
||||
|
||||
<license>BSD</license>
|
||||
|
||||
<url type="website">http://ros.org/wiki/nmea_navsat_driver</url>
|
||||
|
||||
<author email="eric@ericperko.com">Eric Perko</author>
|
||||
<author>Steven Martin</author>
|
||||
|
||||
<exec_depend>geometry_msgs</exec_depend>
|
||||
<exec_depend>nmea_msgs</exec_depend>
|
||||
<exec_depend>rclpy</exec_depend>
|
||||
<exec_depend>sensor_msgs</exec_depend>
|
||||
<exec_depend>python3-numpy</exec_depend>
|
||||
<exec_depend>python3-serial</exec_depend>
|
||||
<exec_depend>tf_transformations</exec_depend>
|
||||
|
||||
<test_depend>python3-pytest</test_depend>
|
||||
|
||||
<export>
|
||||
<build_type>ament_python</build_type>
|
||||
</export>
|
||||
</package>
|
||||
@@ -0,0 +1,10 @@
|
||||
[develop]
|
||||
script_dir=$base/lib/nmea_navsat_driver
|
||||
|
||||
[install]
|
||||
install_scripts=$base/lib/nmea_navsat_driver
|
||||
|
||||
[pycodestyle]
|
||||
max-line-length = 120
|
||||
statistics = True
|
||||
show-pep8 = True
|
||||
@@ -0,0 +1,38 @@
|
||||
from glob import glob
|
||||
import os
|
||||
from setuptools import setup
|
||||
|
||||
PACKAGE_NAME = "nmea_navsat_driver"
|
||||
SHARE_DIR = os.path.join("share", PACKAGE_NAME)
|
||||
|
||||
setup(
|
||||
name=PACKAGE_NAME,
|
||||
version='2.0.0',
|
||||
packages=["libnmea_navsat_driver", "libnmea_navsat_driver.nodes"],
|
||||
data_files=[
|
||||
('share/ament_index/resource_index/packages',
|
||||
['resource/' + PACKAGE_NAME]),
|
||||
('share/' + PACKAGE_NAME, ['package.xml']),
|
||||
(os.path.join(SHARE_DIR, "launch"), glob(os.path.join("launch", "*.launch.py"))),
|
||||
(os.path.join(SHARE_DIR, "config"), glob(os.path.join("config", "*.yaml")))],
|
||||
package_dir={'': 'src', },
|
||||
py_modules=[],
|
||||
zip_safe=True,
|
||||
install_requires=['setuptools',
|
||||
'pyserial',
|
||||
'numpy',
|
||||
'pyyaml'],
|
||||
author='Eric Perko',
|
||||
maintainer='Ed Venator',
|
||||
keywords=['ROS2'],
|
||||
description='Package to parse NMEA strings and publish a very simple GPS message.',
|
||||
license='BSD',
|
||||
entry_points={
|
||||
'console_scripts': ['nmea_serial_driver = libnmea_navsat_driver.nodes.nmea_serial_driver:main',
|
||||
'nmea_socket_driver = libnmea_navsat_driver.nodes.nmea_socket_driver:main',
|
||||
'nmea_tcpclient_driver = libnmea_navsat_driver.nodes.nmea_tcpclient_driver:main',
|
||||
'nmea_topic_driver = libnmea_navsat_driver.nodes.nmea_topic_driver:main',
|
||||
'nmea_topic_serial_reader = libnmea_navsat_driver.nodes.nmea_topic_serial_reader:main',
|
||||
],
|
||||
}
|
||||
)
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
# Software License Agreement (BSD License)
|
||||
#
|
||||
# Copyright (c) 2013, Eric Perko
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
# * Neither the names of the authors nor the names of their
|
||||
# affiliated organizations may be used to endorse or promote products derived
|
||||
# from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
# Check the NMEA sentence checksum. Return True if passes and False if failed
|
||||
def check_nmea_checksum(nmea_sentence):
|
||||
split_sentence = nmea_sentence.split('*')
|
||||
if len(split_sentence) != 2:
|
||||
# No checksum bytes were found... improperly formatted/incomplete NMEA data?
|
||||
return False
|
||||
transmitted_checksum = split_sentence[1].strip()
|
||||
|
||||
# Remove the $ at the front
|
||||
data_to_checksum = split_sentence[0][1:]
|
||||
checksum = 0
|
||||
for c in data_to_checksum:
|
||||
checksum ^= ord(c)
|
||||
|
||||
return ("%02X" % checksum) == transmitted_checksum.upper()
|
||||
+394
@@ -0,0 +1,394 @@
|
||||
# Software License Agreement (BSD License)
|
||||
#
|
||||
# Copyright (c) 2013, Eric Perko
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
# * Neither the names of the authors nor the names of their
|
||||
# affiliated organizations may be used to endorse or promote products derived
|
||||
# from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import math
|
||||
|
||||
import rclpy
|
||||
|
||||
from rclpy.node import Node
|
||||
from sensor_msgs.msg import NavSatFix, NavSatStatus, TimeReference
|
||||
from geometry_msgs.msg import TwistStamped, QuaternionStamped
|
||||
from geometry_msgs.msg import PoseStamped
|
||||
from geometry_msgs.msg import TwistWithCovarianceStamped
|
||||
from tf_transformations import quaternion_from_euler
|
||||
from libnmea_navsat_driver.checksum_utils import check_nmea_checksum
|
||||
from libnmea_navsat_driver import parser
|
||||
|
||||
from nmea_msgs.msg import Sentence
|
||||
from nmea_msgs.msg import Gpgga #wheeltec tues
|
||||
from std_msgs.msg import Float32
|
||||
|
||||
class Ros2NMEADriver(Node):
|
||||
def __init__(self):
|
||||
super().__init__('nmea_navsat_driver')
|
||||
|
||||
self.fix_pub = self.create_publisher(NavSatFix, '/gps/fix', 10)
|
||||
self.vel_pub = self.create_publisher(TwistStamped, '/gps/vel', 10)
|
||||
|
||||
self.eagleye_vel_pub = self.create_publisher(TwistWithCovarianceStamped, '/gps/nmea/vel', 10)
|
||||
|
||||
self.pose_pub = self.create_publisher(PoseStamped, '/gps/pose', 10)
|
||||
self.nmea_pub = self.create_publisher(Sentence, 'nmea_sentence', 10)
|
||||
self.gpgga_pub = self.create_publisher(Gpgga, '/gnss/gpgga', 10)
|
||||
self.heading_pub = self.create_publisher(QuaternionStamped, 'heading', 10)
|
||||
self.heading_deg_pub = self.create_publisher(Float32, 'heading_deg', 10)
|
||||
|
||||
self.time_ref_pub = self.create_publisher(TimeReference, 'time_reference', 10)
|
||||
|
||||
self.time_ref_source = self.declare_parameter('time_ref_source', 'gps').value
|
||||
self.use_RMC = self.declare_parameter('useRMC', False).value
|
||||
self.valid_fix = False
|
||||
self.rotation_data=0.0
|
||||
|
||||
self.rotation_data = self.declare_parameter('rotation', 0.0).value
|
||||
print(self.rotation_data)
|
||||
# epe = estimated position error
|
||||
self.default_epe_quality0 = self.declare_parameter('epe_quality0', 1000000).value
|
||||
self.default_epe_quality1 = self.declare_parameter('epe_quality1', 4.0).value
|
||||
self.default_epe_quality2 = self.declare_parameter('epe_quality2', 0.1).value
|
||||
self.default_epe_quality4 = self.declare_parameter('epe_quality4', 0.02).value
|
||||
self.default_epe_quality5 = self.declare_parameter('epe_quality5', 4.0).value
|
||||
self.default_epe_quality9 = self.declare_parameter('epe_quality9', 3.0).value
|
||||
|
||||
self.using_receiver_epe = False
|
||||
|
||||
self.lon_std_dev = float("nan")
|
||||
self.lat_std_dev = float("nan")
|
||||
self.alt_std_dev = float("nan")
|
||||
|
||||
"""Format for this dictionary is the fix type from a GGA message as the key, with
|
||||
each entry containing a tuple consisting of a default estimated
|
||||
position error, a NavSatStatus value, and a NavSatFix covariance value."""
|
||||
self.gps_qualities = {
|
||||
# Unknown
|
||||
-1: [
|
||||
self.default_epe_quality0,
|
||||
NavSatStatus.STATUS_NO_FIX,
|
||||
NavSatFix.COVARIANCE_TYPE_UNKNOWN
|
||||
],
|
||||
# Invalid
|
||||
0: [
|
||||
self.default_epe_quality0,
|
||||
NavSatStatus.STATUS_NO_FIX,
|
||||
NavSatFix.COVARIANCE_TYPE_UNKNOWN
|
||||
],
|
||||
# SPS
|
||||
1: [
|
||||
self.default_epe_quality1,
|
||||
NavSatStatus.STATUS_FIX,
|
||||
NavSatFix.COVARIANCE_TYPE_APPROXIMATED
|
||||
],
|
||||
# DGPS
|
||||
2: [
|
||||
self.default_epe_quality2,
|
||||
NavSatStatus.STATUS_SBAS_FIX,
|
||||
NavSatFix.COVARIANCE_TYPE_APPROXIMATED
|
||||
],
|
||||
# RTK Fix
|
||||
4: [
|
||||
self.default_epe_quality4,
|
||||
NavSatStatus.STATUS_GBAS_FIX,
|
||||
NavSatFix.COVARIANCE_TYPE_APPROXIMATED
|
||||
],
|
||||
# RTK Float
|
||||
5: [
|
||||
self.default_epe_quality5,
|
||||
NavSatStatus.STATUS_GBAS_FIX,
|
||||
NavSatFix.COVARIANCE_TYPE_APPROXIMATED
|
||||
],
|
||||
# WAAS
|
||||
9: [
|
||||
self.default_epe_quality9,
|
||||
NavSatStatus.STATUS_GBAS_FIX,
|
||||
NavSatFix.COVARIANCE_TYPE_APPROXIMATED
|
||||
]
|
||||
}
|
||||
|
||||
# Returns True if we successfully did something with the passed in
|
||||
# nmea_string
|
||||
def add_sentence(self, nmea_string, frame_id, timestamp=None):
|
||||
if not check_nmea_checksum(nmea_string):
|
||||
#self.get_logger().warn("Received a sentence with an invalid checksum. " +
|
||||
# "Sentence was: %s" % nmea_string)
|
||||
return False
|
||||
|
||||
parsed_sentence = parser.parse_nmea_sentence(nmea_string)
|
||||
if not parsed_sentence:
|
||||
#self.get_logger().debug("Failed to parse NMEA sentence. Sentence was: %s" % nmea_string)
|
||||
return False
|
||||
|
||||
if timestamp:
|
||||
current_time = timestamp
|
||||
else:
|
||||
current_time = self.get_clock().now().to_msg()
|
||||
|
||||
current_fix = NavSatFix()
|
||||
current_fix.header.stamp = current_time
|
||||
current_fix.header.frame_id = frame_id
|
||||
current_time_ref = TimeReference()
|
||||
current_time_ref.header.stamp = current_time
|
||||
current_time_ref.header.frame_id = frame_id
|
||||
if self.time_ref_source:
|
||||
current_time_ref.source = self.time_ref_source
|
||||
else:
|
||||
current_time_ref.source = frame_id
|
||||
|
||||
if not self.use_RMC and 'GGA' in parsed_sentence:
|
||||
current_fix.position_covariance_type = NavSatFix.COVARIANCE_TYPE_APPROXIMATED
|
||||
|
||||
data = parsed_sentence['GGA']
|
||||
fix_type = data['fix_type']
|
||||
|
||||
if not (fix_type in self.gps_qualities):
|
||||
fix_type = -1
|
||||
gps_qual = self.gps_qualities[fix_type]
|
||||
default_epe = gps_qual[0]
|
||||
current_fix.status.status = gps_qual[1]
|
||||
|
||||
current_fix.position_covariance_type = gps_qual[2]
|
||||
if current_fix.status.status > 0:
|
||||
self.valid_fix = True
|
||||
else:
|
||||
self.valid_fix = False
|
||||
|
||||
current_fix.status.service = NavSatStatus.SERVICE_GPS
|
||||
latitude = data['latitude']
|
||||
if data['latitude_direction'] == 'S':
|
||||
latitude = -latitude
|
||||
current_fix.latitude = latitude
|
||||
|
||||
longitude = data['longitude']
|
||||
if data['longitude_direction'] == 'W':
|
||||
longitude = -longitude
|
||||
current_fix.longitude = longitude
|
||||
|
||||
# Altitude is above ellipsoid, so adjust for mean-sea-level
|
||||
altitude = data['altitude'] + data['mean_sea_level']
|
||||
current_fix.altitude = altitude
|
||||
|
||||
# use default epe std_dev unless we've received a GST sentence with epes
|
||||
if not self.using_receiver_epe or math.isnan(self.lon_std_dev):
|
||||
self.lon_std_dev = default_epe
|
||||
if not self.using_receiver_epe or math.isnan(self.lat_std_dev):
|
||||
self.lat_std_dev = default_epe
|
||||
if not self.using_receiver_epe or math.isnan(self.alt_std_dev):
|
||||
self.alt_std_dev = default_epe * 2
|
||||
|
||||
hdop = data['hdop']
|
||||
current_fix.position_covariance[0] = (hdop * self.lon_std_dev) ** 2
|
||||
current_fix.position_covariance[4] = (hdop * self.lat_std_dev) ** 2
|
||||
current_fix.position_covariance[8] = (2 * hdop * self.alt_std_dev) ** 2 # FIXME
|
||||
|
||||
self.fix_pub.publish(current_fix)
|
||||
|
||||
if not math.isnan(data['utc_time']):
|
||||
current_time_ref.time_ref = rclpy.time.Time(seconds=data['utc_time']).to_msg()
|
||||
self.last_valid_fix_time = current_time_ref
|
||||
self.time_ref_pub.publish(current_time_ref)
|
||||
|
||||
# wheeltec tues debug
|
||||
gga_msg = Gpgga()
|
||||
gga_msg.header.stamp = current_time
|
||||
gga_msg.header.frame_id = frame_id
|
||||
|
||||
gga_msg.utc_seconds = float(data['utc_time']) if not math.isnan(data['utc_time']) else 0.0
|
||||
gga_msg.lat = latitude
|
||||
gga_msg.lat_dir = data['latitude_direction']
|
||||
gga_msg.lon = longitude
|
||||
gga_msg.lon_dir = data['longitude_direction']
|
||||
gga_msg.gps_qual = data['fix_type']
|
||||
gga_msg.num_sats = data['num_satellites']
|
||||
gga_msg.hdop = data['hdop']
|
||||
gga_msg.alt = altitude
|
||||
gga_msg.altitude_units = data.get('altitude_units', 'M')
|
||||
gga_msg.undulation = data['mean_sea_level']
|
||||
gga_msg.undulation_units = data.get('mean_sea_level_units', 'M')
|
||||
gga_msg.diff_age = int(round(data.get('age_gps_data', 0.0)))
|
||||
gga_msg.station_id = data.get('ref_station_id', '')
|
||||
|
||||
self.gpgga_pub.publish(gga_msg)
|
||||
|
||||
|
||||
nmea_msg = Sentence()
|
||||
nmea_msg.header.stamp = current_time
|
||||
nmea_msg.header.frame_id = frame_id
|
||||
nmea_msg.sentence = nmea_string
|
||||
self.nmea_pub.publish(nmea_msg)
|
||||
|
||||
elif not self.use_RMC and 'VTG' in parsed_sentence:
|
||||
data = parsed_sentence['VTG']
|
||||
|
||||
# Only report VTG data when you've received a valid GGA fix as well.
|
||||
if self.valid_fix:
|
||||
current_vel = TwistStamped()
|
||||
current_vel.header.stamp = current_time
|
||||
current_vel.header.frame_id = frame_id
|
||||
current_vel.twist.linear.x = data['speed'] * math.sin(data['true_course'])
|
||||
current_vel.twist.linear.y = data['speed'] * math.cos(data['true_course'])
|
||||
self.vel_pub.publish(current_vel)
|
||||
|
||||
|
||||
eagleye_current_vel = TwistWithCovarianceStamped()
|
||||
eagleye_current_vel.header.stamp = current_time
|
||||
eagleye_current_vel.header.frame_id = frame_id
|
||||
eagleye_current_vel.twist.twist.linear.x = data['speed'] * math.sin(data['true_course'])
|
||||
eagleye_current_vel.twist.twist.linear.y = data['speed'] * math.cos(data['true_course'])
|
||||
|
||||
std_dev = 0.2 # m/s
|
||||
var = std_dev ** 2 # = 0.004
|
||||
eagleye_current_vel.twist.covariance = [
|
||||
var, 0.0, 0.0, 0.0, 0.0, 0.0, # vx
|
||||
0.0, var, 0.0, 0.0, 0.0, 0.0, # vy
|
||||
0.0, 0.0, 9999.0, 0.0, 0.0, 0.0, # vz unknown
|
||||
0.0, 0.0, 0.0, 9999.0, 0.0, 0.0, # roll rate
|
||||
0.0, 0.0, 0.0, 0.0, 9999.0, 0.0, # pitch rate
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 9999.0 # yaw rate
|
||||
]
|
||||
|
||||
self.eagleye_vel_pub.publish(eagleye_current_vel)
|
||||
|
||||
elif 'RMC' in parsed_sentence:
|
||||
data = parsed_sentence['RMC']
|
||||
nmea_msg = Sentence()
|
||||
nmea_msg.header.stamp = current_time
|
||||
nmea_msg.header.frame_id = frame_id
|
||||
nmea_msg.sentence = nmea_string
|
||||
self.nmea_pub.publish(nmea_msg)
|
||||
|
||||
# Only publish a fix from RMC if the use_RMC flag is set.
|
||||
if self.use_RMC:
|
||||
if data['fix_valid']:
|
||||
current_fix.status.status = NavSatStatus.STATUS_FIX
|
||||
else:
|
||||
current_fix.status.status = NavSatStatus.STATUS_NO_FIX
|
||||
|
||||
current_fix.status.service = NavSatStatus.SERVICE_GPS
|
||||
|
||||
latitude = data['latitude']
|
||||
if data['latitude_direction'] == 'S':
|
||||
latitude = -latitude
|
||||
current_fix.latitude = latitude
|
||||
|
||||
longitude = data['longitude']
|
||||
if data['longitude_direction'] == 'W':
|
||||
longitude = -longitude
|
||||
current_fix.longitude = longitude
|
||||
|
||||
current_fix.altitude = float('NaN')
|
||||
current_fix.position_covariance_type = \
|
||||
NavSatFix.COVARIANCE_TYPE_UNKNOWN
|
||||
|
||||
self.fix_pub.publish(current_fix)
|
||||
|
||||
if not math.isnan(data['utc_time']):
|
||||
current_time_ref.time_ref = rclpy.time.Time(seconds=data['utc_time']).to_msg()
|
||||
self.time_ref_pub.publish(current_time_ref)
|
||||
|
||||
# Publish velocity from RMC regardless, since GGA doesn't provide it.
|
||||
if data['fix_valid']:
|
||||
current_vel = TwistStamped()
|
||||
current_vel.header.stamp = current_time
|
||||
current_vel.header.frame_id = frame_id
|
||||
current_vel.twist.linear.x = data['speed'] * math.sin(data['true_course'])
|
||||
current_vel.twist.linear.y = data['speed'] * math.cos(data['true_course'])
|
||||
self.vel_pub.publish(current_vel)
|
||||
|
||||
eagleye_current_vel = TwistWithCovarianceStamped()
|
||||
eagleye_current_vel.header.stamp = current_time
|
||||
eagleye_current_vel.header.frame_id = frame_id
|
||||
eagleye_current_vel.twist.twist.linear.x = data['speed'] * math.sin(data['true_course'])
|
||||
eagleye_current_vel.twist.twist.linear.y = data['speed'] * math.cos(data['true_course'])
|
||||
|
||||
std_dev = 0.2 # m/s
|
||||
var = std_dev ** 2 # = 0.004
|
||||
eagleye_current_vel.twist.covariance = [
|
||||
var, 0.0, 0.0, 0.0, 0.0, 0.0, # vx
|
||||
0.0, var, 0.0, 0.0, 0.0, 0.0, # vy
|
||||
0.0, 0.0, 9999.0, 0.0, 0.0, 0.0, # vz unknown
|
||||
0.0, 0.0, 0.0, 9999.0, 0.0, 0.0, # roll rate
|
||||
0.0, 0.0, 0.0, 0.0, 9999.0, 0.0, # pitch rate
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 9999.0 # yaw rate
|
||||
]
|
||||
self.eagleye_vel_pub.publish(eagleye_current_vel)
|
||||
|
||||
elif 'GST' in parsed_sentence:
|
||||
data = parsed_sentence['GST']
|
||||
|
||||
# Use receiver-provided error estimate if available
|
||||
self.using_receiver_epe = True
|
||||
self.lon_std_dev = data['lon_std_dev']
|
||||
self.lat_std_dev = data['lat_std_dev']
|
||||
self.alt_std_dev = data['alt_std_dev']
|
||||
elif 'HDT' in parsed_sentence:
|
||||
data = parsed_sentence['HDT']
|
||||
if data['heading']:
|
||||
current_heading = QuaternionStamped()
|
||||
current_heading.header.stamp = current_time
|
||||
current_heading.header.frame_id = frame_id
|
||||
heading_data=data['heading']+self.rotation_data
|
||||
if(heading_data>360):
|
||||
heading_data=heading_data-360
|
||||
#heading_data=data['heading']
|
||||
yaw=math.radians(heading_data)
|
||||
q = quaternion_from_euler(0, 0, yaw)
|
||||
current_heading.quaternion.x = q[0]
|
||||
current_heading.quaternion.y = q[1]
|
||||
current_heading.quaternion.z = q[2]
|
||||
current_heading.quaternion.w = q[3]
|
||||
self.heading_pub.publish(current_heading)
|
||||
|
||||
pose_msg = PoseStamped()
|
||||
pose_msg.header.stamp = current_time
|
||||
pose_msg.header.frame_id = frame_id
|
||||
pose_msg.pose.position.x = current_fix.latitude
|
||||
pose_msg.pose.position.y = current_fix.longitude
|
||||
pose_msg.pose.position.z = current_fix.altitude
|
||||
pose_msg.pose.orientation.x = q[0]
|
||||
pose_msg.pose.orientation.y = q[1]
|
||||
pose_msg.pose.orientation.z = q[2]
|
||||
pose_msg.pose.orientation.w = q[3]
|
||||
self.pose_pub.publish(pose_msg)
|
||||
|
||||
heading_deg_msg = Float32()
|
||||
heading_deg_msg.data = heading_data
|
||||
self.heading_deg_pub.publish(heading_deg_msg)
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
"""Helper method for getting the frame_id with the correct TF prefix"""
|
||||
def get_frame_id(self):
|
||||
frame_id = self.declare_parameter('frame_id', 'navsat_link').value
|
||||
prefix = self.declare_parameter('tf_prefix', '').value
|
||||
if len(prefix):
|
||||
return '%s/%s' % (prefix, frame_id)
|
||||
return frame_id
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
import math
|
||||
import rclpy
|
||||
from rclpy.node import Node
|
||||
from sensor_msgs.msg import NavSatFix
|
||||
from geometry_msgs.msg import PoseStamped
|
||||
from nav_msgs.msg import Path
|
||||
|
||||
EARTH_RADIUS = 6378.137
|
||||
|
||||
class GpsPath(Node):
|
||||
def __init__(self):
|
||||
super().__init__('GpsPath')
|
||||
self.pose_init = False
|
||||
self.state_pub_ = self.create_publisher(Path, 'gps_path', 10)
|
||||
self.subscription_ = self.create_subscription(
|
||||
NavSatFix,
|
||||
'/gps/fix',
|
||||
self.gps_callback,
|
||||
1
|
||||
)
|
||||
|
||||
def rad(self, d):
|
||||
return d * math.pi / 180.0
|
||||
|
||||
def gps_callback(self, gps_msg):
|
||||
if not self.pose_init:
|
||||
self.init_pose = self.lla_pose()
|
||||
self.init_pose.latitude = gps_msg.latitude
|
||||
self.init_pose.longitude = gps_msg.longitude
|
||||
self.init_pose.altitude = gps_msg.altitude
|
||||
self.pose_init = True
|
||||
else:
|
||||
radLat1 = self.rad(self.init_pose.latitude)
|
||||
radLong1 = self.rad(self.init_pose.longitude)
|
||||
radLat2 = self.rad(gps_msg.latitude)
|
||||
radLong2 = self.rad(gps_msg.longitude)
|
||||
|
||||
delta_lat = radLat2 - radLat1
|
||||
delta_long = 0
|
||||
|
||||
if delta_lat > 0:
|
||||
x = -2 * math.asin(math.sqrt(
|
||||
math.pow(math.sin(delta_lat / 2), 2) +
|
||||
math.cos(radLat1) * math.cos(radLat2) *
|
||||
math.pow(math.sin(delta_long / 2), 2)
|
||||
))
|
||||
else:
|
||||
x = 2 * math.asin(math.sqrt(
|
||||
math.pow(math.sin(delta_lat / 2), 2) +
|
||||
math.cos(radLat1) * math.cos(radLat2) *
|
||||
math.pow(math.sin(delta_long / 2), 2)
|
||||
))
|
||||
x *= EARTH_RADIUS * 1000
|
||||
|
||||
delta_lat = 0
|
||||
delta_long = radLong2 - radLong1
|
||||
if delta_long > 0:
|
||||
y = 2 * math.asin(math.sqrt(
|
||||
math.pow(math.sin(delta_lat / 2), 2) +
|
||||
math.cos(radLat2) * math.cos(radLat2) *
|
||||
math.pow(math.sin(delta_long / 2), 2)
|
||||
))
|
||||
else:
|
||||
y = -2 * math.asin(math.sqrt(
|
||||
math.pow(math.sin(delta_lat / 2), 2) +
|
||||
math.cos(radLat2) * math.cos(radLat2) *
|
||||
math.pow(math.sin(delta_long / 2), 2)
|
||||
))
|
||||
y *= EARTH_RADIUS * 1000
|
||||
|
||||
z = gps_msg.altitude - self.init_pose.altitude
|
||||
|
||||
ros_path_ = Path()
|
||||
ros_path_.header.frame_id = 'path'
|
||||
ros_path_.header.stamp = self.get_clock().now().to_msg()
|
||||
pose = PoseStamped()
|
||||
pose.header = ros_path_.header
|
||||
pose.pose.position.x = x
|
||||
pose.pose.position.y = y
|
||||
pose.pose.position.z = z
|
||||
ros_path_.poses.append(pose)
|
||||
|
||||
self.state_pub_.publish(ros_path_)
|
||||
|
||||
self.get_logger().info("( x:%0.6f ,y:%0.6f ,z:%0.6f)", x, y, z)
|
||||
|
||||
class lla_pose:
|
||||
def __init__(self):
|
||||
self.latitude = 0.0
|
||||
self.longitude = 0.0
|
||||
self.altitude = 0.0
|
||||
|
||||
def main(args=None):
|
||||
rclpy.init(args=args)
|
||||
gps_path = GpsPath()
|
||||
|
||||
rclpy.spin(gps_path)
|
||||
rclpy.shutdown()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
# Software License Agreement (BSD License)
|
||||
#
|
||||
# Copyright (c) 2013, Eric Perko
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
# * Neither the names of the authors nor the names of their
|
||||
# affiliated organizations may be used to endorse or promote products derived
|
||||
# from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import serial
|
||||
|
||||
import rclpy
|
||||
|
||||
from libnmea_navsat_driver.driver import Ros2NMEADriver
|
||||
|
||||
|
||||
def main(args=None):
|
||||
rclpy.init(args=args)
|
||||
|
||||
driver = Ros2NMEADriver()
|
||||
frame_id = driver.get_frame_id()
|
||||
|
||||
serial_port = driver.declare_parameter('port', '/dev/ttyACM0').value
|
||||
serial_baud = driver.declare_parameter('baud', 115200).value
|
||||
|
||||
try:
|
||||
GPS = serial.Serial(port=serial_port, baudrate=serial_baud, timeout=2)
|
||||
driver.get_logger().info("Successfully connected to {0} at {1}.".format(serial_port, serial_baud))
|
||||
try:
|
||||
while rclpy.ok():
|
||||
data = GPS.readline().strip()
|
||||
sentence_stamp = driver.get_clock().now().to_msg()
|
||||
try:
|
||||
if isinstance(data, bytes):
|
||||
data = data.decode("utf-8")
|
||||
driver.add_sentence(data, frame_id, timestamp=sentence_stamp)
|
||||
except ValueError as e:
|
||||
driver.get_logger().warn(
|
||||
"Value error, likely due to missing fields in the NMEA message. message was: %s. " % e)
|
||||
|
||||
except Exception as e:
|
||||
driver.get_logger().error("Ros error: {0}".format(e))
|
||||
GPS.close() # Close GPS serial port
|
||||
except serial.SerialException as ex:
|
||||
driver.get_logger().fatal("Could not open serial port: I/O error({0}): {1}".format(ex.errno, ex.strerror))
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
# Software License Agreement (BSD License)
|
||||
#
|
||||
# Copyright (c) 2016, Rein Appeldoorn
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
# * Neither the names of the authors nor the names of their
|
||||
# affiliated organizations may be used to endorse or promote products derived
|
||||
# from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import socket
|
||||
import sys
|
||||
|
||||
import rclpy
|
||||
|
||||
from libnmea_navsat_driver.driver import Ros2NMEADriver
|
||||
|
||||
|
||||
def main(args=None):
|
||||
rclpy.init(args=args)
|
||||
driver = Ros2NMEADriver()
|
||||
|
||||
try:
|
||||
local_ip = driver.declare_parameter('ip', '0.0.0.0').value
|
||||
local_port = driver.declare_parameter('port', 10110).value
|
||||
buffer_size = driver.declare_parameter('buffer_size', 4096).value
|
||||
timeout = driver.declare_parameter('timeout_sec', 2).value
|
||||
except KeyError as e:
|
||||
driver.get_logger().err("Parameter %s not found" % e)
|
||||
sys.exit(1)
|
||||
|
||||
frame_id = driver.get_frame_id()
|
||||
|
||||
driver.get_logger().info(
|
||||
" Using parameters ip {} port {} buffer_size {} timeout_sec {}"
|
||||
.format(local_ip, local_port, buffer_size, timeout))
|
||||
|
||||
# Connection-loop: connect and keep receiving. If receiving fails, reconnect
|
||||
while rclpy.ok():
|
||||
try:
|
||||
# Create a socket
|
||||
socket_ = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
|
||||
# Bind the socket to the port
|
||||
socket_.bind((local_ip, local_port))
|
||||
|
||||
# Set timeout
|
||||
socket_.settimeout(timeout)
|
||||
except socket.error as exc:
|
||||
driver.get_logger().error("Caught exception socket.error when setting up socket: %s" % exc)
|
||||
sys.exit(1)
|
||||
|
||||
# recv-loop: When we're connected, keep receiving stuff until that fails
|
||||
while rclpy.ok():
|
||||
try:
|
||||
data, remote_address = socket_.recvfrom(buffer_size)
|
||||
|
||||
# strip the data
|
||||
data_list = data.decode("ascii").strip().split("\n")
|
||||
|
||||
for data in data_list:
|
||||
|
||||
try:
|
||||
driver.add_sentence(data, frame_id)
|
||||
except ValueError as e:
|
||||
driver.get_logger().warn(
|
||||
"Value error, likely due to missing fields in the NMEA message. "
|
||||
"Error was: %s. Please report this issue at github.com/ros-drivers/nmea_navsat_driver, "
|
||||
"including a bag file with the NMEA sentences that caused it." % e)
|
||||
|
||||
except socket.error as exc:
|
||||
driver.get_logger().error("Caught exception socket.error during recvfrom: %s" % exc)
|
||||
socket_.close()
|
||||
# This will break out of the recv-loop so we start another iteration of the connection-loop
|
||||
break
|
||||
|
||||
socket_.close() # Close socket
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
# Copyright 2022 Open Source Robotics Foundation, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import socket
|
||||
import sys
|
||||
|
||||
import rclpy
|
||||
|
||||
from libnmea_navsat_driver.driver import Ros2NMEADriver
|
||||
|
||||
def main(args=None):
|
||||
rclpy.init(args=args)
|
||||
driver = Ros2NMEADriver()
|
||||
|
||||
try:
|
||||
gnss_ip = driver.declare_parameter('ip', '192.168.131.22').value
|
||||
gnss_port = driver.declare_parameter('port', 9001).value
|
||||
buffer_size = driver.declare_parameter('buffer_size', 4096).value
|
||||
except KeyError as e:
|
||||
driver.get_logger().err("Parameter %s not found" % e)
|
||||
sys.exit(1)
|
||||
|
||||
frame_id = driver.get_frame_id()
|
||||
|
||||
driver.get_logger().info("Using gnss sensor with ip {} and port {}".format(gnss_ip, gnss_port))
|
||||
|
||||
# Connection-loop: connect and keep receiving. If receiving fails, reconnect
|
||||
# Connect to the gnss sensor using tcp
|
||||
while rclpy.ok():
|
||||
try:
|
||||
# Create a socket
|
||||
gnss_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
||||
# Connect to the gnss sensor
|
||||
gnss_socket.connect((gnss_ip, gnss_port))
|
||||
except socket.error as exc:
|
||||
driver.get_logger().error("Caught exception socket.error when setting up socket: %s" % exc)
|
||||
sys.exit(1)
|
||||
|
||||
# recv-loop: When we're connected, keep receiving stuff until that fails
|
||||
partial = ""
|
||||
while rclpy.ok():
|
||||
try:
|
||||
partial += gnss_socket.recv(buffer_size).decode("ascii")
|
||||
|
||||
# strip the data
|
||||
lines = partial.splitlines()
|
||||
if partial.endswith('\n'):
|
||||
full_lines = lines
|
||||
partial = ""
|
||||
else:
|
||||
full_lines = lines[:-1]
|
||||
partial = lines[-1]
|
||||
|
||||
for data in full_lines:
|
||||
try:
|
||||
if driver.add_sentence(data, frame_id):
|
||||
driver.get_logger().info("Received sentence: %s" % data)
|
||||
else:
|
||||
driver.get_logger().warn("Error with sentence: %s" % data)
|
||||
except ValueError as e:
|
||||
driver.get_logger().warn(
|
||||
"Value error, likely due to missing fields in the NMEA message. "
|
||||
"Error was: %s. Please report this issue to me. " % e)
|
||||
|
||||
except socket.error as exc:
|
||||
driver.get_logger().error("Caught exception socket.error when receiving: %s" % exc)
|
||||
gnss_socket.close()
|
||||
break
|
||||
|
||||
|
||||
gnss_socket.close()
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
# Software License Agreement (BSD License)
|
||||
#
|
||||
# Copyright (c) 2013, Eric Perko
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
# * Neither the names of the authors nor the names of their
|
||||
# affiliated organizations may be used to endorse or promote products derived
|
||||
# from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
from functools import partial
|
||||
|
||||
from nmea_msgs.msg import Sentence
|
||||
import rclpy
|
||||
|
||||
from libnmea_navsat_driver.driver import Ros2NMEADriver
|
||||
|
||||
|
||||
def nmea_sentence_callback(nmea_sentence, driver):
|
||||
try:
|
||||
driver.add_sentence(nmea_sentence.sentence, frame_id=nmea_sentence.header.frame_id,
|
||||
timestamp=nmea_sentence.header.stamp)
|
||||
except ValueError as e:
|
||||
rclpy.get_logger().warn(
|
||||
"Value error, likely due to missing fields in the NMEA message. Error was: %s. "
|
||||
"Please report this issue at github.com/ros-drivers/nmea_navsat_driver, including a bag file with "
|
||||
"the NMEA sentences that caused it." % e)
|
||||
|
||||
|
||||
def main(args=None):
|
||||
rclpy.init(args=args)
|
||||
|
||||
driver = Ros2NMEADriver()
|
||||
driver.get_frame_id()
|
||||
|
||||
driver.create_subscription(
|
||||
Sentence, 'nmea_sentence', partial(nmea_sentence_callback, driver=driver), 10)
|
||||
|
||||
rclpy.spin(driver)
|
||||
|
||||
rclpy.shutdown()
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
# Software License Agreement (BSD License)
|
||||
#
|
||||
# Copyright (c) 2013, Eric Perko
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
# * Neither the names of the authors nor the names of their
|
||||
# affiliated organizations may be used to endorse or promote products derived
|
||||
# from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import serial
|
||||
|
||||
from nmea_msgs.msg import Sentence
|
||||
import rclpy
|
||||
|
||||
from libnmea_navsat_driver.driver import Ros2NMEADriver
|
||||
|
||||
|
||||
def main(args=None):
|
||||
rclpy.init(args=args)
|
||||
|
||||
driver = Ros2NMEADriver()
|
||||
|
||||
nmea_pub = driver.create_publisher(Sentence, "nmea_sentence", 10)
|
||||
|
||||
serial_port = driver.declare_parameter('port', '/dev/ttyACM0').value
|
||||
serial_baud = driver.declare_parameter('baud', 9600).value
|
||||
|
||||
# Get the frame_id
|
||||
frame_id = driver.get_frame_id()
|
||||
|
||||
try:
|
||||
GPS = serial.Serial(port=serial_port, baudrate=serial_baud, timeout=2)
|
||||
try:
|
||||
while rclpy.ok():
|
||||
data = GPS.readline().strip()
|
||||
|
||||
sentence = Sentence()
|
||||
sentence.header.stamp = driver.get_clock().now().to_msg()
|
||||
sentence.header.frame_id = frame_id
|
||||
sentence.sentence = data
|
||||
nmea_pub.publish(sentence)
|
||||
|
||||
except Exception as e:
|
||||
driver.get_logger().error("Ros error: {0}".format(e))
|
||||
GPS.close() # Close GPS serial port
|
||||
except serial.SerialException as ex:
|
||||
driver.get_logger().fatal("Could not open serial port: I/O error({0}): {1}".format(ex.errno, ex.strerror))
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
# Software License Agreement (BSD License)
|
||||
#
|
||||
# Copyright (c) 2013, Eric Perko
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions
|
||||
# are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
# * Neither the names of the authors nor the names of their
|
||||
# affiliated organizations may be used to endorse or promote products derived
|
||||
# from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import re
|
||||
import time
|
||||
import calendar
|
||||
import math
|
||||
import rclpy
|
||||
|
||||
logger = rclpy.logging.get_logger('nmea_navsat_driver')
|
||||
|
||||
|
||||
def safe_float(field):
|
||||
try:
|
||||
return float(field)
|
||||
except ValueError:
|
||||
return float('NaN')
|
||||
|
||||
|
||||
def safe_int(field):
|
||||
try:
|
||||
return int(field)
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
|
||||
def convert_latitude(field):
|
||||
return safe_float(field[0:2]) + safe_float(field[2:]) / 60.0
|
||||
|
||||
|
||||
def convert_longitude(field):
|
||||
return safe_float(field[0:3]) + safe_float(field[3:]) / 60.0
|
||||
|
||||
|
||||
def convert_time(nmea_utc):
|
||||
# Get current time in UTC for date information
|
||||
utc_struct = time.gmtime() # immutable, so cannot modify this one
|
||||
utc_list = list(utc_struct)
|
||||
# If one of the time fields is empty, return NaN seconds
|
||||
if not nmea_utc[0:2] or not nmea_utc[2:4] or not nmea_utc[4:6]:
|
||||
return float('NaN')
|
||||
else:
|
||||
hours = int(nmea_utc[0:2])
|
||||
minutes = int(nmea_utc[2:4])
|
||||
seconds = int(nmea_utc[4:6])
|
||||
utc_list[3] = hours
|
||||
utc_list[4] = minutes
|
||||
utc_list[5] = seconds
|
||||
unix_time = calendar.timegm(tuple(utc_list))
|
||||
return unix_time
|
||||
|
||||
|
||||
def convert_status_flag(status_flag):
|
||||
if status_flag == "A":
|
||||
return True
|
||||
elif status_flag == "V":
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def convert_knots_to_mps(knots):
|
||||
return safe_float(knots) * 0.514444444444
|
||||
|
||||
|
||||
# Need this wrapper because math.radians doesn't auto convert inputs
|
||||
def convert_deg_to_rads(degs):
|
||||
return math.radians(safe_float(degs))
|
||||
|
||||
|
||||
"""Format for this dictionary is a sentence identifier (e.g. "GGA") as the key, with a
|
||||
list of tuples where each tuple is a field name, conversion function and index
|
||||
into the split sentence"""
|
||||
parse_maps = {
|
||||
"GGA": [
|
||||
("fix_type", int, 6),
|
||||
("latitude", convert_latitude, 2),
|
||||
("latitude_direction", str, 3),
|
||||
("longitude", convert_longitude, 4),
|
||||
("longitude_direction", str, 5),
|
||||
("altitude", safe_float, 9),
|
||||
("mean_sea_level", safe_float, 11),
|
||||
("hdop", safe_float, 8),
|
||||
("num_satellites", safe_int, 7),
|
||||
("utc_time", convert_time, 1),
|
||||
],
|
||||
"RMC": [
|
||||
("utc_time", convert_time, 1),
|
||||
("fix_valid", convert_status_flag, 2),
|
||||
("latitude", convert_latitude, 3),
|
||||
("latitude_direction", str, 4),
|
||||
("longitude", convert_longitude, 5),
|
||||
("longitude_direction", str, 6),
|
||||
("speed", convert_knots_to_mps, 7),
|
||||
("true_course", convert_deg_to_rads, 8),
|
||||
],
|
||||
"GST": [
|
||||
("utc_time", convert_time, 1),
|
||||
("ranges_std_dev", safe_float, 2),
|
||||
("semi_major_ellipse_std_dev", safe_float, 3),
|
||||
("semi_minor_ellipse_std_dev", safe_float, 4),
|
||||
("semi_major_orientation", safe_float, 5),
|
||||
("lat_std_dev", safe_float, 6),
|
||||
("lon_std_dev", safe_float, 7),
|
||||
("alt_std_dev", safe_float, 8),
|
||||
],
|
||||
"HDT": [
|
||||
("heading", safe_float, 1),
|
||||
],
|
||||
"VTG": [
|
||||
("true_course", convert_deg_to_rads, 1),
|
||||
("speed", convert_knots_to_mps, 5)
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def parse_nmea_sentence(nmea_sentence):
|
||||
# Check for a valid nmea sentence
|
||||
|
||||
if not re.match(r'(^\$GP|^\$GN|^\$GL|^\$IN).*\*[0-9A-Fa-f]{2}$', nmea_sentence):
|
||||
logger.debug("Regex didn't match, sentence not valid NMEA? Sentence was: %s"
|
||||
% repr(nmea_sentence))
|
||||
return False
|
||||
fields = [field.strip(',') for field in nmea_sentence.split(',')]
|
||||
|
||||
# Ignore the $ and talker ID portions (e.g. GP)
|
||||
sentence_type = fields[0][3:]
|
||||
|
||||
if sentence_type not in parse_maps:
|
||||
logger.debug("Sentence type %s not in parse map, ignoring."
|
||||
% repr(sentence_type))
|
||||
return False
|
||||
|
||||
parse_map = parse_maps[sentence_type]
|
||||
|
||||
parsed_sentence = {}
|
||||
for entry in parse_map:
|
||||
parsed_sentence[entry[0]] = entry[1](fields[entry[2]])
|
||||
|
||||
return {sentence_type: parsed_sentence}
|
||||
@@ -0,0 +1,63 @@
|
||||
?Á?®?ç?Æ?ç???¥ÔòØ???×???ª?¥?©?®?ò?áÔöé??ÔòØ?£
|
||||
|
||||
?¥ÔòØ???×???ª?ò?¿ãÆ?×?ó?ó?ò?«?á?Ü?ú?ç?×?¬???¥?£?ñ?Á?Ñ?ÿ?×?ü????ÔòØ?£
|
||||
sudo pip3 install transforms3d
|
||||
sudo apt install ros-humble-tf-transformations
|
||||
sudo apt install ros-humble-gps-umd
|
||||
pip3 install serial
|
||||
pip3 install pyserial
|
||||
pip install um982-driver
|
||||
|
||||
?Á?¬???Á?®?ç?Ü?Â?½?ò?½?½?×???á?Á?Ñ?ÿ?×?ü????ÔòØ?£
|
||||
rosdep install --from-paths src --ignore-src -r -y
|
||||
?Á?¬??
|
||||
rosdepc install --from-paths src --ignore-src -r -y
|
||||
|
||||
---------------------------------------------------------------------------------------------
|
||||
|
||||
?ÁÔòæ?«?Æ?ç???×ÔöÉ?ë?×?¡?«?¥?©?®?ò?áÔöé??ÔòØ?£
|
||||
|
||||
?×ÔöÉ?ë?×?¡?«?×?½?Ñ?ò???º?ò?¬?¢?ò?ë?¼?×???ñ?ò?¬?û?Á???º?ÁÔòù?é:
|
||||
sudo sh wheeltec_gnss.sh
|
||||
|
||||
|
||||
?Á?óÔöÉ?¥?Â??NMEA?ò?¼?à?×?½?½?×?????Á?ù?ëWHEELTEC G60/G70??ÔòØ?£
|
||||
ros2 launch wheeltec_gps_driver wheeltec_nmea_driver.launch.py
|
||||
|
||||
?Á?óÔöÉ?¥?Â??NMEA?ò?¼?à?×?½?½?×?????Á?ù?ëWHEELTEC G90??ÔòØ?£
|
||||
ros2 launch wheeltec_gps_driver wheeltec_dual_rtk_driver_nmea.launch.py
|
||||
|
||||
--gps topic: /gps/fix
|
||||
--gpgga topic: /gnss/gpgga
|
||||
--heading topic: /heading
|
||||
--gps frame_id: navsat_link
|
||||
|
||||
?ò?ú??RVIZ?Á?®?í?×?½Ôûæ?ò?ó???¥Ôòù?à?¥Ôòæ???òÔòæ?¬?×?ó???×ÔöÉÔòú(WHEELTEC G60/G70)??ÔòØ?£
|
||||
ros2 launch wheeltec_gps_driver nmea_gps_path.launch.py
|
||||
|
||||
?ò?ú??RVIZ?Á?®?í?×?½Ôûæ?ò?ó???¥Ôòù?à?¥Ôòæ???òÔòæ?¬?×?ó???×ÔöÉÔòú(WHEELTEC 90)??ÔòØ?£
|
||||
ros2 launch wheeltec_gps_driver nmea_gps_path_dualrtk.launch.py
|
||||
|
||||
---------------------------------------------------------------------------------------------
|
||||
|
||||
?Á?óÔöÉ?¥?Â??UBLOX?ò?¼?à?×?½?½?×?????Á?ù?ëWHEELTEC G70?Á???¡?ò?ÿ??(?ÁÔòù?áWHEELTEC G70)??ÔòØ?£
|
||||
ros2 launch wheeltec_gps_driver wheeltec_ublox_driver.launch.py
|
||||
--gps topic: /ublox_gps_node/fix
|
||||
--gps frame_id: navsat_link
|
||||
|
||||
?Á?óÔöÉ?¥?Â??UBLOX?ò?¼?à?×?½?½?ò?ú??RVIZ?Á?®?í?×?½Ôûæ?ò?ó???¥Ôòù?à?¥Ôòæ???òÔòæ?¬?×?ó???×ÔöÉÔòú(?ÁÔòù?áWHEELTEC G70)??ÔòØ?£
|
||||
ros2 launch wheeltec_gps_driver ublox_gps_path.launch.py
|
||||
|
||||
---------------------------------------------------------------------------------------------
|
||||
|
||||
?Á?óÔöÉ?¥?Â??Unicore?ò?¼?à?×?½?½?×?????Á?ù?ëWHEELTEC G90?Á???¡?ò?ÿ??(?ÁÔòù?áWHEELTEC G90)??ÔòØ?£
|
||||
ros2 launch wheeltec_gps_driver wheeltec_dual_rtk_driver_unicore.launch.py
|
||||
|
||||
?Á?óÔöÉ?¥?Â??Unicore?ò?¼?à?×?½?½?ò?ú??RVIZ?Á?®?í?×?½Ôûæ?ò?ó???¥Ôòù?à?¥Ôòæ???òÔòæ?¬?×?ó???×ÔöÉÔòú(?ÁÔòù?áWHEELTEC G90)??ÔòØ?£
|
||||
ros2 launch wheeltec_gps_driver wheeltec_dual_rtk_driver_unicore.launch.py
|
||||
|
||||
--gps topic: /gps/fix
|
||||
--gps frame_id: navsat_link
|
||||
--utm pose topic:/gps/utm_pose
|
||||
--heading topic:/gps/euler
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
from launch import LaunchDescription
|
||||
from launch_ros.actions import Node
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
import os
|
||||
|
||||
def generate_launch_description():
|
||||
return LaunchDescription([
|
||||
Node(
|
||||
package='wheeltec_dual_rtk_driver',
|
||||
executable='dual_rtk_driver_node',
|
||||
name='dual_rtk_driver_node',
|
||||
parameters=[
|
||||
{'port': '/dev/wheeltec_gnss'},
|
||||
{'baud': 115200},
|
||||
{'gps_frame_id': 'navsat_link'},
|
||||
],
|
||||
output='screen',
|
||||
),
|
||||
])
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
||||
<package format="3">
|
||||
<name>wheeltec_dual_rtk_driver</name>
|
||||
<version>0.0.0</version>
|
||||
<maintainer email="powrbv@gmail.com">root</maintainer>
|
||||
<description>TODO: Package description</description>
|
||||
<license>TODO: License declaration</license>
|
||||
|
||||
<depend>rclpy</depend>
|
||||
|
||||
<test_depend>ament_copyright</test_depend>
|
||||
<test_depend>ament_flake8</test_depend>
|
||||
<test_depend>ament_pep257</test_depend>
|
||||
<test_depend>python3-pytest</test_depend>
|
||||
|
||||
<export>
|
||||
<build_type>ament_python</build_type>
|
||||
</export>
|
||||
</package>
|
||||
@@ -0,0 +1,4 @@
|
||||
[develop]
|
||||
script_dir=$base/lib/wheeltec_dual_rtk_driver
|
||||
[install]
|
||||
install_scripts=$base/lib/wheeltec_dual_rtk_driver
|
||||
@@ -0,0 +1,28 @@
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
package_name = 'wheeltec_dual_rtk_driver'
|
||||
|
||||
setup(
|
||||
name=package_name,
|
||||
version='0.0.0',
|
||||
packages=find_packages(exclude=['test']),
|
||||
data_files=[
|
||||
('share/ament_index/resource_index/packages',
|
||||
['resource/' + package_name]),
|
||||
('share/' + package_name, ['launch/wheeltec_dual_rtk_driver_unicore.launch.py']),
|
||||
('share/' + package_name, ['package.xml']),
|
||||
|
||||
],
|
||||
install_requires=['setuptools','launch'],
|
||||
zip_safe=True,
|
||||
maintainer='wheeltec',
|
||||
description='TODO: Package description',
|
||||
license='TODO: License declaration',
|
||||
tests_require=['pytest'],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
"dual_rtk_driver_node = wheeltec_dual_rtk_driver.wheeltec_dual_rtk_driver:main",
|
||||
"serial_demo_node = wheeltec_dual_rtk_driver.serial_demo:main",
|
||||
],
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
# Copyright 2015 Open Source Robotics Foundation, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from ament_copyright.main import main
|
||||
import pytest
|
||||
|
||||
|
||||
# Remove the `skip` decorator once the source file(s) have a copyright header
|
||||
@pytest.mark.skip(reason='No copyright header has been placed in the generated source file.')
|
||||
@pytest.mark.copyright
|
||||
@pytest.mark.linter
|
||||
def test_copyright():
|
||||
rc = main(argv=['.', 'test'])
|
||||
assert rc == 0, 'Found errors'
|
||||
@@ -0,0 +1,25 @@
|
||||
# Copyright 2017 Open Source Robotics Foundation, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from ament_flake8.main import main_with_errors
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.flake8
|
||||
@pytest.mark.linter
|
||||
def test_flake8():
|
||||
rc, errors = main_with_errors(argv=[])
|
||||
assert rc == 0, \
|
||||
'Found %d code style errors / warnings:\n' % len(errors) + \
|
||||
'\n'.join(errors)
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copyright 2015 Open Source Robotics Foundation, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from ament_pep257.main import main
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.linter
|
||||
@pytest.mark.pep257
|
||||
def test_pep257():
|
||||
rc = main(argv=['.', 'test'])
|
||||
assert rc == 0, 'Found code style errors / warnings'
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
# serial_demo.py
|
||||
# coding=utf-8
|
||||
|
||||
from wheeltec_dual_rtk_driver.um982_serial import UM982Serial
|
||||
import time
|
||||
|
||||
|
||||
def main():
|
||||
port = "/dev/wheeltec_gnss"
|
||||
baudrate = 115200
|
||||
|
||||
print(f"/dev/wheeltec_gnss opening ...")
|
||||
try:
|
||||
um = UM982Serial(port, baudrate)
|
||||
print(f'serial {port} open successfully! data reading...')
|
||||
|
||||
for i in range(10):
|
||||
um.read_frame()
|
||||
if um.fix:
|
||||
print(f"[{i}] fix: {um.fix}")
|
||||
if um.orientation:
|
||||
print(f"[{i}] pose: {um.orientation}")
|
||||
if um.vel:
|
||||
print(f"[{i}] vel: {um.vel}")
|
||||
time.sleep(0.5)
|
||||
|
||||
um.stop()
|
||||
print("test stop,closed")
|
||||
|
||||
except Exception as e:
|
||||
print(f"serial {port} do not open!: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
# coding=utf-8
|
||||
from pyproj import CRS, Transformer
|
||||
import threading
|
||||
import serial
|
||||
import time
|
||||
import math
|
||||
|
||||
|
||||
|
||||
def crc_table():
|
||||
table = []
|
||||
for i in range(256):
|
||||
crc = i
|
||||
for j in range(8, 0, -1):
|
||||
if crc & 1:
|
||||
crc = (crc >> 1) ^ 0xEDB88320
|
||||
else:
|
||||
crc >>= 1
|
||||
table.append(crc)
|
||||
return table
|
||||
|
||||
NMEA_EXPEND_CRC_TABLE = crc_table()
|
||||
|
||||
def open_serial_with_retry(port, baudrate, retry=5, delay=1):
|
||||
for i in range(retry):
|
||||
try:
|
||||
ser = serial.Serial(port, baudrate, timeout=1)
|
||||
#print(f"串口{port}打开成功")
|
||||
return ser
|
||||
except serial.SerialException as e:
|
||||
#print(f"[尝试{i+1}] 串口 {port} 打开失败: {e}")
|
||||
time.sleep(delay)
|
||||
raise serial.SerialException(f"串口 {port} 在 {retry} 次尝试后仍无法打开")
|
||||
|
||||
def nmea_expend_crc(nmea_expend_sentence):
|
||||
def calculate_crc32(data):
|
||||
crc = 0
|
||||
for byte in data:
|
||||
crc = NMEA_EXPEND_CRC_TABLE[(crc ^ byte) & 0xFF] ^ (crc >> 8)
|
||||
return crc & 0xFFFFFFFF
|
||||
|
||||
try:
|
||||
sentence, crc = nmea_expend_sentence[1:].split("*")
|
||||
crc = crc[:8]
|
||||
except:
|
||||
return False
|
||||
calculated_crc = calculate_crc32(sentence.encode())
|
||||
return crc.lower() == format(calculated_crc, '08x')
|
||||
|
||||
def nmea_crc(nmea_sentence):
|
||||
# 移除起始的'$'和'*'及之后的校验和部分
|
||||
try:
|
||||
sentence, crc = nmea_sentence[1:].split("*")
|
||||
crc = crc[:2]
|
||||
except:
|
||||
return False
|
||||
calculated_checksum = 0
|
||||
# 对字符串中的每个字符进行异或运算
|
||||
for char in sentence:
|
||||
calculated_checksum ^= ord(char)
|
||||
# 将计算得到的校验和转换为十六进制格式,并大写
|
||||
calculated_checksum_hex = format(calculated_checksum, 'X')
|
||||
# 校验和比较
|
||||
return calculated_checksum_hex.zfill(2) == crc.upper()
|
||||
|
||||
def msg_seperate(msg:str):
|
||||
return msg[1:msg.find('*')].split(',')
|
||||
|
||||
def PVTSLN_solver(msg:str):
|
||||
"""解析PVTSLN消息,提取GPS时间和位置信息
|
||||
|
||||
PVTSLN消息格式:
|
||||
#PVTSLNA,<len>,<time_system>,<sol_status>,<gps_week>,<gps_ms>,...;<pos_type>,<height>,<lat>,<lon>,...
|
||||
"""
|
||||
parts = msg_seperate(msg)
|
||||
|
||||
# 消息头部分(字段0-9,注意parts[9]可能包含分号)
|
||||
# parts[0] = PVTSLNA
|
||||
# parts[1] = 消息长度
|
||||
# parts[2] = 时间系统 (GPS/BDST/GLOST/GALST)
|
||||
# parts[3] = 解状态 (FINE/...)
|
||||
# parts[4] = GPS周
|
||||
# parts[5] = GPS周内秒(毫秒)
|
||||
gps_week = int(parts[4])
|
||||
gps_seconds = float(parts[5]) / 1000.0 # 毫秒转秒
|
||||
gps_time = (gps_week, gps_seconds)
|
||||
|
||||
# 处理可能包含分号的字段(如 "13;SINGLE")
|
||||
if ';' in parts[9]:
|
||||
pos_type = parts[9].split(';')[1] # 提取分号后的位置类型
|
||||
else:
|
||||
pos_type = parts[10] if len(parts) > 10 else ""
|
||||
|
||||
# 位置数据段(从字段10开始,如果parts[9]包含分号则从parts[10]开始)
|
||||
# 数据段1:主天线位置
|
||||
base_idx = 10 if ';' in parts[9] else 11
|
||||
bestpos_hgt = float(parts[base_idx]) # 海拔高
|
||||
bestpos_lat = float(parts[base_idx + 1]) # 纬度
|
||||
bestpos_lon = float(parts[base_idx + 2]) # 经度
|
||||
bestpos_hgtstd = float(parts[base_idx + 3]) # 高程标准差
|
||||
bestpos_latstd = float(parts[base_idx + 4]) # 纬度标准差
|
||||
bestpos_lonstd = float(parts[base_idx + 5]) # 经度标准差
|
||||
|
||||
fix = (bestpos_hgt, bestpos_lat, bestpos_lon, bestpos_hgtstd, bestpos_latstd, bestpos_lonstd)
|
||||
|
||||
# 额外信息(如果有)
|
||||
num_sats = int(parts[8]) if len(parts) > 8 else 0 # 卫星数
|
||||
|
||||
return fix, gps_time, pos_type, num_sats
|
||||
|
||||
|
||||
def GNHPR_solver(msg:str):
|
||||
parts = msg_seperate(msg)
|
||||
heading = float(parts[3-1])
|
||||
pitch = float(parts[4-1])
|
||||
roll = float(parts[5-1])
|
||||
orientation = (heading, pitch, roll)
|
||||
return orientation
|
||||
|
||||
|
||||
def BESTNAV_solver(msg:str):
|
||||
parts = msg_seperate(msg)
|
||||
vel_hor_std = float(parts[-1]) # 水平速度标准差,单位 m/s
|
||||
vel_ver_std = float(parts[-2]) # 高程速度标准差,单位 m/s
|
||||
vel_ver = float(parts[-3]) # 垂直速度, m/s,正值表示高度增加(向上),负值表示高度下降(向下)
|
||||
vel_heading = float(parts[-4]) # 相对于真北的实际对地运动方向(相对地面轨迹), deg
|
||||
vel_hor = float(parts[-5]) # 对地水平速度, m/s
|
||||
vel_north = vel_hor * math.cos(math.radians(vel_heading)) # 分解得到北方向速度
|
||||
vel_east = vel_hor * math.sin(math.radians(vel_heading)) # 分解得到东方向速度
|
||||
return (vel_east, vel_north, vel_ver, vel_hor_std, vel_hor_std, vel_ver_std)
|
||||
|
||||
|
||||
|
||||
def create_utm_trans(lat, lon):
|
||||
"""构建转换器,用于将WGS84地理坐标系下的点转换为UTM坐标系下的点。
|
||||
|
||||
Args:
|
||||
lon (float): 点的经度。
|
||||
lat (float): 点的纬度。
|
||||
|
||||
Returns:
|
||||
transformer: 转换器
|
||||
"""
|
||||
# UTM区号是根据经度确定的,从-180度开始每6度一个区间。
|
||||
zone_number = int((lon + 180) / 6) + 1
|
||||
# 北半球是赤道(纬度0度)以上的区域。
|
||||
isnorth = lat >= 0
|
||||
# 定义WGS84坐标系
|
||||
wgs84_crs = CRS("epsg:4326")
|
||||
# 根据是否位于北半球,选择合适的UTM EPSG代码
|
||||
utm_crs_str = f"epsg:326{zone_number}" if isnorth else f"epsg:327{zone_number}"
|
||||
utm_crs = CRS(utm_crs_str)
|
||||
# 创建坐标转换器,从WGS84转换到UTM
|
||||
transformer = Transformer.from_crs(wgs84_crs, utm_crs, always_xy=True)
|
||||
return transformer
|
||||
|
||||
|
||||
def utm_trans(transformer, lon, lat):
|
||||
"""将WGS84地理坐标系下的点转换为UTM坐标系下的点。
|
||||
|
||||
Args:
|
||||
transformer: 转换器
|
||||
lon (float): 点的经度。
|
||||
lat (float): 点的纬度。
|
||||
|
||||
Returns:
|
||||
tuple: 一个元组,包含转换后的UTM坐标系下的x(东坐标)和y(北坐标)。
|
||||
"""
|
||||
# 进行坐标转换
|
||||
utm_x, utm_y = transformer.transform(lon, lat)
|
||||
return (utm_x, utm_y)
|
||||
|
||||
|
||||
class UM982Serial(threading.Thread):
|
||||
def __init__(self, port, band):
|
||||
super().__init__()
|
||||
# 打开串口
|
||||
#self.ser = serial.Serial(port, band)
|
||||
self.ser = open_serial_with_retry(port, band)
|
||||
|
||||
# 设置运行标志位
|
||||
self.isRUN = True
|
||||
# 数据
|
||||
self.fix = None # sensor_msgs/NavSatFix 所需要的数据
|
||||
self.orientation = None # 航向角
|
||||
self.vel = None # 速度
|
||||
self.utmpos = None
|
||||
self.gps_time = None # GPS时间 (week, seconds)
|
||||
self.pos_type = "" # 定位类型
|
||||
self.num_sats = 0 # 卫星数
|
||||
# 读初始数据
|
||||
for i in range(30):
|
||||
self.read_frame()
|
||||
print(self.fix)
|
||||
#if self.fix is None:
|
||||
# raise RuntimeError("未能从串口接收到有效的 PVTSLN 定位数据,串口初始化失败")
|
||||
|
||||
# wgs84转utm
|
||||
bestpos_hgt, bestpos_lat, bestpos_lon, bestpos_hgtstd, bestpos_latstd, bestpos_lonstd = self.fix
|
||||
self.transformer = create_utm_trans(bestpos_lat, bestpos_lon)
|
||||
self.utmpos = utm_trans(self.transformer, bestpos_lon, bestpos_lat)
|
||||
|
||||
|
||||
def stop(self):
|
||||
""" 结束运行 """
|
||||
self.isRUN = False
|
||||
time.sleep(0.1)
|
||||
self.ser.close()
|
||||
|
||||
|
||||
# def read_frame(self):
|
||||
# frame = self.ser.readline().decode('utf-8')
|
||||
# if frame.startswith("#PVTSLNA") and nmea_expend_crc(frame):
|
||||
# self.fix = PVTSLN_solver(frame)
|
||||
# elif frame.startswith("$GNHPR") and nmea_crc(frame):
|
||||
# self.orientation = GNHPR_solver(frame)
|
||||
# elif frame.startswith("#BESTNAVA") and nmea_expend_crc(frame):
|
||||
# self.vel = BESTNAV_solver(frame)
|
||||
|
||||
def read_frame(self):
|
||||
try:
|
||||
raw = self.ser.readline()
|
||||
frame = raw.decode('utf-8', errors='ignore').strip()
|
||||
if frame.startswith("#PVTSLNA") and nmea_expend_crc(frame):
|
||||
self.fix, self.gps_time, self.pos_type, self.num_sats = PVTSLN_solver(frame)
|
||||
elif frame.startswith("$GNHPR") and nmea_crc(frame):
|
||||
self.orientation = GNHPR_solver(frame)
|
||||
elif frame.startswith("#BESTNAVA") and nmea_expend_crc(frame):
|
||||
self.vel = BESTNAV_solver(frame)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
|
||||
def run(self):
|
||||
while self.isRUN:
|
||||
self.read_frame()
|
||||
bestpos_hgt, bestpos_lat, bestpos_lon, bestpos_hgtstd, bestpos_latstd, bestpos_lonstd = self.fix
|
||||
self.utmpos = utm_trans(self.transformer, bestpos_lon, bestpos_lat)
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
um982 = UM982Serial("/dev/wheeltec_gnss", 115200)
|
||||
um982.start()
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
# coding=utf-8
|
||||
import sys
|
||||
import math
|
||||
|
||||
import rclpy
|
||||
from rclpy.node import Node
|
||||
from sensor_msgs.msg import NavSatFix, TimeReference
|
||||
from nav_msgs.msg import Odometry
|
||||
from tf_transformations import quaternion_from_euler, euler_from_quaternion
|
||||
|
||||
# GPS纪元偏移:从1980-01-06到1970-01-01的秒数,减去18秒闰秒
|
||||
GPS_EPOCH_OFFSET = 315964800 - 18
|
||||
|
||||
|
||||
from wheeltec_dual_rtk_driver.um982_serial import UM982Serial
|
||||
from geometry_msgs.msg import Vector3Stamped
|
||||
|
||||
|
||||
class wheeltec_dual_rtk_driver(Node):
|
||||
def _ros_log_debug(self, log_data):
|
||||
self.get_logger().debug(str(log_data))
|
||||
|
||||
def _ros_log_info(self, log_data):
|
||||
self.get_logger().info(str(log_data))
|
||||
|
||||
def _ros_log_warn(self, log_data):
|
||||
self.get_logger().warn(str(log_data))
|
||||
|
||||
def _ros_log_error(self, log_data):
|
||||
self.get_logger().error(str(log_data))
|
||||
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__('um982_serial_driver')
|
||||
global gps_frame_id
|
||||
|
||||
# Step1:从参数服务器获取所有参数
|
||||
self.declare_parameter('port', '/dev/wheeltec_gnss')
|
||||
self.declare_parameter('baud', 115200)
|
||||
self.declare_parameter('gps_frame_id', 'navsat_link')
|
||||
self.declare_parameter('publish_rate', 20.0)
|
||||
self.declare_parameter('fix_topic', '/gps/fix')
|
||||
self.declare_parameter('utm_pose_topic', '/gps/utm_pose')
|
||||
self.declare_parameter('euler_topic', '/gps/euler')
|
||||
self.declare_parameter('time_reference_topic', '/gps/time_reference')
|
||||
|
||||
port = self.get_parameter('port').get_parameter_value().string_value
|
||||
baud = self.get_parameter('baud').get_parameter_value().integer_value
|
||||
gps_frame_id = self.get_parameter('gps_frame_id').get_parameter_value().string_value
|
||||
publish_rate = self.get_parameter('publish_rate').get_parameter_value().double_value
|
||||
fix_topic = self.get_parameter('fix_topic').get_parameter_value().string_value
|
||||
utm_pose_topic = self.get_parameter('utm_pose_topic').get_parameter_value().string_value
|
||||
euler_topic = self.get_parameter('euler_topic').get_parameter_value().string_value
|
||||
time_reference_topic = self.get_parameter('time_reference_topic').get_parameter_value().string_value
|
||||
|
||||
# Step2:打开串口
|
||||
try:
|
||||
self.um982serial = UM982Serial(port, baud)
|
||||
self._ros_log_info(f'serial {port} open successfully!')
|
||||
except:
|
||||
self._ros_log_error(f'serial {port} do not open!')
|
||||
sys.exit(0)
|
||||
# Step3:新建一个线程用于处理串口数据
|
||||
self.um982serial.start()
|
||||
# Step4:ROS相关
|
||||
self.fix_pub = self.create_publisher(NavSatFix, fix_topic, 10)
|
||||
self.utm_pub = self.create_publisher(Odometry, utm_pose_topic, 10)
|
||||
self.euler_pub = self.create_publisher(Vector3Stamped, euler_topic, 10)
|
||||
self.time_ref_pub = self.create_publisher(TimeReference, time_reference_topic, 10)
|
||||
|
||||
self.pub_timer = self.create_timer(1.0 / publish_rate, self.pub_task)
|
||||
self._ros_log_info(f'GPS publish rate: {publish_rate} Hz')
|
||||
|
||||
def pub_task(self):
|
||||
bestpos_hgt, bestpos_lat, bestpos_lon, bestpos_hgtstd, bestpos_latstd, bestpos_lonstd = self.um982serial.fix
|
||||
utm_x, utm_y = self.um982serial.utmpos
|
||||
vel_east, vel_north, vel_ver, vel_east_std, vel_north_std, vel_ver_std = self.um982serial.vel
|
||||
heading, pitch, roll = self.um982serial.orientation
|
||||
gps_week, gps_seconds = self.um982serial.gps_time if self.um982serial.gps_time else (0, 0.0)
|
||||
this_time = self.get_clock().now().to_msg()
|
||||
|
||||
# Step 1: Publish GPS Fix Data
|
||||
fix_msg = NavSatFix()
|
||||
fix_msg.header.stamp = this_time
|
||||
fix_msg.header.frame_id = gps_frame_id
|
||||
fix_msg.latitude = bestpos_lat
|
||||
fix_msg.longitude = bestpos_lon
|
||||
fix_msg.altitude = bestpos_hgt
|
||||
fix_msg.position_covariance[0] = float(bestpos_latstd)**2
|
||||
fix_msg.position_covariance[4] = float(bestpos_lonstd)**2
|
||||
fix_msg.position_covariance[8] = float(bestpos_hgtstd)**2
|
||||
fix_msg.position_covariance_type = NavSatFix.COVARIANCE_TYPE_DIAGONAL_KNOWN
|
||||
self.fix_pub.publish(fix_msg)
|
||||
|
||||
euler_msg = Vector3Stamped()
|
||||
euler_msg.header.stamp = this_time
|
||||
euler_msg.header.frame_id = 'euler_link'
|
||||
euler_msg.vector.x = roll # x轴表示roll(横滚)?math.radians(roll)math.radians(pitch)math.radians(heading)
|
||||
euler_msg.vector.y = pitch # y轴表示pitch(俯仰)
|
||||
euler_msg.vector.z = heading # z轴表示heading(偏航)
|
||||
self.euler_pub.publish(euler_msg)
|
||||
|
||||
# Step 2: Publish UTM Position Data
|
||||
odom_msg = Odometry()
|
||||
odom_msg.header.stamp = this_time
|
||||
odom_msg.header.frame_id = 'earth'
|
||||
odom_msg.child_frame_id = 'base_link'
|
||||
odom_msg.pose.pose.position.x = utm_x
|
||||
odom_msg.pose.pose.position.y = utm_y
|
||||
odom_msg.pose.pose.position.z = bestpos_hgt
|
||||
quaternion = quaternion_from_euler(math.radians(roll), math.radians(pitch), math.radians(heading))
|
||||
odom_msg.pose.pose.orientation.x = quaternion[0]
|
||||
odom_msg.pose.pose.orientation.y = quaternion[1]
|
||||
odom_msg.pose.pose.orientation.z = quaternion[2]
|
||||
odom_msg.pose.pose.orientation.w = quaternion[3]
|
||||
odom_msg.pose.covariance = [0.0] * 36
|
||||
odom_msg.pose.covariance[0] = float(bestpos_latstd)**2
|
||||
odom_msg.pose.covariance[7] = float(bestpos_lonstd)**2
|
||||
odom_msg.pose.covariance[14] = float(bestpos_hgtstd)**2
|
||||
odom_msg.pose.covariance[21] = 0.1
|
||||
odom_msg.pose.covariance[28] = 0.1
|
||||
odom_msg.pose.covariance[35] = 0.1
|
||||
odom_msg.twist.twist.linear.x = vel_east
|
||||
odom_msg.twist.twist.linear.y = vel_north
|
||||
odom_msg.twist.twist.linear.z = vel_ver
|
||||
odom_msg.twist.covariance = [0.0] * 36
|
||||
odom_msg.twist.covariance[0] = float(vel_east_std)**2
|
||||
odom_msg.twist.covariance[7] = float(vel_north_std)**2
|
||||
odom_msg.twist.covariance[14] = float(vel_ver_std)**2
|
||||
self.utm_pub.publish(odom_msg)
|
||||
|
||||
# Step 3: Publish GPS Time Reference for sensor synchronization
|
||||
time_ref_msg = TimeReference()
|
||||
time_ref_msg.header.stamp = this_time # ROS接收时间
|
||||
# GPS总秒数 = GPS周 * 604800 + 周内秒 + 纪元偏移
|
||||
gps_total_seconds = gps_week * 604800 + int(gps_seconds) + GPS_EPOCH_OFFSET
|
||||
gps_nanoseconds = int((gps_seconds % 1.0) * 1e9)
|
||||
time_ref_msg.time_ref.sec = gps_total_seconds
|
||||
time_ref_msg.time_ref.nanosec = gps_nanoseconds
|
||||
time_ref_msg.source = "gps"
|
||||
self.time_ref_pub.publish(time_ref_msg)
|
||||
|
||||
# Test
|
||||
_, _, yaw = euler_from_quaternion(quaternion)
|
||||
#print(heading)
|
||||
|
||||
|
||||
def run(self):
|
||||
if rclpy.ok():
|
||||
rclpy.spin(self)
|
||||
|
||||
def stop(self):
|
||||
self.um982serial.stop()
|
||||
self.pub_timer.cancel()
|
||||
|
||||
|
||||
|
||||
import time
|
||||
import signal
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
dual_rtk_driver.stop()
|
||||
time.sleep(0.1)
|
||||
if rclpy.ok():
|
||||
rclpy.shutdown()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
rclpy.init()
|
||||
dual_rtk_driver = wheeltec_dual_rtk_driver()
|
||||
|
||||
|
||||
def main():
|
||||
dual_rtk_driver.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,11 @@
|
||||
echo 'KERNEL=="ttyACM*", ATTRS{idVendor}=="1546", ATTRS{idProduct}=="01a9", MODE:="0777", GROUP:="dialout", SYMLINK+="wheeltec_gnss"' >/etc/udev/rules.d/wheeltec_gnss.rules
|
||||
echo 'KERNEL=="ttyUSB*", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60",ATTRS{serial}=="0005", MODE:="0777", GROUP:="dialout", SYMLINK+="wheeltec_gnss"' >/etc/udev/rules.d/wheeltec_gps.rules
|
||||
echo 'KERNEL=="ttyCH343USB*", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="55d4", ATTRS{serial}=="0005", MODE:="0777", GROUP:="dialout", SYMLINK+="wheeltec_gnss"' >/etc/udev/rules.d/wheeltec_gnss_343.rules
|
||||
#CH9102,同时系统没有安装对应驱动 串口号0005
|
||||
echo 'KERNEL=="ttyACM*", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="55d4",ATTRS{serial}=="0005", MODE:="0777", GROUP:="dialout", SYMLINK+="wheeltec_gnss"' >>/etc/udev/rules.d/wheeltec_gnss_343.rules
|
||||
|
||||
service udev reload
|
||||
sleep 2
|
||||
service udev restart
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
cmake_minimum_required(VERSION 3.8)
|
||||
project(wheeltec_gps_driver)
|
||||
|
||||
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
add_compile_options(-Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
|
||||
# find dependencies
|
||||
find_package(ament_cmake REQUIRED)
|
||||
# uncomment the following section in order to fill in
|
||||
# further dependencies manually.
|
||||
# find_package(<dependency> REQUIRED)
|
||||
find_package(ament_cmake REQUIRED)
|
||||
find_package(geometry_msgs REQUIRED)
|
||||
find_package(nav2_msgs REQUIRED)
|
||||
find_package(rclcpp REQUIRED)
|
||||
find_package(rclpy REQUIRED)
|
||||
find_package(sensor_msgs REQUIRED)
|
||||
find_package(tf2_geometry_msgs REQUIRED)
|
||||
|
||||
include_directories(include)
|
||||
|
||||
add_executable(gps_path src/gps_path.cpp)
|
||||
ament_target_dependencies(gps_path geometry_msgs nav2_msgs rclcpp sensor_msgs tf2_geometry_msgs)
|
||||
|
||||
install(TARGETS
|
||||
gps_path
|
||||
DESTINATION lib/${PROJECT_NAME}
|
||||
)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
find_package(ament_lint_auto REQUIRED)
|
||||
# the following line skips the linter which checks for copyrights
|
||||
# comment the line when a copyright and license is added to all source files
|
||||
set(ament_cmake_copyright_FOUND TRUE)
|
||||
# the following line skips cpplint (only works in a git repo)
|
||||
# comment the line when this package is in a git repo and when
|
||||
# a copyright and license is added to all source files
|
||||
set(ament_cmake_cpplint_FOUND TRUE)
|
||||
ament_lint_auto_find_test_dependencies()
|
||||
endif()
|
||||
|
||||
install(
|
||||
DIRECTORY launch config
|
||||
DESTINATION share/${PROJECT_NAME}
|
||||
)
|
||||
|
||||
ament_package()
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# Wheeltec Dual RTK GNSS Driver Parameters
|
||||
# 双RTK GNSS驱动参数配置
|
||||
|
||||
/**:
|
||||
ros__parameters:
|
||||
# ============================================
|
||||
# 串口连接参数
|
||||
# ============================================
|
||||
port: "/dev/ttyACM0" # GNSS设备串口路径
|
||||
baud: 115200 # 串口波特率
|
||||
|
||||
# ============================================
|
||||
# 坐标系配置
|
||||
# ============================================
|
||||
gps_frame_id: "gnss_link" # GPS消息的frame_id
|
||||
|
||||
# ============================================
|
||||
# 发布频率设置
|
||||
# ============================================
|
||||
publish_rate: 20.0 # GPS数据发布频率 (Hz)
|
||||
|
||||
# ============================================
|
||||
# 话题名称配置
|
||||
# ============================================
|
||||
fix_topic: "/sensing/gnss/fix" # NavSatFix GPS原始数据
|
||||
utm_pose_topic: "/sensing/gnss/utm_pose" # UTM坐标系下的里程计数据
|
||||
euler_topic: "/sensing/gnss/euler" # 欧拉角 (roll, pitch, heading)
|
||||
time_reference_topic: "/sensing/gnss/time_reference" # GPS时间参考
|
||||
|
||||
# ============================================
|
||||
# NMEA解析参数 (保留用于兼容性)
|
||||
# ============================================
|
||||
time_ref_source: "gps" # 时间参考源
|
||||
useRMC: False # 是否使用RMC消息获取位置
|
||||
|
||||
# ============================================
|
||||
# 安装参数
|
||||
# ============================================
|
||||
# 设备安装角度旋转(度),用于校准安装方向
|
||||
rotation: 0.0
|
||||
|
||||
# ============================================
|
||||
# TF参数
|
||||
# ============================================
|
||||
# TF坐标前缀
|
||||
tf_prefix: ""
|
||||
# 是否发布TF变换
|
||||
publish_tf: true
|
||||
|
||||
# ============================================
|
||||
# 诊断参数
|
||||
# ============================================
|
||||
# 估计位置误差(EPE)质量等级参数
|
||||
epe_quality0: 1000000 # 未知/无效
|
||||
epe_quality1: 4.0 # SPS模式
|
||||
epe_quality2: 0.1 # DGPS模式
|
||||
epe_quality4: 0.02 # RTK固定解
|
||||
epe_quality5: 4.0 # RTK浮点解
|
||||
epe_quality9: 3.0 # WAAS模式
|
||||
@@ -0,0 +1,32 @@
|
||||
# NMEA GNSS Driver Parameters
|
||||
# NMEA GNSS驱动参数配置
|
||||
|
||||
nmea_navsat_driver:
|
||||
ros__parameters:
|
||||
# ============================================
|
||||
# 串口连接参数
|
||||
# ============================================
|
||||
port: "/dev/ttyACM0" # GNSS设备串口路径
|
||||
baud: 9600 # 串口波特率 (NMEA通常使用9600)
|
||||
|
||||
# ============================================
|
||||
# 坐标系配置
|
||||
# ============================================
|
||||
frame_id: "gnss_link" # GPS消息的frame_id
|
||||
|
||||
# ============================================
|
||||
# NMEA解析参数
|
||||
# ============================================
|
||||
time_ref_source: "gps" # 时间参考源
|
||||
useRMC: False # 是否使用RMC消息获取位置
|
||||
|
||||
# ============================================
|
||||
# 安装参数
|
||||
# ============================================
|
||||
# 设备安装角度旋转(度),用于校准安装方向
|
||||
rotation: 0.0
|
||||
|
||||
# ============================================
|
||||
# TF参数
|
||||
# ============================================
|
||||
tf_prefix: "" # TF坐标前缀
|
||||
@@ -0,0 +1,7 @@
|
||||
nmea_navsat_driver:
|
||||
ros__parameters:
|
||||
port: "/dev/ttyACM0"
|
||||
baud: 9600
|
||||
frame_id: "navsat_link"
|
||||
time_ref_source: "gps"
|
||||
useRMC: False
|
||||
@@ -0,0 +1,161 @@
|
||||
Panels:
|
||||
- Class: rviz_common/Displays
|
||||
Help Height: 78
|
||||
Name: Displays
|
||||
Property Tree Widget:
|
||||
Expanded:
|
||||
- /Global Options1
|
||||
- /Status1
|
||||
Splitter Ratio: 0.5
|
||||
Tree Height: 557
|
||||
- Class: rviz_common/Selection
|
||||
Name: Selection
|
||||
- Class: rviz_common/Tool Properties
|
||||
Expanded:
|
||||
- /2D Goal Pose1
|
||||
- /Publish Point1
|
||||
Name: Tool Properties
|
||||
Splitter Ratio: 0.5886790156364441
|
||||
- Class: rviz_common/Views
|
||||
Expanded:
|
||||
- /Current View1
|
||||
Name: Views
|
||||
Splitter Ratio: 0.5
|
||||
- Class: rviz_common/Time
|
||||
Experimental: false
|
||||
Name: Time
|
||||
SyncMode: 0
|
||||
SyncSource: ""
|
||||
Visualization Manager:
|
||||
Class: ""
|
||||
Displays:
|
||||
- Alpha: 0.5
|
||||
Cell Size: 1
|
||||
Class: rviz_default_plugins/Grid
|
||||
Color: 160; 160; 164
|
||||
Enabled: true
|
||||
Line Style:
|
||||
Line Width: 0.029999999329447746
|
||||
Value: Lines
|
||||
Name: Grid
|
||||
Normal Cell Count: 0
|
||||
Offset:
|
||||
X: 0
|
||||
Y: 0
|
||||
Z: 0
|
||||
Plane: XY
|
||||
Plane Cell Count: 10
|
||||
Reference Frame: <Fixed Frame>
|
||||
Value: true
|
||||
- Alpha: 1
|
||||
Buffer Length: 1
|
||||
Class: rviz_default_plugins/Path
|
||||
Color: 25; 255; 0
|
||||
Enabled: true
|
||||
Head Diameter: 0.30000001192092896
|
||||
Head Length: 0.20000000298023224
|
||||
Length: 0.30000001192092896
|
||||
Line Style: Lines
|
||||
Line Width: 0.029999999329447746
|
||||
Name: Path
|
||||
Offset:
|
||||
X: 0
|
||||
Y: 0
|
||||
Z: 0
|
||||
Pose Color: 255; 85; 255
|
||||
Pose Style: None
|
||||
Radius: 0.029999999329447746
|
||||
Shaft Diameter: 0.10000000149011612
|
||||
Shaft Length: 0.10000000149011612
|
||||
Topic:
|
||||
Depth: 5
|
||||
Durability Policy: Volatile
|
||||
Filter size: 10
|
||||
History Policy: Keep Last
|
||||
Reliability Policy: Reliable
|
||||
Value: /gps_path
|
||||
Value: true
|
||||
Enabled: true
|
||||
Global Options:
|
||||
Background Color: 48; 48; 48
|
||||
Fixed Frame: path
|
||||
Frame Rate: 30
|
||||
Name: root
|
||||
Tools:
|
||||
- Class: rviz_default_plugins/Interact
|
||||
Hide Inactive Objects: true
|
||||
- Class: rviz_default_plugins/MoveCamera
|
||||
- Class: rviz_default_plugins/Select
|
||||
- Class: rviz_default_plugins/FocusCamera
|
||||
- Class: rviz_default_plugins/Measure
|
||||
Line color: 128; 128; 0
|
||||
- Class: rviz_default_plugins/SetInitialPose
|
||||
Covariance x: 0.25
|
||||
Covariance y: 0.25
|
||||
Covariance yaw: 0.06853891909122467
|
||||
Topic:
|
||||
Depth: 5
|
||||
Durability Policy: Volatile
|
||||
History Policy: Keep Last
|
||||
Reliability Policy: Reliable
|
||||
Value: /initialpose
|
||||
- Class: rviz_default_plugins/SetGoal
|
||||
Topic:
|
||||
Depth: 5
|
||||
Durability Policy: Volatile
|
||||
History Policy: Keep Last
|
||||
Reliability Policy: Reliable
|
||||
Value: /goal_pose
|
||||
- Class: rviz_default_plugins/PublishPoint
|
||||
Single click: true
|
||||
Topic:
|
||||
Depth: 5
|
||||
Durability Policy: Volatile
|
||||
History Policy: Keep Last
|
||||
Reliability Policy: Reliable
|
||||
Value: /clicked_point
|
||||
Transformation:
|
||||
Current:
|
||||
Class: rviz_default_plugins/TF
|
||||
Value: true
|
||||
Views:
|
||||
Current:
|
||||
Class: rviz_default_plugins/Orbit
|
||||
Distance: 8.548384666442871
|
||||
Enable Stereo Rendering:
|
||||
Stereo Eye Separation: 0.05999999865889549
|
||||
Stereo Focal Distance: 1
|
||||
Swap Stereo Eyes: false
|
||||
Value: false
|
||||
Focal Point:
|
||||
X: 0
|
||||
Y: 0
|
||||
Z: 0
|
||||
Focal Shape Fixed Size: true
|
||||
Focal Shape Size: 0.05000000074505806
|
||||
Invert Z Axis: false
|
||||
Name: Current View
|
||||
Near Clip Distance: 0.009999999776482582
|
||||
Pitch: 0.8553979992866516
|
||||
Target Frame: <Fixed Frame>
|
||||
Value: Orbit (rviz)
|
||||
Yaw: 2.980397939682007
|
||||
Saved: ~
|
||||
Window Geometry:
|
||||
Displays:
|
||||
collapsed: false
|
||||
Height: 854
|
||||
Hide Left Dock: false
|
||||
Hide Right Dock: true
|
||||
QMainWindow State: 000000ff00000000fd000000040000000000000156000002b8fc0200000008fb0000001200530065006c0065006300740069006f006e00000001e10000009b0000005c00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000003d000002b8000000c900fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261000000010000010f000002b0fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073000000003d000002b0000000a400fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000005cb0000003efc0100000002fb0000000800540069006d00650100000000000005cb000002fb00fffffffb0000000800540069006d006501000000000000045000000000000000000000046f000002b800000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000
|
||||
Selection:
|
||||
collapsed: false
|
||||
Time:
|
||||
collapsed: false
|
||||
Tool Properties:
|
||||
collapsed: false
|
||||
Views:
|
||||
collapsed: true
|
||||
Width: 1483
|
||||
X: 70
|
||||
Y: 27
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# u-blox GNSS Driver Parameters
|
||||
# u-blox GNSS驱动参数配置
|
||||
|
||||
ublox_gps_node:
|
||||
ros__parameters:
|
||||
# ============================================
|
||||
# 设备连接参数
|
||||
# ============================================
|
||||
device: "/dev/ttyACM0" # u-blox设备串口路径
|
||||
frame_id: "navsat_link" # GPS消息的frame_id
|
||||
|
||||
# ============================================
|
||||
# 串口参数
|
||||
# ============================================
|
||||
uart1:
|
||||
baudrate: 115200 # 波特率
|
||||
in: 7 # 输入协议 (7 = RTCM3 + NMEA + UBX)
|
||||
out: 1 # 输出协议 (1 = UBX)
|
||||
|
||||
# ============================================
|
||||
# 消息配置
|
||||
# ============================================
|
||||
# 导航消息发布频率 (Hz)
|
||||
rate: 10.0
|
||||
|
||||
# 导航解算动态模型
|
||||
# 可选值: portable, stationary, pedestrian, automotive, sea, airborne1g, airborne2g, airborne4g, wrist
|
||||
dyn_model: "automotive"
|
||||
|
||||
# ============================================
|
||||
# GNSS系统配置
|
||||
# ============================================
|
||||
gnss:
|
||||
gps: true # GPS启用
|
||||
glonass: true # GLONASS启用
|
||||
beidou: true # 北斗启用
|
||||
galileo: true # Galileo启用
|
||||
|
||||
# ============================================
|
||||
# SBAS配置
|
||||
# ============================================
|
||||
sbas: true # SBAS启用
|
||||
# ============================================
|
||||
# 诊断参数
|
||||
# ============================================
|
||||
# 是否启用详细诊断信息
|
||||
publish:
|
||||
nav:
|
||||
all: true
|
||||
rxm:
|
||||
all: false
|
||||
mon:
|
||||
all: false
|
||||
aid:
|
||||
all: false
|
||||
esf:
|
||||
all: false
|
||||
hnr:
|
||||
all: false
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# Configuration Settings for C94-M8P device
|
||||
ublox_gps_node:
|
||||
ros__parameters:
|
||||
debug: 0 # Range 0-4 (0 means no debug statements will print)
|
||||
device: /dev/ttyACM0
|
||||
frame_id: navsat_link
|
||||
uart1:
|
||||
baudrate: 9600
|
||||
# TMODE3 Config
|
||||
tmode3: 1 # Survey-In Mode
|
||||
sv_in:
|
||||
reset: True # True: disables and re-enables survey-in (resets)
|
||||
# False: Disables survey-in only if TMODE3 is
|
||||
# disabled
|
||||
min_dur: 300 # Survey-In Minimum Duration [s]
|
||||
acc_lim: 3.0 # Survey-In Accuracy Limit [m]
|
||||
|
||||
gnss:
|
||||
glonass: true
|
||||
beidou: true
|
||||
qzss: true
|
||||
|
||||
inf:
|
||||
all: true # Whether to display all INF messages in console
|
||||
|
||||
publish:
|
||||
all: false
|
||||
aid:
|
||||
hui: false
|
||||
nav:
|
||||
posecef: false
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
nmea_navsat_driver:
|
||||
ros__parameters:
|
||||
port: "/dev/ttyACM0"
|
||||
baud: 115200
|
||||
frame_id: "navsat_link"
|
||||
time_ref_source: "gps"
|
||||
useRMC: False
|
||||
rotation: 90.0
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
from launch import LaunchDescription
|
||||
from launch.substitutions import EnvironmentVariable
|
||||
import launch.actions
|
||||
import launch_ros.actions
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
import os
|
||||
from launch.actions import (DeclareLaunchArgument, GroupAction,
|
||||
IncludeLaunchDescription, SetEnvironmentVariable)
|
||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||
|
||||
def generate_launch_description():
|
||||
bringup_dir = get_package_share_directory('wheeltec_gps_driver')
|
||||
launch_dir = os.path.join(bringup_dir, 'launch')
|
||||
|
||||
wheeltec_gps = IncludeLaunchDescription(
|
||||
PythonLaunchDescriptionSource(os.path.join(launch_dir, 'wheeltec_nmea_driver.launch.py')),
|
||||
)
|
||||
|
||||
return LaunchDescription([
|
||||
wheeltec_gps,
|
||||
launch_ros.actions.Node(
|
||||
package='wheeltec_gps_driver', executable='gps_path', output='screen',
|
||||
remappings=[('/gps_topic','/gps/fix')]),
|
||||
|
||||
])
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
from launch import LaunchDescription
|
||||
from launch.substitutions import EnvironmentVariable
|
||||
import launch.actions
|
||||
import launch_ros.actions
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
import os
|
||||
from launch.actions import (DeclareLaunchArgument, GroupAction,
|
||||
IncludeLaunchDescription, SetEnvironmentVariable)
|
||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||
|
||||
def generate_launch_description():
|
||||
bringup_dir = get_package_share_directory('wheeltec_gps_driver')
|
||||
launch_dir = os.path.join(bringup_dir, 'launch')
|
||||
|
||||
wheeltec_gps = IncludeLaunchDescription(
|
||||
PythonLaunchDescriptionSource(os.path.join(launch_dir, 'wheeltec_dual_rtk_driver_nmea.launch.py')),
|
||||
)
|
||||
|
||||
return LaunchDescription([
|
||||
wheeltec_gps,
|
||||
launch_ros.actions.Node(
|
||||
package='wheeltec_gps_driver', executable='gps_path', output='screen',
|
||||
remappings=[('/gps_topic','/gps/fix')]),
|
||||
|
||||
])
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
from launch import LaunchDescription
|
||||
from launch.substitutions import EnvironmentVariable
|
||||
import launch.actions
|
||||
import launch_ros.actions
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
import os
|
||||
from launch.actions import (DeclareLaunchArgument, GroupAction,
|
||||
IncludeLaunchDescription, SetEnvironmentVariable)
|
||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||
|
||||
def generate_launch_description():
|
||||
bringup_dir = get_package_share_directory('wheeltec_gps_driver')
|
||||
launch_dir = os.path.join(bringup_dir, 'launch')
|
||||
|
||||
wheeltec_gps = IncludeLaunchDescription(
|
||||
PythonLaunchDescriptionSource(os.path.join(launch_dir, 'wheeltec_ublox_driver.launch.py')),
|
||||
)
|
||||
|
||||
return LaunchDescription([
|
||||
wheeltec_gps,
|
||||
launch_ros.actions.Node(
|
||||
package='wheeltec_gps_driver', executable='gps_path', output='screen',
|
||||
remappings=[('/gps_topic','/ublox_gps_node/fix')]),
|
||||
|
||||
])
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
from launch import LaunchDescription
|
||||
from launch.substitutions import EnvironmentVariable
|
||||
import launch.actions
|
||||
import launch_ros.actions
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
import os
|
||||
from launch.actions import (DeclareLaunchArgument, GroupAction,
|
||||
IncludeLaunchDescription, SetEnvironmentVariable)
|
||||
from launch.launch_description_sources import PythonLaunchDescriptionSource
|
||||
|
||||
def generate_launch_description():
|
||||
bringup_dir = get_package_share_directory('wheeltec_gps_driver')
|
||||
launch_dir = os.path.join(bringup_dir, 'launch')
|
||||
|
||||
wheeltec_gps = IncludeLaunchDescription(
|
||||
PythonLaunchDescriptionSource(os.path.join(launch_dir, 'wheeltec_dual_rtk_driver_unicore.launch.py')),
|
||||
)
|
||||
|
||||
return LaunchDescription([
|
||||
wheeltec_gps,
|
||||
launch_ros.actions.Node(
|
||||
package='wheeltec_gps_driver', executable='gps_path', output='screen',
|
||||
remappings=[('/gps_topic','/gps/fix')]),
|
||||
|
||||
])
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# Copyright 2018 Open Source Robotics Foundation, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
""" A simple launch file for the nmea_serial_driver node. """
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
from launch import LaunchDescription, LaunchIntrospector, LaunchService
|
||||
from launch_ros import actions
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
"""Generate a launch description for a single serial driver."""
|
||||
config_file = os.path.join(get_package_share_directory("wheeltec_gps_driver"), "config", "wheeltec_dual_rtk_nmea_serial_driver.yaml")
|
||||
driver_node = actions.Node(
|
||||
package='nmea_navsat_driver',
|
||||
executable='nmea_serial_driver',
|
||||
output='screen',
|
||||
parameters=[config_file])
|
||||
|
||||
return LaunchDescription([driver_node])
|
||||
|
||||
|
||||
def main(argv):
|
||||
ld = generate_launch_description()
|
||||
|
||||
print('Starting introspection of launch description...')
|
||||
print('')
|
||||
|
||||
print(LaunchIntrospector().format_launch_description(ld))
|
||||
|
||||
print('')
|
||||
print('Starting launch of launch description...')
|
||||
print('')
|
||||
|
||||
ls = LaunchService()
|
||||
ls.include_launch_description(ld)
|
||||
return ls.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
from launch import LaunchDescription
|
||||
from launch_ros.actions import Node
|
||||
from launch.actions import DeclareLaunchArgument
|
||||
from launch.substitutions import LaunchConfiguration
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
import os
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
"""Generate launch description for Wheeltec Dual RTK GNSS Driver."""
|
||||
# 获取参数文件路径
|
||||
pkg_share = get_package_share_directory('wheeltec_gps_driver')
|
||||
default_param_file = os.path.join(pkg_share, 'config', 'dual_rtk_driver.param.yaml')
|
||||
|
||||
# 声明启动参数
|
||||
param_file_arg = DeclareLaunchArgument(
|
||||
'param_file',
|
||||
default_value=default_param_file,
|
||||
description='Path to the parameter file'
|
||||
)
|
||||
|
||||
param_file = LaunchConfiguration('param_file')
|
||||
|
||||
dual_rtk_driver_node = Node(
|
||||
package='wheeltec_dual_rtk_driver',
|
||||
executable='dual_rtk_driver_node',
|
||||
name='dual_rtk_driver_node',
|
||||
parameters=[param_file],
|
||||
output='screen',
|
||||
)
|
||||
|
||||
return LaunchDescription([
|
||||
param_file_arg,
|
||||
dual_rtk_driver_node,
|
||||
])
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
# Copyright 2018 Open Source Robotics Foundation, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Launch file for the NMEA serial driver with YAML configuration."""
|
||||
|
||||
import os
|
||||
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
from launch import LaunchDescription
|
||||
from launch_ros import actions
|
||||
from launch.actions import DeclareLaunchArgument
|
||||
from launch.substitutions import LaunchConfiguration
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
"""Generate a launch description for NMEA serial driver."""
|
||||
# 获取参数文件路径
|
||||
pkg_share = get_package_share_directory("wheeltec_gps_driver")
|
||||
default_param_file = os.path.join(pkg_share, "config", "nmea_driver.param.yaml")
|
||||
|
||||
# 声明启动参数
|
||||
param_file_arg = DeclareLaunchArgument(
|
||||
'param_file',
|
||||
default_value=default_param_file,
|
||||
description='Path to the parameter file'
|
||||
)
|
||||
|
||||
param_file = LaunchConfiguration('param_file')
|
||||
|
||||
driver_node = actions.Node(
|
||||
package='nmea_navsat_driver',
|
||||
executable='nmea_serial_driver',
|
||||
name='nmea_serial_driver',
|
||||
output='screen',
|
||||
parameters=[param_file],
|
||||
)
|
||||
|
||||
return LaunchDescription([param_file_arg, driver_node])
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import os
|
||||
import ament_index_python.packages
|
||||
import launch
|
||||
import launch_ros.actions
|
||||
from launch.actions import DeclareLaunchArgument
|
||||
from launch.substitutions import LaunchConfiguration
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
"""Generate launch description for u-blox GPS driver."""
|
||||
# 获取参数文件路径
|
||||
pkg_share = ament_index_python.packages.get_package_share_directory('wheeltec_gps_driver')
|
||||
default_param_file = os.path.join(pkg_share, 'config', 'ublox_driver.param.yaml')
|
||||
|
||||
# 声明启动参数
|
||||
param_file_arg = DeclareLaunchArgument(
|
||||
'param_file',
|
||||
default_value=default_param_file,
|
||||
description='Path to the parameter file'
|
||||
)
|
||||
|
||||
param_file = LaunchConfiguration('param_file')
|
||||
|
||||
ublox_gps_node = launch_ros.actions.Node(
|
||||
package='ublox_gps',
|
||||
executable='ublox_gps_node',
|
||||
name='ublox_gps_node',
|
||||
output='both',
|
||||
parameters=[param_file],
|
||||
)
|
||||
|
||||
return launch.LaunchDescription([
|
||||
param_file_arg,
|
||||
ublox_gps_node,
|
||||
launch.actions.RegisterEventHandler(
|
||||
event_handler=launch.event_handlers.OnProcessExit(
|
||||
target_action=ublox_gps_node,
|
||||
on_exit=[launch.actions.EmitEvent(
|
||||
event=launch.events.Shutdown())],
|
||||
)),
|
||||
])
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
||||
<package format="3">
|
||||
<name>wheeltec_gps_driver</name>
|
||||
<version>0.0.0</version>
|
||||
<description>TODO: Package description</description>
|
||||
<maintainer email="wheeltec@todo.todo">wheeltec</maintainer>
|
||||
<license>Apache-2.0</license>
|
||||
|
||||
<buildtool_depend>ament_cmake</buildtool_depend>
|
||||
|
||||
<test_depend>ament_lint_auto</test_depend>
|
||||
<test_depend>ament_lint_common</test_depend>
|
||||
|
||||
<export>
|
||||
<build_type>ament_cmake</build_type>
|
||||
</export>
|
||||
</package>
|
||||
@@ -0,0 +1,160 @@
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include <sensor_msgs/msg/nav_sat_fix.hpp>
|
||||
#include <geometry_msgs/msg/pose_stamped.hpp>
|
||||
#include <nav_msgs/msg/path.hpp>
|
||||
|
||||
#include <tf2_ros/transform_broadcaster.h>
|
||||
#include <tf2_geometry_msgs/tf2_geometry_msgs.hpp>
|
||||
|
||||
#include <math.h>
|
||||
#define EARTH_RADIUS 6378.137
|
||||
using std::placeholders::_1;
|
||||
bool pose_init;
|
||||
|
||||
rclcpp::Node::SharedPtr nh_=nullptr;
|
||||
|
||||
class GpsPath : public rclcpp::Node
|
||||
{
|
||||
public:
|
||||
GpsPath()
|
||||
: Node("GpsPath")
|
||||
{
|
||||
state_pub_ = create_publisher<nav_msgs::msg::Path>("gps_path", 10);
|
||||
subscription_ = this->create_subscription<sensor_msgs::msg::NavSatFix>(
|
||||
"/gps_topic", 1, std::bind(&GpsPath::gps_callback, this, _1));
|
||||
}
|
||||
struct lla_pose
|
||||
{
|
||||
double latitude;
|
||||
double longitude;
|
||||
double altitude;
|
||||
};
|
||||
double rad(double d)
|
||||
{
|
||||
return d * 3.1415926 / 180.0;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
void publishTFFrames(const sensor_msgs::msg::NavSatFix::SharedPtr gps_msg_ptr, const geometry_msgs::msg::PoseStamped& current_position) {
|
||||
|
||||
//static tf2_ros::TransformBroadcaster tf_broadcaster;
|
||||
static std::shared_ptr<tf2_ros::TransformBroadcaster> tf_broadcaster;
|
||||
tf_broadcaster =std::make_shared<tf2_ros::TransformBroadcaster>(this);
|
||||
|
||||
|
||||
// 发布GPS轨迹原点的TF坐标
|
||||
geometry_msgs::msg::TransformStamped init_tf;
|
||||
init_tf.header.stamp = rclcpp::Time(gps_msg_ptr->header.stamp.sec, gps_msg_ptr->header.stamp.nanosec);
|
||||
init_tf.header.frame_id = "path"; // 假设GPS轨迹原点位于world坐标系
|
||||
init_tf.child_frame_id = "gps_origin";
|
||||
init_tf.transform.translation.x = 0.0;
|
||||
init_tf.transform.translation.y = 0.0;
|
||||
init_tf.transform.translation.z = 0.0;
|
||||
tf2::Quaternion q;
|
||||
q.setRPY(0, 0, 0);
|
||||
init_tf.transform.rotation = tf2::toMsg(q);
|
||||
tf_broadcaster->sendTransform(init_tf);
|
||||
|
||||
// 发布当前轨迹位置的TF坐标
|
||||
geometry_msgs::msg::TransformStamped current_tf;
|
||||
//current_tf.header.stamp = ros::Time::now();
|
||||
current_tf.header.stamp = rclcpp::Time(current_position.header.stamp.sec, current_position.header.stamp.nanosec);
|
||||
current_tf.header.frame_id = "path"; // 假设当前轨迹位置位于world坐标系
|
||||
current_tf.child_frame_id = "current_path_position";
|
||||
current_tf.transform.translation.x = current_position.pose.position.x;
|
||||
current_tf.transform.translation.y = current_position.pose.position.y;
|
||||
current_tf.transform.translation.z = current_position.pose.position.z;
|
||||
current_tf.transform.rotation = current_position.pose.orientation;
|
||||
q.setRPY(0, 0, 0); // 设置姿态为单位四元数
|
||||
current_tf.transform.rotation = tf2::toMsg(q);
|
||||
tf_broadcaster->sendTransform(current_tf);
|
||||
}
|
||||
|
||||
|
||||
void gps_callback(const sensor_msgs::msg::NavSatFix::SharedPtr gps_msg)
|
||||
{
|
||||
if(!pose_init)
|
||||
{
|
||||
init_pose.latitude = gps_msg->latitude;
|
||||
init_pose.longitude = gps_msg->longitude;
|
||||
init_pose.altitude = gps_msg->altitude;
|
||||
pose_init = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
//计算相对位置
|
||||
double radLat1 ,radLat2, radLong1,radLong2,delta_lat,delta_long,x,y;
|
||||
|
||||
radLat1 = rad(init_pose.latitude);
|
||||
radLong1 = rad(init_pose.longitude);
|
||||
|
||||
radLat2 = rad(gps_msg->latitude);
|
||||
radLong2 = rad(gps_msg->longitude);
|
||||
//计算x
|
||||
delta_lat = radLat2 - radLat1;
|
||||
delta_long = 0;
|
||||
|
||||
|
||||
if(delta_lat>0)
|
||||
x = -2*asin( sqrt( pow( sin( delta_lat/2 ),2) + cos( radLat1 )*cos( radLat2)*pow( sin( delta_long/2 ),2 ) ));
|
||||
else
|
||||
x = 2*asin( sqrt( pow( sin( delta_lat/2 ),2) + cos( radLat1 )*cos( radLat2)*pow( sin( delta_long/2 ),2 ) ));
|
||||
x = x*EARTH_RADIUS*1000;
|
||||
|
||||
//计算y
|
||||
delta_lat = 0;
|
||||
delta_long = radLong2 - radLong1;
|
||||
if(delta_long>0)
|
||||
y = 2*asin( sqrt( pow( sin( delta_lat/2 ),2) + cos( radLat2 )*cos( radLat2)*pow( sin( delta_long/2 ),2 ) ) );
|
||||
else
|
||||
y = -2*asin( sqrt( pow( sin( delta_lat/2 ),2) + cos( radLat2 )*cos( radLat2)*pow( sin( delta_long/2 ),2 ) ) );
|
||||
y = y*EARTH_RADIUS*1000;
|
||||
|
||||
//计算z
|
||||
double z = gps_msg->altitude - init_pose.altitude;
|
||||
// 更新当前位置
|
||||
geometry_msgs::msg::PoseStamped current_position;
|
||||
current_position.header.frame_id = "path";
|
||||
current_position.header.stamp = rclcpp::Node::now();
|
||||
current_position.pose.position.x = x;
|
||||
current_position.pose.position.y = y;
|
||||
current_position.pose.position.z = z;
|
||||
|
||||
//·发布轨迹
|
||||
ros_path_.header.frame_id = "path";
|
||||
ros_path_.header.stamp = rclcpp::Node::now();
|
||||
geometry_msgs::msg::PoseStamped pose;
|
||||
pose.header = ros_path_.header;
|
||||
pose.pose.position.x = x;
|
||||
pose.pose.position.y = y;
|
||||
pose.pose.position.z = z;
|
||||
ros_path_.poses.push_back(pose);
|
||||
|
||||
state_pub_->publish(ros_path_);
|
||||
|
||||
RCLCPP_INFO(this->get_logger(),"( x:%0.6f ,y:%0.6f ,z:%0.6f)",x ,y ,z );
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
nav_msgs::msg::Path ros_path_;
|
||||
lla_pose init_pose;
|
||||
rclcpp::Publisher<nav_msgs::msg::Path>::SharedPtr state_pub_;
|
||||
rclcpp::Subscription<sensor_msgs::msg::NavSatFix>::SharedPtr subscription_;
|
||||
};
|
||||
|
||||
int main(int argc, char * argv[])
|
||||
{
|
||||
rclcpp::init(argc, argv);
|
||||
pose_init = false;
|
||||
rclcpp::spin(std::make_shared<GpsPath>());
|
||||
rclcpp::shutdown();
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,187 @@
|
||||
# ROS2串口例程
|
||||
|
||||
本文档介绍如何在ROS2下来读取超核电子IMU&GNSS的数据,并提供了c++语言例程代码,通过执行ROS2命令,运行相应的节点,就可以看到打印到终端上的信息。
|
||||
|
||||
* 测试环境:Ubuntu20.04
|
||||
|
||||
* ROS版本:ROS2 Foxy
|
||||
|
||||
* 测试设备:超核电子IMU系列产品
|
||||
|
||||
## 安装USB-UART驱动
|
||||
|
||||
Ubuntu 系统自带CP210x的驱动,默认不需要安装串口驱动。将调试版连接到电脑上时,会自动识别设备。识别成功后,会在dev目录下出现一个对应的设备:ttyUSBx
|
||||
|
||||
检查USB-UART设备是否被Ubantu识别:
|
||||
|
||||
1. 打开终端,输入`ls /dev`,先查看已经存在的串口设备。
|
||||
2. 查看是否已经存在 ttyUSBx 这个设备文件,便于确认对应的端口号。
|
||||
4. 接下来插入USB线,连接调试板,然后再次执行`ls /dev`。 dev目录下多了一个设备`ttyUSB0`:
|
||||
|
||||
```shell
|
||||
linux@ubuntu:~$ ls /dev
|
||||
.....
|
||||
hpet net tty11 tty4 ttyS0 ttyUSB0 vhost-vsock
|
||||
hugepages null tty12 tty40 ttyS1 udmabuf vmci
|
||||
......
|
||||
```
|
||||
|
||||
4.打开USB设备的可执行权限:
|
||||
|
||||
```shell
|
||||
$ sudo chmod 777 /dev/ttyUSB0
|
||||
```
|
||||
|
||||
## 编译hipnuc_ws工作空间
|
||||
|
||||
1. 打开终端进入/examples/ROS2/hipnuc_ws 目录
|
||||
2. 执行`colcon build`命令,编译成功后出现如下信息。
|
||||
|
||||
```shell
|
||||
linux@ubuntu20:~/hipnuc_ws$ colcon build
|
||||
Starting >>> hipnuc_gnss
|
||||
Starting >>> hipnuc_imu
|
||||
Finished <<< hipnuc_imu [0.44s]
|
||||
Finished <<< hipnuc_gnss [0.49s]
|
||||
|
||||
Summary: 2 packages finished [0.61s]
|
||||
linux@ubuntu20:~/hipnuc_ws$
|
||||
```
|
||||
|
||||
## 修改串口波特率和设备号
|
||||
|
||||
1. 在Ubuntu环境中,支持的波特率为115200, 460800, 921600。本例程使用的默认波特率是115200,默认打开的串口名称是/dev/ttyUSB0。
|
||||
|
||||
2. 如果您需要更高的输出频率,请修改`config/hipnuc_config.yaml`文件中的配置参数。
|
||||
|
||||
```c
|
||||
#hipnuc_imu config file
|
||||
IMU_publisher:
|
||||
ros__parameters:
|
||||
serial_port: "/dev/ttyUSB0"
|
||||
baud_rate: 115200
|
||||
frame_id: "base_link"
|
||||
imu_topic: "/IMU_data"
|
||||
|
||||
|
||||
#hipnuc_gnss config file
|
||||
INS_publisher:
|
||||
ros__parameters:
|
||||
serial_port: "/dev/ttyUSB0"
|
||||
baud_rate: 115200
|
||||
frame_id: "gnss_link"
|
||||
imu_topic: "/rawimu_data"
|
||||
nav_topic: "/NavSatFix_data"
|
||||
```
|
||||
|
||||
注意修改后需要回到hipnuc_ws目录下,重新执行`colcon build`命令
|
||||
|
||||
## 显示数据
|
||||
查看数据方式:
|
||||
|
||||
1、输出ROS 定义的sensor_msgs::Imu。
|
||||
|
||||
2、输出ROS 定义的sensor_msgs::NavSatFix
|
||||
|
||||
### 输出ROS标准 Imu.msg
|
||||
|
||||
1. 打开终端,执行:
|
||||
|
||||
```shell
|
||||
linux@ubuntu20:~$ ros2 launch hipnuc_imu imu_spec_msg.launch.py
|
||||
```
|
||||
|
||||
2.如果执行失败,提示找不到相应的launch文件,则需要配置环境,在当前终端执行:
|
||||
|
||||
```shell
|
||||
linux@ubuntu:~$source <hipnuc_ws_dir>/install/setup.bash
|
||||
```
|
||||
|
||||
3.执行成功后,就可以看到所有的信息:
|
||||
|
||||
```c
|
||||
[listener-2] ---
|
||||
[listener-2] header:
|
||||
[listener-2] stamp:
|
||||
[listener-2] secs:1639099575
|
||||
[listener-2] nanosecs:538349240
|
||||
[listener-2] frame_id:base_link
|
||||
[listener-2] orientation:
|
||||
[listener-2] x: -0.095125280320644379
|
||||
[listener-2] y: -0.483648955821990967
|
||||
[listener-2] z: 0.053129896521568298
|
||||
[listener-2] w: 0.868453860282897949
|
||||
[listener-2] orientation_covariance: [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||||
[listener-2] angular_velocity:
|
||||
[listener-2] x: -0.000815955184543841
|
||||
[listener-2] y: -0.001057390143056437
|
||||
[listener-2] z: 0.001062464062371403
|
||||
[listener-2] angular_velocity_covariance: [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||||
[listener-2] linear_acceleration:
|
||||
[listener-2] x: 8.110355603694916482
|
||||
[listener-2] y: -2.125157430768013000
|
||||
[listener-2] z: 5.013053989410400924
|
||||
[listener-2] linear_acceleration_covariance: [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||||
[listener-2] ---
|
||||
```
|
||||
|
||||
4、另开一个终端窗口,执行`ros2 topic hz /Imu_data`,可以查看话题发布的频率。
|
||||
|
||||
```shell
|
||||
linux@ubuntu20:~$ ros2 topic hz /Imu_data
|
||||
average rate: 100.032
|
||||
min: 0.008s max: 0.012s std dev: 0.00058s window: 102
|
||||
average rate: 100.014
|
||||
min: 0.008s max: 0.012s std dev: 0.00054s window: 202
|
||||
average rate: 100.019
|
||||
min: 0.007s max: 0.013s std dev: 0.00064s window: 303
|
||||
^C
|
||||
linux@ubuntu20:~$
|
||||
```
|
||||
|
||||
### 输出ROS标准的NavSatFix.msg
|
||||
|
||||
1、打开终端,执行:
|
||||
|
||||
```shell
|
||||
linux@ubuntu20:~$ ros2 launch hipnuc_gnss nav_spec_msg.launch.py
|
||||
```
|
||||
|
||||
2、如果执行失败,提示找不到相应的launch文件,则需要配置环境,在当前终端执行:
|
||||
|
||||
```shell
|
||||
linux@ubuntu:~$source <hipnuc_ws_dir>/install/setup.bash
|
||||
```
|
||||
|
||||
3、执行成功后,可以看到如下信息:
|
||||
|
||||
```shell
|
||||
[listener_INS-2] header:
|
||||
[listener_INS-2] stamp:
|
||||
[listener_INS-2] secs: 1724034005
|
||||
[listener_INS-2] nanosecs: 370900173
|
||||
[listener_INS-2] frame_id: gnss_link
|
||||
[listener_INS-2] status:
|
||||
[listener_INS-2] status: 1
|
||||
[listener_INS-2] service: 0
|
||||
[listener_INS-2] latitude: 40.20336080
|
||||
[listener_INS-2] longitude: 116.24086010
|
||||
[listener_INS-2] altitude: 66.30100000
|
||||
[listener_INS-2] orientation_covariance: [ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||||
[listener_INS-2] position_covariance_type: 0
|
||||
```
|
||||
|
||||
4、另开一个终端窗口,执行`ros2 topic hz /NavSatFix_data`,可以查看话题发布的频率。
|
||||
|
||||
```shell
|
||||
linux@ubuntu20:~$ ros2 topic hz /NavSatFix_data`
|
||||
average rate: 10.032
|
||||
min: 0.008s max: 0.012s std dev: 0.00058s window: 10
|
||||
average rate: 10.014
|
||||
min: 0.008s max: 0.012s std dev: 0.00054s window: 20
|
||||
average rate: 10.019
|
||||
min: 0.007s max: 0.013s std dev: 0.00064s window: 30
|
||||
^C
|
||||
linux@ubuntu20:~$
|
||||
```
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
project(hipnuc_imu)
|
||||
|
||||
# Default to C99
|
||||
if(NOT CMAKE_C_STANDARD)
|
||||
set(CMAKE_C_STANDARD 99)
|
||||
endif()
|
||||
|
||||
# Default to C++14
|
||||
if(NOT CMAKE_CXX_STANDARD)
|
||||
set(CMAKE_CXX_STANDARD 14)
|
||||
endif()
|
||||
|
||||
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
add_compile_options(-Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
|
||||
# find dependencies
|
||||
find_package(ament_cmake REQUIRED)
|
||||
find_package(rclcpp REQUIRED)
|
||||
find_package(std_msgs REQUIRED)
|
||||
find_package(sensor_msgs REQUIRED)
|
||||
find_package(driver_timestamp_sync REQUIRED)
|
||||
|
||||
# uncomment the following section in order to fill in
|
||||
# further dependencies manually.
|
||||
# find_package(<dependency> REQUIRED)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
find_package(ament_lint_auto REQUIRED)
|
||||
# the following line skips the linter which checks for copyrights
|
||||
# uncomment the line when a copyright and license is not present in all source files
|
||||
#set(ament_cmake_copyright_FOUND TRUE)
|
||||
# the following line skips cpplint (only works in a git repo)
|
||||
# uncomment the line when this package is not in a git repo
|
||||
#set(ament_cmake_cpplint_FOUND TRUE)
|
||||
ament_lint_auto_find_test_dependencies()
|
||||
endif()
|
||||
|
||||
add_executable(talker src/serial_port.cpp src/hipnuc_dec.c)
|
||||
ament_target_dependencies(talker rclcpp std_msgs sensor_msgs driver_timestamp_sync)
|
||||
|
||||
add_executable(listener src/sub_spec.cpp)
|
||||
ament_target_dependencies(listener rclcpp std_msgs sensor_msgs)
|
||||
|
||||
install(TARGETS talker listener DESTINATION lib/${PROJECT_NAME})
|
||||
install(DIRECTORY launch DESTINATION share/${PROJECT_NAME})
|
||||
install(DIRECTORY
|
||||
config/
|
||||
DESTINATION share/${PROJECT_NAME}/config
|
||||
)
|
||||
|
||||
ament_package()
|
||||
@@ -0,0 +1,13 @@
|
||||
IMU_publisher:
|
||||
ros__parameters:
|
||||
serial_port: "/dev/ttyUSB0"
|
||||
baud_rate: 115200
|
||||
frame_id: "imu_link"
|
||||
imu_topic: "/sensing/imu/hipnuc/imu_raw"
|
||||
use_timestamp_sync: false
|
||||
time_offset_sec: 0.0
|
||||
bias_alpha: 0.01
|
||||
jump_warn_threshold_ms: 1000
|
||||
clamp_backward_time: true
|
||||
timestamp_diagnostics: false
|
||||
timestamp_diagnostics_throttle_ms: 2000
|
||||
@@ -0,0 +1,28 @@
|
||||
##launch file
|
||||
from launch import LaunchDescription
|
||||
from launch_ros.actions import Node
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
import os
|
||||
|
||||
def generate_launch_description():
|
||||
config = os.path.join(
|
||||
get_package_share_directory('hipnuc_imu'),
|
||||
'config',
|
||||
'hipnuc_config.yaml',
|
||||
),
|
||||
|
||||
return LaunchDescription([
|
||||
Node(
|
||||
package='hipnuc_imu',
|
||||
executable='talker',
|
||||
name='IMU_publisher',
|
||||
parameters=[config],
|
||||
output='screen',
|
||||
),
|
||||
Node(
|
||||
package='hipnuc_imu',
|
||||
executable='listener',
|
||||
output='screen'
|
||||
),
|
||||
])
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
||||
<package format="3">
|
||||
<name>hipnuc_imu</name>
|
||||
<version>0.0.0</version>
|
||||
<description>hipnuc_imu_data </description>
|
||||
<maintainer email="linux@todo.todo">linux</maintainer>
|
||||
<license>Apache License 2.0 </license>
|
||||
|
||||
<buildtool_depend>ament_cmake</buildtool_depend>
|
||||
|
||||
<depend>rclcpp</depend>
|
||||
<depend>std_msgs</depend>
|
||||
<depend>sensor_msgs</depend>
|
||||
<depend>driver_timestamp_sync</depend>
|
||||
|
||||
<test_depend>ament_lint_auto</test_depend>
|
||||
<test_depend>ament_lint_common</test_depend>
|
||||
|
||||
<export>
|
||||
<build_type>ament_cmake</build_type>
|
||||
</export>
|
||||
</package>
|
||||
@@ -0,0 +1,136 @@
|
||||
#ifndef __HIPNUC_H__
|
||||
#define __HIPNUC_H__
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"{
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
#ifdef QT_CORE_LIB
|
||||
#pragma pack(push)
|
||||
#pragma pack(1)
|
||||
#endif
|
||||
|
||||
#define CHSYNC1 (0x5A) /* CHAOHE message sync code 1 */
|
||||
#define CHSYNC2 (0xA5) /* CHAOHE message sync code 2 */
|
||||
#define CH_HDR_SIZE (0x06) /* CHAOHE protocol header size */
|
||||
#define HIPNUC_MAX_RAW_SIZE (128+CH_HDR_SIZE)
|
||||
|
||||
|
||||
/**
|
||||
* packet 0x91: IMU data(floating point)
|
||||
*/
|
||||
typedef struct __attribute__((__packed__))
|
||||
{
|
||||
uint8_t tag; /* data packet tag, if tag = 0x00, means that this packet is null */
|
||||
uint16_t pps_sync_ms;
|
||||
int8_t temp;
|
||||
float prs;
|
||||
uint32_t ts; /* timestamp */
|
||||
float acc[3];
|
||||
float gyr[3];
|
||||
float mag[3];
|
||||
float roll;
|
||||
float pitch;
|
||||
float yaw;
|
||||
float quat[4];
|
||||
} hi91_t;
|
||||
|
||||
/**
|
||||
* packet 0x92: IMU data(int type)
|
||||
*/
|
||||
typedef struct __attribute__((__packed__))
|
||||
{
|
||||
uint8_t tag;
|
||||
uint16_t status;
|
||||
int8_t temperature;
|
||||
uint16_t sync_time;
|
||||
int16_t air_pressure;
|
||||
int16_t reserved;
|
||||
int16_t gyr_b[3];
|
||||
int16_t acc_b[3];
|
||||
int16_t mag_b[3];
|
||||
int32_t roll;
|
||||
int32_t pitch;
|
||||
int32_t yaw;
|
||||
int16_t quat[4];
|
||||
} hi92_t;
|
||||
|
||||
/**
|
||||
* packet 0x81: INS data, include lat, lon, eul, quat, raw IMU data
|
||||
*/
|
||||
typedef struct __attribute__((__packed__))
|
||||
{
|
||||
uint8_t tag;
|
||||
uint16_t status;
|
||||
uint8_t ins_status;
|
||||
uint16_t gpst_wn;
|
||||
uint32_t gpst_tow;
|
||||
uint16_t sync_time;
|
||||
int16_t gyr_b[3];
|
||||
int16_t acc_b[3];
|
||||
int16_t mag_b[3];
|
||||
int16_t air_pressure;
|
||||
int16_t reserved1;
|
||||
int8_t temperature;
|
||||
uint8_t utc_year;
|
||||
uint8_t utc_mouth;
|
||||
uint8_t utc_day;
|
||||
uint8_t utc_hour;
|
||||
uint8_t utc_min;
|
||||
uint16_t utc_msec;
|
||||
int16_t roll;
|
||||
int16_t pitch;
|
||||
uint16_t yaw;
|
||||
int16_t quat[4];
|
||||
int32_t ins_lon;
|
||||
int32_t ins_lat;
|
||||
int32_t ins_msl;
|
||||
uint8_t pdop;
|
||||
uint8_t hdop;
|
||||
uint8_t solq_pos;
|
||||
uint8_t nv_pos;
|
||||
uint8_t solq_heading;
|
||||
uint8_t nv_heading;
|
||||
uint8_t diff_age;
|
||||
int16_t undulation;
|
||||
uint8_t reserved;
|
||||
int16_t vel_enu[3];
|
||||
int16_t acc_enu[3];
|
||||
int32_t gnss_lon;
|
||||
int32_t gnss_lat;
|
||||
int32_t gnss_msl;
|
||||
uint8_t reserved2[2];
|
||||
} hi81_t;
|
||||
|
||||
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int nbyte; /* number of bytes in message buffer */
|
||||
int len; /* message length (bytes) */
|
||||
uint8_t buf[HIPNUC_MAX_RAW_SIZE]; /* message raw buffer */
|
||||
hi91_t hi91;
|
||||
hi92_t hi92;
|
||||
hi81_t hi81;
|
||||
}hipnuc_raw_t;
|
||||
|
||||
#ifdef QT_CORE_LIB
|
||||
#pragma pack(pop)
|
||||
#endif
|
||||
|
||||
int hipnuc_input(hipnuc_raw_t *raw, uint8_t data);
|
||||
int hipnuc_dump_packet(hipnuc_raw_t *raw, char *buf, size_t buf_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
#include "hipnuc.h"
|
||||
|
||||
|
||||
/* legcy support of HI226/HI229 */
|
||||
#define HIPNUC_ID_USRID (0x90)
|
||||
#define HIPNUC_ID_ACC_RAW (0xA0)
|
||||
#define HIPNUC_ID_ACC_CAL (0xA1)
|
||||
#define HIPNUC_ID_GYR_RAW (0xB0)
|
||||
#define HIPNUC_ID_GYR_CAL (0xB1)
|
||||
#define HIPNUC_ID_MAG_RAW (0xC0)
|
||||
#define HIPNUC_ID_EUL (0xD0)
|
||||
#define HIPNUC_ID_QUAT (0xD1)
|
||||
#define HIPNUC_ID_PRS (0xF0)
|
||||
|
||||
/* new HiPNUC standard packet */
|
||||
#define HIPNUC_ID_IMUSOL (0x91)
|
||||
#define HIPNUC_ID_IMUBIN (0x92)
|
||||
#define HIPNUC_ID_INSSOL (0x81)
|
||||
|
||||
#ifndef D2R
|
||||
#define D2R (0.0174532925199433F)
|
||||
#endif
|
||||
|
||||
#ifndef R2D
|
||||
#define R2D (57.2957795130823F)
|
||||
#endif
|
||||
|
||||
#ifndef GRAVITY
|
||||
#define GRAVITY (9.8F)
|
||||
#endif
|
||||
|
||||
|
||||
static void hipnuc_crc16(uint16_t *inital, const uint8_t *buf, uint32_t len);
|
||||
|
||||
/* common type conversion */
|
||||
#define I2(p) (*((int16_t *)(p)))
|
||||
static uint16_t U2(uint8_t *p)
|
||||
{
|
||||
uint16_t u;
|
||||
memcpy(&u, p, 2);
|
||||
return u;
|
||||
}
|
||||
|
||||
static float R4(uint8_t *p)
|
||||
{
|
||||
float r;
|
||||
memcpy(&r, p, 4);
|
||||
return r;
|
||||
}
|
||||
|
||||
/* parse the payload of a frame and feed into data section */
|
||||
static int parse_data(hipnuc_raw_t *raw)
|
||||
{
|
||||
int ofs = 0;
|
||||
uint8_t *p = &raw->buf[CH_HDR_SIZE];
|
||||
|
||||
/* ignore all previous data */
|
||||
raw->hi91.tag = 0;
|
||||
raw->hi81.tag = 0;
|
||||
raw->hi92.tag = 0;
|
||||
|
||||
while (ofs < raw->len)
|
||||
{
|
||||
switch (p[ofs])
|
||||
{
|
||||
case HIPNUC_ID_USRID:
|
||||
ofs += 2;
|
||||
break;
|
||||
case HIPNUC_ID_ACC_RAW:
|
||||
case HIPNUC_ID_ACC_CAL:
|
||||
raw->hi91.tag = HIPNUC_ID_IMUSOL;
|
||||
raw->hi91.acc[0] = (float)I2(p + ofs + 1) / 1000;
|
||||
raw->hi91.acc[1] = (float)I2(p + ofs + 3) / 1000;
|
||||
raw->hi91.acc[2] = (float)I2(p + ofs + 5) / 1000;
|
||||
ofs += 7;
|
||||
break;
|
||||
case HIPNUC_ID_GYR_RAW:
|
||||
case HIPNUC_ID_GYR_CAL:
|
||||
raw->hi91.tag = HIPNUC_ID_IMUSOL;
|
||||
raw->hi91.gyr[0] = (float)I2(p + ofs + 1) / 10;
|
||||
raw->hi91.gyr[1] = (float)I2(p + ofs + 3) / 10;
|
||||
raw->hi91.gyr[2] = (float)I2(p + ofs + 5) / 10;
|
||||
ofs += 7;
|
||||
break;
|
||||
case HIPNUC_ID_MAG_RAW:
|
||||
raw->hi91.tag = HIPNUC_ID_IMUSOL;
|
||||
raw->hi91.mag[0] = (float)I2(p + ofs + 1) / 10;
|
||||
raw->hi91.mag[1] = (float)I2(p + ofs + 3) / 10;
|
||||
raw->hi91.mag[2] = (float)I2(p + ofs + 5) / 10;
|
||||
ofs += 7;
|
||||
break;
|
||||
case HIPNUC_ID_EUL:
|
||||
raw->hi91.tag = HIPNUC_ID_IMUSOL;
|
||||
raw->hi91.pitch = (float)I2(p + ofs + 1) / 100;
|
||||
raw->hi91.roll = (float)I2(p + ofs + 3) / 100;
|
||||
raw->hi91.yaw = (float)I2(p + ofs + 5) / 10;
|
||||
ofs += 7;
|
||||
break;
|
||||
case HIPNUC_ID_QUAT:
|
||||
raw->hi91.tag = HIPNUC_ID_IMUSOL;
|
||||
raw->hi91.quat[0] = R4(p + ofs + 1);
|
||||
raw->hi91.quat[1] = R4(p + ofs + 5);
|
||||
raw->hi91.quat[2] = R4(p + ofs + 9);
|
||||
raw->hi91.quat[3] = R4(p + ofs + 13);
|
||||
ofs += 17;
|
||||
break;
|
||||
case HIPNUC_ID_PRS:
|
||||
raw->hi91.tag = HIPNUC_ID_IMUSOL;
|
||||
raw->hi91.prs = R4(p + ofs + 1);
|
||||
ofs += 5;
|
||||
break;
|
||||
case HIPNUC_ID_IMUSOL:
|
||||
memcpy(&raw->hi91, p + ofs, sizeof(hi91_t));
|
||||
ofs += sizeof(hi91_t);
|
||||
break;
|
||||
case HIPNUC_ID_INSSOL:
|
||||
memcpy(&raw->hi81, p + ofs, sizeof(hi81_t));
|
||||
ofs += sizeof(hi81_t);
|
||||
break;
|
||||
case HIPNUC_ID_IMUBIN:
|
||||
memcpy(&raw->hi92, p + ofs, sizeof(hi92_t));
|
||||
ofs += sizeof(hi92_t);
|
||||
break;
|
||||
default:
|
||||
ofs++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int decode_hipnuc(hipnuc_raw_t *raw)
|
||||
{
|
||||
uint16_t crc = 0;
|
||||
|
||||
/* checksum */
|
||||
hipnuc_crc16(&crc, raw->buf, (CH_HDR_SIZE-2));
|
||||
hipnuc_crc16(&crc, raw->buf + CH_HDR_SIZE, raw->len);
|
||||
if (crc != U2(raw->buf + (CH_HDR_SIZE-2)))
|
||||
{
|
||||
// NL_TRACE("ch checksum error: frame:0x%X calcuate:0x%X, len:%d\n", U2(raw->buf + 4), crc, raw->len);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return parse_data(raw);
|
||||
}
|
||||
|
||||
/* sync code */
|
||||
static int sync_hipnuc(uint8_t *buf, uint8_t data)
|
||||
{
|
||||
buf[0] = buf[1];
|
||||
buf[1] = data;
|
||||
return buf[0] == CHSYNC1 && buf[1] == CHSYNC2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief hipnuc decoder input, read one byte at one time.
|
||||
*
|
||||
* @param raw is the decoder struct.
|
||||
* @param data is the one byte read from stram.
|
||||
* @param buf is the log string buffer, ireturn > 0: decoder received a frame successfully, else: receiver not receive a frame successfully.
|
||||
*
|
||||
*/
|
||||
int hipnuc_input(hipnuc_raw_t *raw, uint8_t data)
|
||||
{
|
||||
/* synchronize frame */
|
||||
if (raw->nbyte == 0)
|
||||
{
|
||||
if (!sync_hipnuc(raw->buf, data))
|
||||
return 0;
|
||||
raw->nbyte = 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
raw->buf[raw->nbyte++] = data;
|
||||
|
||||
if (raw->nbyte == CH_HDR_SIZE)
|
||||
{
|
||||
if ((raw->len = U2(raw->buf + 2)) > (HIPNUC_MAX_RAW_SIZE - CH_HDR_SIZE))
|
||||
{
|
||||
// NL_TRACE("ch length error: len=%d\n",raw->len);
|
||||
raw->nbyte = 0;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (raw->nbyte < CH_HDR_SIZE || raw->nbyte < (raw->len + CH_HDR_SIZE))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
raw->nbyte = 0;
|
||||
|
||||
return decode_hipnuc(raw);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief convert packet to string, only dump parts of data
|
||||
*
|
||||
* @param raw is struct of decoder
|
||||
* @param buf is the log string buffer, make sure buf larger than 256
|
||||
* @param buf_size is the szie of the log buffer
|
||||
*
|
||||
*/
|
||||
int hipnuc_dump_packet(hipnuc_raw_t *raw, char *buf, size_t buf_size)
|
||||
{
|
||||
int written = 0;
|
||||
int ret;
|
||||
|
||||
/* dump 0x91 packet */
|
||||
if(raw->hi91.tag == HIPNUC_ID_IMUSOL)
|
||||
{
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s0x%X\r\n", "tag:", raw->hi91.tag);
|
||||
if (ret > 0) written += ret;
|
||||
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s%d\r\n", "sync_time(ms):", raw->hi91.pps_sync_ms);
|
||||
if (ret > 0) written += ret;
|
||||
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s%.3f %.3f %.3f\r\n", "acc(m/s^(2)):", raw->hi91.acc[0]*GRAVITY, raw->hi91.acc[1]*GRAVITY, raw->hi91.acc[2]*GRAVITY);
|
||||
if (ret > 0) written += ret;
|
||||
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s%.3f %.3f %.3f\r\n", "gyr(deg/s):", raw->hi91.gyr[0], raw->hi91.gyr[1], raw->hi91.gyr[2]);
|
||||
if (ret > 0) written += ret;
|
||||
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s%.3f %.3f %.3f\r\n", "mag(uT):", raw->hi91.mag[0], raw->hi91.mag[1], raw->hi91.mag[2]);
|
||||
if (ret > 0) written += ret;
|
||||
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s%.3f %.3f %.3f\r\n", "Roll/Pitch/Yaw(deg):", raw->hi91.roll, raw->hi91.pitch, raw->hi91.yaw);
|
||||
if (ret > 0) written += ret;
|
||||
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s%d\r\n", "timestamp(ms):", raw->hi91.ts);
|
||||
if (ret > 0) written += ret;
|
||||
}
|
||||
|
||||
/* dump 0x92 packet */
|
||||
if(raw->hi92.tag == HIPNUC_ID_IMUBIN)
|
||||
{
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s0x%X\r\n", "tag:", raw->hi92.tag);
|
||||
if (ret > 0) written += ret;
|
||||
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s%d\r\n", "temperature", raw->hi92.temperature);
|
||||
if (ret > 0) written += ret;
|
||||
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s%d\r\n", "sync_time(ms):", raw->hi92.sync_time);
|
||||
if (ret > 0) written += ret;
|
||||
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s%.3f %.3f %.3f\r\n", "acc(m/s^(2)):", raw->hi92.acc_b[0]*0.0048828, raw->hi92.acc_b[1]*0.0048828, raw->hi92.acc_b[2]*0.0048828);
|
||||
if (ret > 0) written += ret;
|
||||
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s%.3f %.3f %.3f\r\n", "gyr(deg/s):", raw->hi92.gyr_b[0]*(0.001*R2D), raw->hi92.gyr_b[1]*(0.001*R2D), raw->hi92.gyr_b[2]*(0.001*R2D));
|
||||
if (ret > 0) written += ret;
|
||||
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s%.3f %.3f %.3f\r\n", "mag(uT):", raw->hi92.mag_b[0]*0.030517, raw->hi92.mag_b[1]*0.030517, raw->hi92.mag_b[2]*0.030517);
|
||||
if (ret > 0) written += ret;
|
||||
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s%.3f %.3f %.3f\r\n", "Roll/Pitch/Yaw(deg):", raw->hi92.roll*0.001, raw->hi92.pitch*0.001, raw->hi92.yaw*0.001);
|
||||
if (ret > 0) written += ret;
|
||||
}
|
||||
|
||||
/* dump 0x81 packet */
|
||||
if(raw->hi81.tag == HIPNUC_ID_INSSOL)
|
||||
{
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s0x%X\r\n", "tag:", raw->hi81.tag);
|
||||
if (ret > 0) written += ret;
|
||||
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s%d\r\n", "solq_pos:", raw->hi81.solq_pos);
|
||||
if (ret > 0) written += ret;
|
||||
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s%d\r\n", "sat number:", raw->hi81.nv_pos);
|
||||
if (ret > 0) written += ret;
|
||||
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s%.7f %.7f\r\n", "Lat/Lon(deg):", raw->hi81.ins_lat*1e-7, raw->hi81.ins_lon*1e-7);
|
||||
if (ret > 0) written += ret;
|
||||
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s%.3f\r\n", "height(m):", raw->hi81.ins_msl*1e-3);
|
||||
if (ret > 0) written += ret;
|
||||
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s%.3f %.3f %.3f\r\n", "acc(m/s^(2)):", raw->hi81.acc_b[0]*0.0048828, raw->hi81.acc_b[1]*0.0048828, raw->hi81.acc_b[2]*0.0048828);
|
||||
if (ret > 0) written += ret;
|
||||
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s%.3f %.3f %.3f\r\n", "gyr(deg/s):", raw->hi81.gyr_b[0]*(0.001*R2D), raw->hi81.gyr_b[1]*(0.001*R2D), raw->hi81.gyr_b[2]*(0.001*R2D));
|
||||
if (ret > 0) written += ret;
|
||||
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s%.3f %.3f %.3f\r\n", "mag(uT):", raw->hi81.mag_b[0]*0.030517, raw->hi81.mag_b[1]*0.030517, raw->hi81.mag_b[2]*0.030517);
|
||||
if (ret > 0) written += ret;
|
||||
|
||||
ret = snprintf(buf + written, buf_size - written, "%-16s%.3f %.3f %.3f\r\n", "Roll/Pitch/Yaw(deg):", raw->hi81.roll*0.01, raw->hi81.pitch*0.01, raw->hi81.yaw*0.01);
|
||||
if (ret > 0) written += ret;
|
||||
}
|
||||
|
||||
return written;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief calcuate hipnuc_crc16
|
||||
*
|
||||
* @param inital is intial value
|
||||
* @param buf is input buffer pointer
|
||||
* @param len is length of the buffer
|
||||
*
|
||||
*/
|
||||
static void hipnuc_crc16(uint16_t *inital, const uint8_t *buf, uint32_t len)
|
||||
{
|
||||
uint32_t crc = *inital;
|
||||
uint32_t j;
|
||||
for (j=0; j < len; ++j)
|
||||
{
|
||||
uint32_t i;
|
||||
uint32_t byte = buf[j];
|
||||
crc ^= byte << 8;
|
||||
for (i = 0; i < 8; ++i)
|
||||
{
|
||||
uint32_t temp = crc << 1;
|
||||
if (crc & 0x8000)
|
||||
{
|
||||
temp ^= 0x1021;
|
||||
}
|
||||
crc = temp;
|
||||
}
|
||||
}
|
||||
*inital = crc;
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
#include <iostream>
|
||||
#include <sensor_msgs/msg/imu.hpp>
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <termios.h>
|
||||
|
||||
#include "driver_timestamp_sync/timestamp_sync.hpp"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"{
|
||||
#endif
|
||||
#include <poll.h>
|
||||
#include "hipnuc.h"
|
||||
|
||||
#define GRA_ACC (9.8)
|
||||
#define DEG_TO_RAD (0.01745329)
|
||||
#define BUF_SIZE (1024)
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using namespace std;
|
||||
static hipnuc_raw_t raw;
|
||||
|
||||
class IMUPublisher : public rclcpp::Node
|
||||
{
|
||||
public:
|
||||
int fd = 0;
|
||||
uint8_t buf[BUF_SIZE] = {0};
|
||||
IMUPublisher() : Node("IMU_publisher")
|
||||
{
|
||||
this->declare_parameter<std::string>("serial_port", "/dev/ttyUSB1");
|
||||
this->declare_parameter<int>("baud_rate", 460800);
|
||||
this->declare_parameter<std::string>("frame_id", "base_link");
|
||||
this->declare_parameter<std::string>("imu_topic", "/IMU_data");
|
||||
this->declare_parameter<bool>("use_timestamp_sync", true);
|
||||
this->declare_parameter<double>("time_offset_sec", 0.0);
|
||||
this->declare_parameter<double>("bias_alpha", 0.01);
|
||||
this->declare_parameter<int>("jump_warn_threshold_ms", 1000);
|
||||
this->declare_parameter<bool>("clamp_backward_time", true);
|
||||
this->declare_parameter<bool>("timestamp_diagnostics", false);
|
||||
this->declare_parameter<int>("timestamp_diagnostics_throttle_ms", 2000);
|
||||
|
||||
this->get_parameter("serial_port", serial_port);
|
||||
this->get_parameter("baud_rate", baud_rate);
|
||||
this->get_parameter("frame_id", frame_id);
|
||||
this->get_parameter("imu_topic", imu_topic);
|
||||
|
||||
use_timestamp_sync_ = this->get_parameter("use_timestamp_sync").as_bool();
|
||||
|
||||
driver_timestamp_sync::TimestampSyncConfig config;
|
||||
config.time_offset_sec = this->get_parameter("time_offset_sec").as_double();
|
||||
config.bias_alpha = this->get_parameter("bias_alpha").as_double();
|
||||
config.jump_warn_threshold_ms = static_cast<uint32_t>(
|
||||
this->get_parameter("jump_warn_threshold_ms").as_int());
|
||||
config.clamp_backward_time = this->get_parameter("clamp_backward_time").as_bool();
|
||||
timestamp_sync_.set_config(config);
|
||||
timestamp_diagnostics_ = this->get_parameter("timestamp_diagnostics").as_bool();
|
||||
timestamp_diagnostics_throttle_ms_ = this->get_parameter(
|
||||
"timestamp_diagnostics_throttle_ms").as_int();
|
||||
|
||||
RCLCPP_INFO(this->get_logger(),"serial_port: %s\r\n", serial_port.c_str());
|
||||
RCLCPP_INFO(this->get_logger(), "baud_rate: %d\r\n", baud_rate);
|
||||
RCLCPP_INFO(this->get_logger(), "frame_id: %s\r\n", frame_id.c_str());
|
||||
RCLCPP_INFO(this->get_logger(), "imu_topic: %s\r\n", imu_topic.c_str());
|
||||
RCLCPP_INFO(
|
||||
this->get_logger(),
|
||||
"timestamp_sync: enabled=%s offset=%.6f alpha=%.3f jump_warn_ms=%u clamp_backward=%s diagnostics=%s throttle_ms=%d",
|
||||
use_timestamp_sync_ ? "true" : "false",
|
||||
config.time_offset_sec,
|
||||
config.bias_alpha,
|
||||
config.jump_warn_threshold_ms,
|
||||
config.clamp_backward_time ? "true" : "false",
|
||||
timestamp_diagnostics_ ? "true" : "false",
|
||||
timestamp_diagnostics_throttle_ms_);
|
||||
|
||||
imu_data.header.frame_id = frame_id;
|
||||
imu_pub = this->create_publisher<sensor_msgs::msg::Imu>(imu_topic, 20);
|
||||
|
||||
fd = open_serial(serial_port, baud_rate);
|
||||
|
||||
while(1)
|
||||
imu_read();
|
||||
}
|
||||
|
||||
private:
|
||||
void imu_read(void)
|
||||
{
|
||||
struct pollfd p;
|
||||
p.fd = fd;
|
||||
p.events = POLLIN;
|
||||
|
||||
int rpoll = poll(&p, 1, 5);
|
||||
|
||||
if(rpoll == 0)
|
||||
return ;
|
||||
|
||||
const auto host_rx_time = this->get_clock()->now();
|
||||
int n = read(fd, buf, sizeof(buf));
|
||||
|
||||
for(int i = 0; i < n; i++)
|
||||
{
|
||||
int rev = hipnuc_input(&raw, buf[i]);
|
||||
|
||||
if(rev)
|
||||
{
|
||||
imu_data.orientation.w = raw.hi91.quat[0];
|
||||
imu_data.orientation.x = raw.hi91.quat[1];
|
||||
imu_data.orientation.y = raw.hi91.quat[2];
|
||||
imu_data.orientation.z = raw.hi91.quat[3];
|
||||
imu_data.angular_velocity.x = raw.hi91.gyr[0] * DEG_TO_RAD;
|
||||
imu_data.angular_velocity.y = raw.hi91.gyr[1] * DEG_TO_RAD;
|
||||
imu_data.angular_velocity.z = raw.hi91.gyr[2] * DEG_TO_RAD;
|
||||
imu_data.linear_acceleration.x = raw.hi91.acc[0] * GRA_ACC;
|
||||
imu_data.linear_acceleration.y = raw.hi91.acc[1] * GRA_ACC;
|
||||
imu_data.linear_acceleration.z = raw.hi91.acc[2] * GRA_ACC;
|
||||
|
||||
if (use_timestamp_sync_)
|
||||
{
|
||||
const auto sync_result = timestamp_sync_.reconstruct(raw.hi91.ts, host_rx_time);
|
||||
imu_data.header.stamp = sync_result.stamp;
|
||||
|
||||
if (sync_result.large_jump_detected)
|
||||
{
|
||||
RCLCPP_WARN_THROTTLE(
|
||||
this->get_logger(), *this->get_clock(), 2000,
|
||||
"hipnuc timestamp jump detected: raw_ts_ms=%u delta_ms=%u unwrapped_ms=%llu",
|
||||
raw.hi91.ts,
|
||||
sync_result.delta_tick_ms,
|
||||
static_cast<unsigned long long>(sync_result.unwrapped_device_tick_ms));
|
||||
}
|
||||
|
||||
if (sync_result.backward_clamped)
|
||||
{
|
||||
RCLCPP_WARN_THROTTLE(
|
||||
this->get_logger(), *this->get_clock(), 2000,
|
||||
"hipnuc backward timestamp clamped: raw_ts_ms=%u stamp_ns=%lld",
|
||||
raw.hi91.ts,
|
||||
static_cast<long long>(sync_result.stamp.nanoseconds()));
|
||||
}
|
||||
|
||||
if (timestamp_diagnostics_)
|
||||
{
|
||||
RCLCPP_INFO_THROTTLE(
|
||||
this->get_logger(), *this->get_clock(), timestamp_diagnostics_throttle_ms_,
|
||||
"hipnuc timestamp: raw_ts_ms=%u host_rx_ns=%lld stamp_ns=%lld delta_ms=%u bias_ns=%lld init=%s",
|
||||
raw.hi91.ts,
|
||||
static_cast<long long>(host_rx_time.nanoseconds()),
|
||||
static_cast<long long>(sync_result.stamp.nanoseconds()),
|
||||
sync_result.delta_tick_ms,
|
||||
static_cast<long long>(sync_result.filtered_bias_ns),
|
||||
sync_result.initialized_this_call ? "true" : "false");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
imu_data.header.stamp = host_rx_time;
|
||||
}
|
||||
|
||||
imu_pub->publish(imu_data);
|
||||
}
|
||||
}
|
||||
|
||||
memset(buf,0,sizeof(buf));
|
||||
}
|
||||
|
||||
int open_serial(std::string port, int baud)
|
||||
{
|
||||
const char* port_device = port.c_str();
|
||||
int fd = open(port_device, O_RDWR | O_NOCTTY | O_NONBLOCK);
|
||||
|
||||
if(fd == -1)
|
||||
{
|
||||
perror("unable to open serial port");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if(fcntl(fd, F_SETFL, O_NONBLOCK) < 0)
|
||||
cout << "fcntl failed" << "\n" << endl;
|
||||
else
|
||||
fcntl(fd, F_SETFL, O_NONBLOCK);
|
||||
|
||||
struct termios options;
|
||||
memset(&options, 0, sizeof(options));
|
||||
tcgetattr(fd, &options);
|
||||
|
||||
switch(baud)
|
||||
{
|
||||
case 115200:
|
||||
cfsetispeed(&options, B115200);
|
||||
cfsetospeed(&options, B115200);
|
||||
break;
|
||||
case 460800:
|
||||
cfsetispeed(&options, B460800);
|
||||
cfsetospeed(&options, B460800);
|
||||
break;
|
||||
case 921600:
|
||||
cfsetispeed(&options, B921600);
|
||||
cfsetospeed(&options, B921600);
|
||||
break;
|
||||
}
|
||||
|
||||
options.c_cflag &= ~PARENB;
|
||||
options.c_cflag &= ~CSTOPB;
|
||||
options.c_cflag &= ~CSIZE;
|
||||
options.c_cflag |= HUPCL;
|
||||
options.c_cflag |= CS8;
|
||||
options.c_cflag &= ~CRTSCTS;
|
||||
options.c_cflag |= CREAD | CLOCAL;
|
||||
|
||||
options.c_iflag &= ~(IXON | IXOFF | IXANY);
|
||||
options.c_iflag &= ~(INLCR|ICRNL);
|
||||
|
||||
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
|
||||
|
||||
options.c_oflag &= ~OPOST;
|
||||
options.c_oflag &= ~(ONLCR|OCRNL);
|
||||
|
||||
options.c_cc[VMIN] = 0;
|
||||
options.c_cc[VTIME] = 0;
|
||||
tcsetattr(fd, TCSANOW, &options);
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
|
||||
std::string serial_port;
|
||||
int baud_rate;
|
||||
std::string frame_id;
|
||||
std::string imu_topic;
|
||||
sensor_msgs::msg::Imu imu_data = sensor_msgs::msg::Imu();
|
||||
rclcpp::Publisher<sensor_msgs::msg::Imu>::SharedPtr imu_pub;
|
||||
driver_timestamp_sync::TimestampSync timestamp_sync_;
|
||||
bool use_timestamp_sync_{true};
|
||||
bool timestamp_diagnostics_{false};
|
||||
int timestamp_diagnostics_throttle_ms_{2000};
|
||||
};
|
||||
|
||||
|
||||
int main(int argc, const char * argv[])
|
||||
{
|
||||
rclcpp::init(argc, argv);
|
||||
rclcpp::spin(std::make_shared<IMUPublisher>());
|
||||
rclcpp::shutdown();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
#include <unistd.h>
|
||||
#include <memory>
|
||||
#include <iostream>
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "std_msgs/msg/string.hpp"
|
||||
#include <sensor_msgs/msg/imu.hpp>
|
||||
#include <iomanip>
|
||||
|
||||
rclcpp::Node::SharedPtr nh = nullptr;
|
||||
using namespace std;
|
||||
|
||||
void topic_callback(const sensor_msgs::msg::Imu::SharedPtr msg)
|
||||
{
|
||||
|
||||
cout << "header:" << "\n";
|
||||
cout << " " << "stamp:"<< "\n";
|
||||
cout << " " << "secs:" << msg->header.stamp.sec<< "\n";
|
||||
cout << " " << "nanosecs:" << msg->header.stamp.nanosec << "\n";
|
||||
cout << " " << "frame_id:" << msg->header.frame_id << "\n" ;
|
||||
|
||||
cout << "orientation:" << "\n";
|
||||
cout << " " << "x: " << fixed << setprecision(18) << msg->orientation.x << "\n";
|
||||
cout << " " << "y: " << fixed << setprecision(18) << msg->orientation.y << "\n";
|
||||
cout << " " << "z: " << fixed << setprecision(18) << msg->orientation.z << "\n";
|
||||
cout << " " << "w: " << fixed << setprecision(18) << msg->orientation.w << "\n";
|
||||
cout << "orientation_covariance: [ " << fixed << setprecision(1) << msg->orientation_covariance[0];
|
||||
cout << ", " << msg->orientation_covariance[1];
|
||||
cout << ", " << msg->orientation_covariance[2];
|
||||
cout << ", " << msg->orientation_covariance[3];
|
||||
cout << ", " << msg->orientation_covariance[4];
|
||||
cout << ", " << msg->orientation_covariance[5];
|
||||
cout << ", " << msg->orientation_covariance[6];
|
||||
cout << ", " << msg->orientation_covariance[7];
|
||||
cout << ", " << msg->orientation_covariance[8] << "]" << "\n";
|
||||
|
||||
cout << "angular_velocity: " << "\n";
|
||||
cout << " " << "x: " << fixed << setprecision(18) << msg->angular_velocity.x << "\n";
|
||||
cout << " " << "y: " << fixed << setprecision(18) << msg->angular_velocity.y << "\n";
|
||||
cout << " " << "z: " << fixed << setprecision(18) << msg->angular_velocity.z << "\n";
|
||||
cout << "angular_velocity_covariance: [ " << fixed << setprecision(1) << msg->angular_velocity_covariance[0];
|
||||
cout << ", " << msg->angular_velocity_covariance[1];
|
||||
cout << ", " << msg->angular_velocity_covariance[2];
|
||||
cout << ", " << msg->angular_velocity_covariance[3];
|
||||
cout << ", " << msg->angular_velocity_covariance[4];
|
||||
cout << ", " << msg->angular_velocity_covariance[5];
|
||||
cout << ", " << msg->angular_velocity_covariance[6];
|
||||
cout << ", " << msg->angular_velocity_covariance[7];
|
||||
cout << ", " << msg->angular_velocity_covariance[8] << "]" << "\n";
|
||||
|
||||
cout << "linear_acceleration:" << "\n";
|
||||
cout << " " << "x: " << fixed << setprecision(18) << msg->linear_acceleration.x << "\n" ;
|
||||
cout << " " << "y: " << fixed << setprecision(18) << msg->linear_acceleration.y << "\n" ;
|
||||
cout << " " << "z: " << fixed << setprecision(18) << msg->linear_acceleration.z << "\n" ;
|
||||
cout << "linear_acceleration_covariance: [ " << fixed << setprecision(1) << msg->linear_acceleration_covariance[0];
|
||||
cout << ", " << msg->linear_acceleration_covariance[1] ;
|
||||
cout << ", " << msg->linear_acceleration_covariance[2] ;
|
||||
cout << ", " << msg->linear_acceleration_covariance[3] ;
|
||||
cout << ", " << msg->linear_acceleration_covariance[4] ;
|
||||
cout << ", " << msg->linear_acceleration_covariance[5] ;
|
||||
cout << ", " << msg->linear_acceleration_covariance[6] ;
|
||||
cout << ", " << msg->linear_acceleration_covariance[7] ;
|
||||
cout << ", " << msg->linear_acceleration_covariance[8] << "]" << "\n" << "---" << endl;
|
||||
}
|
||||
|
||||
|
||||
int main(int argc,const char* argv[])
|
||||
{
|
||||
rclcpp::init(argc, argv);
|
||||
nh = std::make_shared<rclcpp::Node>("imu_sub");
|
||||
rclcpp::Subscription<sensor_msgs::msg::Imu>::SharedPtr imu_sub ;
|
||||
imu_sub = nh->create_subscription<sensor_msgs::msg::Imu>("IMU_data", 10,topic_callback);
|
||||
rclcpp::spin(nh);
|
||||
rclcpp::shutdown();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#CP2102 串口号0003 设置别名为wheeltec_IMU
|
||||
echo 'KERNEL=="ttyUSB*", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60",ATTRS{serial}=="0003", MODE:="0777", GROUP:="dialout", SYMLINK+="wheeltec_IMU"' >/etc/udev/rules.d/wheeltec_imu.rules
|
||||
|
||||
#CH9102 ,如果已经安装了驱动使用CH343
|
||||
echo 'KERNEL=="ttyCH343USB*", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="55d4", ATTRS{serial}=="0003", MODE:="0777", GROUP:="dialout", SYMLINK+="wheeltec_IMU"' >/etc/udev/rules.d/wheeltec_imu_343.rules
|
||||
|
||||
#CH9102 如果没有安装驱动
|
||||
echo 'KERNEL=="ttyACM*", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="55d4", ATTRS{serial}=="0003", MODE:="0777", GROUP:="dialout", SYMLINK+="wheeltec_IMU"' >/etc/udev/rules.d/wheeltec_imu_ACM.rules
|
||||
|
||||
service udev reload
|
||||
sleep 2
|
||||
service udev restart
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,96 @@
|
||||
cmake_minimum_required(VERSION 3.8)
|
||||
project(pointcloud_process)
|
||||
|
||||
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
add_compile_options(-Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
|
||||
# find dependencies
|
||||
find_package(ament_cmake REQUIRED)
|
||||
find_package(rclcpp REQUIRED)
|
||||
find_package(sensor_msgs REQUIRED)
|
||||
find_package(std_msgs REQUIRED)
|
||||
find_package(tf2 REQUIRED)
|
||||
find_package(tf2_ros REQUIRED)
|
||||
find_package(pcl_conversions REQUIRED)
|
||||
find_package(pcl_ros REQUIRED)
|
||||
find_package(yaml-cpp REQUIRED)
|
||||
find_package(rclcpp_components REQUIRED)
|
||||
|
||||
add_library(pointcloud_converter_component SHARED
|
||||
src/pointcloud_converter_component.cpp
|
||||
)
|
||||
|
||||
target_include_directories(pointcloud_converter_component PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
$<INSTALL_INTERFACE:include>
|
||||
)
|
||||
|
||||
ament_target_dependencies(pointcloud_converter_component
|
||||
"rclcpp"
|
||||
"rclcpp_components"
|
||||
"sensor_msgs"
|
||||
"std_msgs"
|
||||
)
|
||||
|
||||
rclcpp_components_register_node(pointcloud_converter_component
|
||||
PLUGIN "XyzirtToXyzircaedtNode"
|
||||
EXECUTABLE pointcloud_converter_component_node
|
||||
)
|
||||
|
||||
install(TARGETS pointcloud_converter_component pointcloud_converter_component_node
|
||||
ARCHIVE DESTINATION lib
|
||||
LIBRARY DESTINATION lib
|
||||
RUNTIME DESTINATION lib/${PROJECT_NAME}
|
||||
)
|
||||
|
||||
add_library(pointcloud_transform_component SHARED
|
||||
src/pointcloud_transform.cpp
|
||||
)
|
||||
|
||||
target_include_directories(pointcloud_transform_component PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
$<INSTALL_INTERFACE:include>
|
||||
)
|
||||
|
||||
target_compile_features(pointcloud_transform_component PUBLIC cxx_std_17)
|
||||
|
||||
ament_target_dependencies(pointcloud_transform_component
|
||||
"rclcpp"
|
||||
"rclcpp_components"
|
||||
"sensor_msgs"
|
||||
"std_msgs"
|
||||
"tf2"
|
||||
"tf2_ros"
|
||||
"pcl_conversions"
|
||||
"pcl_ros"
|
||||
"yaml-cpp"
|
||||
)
|
||||
|
||||
rclcpp_components_register_node(pointcloud_transform_component
|
||||
PLUGIN "PointCloudInterface"
|
||||
EXECUTABLE pointcloud_transform_node
|
||||
)
|
||||
|
||||
install(TARGETS pointcloud_transform_component pointcloud_transform_node
|
||||
ARCHIVE DESTINATION lib
|
||||
LIBRARY DESTINATION lib
|
||||
RUNTIME DESTINATION lib/${PROJECT_NAME}
|
||||
)
|
||||
|
||||
install(DIRECTORY launch
|
||||
DESTINATION share/${PROJECT_NAME})
|
||||
|
||||
if(BUILD_TESTING)
|
||||
find_package(ament_lint_auto REQUIRED)
|
||||
# the following line skips the linter which checks for copyrights
|
||||
# comment the line when a copyright and license is added to all source files
|
||||
set(ament_cmake_copyright_FOUND TRUE)
|
||||
# the following line skips cpplint (only works in a git repo)
|
||||
# comment the line when this package is in a git repo and when
|
||||
# a copyright and license is added to all source files
|
||||
set(ament_cmake_cpplint_FOUND TRUE)
|
||||
ament_lint_auto_find_test_dependencies()
|
||||
endif()
|
||||
|
||||
ament_package()
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
#ifndef POINTCLOUD_CONVERTER_COMPONENT_HPP_
|
||||
#define POINTCLOUD_CONVERTER_COMPONENT_HPP_
|
||||
|
||||
#include <rclcpp/rclcpp.hpp>
|
||||
#include <sensor_msgs/msg/point_cloud2.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class XyzirtToXyzircaedtNode : public rclcpp::Node
|
||||
{
|
||||
public:
|
||||
explicit XyzirtToXyzircaedtNode(const rclcpp::NodeOptions & options);
|
||||
|
||||
private:
|
||||
void pandarCloudCb(const sensor_msgs::msg::PointCloud2::ConstSharedPtr msg) const;
|
||||
void rslidarCloudCb(const sensor_msgs::msg::PointCloud2::ConstSharedPtr msg) const;
|
||||
void timooCloudCb(const sensor_msgs::msg::PointCloud2::ConstSharedPtr msg) const;
|
||||
void fastlioCloudCb(const sensor_msgs::msg::PointCloud2::ConstSharedPtr msg) const;
|
||||
void liorfCloudCb(const sensor_msgs::msg::PointCloud2::ConstSharedPtr msg) const;
|
||||
|
||||
bool hasField(const sensor_msgs::msg::PointCloud2 & cloud, const std::string & name) const;
|
||||
|
||||
int addPointField(sensor_msgs::msg::PointCloud2 & cloud,
|
||||
const std::string & name, int count, int datatype, int offset) const;
|
||||
|
||||
rclcpp::Subscription<sensor_msgs::msg::PointCloud2>::SharedPtr sub_;
|
||||
rclcpp::Publisher<sensor_msgs::msg::PointCloud2>::SharedPtr pub_;
|
||||
rclcpp::Publisher<sensor_msgs::msg::PointCloud2>::SharedPtr pub_fastlio_;
|
||||
rclcpp::Publisher<sensor_msgs::msg::PointCloud2>::SharedPtr pub_liorf_;
|
||||
rclcpp::CallbackGroup::SharedPtr cb_group_;
|
||||
std::string lidar_type_;
|
||||
bool enable_fastlio_;
|
||||
bool enable_liorf_;
|
||||
};
|
||||
|
||||
#endif
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
#ifndef POINTCLOUD_TRANSFORM_HPP_
|
||||
#define POINTCLOUD_TRANSFORM_HPP_
|
||||
|
||||
#include <rclcpp/rclcpp.hpp>
|
||||
#include <sensor_msgs/msg/point_cloud2.hpp>
|
||||
#include <tf2_ros/buffer.h>
|
||||
#include <tf2_ros/transform_listener.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class PointCloudInterface : public rclcpp::Node
|
||||
{
|
||||
public:
|
||||
explicit PointCloudInterface(const rclcpp::NodeOptions & node_options);
|
||||
virtual ~PointCloudInterface();
|
||||
|
||||
private:
|
||||
struct CropBox
|
||||
{
|
||||
double min_x;
|
||||
double max_x;
|
||||
double min_y;
|
||||
double max_y;
|
||||
double min_z;
|
||||
double max_z;
|
||||
};
|
||||
|
||||
void processScan(const sensor_msgs::msg::PointCloud2::ConstSharedPtr scanMsg);
|
||||
void setupTF();
|
||||
std::unique_ptr<sensor_msgs::msg::PointCloud2> removeSelfPoints(
|
||||
const sensor_msgs::msg::PointCloud2 & input);
|
||||
bool getFieldOffset(
|
||||
const sensor_msgs::msg::PointCloud2 & cloud, const std::string & field_name,
|
||||
uint32_t & offset) const;
|
||||
|
||||
rclcpp::Subscription<sensor_msgs::msg::PointCloud2>::SharedPtr carla_cloud_;
|
||||
rclcpp::Publisher<sensor_msgs::msg::PointCloud2>::SharedPtr pandar_points_localization;
|
||||
rclcpp::Publisher<sensor_msgs::msg::PointCloud2>::SharedPtr pandar_points_perception;
|
||||
|
||||
std::shared_ptr<tf2_ros::Buffer> tf_buffer_;
|
||||
std::shared_ptr<tf2_ros::TransformListener> tf_listener_;
|
||||
rclcpp::CallbackGroup::SharedPtr cb_group_;
|
||||
std::string tf_output_frame_;
|
||||
bool enable_self_crop_;
|
||||
bool self_crop_apply_to_localization_;
|
||||
CropBox self_crop_box_;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,123 @@
|
||||
from launch import LaunchDescription
|
||||
from launch_ros.actions import Node
|
||||
from launch_ros.actions import ComposableNodeContainer
|
||||
from launch_ros.descriptions import ComposableNode
|
||||
from launch_ros.parameter_descriptions import ParameterValue
|
||||
from launch.actions import DeclareLaunchArgument
|
||||
from launch.substitutions import LaunchConfiguration
|
||||
|
||||
def generate_launch_description():
|
||||
lidar_type_arg = DeclareLaunchArgument(
|
||||
'lidar_type',
|
||||
default_value='timoo',
|
||||
description='Lidar type: pandar, timoo, or rslidar'
|
||||
)
|
||||
|
||||
input_pointcloud_topic_arg = DeclareLaunchArgument(
|
||||
'input_pointcloud_topic',
|
||||
default_value='/timoo/top/pointcloud',
|
||||
description='Raw lidar pointcloud topic'
|
||||
)
|
||||
|
||||
enable_fastlio_arg = DeclareLaunchArgument(
|
||||
'enable_fastlio',
|
||||
default_value='false',
|
||||
description='Enable Fast-LIO2 pointcloud output'
|
||||
)
|
||||
|
||||
enable_liorf_arg = DeclareLaunchArgument(
|
||||
'enable_liorf',
|
||||
default_value='false',
|
||||
description='Enable LIO-RF pointcloud output'
|
||||
)
|
||||
enable_self_crop_arg = DeclareLaunchArgument(
|
||||
'enable_self_crop',
|
||||
default_value='true',
|
||||
description='Remove ego-vehicle fixed-structure points from perception pointcloud'
|
||||
)
|
||||
self_crop_apply_to_localization_arg = DeclareLaunchArgument(
|
||||
'self_crop_apply_to_localization',
|
||||
default_value='true',
|
||||
description='Apply self crop to localization pointcloud as well'
|
||||
)
|
||||
self_crop_min_x_arg = DeclareLaunchArgument('self_crop_min_x', default_value='1.00')
|
||||
self_crop_max_x_arg = DeclareLaunchArgument('self_crop_max_x', default_value='1.70')
|
||||
self_crop_min_y_arg = DeclareLaunchArgument('self_crop_min_y', default_value='-0.65')
|
||||
self_crop_max_y_arg = DeclareLaunchArgument('self_crop_max_y', default_value='-0.05')
|
||||
self_crop_min_z_arg = DeclareLaunchArgument('self_crop_min_z', default_value='1.10')
|
||||
self_crop_max_z_arg = DeclareLaunchArgument('self_crop_max_z', default_value='2.05')
|
||||
|
||||
lidar_type = LaunchConfiguration('lidar_type')
|
||||
input_pointcloud_topic = LaunchConfiguration('input_pointcloud_topic')
|
||||
enable_fastlio = LaunchConfiguration('enable_fastlio')
|
||||
enable_liorf = LaunchConfiguration('enable_liorf')
|
||||
enable_self_crop = LaunchConfiguration('enable_self_crop')
|
||||
self_crop_apply_to_localization = LaunchConfiguration('self_crop_apply_to_localization')
|
||||
self_crop_min_x = LaunchConfiguration('self_crop_min_x')
|
||||
self_crop_max_x = LaunchConfiguration('self_crop_max_x')
|
||||
self_crop_min_y = LaunchConfiguration('self_crop_min_y')
|
||||
self_crop_max_y = LaunchConfiguration('self_crop_max_y')
|
||||
self_crop_min_z = LaunchConfiguration('self_crop_min_z')
|
||||
self_crop_max_z = LaunchConfiguration('self_crop_max_z')
|
||||
|
||||
return LaunchDescription([
|
||||
lidar_type_arg,
|
||||
input_pointcloud_topic_arg,
|
||||
enable_fastlio_arg,
|
||||
enable_liorf_arg,
|
||||
enable_self_crop_arg,
|
||||
self_crop_apply_to_localization_arg,
|
||||
self_crop_min_x_arg,
|
||||
self_crop_max_x_arg,
|
||||
self_crop_min_y_arg,
|
||||
self_crop_max_y_arg,
|
||||
self_crop_min_z_arg,
|
||||
self_crop_max_z_arg,
|
||||
ComposableNodeContainer(
|
||||
name='pc_processor_container',
|
||||
namespace='',
|
||||
package='rclcpp_components',
|
||||
executable='component_container_mt',
|
||||
composable_node_descriptions=[
|
||||
# 节点 1: 格式转换 (XYZIRT -> XYZIRCAEDT + FastLIO2 + LIO-RF)
|
||||
ComposableNode(
|
||||
package='pointcloud_process',
|
||||
plugin='XyzirtToXyzircaedtNode',
|
||||
name='converter',
|
||||
parameters=[{
|
||||
'lidar_type': lidar_type,
|
||||
'enable_fastlio': enable_fastlio,
|
||||
'enable_liorf': enable_liorf
|
||||
}],
|
||||
remappings=[
|
||||
('~/input/pointcloud', input_pointcloud_topic),
|
||||
('~/output/pointcloud', '/sensing/lidar/top/converted/pointcloud'),
|
||||
('~/output/fastlio_pointcloud', '/sensing/lidar/top/fastlio/pointcloud'),
|
||||
('~/output/liorf_pointcloud', '/sensing/lidar/top/liorf/pointcloud')
|
||||
]
|
||||
),
|
||||
|
||||
# 节点 2: 坐标变换
|
||||
ComposableNode(
|
||||
package='pointcloud_process',
|
||||
plugin='PointCloudInterface',
|
||||
name='transformer',
|
||||
parameters=[{
|
||||
'enable_self_crop': ParameterValue(enable_self_crop, value_type=bool),
|
||||
'self_crop_apply_to_localization': ParameterValue(
|
||||
self_crop_apply_to_localization, value_type=bool),
|
||||
'self_crop_min_x': ParameterValue(self_crop_min_x, value_type=float),
|
||||
'self_crop_max_x': ParameterValue(self_crop_max_x, value_type=float),
|
||||
'self_crop_min_y': ParameterValue(self_crop_min_y, value_type=float),
|
||||
'self_crop_max_y': ParameterValue(self_crop_max_y, value_type=float),
|
||||
'self_crop_min_z': ParameterValue(self_crop_min_z, value_type=float),
|
||||
'self_crop_max_z': ParameterValue(self_crop_max_z, value_type=float)
|
||||
}],
|
||||
remappings=[('~/input/pointcloud', '/sensing/lidar/top/converted/pointcloud'),
|
||||
('~/output/localization/pointcloud', '/sensing/lidar/top/localization/pointcloud'),
|
||||
('~/output/perception/pointcloud', '/sensing/lidar/top/perception/pointcloud')]
|
||||
)
|
||||
],
|
||||
output='screen',
|
||||
)
|
||||
])
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
||||
<package format="3">
|
||||
<name>pointcloud_process</name>
|
||||
<version>0.0.0</version>
|
||||
<description>TODO: Package description</description>
|
||||
<maintainer email="root@todo.todo">root</maintainer>
|
||||
<license>TODO: License declaration</license>
|
||||
|
||||
<buildtool_depend>ament_cmake</buildtool_depend>
|
||||
|
||||
<depend>rclcpp</depend>
|
||||
<depend>sensor_msgs</depend>
|
||||
<depend>std_msgs</depend>
|
||||
<depend>tf2</depend>
|
||||
<depend>tf2_ros</depend>
|
||||
<depend>pcl_ros</depend>
|
||||
<depend>pcl_conversions</depend>
|
||||
|
||||
<test_depend>ament_lint_auto</test_depend>
|
||||
<test_depend>ament_lint_common</test_depend>
|
||||
|
||||
<export>
|
||||
<build_type>ament_cmake</build_type>
|
||||
</export>
|
||||
</package>
|
||||
@@ -0,0 +1,444 @@
|
||||
#include <sensor_msgs/point_cloud2_iterator.hpp>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include "rclcpp_components/register_node_macro.hpp"
|
||||
#include "pointcloud_process/pointcloud_converter_component.hpp"
|
||||
|
||||
XyzirtToXyzircaedtNode::XyzirtToXyzircaedtNode(const rclcpp::NodeOptions & options)
|
||||
: Node("xyzirt_to_xyzircaedt_node", options)
|
||||
{
|
||||
this->declare_parameter("lidar_type", "pandar");
|
||||
this->declare_parameter("enable_fastlio", false);
|
||||
this->declare_parameter("enable_liorf", false);
|
||||
this->get_parameter("lidar_type", lidar_type_);
|
||||
this->get_parameter("enable_fastlio", enable_fastlio_);
|
||||
this->get_parameter("enable_liorf", enable_liorf_);
|
||||
|
||||
cb_group_ = this->create_callback_group(rclcpp::CallbackGroupType::Reentrant);
|
||||
auto sub_opt = rclcpp::SubscriptionOptions();
|
||||
sub_opt.callback_group = cb_group_;
|
||||
|
||||
if (lidar_type_ == "timoo") {
|
||||
sub_ = create_subscription<sensor_msgs::msg::PointCloud2>(
|
||||
"~/input/pointcloud", 10,
|
||||
std::bind(&XyzirtToXyzircaedtNode::timooCloudCb, this, std::placeholders::_1),
|
||||
sub_opt);
|
||||
} else if (lidar_type_ == "rslidar" || lidar_type_ == "robosense") {
|
||||
sub_ = create_subscription<sensor_msgs::msg::PointCloud2>(
|
||||
"~/input/pointcloud", 10,
|
||||
std::bind(&XyzirtToXyzircaedtNode::rslidarCloudCb, this, std::placeholders::_1),
|
||||
sub_opt);
|
||||
} else {
|
||||
sub_ = create_subscription<sensor_msgs::msg::PointCloud2>(
|
||||
"~/input/pointcloud", 10,
|
||||
std::bind(&XyzirtToXyzircaedtNode::pandarCloudCb, this, std::placeholders::_1),
|
||||
sub_opt);
|
||||
}
|
||||
|
||||
pub_ = create_publisher<sensor_msgs::msg::PointCloud2>(
|
||||
"~/output/pointcloud", rclcpp::SensorDataQoS());
|
||||
if (enable_fastlio_) {
|
||||
pub_fastlio_ = create_publisher<sensor_msgs::msg::PointCloud2>(
|
||||
"~/output/fastlio_pointcloud", rclcpp::SensorDataQoS());
|
||||
}
|
||||
if (enable_liorf_) {
|
||||
pub_liorf_ = create_publisher<sensor_msgs::msg::PointCloud2>(
|
||||
"~/output/liorf_pointcloud", rclcpp::SensorDataQoS());
|
||||
}
|
||||
}
|
||||
|
||||
bool XyzirtToXyzircaedtNode::hasField(
|
||||
const sensor_msgs::msg::PointCloud2 & cloud, const std::string & name) const
|
||||
{
|
||||
for (const auto & field : cloud.fields) {
|
||||
if (field.name == name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int XyzirtToXyzircaedtNode::addPointField(
|
||||
sensor_msgs::msg::PointCloud2 & cloud, const std::string & name,
|
||||
int count, int datatype, int offset) const
|
||||
{
|
||||
sensor_msgs::msg::PointField field;
|
||||
field.name = name;
|
||||
field.count = count;
|
||||
field.datatype = datatype;
|
||||
field.offset = offset;
|
||||
cloud.fields.push_back(field);
|
||||
return offset + (count * (datatype == sensor_msgs::msg::PointField::FLOAT32 ? 4 :
|
||||
datatype == sensor_msgs::msg::PointField::UINT16 ? 2 : 1));
|
||||
}
|
||||
|
||||
void XyzirtToXyzircaedtNode::pandarCloudCb(const sensor_msgs::msg::PointCloud2::ConstSharedPtr msg) const
|
||||
{
|
||||
auto out = std::make_unique<sensor_msgs::msg::PointCloud2>();
|
||||
out->header = msg->header;
|
||||
out->width = msg->width;
|
||||
out->height = msg->height;
|
||||
out->is_dense = msg->is_dense;
|
||||
|
||||
int offset = 0;
|
||||
offset = addPointField(*out, "x", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "y", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "z", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "intensity", 1, sensor_msgs::msg::PointField::UINT8, offset);
|
||||
offset = addPointField(*out, "return_type", 1, sensor_msgs::msg::PointField::UINT8, offset);
|
||||
offset = addPointField(*out, "channel", 1, sensor_msgs::msg::PointField::UINT16, offset);
|
||||
offset = addPointField(*out, "azimuth", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "elevation", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "distance", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "time_stamp", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
|
||||
out->point_step = offset;
|
||||
out->row_step = out->width * out->point_step;
|
||||
out->data.resize(out->row_step * out->height);
|
||||
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_x(*msg, "x");
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_y(*msg, "y");
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_z(*msg, "z");
|
||||
sensor_msgs::PointCloud2ConstIterator<uint8_t> iter_intensity(*msg, "intensity");
|
||||
sensor_msgs::PointCloud2ConstIterator<double> iter_time(*msg, "timestamp");
|
||||
|
||||
sensor_msgs::PointCloud2Iterator<float> out_x(*out, "x");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_y(*out, "y");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_z(*out, "z");
|
||||
sensor_msgs::PointCloud2Iterator<uint8_t> out_intensity(*out, "intensity");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_distance(*out, "distance");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_azimuth(*out, "azimuth");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_elevation(*out, "elevation");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_time(*out, "time_stamp");
|
||||
|
||||
double frame_start_ts = iter_time[0];
|
||||
|
||||
for (size_t i = 0; i < out->width * out->height; ++i, ++iter_x, ++iter_y, ++iter_z, ++iter_intensity, ++iter_time,
|
||||
++out_x, ++out_y, ++out_z, ++out_intensity, ++out_distance, ++out_azimuth, ++out_elevation, ++out_time)
|
||||
{
|
||||
*out_x = *iter_x;
|
||||
*out_y = *iter_y;
|
||||
*out_z = *iter_z;
|
||||
*out_intensity = static_cast<uint8_t>(*iter_intensity);
|
||||
|
||||
float dist = std::sqrt((*iter_x) * (*iter_x) + (*iter_y) * (*iter_y) + (*iter_z) * (*iter_z));
|
||||
*out_distance = dist;
|
||||
*out_azimuth = std::atan2((*iter_y), (*iter_x));
|
||||
*out_elevation = std::asin((*iter_z) / (dist + 1e-6f));
|
||||
*out_time = static_cast<float>(*iter_time - frame_start_ts);
|
||||
}
|
||||
|
||||
pub_->publish(std::move(out));
|
||||
|
||||
if (enable_fastlio_) {
|
||||
fastlioCloudCb(msg);
|
||||
}
|
||||
if (enable_liorf_) {
|
||||
liorfCloudCb(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void XyzirtToXyzircaedtNode::timooCloudCb(const sensor_msgs::msg::PointCloud2::ConstSharedPtr msg) const
|
||||
{
|
||||
auto out = std::make_unique<sensor_msgs::msg::PointCloud2>();
|
||||
out->header = msg->header;
|
||||
out->width = msg->width;
|
||||
out->height = msg->height;
|
||||
out->is_dense = msg->is_dense;
|
||||
|
||||
int offset = 0;
|
||||
offset = addPointField(*out, "x", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "y", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "z", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "intensity", 1, sensor_msgs::msg::PointField::UINT8, offset);
|
||||
offset = addPointField(*out, "return_type", 1, sensor_msgs::msg::PointField::UINT8, offset);
|
||||
offset = addPointField(*out, "channel", 1, sensor_msgs::msg::PointField::UINT16, offset);
|
||||
offset = addPointField(*out, "azimuth", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "elevation", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "distance", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "time_stamp", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
|
||||
out->point_step = offset;
|
||||
out->row_step = out->width * out->point_step;
|
||||
out->data.resize(out->row_step * out->height);
|
||||
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_x(*msg, "x");
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_y(*msg, "y");
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_z(*msg, "z");
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_intensity(*msg, "intensity");
|
||||
sensor_msgs::PointCloud2ConstIterator<uint16_t> iter_ring(*msg, "ring");
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_time(*msg, "time");
|
||||
|
||||
sensor_msgs::PointCloud2Iterator<float> out_x(*out, "x");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_y(*out, "y");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_z(*out, "z");
|
||||
sensor_msgs::PointCloud2Iterator<uint8_t> out_intensity(*out, "intensity");
|
||||
sensor_msgs::PointCloud2Iterator<uint8_t> out_return_type(*out, "return_type");
|
||||
sensor_msgs::PointCloud2Iterator<uint16_t> out_channel(*out, "channel");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_azimuth(*out, "azimuth");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_elevation(*out, "elevation");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_distance(*out, "distance");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_time(*out, "time_stamp");
|
||||
|
||||
double frame_start_ts = static_cast<double>(iter_time[0]);
|
||||
|
||||
for (size_t i = 0; i < out->width * out->height; ++i, ++iter_x, ++iter_y, ++iter_z, ++iter_intensity, ++iter_ring, ++iter_time,
|
||||
++out_x, ++out_y, ++out_z, ++out_intensity, ++out_return_type, ++out_channel, ++out_azimuth, ++out_elevation, ++out_distance, ++out_time)
|
||||
{
|
||||
*out_x = *iter_x;
|
||||
*out_y = *iter_y;
|
||||
*out_z = *iter_z;
|
||||
*out_intensity = static_cast<uint8_t>(*iter_intensity);
|
||||
*out_return_type = 0;
|
||||
*out_channel = *iter_ring;
|
||||
|
||||
float dist = std::sqrt((*iter_x) * (*iter_x) + (*iter_y) * (*iter_y) + (*iter_z) * (*iter_z));
|
||||
*out_distance = dist;
|
||||
*out_azimuth = std::atan2((*iter_y), (*iter_x));
|
||||
*out_elevation = std::asin((*iter_z) / (dist + 1e-6f));
|
||||
*out_time = static_cast<float>(static_cast<double>(*iter_time) - frame_start_ts);
|
||||
}
|
||||
|
||||
pub_->publish(std::move(out));
|
||||
|
||||
if (enable_fastlio_) {
|
||||
fastlioCloudCb(msg);
|
||||
}
|
||||
if (enable_liorf_) {
|
||||
liorfCloudCb(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void XyzirtToXyzircaedtNode::rslidarCloudCb(const sensor_msgs::msg::PointCloud2::ConstSharedPtr msg) const
|
||||
{
|
||||
const char * required_fields[] = {"x", "y", "z", "intensity", "ring", "timestamp"};
|
||||
for (const auto * field : required_fields) {
|
||||
if (!hasField(*msg, field)) {
|
||||
RCLCPP_ERROR(
|
||||
get_logger(),
|
||||
"rslidar pointcloud is missing required field '%s'. Build rslidar_sdk with POINT_TYPE XYZIRT.",
|
||||
field);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto out = std::make_unique<sensor_msgs::msg::PointCloud2>();
|
||||
out->header = msg->header;
|
||||
out->width = msg->width;
|
||||
out->height = msg->height;
|
||||
out->is_dense = msg->is_dense;
|
||||
|
||||
int offset = 0;
|
||||
offset = addPointField(*out, "x", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "y", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "z", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "intensity", 1, sensor_msgs::msg::PointField::UINT8, offset);
|
||||
offset = addPointField(*out, "return_type", 1, sensor_msgs::msg::PointField::UINT8, offset);
|
||||
offset = addPointField(*out, "channel", 1, sensor_msgs::msg::PointField::UINT16, offset);
|
||||
offset = addPointField(*out, "azimuth", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "elevation", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "distance", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "time_stamp", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
|
||||
out->point_step = offset;
|
||||
out->row_step = out->width * out->point_step;
|
||||
out->data.resize(out->row_step * out->height);
|
||||
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_x(*msg, "x");
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_y(*msg, "y");
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_z(*msg, "z");
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_intensity(*msg, "intensity");
|
||||
sensor_msgs::PointCloud2ConstIterator<uint16_t> iter_ring(*msg, "ring");
|
||||
sensor_msgs::PointCloud2ConstIterator<double> iter_timestamp(*msg, "timestamp");
|
||||
|
||||
sensor_msgs::PointCloud2Iterator<float> out_x(*out, "x");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_y(*out, "y");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_z(*out, "z");
|
||||
sensor_msgs::PointCloud2Iterator<uint8_t> out_intensity(*out, "intensity");
|
||||
sensor_msgs::PointCloud2Iterator<uint8_t> out_return_type(*out, "return_type");
|
||||
sensor_msgs::PointCloud2Iterator<uint16_t> out_channel(*out, "channel");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_azimuth(*out, "azimuth");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_elevation(*out, "elevation");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_distance(*out, "distance");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_time(*out, "time_stamp");
|
||||
|
||||
const double frame_start_ts = iter_timestamp[0];
|
||||
|
||||
for (size_t i = 0; i < out->width * out->height;
|
||||
++i, ++iter_x, ++iter_y, ++iter_z, ++iter_intensity, ++iter_ring, ++iter_timestamp,
|
||||
++out_x, ++out_y, ++out_z, ++out_intensity, ++out_return_type, ++out_channel,
|
||||
++out_azimuth, ++out_elevation, ++out_distance, ++out_time) {
|
||||
*out_x = *iter_x;
|
||||
*out_y = *iter_y;
|
||||
*out_z = *iter_z;
|
||||
*out_intensity = static_cast<uint8_t>(
|
||||
std::max(0.0F, std::min(255.0F, *iter_intensity)));
|
||||
*out_return_type = 0;
|
||||
*out_channel = *iter_ring;
|
||||
|
||||
const float dist = std::sqrt((*iter_x) * (*iter_x) + (*iter_y) * (*iter_y) + (*iter_z) * (*iter_z));
|
||||
*out_distance = dist;
|
||||
*out_azimuth = std::atan2((*iter_y), (*iter_x));
|
||||
*out_elevation = std::asin((*iter_z) / (dist + 1e-6f));
|
||||
*out_time = static_cast<float>(*iter_timestamp - frame_start_ts);
|
||||
}
|
||||
|
||||
pub_->publish(std::move(out));
|
||||
|
||||
if (enable_fastlio_) {
|
||||
fastlioCloudCb(msg);
|
||||
}
|
||||
if (enable_liorf_) {
|
||||
liorfCloudCb(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void XyzirtToXyzircaedtNode::fastlioCloudCb(const sensor_msgs::msg::PointCloud2::ConstSharedPtr msg) const
|
||||
{
|
||||
auto out = std::make_unique<sensor_msgs::msg::PointCloud2>();
|
||||
out->header = msg->header;
|
||||
out->width = msg->width;
|
||||
out->height = msg->height;
|
||||
out->is_dense = msg->is_dense;
|
||||
|
||||
int offset = 0;
|
||||
offset = addPointField(*out, "x", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "y", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "z", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "intensity", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "time", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "ring", 1, sensor_msgs::msg::PointField::UINT8, offset);
|
||||
|
||||
out->point_step = offset;
|
||||
out->row_step = out->width * out->point_step;
|
||||
out->data.resize(out->row_step * out->height);
|
||||
|
||||
bool has_time_field = false;
|
||||
for (const auto& field : msg->fields) {
|
||||
if (field.name == "time") {
|
||||
has_time_field = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_x(*msg, "x");
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_y(*msg, "y");
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_z(*msg, "z");
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_intensity(*msg, "intensity");
|
||||
sensor_msgs::PointCloud2ConstIterator<uint16_t> iter_ring(*msg, "ring");
|
||||
|
||||
sensor_msgs::PointCloud2Iterator<float> out_x(*out, "x");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_y(*out, "y");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_z(*out, "z");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_intensity(*out, "intensity");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_time(*out, "time");
|
||||
sensor_msgs::PointCloud2Iterator<uint8_t> out_ring(*out, "ring");
|
||||
|
||||
if (has_time_field) {
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_time(*msg, "time");
|
||||
double frame_start_ts = static_cast<double>(iter_time[0]);
|
||||
|
||||
for (size_t i = 0; i < out->width * out->height; ++i, ++iter_x, ++iter_y, ++iter_z, ++iter_intensity, ++iter_ring, ++iter_time,
|
||||
++out_x, ++out_y, ++out_z, ++out_intensity, ++out_time, ++out_ring)
|
||||
{
|
||||
*out_x = *iter_x;
|
||||
*out_y = *iter_y;
|
||||
*out_z = *iter_z;
|
||||
*out_intensity = *iter_intensity;
|
||||
*out_ring = static_cast<uint8_t>(*iter_ring);
|
||||
*out_time = static_cast<float>(static_cast<double>(*iter_time) - frame_start_ts);
|
||||
}
|
||||
} else {
|
||||
sensor_msgs::PointCloud2ConstIterator<double> iter_timestamp(*msg, "timestamp");
|
||||
double frame_start_ts = iter_timestamp[0];
|
||||
|
||||
for (size_t i = 0; i < out->width * out->height; ++i, ++iter_x, ++iter_y, ++iter_z, ++iter_intensity, ++iter_ring, ++iter_timestamp,
|
||||
++out_x, ++out_y, ++out_z, ++out_intensity, ++out_time, ++out_ring)
|
||||
{
|
||||
*out_x = *iter_x;
|
||||
*out_y = *iter_y;
|
||||
*out_z = *iter_z;
|
||||
*out_intensity = *iter_intensity;
|
||||
*out_ring = static_cast<uint8_t>(*iter_ring);
|
||||
*out_time = static_cast<float>(*iter_timestamp - frame_start_ts);
|
||||
}
|
||||
}
|
||||
|
||||
pub_fastlio_->publish(std::move(out));
|
||||
}
|
||||
|
||||
void XyzirtToXyzircaedtNode::liorfCloudCb(const sensor_msgs::msg::PointCloud2::ConstSharedPtr msg) const
|
||||
{
|
||||
auto out = std::make_unique<sensor_msgs::msg::PointCloud2>();
|
||||
out->header = msg->header;
|
||||
out->width = msg->width;
|
||||
out->height = msg->height;
|
||||
out->is_dense = msg->is_dense;
|
||||
|
||||
int offset = 0;
|
||||
offset = addPointField(*out, "x", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "y", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "z", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "intensity", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
offset = addPointField(*out, "ring", 1, sensor_msgs::msg::PointField::UINT16, offset);
|
||||
offset = addPointField(*out, "time", 1, sensor_msgs::msg::PointField::FLOAT32, offset);
|
||||
|
||||
out->point_step = offset;
|
||||
out->row_step = out->width * out->point_step;
|
||||
out->data.resize(out->row_step * out->height);
|
||||
|
||||
bool has_time_field = false;
|
||||
for (const auto& field : msg->fields) {
|
||||
if (field.name == "time") {
|
||||
has_time_field = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_x(*msg, "x");
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_y(*msg, "y");
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_z(*msg, "z");
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_intensity(*msg, "intensity");
|
||||
sensor_msgs::PointCloud2ConstIterator<uint16_t> iter_ring(*msg, "ring");
|
||||
|
||||
sensor_msgs::PointCloud2Iterator<float> out_x(*out, "x");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_y(*out, "y");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_z(*out, "z");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_intensity(*out, "intensity");
|
||||
sensor_msgs::PointCloud2Iterator<uint16_t> out_ring(*out, "ring");
|
||||
sensor_msgs::PointCloud2Iterator<float> out_time(*out, "time");
|
||||
|
||||
if (has_time_field) {
|
||||
sensor_msgs::PointCloud2ConstIterator<float> iter_time(*msg, "time");
|
||||
double frame_start_ts = static_cast<double>(iter_time[0]);
|
||||
|
||||
for (size_t i = 0; i < out->width * out->height; ++i, ++iter_x, ++iter_y, ++iter_z, ++iter_intensity, ++iter_ring, ++iter_time,
|
||||
++out_x, ++out_y, ++out_z, ++out_intensity, ++out_ring, ++out_time)
|
||||
{
|
||||
*out_x = *iter_x;
|
||||
*out_y = *iter_y;
|
||||
*out_z = *iter_z;
|
||||
*out_intensity = *iter_intensity;
|
||||
*out_ring = *iter_ring;
|
||||
*out_time = static_cast<float>(static_cast<double>(*iter_time) - frame_start_ts);
|
||||
}
|
||||
} else {
|
||||
sensor_msgs::PointCloud2ConstIterator<double> iter_timestamp(*msg, "timestamp");
|
||||
double frame_start_ts = iter_timestamp[0];
|
||||
|
||||
for (size_t i = 0; i < out->width * out->height; ++i, ++iter_x, ++iter_y, ++iter_z, ++iter_intensity, ++iter_ring, ++iter_timestamp,
|
||||
++out_x, ++out_y, ++out_z, ++out_intensity, ++out_ring, ++out_time)
|
||||
{
|
||||
*out_x = *iter_x;
|
||||
*out_y = *iter_y;
|
||||
*out_z = *iter_z;
|
||||
*out_intensity = *iter_intensity;
|
||||
*out_ring = *iter_ring;
|
||||
*out_time = static_cast<float>(*iter_timestamp - frame_start_ts);
|
||||
}
|
||||
}
|
||||
|
||||
pub_liorf_->publish(std::move(out));
|
||||
}
|
||||
|
||||
RCLCPP_COMPONENTS_REGISTER_NODE(XyzirtToXyzircaedtNode)
|
||||
@@ -0,0 +1,157 @@
|
||||
#include "pointcloud_process/pointcloud_transform.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <pcl_ros/transforms.hpp>
|
||||
#include <utility>
|
||||
#include "rclcpp_components/register_node_macro.hpp"
|
||||
|
||||
PointCloudInterface::PointCloudInterface(const rclcpp::NodeOptions & node_options)
|
||||
: Node("carla_pointcloud_interface_node", node_options)
|
||||
{
|
||||
tf_output_frame_ = this->declare_parameter<std::string>("tf_output_frame", "base_link");
|
||||
enable_self_crop_ = this->declare_parameter<bool>("enable_self_crop", true);
|
||||
self_crop_apply_to_localization_ =
|
||||
this->declare_parameter<bool>("self_crop_apply_to_localization", true);
|
||||
self_crop_box_.min_x = this->declare_parameter<double>("self_crop_min_x", 1.00);
|
||||
self_crop_box_.max_x = this->declare_parameter<double>("self_crop_max_x", 1.70);
|
||||
self_crop_box_.min_y = this->declare_parameter<double>("self_crop_min_y", -0.65);
|
||||
self_crop_box_.max_y = this->declare_parameter<double>("self_crop_max_y", -0.05);
|
||||
self_crop_box_.min_z = this->declare_parameter<double>("self_crop_min_z", 1.10);
|
||||
self_crop_box_.max_z = this->declare_parameter<double>("self_crop_max_z", 2.05);
|
||||
|
||||
cb_group_ = this->create_callback_group(rclcpp::CallbackGroupType::Reentrant);
|
||||
auto sub_opt = rclcpp::SubscriptionOptions();
|
||||
sub_opt.callback_group = cb_group_;
|
||||
|
||||
carla_cloud_ = this->create_subscription<sensor_msgs::msg::PointCloud2>(
|
||||
"~/input/pointcloud", rclcpp::SensorDataQoS(),
|
||||
std::bind(&PointCloudInterface::processScan, this, std::placeholders::_1),
|
||||
sub_opt);
|
||||
|
||||
setupTF();
|
||||
|
||||
pandar_points_localization = this->create_publisher<sensor_msgs::msg::PointCloud2>(
|
||||
"~/output/localization/pointcloud", rclcpp::SensorDataQoS());
|
||||
pandar_points_perception = this->create_publisher<sensor_msgs::msg::PointCloud2>(
|
||||
"~/output/perception/pointcloud", rclcpp::SensorDataQoS());
|
||||
}
|
||||
|
||||
void PointCloudInterface::setupTF()
|
||||
{
|
||||
tf_buffer_ = std::make_shared<tf2_ros::Buffer>(this->get_clock());
|
||||
tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_);
|
||||
}
|
||||
|
||||
void PointCloudInterface::processScan(const sensor_msgs::msg::PointCloud2::ConstSharedPtr scanMsg)
|
||||
{
|
||||
auto transformed_cloud = std::make_unique<sensor_msgs::msg::PointCloud2>();
|
||||
if (pcl_ros::transformPointCloud(tf_output_frame_, *scanMsg, *transformed_cloud, *tf_buffer_))
|
||||
{
|
||||
transformed_cloud->header.stamp = scanMsg->header.stamp;
|
||||
|
||||
auto perception_msg = enable_self_crop_
|
||||
? removeSelfPoints(*transformed_cloud)
|
||||
: std::make_unique<sensor_msgs::msg::PointCloud2>(*transformed_cloud);
|
||||
|
||||
if (enable_self_crop_ && self_crop_apply_to_localization_) {
|
||||
auto localization_msg = removeSelfPoints(*transformed_cloud);
|
||||
pandar_points_localization->publish(std::move(localization_msg));
|
||||
} else {
|
||||
pandar_points_localization->publish(std::move(transformed_cloud));
|
||||
}
|
||||
|
||||
pandar_points_perception->publish(std::move(perception_msg));
|
||||
}
|
||||
}
|
||||
|
||||
bool PointCloudInterface::getFieldOffset(
|
||||
const sensor_msgs::msg::PointCloud2 & cloud, const std::string & field_name,
|
||||
uint32_t & offset) const
|
||||
{
|
||||
for (const auto & field : cloud.fields) {
|
||||
if (field.name == field_name && field.datatype == sensor_msgs::msg::PointField::FLOAT32) {
|
||||
offset = field.offset;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<sensor_msgs::msg::PointCloud2> PointCloudInterface::removeSelfPoints(
|
||||
const sensor_msgs::msg::PointCloud2 & input)
|
||||
{
|
||||
uint32_t x_offset = 0;
|
||||
uint32_t y_offset = 0;
|
||||
uint32_t z_offset = 0;
|
||||
if (!getFieldOffset(input, "x", x_offset) || !getFieldOffset(input, "y", y_offset) ||
|
||||
!getFieldOffset(input, "z", z_offset)) {
|
||||
RCLCPP_WARN_THROTTLE(
|
||||
get_logger(), *get_clock(), 5000,
|
||||
"Self crop skipped because PointCloud2 does not have FLOAT32 x/y/z fields");
|
||||
return std::make_unique<sensor_msgs::msg::PointCloud2>(input);
|
||||
}
|
||||
|
||||
const size_t point_count = static_cast<size_t>(input.width) * static_cast<size_t>(input.height);
|
||||
const uint32_t max_field_end =
|
||||
std::max(std::max(x_offset, y_offset), z_offset) + static_cast<uint32_t>(sizeof(float));
|
||||
const size_t expected_size = static_cast<size_t>(input.row_step) * input.height;
|
||||
if (input.point_step == 0 || input.row_step < input.width * input.point_step ||
|
||||
max_field_end > input.point_step || input.data.size() < expected_size) {
|
||||
RCLCPP_WARN_THROTTLE(
|
||||
get_logger(), *get_clock(), 5000,
|
||||
"Self crop skipped because PointCloud2 data layout is invalid");
|
||||
return std::make_unique<sensor_msgs::msg::PointCloud2>(input);
|
||||
}
|
||||
|
||||
auto output = std::make_unique<sensor_msgs::msg::PointCloud2>();
|
||||
output->header = input.header;
|
||||
output->fields = input.fields;
|
||||
output->is_bigendian = input.is_bigendian;
|
||||
output->point_step = input.point_step;
|
||||
output->is_dense = input.is_dense;
|
||||
output->height = 1;
|
||||
output->data.resize(input.data.size());
|
||||
|
||||
size_t output_size = 0;
|
||||
size_t removed_count = 0;
|
||||
for (uint32_t row = 0; row < input.height; ++row) {
|
||||
const size_t row_offset = static_cast<size_t>(row) * input.row_step;
|
||||
for (uint32_t col = 0; col < input.width; ++col) {
|
||||
const size_t input_offset = row_offset + static_cast<size_t>(col) * input.point_step;
|
||||
float x = 0.0F;
|
||||
float y = 0.0F;
|
||||
float z = 0.0F;
|
||||
std::memcpy(&x, &input.data[input_offset + x_offset], sizeof(float));
|
||||
std::memcpy(&y, &input.data[input_offset + y_offset], sizeof(float));
|
||||
std::memcpy(&z, &input.data[input_offset + z_offset], sizeof(float));
|
||||
|
||||
const bool inside_self_box =
|
||||
std::isfinite(x) && std::isfinite(y) && std::isfinite(z) &&
|
||||
x >= self_crop_box_.min_x && x <= self_crop_box_.max_x &&
|
||||
y >= self_crop_box_.min_y && y <= self_crop_box_.max_y &&
|
||||
z >= self_crop_box_.min_z && z <= self_crop_box_.max_z;
|
||||
if (inside_self_box) {
|
||||
++removed_count;
|
||||
continue;
|
||||
}
|
||||
|
||||
std::memcpy(&output->data[output_size], &input.data[input_offset], input.point_step);
|
||||
output_size += input.point_step;
|
||||
}
|
||||
}
|
||||
|
||||
output->data.resize(output_size);
|
||||
output->width = static_cast<uint32_t>(output_size / output->point_step);
|
||||
output->row_step = static_cast<uint32_t>(output->data.size());
|
||||
|
||||
RCLCPP_DEBUG_THROTTLE(
|
||||
get_logger(), *get_clock(), 5000, "Self crop removed %zu/%zu points", removed_count,
|
||||
point_count);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
PointCloudInterface::~PointCloudInterface() {}
|
||||
|
||||
RCLCPP_COMPONENTS_REGISTER_NODE(PointCloudInterface)
|
||||
@@ -0,0 +1,115 @@
|
||||
cmake_minimum_required(VERSION 3.8)
|
||||
project(timoo_ros2_driver VERSION 0.0.1 LANGUAGES CXX C)
|
||||
|
||||
# Default to C++17 (组件需要)
|
||||
if(NOT CMAKE_CXX_STANDARD)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
endif()
|
||||
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS on)
|
||||
|
||||
# Fix MPI/VTK issues by setting policies
|
||||
if(POLICY CMP0144)
|
||||
cmake_policy(SET CMP0144 NEW)
|
||||
endif()
|
||||
|
||||
# Find MPI first with proper configuration
|
||||
find_package(MPI REQUIRED)
|
||||
|
||||
# Find dependencies
|
||||
find_package(ament_cmake REQUIRED)
|
||||
find_package(ament_cmake_ros REQUIRED)
|
||||
find_package(rclcpp REQUIRED)
|
||||
find_package(rclcpp_components REQUIRED)
|
||||
find_package(std_msgs REQUIRED)
|
||||
find_package(sensor_msgs REQUIRED)
|
||||
find_package(PCL REQUIRED)
|
||||
find_package(yaml-cpp REQUIRED)
|
||||
find_package(pcl_conversions REQUIRED)
|
||||
find_package(driver_timestamp_sync REQUIRED)
|
||||
|
||||
#=======================================
|
||||
# Custom Point Type (XYZI,XYZIRT)
|
||||
#=======================================
|
||||
set(POINT_TYPE XYZIRT)
|
||||
|
||||
#========================
|
||||
# Point Type Definition
|
||||
#========================
|
||||
if(${POINT_TYPE} STREQUAL "XYZI")
|
||||
add_definitions(-DPOINT_TYPE_XYZI)
|
||||
message(STATUS "Point type is XYZI")
|
||||
elseif(${POINT_TYPE} STREQUAL "XYZIRT")
|
||||
add_definitions(-DPOINT_TYPE_XYZIRT)
|
||||
message(STATUS "Point type is XYZIRT")
|
||||
endif()
|
||||
|
||||
# Include directories
|
||||
include_directories(
|
||||
include
|
||||
${PCL_INCLUDE_DIRS}
|
||||
${YAML_CPP_INCLUDE_DIR}
|
||||
/opt/timoo_lidar_driver/include
|
||||
)
|
||||
|
||||
link_directories(${PCL_LIBRARY_DIRS})
|
||||
add_definitions(${PCL_DEFINITIONS})
|
||||
|
||||
#===============================
|
||||
# Create component library (ROS2插件)
|
||||
#===============================
|
||||
add_library(${PROJECT_NAME} SHARED
|
||||
src/timoo_driver_node.cpp
|
||||
)
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
$<INSTALL_INTERFACE:include>
|
||||
)
|
||||
|
||||
ament_target_dependencies(${PROJECT_NAME}
|
||||
rclcpp
|
||||
rclcpp_components
|
||||
std_msgs
|
||||
sensor_msgs
|
||||
pcl_conversions
|
||||
driver_timestamp_sync
|
||||
)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
yaml-cpp
|
||||
${PCL_LIBRARIES}
|
||||
/opt/timoo_lidar_driver/lib/libtimoo_driver.so
|
||||
/opt/timoo_lidar_driver/lib/libtimoo_input.so
|
||||
/opt/timoo_lidar_driver/lib/libtimoo_data_parser.so
|
||||
/opt/timoo_lidar_driver/lib/libtimoo_func.so
|
||||
)
|
||||
|
||||
# Register component (ROS2插件注册)
|
||||
rclcpp_components_register_nodes(${PROJECT_NAME} "timoo_ros2_driver::TimooDriverNode")
|
||||
|
||||
#===============================
|
||||
# Install
|
||||
#===============================
|
||||
install(TARGETS ${PROJECT_NAME}
|
||||
EXPORT export_${PROJECT_NAME}
|
||||
LIBRARY DESTINATION lib
|
||||
ARCHIVE DESTINATION lib
|
||||
RUNTIME DESTINATION bin
|
||||
INCLUDES DESTINATION include
|
||||
)
|
||||
|
||||
install(DIRECTORY include/
|
||||
DESTINATION include
|
||||
)
|
||||
|
||||
install(DIRECTORY launch config
|
||||
DESTINATION share/${PROJECT_NAME}
|
||||
)
|
||||
|
||||
ament_export_include_directories(include)
|
||||
ament_export_libraries(${PROJECT_NAME})
|
||||
ament_export_targets(export_${PROJECT_NAME})
|
||||
|
||||
ament_package()
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
# timoo ROS2驱动
|
||||
|
||||
#### 依赖
|
||||
使用前需参照[TimooLidarDriveSDK](http://192.168.9.120/timoo-software/timoolidardriversdk) 对驱动库进行安装
|
||||
|
||||
#### 操作方式
|
||||
|
||||
```bash
|
||||
# 激活环境
|
||||
source /opt/ros/eloquent/setup.bash
|
||||
# 使用SDk
|
||||
export CMAKE_PREFIX_PATH=$CMAKE_PREFIX_PATH:/opt/timoo_lidar_driver
|
||||
# 编译
|
||||
colcon build
|
||||
# 启动驱动(3雷达驱动)
|
||||
source install/setup.bash && ros2 launch timoo_ros2_driver multi_lidar.launch.py
|
||||
# 启动驱动(单雷达驱动)
|
||||
source install/setup.bash && ros2 launch timoo_ros2_driver timoo.launch.py
|
||||
|
||||
#另开终端激活环境
|
||||
source /opt/ros/eloquent/setup.bash
|
||||
# 启动rviz
|
||||
rviz2 -f timoo
|
||||
```
|
||||
@@ -0,0 +1,49 @@
|
||||
# Timoo Lidar Driver Parameters
|
||||
# 从该YAML文件加载所有驱动参数
|
||||
|
||||
/**:
|
||||
ros__parameters:
|
||||
# ============================================
|
||||
# LiDAR连接参数
|
||||
# ============================================
|
||||
udp_port: 2368 # LiDAR数据UDP端口
|
||||
status_port: 8603 # 设备状态UDP端口
|
||||
imu_port: 65535 # IMU数据UDP端口
|
||||
host_ip: "192.168.1.106" # 本机IP地址
|
||||
|
||||
# ============================================
|
||||
# LiDAR型号配置
|
||||
# 可选值: TIMOO16, TIMOO32, TIMOO1550, TIMOO1550STD, TIMOO128
|
||||
# ============================================
|
||||
lidar_type: "TIMOO32"
|
||||
|
||||
# ============================================
|
||||
# 点云处理参数
|
||||
# ============================================
|
||||
cut_angle: -1.0 # 分帧角度(弧度),-0.1表示自动分帧
|
||||
fixed_points_count: false # 是否使用固定点数量
|
||||
remove_invalid_points: true # 是否移除无效点
|
||||
|
||||
# ============================================
|
||||
# 距离过滤参数
|
||||
# ============================================
|
||||
min_distance: 0.2 # 最小探测距离(m)
|
||||
max_distance: 150.0 # 最大探测距离(m)
|
||||
|
||||
# ============================================
|
||||
# 话题配置
|
||||
# ============================================
|
||||
# 点云话题
|
||||
points_topic: "/timoo/top/pointcloud" # 点云发布话题名称
|
||||
points_frame_id: "lidar_top" # 点云消息的frame_id
|
||||
|
||||
# IMU话题
|
||||
imu_topic: "/timoo/top/imu" # IMU发布话题名称
|
||||
imu_frame_id: "imu_top" # IMU消息的frame_id
|
||||
|
||||
# ============================================
|
||||
# GNSS时间同步参数
|
||||
# ============================================
|
||||
use_gnss_time_sync: false # 是否启用GNSS时间同步
|
||||
use_packet_timestamp: false # false: 点云header使用ROS接收时间,避免未同步雷达包时间导致TF外推
|
||||
gnss_time_ref_topic: "/nmea_navsat_driver/time_reference" # GNSS时间参考话题
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
// Copyright 2026 FaRui
|
||||
|
||||
#ifndef TIMOO_ROS2_DRIVER__TIMOO_DRIVER_NODE_HPP_
|
||||
#define TIMOO_ROS2_DRIVER__TIMOO_DRIVER_NODE_HPP_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <rclcpp/rclcpp.hpp>
|
||||
#include <sensor_msgs/msg/point_cloud2.hpp>
|
||||
#include <sensor_msgs/msg/imu.hpp>
|
||||
#include <sensor_msgs/msg/time_reference.hpp>
|
||||
#include <pcl_conversions/pcl_conversions.h>
|
||||
#include <pcl/io/pcd_io.h>
|
||||
#include "driver_timestamp_sync/gnss_time_anchor.hpp"
|
||||
|
||||
// Timoo SDK forward declarations
|
||||
namespace timoo {
|
||||
namespace driver {
|
||||
class TimooDriver;
|
||||
namespace base {
|
||||
struct TimooPointCloud;
|
||||
struct IMUData;
|
||||
using TimooPointCloudPtr = std::shared_ptr<TimooPointCloud>;
|
||||
using TimooIMUPtr = std::shared_ptr<IMUData>;
|
||||
} // namespace base
|
||||
} // namespace driver
|
||||
} // namespace timoo
|
||||
|
||||
namespace timoo_ros2_driver
|
||||
{
|
||||
|
||||
// 自定义点类型定义
|
||||
struct TimooPointXYZIRT {
|
||||
PCL_ADD_POINT4D;
|
||||
float intensity;
|
||||
uint16_t ring = 0;
|
||||
float time = 0.0F;
|
||||
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
|
||||
} EIGEN_ALIGN16;
|
||||
|
||||
class TimooDriverNode : public rclcpp::Node
|
||||
{
|
||||
public:
|
||||
explicit TimooDriverNode(const rclcpp::NodeOptions & options);
|
||||
~TimooDriverNode();
|
||||
|
||||
private:
|
||||
// 参数声明
|
||||
void declare_parameters();
|
||||
void load_parameters();
|
||||
|
||||
// GNSS时间同步设置
|
||||
void setup_gnss_time_sync();
|
||||
|
||||
// 获取时间戳(考虑GNSS同步)
|
||||
rclcpp::Time get_timestamp();
|
||||
rclcpp::Time get_pointcloud_timestamp(double packet_timestamp_sec) const;
|
||||
rclcpp::Time to_ros_time(double timestamp_sec) const;
|
||||
|
||||
// 回调函数
|
||||
void on_point_cloud(const timoo::driver::base::TimooPointCloudPtr & points_data);
|
||||
void on_imu_data(const timoo::driver::base::TimooIMUPtr & imu_data);
|
||||
void on_gnss_time_ref(const sensor_msgs::msg::TimeReference::SharedPtr msg);
|
||||
|
||||
// 点云处理模板函数
|
||||
template <typename PointT>
|
||||
void process_point_cloud(const timoo::driver::base::TimooPointCloudPtr & points_data);
|
||||
|
||||
// 参数
|
||||
struct {
|
||||
double cut_angle = 360.0;
|
||||
int udp_port = 2368;
|
||||
int status_port = 8603;
|
||||
int imu_port = 7788;
|
||||
bool fixed_points_count = false;
|
||||
bool remove_invalid_points = false;
|
||||
std::string lidar_type = "TIMOO32";
|
||||
std::string host_ip = "192.168.1.106";
|
||||
double min_distance = 0.2;
|
||||
double max_distance = 150.0;
|
||||
double min_angle = 0.0;
|
||||
double max_angle = 360.0;
|
||||
std::string points_topic = "/timoo/top/pointcloud";
|
||||
std::string points_frame_id = "timoo";
|
||||
std::string imu_topic = "imu_data";
|
||||
std::string imu_frame_id = "lidar_top";
|
||||
bool use_gnss_time_sync = false;
|
||||
bool use_packet_timestamp = true;
|
||||
std::string gnss_time_ref_topic = "/nmea_navsat_driver/time_reference";
|
||||
} params_;
|
||||
|
||||
// 发布者
|
||||
rclcpp::Publisher<sensor_msgs::msg::PointCloud2>::SharedPtr points_pub_;
|
||||
rclcpp::Publisher<sensor_msgs::msg::Imu>::SharedPtr imu_pub_;
|
||||
|
||||
// GNSS时间同步
|
||||
std::shared_ptr<driver_timestamp_sync::GnssTimeAnchor> gnss_anchor_;
|
||||
rclcpp::Subscription<sensor_msgs::msg::TimeReference>::SharedPtr gnss_time_sub_;
|
||||
|
||||
// Timoo驱动
|
||||
std::unique_ptr<timoo::driver::TimooDriver> timoo_driver_;
|
||||
|
||||
// 运行状态
|
||||
bool is_running_ = false;
|
||||
};
|
||||
|
||||
} // namespace timoo_ros2_driver
|
||||
|
||||
#endif // TIMOO_ROS2_DRIVER__TIMOO_DRIVER_NODE_HPP_
|
||||
@@ -0,0 +1,86 @@
|
||||
from launch import LaunchDescription
|
||||
from launch_ros.actions import Node
|
||||
from launch.actions import DeclareLaunchArgument, TimerAction
|
||||
from launch.substitutions import LaunchConfiguration
|
||||
import os
|
||||
|
||||
def generate_launch_description():
|
||||
os.environ['LD_LIBRARY_PATH'] = f"/opt/timoo_lidar_driver/lib:{os.environ.get('LD_LIBRARY_PATH', '')}"
|
||||
|
||||
return LaunchDescription([
|
||||
# 雷达1 - 立即启动
|
||||
Node(
|
||||
package='timoo_ros2_driver',
|
||||
node_executable='tm_ros_driver_node',
|
||||
node_name='tm_ros_driver_node',
|
||||
node_namespace='lidar1',
|
||||
output='screen',
|
||||
parameters=[{
|
||||
'cut_angle': -0.1,
|
||||
'min_distance': 0.2,
|
||||
'max_distance': 150.0,
|
||||
'udp_port': 2368,
|
||||
'status_port': 8603,
|
||||
'imu_port': 65535,
|
||||
'fixed_points_count': True,
|
||||
'remove_invalid_points': False,
|
||||
'lidar_type': 'TIMOO16',
|
||||
'host_ip': '192.168.1.106',
|
||||
'points_topic': 'timoo_points',
|
||||
}]
|
||||
),
|
||||
|
||||
# 雷达2 - 延迟2秒启动
|
||||
TimerAction(
|
||||
period=2.0,
|
||||
actions=[
|
||||
Node(
|
||||
package='timoo_ros2_driver',
|
||||
node_executable='tm_ros_driver_node',
|
||||
node_name='tm_ros_driver_node',
|
||||
node_namespace='lidar2',
|
||||
output='screen',
|
||||
parameters=[{
|
||||
'cut_angle': -0.1,
|
||||
'min_distance': 0.2,
|
||||
'max_distance': 150.0,
|
||||
'udp_port': 2369,
|
||||
'status_port': 8604,
|
||||
'imu_port': 65536,
|
||||
'fixed_points_count': True,
|
||||
'remove_invalid_points': False,
|
||||
'lidar_type': 'TIMOO16',
|
||||
'host_ip': '192.168.1.106',
|
||||
'points_topic': 'timoo_points',
|
||||
}]
|
||||
)
|
||||
]
|
||||
),
|
||||
|
||||
# 雷达3 - 延迟4秒启动
|
||||
TimerAction(
|
||||
period=4.0,
|
||||
actions=[
|
||||
Node(
|
||||
package='timoo_ros2_driver',
|
||||
node_executable='tm_ros_driver_node',
|
||||
node_name='tm_ros_driver_node',
|
||||
node_namespace='lidar3',
|
||||
output='screen',
|
||||
parameters=[{
|
||||
'cut_angle': -0.1,
|
||||
'min_distance': 0.2,
|
||||
'max_distance': 150.0,
|
||||
'udp_port': 2370,
|
||||
'status_port': 8605,
|
||||
'imu_port': 6553,
|
||||
'fixed_points_count': True,
|
||||
'remove_invalid_points': False,
|
||||
'lidar_type': 'TIMOO16',
|
||||
'host_ip': '192.168.1.106',
|
||||
'points_topic': 'timoo_points',
|
||||
}]
|
||||
)
|
||||
]
|
||||
)
|
||||
])
|
||||
@@ -0,0 +1,79 @@
|
||||
from launch import LaunchDescription
|
||||
from launch_ros.actions import ComposableNodeContainer, Node
|
||||
from launch_ros.descriptions import ComposableNode
|
||||
from launch.actions import DeclareLaunchArgument
|
||||
from launch.substitutions import LaunchConfiguration
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
import os
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
# 获取参数文件路径
|
||||
pkg_share = get_package_share_directory('timoo_ros2_driver')
|
||||
default_param_file = os.path.join(pkg_share, 'config', 'timoo_driver.param.yaml')
|
||||
|
||||
# 声明启动参数
|
||||
param_file_arg = DeclareLaunchArgument(
|
||||
'param_file',
|
||||
default_value=default_param_file,
|
||||
description='Path to the parameter file'
|
||||
)
|
||||
|
||||
# 作为组件运行的选项
|
||||
use_container_arg = DeclareLaunchArgument(
|
||||
'use_container',
|
||||
default_value='false',
|
||||
description='Whether to run as a composable node in a container'
|
||||
)
|
||||
|
||||
# 容器名称
|
||||
container_name_arg = DeclareLaunchArgument(
|
||||
'container_name',
|
||||
default_value='timoo_container',
|
||||
description='Name of the container node'
|
||||
)
|
||||
|
||||
param_file = LaunchConfiguration('param_file')
|
||||
use_container = LaunchConfiguration('use_container')
|
||||
container_name = LaunchConfiguration('container_name')
|
||||
|
||||
# 方式1: 作为独立节点运行(传统方式)
|
||||
standalone_node = Node(
|
||||
package='timoo_ros2_driver',
|
||||
executable='timoo_ros2_driver_node',
|
||||
name='timoo_ros_driver_node',
|
||||
output='screen',
|
||||
parameters=[param_file],
|
||||
condition=lambda context: not context.launch_configurations.get('use_container', 'false') == 'true'
|
||||
)
|
||||
|
||||
# 方式2: 作为组件节点在容器中运行
|
||||
# 注意:这需要与外部容器配合,或创建一个新的容器
|
||||
composable_node = ComposableNode(
|
||||
package='timoo_ros2_driver',
|
||||
plugin='timoo_ros2_driver::TimooDriverNode',
|
||||
name='timoo_ros_driver_node',
|
||||
parameters=[param_file],
|
||||
extra_arguments=[{'use_intra_process_comms': True}]
|
||||
)
|
||||
|
||||
# 创建容器(当需要时使用)
|
||||
container = ComposableNodeContainer(
|
||||
name=container_name,
|
||||
namespace='',
|
||||
package='rclcpp_components',
|
||||
executable='component_container',
|
||||
composable_node_descriptions=[composable_node],
|
||||
output='screen',
|
||||
condition=lambda context: context.launch_configurations.get('use_container', 'false') == 'true'
|
||||
)
|
||||
|
||||
return LaunchDescription([
|
||||
param_file_arg,
|
||||
use_container_arg,
|
||||
container_name_arg,
|
||||
# 默认使用独立节点
|
||||
standalone_node,
|
||||
# 如果使用容器,取消注释下面这行
|
||||
# container,
|
||||
])
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
from launch import LaunchDescription
|
||||
from launch_ros.actions import ComposableNodeContainer
|
||||
from launch_ros.descriptions import ComposableNode
|
||||
from launch.actions import DeclareLaunchArgument
|
||||
from launch.substitutions import LaunchConfiguration
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
import os
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
"""Generate launch description for Timoo Lidar Driver as a composable node."""
|
||||
# 获取参数文件路径
|
||||
pkg_share = get_package_share_directory('timoo_ros2_driver')
|
||||
default_param_file = os.path.join(pkg_share, 'config', 'timoo_driver.param.yaml')
|
||||
|
||||
# 声明启动参数
|
||||
param_file_arg = DeclareLaunchArgument(
|
||||
'param_file',
|
||||
default_value=default_param_file,
|
||||
description='Path to the parameter file'
|
||||
)
|
||||
|
||||
container_name_arg = DeclareLaunchArgument(
|
||||
'container_name',
|
||||
default_value='timoo_driver_container',
|
||||
description='Name of the container node'
|
||||
)
|
||||
|
||||
param_file = LaunchConfiguration('param_file')
|
||||
container_name = LaunchConfiguration('container_name')
|
||||
|
||||
# 创建组件节点
|
||||
timoo_composable_node = ComposableNode(
|
||||
package='timoo_ros2_driver',
|
||||
plugin='timoo_ros2_driver::TimooDriverNode',
|
||||
name='timoo_ros_driver_node',
|
||||
# 从YAML文件加载所有参数
|
||||
parameters=[param_file],
|
||||
extra_arguments=[{'use_intra_process_comms': True}]
|
||||
)
|
||||
|
||||
# 创建容器
|
||||
container = ComposableNodeContainer(
|
||||
name=container_name,
|
||||
namespace='',
|
||||
package='rclcpp_components',
|
||||
executable='component_container',
|
||||
composable_node_descriptions=[timoo_composable_node],
|
||||
output='screen',
|
||||
)
|
||||
|
||||
return LaunchDescription([
|
||||
param_file_arg,
|
||||
container_name_arg,
|
||||
container,
|
||||
])
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
from launch import LaunchDescription
|
||||
from launch_ros.actions import LoadComposableNodes
|
||||
from launch_ros.descriptions import ComposableNode
|
||||
from launch.actions import DeclareLaunchArgument
|
||||
from launch.substitutions import LaunchConfiguration
|
||||
from ament_index_python.packages import get_package_share_directory
|
||||
import os
|
||||
|
||||
|
||||
def generate_launch_description():
|
||||
"""Generate launch description to load Timoo Driver into an existing container."""
|
||||
# 获取参数文件路径
|
||||
pkg_share = get_package_share_directory('timoo_ros2_driver')
|
||||
default_param_file = os.path.join(pkg_share, 'config', 'timoo_driver.param.yaml')
|
||||
|
||||
# 声明启动参数
|
||||
param_file_arg = DeclareLaunchArgument(
|
||||
'param_file',
|
||||
default_value=default_param_file,
|
||||
description='Path to the parameter file'
|
||||
)
|
||||
|
||||
container_name_arg = DeclareLaunchArgument(
|
||||
'container_name',
|
||||
default_value='sensing_container',
|
||||
description='Name of the target container to load the node into'
|
||||
)
|
||||
|
||||
param_file = LaunchConfiguration('param_file')
|
||||
container_name = LaunchConfiguration('container_name')
|
||||
|
||||
# 创建加载组件的动作
|
||||
load_composable_nodes = LoadComposableNodes(
|
||||
target_container=container_name,
|
||||
composable_node_descriptions=[
|
||||
ComposableNode(
|
||||
package='timoo_ros2_driver',
|
||||
plugin='timoo_ros2_driver::TimooDriverNode',
|
||||
name='timoo_ros_driver_node',
|
||||
# 从YAML文件加载所有参数
|
||||
parameters=[param_file],
|
||||
extra_arguments=[{'use_intra_process_comms': True}]
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
return LaunchDescription([
|
||||
param_file_arg,
|
||||
container_name_arg,
|
||||
load_composable_nodes,
|
||||
])
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
|
||||
<package format="3">
|
||||
<name>timoo_ros2_driver</name>
|
||||
<version>0.0.1</version>
|
||||
<description>The timoo_ros2_driver package</description>
|
||||
<maintainer email="gongjie@tm.com">tm</maintainer>
|
||||
<license>TODO</license>
|
||||
<buildtool_depend>ament_cmake_ros</buildtool_depend>
|
||||
|
||||
<depend>rclcpp</depend>
|
||||
<depend>rclcpp_components</depend>
|
||||
<depend>sensor_msgs</depend>
|
||||
<depend>pcl_conversions</depend>
|
||||
<depend>libpcl-all-dev</depend>
|
||||
<depend>driver_timestamp_sync</depend>
|
||||
|
||||
<test_depend>ament_lint_auto</test_depend>
|
||||
<test_depend>ament_lint_common</test_depend>
|
||||
<export>
|
||||
<build_type>ament_cmake</build_type>
|
||||
</export>
|
||||
</package>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user