2727 hRangeLbl_ = [] % "Range: last X min (max. 30 min)" label under sparkline
2828 SparkWindowSec_ = 1800 % sparkline horizon — last 30 minutes of data
2929 ThresholdsCache_ = [] % containers.Map(tagKey -> rules cell) ; built lazily
30+ IsRendering_ = false % re-entrance guard — refreshLive no-ops while true
3031 hTagTable_ = [] % uitable in tag state (live mode updates Data only)
3132 hTagTitle_ = [] % uilabel for the tag-name title (in-place update)
3233 hDashTable_ = [] % uitable in dashboard state (live mode updates Data only)
@@ -115,9 +116,12 @@ function refreshLive(obj)
115116 % Called by the orchestrator's live timer at LivePeriod_.
116117 % - tag state: update uitable Data + sparkline XData/YData
117118 % - dashboard state: update uitable Data
119+ % - multitag state: update each card's sparkline in place
118120 % - other states: no-op
119121 % Falls back to a full renderState_() if the cached handles are
120- % stale (e.g., setState swapped state but the timer ticked first).
122+ % stale. No-ops while a render is in progress to avoid
123+ % re-entering and accessing half-built widgets.
124+ if obj .IsRendering_ ; return ; end
121125 try
122126 switch obj .State_
123127 case ' tag'
@@ -259,6 +263,11 @@ function updateDashboardInPlace_(obj, db)
259263 end
260264 end
261265
266+ function clearIsRendering_(obj )
267+ % CLEARISRENDERING_ Reset the re-entrance guard (called by onCleanup).
268+ obj.IsRendering_ = false ;
269+ end
270+
262271 function alertOrLog_(obj , err )
263272 % ALERTORLOG_ Best-effort error surface that won't crash on invisible figures.
264273 % Use this instead of raw uialert in catch blocks: uialert refuses
@@ -291,21 +300,28 @@ function alertOrLog_(obj, err)
291300
292301 function renderState_(obj )
293302 % RENDERSTATE_ Clear hContent_.Children and dispatch to per-state renderer.
303+ wasInvisible = false ;
304+ obj.IsRendering_ = true ;
305+ cleanupGuard = onCleanup(@() obj .clearIsRendering_());
294306 try
295307 if isempty(obj .hContent_ ) || ~isvalid(obj .hContent_ ); return ; end
296- % Hide hContent_ before deleting children. Without this,
297- % the delete cascade fires position-update events against
298- % freshly-stale panel handles; if a DashboardEngine live
299- % timer happens to drawnow during our delete, it dispatches
300- % those events on invalid handles ('Value must be a handle'
301- % errors). Visibility=off suppresses position events for
302- % the subtree being torn down.
303- try ; obj.hContent_.Visible = ' off' ; catch ; end
308+ % Hide hContent_ for the ENTIRE delete + build cycle.
309+ % Without this, MATLAB dispatches position-update events
310+ % during widget creation/deletion. A DashboardEngine live
311+ % timer firing drawnow during this dispatches those events
312+ % against half-built or freshly-stale handles, producing
313+ % 'Value must be a handle' / 'Invalid or deleted object'
314+ % errors — most visible on multitag renders that build
315+ % several axes back-to-back.
316+ try
317+ obj.hContent_.Visible = ' off' ;
318+ wasInvisible = true ;
319+ catch
320+ end
304321 kids = obj .hContent_ .Children ;
305322 for i = numel(kids ): -1 : 1
306323 try ; delete(kids(i )); catch ; end
307324 end
308- try ; obj.hContent_.Visible = ' on' ; catch ; end
309325 obj.hSparkAxes_ = []; obj.hSparkPanel_ = []; obj.hSparkLine_ = [];
310326 obj.hRangeLbl_ = [];
311327 obj.hOpenDetail_ = []; obj.hPlayBtn_ = []; obj.hPauseBtn_ = [];
@@ -325,7 +341,13 @@ function renderState_(obj)
325341 error(' FastSenseCompanion:invalidState' , ...
326342 ' Unknown inspector state: ''%s'' .' , obj .State_ );
327343 end
344+ if wasInvisible
345+ try ; obj.hContent_.Visible = ' on' ; catch ; end
346+ end
328347 catch err
348+ if wasInvisible
349+ try ; obj.hContent_.Visible = ' on' ; catch ; end
350+ end
329351 obj .alertOrLog_(err );
330352 end
331353 end
0 commit comments