Skip to content
Merged

Dev #32

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
b23917e
feat: move lucy_msgs package into lucy_ros_packages
charlesmadjeri Apr 14, 2026
76285cd
feat(lucy_msgs): add ConfigurePipeline action definition
charlesmadjeri Apr 14, 2026
d9db37e
feat(lucy_msgs): add config management service definitions
charlesmadjeri Apr 14, 2026
30aab9c
fix(lucy_msgs): use generic robot package name in interface comments
charlesmadjeri Apr 14, 2026
bdfca2b
Merge pull request #12 from Sentience-Robotics/cma/config-pipeline-lu…
charlesmadjeri Apr 15, 2026
1cf863e
feat(uros config pipeline): add basic architecture
m-brl Apr 4, 2026
21e353c
feat(lucy_config_generator): add package, generation parity, and scop…
charlesmadjeri Apr 18, 2026
12359a0
Merge pull request #14 from Sentience-Robotics/mbo/#92/feature-genera…
charlesmadjeri Apr 19, 2026
d225bdd
Merge pull request #13 from Sentience-Robotics/cma/config-pipeline-ya…
charlesmadjeri Apr 25, 2026
7f0244c
feat(lucy_config_pipeline): add config services and pipeline action s…
charlesmadjeri Apr 25, 2026
5d8ee49
feat(lucy_ros2_control): read joint params from HardwareInfo
charlesmadjeri Apr 25, 2026
f2aa0b2
evol(lucy_config_generator): board internal_servo_slots and validatio…
charlesmadjeri Apr 25, 2026
cb9c906
evol(lucy_config_pipeline): JSON error format and action/service inte…
charlesmadjeri Apr 25, 2026
4a6a684
fix(ci): remove obsolete lucy_ros2_control coverage path after #98 re…
charlesmadjeri Apr 25, 2026
46e7a02
feat(lucy_config_pipeline): add firmware build phase and refactor pac…
charlesmadjeri Apr 25, 2026
d2e6f02
feat(lucy_config_pipeline): add RP2040 flash phase with post-flash ch…
charlesmadjeri Apr 26, 2026
4ae404b
Merge pull request #17 from Sentience-Robotics/cma/feat-pipeline-buil…
charlesmadjeri Apr 26, 2026
b8cf40e
Merge pull request #16 from Sentience-Robotics/cma/config-pipeline-ro…
charlesmadjeri Apr 26, 2026
65fd2a4
Merge pull request #15 from Sentience-Robotics/cma/config-pipeline-se…
charlesmadjeri Apr 26, 2026
8d381d6
fix(lucy_config_pipeline): rename python package dir to src and updat…
charlesmadjeri Apr 26, 2026
d0b3699
Merge pull request #18 from Sentience-Robotics/cma/feat-pipeline-flas…
charlesmadjeri Apr 27, 2026
9075e52
evol(config-pipeline): adapt and improve for control panel compatibility
charlesmadjeri May 10, 2026
07942d4
Merge pull request #20 from Sentience-Robotics/cma/feat-pipeline-flas…
charlesmadjeri May 12, 2026
d9c8b0e
Merge pull request #19 from Sentience-Robotics/dev-feat-config-pipeline
charlesmadjeri May 14, 2026
4e8e8c7
evol(launchfiles): refacto
charlesmadjeri May 14, 2026
107428e
Merge pull request #21 from Sentience-Robotics/cma/evol-refacto-launc…
charlesmadjeri May 16, 2026
62d1a5a
refacto(launch file): change launch responsability
m-brl May 22, 2026
7116615
Merge pull request #23 from Sentience-Robotics/aes/fix-rosbridge-conn…
Arcod7 May 22, 2026
9a6c1f1
clean(code): lint launch file
m-brl May 23, 2026
4ba4a35
Merge pull request #23 from Sentience-Robotics/aes/fix-rosbridge-conn…
Arcod7 May 22, 2026
6104a72
clean(bringup): launch configuration of gazebo and rebase dev
Arcod7 May 24, 2026
7d87519
Merge branch 'dev' into mbo/decoupling-launch-file
Arcod7 May 24, 2026
fe9c135
fix(ci): joint state publisher not used
Arcod7 May 24, 2026
e7d0b82
fix(ci): joint state publisher not used
Arcod7 May 24, 2026
5dd3c0a
fix(ci): camera stream controller should destroy
Arcod7 May 24, 2026
3419978
Merge pull request #24 from Sentience-Robotics/mbo/decoupling-launch-…
charlesmadjeri May 24, 2026
6d0508c
rm(config-generator): drop candidate_urdf_joints schema validation
charlesmadjeri May 24, 2026
374d797
feat(config-generator): emit single-controller simulation artifacts
charlesmadjeri May 25, 2026
7a4c1ff
feat(msgs): add simulation_only goal to ConfigurePipeline.action
charlesmadjeri May 25, 2026
a8694f5
feat(control-supervisor): /lucy_control/restart supervises control stack
charlesmadjeri May 25, 2026
af195b0
evol(config-pipeline): split install, reload phase, simulation_only b…
charlesmadjeri May 25, 2026
3b0589d
fix(bringup): resolve controllers.yaml from source robot_root
charlesmadjeri May 25, 2026
9a2b67a
fix(control-supervisor): publish robot_description via params YAML; s…
charlesmadjeri May 25, 2026
90a73a1
fix(config-generator): simulation_only keeps 3 JTCs; add use_mock_har…
charlesmadjeri May 25, 2026
9137f3a
feat(control-supervisor): publish latched /lucy/gazebo_running (std_m…
charlesmadjeri May 25, 2026
39d9a48
fix(config-pipeline): fix stuck second pipeline run after reload
charlesmadjeri May 25, 2026
99db27e
fix(bringup): wrap long launch import for flake8 E501
charlesmadjeri May 25, 2026
4029a79
Merge pull request #25 from Sentience-Robotics/cma/feat/sim-only-cont…
charlesmadjeri May 25, 2026
86c4088
feat(interface): add CLI interface
Mael-RABOT May 26, 2026
6211141
fix(run): add readme
Mael-RABOT May 26, 2026
40e5810
Merge pull request #26 from Sentience-Robotics/mra/feat-cli-interface
Mael-RABOT May 26, 2026
f7293ee
3d view URDF and meshes exposed via ROS (#27)
Arcod7 Jun 1, 2026
06e2c1c
feat(lucy_bringup): headless Gazebo arg and fix robot_description YAM…
charlesmadjeri Jun 1, 2026
79ca733
evol(tests): expect robot_name inmoov in config pipeline validator
charlesmadjeri Jun 1, 2026
dd785d8
evol(bringup): wrap long URDF properties.xacro path in lucy.launch.py…
charlesmadjeri Jun 1, 2026
d90becd
Merge pull request #28 from Sentience-Robotics/cma/evol-urdf-calibrated
charlesmadjeri Jun 3, 2026
4f6f18a
feat(config-ros2-control): centralize generated-file names and clamp …
charlesmadjeri Jun 4, 2026
e6a22bd
Merge pull request #29 from Sentience-Robotics/cma/feat-urdf-alignement
charlesmadjeri Jun 5, 2026
6b43694
feat(bringup): select robot description package at launch via robot_p…
charlesmadjeri Jun 7, 2026
772d813
Merge pull request #31 from Sentience-Robotics/cma/evol-bringup-robot…
charlesmadjeri Jun 8, 2026
a4b7c08
feat(camera config): add camera config generation
m-brl Jun 5, 2026
db58e4f
evol(generation): add camera and sensor generation for gazebo
m-brl Jun 8, 2026
656399f
clean(code): lint code and remove unused package
m-brl Jun 8, 2026
7f2b94d
clean(code): lint
m-brl Jun 8, 2026
3ba7ad9
fix(test): delete deprecated test
m-brl Jun 9, 2026
53748b9
wip(wip): wip
m-brl Jun 9, 2026
ecf166a
feat(gazebo): bridge sim cameras as raw and republish compressed for LCP
charlesmadjeri Jun 11, 2026
73f16e0
feat(topic): edit topic name
m-brl Jun 11, 2026
0b97adf
feat(compressed topic): add defined compressed topic
m-brl Jun 11, 2026
28713f2
Merge pull request #30 from Sentience-Robotics/mbo/feat-camera-config
charlesmadjeri Jun 11, 2026
c1ce3d0
fix(setup.cfg): setup setup
m-brl Jun 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ jobs:
cmake \
git
rosdep init || true
rosdep update
rosdep update || true

- name: Workspace layout
run: |
mkdir -p ws/src
cp -r camera_ros lucy_bringup lucy_ros2_control ws/src/
cp -r camera_ros lucy_bringup lucy_ros2_control lucy_config_generator ws/src/

# Do not `rm -rf /var/lib/apt/lists/*` before this: rosdep/apt need indexes for packages.ros.org.
# Skip keys: not in this CI tree (thais_urdf), external/third-party (audio_common), or hardware agents not needed to compile/test.
Expand All @@ -61,7 +61,7 @@ jobs:
run: |
source /opt/ros/humble/setup.bash
colcon build --symlink-install \
--packages-select camera_ros lucy_bringup lucy_ros2_control \
--packages-select camera_ros lucy_bringup lucy_ros2_control lucy_config_generator \
--cmake-args -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON

- name: colcon test
Expand All @@ -71,9 +71,9 @@ jobs:
source /opt/ros/humble/setup.bash
source install/setup.bash
colcon test \
--packages-select camera_ros lucy_bringup lucy_ros2_control \
--packages-select camera_ros lucy_bringup lucy_ros2_control lucy_config_generator \
--event-handlers console_direct+ \
--pytest-args "--tb=short -vv"
--pytest-args -vv --tb=short

- name: colcon test-result
working-directory: ws
Expand Down Expand Up @@ -106,11 +106,11 @@ jobs:
--cov-report=html:build/coverage_reports/html_lucy_bringup \
--cov-report=term-missing \
--cov-branch
# lucy_ros2_controlYAML validation tests only (no C++ coverage here)
python3 -m pytest src/lucy_ros2_control/test/ \
--cov=src/lucy_ros2_control/test \
--cov-report=xml:build/coverage_reports/lucy_ros2_control.xml \
--cov-report=html:build/coverage_reports/html_lucy_ros2_control \
# lucy_config_generatorgolden template tests
python3 -m pytest src/lucy_config_generator/test/ \
--cov=src/lucy_config_generator/lucy_config_generator \
--cov-report=xml:build/coverage_reports/lucy_config_generator.xml \
--cov-report=html:build/coverage_reports/html_lucy_config_generator \
--cov-report=term-missing \
--cov-branch

Expand All @@ -119,7 +119,7 @@ jobs:
if: success()
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ws/build/coverage_reports/camera_ros.xml,ws/build/coverage_reports/lucy_bringup.xml,ws/build/coverage_reports/lucy_ros2_control.xml
files: ws/build/coverage_reports/camera_ros.xml,ws/build/coverage_reports/lucy_bringup.xml,ws/build/coverage_reports/lucy_config_generator.xml
flags: lucy_ros_packages
name: lucy_ros_packages
fail_ci_if_error: false
Expand Down
37 changes: 20 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ ROS 2 **Humble** repository for **Lucy** (Sentience Robotics): runtime bringup,

| Package | One-line role |
|---------|----------------|
| [**lucy_bringup**](lucy_bringup/) | Jetson-oriented **system launch**: micro-ROS agents, `rosbridge_server`, RealSense, `camera_ros`, delayed [`lucy_ros2_control`](lucy_ros2_control/) bringup. |
| [**lucy_bringup**](lucy_bringup/) | Jetson **system launch**: micro-ROS agents, **`web_ros_api`** (rosbridge + **`lucy_config_pipeline`**), RealSense, `camera_ros`, delayed [`lucy_ros2_control`](lucy_ros2_control/) bringup; **`lucy_*_development`** composes the web stack with **`thais_urdf`** RViz/Gazebo. |
| [**lucy_ros2_control**](lucy_ros2_control/) | **Hardware** `ros2_control` plugin (`LucySystemHardware`), controller YAML, `control.launch.py` for the real robot stack (no RViz/rosbridge in that launch). |
| [**lucy_config_generator**](lucy_config_generator/) | **Config pipeline**: reads **`thais_urdf`** hardware YAML and emits RP2040 firmware C, `ros2_control` xacro, and `controllers.yaml` (see package README). |
| [**lucy_config_pipeline**](lucy_config_pipeline/) | **Config store + `ConfigurePipeline` action**: validate YAML, generate artifacts, build/flash RP2040 firmware via `picotool` [README](lucy_config_pipeline/README.md). |
| [**camera_ros**](camera_ros/) | GStreamer-based **MJPEG** → `sensor_msgs/CompressedImage`; client-aware activation. |

Package names match directories (`<name>` in each `package.xml`).

## How this repo fits the platform

- **Robot model, RViz, Gazebo, and “combo” launches** (real or sim + rosbridge) live in the sibling repo **[thais_urdf](https://github.com/Sentience-Robotics/thais_urdf)** (or your fork), package name `thais_urdf`. `lucy_ros2_control` expects URDF/xacro and meshes from that tree when using default paths.
- **Robot model, RViz, and Gazebo** live in the sibling repo **[thais_urdf](https://github.com/Sentience-Robotics/thais_urdf)** (package name `thais_urdf`). **`lucy_bringup`** owns **rosbridge + hardware config** (**`web_ros_api.launch.py`**) and **`lucy.launch.py`** (composition via **`real`**, **`rviz`**, **`gazebo`**). `lucy_ros2_control` expects URDF/xacro and meshes from **`thais_urdf`** when using default paths.
- **Web control panel** and teleop semantics are **not** in this repo; they consume the same topics/controllers documented in lucy_ws docs.

## Requirements
Expand All @@ -23,14 +25,18 @@ Package names match directories (`<name>` in each `package.xml`).
- **ROS**: [ROS 2 Humble](https://docs.ros.org/en/humble/Installation.html).
- **Per-package extras**: Jetson-typical USB video and audio stacks for bringup; RealSense SDK stack for `realsense2_camera`; serial devices for micro-ROS. See each package README and `lucy_bringup/REALSENSE.md`.

## Picotool and passwordless sudo

The **`lucy_config_pipeline`** flash phase runs **`sudo picotool`**. Copy-paste **sudoers** setup (Ubuntu 22.04) lives in **[lucy_config_pipeline/README.md — Passwordless sudo for picotool](lucy_config_pipeline/README.md#passwordless-sudo-for-picotool)** (same repository; no `../` path).

## Building (colcon workspace)

Treat this repository as **`src/lucy_ros_packages`** (clone the contents into that folder) **or** clone in place so that packages are direct children of your workspace `src/`:

```text
lucy_ws/
└── src/
├── lucy_ros_packages/ # this repo: lucy_bringup, lucy_ros2_control, camera_ros
├── lucy_ros_packages/ # this repo: lucy_bringup, lucy_ros2_control, lucy_config_generator, lucy_config_pipeline, camera_ros
└── thais_urdf/ # robot description + sim launches (separate repo)
```

Expand All @@ -40,15 +46,15 @@ Example build:
source /opt/ros/humble/setup.bash
cd lucy_ws
colcon build --symlink-install \
--packages-select lucy_bringup lucy_ros2_control camera_ros
--packages-select lucy_bringup lucy_ros2_control lucy_config_generator lucy_config_pipeline camera_ros
source install/setup.bash
```

To include simulation/description from the other repo:

```bash
colcon build --symlink-install \
--packages-select lucy_bringup lucy_ros2_control camera_ros thais_urdf
--packages-select lucy_bringup lucy_ros2_control lucy_config_generator lucy_config_pipeline camera_ros thais_urdf
```

## Quick start
Expand All @@ -70,11 +76,11 @@ From your **workspace root** (e.g. `lucy_ws`), with packages under `src/lucy_ros
```bash
source /opt/ros/humble/setup.bash
colcon build --symlink-install \
--packages-select camera_ros lucy_bringup lucy_ros2_control \
--packages-select camera_ros lucy_bringup lucy_ros2_control lucy_config_generator \
--cmake-args -DBUILD_TESTING=ON
source install/setup.bash

colcon test --packages-select camera_ros lucy_bringup lucy_ros2_control --event-handlers console_direct+
colcon test --packages-select camera_ros lucy_bringup lucy_ros2_control lucy_config_generator --event-handlers console_direct+
colcon test-result --verbose
```

Expand All @@ -94,31 +100,28 @@ python3 -m pytest src/lucy_ros_packages/lucy_bringup/test/ \
--cov-report=xml:build/coverage_reports/lucy_bringup.xml \
--cov-report=html:build/coverage_html/lucy_bringup

python3 -m pytest src/lucy_ros_packages/lucy_ros2_control/test/ \
--cov=src/lucy_ros_packages/lucy_ros2_control/test \
--cov-report=term-missing \
--cov-report=xml:build/coverage_reports/lucy_ros2_control.xml \
--cov-report=html:build/coverage_html/lucy_ros2_control
```

**Meaningful line coverage** is mostly from **`camera_ros/scripts`**; bringup tests only **byte-compile** launch files, and `lucy_ros2_control` tests cover **YAML + Python helpers**, not the C++ hardware plugin.
**Meaningful line coverage** is mostly from **`camera_ros/scripts`**; bringup tests only **byte-compile** launch files.

## CI

GitHub Actions (`.github/workflows/ci.yml`) runs **`rosdep`**, **`colcon build`**, **`colcon test`**, then **`pytest-cov`** over all Python tests, producing Cobertura XML under `ws/build/coverage_reports/` for **`camera_ros`**, **`lucy_bringup`**, and **`lucy_ros2_control`**. Reports are uploaded to [**Codecov**](https://codecov.io) when **`CODECOV_TOKEN`** is set on the repo, and HTML/XML are attached as workflow artifacts (`coverage-lucy_ros_packages`). See [`doc/DEVELOPER.md`](doc/DEVELOPER.md) §5.
GitHub Actions (`.github/workflows/ci.yml`) runs **`rosdep`**, **`colcon build`**, **`colcon test`**, then **`pytest-cov`** over Python tests, producing Cobertura XML under `ws/build/coverage_reports/` for **`camera_ros`**, **`lucy_bringup`**, and **`lucy_config_generator`**. Reports are uploaded to [**Codecov**](https://codecov.io) when **`CODECOV_TOKEN`** is set on the repo, and HTML/XML are attached as workflow artifacts (`coverage-lucy_ros_packages`). See [`docs/DEVELOPER.md`](docs/DEVELOPER.md) §5.

## Documentation map

| Doc | Audience |
|-----|----------|
| This file | Anyone cloning **this** repository |
| [**doc/DEVELOPER.md**](doc/DEVELOPER.md) | **Contributors** — build, CI, package internals, extension checklist |
| [**docs/DEVELOPER.md**](docs/DEVELOPER.md) | **Contributors** — build, CI, package internals, extension checklist |
| [lucy_bringup/README.md](lucy_bringup/README.md) | Operators and integrators (devices, tmux, launch args) |
| [lucy_ros2_control/README.md](lucy_ros2_control/README.md) | Control stack quick start |
| [**doc/ROS2_CONTROL.md**](doc/ROS2_CONTROL.md) | **ros2_control** — general concepts + Lucy (`LucySystemHardware`, topics, launches) |
| [**docs/ROS2_CONTROL.md**](docs/ROS2_CONTROL.md) | **ros2_control** — general concepts + Lucy (`LucySystemHardware`, topics, launches) |
| [**lucy_config_generator/README.md**](lucy_config_generator/README.md) | Hardware YAML → firmware C, `ros2_control` xacro, controllers |
| [**lucy_config_pipeline/README.md**](lucy_config_pipeline/README.md) | Config services + pipeline action (build/flash); [passwordless sudo for picotool](lucy_config_pipeline/README.md#passwordless-sudo-for-picotool) |
| [camera_ros/README.md](camera_ros/README.md) | Camera topics, parameters, troubleshooting |

If these repos live under **`lucy_ws`**, see **`lucy_ws/docs/developer_lucy_packages.md`** (index into each repo’s `doc/DEVELOPER.md`) and **`lucy_ws/docs/simulation_and_visualization.md`** (full-stack pipeline).
If these repos live under **`lucy_ws`**, see **`lucy_ws/docs/developer_lucy_packages.md`** (index into each repo’s `docs/DEVELOPER.md`) and **`lucy_ws/docs/simulation_and_visualization.md`** (full-stack pipeline).

## License

Expand Down
12 changes: 0 additions & 12 deletions camera_ros/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,6 @@ find_package(sensor_msgs REQUIRED)
find_package(std_srvs REQUIRED)
find_package(std_msgs REQUIRED)
find_package(cv_bridge REQUIRED)
find_package(rosidl_default_generators REQUIRED)

# Generate Python classes for the custom service
rosidl_generate_interfaces(${PROJECT_NAME}
"srv/GetInt.srv"
DEPENDENCIES std_msgs
ADD_LINTER_TESTS
)

# Bind-mounted / persisted build dirs can leave .../ament_cmake_python/<pkg>/<pkg> as a directory
# from an interrupted build; the next build then fails creating the symlink ("Is a directory").
if(TARGET ament_cmake_python_symlink_${PROJECT_NAME})
Expand Down Expand Up @@ -94,7 +85,4 @@ if(BUILD_TESTING)
)
endif()

# Export the generated interfaces
ament_export_dependencies(rosidl_default_runtime)

ament_package()
5 changes: 0 additions & 5 deletions camera_ros/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@
<depend>std_srvs</depend>
<depend>std_msgs</depend>
<depend>cv_bridge</depend>
<depend>rosidl_default_generators</depend>
<depend>rosidl_default_runtime</depend>

<exec_depend>python3-opencv</exec_depend>
<exec_depend>gstreamer1.0-tools</exec_depend>
<exec_depend>gstreamer1.0-plugins-good</exec_depend>
Expand All @@ -33,8 +30,6 @@
<test_depend>python3-pytest-mock</test_depend>
<test_depend>python3-pytest-cov</test_depend>

<member_of_group>rosidl_interface_packages</member_of_group>

<export>
<build_type>ament_cmake</build_type>
</export>
Expand Down
7 changes: 0 additions & 7 deletions camera_ros/scripts/camera_publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from sensor_msgs.msg import CompressedImage
from std_msgs.msg import Int32
from std_srvs.srv import SetBool
from camera_ros.srv import GetInt
import cv2

FPS = 10.0
Expand Down Expand Up @@ -97,8 +96,6 @@ def __init__(self):
# Create services for streaming control
self.create_service(SetBool, 'start_streaming', self.start_streaming_callback)
self.create_service(SetBool, 'stop_streaming', self.stop_streaming_callback)
self.create_service(GetInt, 'get_client_count', self.get_client_count)

# Subscribe to client count for automatic control
self.create_subscription(Int32, '/client_count', self.client_count_callback, 10)

Expand All @@ -107,10 +104,6 @@ def __init__(self):
f"at {self.target_fps} FPS"
)

def get_client_count(self, request, response):
response.value = self.client_count
return response

def start_streaming_callback(self, request, response):
"""Service: Start streaming."""
if not self.is_streaming:
Expand Down
27 changes: 0 additions & 27 deletions camera_ros/test/test_camera_publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,33 +258,6 @@ def test_client_count_callback_stop(
assert node.is_streaming is False
assert node.client_count == 0

@patch('cv2.VideoCapture')
@patch('subprocess.run')
def test_get_client_count_service(
self, mock_subprocess, mock_videocapture, rclpy_init_shutdown
):
"""Test get_client_count service."""
# Import here to ensure path is set up
from camera_ros.srv import GetInt
from camera_publisher import CameraPublisher

# Mock subprocess and VideoCapture
mock_subprocess.return_value.returncode = 0
mock_subprocess.return_value.stdout = (
"webcamproduct: usb-webcam:\n /dev/video6\n"
)
mock_cap = MagicMock()
mock_cap.isOpened.return_value = True
mock_videocapture.return_value = mock_cap

node = CameraPublisher()
node.client_count = 5

request = GetInt.Request()
response = node.get_client_count(request, GetInt.Response())

assert response.value == 5

@patch('cv2.VideoCapture')
@patch('subprocess.run')
def test_topic_name_ext_camera(
Expand Down
47 changes: 23 additions & 24 deletions camera_ros/test/test_camera_stream_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,33 @@ def rclpy_init_shutdown():
rclpy.shutdown()


@pytest.fixture
def controller_node(rclpy_init_shutdown):
"""Create a CameraStreamController and destroy it before rclpy shuts down."""
from camera_stream_controller import CameraStreamController
node = CameraStreamController()
yield node
node.destroy_node()


class TestCameraStreamController:
"""Unit tests for CameraStreamController node."""

def test_node_initialization(self, rclpy_init_shutdown):
def test_node_initialization(self, controller_node):
"""Test that controller node initializes correctly."""
from camera_stream_controller import CameraStreamController

node = CameraStreamController()
node = controller_node

assert node.get_name() == 'camera_stream_controller'
assert node.current_client_count == 0
assert node.is_streaming is False
assert node.start_streaming_client is not None
assert node.stop_streaming_client is not None

def test_client_count_callback_start(self, rclpy_init_shutdown):
def test_client_count_callback_start(self, controller_node):
"""Test client count callback starts streaming when clients > 0."""
from camera_stream_controller import CameraStreamController
node = controller_node
with patch.object(CameraStreamController, 'start_streaming') as mock_start:
node = CameraStreamController()
node.is_streaming = False

msg = Int32()
Expand All @@ -62,11 +69,11 @@ def test_client_count_callback_start(self, rclpy_init_shutdown):
mock_start.assert_called_once()
assert node.current_client_count == 1

def test_client_count_callback_stop(self, rclpy_init_shutdown):
def test_client_count_callback_stop(self, controller_node):
"""Test client count callback stops streaming when clients == 0."""
from camera_stream_controller import CameraStreamController
node = controller_node
with patch.object(CameraStreamController, 'stop_streaming') as mock_stop:
node = CameraStreamController()
node.is_streaming = True

msg = Int32()
Expand All @@ -76,11 +83,9 @@ def test_client_count_callback_stop(self, rclpy_init_shutdown):
mock_stop.assert_called_once()
assert node.current_client_count == 0

def test_start_streaming_service_available(self, rclpy_init_shutdown):
def test_start_streaming_service_available(self, controller_node):
"""Test start_streaming when service is available."""
from camera_stream_controller import CameraStreamController

node = CameraStreamController()
node = controller_node
node.start_streaming_client.wait_for_service = Mock(return_value=True)
node.start_streaming_client.call_async = Mock(return_value=MagicMock())

Expand All @@ -91,11 +96,9 @@ def test_start_streaming_service_available(self, rclpy_init_shutdown):
)
node.start_streaming_client.call_async.assert_called_once()

def test_start_streaming_service_unavailable(self, rclpy_init_shutdown):
def test_start_streaming_service_unavailable(self, controller_node):
"""Test start_streaming when service is unavailable."""
from camera_stream_controller import CameraStreamController

node = CameraStreamController()
node = controller_node
node.start_streaming_client.wait_for_service = Mock(return_value=False)
node.get_logger = Mock()

Expand All @@ -106,11 +109,9 @@ def test_start_streaming_service_unavailable(self, rclpy_init_shutdown):
)
node.get_logger().warn.assert_called()

def test_stop_streaming_service_available(self, rclpy_init_shutdown):
def test_stop_streaming_service_available(self, controller_node):
"""Test stop_streaming when service is available."""
from camera_stream_controller import CameraStreamController

node = CameraStreamController()
node = controller_node
node.stop_streaming_client.wait_for_service = Mock(return_value=True)
node.stop_streaming_client.call_async = Mock(return_value=MagicMock())

Expand All @@ -121,11 +122,9 @@ def test_stop_streaming_service_available(self, rclpy_init_shutdown):
)
node.stop_streaming_client.call_async.assert_called_once()

def test_stop_streaming_service_unavailable(self, rclpy_init_shutdown):
def test_stop_streaming_service_unavailable(self, controller_node):
"""Test stop_streaming when service is unavailable."""
from camera_stream_controller import CameraStreamController

node = CameraStreamController()
node = controller_node
node.stop_streaming_client.wait_for_service = Mock(return_value=False)
node.get_logger = Mock()

Expand Down
Loading
Loading