Skip to content

Commit 474a9fe

Browse files
committed
Swarm: add standalone ArduPilot Gazebo bridge
Signed-off-by: Rhys Mainwaring <rhys.mainwaring@me.com>
1 parent 657cb44 commit 474a9fe

1 file changed

Lines changed: 284 additions & 0 deletions

File tree

scripts/ardupilot_gazebo.py

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
"""
2+
ArduPilot Gazebo Bridge - carry out similar functions to the ArduPilotPlugin
3+
4+
Based on example: ardupilot/libraries/SITL/examples/JSON/pybullet/robot.py
5+
"""
6+
7+
import copy
8+
import json
9+
import math
10+
import os
11+
import socket
12+
import struct
13+
import time
14+
15+
from dataclasses import dataclass
16+
from argparse import ArgumentParser
17+
from pathlib import Path
18+
from transforms3d import euler
19+
20+
GZ_VERSION_GARDEN = "garden"
21+
GZ_VERSION_HARMONIC = "harmonic"
22+
GZ_VERSION_IONIC = "ionic"
23+
GZ_VERSION_JETTY = "jetty"
24+
25+
# Index convention for transforms3d quaternions
26+
QUAT_IDX_W = 0
27+
QUAT_IDX_X = 1
28+
QUAT_IDX_Y = 2
29+
QUAT_IDX_Z = 3
30+
31+
# Constants
32+
MAGIC = 18458
33+
RATE_HZ = 1000
34+
TIME_STEP = 1.0 / RATE_HZ
35+
36+
37+
def gz_version():
38+
"""Return the environment variable GZ_VERSION if set, else default to 'harmonic'"""
39+
return os.environ.get("GZ_VERSION", GZ_VERSION_HARMONIC)
40+
41+
42+
if gz_version() == GZ_VERSION_JETTY:
43+
from gz.msgs.boolean_pb2 import Boolean
44+
from gz.msgs.entity_factory_pb2 import EntityFactory
45+
from gz.msgs.entity_factory_v_pb2 import EntityFactory_V
46+
from gz.msgs.stringmsg_pb2 import StringMsg
47+
48+
49+
# Importing gz.transport into the module global scope causes an odd
50+
# multiprocessing conflict with dronecan. This is a workaround.
51+
def gz_node():
52+
if gz_version() == GZ_VERSION_JETTY:
53+
from gz.transport import Node
54+
55+
return Node()
56+
57+
58+
@dataclass
59+
class Control:
60+
channel: int = 0
61+
type: str = "COMMAND"
62+
use_force: bool = True
63+
joint_name: str = ""
64+
cmd_topic: str = ""
65+
multipler: float = 1.0
66+
offset: float = 0.0
67+
servo_min: float = 1000.0
68+
servo_max: float = 2000.0
69+
output_ready: bool = False
70+
71+
72+
class ArduPilotGazeboBridge:
73+
"""Python port of ArduPilotPlugin.hh"""
74+
75+
def __init__(self, args):
76+
# Configuration
77+
self.world_name = args.world
78+
self.model_name = args.model
79+
80+
# Socket settings
81+
self.address = args.address
82+
self.port = args.port
83+
84+
# Service call timeout
85+
self.timeout = args.timeout
86+
87+
# Connection
88+
self.sock = None
89+
self.last_sitl_frame = -1
90+
self.connected = False
91+
92+
# Packet
93+
self.pwm = None
94+
95+
# Stats
96+
self.frame_count = 0
97+
self.frame_time = time.monotonic()
98+
self.print_frame_count = 1000
99+
100+
# Payload
101+
self.json_str = None
102+
self.start_time = time.monotonic()
103+
104+
# Transport
105+
self.node = gz_node()
106+
self.controls = []
107+
self.pub_commands = []
108+
self.sub_clock = None
109+
self.sub_imu = None
110+
self.sub_pose_info = None
111+
112+
def configure(self):
113+
# Configure model
114+
# TODO
115+
# Hardcoded example to start
116+
control = Control()
117+
control.
118+
119+
120+
# Setup connection
121+
self._init_sockets()
122+
123+
def run(self):
124+
global MAGIC
125+
global RATE_HZ
126+
global TIME_STEP
127+
128+
while True:
129+
# pre-update
130+
# TODO
131+
# self._receive_servo_packet()
132+
try:
133+
data, address = self.sock.recvfrom(100)
134+
except Exception:
135+
time.sleep(0.01)
136+
continue
137+
138+
parse_format = "HHI16H"
139+
if len(data) != struct.calcsize(parse_format):
140+
print(f"Bad packet size: {len(data)}")
141+
continue
142+
143+
decoded = struct.unpack(parse_format, data)
144+
magic = MAGIC
145+
if decoded[0] != magic:
146+
print(f"Incorrect magic: {decoded[0]}")
147+
continue
148+
149+
frame_rate_hz = decoded[1]
150+
frame_number = decoded[2]
151+
self.pwm = decoded[3:]
152+
153+
if frame_rate_hz != RATE_HZ:
154+
RATE_HZ = frame_rate_hz
155+
TIME_STEP = 1.0 / RATE_HZ
156+
# p.setTimeStep(TIME_STEP)
157+
# print(f"Updated rate to {RATE_HZ} Hz")
158+
159+
if frame_number < self.last_sitl_frame:
160+
# vehicle.reset()
161+
time_now = 0.0
162+
print("Controller reset")
163+
elif frame_number != self.last_sitl_frame + 1 and self.connected:
164+
print(f"Missed {frame_number - self.last_sitl_frame - 1} frames")
165+
166+
self.last_sitl_frame = frame_number
167+
168+
if not self.connected:
169+
self.connected = True
170+
print(f"Connected to {address}")
171+
172+
self.frame_count += 1
173+
174+
# post-update
175+
# TODO
176+
# self._create_state_json()
177+
# self._send_state()
178+
phys_time = time.monotonic() - self.start_time
179+
gyro = [0.0, 0.0, 0.0]
180+
accel = [0.0, 0.0, 0.0]
181+
pos = [0.0, 0.0, 0.0]
182+
vel = [0.0, 0.0, 0.0]
183+
quat = [0.0, 0.0, 0.0, 1.0]
184+
185+
json_data = {
186+
"timestamp": phys_time,
187+
"imu": {"gyro": gyro, "accel_body": accel},
188+
"position": pos,
189+
"quaternion": quat,
190+
"velocity": vel,
191+
}
192+
193+
self.sock.sendto(
194+
(json.dumps(json_data, separators=(",", ":")) + "\n").encode("ascii"),
195+
address,
196+
)
197+
198+
if self.frame_count % self.print_frame_count == 0:
199+
now = time.time()
200+
total_time = now - self.frame_time
201+
print(
202+
f"{self.print_frame_count/total_time:.2f} "
203+
f"fps T={phys_time:.3f} "
204+
f"dt={total_time:.3f}"
205+
)
206+
print(f"pwm: {self.pwm}")
207+
208+
self.frame_time = now
209+
210+
def _init_sockets(self):
211+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
212+
self.sock.bind((self.address, self.port))
213+
self.sock.settimeout(0.1)
214+
215+
def _receive_servo_packet(self):
216+
pass
217+
# try:
218+
# data, address = self.sock.recvfrom(100)
219+
# except Exception:
220+
# time.sleep(0.01)
221+
# continue
222+
223+
def _create_state_json(self):
224+
pass
225+
# timestamp = time.monotonic() - self.start_time
226+
227+
# self.json_str = json.dumps(
228+
# {
229+
# "timestamp": timestamp,
230+
# "imu": {"gyro": [0.0, 0.0, 0.0], "accel_body": [0.0, 0.0, 0.0]},
231+
# "position": [0.0, 0.0, 0.0],
232+
# "quaternion": [0.0, 0.0, 0.0, 1.0],
233+
# "velocity": [0.0, 0.0, 0.0],
234+
# }
235+
# )
236+
# print(self.json_str)
237+
# print(len(self.json_str))
238+
239+
def _send_state(self):
240+
pass
241+
# self.sock.sendto(self.json_str, self.address, self.port)
242+
243+
244+
def main():
245+
# Command line args
246+
parser = ArgumentParser(description="Launch ArduPilot Gazebo Bridge")
247+
parser.add_argument("--world", default="runway", type=str, help="world name")
248+
parser.add_argument(
249+
"--model", default="iris_with_ardupilot", type=str, help="model name"
250+
)
251+
parser.add_argument(
252+
"--address", default="127.0.0.1", type=str, help="SITL IPv4 address"
253+
)
254+
parser.add_argument("--port", default="9002", type=int, help="SITL port")
255+
parser.add_argument(
256+
"--timeout", default="5000", type=int, help="timeout for service calls"
257+
)
258+
259+
args = parser.parse_args()
260+
261+
json_str = json.dumps(
262+
{
263+
"timestamp": 1,
264+
"imu": {"gyro": [0.0, 0.0, 0.0], "accel_body": [0.0, 0.0, 0.0]},
265+
"position": [0.0, 0.0, 0.0],
266+
"quaternion": [0.0, 0.0, 0.0, 1.0],
267+
"velocity": [0.0, 0.0, 0.0],
268+
}
269+
)
270+
# print(json_str)
271+
# print(len(json_str))
272+
273+
# ======================================================================= #
274+
# Create bridge
275+
276+
gz_bridge = ArduPilotGazeboBridge(args)
277+
gz_bridge.configure()
278+
gz_bridge.run()
279+
280+
# ======================================================================= #
281+
282+
283+
if __name__ == "__main__":
284+
main()

0 commit comments

Comments
 (0)