Mit C# verschiedene Wetterstatistiken anzeigen

Für diese Projekt setzen wir voraus, dass eine C# Entwicklungsumgebung eingerichtet ist und ein grundsätzliches Verständnis der C# Programmiersprache vorhanden ist.

Falls dies nicht der Fall ist sollte hier begonnen werden. Informationen über die Tinkerforge API sind dann hier zu finden.

Ziele

Das LCD Bricklet ist mit vier Tastern ausgestattet, die dazu genutzt werden sollen um zwischen der Anzeige von verschiedenen Wetterstatistiken umzuschalten. Dazu gehören:

  • Standard Wetteranzeige,
  • 24h Min/Max/Durchschnitt,
  • 24h Graph und
  • Zeit und Datum.

Dieses Projekt baut auf dem Mit C# auf das LCD 20x4 Bricklet schreiben Projekt auf. Im folgenden werden wir Schritt-für-Schritt erklären wie die oben genannten Funktionen implementiert werden können. Wir werden diesmal nur das generelle Konzept erläutern, so dass das Programm einfach zu verstehen sein sollte.

Schritt 1: Daten auslesen

Die Struktur des Programms ist identisch zu der aus dem Mit C# auf das LCD 20x4 Bricklet schreiben Projekt.

Die Wetterdaten erhalten wir über Callback mit 1s Intervall. Dieses mal speichern wir allerdings die Messungen:

static void IlluminanceCB(BrickletAmbientLight sender, int illuminance)
{
    latestIlluminance = illuminance/10.0;
}

static void HumidityCB(BrickletHumidity sender, int humidity)
{
    latestHumidity = humidity/10.0;
}

static void AirPressureCB(BrickletBarometer sender, int airPressure)
{
    latestAirPressure = airPressure/1000.0;

    int temperature = sender.GetChipTemperature();
    latestTemperature = temperature/100.0;
}

Um die letzten Messungen zu nutzen starten wir einen 1s Timer

Timer timer = new Timer(Update, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));

und speichern die Messungen in einer Queue, so dass wir diese später analysieren können:

static private void Update(object state)
{
    // The measurements are initialized with NaN
    if(Double.IsNaN(latestIlluminance) ||
       Double.IsNaN(latestHumidity) ||
       Double.IsNaN(latestAirPressure) ||
       Double.IsNaN(latestTemperature))
    {
        return;
    }

    illuminanceQueue.Enqueue(latestIlluminance);
    if(illuminanceQueue.Count > 60*60*24) // Save max one day worth of measurements
    {
        illuminanceQueue.Dequeue();
    }

    humidityQueue.Enqueue(latestHumidity);
    if(humidityQueue.Count > 60*60*24)
    {
        humidityQueue.Dequeue();
    }

    airPressureQueue.Enqueue(latestAirPressure);
    if(airPressureQueue.Count > 60*60*24)
    {
        airPressureQueue.Dequeue();
    }

    temperatureQueue.Enqueue(latestTemperature);
    if(temperatureQueue.Count > 60*60*24)
    {
        temperatureQueue.Dequeue();
    }

    UpdateSwitch();
}

Auf den ersten Blick mag diese Programmiermethode etwas verwirrend wirken. Warum können wir nicht einfach Getter-Methoden in der Update Funktion aufrufen oder die Messungen währen eines Callbacks speichern?

Es gibt zwei Gründe warum die Nutzung dieses Ansatzes Sinn macht:

  1. Die Callbacks werden in 1s Intervallen aufgerufen aber auch nur, wenn sich der aktuelle Callback-Wert ändert. Das heißt, wenn sich der Messwert für 10s nicht ändert wird auch der Callback erst nach diesen 10s aufgerufen. Das ist vorteilhaft, da so Bandbreite gespart werden kann. Die Messwerte werden gespeichert und können so für Statistiken genutzt werden.
  2. Durch das Arbeiten mit Callbacks und einen separaten Timer mit einem 1s Intervall können auch kurze Verbindungsabbrüche oder ähnliches überbrückt werden. Zum Beispiel: Wenn die Wetterstation über WLAN gesteuert wird und diese Verbindung für 30s unterbrochen ist, wird einfach der letzte Wert benutzt der vor diesen 30s empfangen wurde. Hätten wir Getters hierfür verwendet, so hätten wir Timeouts für jede Messung behandeln müssen. Da die Standard Timeout Zeit bei 2.5s liegt und somit länger ist wie unser 1s Intervall, hätte dies unsere Messungen verzerren können.

Schritt 2: Kontrolle über Taster

Um über die LCD-Taster steuern zu können muss der ButtonPressed Callback zur Initialisierung hinzugefügt werden:

brickletLCD.ButtonPressed += PressedCB;

In diesem Callback wird gespeichert welcher Taster zuletzt gedrückt wurde und ein Zähler für den jeweiligen Taster inkrementiert, falls dieser auch als letztes zuvor gedrückt wurde. Somit können wir zwischen verschiedenen Ansichten in einem Modus (z.B. Anzeige von 24h Graph) wechseln.

private static byte buttonPressed = 0;
private static int[] buttonPressedCounter = {0, 0, 0, 0};

// [...]

static void PressedCB(BrickletLCD20x4 sender, byte button)
{
    if(button == buttonPressed)
    {
        buttonPressedCounter[button]++;
    }
    else
    {
        buttonPressed = button;
    }

    brickletLCD.ClearDisplay();
    UpdateSwitch();
}

Um sofort auf den Tastendruck zu reagieren, löschen wir das Display und rufen UpdateSwitch auf.

Die UpdateSwitch Methode wechselt zwischen den vier verschiedenen Modi.

static private void UpdateSwitch()
{
    switch(buttonPressed)
    {
        case UPDATE_TYPE_STANDARD: UpdateStandard(); break;
        case UPDATE_TYPE_GRAPH: UpdateGraph(); break;
        case UPDATE_TYPE_MIN_MAX_AVG: UpdateMinMaxAvg(); break;
        case UPDATE_TYPE_TIME: UpdateTime(); break;
    }
}

Die vier Modi sind:

  • Standard: Zeige alle 4 Messungen an.
  • Graph: Zeige einen 24h Graphen von jeder Messung an. Durch mehrmaliges drücken des Tasters kann zwischen den Messungen gewechselt werden.
  • MinMaxAvg: Zeigt Minimums-, Maximums- und den Durchschnittswert jeder Messung an. Auch hierbei ist es möglich über einen erneuten Tastendruck zwischen den Messwerten zu wechseln.
  • Time: Zeigt die aktuelle Uhrzeit und das Datum an.

Schritt 3: Anzeige der vier Modi im Display

Die Implementierung der vier Modi:

Standardmodus

Im Standardmodus zeigen wir nur die Messwerte an:

static private void UpdateStandard()
{
    string text = string.Format("Illuminanc {0,6:###.00} lx", latestIlluminance);
    brickletLCD.WriteLine(0, 0, text);

    text = string.Format("Humidity   {0,6:###.00} %", latestHumidity);
    brickletLCD.WriteLine(1, 0, text);

    text = string.Format("Air Press {0,7:####.00} mb", latestAirPressure);
    brickletLCD.WriteLine(2, 0, text);

    text = string.Format("Temperature {0,5:##.00} {1}C", latestTemperature, (char)0xDF);
    brickletLCD.WriteLine(3, 0, text);
}
Standardmodus

Graphmodus

Im Graphmodus zeigen wir einen Balkengrafik der letzten 24 Stunden an:

static private void UpdateGraph()
{
    double barSumMin;
    double barSumMax;

    switch(buttonPressedCounter[1] % 4)
    {
        case MODE_ILLUMINANCE:
            UpdateGraphWriteBars(illuminanceQueue, out barSumMin, out barSumMax);
            UpdateGraphWriteTitle("I: ", barSumMin, barSumMax, illuminanceQueue.Count);
            break;

        case MODE_HUMIDITY:
            UpdateGraphWriteBars(humidityQueue, out barSumMin, out barSumMax);
            UpdateGraphWriteTitle("H: ", barSumMin, barSumMax, humidityQueue.Count);
            break;

        case MODE_AIR_PRESSURE:
            UpdateGraphWriteBars(airPressureQueue, out barSumMin, out barSumMax);
            UpdateGraphWriteTitle("A: ", barSumMin, barSumMax, airPressureQueue.Count);
            break;

        case MODE_TEMPERATURE:
            UpdateGraphWriteBars(temperatureQueue, out barSumMin, out barSumMax);
            UpdateGraphWriteTitle("T: ", barSumMin, barSumMax, temperatureQueue.Count);
            break;
    }
}

Wir gehen hier nicht weiter ins Detail. Die Methoden, wie die Graphen berechnet werden, können zum Schluss im vollständigen Programm betrachtet werden. Für die Balkengraphen müssen allerdings ein paar Custom-Characters bei der Initialisierung konfiguriert werden:

static private void ConfigureCustomChars(BrickletLCD20x4 lcd)
{
    byte[][] c = new byte[8][];
    c[0] = new byte[8] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff};
    c[1] = new byte[8] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff};
    c[2] = new byte[8] {0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff};
    c[3] = new byte[8] {0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff};
    c[4] = new byte[8] {0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff};
    c[5] = new byte[8] {0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
    c[6] = new byte[8] {0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
    c[7] = new byte[8] {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};

    for(byte i = 0; i < c.Length; i++)
    {
        lcd.SetCustomCharacter(i, c[i]);
    }
}

Dies konfiguriert die acht verfügbaren Custom-Characters als Balken mit Höhe eins bis acht.

Graphmodus

MinMaxAvg-Modus

Im MinMaxAvg-Modus zeigen wir das Minimum, Maximum und den Durchschnittswert der letzten 24h an:

static private void UpdateMinMaxAvg()
{
    switch(buttonPressedCounter[2] % 4)
    {
        case MODE_ILLUMINANCE:
            UpdateMinMaxAvgWrite("Illuminance    " + TimeFromSeconds(illuminanceQueue.Count),
                                 "Lux",
                                 GetMinMaxAvg(illuminanceQueue));
            break;

        case MODE_HUMIDITY:
            UpdateMinMaxAvgWrite("Humidity       " + TimeFromSeconds(humidityQueue.Count),
                                 "%RH",
                                 GetMinMaxAvg(humidityQueue));
            break;

        case MODE_AIR_PRESSURE:
            UpdateMinMaxAvgWrite("Air Pressure   " + TimeFromSeconds(airPressureQueue.Count),
                                 "hPa",
                                 GetMinMaxAvg(airPressureQueue));
            break;

        case MODE_TEMPERATURE:
            UpdateMinMaxAvgWrite("Temperature    " + TimeFromSeconds(temperatureQueue.Count),
                                 ((char)0xDF) + "C",
                                 GetMinMaxAvg(temperatureQueue));
            break;
    }
}

Diese Werte werden in 4 Zeilen geschrieben, wobei die erste Zeile den Titel der jeweiligen Messungen anzeigt:

static private void UpdateMinMaxAvgWrite(string title, string unit, double[] values)
{
    String min = string.Format("Min: {0,2:##.00} {1}", values[0], unit);
    String max = string.Format("Max: {0,2:##.00} {1}", values[1], unit);
    String avg = string.Format("Avg: {0,2:##.00} {1}", values[2], unit);
    brickletLCD.WriteLine(0, 0, title);
    brickletLCD.WriteLine(1, 0, min);
    brickletLCD.WriteLine(2, 0, avg);
    brickletLCD.WriteLine(3, 0, max);
}
MinMaxAvg-Modus

Time-Modus

Im Time-Modus zeigt das Display die momentan Uhrzeit und das aktuelle Datum an:

static private void UpdateTime()
{
    string line0 = DateTime.Now.ToString("HH:MM:ss");
    string line1 = DateTime.Now.ToString("dddd");
    string line2 = DateTime.Now.ToString("D");
    brickletLCD.WriteLine((byte)0, (byte)((LINE_LENGTH-line0.Length)/2), line0);
    brickletLCD.WriteLine((byte)1, (byte)((LINE_LENGTH-line1.Length)/2), UTF16ToKS0066U(line1));
    brickletLCD.WriteLine((byte)2, (byte)((LINE_LENGTH-line2.Length)/2), UTF16ToKS0066U(line2));
}

Die UTF16ToKS0066U Methode encodiert Umlaute für den LCD Zeichensatz (ä, ö, ü, etc). Dies wird benötigt, da C# die Daten in der momentanen Betriebssystem-Sprache übergibt.

Time-Modus

Schritt 4: Alles zusammenfügen

Das war es! Nun haben wir eine Wetterstation mit vier Modi, die über die LCD 20x4 Taster gesteuert werden kann.

Zugegeben ist dies alles noch ein wenig elementar. Es gibt genügend Möglichkeiten Dinge zu verbessern. Zum Beispiel wäre es Möglich die Einheit der Messwerte mit anzuzeigen sofern der Messwert nicht zu groß ist. Oder es wäre besser die Graphenbeschriftungen anstatt oben auf der linken Seite darzustellen.

Hier der komplette Quelltext (download):

using Tinkerforge;
using System;
using System.Collections;
using System.Threading;

class WeatherStation
{
    private static string HOST = "localhost";
    private static int PORT = 4223;

    private const byte UPDATE_TYPE_STANDARD = 0;
    private const byte UPDATE_TYPE_GRAPH = 1;
    private const byte UPDATE_TYPE_MIN_MAX_AVG = 2;
    private const byte UPDATE_TYPE_TIME = 3;

    private const int LINE_LENGTH = 20;
    private const int CUSTOM_CHAR_START = 8;
    private const int CUSTOM_CHAR_END = 15;
    private const int BAR_HEIGHT = 24;

    private const int MODE_ILLUMINANCE = 0;
    private const int MODE_HUMIDITY = 1;
    private const int MODE_AIR_PRESSURE = 2;
    private const int MODE_TEMPERATURE = 3;

    private static IPConnection ipcon = null;
    private static BrickletLCD20x4 brickletLCD = null;
    private static BrickletAmbientLight brickletAmbientLight = null;
    private static BrickletAmbientLightV2 brickletAmbientLightV2 = null;
    private static BrickletAmbientLightV3 brickletAmbientLightV3 = null;
    private static BrickletHumidity brickletHumidity = null;
    private static BrickletHumidityV2 brickletHumidityV2 = null;
    private static BrickletBarometer brickletBarometer = null;
    private static BrickletBarometerV2 brickletBarometerV2 = null;

    private static double latestIlluminance = Double.NaN;
    private static double latestHumidity = Double.NaN;
    private static double latestAirPressure = Double.NaN;
    private static double latestTemperature = Double.NaN;

    private static Queue illuminanceQueue = new Queue();
    private static Queue humidityQueue = new Queue();
    private static Queue airPressureQueue = new Queue();
    private static Queue temperatureQueue = new Queue();

    private static byte buttonPressed = 0;
    private static int[] buttonPressedCounter = {0, 0, 0, 0};

    private static Timer timer;

    static void IlluminanceCB(BrickletAmbientLight sender, int illuminance)
    {
        latestIlluminance = illuminance/10.0;
    }

    static void IlluminanceV2CB(BrickletAmbientLightV2 sender, long illuminance)
    {
        latestIlluminance = illuminance/100.0;
    }

    static void IlluminanceV3CB(BrickletAmbientLightV3 sender, long illuminance)
    {
        latestIlluminance = illuminance/100.0;
    }

    static void HumidityCB(BrickletHumidity sender, int humidity)
    {
        latestHumidity = humidity/10.0;
    }

    static void HumidityV2CB(BrickletHumidityV2 sender, int humidity)
    {
        latestHumidity = humidity/100.0;
    }

    static void AirPressureCB(BrickletBarometer sender, int airPressure)
    {
        latestAirPressure = airPressure/1000.0;

        int temperature = sender.GetChipTemperature();
        latestTemperature = temperature/100.0;
    }

    static void AirPressureV2CB(BrickletBarometerV2 sender, int airPressure)
    {
        latestAirPressure = airPressure/1000.0;

        int temperature = sender.GetTemperature();
        latestTemperature = temperature/100.0;
    }

    static void PressedCB(BrickletLCD20x4 sender, byte button)
    {
        if(button == buttonPressed)
        {
            buttonPressedCounter[button]++;
        }
        else
        {
            buttonPressed = button;
        }

        brickletLCD.ClearDisplay();
        UpdateSwitch();
    }

    static void EnumerateCB(object sender, string UID, string connectedUID, char position,
                            short[] hardwareVersion, short[] firmwareVersion,
                            int deviceIdentifier, short enumerationType)
    {
        if(enumerationType == IPConnection.ENUMERATION_TYPE_CONNECTED ||
           enumerationType == IPConnection.ENUMERATION_TYPE_AVAILABLE)
        {
            if(deviceIdentifier == BrickletLCD20x4.DEVICE_IDENTIFIER)
            {
                try
                {
                    brickletLCD = new BrickletLCD20x4(UID, ipcon);
                    brickletLCD.ClearDisplay();
                    brickletLCD.BacklightOn();
                    ConfigureCustomChars(brickletLCD);
                    brickletLCD.ButtonPressed += PressedCB;
                    System.Console.WriteLine("LCD 20x4 initialized");
                }
                catch(TinkerforgeException e)
                {
                    System.Console.WriteLine("LCD 20x4 init failed: " + e.Message);
                    brickletLCD = null;
                }
            }
            else if(deviceIdentifier == BrickletAmbientLight.DEVICE_IDENTIFIER)
            {
                try
                {
                    brickletAmbientLight = new BrickletAmbientLight(UID, ipcon);
                    brickletAmbientLight.SetIlluminanceCallbackPeriod(1000);
                    brickletAmbientLight.Illuminance += IlluminanceCB;
                    System.Console.WriteLine("Ambient Light initialized");
                }
                catch(TinkerforgeException e)
                {
                    System.Console.WriteLine("Ambient Light init failed: " + e.Message);
                    brickletAmbientLight = null;
                }
            }
            else if(deviceIdentifier == BrickletAmbientLightV2.DEVICE_IDENTIFIER)
            {
                try
                {
                    brickletAmbientLightV2 = new BrickletAmbientLightV2(UID, ipcon);
                    brickletAmbientLightV2.SetConfiguration(BrickletAmbientLightV2.ILLUMINANCE_RANGE_64000LUX,
                                                            BrickletAmbientLightV2.INTEGRATION_TIME_200MS);
                    brickletAmbientLightV2.SetIlluminanceCallbackPeriod(1000);
                    brickletAmbientLightV2.Illuminance += IlluminanceV2CB;
                    System.Console.WriteLine("Ambient Light 2.0 initialized");
                }
                catch(TinkerforgeException e)
                {
                    System.Console.WriteLine("Ambient Light 2.0 init failed: " + e.Message);
                    brickletAmbientLightV2 = null;
                }
            }
            else if(deviceIdentifier == BrickletAmbientLightV3.DEVICE_IDENTIFIER)
            {
                try
                {
                    brickletAmbientLightV3 = new BrickletAmbientLightV3(UID, ipcon);
                    brickletAmbientLightV3.SetConfiguration(BrickletAmbientLightV3.ILLUMINANCE_RANGE_64000LUX,
                                                            BrickletAmbientLightV3.INTEGRATION_TIME_200MS);
                    brickletAmbientLightV3.SetIlluminanceCallbackConfiguration(1000, false, 'x', 0, 0);
                    brickletAmbientLightV3.IlluminanceCallback += IlluminanceV3CB;
                    System.Console.WriteLine("Ambient Light 3.0 initialized");
                }
                catch(TinkerforgeException e)
                {
                    System.Console.WriteLine("Ambient Light 3.0 init failed: " + e.Message);
                    brickletAmbientLightV3 = null;
                }
            }
            else if(deviceIdentifier == BrickletHumidity.DEVICE_IDENTIFIER)
            {
                try
                {
                    brickletHumidity = new BrickletHumidity(UID, ipcon);
                    brickletHumidity.SetHumidityCallbackPeriod(1000);
                    brickletHumidity.Humidity += HumidityCB;
                    System.Console.WriteLine("Humidity initialized");
                }
                catch(TinkerforgeException e)
                {
                    System.Console.WriteLine("Humidity init failed: " + e.Message);
                    brickletHumidity = null;
                }
            }
            else if(deviceIdentifier == BrickletHumidityV2.DEVICE_IDENTIFIER)
            {
                try
                {
                    brickletHumidityV2 = new BrickletHumidityV2(UID, ipcon);
                    brickletHumidityV2.SetHumidityCallbackConfiguration(1000, true, 'x', 0, 0);
                    brickletHumidityV2.HumidityCallback += HumidityV2CB;
                    System.Console.WriteLine("Humidity 2.0 initialized");
                }
                catch(TinkerforgeException e)
                {
                    System.Console.WriteLine("Humidity 2.0 init failed: " + e.Message);
                    brickletHumidityV2 = null;
                }
            }
            else if(deviceIdentifier == BrickletBarometer.DEVICE_IDENTIFIER)
            {
                try
                {
                    brickletBarometer = new BrickletBarometer(UID, ipcon);
                    brickletBarometer.SetAirPressureCallbackPeriod(1000);
                    brickletBarometer.AirPressure += AirPressureCB;
                    System.Console.WriteLine("Barometer initialized");
                }
                catch(TinkerforgeException e)
                {
                    System.Console.WriteLine("Barometer init failed: " + e.Message);
                    brickletBarometer = null;
                }
            }
            else if(deviceIdentifier == BrickletBarometerV2.DEVICE_IDENTIFIER)
            {
                try
                {
                    brickletBarometerV2 = new BrickletBarometerV2(UID, ipcon);
                    brickletBarometerV2.SetAirPressureCallbackConfiguration(1000, false, 'x', 0, 0);
                    brickletBarometerV2.AirPressureCallback += AirPressureV2CB;
                    System.Console.WriteLine("Barometer 2.0 initialized");
                }
                catch(TinkerforgeException e)
                {
                    System.Console.WriteLine("Barometer 2.0 init failed: " + e.Message);
                    brickletBarometer = null;
                }
            }
        }
    }

    static void ConnectedCB(object sender, short connectedReason)
    {
        if(connectedReason == IPConnection.CONNECT_REASON_AUTO_RECONNECT)
        {
            System.Console.WriteLine("Auto Reconnect");

            while(true)
            {
                try
                {
                    ipcon.Enumerate();
                    break;
                }
                catch(NotConnectedException e)
                {
                    System.Console.WriteLine("Enumeration Error: " + e.Message);
                    System.Threading.Thread.Sleep(1000);
                }
            }
        }
    }

    static private void ConfigureCustomChars(BrickletLCD20x4 lcd)
    {
        byte[][] c = new byte[8][];
        c[0] = new byte[8] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff};
        c[1] = new byte[8] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff};
        c[2] = new byte[8] {0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff};
        c[3] = new byte[8] {0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff};
        c[4] = new byte[8] {0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff};
        c[5] = new byte[8] {0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
        c[6] = new byte[8] {0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
        c[7] = new byte[8] {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};

        for(byte i = 0; i < c.Length; i++)
        {
            lcd.SetCustomCharacter(i, c[i]);
        }
    }

    static private string TimeFromSeconds(int s)
    {
        string str = "(";
        int m = s/60;
        int h = m/60;

        if(h > 0)
        {
            str += h + "h)";
        }
        else
        {
            if(m == 0)
            {
                m = 1;
            }
            str += m + "m)";
        }

        if(str.Length == 4)
        {
            return " " + str;
        }

        return str;
    }

    // From http://www.tinkerforge.com/en/doc/Software/Bricklets/LCD20x4_Bricklet_CSharp.html#unicode
    // Maps a normal UTF-16 encoded string to the LCD charset
    static string UTF16ToKS0066U(string utf16)
    {
        string ks0066u = "";
        char c;

        for (int i = 0; i < utf16.Length; i++)
        {
            int codePoint = Char.ConvertToUtf32(utf16, i);

            if (Char.IsSurrogate(utf16, i))
            {
                // Skip low surrogate
                i++;
            }

            // ASCII subset from JIS X 0201
            if (codePoint >= 0x0020 && codePoint <= 0x007e)
            {
                // The LCD charset doesn't include '\' and '~', use similar characters instead
                switch (codePoint)
                {
                    case 0x005c: c = (char)0xa4; break; // REVERSE SOLIDUS maps to IDEOGRAPHIC COMMA
                    case 0x007e: c = (char)0x2d; break; // TILDE maps to HYPHEN-MINUS
                    default: c = (char)codePoint; break;
                }
            }
            // Katakana subset from JIS X 0201
            else if (codePoint >= 0xff61 && codePoint <= 0xff9f)
            {
                c = (char)(codePoint - 0xfec0);
            }
            // Special characters
            else
            {
                switch (codePoint)
                {
                    case 0x00a5: c = (char)0x5c; break; // YEN SIGN
                    case 0x2192: c = (char)0x7e; break; // RIGHTWARDS ARROW
                    case 0x2190: c = (char)0x7f; break; // LEFTWARDS ARROW
                    case 0x00b0: c = (char)0xdf; break; // DEGREE SIGN maps to KATAKANA SEMI-VOICED SOUND MARK
                    case 0x03b1: c = (char)0xe0; break; // GREEK SMALL LETTER ALPHA
                    case 0x00c4: c = (char)0xe1; break; // LATIN CAPITAL LETTER A WITH DIAERESIS
                    case 0x00e4: c = (char)0xe1; break; // LATIN SMALL LETTER A WITH DIAERESIS
                    case 0x00df: c = (char)0xe2; break; // LATIN SMALL LETTER SHARP S
                    case 0x03b5: c = (char)0xe3; break; // GREEK SMALL LETTER EPSILON
                    case 0x00b5: c = (char)0xe4; break; // MICRO SIGN
                    case 0x03bc: c = (char)0xe4; break; // GREEK SMALL LETTER MU
                    case 0x03c2: c = (char)0xe5; break; // GREEK SMALL LETTER FINAL SIGMA
                    case 0x03c1: c = (char)0xe6; break; // GREEK SMALL LETTER RHO
                    case 0x221a: c = (char)0xe8; break; // SQUARE ROOT
                    case 0x00b9: c = (char)0xe9; break; // SUPERSCRIPT ONE maps to SUPERSCRIPT (minus) ONE
                    case 0x00a4: c = (char)0xeb; break; // CURRENCY SIGN
                    case 0x00a2: c = (char)0xec; break; // CENT SIGN
                    case 0x2c60: c = (char)0xed; break; // LATIN CAPITAL LETTER L WITH DOUBLE BAR
                    case 0x00f1: c = (char)0xee; break; // LATIN SMALL LETTER N WITH TILDE
                    case 0x00d6: c = (char)0xef; break; // LATIN CAPITAL LETTER O WITH DIAERESIS
                    case 0x00f6: c = (char)0xef; break; // LATIN SMALL LETTER O WITH DIAERESIS
                    case 0x03f4: c = (char)0xf2; break; // GREEK CAPITAL THETA SYMBOL
                    case 0x221e: c = (char)0xf3; break; // INFINITY
                    case 0x03a9: c = (char)0xf4; break; // GREEK CAPITAL LETTER OMEGA
                    case 0x00dc: c = (char)0xf5; break; // LATIN CAPITAL LETTER U WITH DIAERESIS
                    case 0x00fc: c = (char)0xf5; break; // LATIN SMALL LETTER U WITH DIAERESIS
                    case 0x03a3: c = (char)0xf6; break; // GREEK CAPITAL LETTER SIGMA
                    case 0x03c0: c = (char)0xf7; break; // GREEK SMALL LETTER PI
                    case 0x0304: c = (char)0xf8; break; // COMBINING MACRON
                    case 0x00f7: c = (char)0xfd; break; // DIVISION SIGN

                    default:
                    case 0x25a0: c = (char)0xff; break; // BLACK SQUARE
                }
            }

            // Special handling for 'x' followed by COMBINING MACRON
            if (c == (char)0xf8)
            {
                if (!ks0066u.EndsWith("x"))
                {
                    c = (char)0xff; // BLACK SQUARE
                }

                if (ks0066u.Length > 0)
                {
                    ks0066u = ks0066u.Remove(ks0066u.Length - 1, 1);
                }
            }

            ks0066u += c;
        }

        return ks0066u;
    }

    static private double[] GetMinMaxAvg(Queue q)
    {
        if(q.Count == 0)
        {
            return new double[] {0.0, 0.0, 0.0};
        }

        double min = 10000.0;
        double max = -10000.0;
        double avg = 0.0;

        foreach(double v in q)
        {
            if(v < min)
            {
                min = v;
            }

            if(v > max)
            {
                max = v;
            }

            avg += v;
        }

        avg /= q.Count;

        return new double[] {min, max, avg};
    }

    static private void UpdateStandard()
    {
        string text = string.Format("Illumina {0,8:###.00} lx", latestIlluminance);
        brickletLCD.WriteLine(0, 0, text);

        text = string.Format("Humidity   {0,6:###.00} %", latestHumidity);
        brickletLCD.WriteLine(1, 0, text);

        text = string.Format("Air Press {0,7:####.00} mb", latestAirPressure);
        brickletLCD.WriteLine(2, 0, text);

        text = string.Format("Temperature {0,5:##.00} {1}C", latestTemperature, (char)0xDF);
        brickletLCD.WriteLine(3, 0, text);
    }

    static private void UpdateGraph()
    {
        double barSumMin;
        double barSumMax;

        switch(buttonPressedCounter[1] % 4)
        {
            case MODE_ILLUMINANCE:
                UpdateGraphWriteBars(illuminanceQueue, out barSumMin, out barSumMax);
                UpdateGraphWriteTitle("I: ", barSumMin, barSumMax, illuminanceQueue.Count);
                break;

            case MODE_HUMIDITY:
                UpdateGraphWriteBars(humidityQueue, out barSumMin, out barSumMax);
                UpdateGraphWriteTitle("H: ", barSumMin, barSumMax, humidityQueue.Count);
                break;

            case MODE_AIR_PRESSURE:
                UpdateGraphWriteBars(airPressureQueue, out barSumMin, out barSumMax);
                UpdateGraphWriteTitle("A: ", barSumMin, barSumMax, airPressureQueue.Count);
                break;

            case MODE_TEMPERATURE:
                UpdateGraphWriteBars(temperatureQueue, out barSumMin, out barSumMax);
                UpdateGraphWriteTitle("T: ", barSumMin, barSumMax, temperatureQueue.Count);
                break;
        }
    }

    static private void UpdateGraphWriteTitle(string s, double barSumMin, double barSumMax, int count)
    {
        string line0 = s + (int)barSumMin + " - " + (int)(barSumMax+1.0);
        string time = TimeFromSeconds(count);

        int numSpaces = LINE_LENGTH - line0.Length - time.Length;
        for(int i = 0; i < numSpaces; i++)
        {
            line0 += " ";
        }

        brickletLCD.WriteLine(0, 0, line0 + time);
    }

    static private void UpdateGraphWriteBars(Queue q, out double barSumMin, out double barSumMax)
    {
        barSumMin = 10000.0;
        barSumMax = -10000.0;

        int count = q.Count;

        int countBars = count/LINE_LENGTH;
        if(countBars == 0)
        {
            countBars = 1;
        }

        double[] barSum = {0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,
                           0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0};
        int i = 0;
        foreach(double v in q)
        {
            barSum[i/countBars] += v;
            i++;
            if(i == LINE_LENGTH*countBars)
            {
                break;
            }
        }

        for(i = 0; i < barSum.Length; i++)
        {
            barSum[i] /= countBars;
            if(barSum[i] < barSumMin)
            {
                barSumMin = barSum[i];
            }
            if(barSum[i] > barSumMax)
            {
                barSumMax = barSum[i];
            }
        }

        double scale = (BAR_HEIGHT-1)/(barSumMax - barSumMin);
        double offset = barSumMin * scale - 1;

        int[] barHeight = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
        for(i = 0; i < barSum.Length; i++)
        {
            barHeight[i] = (int)Math.Round(barSum[i] * scale - offset, 0);
        }

        char[][] lines = new char[4][];
        lines[1] = new char[LINE_LENGTH] {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
                                          ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '};
        lines[2] = new char[LINE_LENGTH] {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
                                          ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '};
        lines[3] = new char[LINE_LENGTH] {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
                                          ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '};

        for(i = 0; i < barHeight.Length; i++)
        {
            int barLength = barHeight[i]/8;
            int j = 0;
            for(j = 0; j < barLength; j++)
            {
                lines[j+1][i] = (char)CUSTOM_CHAR_END;
            }
            if(j < 3)
            {
                lines[j+1][i] = (char)((barHeight[i]-1)/3 + CUSTOM_CHAR_START);
            }
        }

        for(byte line = 1; line < 4; line++)
        {
            brickletLCD.WriteLine((byte)(4-line), 0, new string(lines[line]));
        }
    }

    static private void UpdateMinMaxAvg()
    {
        switch(buttonPressedCounter[2] % 4)
        {
            case MODE_ILLUMINANCE:
                UpdateMinMaxAvgWrite("Illuminance    " + TimeFromSeconds(illuminanceQueue.Count),
                                     "lx",
                                     GetMinMaxAvg(illuminanceQueue));
                break;

            case MODE_HUMIDITY:
                UpdateMinMaxAvgWrite("Humidity       " + TimeFromSeconds(humidityQueue.Count),
                                     "%RH",
                                     GetMinMaxAvg(humidityQueue));
                break;

            case MODE_AIR_PRESSURE:
                UpdateMinMaxAvgWrite("Air Pressure   " + TimeFromSeconds(airPressureQueue.Count),
                                     "hPa",
                                     GetMinMaxAvg(airPressureQueue));
                break;

            case MODE_TEMPERATURE:
                UpdateMinMaxAvgWrite("Temperature    " + TimeFromSeconds(temperatureQueue.Count),
                                     ((char)0xDF) + "C",
                                     GetMinMaxAvg(temperatureQueue));
                break;
        }
    }

    static private void UpdateMinMaxAvgWrite(string title, string unit, double[] values)
    {
        String min = string.Format("Min: {0,2:##.00} {1}", values[0], unit);
        String max = string.Format("Max: {0,2:##.00} {1}", values[1], unit);
        String avg = string.Format("Avg: {0,2:##.00} {1}", values[2], unit);
        brickletLCD.WriteLine(0, 0, title);
        brickletLCD.WriteLine(1, 0, min);
        brickletLCD.WriteLine(2, 0, avg);
        brickletLCD.WriteLine(3, 0, max);
    }

    static private void UpdateTime()
    {
        string line0 = DateTime.Now.ToString("HH:mm:ss");
        string line1 = DateTime.Now.ToString("dddd");
        string line2 = DateTime.Now.ToString("D");
        brickletLCD.WriteLine((byte)0, (byte)((LINE_LENGTH-line0.Length)/2), line0);
        brickletLCD.WriteLine((byte)1, (byte)((LINE_LENGTH-line1.Length)/2), UTF16ToKS0066U(line1));
        brickletLCD.WriteLine((byte)2, (byte)((LINE_LENGTH-line2.Length)/2), UTF16ToKS0066U(line2));
    }

    static private void UpdateSwitch()
    {
        switch(buttonPressed)
        {
            case UPDATE_TYPE_STANDARD: UpdateStandard(); break;
            case UPDATE_TYPE_GRAPH: UpdateGraph(); break;
            case UPDATE_TYPE_MIN_MAX_AVG: UpdateMinMaxAvg(); break;
            case UPDATE_TYPE_TIME: UpdateTime(); break;
        }
    }

    static private void Update(object state)
    {
        if(Double.IsNaN(latestIlluminance) ||
           Double.IsNaN(latestHumidity) ||
           Double.IsNaN(latestAirPressure) ||
           Double.IsNaN(latestTemperature))
        {
            return;
        }

        illuminanceQueue.Enqueue(latestIlluminance);
        if(illuminanceQueue.Count > 60*60*24)
        {
            illuminanceQueue.Dequeue();
        }

        humidityQueue.Enqueue(latestHumidity);
        if(humidityQueue.Count > 60*60*24)
        {
            humidityQueue.Dequeue();
        }

        airPressureQueue.Enqueue(latestAirPressure);
        if(airPressureQueue.Count > 60*60*24)
        {
            airPressureQueue.Dequeue();
        }

        temperatureQueue.Enqueue(latestTemperature);
        if(temperatureQueue.Count > 60*60*24)
        {
            temperatureQueue.Dequeue();
        }

        UpdateSwitch();
    }

    static void Main()
    {
        ipcon = new IPConnection();
        while(true)
        {
            try
            {
                ipcon.Connect(HOST, PORT);
                break;
            }
            catch(System.Net.Sockets.SocketException e)
            {
                System.Console.WriteLine("Connection Error: " + e.Message);
                System.Threading.Thread.Sleep(1000);
            }
        }

        ipcon.EnumerateCallback += EnumerateCB;
        ipcon.Connected += ConnectedCB;

        while(true)
        {
            try
            {
                ipcon.Enumerate();
                break;
            }
            catch(NotConnectedException e)
            {
                System.Console.WriteLine("Enumeration Error: " + e.Message);
                System.Threading.Thread.Sleep(1000);
            }
        }

        timer = new Timer(Update, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));

        System.Console.WriteLine("Press enter to exit");
        System.Console.ReadLine();
        ipcon.Disconnect();
    }
}