This commit is contained in:
2026-08-27 18:24:22 +02:00
parent 3d47f479e6
commit 10cd37eea7
26 changed files with 4905 additions and 860 deletions
+29 -6
View File
@@ -71,6 +71,11 @@ class MavlinkWorker:
self.axes = Axes()
self.output_axes = Axes()
self.last_control_at = 0.0
# Enabled by the web bridge only; the legacy Tk frontend is unchanged.
self.require_input_lease = False
self.last_input_at = 0.0
self.last_input_sequence = -1
self.input_timeout = 1.0
# Smooth ESC commands without adding a perceptible one-second lag.
# At 20 Hz this produces five small steps from neutral to full command.
self.ramp_seconds = 0.25
@@ -200,6 +205,10 @@ class MavlinkWorker:
payload = str(msg)
self.log("RX", payload)
if msg_type == "HEARTBEAT" and getattr(msg, "type", 0) != mavutil.mavlink.MAV_TYPE_GCS:
if getattr(msg, "autopilot", mavutil.mavlink.MAV_AUTOPILOT_INVALID) == mavutil.mavlink.MAV_AUTOPILOT_INVALID:
return
if self.target_system and (msg.get_srcSystem(), msg.get_srcComponent()) != (self.target_system, self.target_component):
return
first = not self.target_system
self.target_system = msg.get_srcSystem()
self.target_component = msg.get_srcComponent()
@@ -213,15 +222,17 @@ class MavlinkWorker:
mode = str(msg.custom_mode)
autopilot = mavutil.mavlink.enums["MAV_AUTOPILOT"].get(msg.autopilot)
ap_name = autopilot.name if autopilot else str(msg.autopilot)
self.events.put(("heartbeat", self.target_system, ap_name, mode, armed))
self.events.put(("heartbeat", self.target_system, ap_name, mode, armed, self.last_heartbeat))
if first:
self.events.put(("status", "Heartbeat received; controls are neutral."))
self._configure_low_bandwidth_streams()
self._start_servo_scan()
elif msg_type == "COMMAND_ACK":
if (msg.get_srcSystem(), msg.get_srcComponent()) != (self.target_system, self.target_component):
return
result = mavutil.mavlink.enums["MAV_RESULT"].get(msg.result)
result_name = result.name if result else str(msg.result)
self.events.put(("ack", int(msg.command), result_name))
self.events.put(("ack", int(msg.command), result_name, self.last_rx))
elif msg_type == "STATUSTEXT":
text = msg.text
if isinstance(text, bytes):
@@ -343,10 +354,17 @@ class MavlinkWorker:
if not conn or not self.target_system:
return
with self.lock:
active = not force_neutral
now = time.monotonic()
expired = self.require_input_lease and (
not self.armed or now - self.last_heartbeat > 3.0
or now - self.last_input_at > self.input_timeout)
if expired or force_neutral:
# Never replay a stored command after telemetry/UI recovery.
self.axes = Axes()
self.output_axes = Axes()
active = not (force_neutral or expired)
target = self.axes if active else Axes()
gain = self.gain
now = time.monotonic()
if force_neutral:
self.output_axes = Axes()
else:
@@ -418,7 +436,7 @@ class MavlinkWorker:
manual,
)
def set_axes(self, axes: Axes):
def set_axes(self, axes: Axes, sequence=None):
def conditioned(value):
# Ignore tiny pointer/key return noise, then quantize to stable 1% steps.
if abs(value) < 0.04:
@@ -426,6 +444,11 @@ class MavlinkWorker:
return round(max(-1.0, min(1.0, value)), 2)
with self.lock:
if sequence is not None:
if sequence <= self.last_input_sequence:
return
self.last_input_sequence = sequence
self.last_input_at = time.monotonic()
self.axes = Axes(
conditioned(axes.forward), conditioned(axes.lateral),
conditioned(axes.vertical), conditioned(axes.yaw),
@@ -1185,7 +1208,7 @@ class App(tk.Tk):
event = self.events.get_nowait()
kind = event[0]
if kind == "heartbeat":
_, system, autopilot, mode, armed = event
_, system, autopilot, mode, armed = event[:5]
self.vehicle_label.configure(
text=f"{mode} • {'ARMED' if armed else 'DISARMED'}"
)