Skip to content

Commit 8c24233

Browse files
committed
Sensors: add script to publish rotor joint velocities using dronecan
Signed-off-by: Rhys Mainwaring <rhys.mainwaring@me.com>
1 parent 0c02469 commit 8c24233

1 file changed

Lines changed: 225 additions & 96 deletions

File tree

scripts/dronecan_sensor.py

Lines changed: 225 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,140 +1,269 @@
1+
#!/usr/bin/env python3
12
"""
23
Publish sensor data using DroneCAN
34
"""
45

56
import dronecan
7+
import math
68
import threading
79
import time
810

11+
from argparse import ArgumentParser
12+
913
GZ_VERSION_GARDEN = "garden"
1014
GZ_VERSION_HARMONIC = "harmonic"
1115
GZ_VERSION_IONIC = "ionic"
1216

1317
GZ_VERSION = GZ_VERSION_HARMONIC
1418

1519
if GZ_VERSION == GZ_VERSION_GARDEN:
16-
from gz.msgs9.fluid_pressure_pb2 import FluidPressure
17-
from gz.msgs9.air_speed_pb2 import AirSpeed
18-
from gz.msgs9.altimeter_pb2 import Altimeter
19-
from gz.msgs9.magnetometer_pb2 import Magnetometer
20-
from gz.msgs9.navsat_pb2 import NavSat
21-
from gz.msgs9.imu_pb2 import IMU
22-
23-
from gz.transport12 import Node
24-
20+
from gz.msgs9.model_pb2 import Model
2521
elif GZ_VERSION == GZ_VERSION_HARMONIC:
26-
from gz.msgs10.fluid_pressure_pb2 import FluidPressure
27-
from gz.msgs10.air_speed_pb2 import AirSpeed
28-
from gz.msgs10.altimeter_pb2 import Altimeter
29-
from gz.msgs10.magnetometer_pb2 import Magnetometer
30-
from gz.msgs10.navsat_pb2 import NavSat
31-
from gz.msgs10.imu_pb2 import IMU
22+
# from gz.msgs10.air_speed_pb2 import AirSpeed
23+
# from gz.msgs10.altimeter_pb2 import Altimeter
24+
# from gz.msgs10.axis_pb2 import Axis
25+
# from gz.msgs10.axis_aligned_box_pb2 import AxisAlignedBox
26+
# from gz.msgs10.fluid_pressure_pb2 import FluidPressure
27+
# from gz.msgs10.imu_pb2 import IMU
28+
# from gz.msgs10.joint_pb2 import Joint
29+
# from gz.msgs10.link_pb2 import Link
30+
# from gz.msgs10.magnetometer_pb2 import Magnetometer
31+
from gz.msgs10.model_pb2 import Model
32+
# from gz.msgs10.navsat_pb2 import NavSat
33+
# from gz.msgs10.pose_pb2 import Pose
34+
# from gz.msgs10.sensor_pb2 import Sensor
35+
# from gz.msgs10.vector3d_pb2 import Vector3d
36+
# from gz.msgs10.visual_pb2 import Visual
37+
# from gz.msgs10.header_pb2 import Header
38+
elif GZ_VERSION == GZ_VERSION_IONIC:
39+
from gz.msgs11.model_pb2 import Model
40+
41+
42+
# def sensor_topics():
43+
# world = "iris_runway"
44+
# model = "iris_with_gimbal"
45+
# sub_model = "iris_with_standoffs"
46+
# link = "base_link"
47+
48+
# topic = (
49+
# f"/world/{world}/model/{model}"
50+
# f"/model/{sub_model}/link/{link}"
51+
# f"/sensor/air_pressure_sensor/air_pressure"
52+
# )
53+
54+
# topic = (
55+
# f"/world/{world}/model/{model}"
56+
# f"/model/{sub_model}/link/base_link"
57+
# f"/sensor/air_speed_sensor/air_speed"
58+
# )
59+
60+
# topic = (
61+
# f"/world/{world}/model/{model}"
62+
# f"/model/{sub_model}/link/{link}"
63+
# f"/sensor/altimeter_sensor/altimeter"
64+
# )
65+
66+
# topic = (
67+
# f"/world/{world}/model/{model}"
68+
# f"/model/{sub_model}/link/{link}"
69+
# f"/sensor/magnetometer_sensor/magnetometer"
70+
# )
71+
72+
# topic = (
73+
# f"/world/{world}/model/{model}"
74+
# f"/model/{sub_model}/link/{link}"
75+
# f"/sensor/navsat_sensor/navsat"
76+
# )
77+
78+
# link = "imu_link"
79+
80+
# topic = (
81+
# f"/world/{world}/model/{model}"
82+
# f"/model/{sub_model}/link/{link}"
83+
# f"/sensor/imu_sensor/imu"
84+
# )
85+
86+
87+
# Importing gz.transport13 into the module global scope causes an odd conflict
88+
# with dronecan. This is a workaround.
89+
def gz_node():
90+
if GZ_VERSION == GZ_VERSION_GARDEN:
91+
from gz.transport12 import Node
92+
elif GZ_VERSION == GZ_VERSION_HARMONIC:
93+
from gz.transport13 import Node
94+
elif GZ_VERSION == GZ_VERSION_IONIC:
95+
from gz.transport14 import Node
96+
97+
return Node()
98+
99+
100+
class JointStates:
101+
def __init__(self, world, model, debug):
102+
103+
self._world = world
104+
self._model = model
105+
self._debug = debug
32106

33-
from gz.transport13 import Node
107+
self._lock = threading.Lock()
108+
self._node = gz_node()
34109

35-
elif GZ_VERSION == GZ_VERSION_IONIC:
36-
from gz.msgs11.fluid_pressure_pb2 import FluidPressure
37-
from gz.msgs11.air_speed_pb2 import AirSpeed
38-
from gz.msgs11.altimeter_pb2 import Altimeter
39-
from gz.msgs11.magnetometer_pb2 import Magnetometer
40-
from gz.msgs11.navsat_pb2 import NavSat
41-
from gz.msgs11.imu_pb2 import IMU
42-
43-
from gz.transport14 import Node
44-
45-
46-
def sensor_topics():
47-
world = "iris_runway"
48-
model = "iris_with_gimbal"
49-
sub_model = "iris_with_standoffs"
50-
link = "base_link"
51-
52-
topic = (
53-
f"/world/{world}/model/{model}"
54-
f"/model/{sub_model}/link/{link}"
55-
f"/sensor/air_pressure_sensor/air_pressure"
56-
)
110+
# Model
111+
self._model_do_print_msg = self._debug
112+
self._model_msg = None
113+
self._model_topic = f"/world/{self._world}/model/{self._model}/joint_state"
114+
self._model_sub = self._node.subscribe(Model, self._model_topic, self._model_cb)
57115

58-
topic = (
59-
f"/world/{world}/model/{model}"
60-
f"/model/{sub_model}/link/base_link"
61-
f"/sensor/air_speed_sensor/air_speed"
62-
)
116+
def _model_cb(self, msg):
117+
with self._lock:
118+
self._model_msg = msg
119+
do_print_msg = self._model_do_print_msg
63120

64-
topic = (
65-
f"/world/{world}/model/{model}"
66-
f"/model/{sub_model}/link/{link}"
67-
f"/sensor/altimeter_sensor/altimeter"
68-
)
121+
# if do_print_msg:
122+
# print(msg)
69123

70-
topic = (
71-
f"/world/{world}/model/{model}"
72-
f"/model/{sub_model}/link/{link}"
73-
f"/sensor/magnetometer_sensor/magnetometer"
74-
)
124+
def joint_by_name(self, joint_name):
125+
with self._lock:
126+
model = self._model_msg
75127

76-
topic = (
77-
f"/world/{world}/model/{model}"
78-
f"/model/{sub_model}/link/{link}"
79-
f"/sensor/navsat_sensor/navsat"
80-
)
128+
if model is None:
129+
return None
81130

82-
link = "imu_link"
131+
# Find the first joint that matches
132+
matches = [x for x in model.joint if x.name == joint_name]
133+
if matches:
134+
return matches[0]
135+
else:
136+
return None
83137

84-
topic = (
85-
f"/world/{world}/model/{model}"
86-
f"/model/{sub_model}/link/{link}"
87-
f"/sensor/imu_sensor/imu"
88-
)
138+
def joint_velocity(self, joint_name):
139+
joint = self.joint_by_name(joint_name)
140+
if joint is None:
141+
return float("nan")
89142

143+
axis1 = joint.axis1
144+
vel_rads = axis1.velocity
145+
return vel_rads
90146

91-
class SensorSubscribers:
92-
def __init__(self):
93147

94-
world = "iris_runway"
95-
model = "iris_with_gimbal"
96-
sub_model = "iris_with_standoffs"
97-
link = "base_link"
148+
class DroneCANNode:
149+
def __init__(self, uri, node_id, rate, debug):
150+
self._uri = uri
151+
self._node_id = node_id
152+
self._rate = rate
153+
self._debug = debug
98154

155+
self._rpm = [0, 0, 0, 0]
156+
157+
self._node = None
99158
self._lock = threading.Lock()
100-
self._node = Node()
101-
102-
# Magnetometer
103-
self._sensor_do_print_msg = True
104-
self._sensor_msg = None
105-
self._sensor_topic = (
106-
f"/world/{world}/model/{model}"
107-
f"/model/{sub_model}/link/{link}"
108-
f"/sensor/magnetometer_sensor/magnetometer"
109-
)
110-
111-
self._sensor_sub = self._node.subscribe(
112-
Magnetometer, self._sensor_topic, self._sensor_cb
113-
)
114-
115-
def _sensor_cb(self, msg):
159+
self._task_thread = threading.Thread(target=self._run)
160+
self._task_thread.start()
161+
162+
def _run(self):
116163
with self._lock:
117-
self._sensor_msg = msg
118-
do_print_msg = self._sensor_do_print_msg
164+
uri = self._uri
165+
node_id = self._node_id
166+
rate = self._rate
119167

120-
if do_print_msg:
121-
print(msg)
168+
# Initialise a DroneCAN node instance.
169+
self._node = dronecan.make_node(uri, node_id=node_id, bitrate=1000000)
122170

171+
# Setup to publish sensor measurement
172+
self._node.periodic(1.0 / rate, self._pub_esc_status)
123173

124-
class DroneCANNode():
125-
def __init__(self):
126-
pass
174+
# Running the node
175+
while True:
176+
try:
177+
self._node.spin()
178+
except dronecan.transport.TransferError as ex:
179+
print(ex)
180+
pass
127181

182+
def _pub_esc_status(self):
183+
with self._lock:
184+
debug = self._debug
185+
rpm = self._rpm
186+
187+
# fake esc data
188+
# s = math.sin(time.time() * math.pi * 2)
189+
# rpm[0] = int(10000 + 1000 * s)
190+
# rpm[1] = int(10500 + 1000 * s)
191+
# rpm[2] = int(11000 + 1000 * s)
192+
# rpm[3] = int(11500 + 1000 * s)
193+
194+
msg = dronecan.uavcan.equipment.esc.Status()
195+
196+
# esc 0, 1, 2, 3
197+
msg.error_count = 0
198+
msg.voltage = 4.0 * 4.2
199+
msg.current = 5.0
200+
msg.temperature = 45.0 + 273.15
201+
msg.power_rating_pct = 50
202+
203+
msg.esc_index = 0
204+
msg.rpm = abs(rpm[msg.esc_index])
205+
self._node.broadcast(msg)
206+
207+
msg.esc_index = 1
208+
msg.rpm = abs(rpm[msg.esc_index])
209+
self._node.broadcast(msg)
210+
211+
msg.esc_index = 2
212+
msg.rpm = abs(rpm[msg.esc_index])
213+
self._node.broadcast(msg)
214+
215+
msg.esc_index = 3
216+
msg.rpm = abs(rpm[msg.esc_index])
217+
self._node.broadcast(msg)
218+
219+
if debug:
220+
# print(f"idx {msg.esc_index}, rpm: {msg.rpm}")
221+
print(dronecan.to_yaml(msg))
222+
223+
def set_rpm(self, esc_index, rpm):
224+
with self._lock:
225+
self._rpm[esc_index] = rpm
128226

129227

228+
def main():
229+
# Command line args
230+
parser = ArgumentParser(description="Publish DroneCAN ESC")
231+
parser.add_argument("uri", default=None, type=str, help="CAN URI")
232+
parser.add_argument("--node-id", default=100, type=int, help="CAN node ID")
233+
parser.add_argument("--rate", type=float, default=50, help="broadcast rate Hz")
234+
parser.add_argument("--debug", action="store_true", help="enable debug")
235+
parser.add_argument("--world", default="iris_runway", type=str, help="world name")
236+
parser.add_argument(
237+
"--model", default="iris_with_gimbal", type=str, help="model name"
238+
)
239+
args = parser.parse_args()
130240

241+
# Subscribe to joint states
242+
joint_states = JointStates(args.world, args.model, args.debug)
131243

132-
def main():
133-
sensor_topics()
244+
# DroneCAN node
245+
dronecan_node = DroneCANNode(args.uri, args.node_id, args.rate, args.debug)
134246

135-
sensor_subs = SensorSubscribers()
247+
def rads_to_rpm(vel_rads):
248+
if math.isnan(vel_rads):
249+
return 0
250+
else:
251+
return vel_rads * 30.0 / math.pi
136252

253+
# Run the node
137254
while True:
255+
# Read from joint states
256+
rpm0 = int(rads_to_rpm(joint_states.joint_velocity("rotor_0_joint")))
257+
rpm1 = int(rads_to_rpm(joint_states.joint_velocity("rotor_1_joint")))
258+
rpm2 = int(rads_to_rpm(joint_states.joint_velocity("rotor_2_joint")))
259+
rpm3 = int(rads_to_rpm(joint_states.joint_velocity("rotor_3_joint")))
260+
261+
# Write to dronecan node
262+
dronecan_node.set_rpm(0, rpm0)
263+
dronecan_node.set_rpm(1, rpm1)
264+
dronecan_node.set_rpm(2, rpm2)
265+
dronecan_node.set_rpm(3, rpm3)
266+
138267
time.sleep(0.01)
139268

140269

0 commit comments

Comments
 (0)