Zak McKracken's blog
Home automation, retro stuff and .NET

Weather station with .NET - Part 1

I’ve always been a fan of those little ARM-boards that can be programmed with C#.

So far, I was playing around with a Netduino-board in my office. I use it for automatic testing my software where I need to simulate user action like pressing a (physical) button on a device every minute. This is a very simple task which does not use the potential of this board.

So, for my house automation I use a function, which I call “shield”: The shield function will lower or raise the blinds in certain rooms if temperature rises to high. I use this in the rooms to the south and to the west. It works noticeable and is cheaper than a climate control. This is very interesting to see and guests react sometimes strange, when the house does this on its own Smiley.

To improve this function, I need some extra sensors. A good job for a FEZ Panda II board with an Ethernet shield! I use this board, because I ordered it for a project I didn’t realize some time ago.

I want to know the brightness outside, because if the temperature in the room is high, but there is no sun, lowering the blinds will not help Zwinkerndes Smiley.

Additionally I want to know the temperature on the south side of the house and (probably) expand it with a wind meter or a rain gauge in the future.

So I ordered two sensors:

- Adafruit TSl2561 lux sensor

- GHI Barometer module from .NET Gadgeteer platform

Both sensors use the I2C bus. The barometer module from GHI is intended for use with the .NET Gadgeteer platform and uses an “I”-type socket. This is actually a standardized header, but technically it’s also an I2C bus. So I cut the cable and rewired it to the FEZ Panda shield. This works without any problems so far, but of course you can’t use the GHI libraries for the sensor. You need to build your own.

I will provide the source code, when the project works (for now, the lux meter does not work).


Step 0: Connect all things together

I first connect the shield to the base FEZ Panda board (of course). For power input, I use the Vin pin which is also available on the shield.

The barometer module needs 3.3V, so do not connect it to 5V! Now I connect the SCL and SDA lines, which are available at Pin GPIO_Pin3 (SCL) and GPIO_Pin2 (SDA). In addition, one more pin needs to be connected and set to high in order to read the values from the module. I use GPIO_Pin5 for this.

The communication over the I2C bus can be completely done over the Microsoft.SPOT.Hardware.I2CDevice class.


Step 1: Build up some communication for the web server

The Class Webserver is based on GHI’s Webserver class that will work with the Wiznet W5100 Ethernet libraries. I cut a lot of things out that I do not need (file access on SD card, file upload functions).

Basically, the class sets up a web server and listens. If there’s a request, data is collected (only then, why should I collect the data, when nobody wants it and I do not want to store it on the device). Of course, you could change that behavior and poll data in a defined interval and store it on a SD card, if you want.

I need three different answers from the web server:

1. Answer for http://{FEZ_IP_ADDRESS}: This will bring up a HTML page with the measured data

2. Answer for http://{FEZ_IP_ADDRESS}/data.xml: This is used by my server software. Data is presented on a XML page that can be used without big parsing.

3. Answer for all other calls: There’s a small 404 page (just for fun)

The class looks like this:

using Microsoft.SPOT;
using GHIElectronics.NETMF.Net;
using System.Text;
using System.Threading;
using GHIElectronics.NETMF.FEZ;
using System;

namespace FezWeather
{
    public static class MyHttpServer
    {
        public static void StartServer()
        {
            Debug.Print("Starting HttpServer...");
            Thread httpThread = new Thread((new PrefixKeeper("http")).RunServerDelegate);
            httpThread.Start();
        }

        /// <summary>
        /// All this class does is keeps the prefix and provides 
        /// RunServerDelegate to run in separate thread.
        /// RunServerDelegate calls HttpServerApp.RunServer with saved prefix.
        /// </summary>
        class PrefixKeeper
        {
            /// <summary>
            /// Keeps the prefix to start server.
            /// </summary>
            private string m_prefix;

            /// <summary>
            /// Saves the prefix
            /// </summary>
            /// <param name="prefix">Prefix</param>
            internal PrefixKeeper(string prefix)
            {
                m_prefix = prefix;
            }

            /// <summary>
            /// Delegate to run server in separate thread.
            /// </summary>
            internal void RunServerDelegate()
            {
                MyHttpServer.RunServer(m_prefix);
            }
        }

        internal static void RunServer(string prefix)
        {
            HttpListener listener = new HttpListener(prefix, -1);

            listener.Start();
            Debug.Print("---o0o--- HttpListener is up and running ---o0o---");

            while (true)
            {
                HttpListenerContext context = null;

                try
                {
                    context = listener.GetContext();
                    HttpListenerRequest request = context.Request;
                    if (request.HttpMethod.ToUpper() == "GET")
                    {
                        Program.LEDAccess.StartBlinking(25, 25);

                        Program.GetAllData();
                        ProcessClientGetRequest(context);

                        Program.LEDAccess.StopBlinking();
                        Program.LEDAccess.ShutOff();
                    }
                    
                    if (context != null)
                    {
                        context.Close();
                        context = null;
                    }
                }
                catch
                {
                    if (context != null)
                    {
                        context.Close();
                        context = null;
                    }
                }
            }
        }

        private static void ProcessClientGetRequest(HttpListenerContext context)
        {
            HttpListenerResponse response = context.Response;

            Debug.Print("Query from IP: " + context.Request.RemoteEndPoint.Address.ToString() + " @ " + DateTime.Now.ToString());
            
            response.StatusCode = (int)HttpStatusCode.OK;

            string strResp = string.Empty;

            switch (context.Request.RawUrl)
            {
                case "/data.xml":
                    strResp = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n";
                    strResp += "<weather>\r\n";
                    strResp += "<temperature>" + Program.Temperature + "</temperature>\r\n";
                    strResp += "<brightness>" + Program.Brightness + "</brightness>\r\n";
                    strResp += "<pressure>" + Program.Pressure + "</pressure>\r\n</weather>\r\n";

                    response.ContentType = "application/xml";
                    break;

                case "/":
                    strResp = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\"><html><head><title>FEZWeather station</title></head>";
                    strResp += "<body style=\"background-color:#007CE2; font-family:Courier; color:White\">    <h1>FEZ current weather values</h1><h3>Temperature:" + Program.Temperature + " &deg;C</h3><h3>Pressure:" + Program.Pressure + " hPa</h3><h3>Brightness:" + Program.Brightness + " lux</h3>";
                    strResp += "<br /><br />Data in XML-format? Look here: <a href=\"data.xml\">data.xml</a></body></html>";

                    response.ContentType = "text/html";
                    break;

                default:
                    strResp = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\"><html><head><title>FEZWeather station</title></head>";
                    strResp += "<body style=\"background-color:#007CE2; font-family:Courier; color:White\">    <h1>Not found</h1>    <h3>sorry...</h3>    </body></html>";

                    response.ContentType = "text/html";
                    break;

            }

            byte[] messageBody = Encoding.UTF8.GetBytes(strResp);
            response.OutputStream.Write(messageBody, 0, messageBody.Length);
        }
    }
}

To use this in my main program, I declare some static byte[] arrays to store the network information:
  static byte[] IP = { 192, 168, 0, 219 };                    //  IP address of FEZ Panda II ethernet shield
  static byte[] GW = { 192, 168, 0, 1 };                      //  Your gateway address
  static byte[] SUB = { 255, 255, 255, 0 };                   //  Your subnet mask
  static byte[] DNS = { 192, 168, 0, 1 };                     //  Your DNS
  static byte[] MAC = { 0x00, 0x26, 0x1C, 0x7B, 0x29, 0xE8 }; //  Your MAC address

Although you could use DHCP, I decided to supply a manual IP address.. So, in Main() I call:

WIZnet_W5100.Enable(SPI.SPI_module.SPI1, (Cpu.Pin)FEZ_Pin.Digital.Di10, (Cpu.Pin)FEZ_Pin.Digital.Di9, true);
NetworkInterface.EnableStaticIP(IP, SUB, GW, MAC);
NetworkInterface.EnableStaticDns(DNS);

MyHttpServer.StartServer();

That’s all to do for the web server part. The web server will query the modules if there’s a request and supply it to the user.


Step 2: Talking to the barometer module (as this seems to be easier)

First of all, I need some objects for I2C communication. I use the built-in methods, which makes it very easy to access the bus.

Most of the code is based in the Gadgeteer sample code found here: http://gadgeteer.codeplex.com/SourceControl/changeset/view/19905#157772 

The barometer module has two addresses: One for the eeprom, where some coefficients are stored, the other is the module itself, where I can obtain the values.

So, first some declarations:

public static string Temperature = "0.0";                   //  Temperature value in °C used for XML output
public static string Brightness = "1.0";                    //  LUX value used for XML output
public static string Pressure = "2.0";                      //  Pressure value in hPa used for XML output

static I2CDevice FEZ_I2C;                                   //  I2C object
static OutputPort XCLR;                                     //  XCLR line. Needed to be set HIGH when reading temp/pressure

static I2CDevice.Configuration conDevBARO;              //  Config for barometer module
static I2CDevice.Configuration conDevBAROCfg;           //  Config for barometer module eeprom

static byte BARO_ADDRESS = 0x77;                        //  Address of barometer
static byte BARO_EEPROM = 0x50;                         //  Address of barometer eeprom
            
static byte[] _readBuffer144 = new byte[18];    //  Byte Array for storing config loaded from eeprom
static Coefficients Coeff;                      //  Coeffients for calulation (loaded from eeprom)

struct Coefficients
{
     public int C1, C2, C3, C4, C5, C6, C7;
     public int A, B, C, D;
 }

In Main() I need to read the coefficients from the eeprom first, so I open address 0x50 and create some transaction to get these values. I need them later for calculation.

Coeff = new Coefficients();

// Setup barometer sensor
XCLR = new OutputPort(Cpu.Pin.GPIO_Pin5, false);
conDevBAROCfg = new I2CDevice.Configuration(BARO_EEPROM, 400);
conDevBARO = new I2CDevice.Configuration(BARO_ADDRESS, 400);
FEZ_I2C = new I2CDevice(conDevBAROCfg);

I2CDevice.I2CTransaction[] xActionsGetBaroCfg = new I2CDevice.I2CTransaction[2];
byte[] CallRegister = new byte[1] { 0x10 };
xActionsGetBaroCfg[0] = I2CDevice.CreateWriteTransaction(CallRegister);
xActionsGetBaroCfg[1] = I2CDevice.CreateReadTransaction(_readBuffer144);

FEZ_I2C.Execute(xActionsGetBaroCfg, 1000);

Coeff.C1 = (_readBuffer144[0] << 8) + _readBuffer144[1];
Coeff.C2 = (_readBuffer144[2] << 8) + _readBuffer144[3];
Coeff.C3 = (_readBuffer144[4] << 8) + _readBuffer144[5];
Coeff.C4 = (_readBuffer144[6] << 8) + _readBuffer144[7];
Coeff.C5 = (_readBuffer144[8] << 8) + _readBuffer144[9];
Coeff.C6 = (_readBuffer144[10] << 8) + _readBuffer144[11];
Coeff.C7 = (_readBuffer144[12] << 8) + _readBuffer144[13];
Coeff.A = _readBuffer144[14];
Coeff.B = _readBuffer144[15];
Coeff.C = _readBuffer144[16];
Coeff.D = _readBuffer144[17];

conDevBAROCfg = null;
FEZ_I2C.Dispose();
 
 

Remarks: Always set the Configuration objects to null and dispose the FEZ_I2C object, otherwise you can’t talk to another I2C module!

For getting the temperature and pressure value, I need to connect to address 0x77.

static void GetDataTemp()
        {
            double dUT, OFF, SENS, X;
            double P, T;
            int D1, D2;

            byte[] _readBuffer16 = new byte[2];
            byte[] ReadByte = new byte[1] { 0xFD };

            XCLR.Write(true);

            // Get raw pressure value
            I2CDevice.I2CTransaction[] xWeatherAction = new I2CDevice.I2CTransaction[1];
            byte[] GetPressure = new byte[2] { 0xFF, 0xF0 };
            xWeatherAction[0] = I2CDevice.CreateWriteTransaction(GetPressure);
            FEZ_I2C.Execute(xWeatherAction, 1000);
            Thread.Sleep(50);

            xWeatherAction = new I2CDevice.I2CTransaction[2];
            xWeatherAction[0] = I2CDevice.CreateWriteTransaction(ReadByte);
            xWeatherAction[1] = I2CDevice.CreateReadTransaction(_readBuffer16);
            
            FEZ_I2C.Execute(xWeatherAction, 1000);

            D1 = (_readBuffer16[0] << 8) | _readBuffer16[1];

            Debug.Print("RAW D1=" + D1.ToString());

            // Get raw temperature value
            _readBuffer16 = new byte[2];
            //ReadByte = new byte[1] { 0xF0 };
            xWeatherAction = new I2CDevice.I2CTransaction[1];
            byte[] GetTemp = new byte[2] { 0xFF, 0xE8 };
            xWeatherAction[0] = I2CDevice.CreateWriteTransaction(GetTemp);
            FEZ_I2C.Execute(xWeatherAction, 1000);
            Thread.Sleep(50);

            xWeatherAction = new I2CDevice.I2CTransaction[2];
            xWeatherAction[0] = I2CDevice.CreateWriteTransaction(ReadByte);
            xWeatherAction[1] = I2CDevice.CreateReadTransaction(_readBuffer16);

            FEZ_I2C.Execute(xWeatherAction, 1000);

            D2 = (_readBuffer16[0] << 8) | _readBuffer16[1];
            Debug.Print("RAW D2=" + D2.ToString());
            XCLR.Write(false);


            //////////////////////////////////////////////////////////////
            // Calculate temperature and pressure based on calibration data
            ////////////////////////////////////////////////////////////////

            // Step 1. Get temperature value.

            // D2 >= C5 dUT= D2-C5 - ((D2-C5)/2^7) * ((D2-C5)/2^7) * A / 2^C
            if (D2 >= Coeff.C5)
            {
                dUT = D2 - Coeff.C5 - ((D2 - Coeff.C5) / System.Math.Pow(2, 7) * ((D2 - Coeff.C5) / System.Math.Pow(2, 7)) * Coeff.A / System.Math.Pow(2, Coeff.C));
            }
            // D2 <  C5 dUT= D2-C5 - ((D2-C5)/2^7) * ((D2-C5)/2^7) * B / 2^C
            else
            {
                dUT = D2 - Coeff.C5 - ((D2 - Coeff.C5) / System.Math.Pow(2, 7) * ((D2 - Coeff.C5) / System.Math.Pow(2, 7)) * Coeff.B / System.Math.Pow(2, Coeff.C));
            }

            // Step 2. Calculate offset, sensitivity and final pressure value.

            // OFF=(C2+(C4-1024)*dUT/2^14)*4
            OFF = (Coeff.C2 + (Coeff.C4 - 1024) * dUT / System.Math.Pow(2, 14)) * 4;
            // SENS = C1+ C3*dUT/2^10
            SENS = Coeff.C1 + Coeff.C3 * dUT / System.Math.Pow(2, 10);
            // X= SENS * (D1-7168)/2^14 - OFF
            X = SENS * (D1 - 7168) / System.Math.Pow(2, 14) - OFF;
            // P=X*10/2^5+C7
            P = X * 10 / System.Math.Pow(2, 5) + Coeff.C7;

            // Step 3. Calculate temperature

            // T = 250 + dUT * C6 / 2 ^ 16-dUT/2^D
            T = 250 + dUT * Coeff.C6 / System.Math.Pow(2, 16) - dUT / System.Math.Pow(2, Coeff.D);

            double Temp = T / 10;
            int Press = (int)P / 10;
            
            Debug.Print(Temp.ToString("F1") + " °C");
            Debug.Print(Press.ToString() + " hPa");

            Temperature = Temp.ToString("F1");
            Pressure = Press.ToString();
        }
 
This stores the measured values in the static strings declared above. The method is called by the web server to get the current values.


Star Trek inspired home automation visualisation

I’ve always been a more or less active fan of Star Trek. During the construction phase of my house I started coding a GUI for controlling the house which has an EIB. Just for fun I designed a version inspired by the LCARS design used in Star Trek TNG and showed this to my wife. I showed her several designs before but this was the only one, she really liked. So I decided to go on with this.

I started a C# WinForms application. The software runs on a wall mounted Shuttle Barebone-PC. First plan was an industrial panel-pc but the processor was too slow. The now-used Atom is ok.

I started with the LCARS-controls found on Codeproject.

Since the classic LCARS design divides the screen into two parts this tended to be impracticable, so I used my own design

For now the software is able to:

  • Switch lights/wall outlets
  • Show current temperatures for all room controllers
  • Show outside temperature with a 24h trend chart
  • Show the status of the two heat pumps
  • Provide an alarm clock (e.g. for cooking)
  • Play internet radio streams
  • Control absence
  • Mute the door bell
  • Speak status messages via speech synthesis

For now, I’m working on an integration of my electric meter. The main heat pump and the electric meter are connected to my LAN. I also tried some speech recognition, but I’ve problems with the microphone. I't’s working when you are right in front of the PC, but not far away, let’s say on the other side of the room.

So this is the main view. The table displays raw values which are sent over the EIB – completely useless but looks great Zwinkerndes Smiley

1

For each floor I have a different view. Here you can see the temperatures and check the status of the lights (the buttons are blinking when a light is switched on)

2

This is the view for the heat pump:

3

 

Next step would be to integrate a control of my squeezebox server (I use different Squeezeboxes through the house as a multiroom audio solution)



Let's get started

Thank's for invitation, here I am :-)