Skip to content

Commit 34147ae

Browse files
feat(camera): add external camera streaming with tests and ci-cd
Signed-off-by: charlesmadjeri <studies@madjeri.com>
1 parent 5831828 commit 34147ae

19 files changed

Lines changed: 1349 additions & 96 deletions

.github/workflows/ci.yml

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main, dev ]
6+
pull_request:
7+
branches: [ main, dev ]
8+
9+
jobs:
10+
lint:
11+
name: Lint
12+
runs-on: ubuntu-latest
13+
container:
14+
image: osrf/ros:humble-desktop
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- name: Install dependencies
19+
run: |
20+
apt-get update && apt-get install -y \
21+
python3-pip \
22+
python3-pytest \
23+
python3-pytest-mock \
24+
python3-opencv \
25+
gstreamer1.0-tools \
26+
gstreamer1.0-plugins-base \
27+
gstreamer1.0-plugins-good \
28+
gstreamer1.0-plugins-bad \
29+
gstreamer1.0-plugins-ugly \
30+
v4l-utils \
31+
build-essential \
32+
cmake \
33+
git \
34+
&& rm -rf /var/lib/apt/lists/*
35+
36+
- name: Setup workspace structure
37+
run: |
38+
mkdir -p ws/src
39+
cp -r camera_ros lucy_bringup ws/src/
40+
41+
- name: Build workspace
42+
working-directory: /__w/lucy_ros_packages/lucy_ros_packages/ws
43+
shell: bash
44+
run: |
45+
source /opt/ros/humble/setup.bash
46+
colcon build --packages-select camera_ros lucy_bringup --cmake-args -DBUILD_TESTING=ON
47+
48+
- name: Run linters
49+
working-directory: /__w/lucy_ros_packages/lucy_ros_packages/ws
50+
shell: bash
51+
run: |
52+
source /opt/ros/humble/setup.bash
53+
source install/setup.bash
54+
colcon test --packages-select camera_ros lucy_bringup --event-handlers console_direct+ || true
55+
56+
- name: Check lint results
57+
working-directory: /__w/lucy_ros_packages/lucy_ros_packages/ws
58+
shell: bash
59+
run: |
60+
source /opt/ros/humble/setup.bash
61+
source install/setup.bash
62+
colcon test-result --verbose || true
63+
64+
test:
65+
name: Unit Tests
66+
runs-on: ubuntu-latest
67+
container:
68+
image: osrf/ros:humble-desktop
69+
steps:
70+
- uses: actions/checkout@v4
71+
72+
- name: Install dependencies
73+
run: |
74+
apt-get update && apt-get install -y \
75+
python3-pip \
76+
python3-pytest \
77+
python3-pytest-mock \
78+
python3-pytest-cov \
79+
python3-opencv \
80+
gstreamer1.0-tools \
81+
gstreamer1.0-plugins-base \
82+
gstreamer1.0-plugins-good \
83+
gstreamer1.0-plugins-bad \
84+
gstreamer1.0-plugins-ugly \
85+
v4l-utils \
86+
build-essential \
87+
cmake \
88+
git \
89+
&& rm -rf /var/lib/apt/lists/*
90+
91+
- name: Install Python test dependencies
92+
run: |
93+
pip3 install --no-cache-dir pytest pytest-mock pytest-cov
94+
95+
- name: Setup workspace structure
96+
run: |
97+
mkdir -p ws/src
98+
cp -r camera_ros lucy_bringup ws/src/
99+
100+
- name: Build workspace with tests
101+
working-directory: /__w/lucy_ros_packages/lucy_ros_packages/ws
102+
shell: bash
103+
run: |
104+
source /opt/ros/humble/setup.bash
105+
colcon build --packages-select camera_ros lucy_bringup --cmake-args -DBUILD_TESTING=ON
106+
107+
- name: Run unit tests
108+
working-directory: /__w/lucy_ros_packages/lucy_ros_packages/ws
109+
shell: bash
110+
run: |
111+
source /opt/ros/humble/setup.bash
112+
source install/setup.bash
113+
colcon test --packages-select camera_ros lucy_bringup --event-handlers console_direct+
114+
115+
- name: Check test results
116+
working-directory: /__w/lucy_ros_packages/lucy_ros_packages/ws
117+
shell: bash
118+
run: |
119+
source /opt/ros/humble/setup.bash
120+
source install/setup.bash
121+
colcon test-result --verbose
122+
# Fail if any tests failed
123+
if colcon test-result --verbose | grep -q "failed"; then
124+
echo "Some tests failed!"
125+
exit 1
126+
fi
127+
128+
- name: Generate coverage report
129+
working-directory: /__w/lucy_ros_packages/lucy_ros_packages/ws
130+
shell: bash
131+
run: |
132+
source /opt/ros/humble/setup.bash
133+
source install/setup.bash
134+
cd build/camera_ros
135+
python3 -m pytest ../../src/camera_ros/test/ \
136+
--cov=../../src/camera_ros/scripts \
137+
--cov-report=xml:coverage.xml \
138+
--cov-report=html:htmlcov \
139+
--cov-report=term-missing \
140+
--cov-branch || true
141+
142+
- name: Upload coverage to Codecov
143+
uses: codecov/codecov-action@v3
144+
with:
145+
file: ${{ github.workspace }}/ws/build/camera_ros/coverage.xml
146+
flags: unittests
147+
name: codecov-umbrella
148+
fail_ci_if_error: false
149+
150+
- name: Upload coverage HTML report
151+
uses: actions/upload-artifact@v4
152+
if: always()
153+
with:
154+
name: coverage-report
155+
path: ${{ github.workspace }}/ws/build/camera_ros/htmlcov/
156+
retention-days: 30

.gitignore

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,57 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
.Python
7+
*.egg-info/
8+
dist/
9+
build/
10+
.eggs/
11+
12+
# Virtual environments
113
.venv
14+
venv/
15+
ENV/
16+
env/
17+
18+
# Coverage reports
19+
htmlcov/
20+
.coverage
21+
.coverage.*
22+
coverage.xml
23+
*.cover
24+
.hypothesis/
25+
26+
# Test artifacts
27+
.pytest_cache/
28+
test_results/
29+
*.xunit.xml
30+
.tox/
31+
.nox/
32+
33+
# ROS2 build artifacts
34+
install/
35+
log/
36+
build/
37+
38+
# IDE
39+
.vscode/
40+
.idea/
41+
*.swp
42+
*.swo
43+
*~
44+
.DS_Store
45+
46+
# OS
47+
Thumbs.db
48+
.DS_Store
49+
50+
# ROS2 specific
51+
.ros/
52+
53+
# Temporary files
54+
*.tmp
55+
*.log
56+
*.bak
57+
*.orig

camera_ros/CMakeLists.txt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ rosidl_generate_interfaces(${PROJECT_NAME}
2525
# Install Python scripts
2626
install(PROGRAMS
2727
scripts/camera_publisher.py
28+
scripts/camera_stream_controller.py
2829
DESTINATION lib/${PROJECT_NAME}
2930
)
3031

@@ -54,7 +55,38 @@ install(FILES
5455

5556
if(BUILD_TESTING)
5657
find_package(ament_lint_auto REQUIRED)
58+
find_package(ament_cmake_pytest REQUIRED)
5759
ament_lint_auto_find_test_dependencies()
60+
61+
# Install test files
62+
install(DIRECTORY test
63+
DESTINATION share/${PROJECT_NAME}
64+
)
65+
66+
# Install pytest.ini for coverage configuration
67+
install(FILES
68+
pytest.ini
69+
DESTINATION share/${PROJECT_NAME}
70+
)
71+
72+
# Install coverage generation script
73+
install(PROGRAMS
74+
scripts/generate_coverage.sh
75+
DESTINATION lib/${PROJECT_NAME}
76+
)
77+
78+
# Add pytest tests with coverage
79+
ament_add_pytest_test(test_camera_publisher test/test_camera_publisher.py
80+
TIMEOUT 60
81+
PYTHON_EXECUTABLE "${PYTHON3_EXECUTABLE}"
82+
APPEND_ENV PYTHONPATH="${CMAKE_CURRENT_SOURCE_DIR}/scripts:${CMAKE_CURRENT_SOURCE_DIR}"
83+
)
84+
85+
ament_add_pytest_test(test_camera_stream_controller test/test_camera_stream_controller.py
86+
TIMEOUT 60
87+
PYTHON_EXECUTABLE "${PYTHON3_EXECUTABLE}"
88+
APPEND_ENV PYTHONPATH="${CMAKE_CURRENT_SOURCE_DIR}/scripts:${CMAKE_CURRENT_SOURCE_DIR}"
89+
)
5890
endif()
5991

6092
# Export the generated interfaces

camera_ros/camera_ros/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"""
1717
Camera ROS Package.
1818
19-
Zero-copy MJPEG camera publisher optimized for NVIDIA Jetson AGX Orin."""
19+
Zero-copy MJPEG camera publisher optimized for NVIDIA Jetson AGX Orin.
20+
"""
2021

2122
__version__ = "1.0.0"

camera_ros/launch/camera.launch.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,21 @@
1717
"""
1818
Launch file for zero-copy MJPEG camera publisher.
1919
20-
This launch file starts the camera node with optimized settings for NVIDIA Jetson AGX Orin."""
20+
This launch file starts the camera node with optimized settings for
21+
NVIDIA Jetson AGX Orin.
22+
"""
2123

2224
from launch import LaunchDescription
2325
from launch_ros.actions import Node
2426
from launch.actions import DeclareLaunchArgument, LogInfo
2527
from launch.substitutions import LaunchConfiguration
2628

29+
2730
def generate_launch_description():
2831
# Launch arguments
2932
fps_arg = DeclareLaunchArgument(
3033
'fps',
31-
default_value='15.0',
34+
default_value='10.0',
3235
description='Camera frame rate (1.0-30.0 FPS)'
3336
)
3437

@@ -38,6 +41,24 @@ def generate_launch_description():
3841
description='Camera device path'
3942
)
4043

44+
vendor_id_arg = DeclareLaunchArgument(
45+
'vendor_id',
46+
default_value='',
47+
description='USB vendor ID for camera identification (e.g., 0x046d)'
48+
)
49+
50+
product_id_arg = DeclareLaunchArgument(
51+
'product_id',
52+
default_value='',
53+
description='USB product ID for camera identification'
54+
)
55+
56+
serial_number_arg = DeclareLaunchArgument(
57+
'serial_number',
58+
default_value='',
59+
description='Camera serial number for identification'
60+
)
61+
4162
# Camera node
4263
camera_node = Node(
4364
package='camera_ros',
@@ -47,12 +68,30 @@ def generate_launch_description():
4768
parameters=[{
4869
'fps': LaunchConfiguration('fps'),
4970
'device': LaunchConfiguration('device'),
71+
'vendor_id': LaunchConfiguration('vendor_id'),
72+
'product_id': LaunchConfiguration('product_id'),
73+
'serial_number': LaunchConfiguration('serial_number'),
5074
}],
5175
)
5276

77+
# Camera stream controller node
78+
camera_controller_node = Node(
79+
package='camera_ros',
80+
executable='camera_stream_controller.py',
81+
name='camera_stream_controller',
82+
output='screen',
83+
)
84+
5385
return LaunchDescription([
5486
fps_arg,
5587
device_arg,
56-
LogInfo(msg='Starting zero-copy MJPEG camera publisher with automatic client-based activation'),
88+
vendor_id_arg,
89+
product_id_arg,
90+
serial_number_arg,
91+
LogInfo(
92+
msg='Starting zero-copy MJPEG camera publisher with '
93+
'service-based streaming control'
94+
),
5795
camera_node,
96+
camera_controller_node,
5897
])

camera_ros/package.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828

2929
<test_depend>ament_lint_auto</test_depend>
3030
<test_depend>ament_lint_common</test_depend>
31+
<test_depend>ament_cmake_pytest</test_depend>
32+
<test_depend>python3-pytest</test_depend>
33+
<test_depend>python3-pytest-mock</test_depend>
34+
<test_depend>python3-pytest-cov</test_depend>
3135

3236
<member_of_group>rosidl_interface_packages</member_of_group>
3337

camera_ros/pytest.ini

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[pytest]
2+
# Pytest configuration for camera_ros package
3+
testpaths = test
4+
python_files = test_*.py
5+
python_classes = Test*
6+
python_functions = test_*
7+
addopts =
8+
-v
9+
--strict-markers
10+
--tb=short
11+
--cov=scripts
12+
--cov-report=term-missing
13+
--cov-report=html:htmlcov
14+
--cov-report=xml:coverage.xml
15+
--cov-branch
16+
--no-cov-on-fail
17+

0 commit comments

Comments
 (0)