Skip to content

Commit 7aecc5a

Browse files
committed
mavproxy_param: handle param set requests asynchronously
Stop using pymavlink's mavparm.mavset method as its retry logic is synchronous, meaning MAVProxy stops processing in the main loop in the case there are link issues occuring Creates an internal queue of parameters to send. These are copied into a dictionary which is processed in mavproy_param's idle task.
1 parent b39a31c commit 7aecc5a

File tree

1 file changed

+175
-9
lines changed

1 file changed

+175
-9
lines changed

MAVProxy/modules/mavproxy_param.py

Lines changed: 175 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@
2828
# py3
2929
from io import BytesIO as SIO
3030

31+
try:
32+
import queue as Queue
33+
from queue import Empty
34+
except ImportError:
35+
import Queue
36+
from Queue import Empty
37+
3138

3239
class ParamState:
3340
'''this class is separated to make it possible to use the parameter
@@ -56,6 +63,141 @@ def __init__(self, mav_param, logdir, vehicle_name, parm_file, mpstate, sysid):
5663
self.default_params = None
5764
self.watch_patterns = set()
5865

66+
# dictionary of ParamSet objects we are processing:
67+
self.parameters_to_set = {}
68+
# a Queue which onto which ParamSet objects can be pushed in a
69+
# thread-safe manner:
70+
self.parameters_to_set_input_queue = Queue.Queue()
71+
72+
class ParamSet():
73+
'''class to hold information about a parameter set being attempted'''
74+
def __init__(self, master, name, value, param_type=None, attempts=None):
75+
self.master = master
76+
self.name = name
77+
self.value = value
78+
self.param_type = param_type
79+
self.attempts_remaining = attempts
80+
self.retry_interval = 1 # seconds
81+
self.last_value_received = None
82+
83+
if self.attempts_remaining is None:
84+
self.attempts_remaining = 3
85+
86+
self.request_sent = 0 # this is a timestamp
87+
88+
def normalize_parameter_for_param_set_send(self, name, value, param_type):
89+
'''uses param_type to convert value into a value suitable for passing
90+
into the mavlink param_set_send binding. Note that this
91+
is a copy of a method in pymavlink, in case the user has
92+
an older version of that library.
93+
'''
94+
if param_type is not None and param_type != mavutil.mavlink.MAV_PARAM_TYPE_REAL32:
95+
# need to encode as a float for sending
96+
if param_type == mavutil.mavlink.MAV_PARAM_TYPE_UINT8:
97+
vstr = struct.pack(">xxxB", int(value))
98+
elif param_type == mavutil.mavlink.MAV_PARAM_TYPE_INT8:
99+
vstr = struct.pack(">xxxb", int(value))
100+
elif param_type == mavutil.mavlink.MAV_PARAM_TYPE_UINT16:
101+
vstr = struct.pack(">xxH", int(value))
102+
elif param_type == mavutil.mavlink.MAV_PARAM_TYPE_INT16:
103+
vstr = struct.pack(">xxh", int(value))
104+
elif param_type == mavutil.mavlink.MAV_PARAM_TYPE_UINT32:
105+
vstr = struct.pack(">I", int(value))
106+
elif param_type == mavutil.mavlink.MAV_PARAM_TYPE_INT32:
107+
vstr = struct.pack(">i", int(value))
108+
else:
109+
print("can't send %s of type %u" % (name, param_type))
110+
return None
111+
numeric_value, = struct.unpack(">f", vstr)
112+
else:
113+
if isinstance(value, str) and value.lower().startswith('0x'):
114+
numeric_value = int(value[2:], 16)
115+
else:
116+
try:
117+
numeric_value = float(value)
118+
except ValueError:
119+
print(f"can't convert {name} ({value}, {type(value)}) to float")
120+
return None
121+
122+
return numeric_value
123+
124+
def send_set(self):
125+
numeric_value = self.normalize_parameter_for_param_set_send(self.name, self.value, self.param_type)
126+
if numeric_value is None:
127+
print(f"can't send {self.name} of type {self.param_type}")
128+
self.attempts_remaining = 0
129+
return
130+
# print(f"Sending set attempts-remaining={self.attempts_remaining}")
131+
self.master.param_set_send(
132+
self.name.upper(),
133+
numeric_value,
134+
parm_type=self.param_type,
135+
)
136+
self.request_sent = time.time()
137+
self.attempts_remaining -= 1
138+
139+
def expired(self):
140+
if self.attempts_remaining > 0:
141+
return False
142+
return time.time() - self.request_sent > self.retry_interval
143+
144+
def due_for_retry(self):
145+
if self.attempts_remaining <= 0:
146+
return False
147+
return time.time() - self.request_sent > self.retry_interval
148+
149+
def handle_PARAM_VALUE(self, m, value):
150+
'''handle PARAM_VALUE packet m which has already been checked for a
151+
match against self.name. Returns true if this Set is now
152+
satisfied. value is the value extracted and potentially
153+
manipulated from the packet
154+
'''
155+
self.last_value_received = value
156+
if abs(value - float(self.value)) > 0.00001:
157+
return False
158+
159+
return True
160+
161+
def print_expired_message(self):
162+
reason = ""
163+
if self.last_value_received is None:
164+
reason = " (no PARAM_VALUE received)"
165+
else:
166+
reason = f" (invalid returned value {self.last_value_received})"
167+
print(f"Failed to set {self.name} to {self.value}{reason}")
168+
169+
def run_parameter_set_queue(self):
170+
# firstly move anything from the input queue into our
171+
# collection of things to send:
172+
try:
173+
while True:
174+
new_parameter_to_set = self.parameters_to_set_input_queue.get(block=False)
175+
self.parameters_to_set[new_parameter_to_set.name] = new_parameter_to_set
176+
except Empty:
177+
pass
178+
179+
# now send any parameter-sets which are due to be sent out,
180+
# either because they are new or because we need to retry:
181+
count = 0
182+
keys_to_remove = [] # remove entries after iterating the dict
183+
for (key, parameter_to_set) in self.parameters_to_set.items():
184+
if parameter_to_set.expired():
185+
parameter_to_set.print_expired_message()
186+
keys_to_remove.append(key)
187+
continue
188+
if not parameter_to_set.due_for_retry():
189+
continue
190+
# send parameter set:
191+
parameter_to_set.send_set()
192+
# rate-limit to 10 items per call:
193+
count += 1
194+
if count > 10:
195+
break
196+
197+
# complete purging of expired parameter-sets:
198+
for key in keys_to_remove:
199+
del self.parameters_to_set[key]
200+
59201
def use_ftp(self):
60202
'''return true if we should try ftp for download'''
61203
if self.ftp_failed:
@@ -132,6 +274,16 @@ def handle_mavlink_packet(self, master, m):
132274
self.fetch_set = None
133275
if self.fetch_set is not None and len(self.fetch_set) == 0:
134276
self.fetch_check(master, force=True)
277+
278+
# if we were setting this parameter then check it's the
279+
# value we want and, if so, stop setting the parameter
280+
try:
281+
if self.parameters_to_set[param_id].handle_PARAM_VALUE(m, value):
282+
# print(f"removing set of param_id ({self.parameters_to_set[param_id].value} vs {value})")
283+
del self.parameters_to_set[param_id]
284+
except KeyError:
285+
pass
286+
135287
elif m.get_type() == 'HEARTBEAT':
136288
if m.get_srcComponent() == 1:
137289
# remember autopilot types so we can handle PX4 parameters
@@ -493,7 +645,21 @@ def param_bitmask_modify(self, master, args):
493645
return
494646

495647
# Update the parameter
496-
self.mav_param.mavset(master, uname, value, retries=3, parm_type=ptype)
648+
self.set_parameter(master, uname, value, attempts=3, parm_type=ptype)
649+
650+
def set_parameter(self, master, name, value, attempts=None, param_type=None):
651+
'''convenient intermediate method which determines parameter type for
652+
lazy callers'''
653+
if param_type is None:
654+
param_type = self.param_types.get(name, None)
655+
656+
self.parameters_to_set_input_queue.put(ParamState.ParamSet(
657+
master,
658+
name,
659+
value,
660+
attempts=attempts,
661+
param_type=param_type,
662+
))
497663

498664
def param_revert(self, master, args):
499665
'''handle param revert'''
@@ -519,10 +685,7 @@ def param_revert(self, master, args):
519685
if s1 == s2:
520686
continue
521687
print("Reverting %-16.16s %s -> %s" % (p, s1, s2))
522-
ptype = None
523-
if p in self.param_types:
524-
ptype = self.param_types[p]
525-
self.mav_param.mavset(master, p, defaults[p], retries=3, parm_type=ptype)
688+
self.set_parameter(master, p, defaults[p], attempts=3)
526689
count += 1
527690
print("Reverted %u parameters" % count)
528691

@@ -596,10 +759,7 @@ def handle_command(self, master, mpstate, args):
596759
print("Unable to find parameter '%s'" % param)
597760
return
598761
uname = param.upper()
599-
ptype = None
600-
if uname in self.param_types:
601-
ptype = self.param_types[uname]
602-
self.mav_param.mavset(master, uname, value, retries=3, parm_type=ptype)
762+
self.set_parameter(master, uname, value, attempts=3)
603763

604764
if (param.upper() == "WP_LOITER_RAD" or param.upper() == "LAND_BREAK_PATH"):
605765
# need to redraw rally points
@@ -881,6 +1041,12 @@ def idle_task(self):
8811041
else:
8821042
self.menu_added_console = False
8831043

1044+
self.run_parameter_set_queues()
1045+
1046+
def run_parameter_set_queues(self):
1047+
for pstate in self.pstate.values():
1048+
pstate.run_parameter_set_queue()
1049+
8841050
def cmd_param(self, args):
8851051
'''control parameters'''
8861052
self.check_new_target_system()

0 commit comments

Comments
 (0)