Skip to content

Commit fbeb05a

Browse files
authored
Gl/swarinfo 638 visualisation tool (#3)
1 parent 7b0ba94 commit fbeb05a

10 files changed

Lines changed: 276 additions & 64 deletions

File tree

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Python Test Bench for the SwarmUS Platform
2+
3+
4+
## Using the graphical tool
5+
6+
This code repository boasts a graphical tool to visualise the interlocalisation feature.
7+
To use the tool, you will need one stationary HiveBoard with its BeeBoards and at least one mobile
8+
HiveBoard/BeeBoard assembly.
9+
10+
The stationary BeeBoard must be powered by the barrel connector (**not** by USB). Plug the Micro-USB cable to your
11+
computer for the serial connection.
12+
13+
Next, start the mobile HiveBoard/BeeBoard assemblies.
14+
15+
To start the tool, start the `main.py` file under `src/visualisation_tool`. By default, the visualisation tool will
16+
use the com port `/dev/ttyACM0`. To edit this value, edit the `COM_PORT` variable in `DataUpdater.py`.
17+
18+
The visualisation tool will open a window with an orange dot at the center, representing the stationary HiveBoard. The
19+
mobile HiveBoards will show as coloured circles and their position will be updated at regular intervals. Specific
20+
neighbors can be hidden by unchecking the 'visible' check box in the table underneath the graph.
21+
22+
![](img/visualisation_tool.png)

img/visualisation_tool.png

86.4 KB
Loading

src/hiveboard/HiveBoard.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ def set_neighbor_list_callback(self, callback):
4444
def set_neighbor_position_callback(self, callback):
4545
self.neighbor_position_callback = callback
4646

47+
def set_neighbor_list_callback(self, callback):
48+
self.neighbors_list_callback = callback
49+
50+
def set_neighbor_position_callback(self, callback):
51+
self.neighbor_position_callback = callback
52+
4753
def kill_receiver(self):
4854
self._run = False
4955
self._proto_stream.kill_stream()
@@ -156,6 +162,42 @@ def send_get_neighbor_position_request(self, destination: int, neighbor_id: id):
156162

157163
self._proto_stream.write_message_to_stream(msg)
158164

165+
def send_get_neighbors_request(self, destination: int):
166+
neighbors_request = GetNeighborsListRequest()
167+
168+
hivemind_api_request = HiveMindHostApiRequest()
169+
hivemind_api_request.neighbors_list.CopyFrom(neighbors_request)
170+
171+
request = Request()
172+
request.hivemind_host.CopyFrom(hivemind_api_request)
173+
174+
msg = Message()
175+
msg.source_id = self.uuid
176+
msg.destination_id = destination
177+
msg.request.CopyFrom(request)
178+
179+
self._proto_stream.write_message_to_stream(msg)
180+
181+
def send_get_neighbor_position_request(self, destination: int, neighbor_id: id):
182+
neighbor_position_request = GetNeighborRequest()
183+
neighbor_position_request.neighbor_id = neighbor_id
184+
185+
hivemind_host_api_request = HiveMindHostApiRequest()
186+
hivemind_host_api_request.neighbor.CopyFrom(neighbor_position_request)
187+
188+
request = Request()
189+
request.hivemind_host.CopyFrom(hivemind_host_api_request)
190+
191+
msg = Message()
192+
msg.source_id = self.uuid
193+
msg.destination_id = destination
194+
msg.request.CopyFrom(request)
195+
196+
self._proto_stream.write_message_to_stream(msg)
197+
198+
199+
200+
159201

160202

161203

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import time
33

44
from Graph2D import Graph2D
5-
from PyQt5.QtCore import QObject, QThread, pyqtSignal
5+
from NeighborCoordinateTable import NeighborCoordinateTable
6+
from PyQt5.QtCore import QObject, QThread, pyqtSignal, pyqtSlot
67
import numpy as np
78
import sys
89

@@ -17,19 +18,23 @@
1718

1819

1920
class DataUpdater(QObject):
20-
new_point = pyqtSignal(int, float, float)
21+
new_cartesian_point = pyqtSignal(int, float, float)
22+
new_polar_point = pyqtSignal(int, float, float)
2123
received_greeting = pyqtSignal(int)
2224

23-
def __init__(self, graph: Graph2D):
25+
def __init__(self, graph: Graph2D, table: NeighborCoordinateTable):
2426
super().__init__()
2527

2628
self.graph = graph
27-
self.new_point.connect(self.graph.update_point)
29+
self.neighbor_table = table
30+
self.new_cartesian_point.connect(self.graph.update_point)
31+
self.new_polar_point.connect(self.neighbor_table.update_neighbor)
2832
self.hiveboard = None
2933
self.hiveboard_connnected = False
3034
self.target_agent_id = 0
3135

3236
self.neighbor_list = []
37+
self.neighbor_table.hide_neighbors.connect(self.graph.hide_neighbors)
3338

3439
self.start_connection_thread()
3540
self.start_greeting_thread()
@@ -75,11 +80,11 @@ def handle_neighbor_list(self, neighbor_list):
7580
print(f"New neighbor list is {self.neighbor_list}")
7681

7782
def handle_neigbor_update(self, neighbor):
78-
y = -neighbor.position.distance * np.cos(neighbor.position.azimuth / 180 * np.pi)
79-
x = neighbor.position.distance * np.sin(neighbor.position.azimuth / 180 * np.pi)
8083
neighbor_id = neighbor.neighbor_id
81-
self.new_point.emit(neighbor_id, x, y)
82-
print(f"Agent {neighbor_id} now at ({x},{y})")
84+
self.new_polar_point.emit(neighbor_id, neighbor.position.distance, neighbor.position.azimuth)
85+
y = neighbor.position.distance * np.cos(neighbor.position.azimuth / 180 * np.pi)
86+
x = neighbor.position.distance * np.sin(neighbor.position.azimuth / 180 * np.pi)
87+
self.new_cartesian_point.emit(neighbor_id, x, y)
8388

8489
def request_neighbors_update(self):
8590
while True:
@@ -91,4 +96,4 @@ def request_neighbors_update(self):
9196
for neighbor_id in self.neighbor_list:
9297
self.hiveboard.send_get_neighbor_position_request(destination=self.target_agent_id,
9398
neighbor_id=neighbor_id)
94-
time.sleep(0.1)
99+
time.sleep(0.2)

src/visualisation_tool/Graph2D.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from PyQt5 import QtWidgets
2+
from PyQt5.QtCore import pyqtSlot
3+
from pyqtgraph import PlotWidget, plot, GridItem, LegendItem
4+
import pyqtgraph as pg
5+
6+
COLOR_OFFSET = 15
7+
8+
9+
class Graph2D(QtWidgets.QWidget):
10+
11+
def __init__(self, parent):
12+
super(Graph2D, self).__init__(parent)
13+
self.layout = QtWidgets.QGridLayout()
14+
self.setLayout(self.layout)
15+
self.scatters = {}
16+
self.hidden_neighbors = []
17+
18+
self.create_2d_plot()
19+
self.create_grid()
20+
self.create_scatter_element()
21+
22+
def create_2d_plot(self):
23+
self.graphWidget = pg.plot()
24+
#self.graphWidget.setBackground("w")
25+
self.graphWidget.hideAxis('bottom')
26+
self.graphWidget.hideAxis('left')
27+
self.graphWidget.setXRange(-7, 7)
28+
self.graphWidget.setYRange(-7, 7)
29+
self.graphWidget.setMouseEnabled(x=False, y=False)
30+
self.graphWidget.addLegend()
31+
self.layout.addWidget(self.graphWidget)
32+
33+
def create_grid(self):
34+
self.grid = GridItem(pen='white', textPen='white')
35+
self.graphWidget.addItem(self.grid)
36+
37+
def create_scatter_element(self):
38+
self.base = pg.ScatterPlotItem()
39+
base_symbol = {'pos': [0, 0],
40+
'pen': {'color': 'w', 'width': 1},
41+
'brush': pg.intColor(10, 100), # Orange
42+
'symbol': 'd',
43+
'size': 30}
44+
self.base.addPoints([base_symbol])
45+
self.graphWidget.addItem(self.base)
46+
47+
@pyqtSlot(int, float, float)
48+
def update_point(self, neighbor_id: int, x: float, y: float):
49+
if neighbor_id not in self.scatters.keys() and neighbor_id not in self.hidden_neighbors:
50+
self.scatters.update({neighbor_id: {"scatter": pg.ScatterPlotItem(name=f"agent {neighbor_id}",
51+
brush=pg.intColor(neighbor_id * COLOR_OFFSET, 100)),
52+
"spot": {}}})
53+
self.graphWidget.addItem(self.scatters[neighbor_id]["scatter"])
54+
if neighbor_id not in self.hidden_neighbors:
55+
self.scatters[neighbor_id]["spot"] = {'pos': [x, y],
56+
'pen': {'color': 'w', 'width': 1},
57+
# 100 is the number int values for the color spectrum,
58+
# COLOR_OFFSET is the offset to apply to have 6 different values for points
59+
'brush': pg.intColor(neighbor_id * COLOR_OFFSET, 100),
60+
'size': 20}
61+
self.scatters[neighbor_id]["scatter"].setData(spots=[self.scatters[neighbor_id]["spot"]],
62+
name=f"Agent {neighbor_id}")
63+
64+
@pyqtSlot(list)
65+
def hide_neighbors(self, hidden_neighbors: list):
66+
self.hidden_neighbors = hidden_neighbors
67+
for neighbor_id in self.hidden_neighbors:
68+
if neighbor_id in self.scatters.keys():
69+
element = self.scatters.pop(neighbor_id)
70+
self.graphWidget.removeItem(element["scatter"])
71+
Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,47 +3,54 @@
33

44
from PyQt5.QtWidgets import *
55
from Graph2D import Graph2D
6-
from PyQt5.QtCore import QThread, pyqtSlot
6+
from PyQt5.QtCore import QThread, pyqtSlot, Qt, pyqtSignal
77
from pyqtgraph import PlotWidget, plot
88
import pyqtgraph as pg
99
import numpy as np
10+
from NeighborCoordinateTable import NeighborCoordinateTable
1011
from DataUpdater import DataUpdater
1112

1213

1314
class MainWindow(QMainWindow):
1415

16+
refresh_neighbor = pyqtSignal()
17+
1518
def __init__(self):
1619
super(MainWindow, self).__init__()
1720
self.target_agent_id = None
1821

1922
# Qt shenaninigans
2023
self.setWindowTitle("SwarmUS Interlocalisation Visualisation Tool")
21-
self.container = QWidget()
22-
self.main_layout = QVBoxLayout()
23-
self.container.setLayout(self.main_layout)
24+
self.container = QSplitter(Qt.Vertical)
2425
self.setCentralWidget(self.container)
2526

2627
# Create UI elements here
2728
self.create_2d_graph_area()
2829
self.create_menu_bar()
30+
self.create_neighbor_table()
2931

3032
# Add backend elements here
3133
self.start_data_acquisition()
3234
self.data_updater.received_greeting.connect(self.update_target_agent_from_greeting)
3335

3436
def create_menu_bar(self):
3537
menu_bar = self.menuBar()
36-
config_menu = QMenu("&Configuration", self)
37-
target_agent_menu = QMenu("Target Agent", self)
38+
39+
target_agent_menu = QMenu("&Target Agent", self)
3840
self.checkable_agents = []
3941
for i in range(1, 7):
4042
agent = QAction(f"Agent {i}", self)
4143
agent.setCheckable(True)
4244
target_agent_menu.addAction(agent)
4345
self.checkable_agents.append(agent)
4446
agent.triggered.connect(self.update_target_agent_from_config)
45-
config_menu.addMenu(target_agent_menu)
46-
menu_bar.addMenu(config_menu)
47+
48+
refresh_neighbors = QAction("&Refresh neighbor list", self)
49+
refresh_neighbors.triggered.connect(self.refresh_neighbor_list)
50+
self.refresh_neighbor.emit()
51+
52+
menu_bar.addMenu(target_agent_menu)
53+
menu_bar.addAction(refresh_neighbors)
4754

4855
@pyqtSlot(bool)
4956
def update_target_agent_from_config(self, checked: bool):
@@ -57,7 +64,7 @@ def update_target_agent_from_config(self, checked: bool):
5764
new_agent_id = self.checkable_agents.index(agent) + 1
5865
self.target_agent_id = new_agent_id
5966
self.data_updater.target_agent_id = self.target_agent_id
60-
self.data_updater.neighbor_list.clear()
67+
del self.data_updater.neighbor_list[:]
6168
print(f"New target agent is {self.target_agent_id} based on configuration set")
6269

6370
@pyqtSlot(int)
@@ -69,12 +76,22 @@ def update_target_agent_from_greeting(self, agent_id):
6976
self.data_updater.target_agent_id = self.target_agent_id
7077
print(f"New target agent is {self.target_agent_id} based on greeting received")
7178

79+
@pyqtSlot()
80+
def refresh_neighbor_list(self):
81+
self.data_updater.hiveboard.send_get_neighbors_request(self.target_agent_id)
82+
self.neighbor_table.clear_table()
83+
7284
def create_2d_graph_area(self):
7385
self.graphWidget = Graph2D(self)
74-
self.main_layout.addWidget(self.graphWidget)
86+
self.container.addWidget(self.graphWidget)
87+
88+
def create_neighbor_table(self):
89+
self.neighbor_table = NeighborCoordinateTable(0, 5)
90+
self.container.addWidget(self.neighbor_table)
91+
self.refresh_neighbor.connect(self.neighbor_table.clear_table)
7592

7693
def start_data_acquisition(self):
77-
self.data_updater = DataUpdater(self.graphWidget)
94+
self.data_updater = DataUpdater(self.graphWidget, self.neighbor_table)
7895
self.data_updater_thread = QThread()
7996
self.data_updater.moveToThread(self.data_updater_thread)
8097
self.data_updater_thread.started.connect(self.data_updater.request_neighbors_update)

0 commit comments

Comments
 (0)