Skip to content

Commit 2a442ea

Browse files
Ali BarariAli Barari
authored andcommitted
autotest: add test for MAVLink wheel encoders
Streams WHEEL_DISTANCE for four wheels at a few Hz from a component on the vehicle, and checks that every sample sent to EKF3 carries the true wheel speed, that no readings are lost, that a gap in the stream is not fused as one long reading, that all four distances are logged, and that WHEEL_DISTANCE from another system is ignored.
1 parent f663a06 commit 2a442ea

1 file changed

Lines changed: 143 additions & 0 deletions

File tree

‎Tools/autotest/rover.py‎

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4826,6 +4826,148 @@ def WheelEncoders(self):
48264826
raise NotAchievedException("wheel distance incorrect")
48274827
self.disarm_vehicle()
48284828

4829+
def MAVLinkWheelEncoders(self):
4830+
'''test wheel encoders fed by WHEEL_DISTANCE messages'''
4831+
radius = 0.05
4832+
# one encoder on each of four wheels, told apart by position
4833+
wheel_positions = [(0.1, -0.1), (0.1, 0.1), (-0.1, -0.1), (-0.1, 0.1)]
4834+
params = {
4835+
"LOG_DISARMED": 1,
4836+
"LOG_REPLAY": 1, # RWOH records each sample sent to EKF3
4837+
}
4838+
for i, (x, y) in enumerate(wheel_positions):
4839+
prefix = "WENC" if i == 0 else "WENC%u" % (i+1)
4840+
params[prefix + "_TYPE"] = 2
4841+
params[prefix + "_RADIUS"] = radius
4842+
params[prefix + "_POS_X"] = x
4843+
params[prefix + "_POS_Y"] = y
4844+
self.set_parameters(params)
4845+
self.reboot_sitl()
4846+
4847+
# the encoder board is a component on this vehicle
4848+
board = mavutil.mavlink_connection(
4849+
self.sitl_serial_endpoint(2),
4850+
source_system=self.sysid_thismav(),
4851+
source_component=mavutil.mavlink.MAV_COMP_ID_PERIPHERAL,
4852+
)
4853+
4854+
def send_wheel_distance(conn, distances):
4855+
conn.mav.wheel_distance_send(
4856+
int(self.get_sim_time_cached() * 1e6),
4857+
len(distances),
4858+
distances + [0] * (16 - len(distances)),
4859+
)
4860+
4861+
# the board sends at 5Hz, slower than the vehicle's 50Hz wheel
4862+
# encoder loop, with all wheels optionally moving at speed
4863+
board_distances = [1.5, -2.25, 3.0, -0.75]
4864+
speed = 0.5
4865+
motion = {"start": None, "stop": None, "paused": False}
4866+
last_sent = [0]
4867+
4868+
def send_from_board(mav, m):
4869+
now = self.get_sim_time_cached()
4870+
if motion["paused"] or now - last_sent[0] < 0.2:
4871+
return
4872+
last_sent[0] = now
4873+
travelled = 0
4874+
if motion["start"] is not None:
4875+
travelled = speed * (now - motion["start"])
4876+
send_wheel_distance(board, [d + travelled for d in board_distances])
4877+
4878+
def wait_reported_distances(distances, timeout=10):
4879+
tstart = self.get_sim_time()
4880+
while self.get_sim_time_cached() - tstart < timeout:
4881+
m = self.assert_receive_message('WHEEL_DISTANCE', timeout=5)
4882+
if m.get_srcComponent() != mavutil.mavlink.MAV_COMP_ID_AUTOPILOT1:
4883+
# the board's own messages are routed to us too
4884+
continue
4885+
if (m.count == len(distances) and
4886+
all(abs(m.distance[i] - d) < 0.001 for i, d in enumerate(distances))):
4887+
return
4888+
raise NotAchievedException("Vehicle did not report wheel distances %s" % str(distances))
4889+
4890+
self.context_push()
4891+
try:
4892+
self.install_message_hook_context(send_from_board)
4893+
4894+
self.start_subtest("Distances from the vehicle's own encoder board are used")
4895+
wait_reported_distances(board_distances)
4896+
4897+
self.start_subtest("Distances from another vehicle are ignored")
4898+
tstart = self.get_sim_time()
4899+
while self.get_sim_time_cached() - tstart < 5:
4900+
# our GCS connection has a different system ID
4901+
send_wheel_distance(self.mav, [99.0] * 4)
4902+
m = self.assert_receive_message('WHEEL_DISTANCE', timeout=5)
4903+
if (m.get_srcComponent() == mavutil.mavlink.MAV_COMP_ID_AUTOPILOT1 and
4904+
abs(m.distance[0] - 99.0) < 1):
4905+
raise NotAchievedException("Accepted wheel distance from another system")
4906+
4907+
self.start_subtest("Stream wheels moving at %.1fm/s, with a gap in the stream" % speed)
4908+
motion["start"] = self.get_sim_time()
4909+
self.delay_sim_time(5, reason="let the wheels turn")
4910+
motion["paused"] = True
4911+
pause_start = self.get_sim_time()
4912+
self.delay_sim_time(1.5, reason="gap in the stream")
4913+
motion["paused"] = False
4914+
pause_stop = self.get_sim_time()
4915+
self.delay_sim_time(5, reason="let the wheels turn")
4916+
motion["stop"] = self.get_sim_time()
4917+
finally:
4918+
self.context_pop()
4919+
board.close()
4920+
4921+
self.start_subtest("All four wheels are logged and EKF3 is sent the true wheel speed")
4922+
# ignore samples straddling the start or end of motion
4923+
window_start = motion["start"] + 1
4924+
window_stop = motion["stop"] - 1
4925+
dfreader = self.dfreader_for_current_onboard_log()
4926+
logged_stationary_distances = False
4927+
samples = {} # wheel position -> samples sent to EKF3
4928+
while True:
4929+
m = dfreader.recv_match(type=['WENC', 'RWOH'])
4930+
if m is None:
4931+
break
4932+
if m.get_type() == 'WENC':
4933+
if all(abs(getattr(m, "Dist%u" % i) - d) < 0.001 for i, d in enumerate(board_distances)):
4934+
logged_stationary_distances = True
4935+
elif window_start < m.TS * 0.001 < window_stop:
4936+
samples.setdefault((round(m.PX, 1), round(m.PY, 1)), []).append(m)
4937+
if not logged_stationary_distances:
4938+
raise NotAchievedException("WENC never logged distances %s" % str(board_distances))
4939+
if sorted(samples.keys()) != sorted(wheel_positions):
4940+
raise NotAchievedException("Expected samples from all four wheels, got %s" % str(samples.keys()))
4941+
4942+
expected_coverage = (window_stop - window_start) - (pause_stop - pause_start)
4943+
for wheel, wheel_samples in samples.items():
4944+
# the stream's timing is jittery in SITL, so allow a wide
4945+
# margin per sample; stale readings fed as zero rotation, a
4946+
# reading divided by a too-short interval, or one spanning the
4947+
# gap in the stream, fall well outside
4948+
for m in wheel_samples:
4949+
sample_speed = m.DA * m.R / m.DT
4950+
if m.DT > 0.5 or not 0.25 * speed < sample_speed < 2 * speed:
4951+
raise NotAchievedException(
4952+
"Wheel at %s sent EKF3 speed %.3fm/s (DA=%f DT=%f), want %.1fm/s" %
4953+
(str(wheel), sample_speed, m.DA, m.DT, speed))
4954+
# readings must resume after the gap
4955+
if not any(m.TS * 0.001 > pause_stop + 1 for m in wheel_samples):
4956+
raise NotAchievedException("Wheel at %s not sent to EKF3 after the gap" % str(wheel))
4957+
# and apart from the gap no readings may be lost
4958+
covered = sum(m.DT for m in wheel_samples)
4959+
travelled = sum(m.DA * m.R for m in wheel_samples)
4960+
if covered < 0.85 * expected_coverage:
4961+
raise NotAchievedException(
4962+
"Wheel at %s samples cover only %.1fs of %.1fs" %
4963+
(str(wheel), covered, expected_coverage))
4964+
if abs(travelled / covered - speed) > 0.05 * speed:
4965+
raise NotAchievedException(
4966+
"Wheel at %s mean speed %.3fm/s, want %.1fm/s" %
4967+
(str(wheel), travelled / covered, speed))
4968+
self.progress("Wheel at %s: %u samples, mean speed %.3fm/s over %.1fs" %
4969+
(str(wheel), len(wheel_samples), travelled / covered, covered))
4970+
48294971
def test_poly_fence_object_avoidance_guided_two_squares(self, target_system=1, target_component=1):
48304972
self.start_subtest("Ensure we can steer around obstacles in guided mode")
48314973
here = self.get_location()
@@ -8182,6 +8324,7 @@ def tests(self):
81828324
# self.GCSRally, # enable me in 2026, needs MAVProxy fixes
81838325
self.MotorTest,
81848326
self.WheelEncoders,
8327+
self.MAVLinkWheelEncoders,
81858328
self.DataFlashOverMAVLink,
81868329
self.DataFlash,
81878330
self.SkidSteer,

0 commit comments

Comments
 (0)