-
Notifications
You must be signed in to change notification settings - Fork 12
Add simple tutorial, update MyRobot example, provide uv environment #1042
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
570c5a0
bd3a2dc
b07b10f
39ba096
33a5c64
ebe6d9f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| name: Deploy Documentation | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - master | ||
| - feature/tutorial | ||
|
|
||
| jobs: | ||
| build-and-deploy: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Set up Python | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: '3.10' | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Really? It is 2026.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, you mean "very old Python version"? Well, some people still need the RealSense backward compatibility, but maybe there is already some replacement? Thanks for comments
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am currently trying to use version 3.12. Pyrealsense2 also works (At least here). |
||
|
|
||
| - name: Install uv | ||
| uses: astral-sh/setup-uv@v5 | ||
|
|
||
| - name: Install dependencies | ||
| run: | | ||
| uv pip install --system -r requirements.txt | ||
| uv pip install --system -r doc/requirements.txt | ||
| uv pip install --system . | ||
|
|
||
| - name: Build documentation | ||
| run: | | ||
| uv run sphinx-build -b html doc/sphinx doc/sphinx/_build/html | ||
|
|
||
| - name: Deploy to GitHub Pages | ||
| uses: peaceiris/actions-gh-pages@v4 | ||
| if: github.event_name == 'push' | ||
| with: | ||
| github_token: ${{ secrets.GITHUB_TOKEN }} | ||
| publish_dir: ./doc/sphinx/_build/html | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,3 +22,4 @@ ign-logs/ | |
| auto-save-list | ||
| tramp | ||
| .\#* | ||
| doc/sphinx/_build/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| sphinx |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ Welcome to osgar's documentation! | |
| :maxdepth: 2 | ||
| :caption: Contents: | ||
|
|
||
| tutorial | ||
| logging | ||
| eduro | ||
| drivers | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,237 @@ | ||
| MyRobot Tutorial | ||
| ================ | ||
|
|
||
| This tutorial explains how to create a simple simulated robot and a control | ||
| application using OSGAR. We will use the ``myrobot`` example as a reference. | ||
|
|
||
| The ``myrobot`` example consists of three main files: | ||
|
|
||
| - ``myrobot.py``: The robot driver, which simulates the robot's behavior. | ||
| - ``myapp.py``: The application, which controls the robot. | ||
| - ``myrobot.json``: The configuration file, which connects the robot and the | ||
| application. | ||
|
|
||
| The Robot Driver - ``myrobot.py`` | ||
| --------------------------------- | ||
|
|
||
| The ``myrobot.py`` file defines a simple simulated robot with differential | ||
| drive. It receives speed commands and simulates the robot's movement, | ||
| publishing its new pose (position and orientation). | ||
|
|
||
| The driver inherits from ``osgar.node.Node`` and implements ``on_desired_speed`` | ||
| and ``on_tick`` methods to handle incoming messages. The ``update()`` method | ||
| is provided by the base class and automatically dispatches messages to the | ||
| corresponding ``on_<channel>`` handlers. | ||
|
|
||
| Here is the code for ``myrobot.py``: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| """ | ||
| Example of robot diver outside OSGAR package | ||
| - simulation of differential robot | ||
| """ | ||
| import math | ||
|
|
||
| from osgar.node import Node | ||
|
|
||
|
|
||
| class MyRobot(Node): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this is a tutorial, couldn't there be more comments? |
||
| def __init__(self, config, bus): | ||
| super().__init__(config, bus) | ||
| bus.register('pose2d', 'emergency_stop') | ||
| self.pose = (0, 0, 0) | ||
| self.speed, self.angular_speed = 0, 0 | ||
| self.desired_speed, self.desired_angular_speed = 0, 0 | ||
| self.last_update = None | ||
|
|
||
| def send_pose(self): | ||
| x, y, heading = self.pose | ||
| self.publish('pose2d', [round(x*1000), round(y*1000), | ||
| round(math.degrees(heading)*100)]) | ||
|
|
||
| def update_pose(self): | ||
| if self.last_update is not None: | ||
| t = (self.time - self.last_update).total_seconds() | ||
| self.speed = self.desired_speed # TODO motion model | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The TODO things are confusing there. |
||
| self.angular_speed = self.desired_angular_speed | ||
|
|
||
| x, y, heading = self.pose | ||
| x += math.cos(heading) * self.speed * t | ||
| y += math.sin(heading) * self.speed * t | ||
| heading += self.angular_speed * t | ||
| self.pose = (x, y, heading) | ||
| self.last_update = self.time | ||
|
|
||
| def on_desired_speed(self, data): | ||
| self.update_pose() | ||
| self.desired_speed, self.desired_angular_speed = data[0]/1000.0, math.radians(data[1]/100.0) | ||
| self.send_pose() | ||
|
|
||
| def on_tick(self, data): | ||
| self.update_pose() | ||
| self.send_pose() | ||
|
|
||
| # vim: expandtab sw=4 ts=4 | ||
|
|
||
|
|
||
| The Application - ``myapp.py`` | ||
| ------------------------------ | ||
|
|
||
| The ``myapp.py`` file defines an application that controls the robot. It sends | ||
| speed commands to make the robot move in a figure eight. | ||
|
|
||
| Here is the code for ``myapp.py``: | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| """ | ||
| Example of a simple application controling robot to move in figure 8 | ||
| """ | ||
| import math | ||
| from datetime import timedelta | ||
|
|
||
| from osgar.node import Node | ||
| from osgar.bus import BusShutdownException | ||
|
|
||
|
|
||
| def distance(pose1, pose2): | ||
| return math.hypot(pose1[0] - pose2[0], pose1[1] - pose2[1]) | ||
|
|
||
|
|
||
| class MyApp(Node): | ||
| def __init__(self, config, bus): | ||
| super().__init__(config, bus) | ||
| bus.register('desired_speed') | ||
| self.max_speed = config.get('max_speed', 0.1) | ||
| self.max_angular_speed = math.radians(50) # TODO config | ||
| self.verbose = False | ||
| self.last_position = (0, 0, 0) | ||
| self.is_moving = False | ||
| self.pose2d = None # TODO should be defined by super().__init__() | ||
|
|
||
| # TODO refactor to common "serializer" | ||
| def send_speed_cmd(self, speed, angular_speed): | ||
| return self.publish('desired_speed', [round(speed*1000), round(math.degrees(angular_speed)*100)]) | ||
|
|
||
| # TODO refactor to common driver (used from sick2018 example) | ||
| def go_straight(self, how_far): | ||
| print(self.time, "go_straight %.1f" % how_far, self.last_position) | ||
| start_pose = self.last_position | ||
| if how_far >= 0: | ||
| self.send_speed_cmd(self.max_speed, 0.0) | ||
| else: | ||
| self.send_speed_cmd(-self.max_speed, 0.0) | ||
| while distance(start_pose, self.last_position) < abs(how_far): | ||
| self.update() | ||
| self.send_speed_cmd(0.0, 0.0) | ||
|
|
||
| def turn(self, angle, with_stop=True): | ||
| print(self.time, "turn %.1f" % math.degrees(angle)) | ||
| start_pose = self.last_position | ||
| if angle >= 0: | ||
| self.send_speed_cmd(0.0, self.max_angular_speed) | ||
| else: | ||
| self.send_speed_cmd(0.0, -self.max_angular_speed) | ||
| while abs(start_pose[2] - self.last_position[2]) < abs(angle): | ||
| self.update() | ||
| if with_stop: | ||
| self.send_speed_cmd(0.0, 0.0) | ||
| start_time = self.time | ||
| while self.time - start_time < timedelta(seconds=2): | ||
| self.update() | ||
| if not self.is_moving: | ||
| break | ||
| print(self.time, 'stop at', self.time - start_time) | ||
|
|
||
| def on_pose2d(self, data): | ||
| prev = self.pose2d | ||
| self.pose2d = data | ||
| x_mm, y_mm, heading_mdeg = self.pose2d | ||
| self.last_position = (x_mm/1000.0, y_mm/1000.0, | ||
| math.radians(heading_mdeg/100.0)) | ||
| self.is_moving = (prev != self.pose2d) | ||
|
|
||
| def run(self): | ||
| try: | ||
| print("MyApp example - figure 8!") | ||
| step_size = 0.5 # meters | ||
| deg90 = math.radians(90) | ||
|
|
||
| for i in range(4): | ||
| self.go_straight(step_size) | ||
| self.turn(deg90) | ||
|
|
||
| for i in range(4): | ||
| self.go_straight(step_size) | ||
| self.turn(-deg90) | ||
|
|
||
| print("END") | ||
| except BusShutdownException: | ||
| pass | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| from osgar.launcher import launch | ||
|
|
||
| launch(app=MyApp, description='Navigate figure eight', prefix='myapp-') | ||
|
|
||
| # vim: expandtab sw=4 ts=4 | ||
|
|
||
| The Configuration - ``myrobot.json`` | ||
| ------------------------------------ | ||
|
|
||
| The ``myrobot.json`` file defines the modules (the robot and the application) | ||
| and the connections between them. We use the built-in ``timer`` driver. | ||
|
|
||
| Here is the content of ``myrobot.json``: | ||
|
|
||
| .. code-block:: json | ||
|
|
||
| { | ||
| "version": 2, | ||
| "robot": { | ||
| "modules": { | ||
| "app": { | ||
| "driver": "myapp:MyApp", | ||
| "in": ["emergency_stop", "pose2d"], | ||
| "out": ["desired_speed"], | ||
| "init": { | ||
| "max_speed": 0.5 | ||
| } | ||
| }, | ||
| "myrobot": { | ||
| "driver": "myrobot:MyRobot", | ||
| "in": ["desired_speed", "tick"], | ||
| "out": ["emergency_stop", "pose2d"], | ||
| "init": {} | ||
| }, | ||
| "timer": { | ||
| "driver": "timer", | ||
| "in": [], | ||
| "out": ["tick"], | ||
| "init": { | ||
| "sleep": 0.1 | ||
| } | ||
| } | ||
| }, | ||
| "links": [["app.desired_speed", "myrobot.desired_speed"], | ||
| ["myrobot.emergency_stop", "app.emergency_stop"], | ||
| ["myrobot.pose2d", "app.pose2d"], | ||
| ["timer.tick", "myrobot.tick"]] | ||
| } | ||
| } | ||
|
|
||
| Running the Example | ||
| ------------------- | ||
|
|
||
| To run the example, you need to execute the ``osgar.record`` module with the | ||
| ``myrobot.json`` configuration file. You also need to add the current directory | ||
| and the ``examples/myrobot`` directory to your ``PYTHONPATH`` so that OSGAR | ||
| can find the modules. | ||
|
|
||
| .. code-block:: bash | ||
|
|
||
| PYTHONPATH=.:examples/myrobot python3 -m osgar.record examples/myrobot/myrobot.json --duration 10 | ||
|
|
||
| This will create a log file with the recorded data from the simulation. | ||
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, thanks to this is https://robotika.github.io/osgar/ already automatically updated