Variable inside timer callback is lost after first iteration in GUI - undefined function or variable

I've spent the past 72 hour (at least) trying to solve this issue. I will try to be as succint as possible.
First, I tried to implement my solution as a nested function that is invoked inside the timer HOWEVER I just read that nested function should NOT be defined inside program control statements (according to this document).
Now that I merged the code into one long and tedious to read code I am facing the next problem: once the first iteration or 'tick' of the timer occurs, Matlab throws an error indicating that one of my variables called "iterations" is undefined.
The flow of my algortihm is the following:
  1. I declared a timer in the opening function of the GUI whose main purpose is to query data from a serial device (Arduino) and get a temperature data point when the value is ready (there is a conversion process from a thermocouple converter). The format of my function is the following:
function controlPanel_OpeningFcn(hObject, eventdata, handles, varargin)
% Choose default command line output for controlPanel
handles.output = hObject;
%timer creation
handles.timer = timer();
set(handles.timer, 'Period',0.5);
set(handles.timer,'ExecutionMode','fixedRate');
set(handles.timer,'TimerFcn',{@continuousTemperatureRead,hObject, handles}); % Here is where you assign the callback function
drawnow;
movegui('center')
% Add all subfolder from the current path to the environment
handles = MainDataStructure(handles); % Initialize the main group data structure
addpath(genpath(handles.base_directory)); % Add all m-file directories to the search path
% Load initial state of controls and buttons
handles = initialState(handles);
% Update handles structure
guidata(hObject, handles);
2. The callback function basically goes over the following steps: a. ask the device if data is ready, b. if so, get a data point, c. initialise counters and a live plot axes ONCE, d. append timestamp and data values, e. display a data point, f. increment on counters, G. update handles
In the following code I replaced some sections with comments ***LIKE THIS*** for the sake of clarity.
function continuousTemperatureRead(myTimer, eventdata,hObject, handles)
handles = guidata(hObject);
% ********** HERE I ASK THE DEVICE IF THE DATA POINT IS READY
numBytes = handles.serialTempControl.BytesAvailable ;
if (numBytes >= 1)
tempQueryStr = getValues(handles.serialTempControl);
if strcmp(tempQueryStr,'1') % The device responds with a '1' if the data point is ready
% *** IF THE DATA IS READY THEN REQUEST THE TEMPERATURE DATA POINT
pause(0.1) % these are neccessary to avoid 'obfuscation' !!!!
if (handles.serialTempControl.BytesAvailable >= 1) % if theres data available
% **** GET TEMPERATURE POINT ***
% *** INITIALISE COUNTERS AND TIMESTAMP:
if isequal(handles.liveTempPlotInit,0) % handles.livePlotInit is a flag to set initial conditions. It should run ONLY ONCE
iterations = 1;
handles.liveTempPlotInit = 1
tic
disp("Initialisation done!")
end
timeAccumulation = toc/60;
temperatureAcum(iterations) = temperaturePoint;
timeAcumVector(iterations) = timeAccumulation; % for later use
if isequal(handles.flagFigure,0) % This is another flag for setting up the axes where the plot will be drawn. It should run ONLY ONCE
axes(handles.tempPlot);
handles.liveTemperature = gca; % The handle of the live temp axis
% Initial setup for graph display
% *********************************
cla(handles.liveTemperature,'reset')
set(handles.liveTemperature,'FontSize',10);
set(handles.liveTemperature, 'XTickLabelRotation',45);
lineTempAcum = line(timeAccumulation, temperaturePoint,'Parent', handles.liveTemperature);
set(lineTempAcum, 'LineWidth', 1);
handles.liveTemperature.YLabel.String = strcat('Temperature',strcat( {' '}, {char(176)},{'C'})); % ylabel('Temperature')
handles.liveTemperature.XGrid = 'on';
handles.liveTemperature.YGrid = 'on';
handles.liveTemperature.XMinorGrid = 'on';
handles.liveTemperature.YMinorGrid = 'on';
grid on
grid minor
handles.flagFigure = 1;
disp("Figure config done")
end
% Add data points
timeTempAcum = get(lineTempAcum, 'xData');
pointsTempAcum = get(lineTempAcum, 'yData');
timeTempAcum = [timeTempAcum timeAccumulation];
pointsTempAcum = [pointsTempAcum temperaturePoint];
set(lineTempAcum, 'xData', timeTempAcum, 'yData', pointsTempAcum);
datetick('x','keeplimits')
iterations = iterations+1;
refreshdata(lineTempAcum);
refreshdata(handles.liveTemperature);
drawnow limitrate % this belongs to the line
end
drawnow % this belongs to the timer
end
end
guidata(hObject, handles);
end
My apologies if the code seems convoluted however it is based on another DAQ GUI I designed some time ago and it works using a different approach (not inside a timer)
This is the behaviour of the GUI:
  1. The code runs once, I can see the "checkpoint" messages from the initialisation routines, HOWEVER after running once, Matlab throws an error:
"Error while evaluating TimerFcn for timer 'timer-8'
Undefined function or variable 'iterations' "
2. The axes are NOT being assigned to handles.tempPlot rather a new window appears on top of the GUI.
What am I missing? Why is the variable lost?
Important note: Even the Code Analyzer engine underlines this and other variables and says that the value assigned to 'iterations' might be unused.
Is the scope or workspace from the timer and all the variables inside of it being restored after every cycle?
Thank you in advance
Daniel

7 个评论

Never pass 'handles' directly to a callback. You should always pass the handle of the GUI in, then within the callback you access the handles as
handles = guidata( hGUI ) % hGUI being that handle to your GUI
...
guidata( hGUI, handles ) % This is only necessary if you changed or added anything to handles in the function
handles is just a struct and behaves like any other struct really, it's not a magic variable, though it is unfortunate it's name is so similar to 'handles', which is the base class to iniherit a class from if you want it to have 'pass by reference' properties.
But in GUIDE it is simply a struct and obeys the rules of workspace scope like any other, thus when it goes out of scope it gets deleted. So if you have made changes to it and let it just go out of scope those changes are lost unless you do that guidata step to write handles back to the GUI.
Even doing this there are situations you can get in a big mess if you have a complex tree of function calls, usually if you involve listeners and the like. I've forgotten the exact details, but I have had situations where a callback triggers a listener whose function edits handles, then code flow returns to the callback which still has the version of handles it was given originally, knowing nothing about what the listener callback did, and so it does its thing and then writes its version of handles to the GUI, overwriting those that were written by the listener callback.
In your case you do ignore the handles passed in anyway and do this.
To add to what Adam is saying:
This:
set(handles.timer,'TimerFcn',{@continuousTemperatureRead,hObject, handles}); % Here is where you assign the callback function
is most likely never going to work as intended, in that the handles variable that the callback will receive every time it is called is going to be a snapshot of the handles variable as it is when the the above line is executed. Subsequent changes to handles would not be reflected.
{xxx, handles}
simply create a cell array whose 2nd element is a copy of the current handles.
So yes, don't pass handles to the callback directly like that.
"Never pass 'handles' directly to a callback."
GUIDE does this.
The function definitions shown in the question are exactly those that are automatically generated by GUIDE:
GUIDE extracts the latest version of handles when it calls its predefined GUI-component callbacks taking handles though. User-defined callbacks don't include this extra hidden step, which is one of the reasons handles confuse people so much in GUIDE. This aspect makes them look like some kind of special object rather than what it actually is (I assume via getappdata - I don't know exactly what it does behind the scenes), since Matlab ensures that the current version of handles is passed in there, not one that was baked in at the time the callback was setup, which is what this
set(handles.timer,'TimerFcn',{@continuousTemperatureRead,hObject, handles})
is doing.
Yes, but every time there is a call to @continuousTemperatureRead, he is assigning handles within that function from handles = guidata(hObject);
So he gets the current version of handles. If it is re-creating handles each time from hObject, and hObject is a "snapshot" from this, then he would not have the issue where iterations is ever undefined, because then by definition his loop would always meet the criterion that handles.liveTempPlotInit == 0 and iterations would be defined. However, that would cause a different problem, in that iterations would never increment.
Therefore, I do not think that this is an issue with passing an old version of handles or hObject.
No, that is why in my answer I went in a different direction. I wrote my comment up here first, based on a glance at what was happening and a skim over the fact he was having issues related to handles. I left my comment in because it is still vaguely relevant and sound advice. In the case here passing handles to the callback is just an irrelevance though rather than doing any harm, since they are instantly overwritten.
Guys. I deeptly appreciate the great feedback that all of you are providing.
Adam: I can confirm that the behavior of the function is the same even after removing handles from the TimeFcn definition. After the first iteration I lose persistent variables as Max mentioned in another comment.
Max: I am in the same 'frequency' with you. I am reading an updated snapshot of handles in the beggining of my callback (based on info that I gathered from other posts). I don't think the problem is on the way I assigned handles in the callback function.
In agreement with both of you I removed handles but I am getting the same result. I now understand that the workspace of this function is refreshed on each iteration and that's why my variables are being lost.
I will reply to your other comments in this thread.
Cheers guys!

请先登录,再进行评论。

 采纳的回答

In continuousTemperatureRead, you assign handles the value from guidata(hObject).
However, in controlPanel_OpeningFcn, handles is never assigned to hObject.guidata.
It looks like there is another custom function, MainDataStructure, but my guess is that hObject is out of scope for that function since it is not given as an argument, unless MainDataStructure shares a parent figure object and the association between handles and that parent is made using a call to guidata there.

7 个评论

Dear Max
Thank you very much for your response.
MainDataStructure is basically a function to set the base directory of my entire project since I am splitting my files into many different folders in order to categorize them. I actually removed that file already and merged it into the opening function. Honestly, it made no sense having three lines of code in one file. I inteded to use it for other purposes but I never implemented anything else.
I am assuming that I am making an EXTREMELY dumb mistake due to my poor understanding of the scope of each handle and object.
Any further ideas? I am still reading everywhere trying to find other clues
Thank you once again
function controlPanel_OpeningFcn(hObject, eventdata, handles, varargin)
% Choose default command line output for controlPanel
handles.output = hObject;
% Add all subfolder from the current path to the environment
%handles = MainDataStructure(handles); % Initialize the main group data structure
handles.base_directory = fileparts(mfilename('fullpath')); % Path to the base directory (containing MainDataStructure.m)
if (handles.base_directory(end) == filesep())
handles.base_directory = handles.base_directory(1 : (end-1)); % Remove the trailing (back)slash
end
addpath(genpath(handles.base_directory)); % Add all m-file directories to the search path
% Load initial state of controls and buttons
handles = initialState(handles);
%timer creation
handles.timer = timer();
set(handles.timer, 'Period',0.5);
set(handles.timer,'ExecutionMode','fixedRate');
set(handles.timer,'TimerFcn',{@continuousTemperatureRead,hObject, handles}); % Here is where you assign the callback function
drawnow;
movegui('center')
% Update handles structure
guidata(hObject,handles);
Sorry, I'm not familiar with the Simulink.SimState.ModelSimState class. If you set a breakpoint after
handles = initialState(handles);
Is the value of liveTempPlotInit field equal to 0? I am making an assumption that it is set from that method, but it's possible (unlikely) that this is the issue.
Where are you making the call to handles.timer.start? Is it possible that the start method on handles.timer is called after handles is overwritten in the scope of some other function?
Excellent questions, Max.
1.InitialState is a function that holds every initial condition for my variables, GUI objects and flags. Yes, liveTempPlotInit equals zero when I launch the program. This inside this file:
% For live plotting the temperature
handles.liveTempPlotInit = 0;
handles.flagFigure = 0;
2. My timer starts when I enable an Arduino-based processing board via a checkbox. Here an excerpt of that part of the code:
.
.
.
if strcmp(tempResponse, "OK")
% If the Arduino responds to an INIT command with 'OK' then
% the device is ready to communicate in a full-duplex protocol
end
set(handles.getTemp, 'Enable', 'off')
handles.comuTempActive = 1; % I indicate that the communication is active
start(handles.timer); % HERE I START THE TIMER!
.
.
.
Just to clarify. The code works flawlessly if I query and read the incoming value from Arduino and I am able to display it in a text box WITHOUT PLOTTING!.
On Monday, when I felt that this version of my code was 'mature' enough I decided to implement a live plot however.....here I am asking you guys :(
Thank you once again!
Cheers
Daniel
Is iterations stored as a persistent or global variable? You might consider storing iterations as a field of handles, and making sure that you call guidata(hObject,handles) after you update handles.iterations so that on each call of continuousTemperatureRead from handles.timer, handles.iterations has the correct value. Otherwise, it is initialized on the first loop iteration, and then it goes away and is never "seen" on subsequent evaluations. That should resolve it, I hope.
Dear Max
  1. Iterations is definitely NOT a global variable. I do my best to avoid them at all cost. It's just a persistent variable (or at least I assumed it was)
  2. I think you've actually read my mind. I've been thinking all of this time: to declare and store my variables as part of the handles structure. I wanted to avoid doing this for the sake of keeping the structure as clean as possible. This means that I might need to declare each one of the variables that I use inside the continuousTemperatureRead function as handles.variableXXX.
Do you all think this is an "elegant" solution?, I always try to write lean code but clearly I always fail :(
I will test it right away and come back with the results.
Thank you all again!
I owe you big time
Daniel
"to declare and store my variables as part of the handles structure"
Good idea, much cleaner: handles is there, you might as well use it.
Haha, I am not sure that I have ever written "elegant" code!
My suggestion is to define lineTempAcum somewhere else: where you first start the TimerFcn for handles.timer.
Then you can add the handle to that matlab.graphics.primitive.Line object as a field of handles. This way you always have a "pointer" to the original line. Reference that field in continuousTemperatureRead, instead of making a call to line (~line 49). If you don't want it showing a line "early" you can always initialize XData and YData as NaN.
This will avoid re-setting the properties of lineTempAcum each time and you won't have to do the extra flag in the middle of your loop (similar to what you did with handles.liveTemperature, which I assume is an axes somewhere).
The only other stuff I see is timeAccumulation, temperatureAcum, and timeAcumVector; afraid I can't think of an "elegant" way to avoid putting those into handles without defining some other smaller variable that is passed back and forth between your TimerFcn and whatever interacts with those arrays.

请先登录,再进行评论。

更多回答(2 个)

This seems like the opposite problem to what I originally thought it was as I got distracted by the handles and assumed they were not updating. The problem is they are updating and thus you have a variable iterations which is only ever defined in an if statement.
This is never a good thing as it means any time that code runs and the if statement returns false the variable is not created, so when it is used lower down it does not exist.
Your first run sets
handles.liveTempPlotInit = 1;
Your second run tests that this field is equal to 0. It isn't, so it doesn't create the variable iteration so this line will fail:
temperatureAcum(iterations) = temperaturePoint;

2 个评论

Adam:
I am assuming that on each 'tick' of the timer, iteration holds it previous value. I am definiing it once then just increasing it and using it later as an index. Is this not acceptable inside a function?
I have plenty of codes with similar structures and the integrity of variables is kept intact.
Please check that iterations inside the conditional is intended just to define its initial value, to be then only incremented.
You are right. On the second run it SHOULD test zero meaning that it should only be increased as its soley purpose is to work as an index for an array that I will save at a later point (not now)
Cheers
Unless it is defined as a persistent variable it will not hold its value from one call of the function to the next. When code execution reaches the end instruction of a function its workspace is lost. Next time you call the function it starts again only with what is passed in. As mentioned in the answer below, put it on the handles structure and then it will be updated since, as we have established, you are getting up to date handles coming through. This is a lot better than using a persistent variable. I only mentioned that at the start because it is another option, though not one to seriously consider here I wouldn't say.

请先登录,再进行评论。

Hello guys
So here's the implementation (so far) for solving the problem I presented to you.
The solution is basically to declare almost every variable as a handles object. I say almost because apparently only those variables whose value will be reused had to be modified, including of course iterations. Just to be safe I did it will all of them but the current temperature datum.
So, my changes are:
  1. The timer function declaration has no handles as an input, as already discussed.
  2. The function continuousTemperatureRead is self-contained and does not call any other functions as I originally intended. guidata is updated at the end of my plotting loop and the conditional that wraps wheter there's serial data or not. handles is invoked at the beginning of this function. I had to implement an extra conditional for the incoming serial data since I was getting some zeros on the plot.
  3. My next BIG ISSUE and therefore why I didn't reply until now, was that the assigment of axes in combination with an animated line resulted on a big mess. At first, a second figure would pop-up, then the axis would show wrong data and so on and so forth.
Finally, I think I have for now what I wanted to achieve with this portion of the code. Below is my final function and the result of this section can be seen in the image ;)
I want to thank you all for your valuable input, specially you Max because you confirmed what I had originally in mind regarding using these variables as part of the handles structure. Having said that, I will accept your answer and THE answer. One thing regarding this:
"Haha, I am not sure that I have ever written "elegant" code!"
Are you saying that you don't code on a tuxedo while drinking dry Martini and wearing a monocle? O_o
Cheers y'all and for sure you will read me around here very soon!
I am working on a massive project for my PostDoc and I still have a very long and winding road ahead to complete this software.
Daniel
function continuousTemperatureRead(myTimer, eventdata,hObject)
handles = guidata(hObject);
% *** ASK SERIAL DEVICE IS DATA IS READY
pause(0.1)
handles.numBytesRead = handles.serialTempControl.BytesAvailable ;
if (handles.numBytesRead >= 1) % If there's data
tempQueryStr = getValues(handles.serialTempControl); % get what the serial buffer has
if strcmp(tempQueryStr,'1') % If the device says "yes, data is ready"
flushinput(handles.serialTempControl); % clean the buffer to read ONLY temperature values
fprintf(handles.serialTempControl, '%s\n', tempRequest); % read the value
pause(0.1) % these are neccessary!!!!
if (handles.serialTempControl.BytesAvailable >= 1)
tempValStr = getValues(handles.serialTempControl);
if ~strcmp(tempValStr,'0.00') % I had to include this to avoid showing zeros that shouldn't be
% here but I will investigate this later on. Perhaps is a matter
% of asyncronism between the Matlab timer and the Arduino controller
temperaturePoint = str2double(tempValStr);
% *** FORMAT THE TEMPERATURE VALUE
% Insert data plotting function here
% The only reason for having this conditional is for the 'tic' function however I might remove it later
% coz I am getting time points in a different way now
if isequal(handles.liveTempPlotInit,0)
handles.iterations = 1;
handles.liveTempPlotInit = 1;
tic % Start counting
end
handles.timeContinuous = toc/60; %only for accumulated data
handles.tempCont(handles.iterations) = temperaturePoint;
handles.timeContinuousVector(handles.iterations) = handles.timeContinuous;
% Initial setup for graph display
% *********************************
if isequal(handles.flagFigure,0)
axes(handles.tempPlot);
% handles.liveTempPlot = gca; % I had to remove this, otherwise it would go crazy
handles.tempPlot.YLabel.String = strcat('Temperature',strcat( {' '}, {char(176)},{'C'})); %ylabel('Temperature')
handles.tempPlot.XLabel.String = "time[min]";
handles.tempPlot.XGrid = 'on';
handles.tempPlot.YGrid = 'on';
handles.tempPlot.XMinorGrid = 'on';
handles.tempPlot.YMinorGrid = 'on';
handles.tempPlot.FontSize = 9;
handles.tempPlot.XTickLabelRotation = 45;
handles.tempPlot.XLabel.String = "time[min]";
handles.animatedLineTemp = animatedline('LineWidth', 1,'Color', 'blue','Parent', handles.tempPlot); %line(handles.timeContinuous, temperaturePoint); % 'Parent', handles.tempPlot
disp("Figure config done")
handles.flagFigure = 1;
end
% Time managmement
t(handles.iterations,1) = datetime('now', 'Format','HH:mm:ss'); % - handles.startTime; %+initialTime;
handles.timePoints(handles.iterations,1) = datenum(t(handles.iterations));
addpoints(handles.animatedLineTemp, handles.timePoints(handles.iterations),temperaturePoint);
handles.tempPlot.XLim = datenum([t(handles.iterations,1)-seconds(30) t(handles.iterations,1)]);
datetick(handles.tempPlot, 'x','keeplimits'); % very important to tell this dude where to show the time
%refreshdata(handles.animatedLineTemp); % same story, refreshingdata causes problems
drawnow limitrate
%refreshdata(handles.tempPlot); % Is this necessary?
handles.iterations = handles.iterations+1;
guidata(hObject, handles); % Yes! only update handles once here
end
end
end
end
guidata(hObject, handles); % in case the conditional jumps here
end

1 个评论

Good that you managed to solve the problem. On this though:
'Just to be safe I did it will all of them but the current temperature datum'
whilst it is often tempting to do that and here it won't do any harm, I would recommend only doing it for those variables you do actually need to hold their value for the next call of the function, especially since you already identified that this is the issue.
Understanding the required scope of all your variables is extremely useful for when you need to debug in future or for just generally understanding exactly what your code is doing. Knowing that a given variable only needs local scope vs another than needs wider scope is useful knowledge. So only adding those variables to 'handles' that really need to be added is usually best as the rest just clutter up what is already a sizeable structure, in terms of fields (it has all your GUI components attached to it too).
Like I said, it does no real harm adding them all, it's just neater not to!

请先登录,再进行评论。

类别

帮助中心File Exchange 中查找有关 Loops and Conditional Statements 的更多信息

产品

版本

R2019a

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!

Translated by