55
66import argparse
77from pathlib import Path
8+ import shutil
9+ import socket
10+ import struct
811import tempfile
912import threading
1013import time
11-
12- from pymavlink import mavutil
14+ import traceback
1315
1416from dualrx_sitl_test import (
1517 BOX_BEEPER ,
4042UART_RX2 = 4
4143BOX_MSP_RC_OVERRIDE = 50
4244
45+ MAVLINK_V1_MAGIC = 0xFE
46+ MAVLINK_MSG_ID_RC_CHANNELS_OVERRIDE = 70
47+ MAVLINK_MSG_ID_RC_CHANNELS_OVERRIDE_CRC = 124
48+ MAVLINK_TARGET_SYSTEM = 1
49+ MAVLINK_TARGET_COMPONENT = 1
50+ MAVLINK_SOURCE_COMPONENT = 191
51+
4352
4453class PeriodicIngress :
4554 def __init__ (self , name : str , channels : list [int ], period_s : float = 0.02 ) -> None :
@@ -50,7 +59,7 @@ def __init__(self, name: str, channels: list[int], period_s: float = 0.02) -> No
5059 self ._lock = threading .Lock ()
5160 self ._stop = threading .Event ()
5261 self ._thread : threading .Thread | None = None
53- self .error : BaseException | None = None
62+ self .error : Exception | None = None
5463
5564 def start (self ) -> None :
5665 self ._thread = threading .Thread (target = self ._run_guarded , name = self .name , daemon = True )
@@ -69,12 +78,12 @@ def set_mode(self, mode: str) -> None:
6978
7079 def check (self ) -> None :
7180 if self .error is not None :
72- raise TestFailure (f"{ self .name } background thread failed: { self .error } " )
81+ raise TestFailure (f"{ self .name } background thread failed: { type ( self .error ). __name__ } : { self . error } " ) from self . error
7382
7483 def _run_guarded (self ) -> None :
7584 try :
7685 self ._run ()
77- except BaseException as exc :
86+ except Exception as exc :
7887 if not self ._stop .is_set ():
7988 self .error = exc
8089 self ._stop .set ()
@@ -83,21 +92,64 @@ def _run(self) -> None:
8392 raise NotImplementedError
8493
8594
95+ def mavlink_x25_crc (data : bytes , crc : int = 0xFFFF ) -> int :
96+ for value in data :
97+ tmp = value ^ (crc & 0xFF )
98+ tmp ^= (tmp << 4 ) & 0xFF
99+ crc = ((crc >> 8 ) ^ (tmp << 8 ) ^ (tmp << 3 ) ^ (tmp >> 4 )) & 0xFFFF
100+ return crc
101+
102+
103+ def mavlink_v1_rc_override (seq : int , source_system : int , channels : list [int ]) -> bytes :
104+ if len (channels ) < 8 :
105+ raise ValueError ("RC_CHANNELS_OVERRIDE test frame needs at least 8 channels" )
106+ payload = struct .pack (
107+ "<8HBB" ,
108+ * [int (value ) & 0xFFFF for value in channels [:8 ]],
109+ MAVLINK_TARGET_SYSTEM ,
110+ MAVLINK_TARGET_COMPONENT ,
111+ )
112+ header = bytes ([
113+ len (payload ),
114+ seq & 0xFF ,
115+ source_system & 0xFF ,
116+ MAVLINK_SOURCE_COMPONENT ,
117+ MAVLINK_MSG_ID_RC_CHANNELS_OVERRIDE ,
118+ ])
119+ crc = mavlink_x25_crc (header + payload )
120+ crc = mavlink_x25_crc (bytes ([MAVLINK_MSG_ID_RC_CHANNELS_OVERRIDE_CRC ]), crc )
121+ return bytes ([MAVLINK_V1_MAGIC ]) + header + payload + struct .pack ("<H" , crc )
122+
123+
86124class MavlinkIngress (PeriodicIngress ):
87125 def __init__ (self , name : str , host : str , port : int , channels : list [int ], source_system : int ) -> None :
88126 super ().__init__ (name , channels )
89- self .connection = mavutil .mavlink_connection (
90- f"tcp:{ host } :{ port } " ,
91- source_system = source_system ,
92- source_component = 191 ,
93- autoreconnect = False ,
94- )
127+ self .source_system = source_system
128+ self .sequence = 0
129+ self .sock : socket .socket | None = None
130+ deadline = time .monotonic () + 5.0
131+ last_error : OSError | None = None
132+ while time .monotonic () < deadline :
133+ try :
134+ self .sock = socket .create_connection ((host , port ), timeout = 1.0 )
135+ self .sock .settimeout (None )
136+ break
137+ except OSError as exc :
138+ last_error = exc
139+ time .sleep (0.05 )
140+ if self .sock is None :
141+ raise TimeoutError (f"{ name } : could not connect to { host } :{ port } : { last_error } " )
95142
96143 def close (self ) -> None :
97144 super ().close ()
98- self .connection .close ()
145+ if self .sock is not None :
146+ try :
147+ self .sock .close ()
148+ except OSError :
149+ pass
99150
100151 def _run (self ) -> None :
152+ assert self .sock is not None
101153 next_send = time .monotonic ()
102154 while not self ._stop .is_set ():
103155 now = time .monotonic ()
@@ -107,11 +159,12 @@ def _run(self) -> None:
107159 if mode != "off" and now >= next_send :
108160 if mode == "partial" :
109161 channels [:4 ] = [0xFFFF ] * 4
110- self .connection .mav .rc_channels_override_send (1 , 1 , * channels [:8 ])
162+ frame = mavlink_v1_rc_override (self .sequence , self .source_system , channels )
163+ self .sock .sendall (frame )
164+ self .sequence = (self .sequence + 1 ) & 0xFF
111165 next_send = now + self .period_s
112166 elif mode == "off" :
113167 next_send = now
114- self .connection .recv_match (blocking = False )
115168 time .sleep (0.002 )
116169
117170
@@ -251,7 +304,12 @@ def exercise_legacy_msp_override(msp: MspClient, rx1, rx2) -> None:
251304
252305
253306def configure_case (kind : str ) -> list [str ]:
254- commands = ["set dual_rx_enabled = ON" , "aux 0 13 0 1700 2100" , "feature TELEMETRY" ]
307+ commands = [
308+ "set dual_rx_enabled = ON" ,
309+ "set mavlink_sysid = 1" ,
310+ "aux 0 13 0 1700 2100" ,
311+ "feature TELEMETRY" ,
312+ ]
255313 if kind == "crsf-mavlink" :
256314 commands += [
257315 "set receiver_type = SERIAL" ,
@@ -284,22 +342,62 @@ def configure_case(kind: str) -> list[str]:
284342 return commands
285343
286344
345+ def sitl_log_count (sitl : SitlProcess , marker : str ) -> int :
346+ try :
347+ text = sitl .log_path .read_text (encoding = "utf-8" , errors = "replace" )
348+ except OSError :
349+ return 0
350+ return text .count (marker )
351+
352+
353+ def wait_for_sitl_reset (sitl : SitlProcess , reset_count_before : int , timeout_s : float = 8.0 ) -> None :
354+ """Wait until CLI `save` has persisted configuration and entered reset."""
355+ deadline = time .monotonic () + timeout_s
356+ last_reset_count = reset_count_before
357+ while time .monotonic () < deadline :
358+ sitl .check_alive ()
359+ last_reset_count = sitl_log_count (sitl , "[SYSTEM] Reset" )
360+ if last_reset_count > reset_count_before :
361+ return
362+ time .sleep (0.02 )
363+ raise TestFailure (
364+ "SITL did not enter CLI save/reset cycle within "
365+ f"{ timeout_s :.1f} s (reset { reset_count_before } ->{ last_reset_count } )\n "
366+ f"{ sitl .tail_log (lines = 80 )} "
367+ )
368+
369+
370+ def hard_restart_sitl_after_configuration (sitl : SitlProcess , reset_count_before : int ) -> None :
371+ """Replace SITL's in-process exec reboot with a clean harness restart.
372+
373+ The mixed serial layout opens UARTs lazily. During the exec-based reboot a
374+ TCP listener can survive long enough for the new instance to hit EADDRINUSE,
375+ leaving a receiver connected to the dying listener. Once `save` has reached
376+ systemReset(), terminate that process completely and start a fresh one from
377+ the saved EEPROM so every UART listener is recreated from a clean process.
378+ """
379+ wait_for_sitl_reset (sitl , reset_count_before )
380+ sitl .stop ()
381+ time .sleep (0.05 )
382+ sitl .start ()
383+
384+
287385def run_case (kind : str , binary : Path , repo : Path , tcp_base : int , temp_dir : Path ) -> None :
288386 eeprom = temp_dir / f"{ kind } .bin"
289- sitl = SitlProcess (binary , repo , eeprom , tcp_base , temp_dir / f"{ kind } .log" )
387+ log_path = temp_dir / f"{ kind } .log"
388+ sitl = SitlProcess (binary , repo , eeprom , tcp_base , log_path )
290389 msp : MspClient | None = None
291390 ingresses : list = []
391+ print (f"\n [CASE] { kind } " )
292392 try :
293393 sitl .start ()
294394 msp_port = tcp_port (tcp_base , UART_MSP )
295395 wait_tcp ("127.0.0.1" , msp_port , 8.0 , sitl )
296396 time .sleep (0.15 )
397+ reset_count = sitl_log_count (sitl , "[SYSTEM] Reset" )
297398 configure_cli ("127.0.0.1" , msp_port , configure_case (kind ))
298- time . sleep ( 1.0 )
399+ hard_restart_sitl_after_configuration ( sitl , reset_count )
299400 wait_tcp ("127.0.0.1" , msp_port , 8.0 , sitl )
300- wait_tcp ("127.0.0.1" , tcp_port (tcp_base , UART_RX1 ), 8.0 , sitl )
301- if kind != "crsf-msp" :
302- wait_tcp ("127.0.0.1" , tcp_port (tcp_base , UART_RX2 ), 8.0 , sitl )
303401
304402 msp = MspClient ("127.0.0.1" , msp_port , timeout_s = 2.0 )
305403 status = msp .link_status ()
@@ -333,6 +431,17 @@ def run_case(kind: str, binary: Path, repo: Path, tcp_base: int, temp_dir: Path)
333431 if kind == "crsf-mavlink" :
334432 exercise_legacy_msp_override (msp , rx1 , rx2 )
335433 exercise_selector (msp , rx1 , rx2 , kind .replace ("-" , " + " ).upper ())
434+ except Exception as exc :
435+ process_code = sitl .process .poll () if sitl .process is not None else None
436+ sitl_state = "still running" if sitl .process is not None and process_code is None else f"exited with code { process_code } "
437+ log_tail = sitl .tail_log (lines = 120 )
438+ raise TestFailure (
439+ f"{ kind } failed: { type (exc ).__name__ } : { exc } \n "
440+ f"SITL state: { sitl_state } \n "
441+ f"SITL log: { log_path } \n "
442+ f"EEPROM: { eeprom } \n "
443+ f"--- SITL LOG TAIL ---\n { log_tail } \n --- END SITL LOG TAIL ---"
444+ ) from exc
336445 finally :
337446 for ingress in reversed (ingresses ):
338447 ingress .close ()
@@ -357,14 +466,17 @@ def main() -> int:
357466 print ("Dual RX mixed-ingress SITL test" )
358467 print (f" repo: { repo } " )
359468 print (f" SITL: { binary } \n " )
469+
470+ temp_dir = Path (tempfile .mkdtemp (prefix = "inav-dualrx-mixed-" ))
360471 try :
361- with tempfile .TemporaryDirectory (prefix = "inav-dualrx-mixed-" ) as temp_name :
362- temp_dir = Path (temp_name )
363- for kind in ("crsf-mavlink" , "crsf-msp" , "mavlink-mavlink" ):
364- run_case (kind , binary , repo , args .tcp_base , temp_dir )
365- except BaseException as exc :
366- print (f"\n [FAIL] { exc } " )
472+ for kind in ("crsf-mavlink" , "crsf-msp" , "mavlink-mavlink" ):
473+ run_case (kind , binary , repo , args .tcp_base , temp_dir )
474+ except Exception :
475+ print (f"\n [FAIL] Full diagnostics retained in: { temp_dir } " )
476+ traceback .print_exc ()
367477 return 1
478+
479+ shutil .rmtree (temp_dir , ignore_errors = True )
368480 print ("\n ALL DUAL RX MIXED-INGRESS SITL TESTS PASSED" )
369481 return 0
370482
0 commit comments