Skip to content

Commit 4026fc9

Browse files
committed
Merge branch 'main' into JRA/ZED-Wrapper
2 parents c3c555d + 7c6adc9 commit 4026fc9

File tree

17 files changed

+272
-144
lines changed

17 files changed

+272
-144
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on:
77
push:
88
branches:
99
- main
10+
- integration
1011
jobs:
1112
ci:
1213
name: CI
@@ -19,11 +20,14 @@ jobs:
1920
steps:
2021
- uses: actions/checkout@v4
2122
with:
23+
# Re-enable when we put the simulator in the CI pipeline.
2224
# lfs: "true"
2325
# This makes sure that $GITHUB_WORKSPACE is the catkin workspace path
2426
path: "src/mrover"
2527
- name: Style Check
2628
run: . /home/mrover/ros2_ws/src/mrover/venv/bin/activate && cd $GITHUB_WORKSPACE/src/mrover/ && ./style.sh
29+
- name: Compiler Cache
30+
uses: hendrikmuhs/[email protected]
2731
- name: Build
2832
if: github.event.pull_request.draft == false
2933
run: . /opt/ros/humble/setup.sh && . /home/mrover/ros2_ws/src/mrover/venv/bin/activate && cd $GITHUB_WORKSPACE/src/mrover/ && ./build.sh

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ install(PROGRAMS
318318
localization/basestation_gps_driver.py
319319
superstructure/superstructure.py
320320
scripts/debug_course_publisher.py
321+
scripts/visualizer.py
321322

322323
# starter project sources
323324
starter_project/autonomy/src/localization.py

ansible/roles/ci/tasks/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
# - neovim
6565
# - sudo
6666
- cmake
67-
# - ccache # Caches intermediate build files, speeds up compilation over time
67+
- ccache # Caches intermediate build files, speeds up compilation over time
6868
- ninja-build # Faster than make
6969
# - tmux
7070
# - htop

config/localization.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ gps_linearization:
1515
rover_frame: "base_link"
1616

1717
# Exception, these are in degrees
18-
ref_lat: 38.4225202
19-
ref_lon: -110.7844653
18+
ref_lat: 42.293195
19+
ref_lon: -83.7096706
2020
ref_alt: 0.0

config/simulator.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ simulator:
2323
tag_0:
2424
type: urdf
2525
uri: package://mrover/urdf/world/tag_0.urdf.xacro
26-
position: [ -2.0, -2.0, 0.7 ]
26+
position: [ 11.0, 3.0, 0.7 ]
2727
tag_1:
2828
type: urdf
2929
uri: package://mrover/urdf/world/tag_1.urdf.xacro
3030
position: [ 15.0, -14.0, 2.4 ]
3131
hammer:
3232
type: urdf
3333
uri: package://mrover/urdf/world/hammer.urdf.xacro
34-
position: [ 5.0, 2.0, 0.7 ]
34+
position: [ -2.0, -2.0, 0.7 ]
3535
bottle:
3636
type: urdf
3737
uri: package://mrover/urdf/world/bottle.urdf.xacro
@@ -58,4 +58,4 @@ simulator:
5858
ref_lon: -110.7844653
5959
ref_alt: 0.0
6060
world_frame: "map"
61-
rover_frame: "base_link"
61+
rover_frame: "sim_base_link"

perception/tag_detector/README.md

Lines changed: 0 additions & 13 deletions
This file was deleted.

scripts/visualizer.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
#!/usr/bin/env python3
2+
3+
from __future__ import annotations
4+
import signal
5+
import graphviz # type: ignore
6+
import time
7+
from PyQt5.QtWidgets import * # type: ignore
8+
from PyQt5.QtCore import * # type: ignore
9+
from PyQt5.QtGui import QPainter # type: ignore
10+
from PyQt5.QtSvg import QSvgRenderer # type:ignore
11+
12+
import rclpy
13+
import rclpy.time
14+
import rclpy.logging
15+
from rclpy.node import Node
16+
from rclpy.executors import ExternalShutdownException
17+
import sys
18+
from mrover.msg import StateMachineStructure, StateMachineStateUpdate
19+
from threading import Lock
20+
from dataclasses import dataclass
21+
from typing import Optional, List, Dict
22+
import threading
23+
24+
25+
STRUCTURE_TOPIC = "nav_structure"
26+
STATUS_TOPIC = "nav_state"
27+
28+
29+
@dataclass
30+
class State:
31+
name: str
32+
children: List[State]
33+
34+
35+
class StateMachine:
36+
def __init__(self):
37+
self.states: Dict[str, State] = {}
38+
self.structure: Optional[StateMachineStructure] = None
39+
self.mutex: Lock = Lock()
40+
self.cur_active: str = ""
41+
self.previous_state: str = ""
42+
self.needs_redraw: bool = True
43+
44+
def set_active_state(self, active_state):
45+
"""
46+
sets the state specified to be active (thread safe)
47+
"""
48+
with self.mutex:
49+
if active_state != self.cur_active and active_state in self.states:
50+
self.previous_state = self.cur_active
51+
self.cur_active = active_state
52+
self.needs_redraw = True
53+
now = rclpy.time.Time()
54+
rclpy.logging.get_logger("Visualizer").info(
55+
f"Current time: {now} Previous state: {self.previous_state} Current State: { self.cur_active}"
56+
)
57+
58+
def _rebuild(self, structure: StateMachineStructure):
59+
"""
60+
rebuilds the state dictionary with a new structure message
61+
"""
62+
self.states = {child.origin: State(child.origin, []) for child in structure.transitions}
63+
for transition in structure.transitions:
64+
origin = transition.origin
65+
for to in transition.destinations:
66+
self.states[origin].children.append(self.states[to])
67+
self.needs_redraw = True
68+
69+
def check_rebuild(self, structure: StateMachineStructure):
70+
"""
71+
checks if the structure passed as input matches the structure already represented (thread safe)
72+
"""
73+
with self.mutex:
74+
if structure == self.structure:
75+
return False
76+
else:
77+
self._rebuild(structure)
78+
self.structure = structure
79+
80+
def container_status_callback(self, status: StateMachineStateUpdate):
81+
self.set_active_state(status.state)
82+
83+
def container_structure_callback(self, structure: StateMachineStructure):
84+
self.check_rebuild(structure)
85+
86+
87+
class GUI(QWidget): # type: ignore
88+
def __init__(self, state_machine_instance, *args, **kwargs):
89+
super().__init__(*args, **kwargs)
90+
self.label: QLabel = QLabel() # type: ignore
91+
self.timer: QTimer = QTimer() # type: ignore
92+
self.renderer: QSvgRenderer = QSvgRenderer()
93+
self.timer.timeout.connect(self.update)
94+
self.timer.start(1)
95+
self.graph: Optional[graphviz.Digraph] = None
96+
self.img = None
97+
self.state_machine: StateMachine = state_machine_instance
98+
self.viz = Node("Visualizer")
99+
100+
self.viz.create_subscription(
101+
StateMachineStructure, STRUCTURE_TOPIC, self.state_machine.container_structure_callback, 1
102+
)
103+
104+
self.viz.create_subscription(
105+
StateMachineStateUpdate, STATUS_TOPIC, self.state_machine.container_status_callback, 1
106+
)
107+
108+
def paintEvent(self, event):
109+
painter = QPainter(self)
110+
if self.img is not None:
111+
self.renderer.load(self.img)
112+
self.resize(self.renderer.defaultSize())
113+
self.renderer.render(painter)
114+
115+
def update(self):
116+
rclpy.spin_once(self.viz)
117+
with self.state_machine.mutex:
118+
if self.graph is None or self.state_machine.needs_redraw:
119+
self.graph = graphviz.Digraph(comment="State Machine Diagram", format="svg")
120+
for state_name in self.state_machine.states.keys():
121+
color = "red" if self.state_machine.cur_active == state_name else "black"
122+
self.graph.node(state_name, color=color)
123+
124+
for state in self.state_machine.states.values():
125+
for child in state.children:
126+
self.graph.edge(state.name, child.name)
127+
128+
self.state_machine.needs_redraw = False
129+
130+
self.img = self.graph.pipe()
131+
self.repaint()
132+
133+
134+
def main():
135+
try:
136+
rclpy.init()
137+
138+
signal.signal(signal.SIGINT, signal.SIG_DFL)
139+
state_machine = StateMachine()
140+
141+
print("Node Created...")
142+
143+
print("Subscriptions Created...")
144+
145+
print("Node Created...")
146+
147+
print("Subscriptions Created...")
148+
149+
app = QApplication([]) # type: ignore
150+
g = GUI(state_machine)
151+
g.show()
152+
app.exec_()
153+
154+
except KeyboardInterrupt:
155+
pass
156+
except ExternalShutdownException:
157+
sys.exit(1)
158+
159+
160+
if __name__ == "__main__":
161+
main()

starter_project/autonomy/launch/starter_project.launch.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ def generate_launch_description():
4242
# ===========
4343
# Navigation
4444
# ===========
45-
# navigation_node,
45+
navigation_node,
4646
# ============
4747
# Localization
4848
# ============
49-
# localization_node
49+
localization_node,
5050
]
5151
)

starter_project/autonomy/src/localization.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# library for interacting with ROS and TF tree
77
import rclpy
88
from rclpy.node import Node
9+
import rclpy.time
910
import tf2_ros
1011

1112
# ROS message types we need to use
@@ -71,8 +72,7 @@ def main():
7172
# let the callback functions run asynchronously in the background
7273
rclpy.spin(localization)
7374

74-
# TODO (ali): shutdown maybe?
75-
# rclpy.shutdown()
75+
rclpy.shutdown()
7676

7777

7878
if __name__ == "__main__":

starter_project/autonomy/src/navigation/context.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@
55

66
import numpy as np
77
import rclpy
8+
from rclpy.publisher import Publisher
9+
from rclpy.subscription import Subscription
810
from rclpy.node import Node
911
import tf2_ros
1012
from geometry_msgs.msg import Twist
1113
from mrover.msg import StarterProjectTag
1214

13-
# import sys
14-
# TODO (ali): python relative imports are the bane of my existence
15-
# sys.path.append('..')
16-
# print(sys.path)
15+
import sys
16+
import os
17+
18+
sys.path.append(os.getcwd() + "/starter_project/autonomy/src")
1719
from util.SE3 import SE3
1820
from visualization_msgs.msg import Marker
1921

@@ -55,25 +57,30 @@ def get_fid_data(self) -> Optional[StarterProjectTag]:
5557
if it exists, otherwise returns None
5658
"""
5759
# TODO: return either None or your position message
58-
pass
5960

6061

61-
class Context(Node):
62+
class Context:
63+
node: Node
6264
tf_buffer: tf2_ros.Buffer
6365
tf_listener: tf2_ros.TransformListener
64-
vel_cmd_publisher: rclpy.Publisher
65-
vis_publisher: rclpy.Publisher
66-
fid_listener: rclpy.Subscriber
66+
vel_cmd_publisher: Publisher
67+
vis_publisher: Publisher
68+
fid_listener: Subscription
6769

6870
# Use these as the primary interfaces in states
6971
rover: Rover
7072
env: Environment
73+
disable_requested: bool
7174

72-
def __init__(self):
75+
def setup(self, node: Node):
76+
self.node = node
7377
self.tf_buffer = tf2_ros.Buffer()
74-
self.tf_listener = tf2_ros.TransformListener(self.tf_buffer)
75-
self.vel_cmd_publisher = self.create_publisher(Twist, "cmd_vel", queue_size=1)
76-
self.vis_publisher = self.create_publisher("nav_vis", Marker)
78+
79+
self.tf_listener = tf2_ros.TransformListener(self.tf_buffer, self.node)
80+
self.vel_cmd_publisher = node.create_publisher(Twist, "cmd_vel", 1)
81+
self.vis_publisher = node.create_publisher(Marker, "nav_vis", 1)
7782
self.rover = Rover(self)
7883
self.env = Environment(self, None)
79-
self.fid_listener = self.create_subscription(StarterProjectTag, "/tag", self.env.receive_fid_data)
84+
self.disable_requested = False
85+
86+
self.fid_listener = node.create_subscription(StarterProjectTag, "/tag", self.env.receive_fid_data, 1)

starter_project/autonomy/src/navigation/drive_state.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,27 @@
22

33
from context import Context
44
from drive import get_drive_command
5-
from state import BaseState
5+
from state_machine.state import State
6+
from tag_seek import TagSeekState
67

78

8-
class DriveState(BaseState):
9-
def __init__(self, context: Context):
10-
super().__init__(
11-
context,
12-
# TODO:
13-
add_outcomes=["TODO: add outcomes here"],
14-
)
9+
class DriveState(State):
10+
def on_enter(self, context) -> None:
11+
pass
12+
13+
def on_exit(self, context) -> None:
14+
pass
1515

16-
def evaluate(self, ud):
17-
target = np.array([5.5, 2.0, 0.0])
16+
def on_loop(self, context) -> State:
17+
target = np.array([8.0, 2.0, 0.0])
1818

19-
# TODO: get the rover's pose, if it doesn't exist stay in DriveState (with outcome "driving_to_point")
19+
# TODO: get the rover's pose, if it doesn't exist stay in DriveState (return self)
2020

2121
# TODO: get the drive command and completion status based on target and pose
2222
# (HINT: use get_drive_command(), with completion_thresh set to 0.7 and turn_in_place_thresh set to 0.2)
2323

24-
# TODO: if we are finished getting to the target, go to TagSeekState (with outcome "reached_point")
24+
# TODO: if we are finished getting to the target, go to TagSeekState
2525

2626
# TODO: send the drive command to the rover
2727

28-
# TODO: tell smach to stay in the DriveState by returning with outcome "driving_to_point"
29-
30-
pass
28+
# TODO: tell state machine to stay in the DriveState by returning self

0 commit comments

Comments
 (0)