MATLAB Examples

Continuous monitoring of wireless network of temperature sensors using MATLAB® and XBee® (Part 2)

Contents

Introduction

In part one of this series of blog posts I went through the process of reading voltages from a network of XBee® modules, and building and testing my wireless network of temperature sensors. In this post, I will talk about the details of the temperature sensor network in my apartment, the method I used to gather temperature data from my apartment for longer periods of time, and then methods for plotting the data.

Network of Temperature Sensors

For my apartment I built four edge-nodes. Each edge-node had either 3 or 4 temperature sensors, which I either placed inside the room, outside (with the telephone cord running under the window sash), or inside my radiator (wedged between two pieces of metal close to the steam pipe). The temperature sensors in my radiator will let me determine how quickly my radiator heats up when the heat comes on, and it will also serve as a reference point for when my heat is on or off.

Figure 1: Temperature sensor inside my living room (taped to a lamp cord and normally hidden by the picture frame)

Figure 2: Temperature sensor outside my living room window on the front porch

Figure 3: Temperature sensor in my living room radiator

The XBee can read analog voltages on pins 17-20, so here is how I arranged the temperature sensors in my apartment. As I mentioned in part one, in a few cases I made a dual temperature sensor. This is so that I can periodically check that the sensors are still accurately calibrated. The dual temperature sensors span two columns (two pins) in the table below.

Room XBee XBee Address Pin 17 Pin 18 Pin 19 Pin 20
Living Room XBee 1 0013 A200 40B1 8D3E Not Used Inside Outside Radiator
Office XBee 2 0013 A200 40B1 8EFB Not Used Radiator Inside (dual)
Kitchen XBee 3 0013 A200 40B1 916C Inside 1 (dual) Inside 2 (dual)
Bedroom XBee 4 0013 A200 40B2 2000 Radiator Outside Inside (dual)

Table 1: Arrangement of temperature sensors in my apartment

Because I will be using this information in several different places, I wrote a function called XBeeSensors which returns one variable for the room, addresses, and location of the individual temperature sensors.

Polling for Temperature Data

Once I had the temperature sensors distributed around my apartment, I wanted to read the current temperature, and write the data to a file so that I could analyze it later.

To start with, I needed to open the connection from MATLAB to the coordinator.

xb = xbee('COM3');

Next I requested the voltage from each pin and recorded the answers. Because the XBees may reply in any order, I also retrieved the source address for each reading, and compared the source address to the list of XBee nodes using ismember. By default, MATLAB will fill in missing values in a matrix with 0, which could be mistaken for a valid voltage reading. Therefore I initialized the output matrix volts to be a matrix of all NaN (Not-a-Number) values, with one row per XBee and one column per sensor using the NaN function. This also means that the final size of the matrix is fixed, even if an XBee does not reply.

XBeeAddresses = XBeeSensors;      % Get sensor addresses
numxbees = length(XBeeAddresses); % Get the number of XBees
volts = NaN(numxbees,4);          % Initialize the output matrix
ts = now; % Record the current time.
for p = 1:4
    [v,src] = readVoltage(xb,p+16); % Read the voltages.
    [tf,loc] = ismember(src,XBeeAddresses); % Compare to list of XBees
    loc = loc(tf); % Keep only those sources that match our list of XBees.
    volts(loc,p) = v(tf); % Sort and save the sorted voltages.
end

Then I wrote the voltage values to a log file for storage and analysis later. I used the fprintf function because I needed to write both numerical and string data to file. fprintf takes in a format string and then a list of values to fill the format string. In this case I use the format string:

formatstr = '%0.5f\t%s\t%7.5f\t%7.5f\t%7.5f\t%7.5f\n';

%0.5f indicates that the first value should be a floating point number with 5 decimal places, \t indicates a tab, %s indicates a string, and %7.5f indicates a floating point number with 7 digits, 5 of which are decimal places. This format string will produce the following 6 columns in the data file: Time, XBee Address, Pin 17, Pin 18, Pin 19, and Pin 20

fid = fopen('templog.txt','a'); % Append data to the file if it exists.
for s = 1:numxbees
    fprintf(fid,formatstr,...
        ts,XBeeAddresses{s},volts(s,1),volts(s,2),volts(s,3),volts(s,4));
end
fclose(fid);

The first few lines of the resulting log file looks like this:

735719.92500	0013A20040B18D3E	0.60880	0.73666	0.74018	0.74018
735719.92500	0013A20040B1916C	0.74370	0.75425	0.75777	0.74604
735719.92500	0013A20040B22000	0.74370	0.73783	0.74252	0.74135
735719.92500	0013A20040B18EFB	0.60528	0.73783	0.73783	0.74370

Periodically Polling for Temperature Data

I wanted to poll every 2 minutes, so I put the commands above into a function called XBeePoll and used a MATLAB timer to call XBeePoll every 2 minutes. I added some extra error checking into XBeePoll to make the code more robust, to prevent any errors from interrupting the data collection, and to record the battery voltage in addition to the temperature.

t = timer;
t.Userdata = xb;        % Store the XBee object for access by XBeePoll
t.TimerFcn = @XBeePoll; % Set XBeePoll as the timer function
t.Period = 120;         % Call the timer function every 120 seconds
t.ExecutionMode = 'FixedRate';  % Run the timer at a fixed rate

Then I just started the timer, sat back, and watched the data roll in.

start(t)

I put these last few lines of code into a separate script called XBeeStartPoll so that I can run them again as I am working out any kinks I may find in the system.

When I was done collecting data, I stopped the timer:

stop(t)

Reading the Log Files

I ran the code above for about one hour to make sure everything was working as I expected. The data is stored in the file templog.txt. Then I used the readtable function to quickly and easily read the data file into a MATLAB table. Tables are a powerful new data type that was introduced in MATLAB R2013b. To create a table from my data file, I needed to specify that the file is tab-delimited ('\t'), and that there are no variable names stored in the file.

data = readtable('templog.txt','Delimiter','\t','ReadVariableNames',false);
data(1:8,:)
ans = 

       Var1              Var2            Var3       Var4       Var5       Var6  
    __________    __________________    _______    _______    _______    _______

    7.3572e+05    '0013A20040B18D3E'     0.6088    0.73666    0.74018    0.74018
    7.3572e+05    '0013A20040B1916C'     0.7437    0.75425    0.75777    0.74604
    7.3572e+05    '0013A20040B22000'     0.7437    0.73783    0.74252    0.74135
    7.3572e+05    '0013A20040B18EFB'    0.60528    0.73783    0.73783     0.7437
    7.3572e+05    '0013A20040B18D3E'     0.6088    0.73666    0.74018      0.739
    7.3572e+05    '0013A20040B1916C'     0.7437    0.75425    0.75777    0.74604
    7.3572e+05    '0013A20040B22000'     0.7437    0.73783    0.74252    0.74252
    7.3572e+05    '0013A20040B18EFB'    0.60528    0.73783      0.739     0.7437

This creates a MATLAB table with six variables (one for each column in the data file), but because the variable names were not stored in the data file, I needed to manually specify the variable names.

data.Properties.VariableNames = {'Time','XBee','Pin17','Pin18','Pin19', 'Pin20'};
data(1:8,:)
ans = 

       Time              XBee            Pin17      Pin18      Pin19      Pin20 
    __________    __________________    _______    _______    _______    _______

    7.3572e+05    '0013A20040B18D3E'     0.6088    0.73666    0.74018    0.74018
    7.3572e+05    '0013A20040B1916C'     0.7437    0.75425    0.75777    0.74604
    7.3572e+05    '0013A20040B22000'     0.7437    0.73783    0.74252    0.74135
    7.3572e+05    '0013A20040B18EFB'    0.60528    0.73783    0.73783     0.7437
    7.3572e+05    '0013A20040B18D3E'     0.6088    0.73666    0.74018      0.739
    7.3572e+05    '0013A20040B1916C'     0.7437    0.75425    0.75777    0.74604
    7.3572e+05    '0013A20040B22000'     0.7437    0.73783    0.74252    0.74252
    7.3572e+05    '0013A20040B18EFB'    0.60528    0.73783      0.739     0.7437

Now I want to change the 64 bit XBee addresses into more friendly names.

data.XBee = strrep(data.XBee, '0013A20040B18D3E', 'XBee1');
data.XBee = strrep(data.XBee, '0013A20040B18EFB', 'XBee2');
data.XBee = strrep(data.XBee, '0013A20040B1916C', 'XBee3');
data.XBee = strrep(data.XBee, '0013A20040B22000', 'XBee4');
data(1:8,:)
ans = 

       Time        XBee       Pin17      Pin18      Pin19      Pin20 
    __________    _______    _______    _______    _______    _______

    7.3572e+05    'XBee1'     0.6088    0.73666    0.74018    0.74018
    7.3572e+05    'XBee3'     0.7437    0.75425    0.75777    0.74604
    7.3572e+05    'XBee4'     0.7437    0.73783    0.74252    0.74135
    7.3572e+05    'XBee2'    0.60528    0.73783    0.73783     0.7437
    7.3572e+05    'XBee1'     0.6088    0.73666    0.74018      0.739
    7.3572e+05    'XBee3'     0.7437    0.75425    0.75777    0.74604
    7.3572e+05    'XBee4'     0.7437    0.73783    0.74252    0.74252
    7.3572e+05    'XBee2'    0.60528    0.73783      0.739     0.7437

To make sure that the read operation worked as I expected, I plotted the raw voltage data from Pin 20 on a single XBee:

firstXBee = strcmp(data.XBee,'XBee1');
plot(data.Time(firstXBee),data.Pin20(firstXBee),'.-')
datetick
xlabel('Time')
ylabel('Volts')
title('Raw voltage from Pin 20 on one XBee')

Figure 4: One hour of raw voltage readings from one XBee

Clean-up and Rearrange the Data

That looked like a good start, but to make the data a little easier to work with I wanted to do some clean-up and rearranging of the data.

The raw data from the log file had several entries for each time stamp (one for each XBee), and the data from the four temperature sensors on Pin 17 (for example) are mixed together in one column. It would be easier to work with the data if there were only one row for each unique time stamp, and data from each temperature sensor was listed in its own column.

For example, before rearranging the data, the table looked like this:


Time XBee Pin 17 Pin 18 Pin 19 Pin 20
10:12pm XBee 1 0.6088 0.7367 0.7402 0.7402
10:12pm XBee 3 0.7437 0.7543 0.7578 0.7460
10:12pm XBee 4 0.7437 0.7378 0.7425 0.7414
10:12pm XBee 2 0.6053 0.7378 0.7378 0.7437
10:14pm XBee 1 0.6088 0.7367 0.7402 0.7390
10:14pm XBee 3 0.7437 0.7543 0.7578 0.7460
10:14pm XBee 4 0.7437 0.7378 0.7425 0.7425
10:14pm XBee 2 0.6053 0.7378 0.7390 0.7437

Table 2: Organization of table before unstacking

After rearranging the data, the table should look like this:


Time Pin 17 Pin 18 Pin 19 Pin 20
XBee 1 XBee 2 XBee 3 XBee 4 XBee 1 XBee 2 XBee 3 XBee 4 XBee 1 XBee 2 XBee 3 XBee 4 XBee 1 XBee 2 XBee 3 XBee 4
10:12pm 0.6088 0.6053 0.7437 0.7437 0.7367 0.7378 0.7542 0.7378 0.7402 0.7378 0.7578 0.7425 0.7402 0.7437 0.7460 0.7413
10:14pm 0.6088 0.6053 0.7437 0.7437 0.7367 0.7378 0.7542 0.7378 0.7402 0.7390 0.7578 0.7425 0.7390 0.7437 0.7460 0.7425

Table 3: Organization of table after unstacking

To do this, the first step was to clean up the timestamps. I sampled every two minutes, so I rounded the timestamps to the nearest minute:

precision = 60; % Seconds
precision = precision/(60*60*24); % Convert from seconds to days
data.Time = round(data.Time/precision)*precision;

Next I wanted to combine all the sensor readings from one time stamp into one row. By using a MATLAB table to store the data, I was able to take advantage of a built-in function called unstack that was introduced along with tables in MATLAB R2013b and is designed specifically for this purpose. When calling unstack, I specified an indicator variable (in this case the XBee column) to specify the source of each reading, and which variables should be unstacked (in this case the voltage readings on each of the four pins, Pin17, Pin18, Pin19, and Pin20). The remaining variable (Time) is used to indicate unique observations. The resulting table will have one row per time stamp, and one column for each combination of pin and unique XBee. I use the aggregation function mean so that if there are any readings from the same XBee and Pin that have duplicate time stamps, they are averaged together (instead of added together, which is the default behavior of unstack).

data = unstack(data, {'Pin17','Pin18','Pin19', 'Pin20'}, 'XBee','AggregationFunction',@mean);
data(1:2,:)
ans = 

       Time       Pin17_XBee1    Pin17_XBee2    Pin17_XBee3    Pin17_XBee4    Pin18_XBee1    Pin18_XBee2    Pin18_XBee3    Pin18_XBee4    Pin19_XBee1    Pin19_XBee2    Pin19_XBee3    Pin19_XBee4    Pin20_XBee1    Pin20_XBee2    Pin20_XBee3    Pin20_XBee4
    __________    ___________    ___________    ___________    ___________    ___________    ___________    ___________    ___________    ___________    ___________    ___________    ___________    ___________    ___________    ___________    ___________

    7.3572e+05    0.6088         0.60528        0.7437         0.7437         0.73666        0.73783        0.75425        0.73783        0.74018        0.73783        0.75777        0.74252        0.74018        0.7437         0.74604        0.74135    
    7.3572e+05    0.6088         0.60528        0.7437         0.7437         0.73666        0.73783        0.75425        0.73783        0.74018          0.739        0.75777        0.74252          0.739        0.7437         0.74604        0.74252    

Once the data is unstacked, I can extract the time and voltage readings. You can access data within a MATLAB table in three different ways.

  1. You can access an entire column using the name of that column.
  2. You can access a subset of a table using parenthesis.
  3. You can extract data from the table using curly braces.

The second two options are similar to MATLAB cell arrays. If I use parenthesis when accessing data in a table, the result is another table that contains a subset of the data. However, because I want a matrix of double values, I use curly braces to extract the voltage data from the table and return a matrix of doubles.

ts = data.Time;
volts = data{:,2:end};

Then I can convert from voltages to degrees Fahrenheit

tempC = (volts*100-50);
tempF = tempC*9/5+32;

And then I deleted the two pins that were not connected to temperature sensors.

tempF = tempF(:,3:end);

So that I could repeat these steps later, I created a function called XBeeReadLog and added a few extra checks to the code to make it little more robust and to sort the output alphabetically based on the location of each of the sensors.

Plotting Data

Once I had the temperature matrix (tempF) and a list of time stamps (ts), plotting the data was easy.

plot(ts,tempF)
xlabel('Time')
ylabel('Temperature (\circF)')
title('One Hour of Test Temperature Data')

Figure 5: One hour of test data

I added a few features and created the function plotTemps.

Combining XBeeReadLog and plotTemps, I can now quickly and easily read and plot the data.

[tempF, ts, location, lineSpecs] =  XBeeReadLog('templog.txt',60);
plotTemps(ts,tempF,location,lineSpecs)

Figure 6: One hour of test data plotted with plotTemps

Next time

That looks like a good start. However, I noticed that in most cases two sensors that were touching one another were reading temperatures a few degrees apart, so it looked like I need to do some calibration to get more accurate results. In my next post I will discuss calibrating the temperature sensors.