@@ -125,7 +125,9 @@ def _fail_timeout(self, reason: str) -> SendResult:
125125
126126 Credit: javastraat/meshpoint b04e91c
127127 """
128- self ._contact_cache .invalidate ()
128+ # Cooldown, do not wipe: keep stale names and stop contact
129+ # fetchers from immediately re-hammering a wedged companion.
130+ self ._contact_cache .note_soft_fail ()
129131 trigger = getattr (self ._source , "_trigger_reconnect" , None )
130132 if callable (trigger ):
131133 trigger (reason )
@@ -138,14 +140,23 @@ async def _run_tx_command(
138140 success_log : str ,
139141 timeout_label : str ,
140142 ) -> SendResult :
141- """Run one companion command under the serial lock."""
143+ """Run one companion command under the serial lock.
144+
145+ Pauses auto message fetching for the command window so the
146+ library's background poll cannot steal OK/ERROR events
147+ (same pattern as set_radio).
148+ """
142149 if not self .connected :
143150 return SendResult (success = False , error = "Not connected" )
144151 try :
145152 async with self ._cmd_lock :
146153 if not self .connected or self ._mc is None :
147154 return SendResult (success = False , error = "Not connected" )
148- result = await asyncio .wait_for (factory (), timeout = 10.0 )
155+ await self ._pause_auto_fetch ()
156+ try :
157+ result = await asyncio .wait_for (factory (), timeout = 10.0 )
158+ finally :
159+ await self ._resume_auto_fetch ()
149160 except asyncio .TimeoutError :
150161 return self ._fail_timeout (timeout_label )
151162 except Exception as exc :
@@ -191,6 +202,42 @@ async def _run_tx_command(
191202 await self ._run_post_command ()
192203 return SendResult (success = True , event_type = event_type )
193204
205+ async def _pause_auto_fetch (self ) -> None :
206+ mc = self ._mc
207+ if mc is None :
208+ return
209+ stop = getattr (mc , "stop_auto_message_fetching" , None )
210+ if not callable (stop ):
211+ return
212+ try :
213+ await stop ()
214+ except Exception :
215+ logger .debug ("Could not pause MeshCore auto-fetch" , exc_info = True )
216+
217+ async def _resume_auto_fetch (self ) -> None :
218+ # Prefer the capture-source restart (rebinds subscriptions) when
219+ # bound; otherwise poke the library directly.
220+ restart = getattr (self ._source , "restart_auto_fetching" , None )
221+ if callable (restart ):
222+ try :
223+ await restart ()
224+ return
225+ except Exception :
226+ logger .debug (
227+ "Could not restart MeshCore auto-fetch via source" ,
228+ exc_info = True ,
229+ )
230+ mc = self ._mc
231+ if mc is None :
232+ return
233+ start = getattr (mc , "start_auto_message_fetching" , None )
234+ if not callable (start ):
235+ return
236+ try :
237+ await start ()
238+ except Exception :
239+ logger .debug ("Could not resume MeshCore auto-fetch" , exc_info = True )
240+
194241 async def create_connection (
195242 self ,
196243 port : str ,
@@ -373,10 +420,18 @@ async def sync_channels(self, channel_keys: dict) -> None:
373420 if not self .connected :
374421 logger .debug ("sync_channels: not connected, skipping" )
375422 return
376- await MeshcoreChannelSync (
377- self ._mc ,
378- post_command = self ._run_post_command ,
379- ).sync (channel_keys )
423+ async with self ._cmd_lock :
424+ if not self .connected or self ._mc is None :
425+ return
426+ await self ._pause_auto_fetch ()
427+ try :
428+ await MeshcoreChannelSync (
429+ self ._mc ,
430+ post_command = None ,
431+ ).sync (channel_keys )
432+ finally :
433+ await self ._resume_auto_fetch ()
434+ await self ._run_post_command ()
380435
381436 async def get_contacts (self , * , force : bool = False ) -> list [dict ]:
382437 """Retrieve the companion's contact list.
@@ -407,24 +462,30 @@ async def get_contacts(self, *, force: bool = False) -> list[dict]:
407462 )
408463 except asyncio .TimeoutError :
409464 logger .warning ("get_contacts timed out waiting for companion" )
465+ self ._contact_cache .note_soft_fail ()
410466 return self ._contact_cache .get_stale ()
411467 except Exception :
412468 logger .exception ("Failed to retrieve MeshCore contacts" )
469+ self ._contact_cache .note_soft_fail ()
413470 return self ._contact_cache .get_stale ()
414471
415472 contacts = MeshcoreContactParser .from_command_result (result )
416473 soft_fail = result is None or MeshcoreContactParser ._is_error_event (
417474 result
418475 )
419476 if soft_fail :
477+ # Stamp TTL even on failure so queued callers do not each
478+ # burn another live 5s get_contacts on a wedged companion.
479+ self ._contact_cache .note_soft_fail ()
420480 stale = self ._contact_cache .get_stale ()
421481 if stale :
422482 logger .info (
423483 "get_contacts: soft fail, using stale cache (%d)" ,
424484 len (stale ),
425485 )
426486 return stale
427- else :
428- self ._contact_cache .store (contacts )
487+ logger .info ("get_contacts: soft fail, empty roster (cooling down)" )
488+ return []
489+ self ._contact_cache .store (contacts )
429490 logger .info ("get_contacts: %d contacts parsed" , len (contacts ))
430491 return contacts
0 commit comments