Keyboard joy#652
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces keyboard_joy, a new ROS 2 Python package that enables keyboard-based control by publishing sensor_msgs/Joy messages. The package serves as a joystick replacement for development and testing purposes, with configurable key mappings and support for both hold and sticky axis modes. Note that the current implementation is limited to Xorg environments.
Key changes:
- New keyboard_joy package with configurable YAML-based key mappings
- Support for both axis control (hold/sticky modes) and button inputs via keyboard
- Comprehensive test suite using pytest with mocked keyboard listener
Reviewed changes
Copilot reviewed 8 out of 10 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| mission/keyboard_joy/keyboard_joy/keyboard_joy_node.py | Main node implementation with keyboard event handling, Joy message publishing, and axis/button state management |
| mission/keyboard_joy/config/key_mappings.yaml | Default key mapping configuration defining axis and button bindings |
| mission/keyboard_joy/launch/keyboard_joy_node.launch.py | Launch file for starting the keyboard_joy node with configurable parameters |
| mission/keyboard_joy/test/test_keyboard_joy_node.py | Unit tests covering key mapping, axis modes, and button state transitions |
| mission/keyboard_joy/setup.py | Python package setup configuration with dependencies and entry points |
| mission/keyboard_joy/setup.cfg | Installation script configuration for ROS 2 |
| mission/keyboard_joy/package.xml | ROS 2 package manifest with dependencies |
| mission/keyboard_joy/README.md | Package documentation describing functionality and known limitations |
| mission/keyboard_joy/keyboard_joy/init.py | Empty package initialization file |
| mission/keyboard_joy/resource/keyboard_joy | Empty resource marker file for ament |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
0588283 to
8893e9c
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #652 +/- ##
==========================================
+ Coverage 31.37% 34.64% +3.26%
==========================================
Files 39 43 +4
Lines 2559 2823 +264
Branches 713 749 +36
==========================================
+ Hits 803 978 +175
- Misses 1577 1663 +86
- Partials 179 182 +3
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
|
bro why bootleg chatgpt talking |
Q3rkses
left a comment
There was a problem hiding this comment.
you are looking good cooking
| start_message = r""" | ||
| ██ ▄█▀▓█████▓██ ██▓ ▄▄▄▄ ▒█████ ▄▄▄ ██▀███ ▓█████▄ ▄▄▄██▀▀▀▒█████ ▓██ ██▓ | ||
| ██▄█▒ ▓█ ▀ ▒██ ██▒▓█████▄ ▒██▒ ██▒▒████▄ ▓██ ▒ ██▒▒██▀ ██▌ ▒██ ▒██▒ ██▒▒██ ██▒ | ||
| ▓███▄░ ▒███ ▒██ ██░▒██▒ ▄██▒██░ ██▒▒██ ▀█▄ ▓██ ░▄█ ▒░██ █▌ ░██ ▒██░ ██▒ ▒██ ██░ | ||
| ▓██ █▄ ▒▓█ ▄ ░ ▐██▓░▒██░█▀ ▒██ ██░░██▄▄▄▄██ ▒██▀▀█▄ ░▓█▄ ▌▓██▄██▓ ▒██ ██░ ░ ▐██▓░ | ||
| ▒██▒ █▄░▒████▒ ░ ██▒▓░░▓█ ▀█▓░ ████▓▒░ ▓█ ▓██▒░██▓ ▒██▒░▒████▓ ▓███▒ ░ ████▓▒░ ░ ██▒▓░ | ||
| ▒ ▒▒ ▓▒░░ ▒░ ░ ██▒▒▒ ░▒▓███▀▒░ ▒░▒░▒░ ▒▒ ▓▒█░░ ▒▓ ░▒▓░ ▒▒▓ ▒ ▒▓▒▒░ ░ ▒░▒░▒░ ██▒▒▒ | ||
| ░ ░▒ ▒░ ░ ░ ░▓██ ░▒░ ▒░▒ ░ ░ ▒ ▒░ ▒ ▒▒ ░ ░▒ ░ ▒░ ░ ▒ ▒ ▒ ░▒░ ░ ▒ ▒░ ▓██ ░▒░ | ||
| ░ ░░ ░ ░ ▒ ▒ ░░ ░ ░ ░ ░ ░ ▒ ░ ▒ ░░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ▒ ▒ ▒ ░░ | ||
| ░ ░ ░ ░░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ | ||
| ░ ░ ░ ░ ░ ░ | ||
| """ |
Andeshog
left a comment
There was a problem hiding this comment.
Consider separating the ros layer (publishing) and the logic layer. This would also make the code easier to test, as you would not need to go through ros. Personally I find nested dataclasses better than dictionaries here (easier to read, and safer to use). It would also pretty much mirror the yaml file. Also consider Enum class for "mode" instead of string
Andeshog
left a comment
There was a problem hiding this comment.
Fine as is, but if you managed to separate the logic from ros, the code would be much easier to test (ref. when you removed the previously written tests). The only "ros stuff" is periodically publishing a joy message, so not a compelling reason to pack all the logic into a ros node
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 14 changed files in this pull request and generated 12 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| delta = ( | ||
| self.axis_increment_step | ||
| if target > 0 | ||
| else -self.axis_increment_step | ||
| ) | ||
| next_val = current + delta | ||
|
|
||
| if (delta > 0 and next_val > target) or ( | ||
| delta < 0 and next_val < target | ||
| ): | ||
| next_val = target |
There was a problem hiding this comment.
Logic assumes target value is never zero, but this is not validated. The update_active_axes method calculates delta based on whether target > 0 (lines 143-147), which doesn't handle the case where target == 0. If a user creates an AxisBinding with val=0.0, the axis would increment toward negative infinity instead of staying at zero.
Consider adding validation in the AxisBinding or init to ensure axis values are non-zero, or fix the delta calculation to handle zero targets correctly.
| delta = ( | |
| self.axis_increment_step | |
| if target > 0 | |
| else -self.axis_increment_step | |
| ) | |
| next_val = current + delta | |
| if (delta > 0 and next_val > target) or ( | |
| delta < 0 and next_val < target | |
| ): | |
| next_val = target | |
| diff = target - current | |
| if diff == 0.0: | |
| continue | |
| step = self.axis_increment_step | |
| if abs(diff) <= step: | |
| next_val = target | |
| elif diff > 0: | |
| next_val = current + step | |
| else: | |
| next_val = current - step |
| self.listener.start() | ||
|
|
||
| self.create_timer(self.core.publish_period, self.publish_joy) | ||
| self.create_timer(self.core.axis_update_period, self.update_active_axes) | ||
|
|
||
| self.get_logger().info(start_message) | ||
|
|
There was a problem hiding this comment.
Potential resource leak if node initialization fails after starting listener. The keyboard listener is started on line 50, but if any subsequent initialization steps fail (e.g., create_timer on lines 52-53), the listener will not be stopped. The listener thread will continue running even though the node failed to initialize.
Consider wrapping the initialization in a try-except block that ensures the listener is stopped if initialization fails after it's started, or defer starting the listener until after all other initialization is complete.
| self.listener.start() | |
| self.create_timer(self.core.publish_period, self.publish_joy) | |
| self.create_timer(self.core.axis_update_period, self.update_active_axes) | |
| self.get_logger().info(start_message) | |
| self.create_timer(self.core.publish_period, self.publish_joy) | |
| self.create_timer(self.core.axis_update_period, self.update_active_axes) | |
| self.get_logger().info(start_message) | |
| self.listener.start() |
| mode=AxisMode(str(mode)), | ||
| ) | ||
|
|
||
| button_mappings = keymap.get("buttons", {}) or {} |
There was a problem hiding this comment.
Button mappings loaded from YAML are not type-validated. The button_mappings dictionary values should be integers, but there's no conversion or validation when loading from YAML (line 77). This could lead to runtime errors later when the values are used with int() conversions in the press and release methods.
Consider adding validation or explicit type conversion similar to how axis mappings are handled.
| button_mappings = keymap.get("buttons", {}) or {} | |
| raw_buttons = keymap.get("buttons", {}) or {} | |
| button_mappings: dict[str, int] = {} | |
| for key, button in raw_buttons.items(): | |
| try: | |
| button_mappings[key] = int(button) | |
| except (TypeError, ValueError) as exc: | |
| raise ValueError( | |
| f"Invalid button mapping for key {key!r}: {button!r}" | |
| ) from exc |
…nt a resolvable rosdep key
|
🎉 This PR is included in version 2.6.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This PR introduces
keyboard_joy, a python package that publishessensor_msgs/Joymessages based on keyboard input.TODO: the current implementation uses
pynputand only works on xorg (not Wayland).