Skip to content

Commit b30e033

Browse files
committed
panels: added HobbyWing ESC panel
1 parent e25396c commit b30e033

File tree

3 files changed

+293
-3
lines changed

3 files changed

+293
-3
lines changed

dronecan_gui_tool/panels/__init__.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from . import serial_panel
1616
from . import stats_panel
1717
from . import RemoteID_panel
18+
from . import hobbywing_esc
1819

1920
class PanelDescriptor:
2021
def __init__(self, module):
@@ -35,11 +36,12 @@ def safe_spawn(self, parent, node):
3536
show_error('Panel error', 'Could not spawn panel', ex)
3637

3738

38-
PANELS = sorted([
39+
PANELS = [
3940
PanelDescriptor(esc_panel),
4041
PanelDescriptor(actuator_panel),
4142
PanelDescriptor(RTK_panel),
4243
PanelDescriptor(serial_panel),
4344
PanelDescriptor(stats_panel),
44-
PanelDescriptor(RemoteID_panel)
45-
], key=lambda x: x.name)
45+
PanelDescriptor(RemoteID_panel),
46+
PanelDescriptor(hobbywing_esc)
47+
]
+275
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
#
2+
# Copyright (C) 2023 UAVCAN Development Team <dronecan.org>
3+
#
4+
# This software is distributed under the terms of the MIT License.
5+
#
6+
# Author: Andrew Tridgell
7+
#
8+
9+
import dronecan
10+
from functools import partial
11+
from PyQt5.QtWidgets import QVBoxLayout, QWidget, QLabel, QDialog, \
12+
QPlainTextEdit, QPushButton, QLineEdit, QFileDialog, QComboBox, QHBoxLayout, QSpinBox
13+
from PyQt5.QtCore import QTimer, Qt
14+
from logging import getLogger
15+
from ..widgets import make_icon_button, get_icon, get_monospace_font
16+
from ..widgets import table_display
17+
import random
18+
import base64
19+
import struct
20+
21+
__all__ = 'PANEL_NAME', 'spawn', 'get_icon'
22+
23+
PANEL_NAME = 'Hobbywing ESC Panel'
24+
25+
logger = getLogger(__name__)
26+
27+
_singleton = None
28+
29+
class HobbywingPanel(QDialog):
30+
DEFAULT_INTERVAL = 0.1
31+
32+
def __init__(self, parent, node):
33+
super(HobbywingPanel, self).__init__(parent)
34+
self.setWindowTitle('Hobbywing ESC Panel')
35+
self.setAttribute(Qt.WA_DeleteOnClose) # This is required to stop background timers!
36+
37+
self._node = node
38+
39+
layout = QVBoxLayout()
40+
41+
self.table = table_display.TableDisplay(['Node','ThrottleID','RPM','Voltage','Current','Temperature','Direction'])
42+
layout.addWidget(self.table)
43+
44+
self.baudrate = QComboBox(self)
45+
for b in [50000, 100000, 200000, 250000, 500000, 1000000]:
46+
self.baudrate.addItem(str(b))
47+
self.baudrate.setCurrentText(str(1000000))
48+
self.baudrate_set = QPushButton('Set', self)
49+
self.baudrate_set.clicked.connect(self.on_baudrate_set)
50+
51+
layout.addLayout(self.labelWidget('Baudrate:', [self.baudrate, self.baudrate_set]))
52+
53+
self.throttleid = QSpinBox(self)
54+
self.throttleid.setMinimum(1)
55+
self.throttleid.setMaximum(32)
56+
self.throttleid.setValue(1)
57+
self.throttleid_set = QPushButton('Set', self)
58+
self.throttleid_set.clicked.connect(self.on_throttleid_set)
59+
60+
layout.addLayout(self.labelWidget('ThrottleID:', [self.throttleid, self.throttleid_set]))
61+
62+
self.nodeid = QSpinBox(self)
63+
self.nodeid.setMinimum(1)
64+
self.nodeid.setMaximum(127)
65+
self.nodeid.setValue(1)
66+
self.nodeid_set = QPushButton('Set', self)
67+
self.nodeid_set.clicked.connect(self.on_nodeid_set)
68+
69+
layout.addLayout(self.labelWidget('NodeID:', [self.nodeid, self.nodeid_set]))
70+
71+
self.direction = QComboBox(self)
72+
self.direction.addItem("CW")
73+
self.direction.addItem("CCW")
74+
self.direction.setCurrentText("CW")
75+
self.direction_set = QPushButton('Set', self)
76+
self.direction_set.clicked.connect(self.on_direction_set)
77+
78+
layout.addLayout(self.labelWidget('Direction:', [self.direction, self.direction_set]))
79+
80+
self.msg1rate = QComboBox(self)
81+
for r in [0, 1, 10, 20, 50, 100, 200, 250, 500]:
82+
self.msg1rate.addItem(str(r))
83+
self.msg1rate.setCurrentText("50")
84+
self.msg1rate_set = QPushButton('Set', self)
85+
self.msg1rate_set.clicked.connect(self.on_msg1rate_set)
86+
87+
layout.addLayout(self.labelWidget('Msg1Rate:', [self.msg1rate, self.msg1rate_set]))
88+
89+
self.msg2rate = QComboBox(self)
90+
for r in [0, 1, 10, 20, 50, 100, 200, 250, 500]:
91+
self.msg2rate.addItem(str(r))
92+
self.msg2rate.setCurrentText("10")
93+
self.msg2rate_set = QPushButton('Set', self)
94+
self.msg2rate_set.clicked.connect(self.on_msg2rate_set)
95+
96+
layout.addLayout(self.labelWidget('Msg2Rate:', [self.msg2rate, self.msg2rate_set]))
97+
98+
self.msg3rate = QComboBox(self)
99+
for r in [0, 1, 10, 20, 50, 100, 200, 250, 500]:
100+
self.msg3rate.addItem(str(r))
101+
self.msg3rate.setCurrentText("10")
102+
self.msg3rate_set = QPushButton('Set', self)
103+
self.msg3rate_set.clicked.connect(self.on_msg3rate_set)
104+
105+
layout.addLayout(self.labelWidget('Msg3Rate:', [self.msg3rate, self.msg3rate_set]))
106+
107+
self.setLayout(layout)
108+
self.resize(400, 200)
109+
110+
self.handlers = [node.add_handler(dronecan.com.hobbywing.esc.StatusMsg1, self.handle_StatusMsg1),
111+
node.add_handler(dronecan.com.hobbywing.esc.StatusMsg2, self.handle_StatusMsg2),
112+
node.add_handler(dronecan.com.hobbywing.esc.GetEscID, self.handle_GetEscID)]
113+
114+
QTimer.singleShot(500, self.request_ids)
115+
116+
117+
def handle_reply(self, msg):
118+
'''handle a reply to a set'''
119+
if msg is not None:
120+
print('REPLY: ', dronecan.to_yaml(msg))
121+
else:
122+
print("No reply")
123+
124+
def on_throttleid_set(self):
125+
'''set throttle ID'''
126+
nodeid = self.table.get_selected()
127+
req = dronecan.com.hobbywing.esc.SetID.Request()
128+
req.node_id = nodeid
129+
req.throttle_channel = int(self.throttleid.value())
130+
self._node.request(req, nodeid, self.handle_reply)
131+
132+
def on_nodeid_set(self):
133+
'''set node ID'''
134+
nodeid = self.table.get_selected()
135+
thr_id = int(self.table.data[nodeid][1])
136+
req = dronecan.com.hobbywing.esc.SetID.Request()
137+
req.node_id = int(self.nodeid.value())
138+
req.throttle_channel = thr_id
139+
self._node.request(req, nodeid, self.handle_reply)
140+
141+
def on_baudrate_set(self):
142+
'''set baudrate'''
143+
nodeid = self.table.get_selected()
144+
req = dronecan.com.hobbywing.esc.SetBaud.Request()
145+
bmap = {
146+
1000000 : req.BAUD_1MBPS,
147+
500000 : req.BAUD_500KBPS,
148+
250000 : req.BAUD_250KBPS,
149+
200000 : req.BAUD_200KBPS,
150+
100000 : req.BAUD_100KBPS,
151+
50000 : req.BAUD_50KBPS,
152+
}
153+
baudrate = int(self.baudrate.currentText())
154+
req.baud = bmap[baudrate]
155+
self._node.request(req, nodeid, self.handle_reply)
156+
157+
def on_direction_set(self):
158+
'''set direction'''
159+
nodeid = self.table.get_selected()
160+
req = dronecan.com.hobbywing.esc.SetDirection.Request()
161+
req.direction = 0 if self.direction.currentText() == "CW" else 1
162+
self._node.request(req, nodeid, self.handle_reply)
163+
164+
def set_rate(self, nodeid, msgid, rate):
165+
'''set a message rate'''
166+
req = dronecan.com.hobbywing.esc.SetReportingFrequency.Request()
167+
req.option = req.OPTION_WRITE
168+
req.MSG_ID = msgid
169+
rmap = {
170+
500 : req.RATE_500HZ,
171+
250 : req.RATE_250HZ,
172+
200 : req.RATE_200HZ,
173+
100 : req.RATE_100HZ,
174+
50 : req.RATE_50HZ,
175+
20 : req.RATE_20HZ,
176+
10 : req.RATE_10HZ,
177+
1 : req.RATE_1HZ,
178+
}
179+
if not rate in rmap:
180+
print("Invalid rate %d - must be one of %s" % (rate, ','.join(rmap.keys())))
181+
return
182+
req.rate = rmap[rate]
183+
self._node.request(req, nodeid, self.handle_reply)
184+
185+
def on_msg1rate_set(self):
186+
'''set msg1 rate'''
187+
nodeid = self.table.get_selected()
188+
self.set_rate(nodeid, 20050, int(self.msg1rate.currentText()))
189+
190+
def on_msg2rate_set(self):
191+
'''set msg2 rate'''
192+
nodeid = self.table.get_selected()
193+
self.set_rate(nodeid, 20051, int(self.msg1rate.currentText()))
194+
195+
def on_msg3rate_set(self):
196+
'''set msg3 rate'''
197+
nodeid = self.table.get_selected()
198+
self.set_rate(nodeid, 20052, int(self.msg1rate.currentText()))
199+
200+
def handle_GetEscID(self, msg):
201+
'''handle GetEscID'''
202+
nodeid = msg.transfer.source_node_id
203+
if len(msg.message.payload) != 2:
204+
return
205+
data = self.table.get(nodeid)
206+
if data is None:
207+
data = [nodeid, 0, 0, 0, 0, 0, 0]
208+
data[1] = msg.message.payload[1]
209+
self.table.update(nodeid, data)
210+
211+
def handle_StatusMsg1(self, msg):
212+
'''handle StatusMsg1'''
213+
nodeid = msg.transfer.source_node_id
214+
data = self.table.get(nodeid)
215+
if data is None:
216+
data = [nodeid, 0, 0, 0, 0, 0, 0]
217+
data[6] = "CCW" if msg.message.status & (1<<14) else "CW"
218+
data[2] = msg.message.rpm
219+
self.table.update(nodeid, data)
220+
221+
def handle_StatusMsg2(self, msg):
222+
'''handle StatusMsg2'''
223+
nodeid = msg.transfer.source_node_id
224+
data = self.table.get(nodeid)
225+
if data is None:
226+
data = [nodeid, 0, 0, 0, 0, 0, 0]
227+
data[3] = "%.2f" % (msg.message.input_voltage*0.1)
228+
data[4] = "%.2f" % (msg.message.current*0.1)
229+
data[5] = msg.message.temperature
230+
self.table.update(nodeid, data)
231+
232+
def request_ids(self):
233+
'''call GetEscID'''
234+
QTimer.singleShot(500, self.request_ids)
235+
req = dronecan.com.hobbywing.esc.GetEscID()
236+
req.payload = [0]
237+
self._node.broadcast(req)
238+
239+
def labelWidget(self, label, widgets):
240+
'''a widget with a label'''
241+
hlayout = QHBoxLayout()
242+
hlayout.addWidget(QLabel(label, self))
243+
if not isinstance(widgets, list):
244+
widgets = [widgets]
245+
for w in widgets:
246+
hlayout.addWidget(w)
247+
return hlayout
248+
249+
def __del__(self):
250+
for h in self.handlers:
251+
h.remove()
252+
global _singleton
253+
_singleton = None
254+
255+
def closeEvent(self, event):
256+
super(HobbywingPanel, self).closeEvent(event)
257+
self.__del__()
258+
259+
260+
def spawn(parent, node):
261+
global _singleton
262+
if _singleton is None:
263+
try:
264+
_singleton = HobbywingPanel(parent, node)
265+
except Exception as ex:
266+
print(ex)
267+
268+
_singleton.show()
269+
_singleton.raise_()
270+
_singleton.activateWindow()
271+
272+
return _singleton
273+
274+
275+
get_icon = partial(get_icon, 'asterisk')

dronecan_gui_tool/widgets/table_display.py

+13
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def __init__(self, headers, expire_time=2.0):
1919
self.resizeRowsToContents()
2020
self.expire_time = expire_time
2121
self.show()
22+
self.data = {}
2223
if self.expire_time is not None:
2324
QTimer.singleShot(int(expire_time*500), self.check_expired)
2425

@@ -32,13 +33,25 @@ def update(self, row_key, row):
3233

3334
self.timestamps[row_key] = time.time()
3435
row_idx = self.row_keys.index(row_key)
36+
self.data[row_key] = row
3537
for i in range(len(row)):
3638
self.setItem(row_idx, i, QTableWidgetItem(str(row[i])))
3739

3840
self.resizeColumnsToContents()
3941
self.resizeRowsToContents()
4042
self.show()
4143

44+
def get(self, row_key):
45+
'''get current data for a row'''
46+
return self.data.get(row_key,None)
47+
48+
def get_selected(self):
49+
'''get the selected row key'''
50+
row = self.currentRow()
51+
if row is None:
52+
return None
53+
return self.row_keys[row]
54+
4255
def remove_row(self, row_key):
4356
'''remove a row'''
4457
if not row_key in self.row_keys:

0 commit comments

Comments
 (0)